mirror of https://github.com/Budibase/budibase.git
95 changed files with 1934 additions and 1429 deletions
@ -0,0 +1,52 @@ |
|||
<html> |
|||
<head> |
|||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"> |
|||
<style> |
|||
body, html { |
|||
height: 100% !important; |
|||
font-family: Inter, sans-serif !important; |
|||
margin: 0 !important; |
|||
} |
|||
*, *:before, *:after { |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
|||
<script src='/assets/budibase-client.js'></script> |
|||
<script> |
|||
function receiveMessage(event) { |
|||
if (!event.data) { |
|||
return |
|||
} |
|||
|
|||
// Extract data from message |
|||
const { selectedComponentId, layout, screen } = JSON.parse(event.data) |
|||
|
|||
// Set some flags so the app knows we're in the builder |
|||
window["##BUDIBASE_IN_BUILDER##"] = true |
|||
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout |
|||
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen |
|||
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId |
|||
window["##BUDIBASE_PREVIEW_ID##"] = Math.random() |
|||
|
|||
// Initialise app |
|||
if (window.loadBudibase) { |
|||
loadBudibase() |
|||
} |
|||
} |
|||
|
|||
// Ignore clicks |
|||
["click", "mousedown"].forEach(type => { |
|||
document.addEventListener(type, function(e) { |
|||
e.preventDefault() |
|||
e.stopPropagation() |
|||
return false |
|||
}, true) |
|||
}) |
|||
|
|||
window.addEventListener("message", receiveMessage) |
|||
window.dispatchEvent(new Event("bb-ready")) |
|||
</script> |
|||
</head> |
|||
<body> |
|||
</body> |
|||
</html> |
|||
@ -1,55 +1 @@ |
|||
export default `<html>
|
|||
<head> |
|||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"> |
|||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono"> |
|||
<style> |
|||
body, html { |
|||
height: 100% !important; |
|||
font-family: Inter !important; |
|||
margin: 0px !important; |
|||
} |
|||
*, *:before, *:after { |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
|||
<script src='/assets/budibase-client.js'></script> |
|||
<script> |
|||
function receiveMessage(event) { |
|||
if (!event.data) { |
|||
return |
|||
} |
|||
|
|||
// Extract data from message
|
|||
const { selectedComponentId, page, screen } = JSON.parse(event.data) |
|||
|
|||
// Set some flags so the app knows we're in the builder
|
|||
window["##BUDIBASE_IN_BUILDER##"] = true |
|||
window["##BUDIBASE_PREVIEW_PAGE##"] = page |
|||
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen |
|||
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId |
|||
window["##BUDIBASE_PREVIEW_ID##"] = Math.random() |
|||
|
|||
// Initialise app
|
|||
if (window.loadBudibase) { |
|||
loadBudibase() |
|||
} |
|||
} |
|||
|
|||
let selectedComponentStyle |
|||
|
|||
// Ignore clicks
|
|||
["click", "mousedown"].forEach(type => { |
|||
document.addEventListener(type, function(e) { |
|||
e.preventDefault() |
|||
e.stopPropagation() |
|||
return false |
|||
}, true) |
|||
}) |
|||
|
|||
window.addEventListener("message", receiveMessage) |
|||
window.dispatchEvent(new Event("bb-ready")) |
|||
</script> |
|||
</head> |
|||
<body> |
|||
</body> |
|||
</html>` |
|||
export { default } from "./iframeTemplate.html" |
|||
|
|||
@ -0,0 +1,77 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import { store } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|||
import { DropdownMenu, Modal, ModalContent, Input } from "@budibase/bbui" |
|||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" |
|||
import { cloneDeep } from "lodash/fp" |
|||
|
|||
export let layout |
|||
|
|||
let confirmDeleteDialog |
|||
let editLayoutNameModal |
|||
let dropdown |
|||
let anchor |
|||
let name = layout.name |
|||
|
|||
const deleteLayout = async () => { |
|||
try { |
|||
await store.actions.layouts.delete(layout) |
|||
notifier.success(`Layout ${layout.name} deleted successfully.`) |
|||
} catch (err) { |
|||
notifier.danger(`Error deleting layout: ${err.message}`) |
|||
} |
|||
} |
|||
|
|||
const saveLayout = async () => { |
|||
try { |
|||
const layoutToSave = cloneDeep(layout) |
|||
layoutToSave.name = name |
|||
await store.actions.layouts.save(layoutToSave) |
|||
notifier.success(`Layout saved successfully.`) |
|||
} catch (err) { |
|||
notifier.danger(`Error saving layout: ${err.message}`) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor} on:click|stopPropagation> |
|||
<div class="icon" on:click={() => dropdown.show()}> |
|||
<i class="ri-more-line" /> |
|||
</div> |
|||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|||
<DropdownContainer> |
|||
<DropdownItem |
|||
icon="ri-pencil-line" |
|||
title="Edit" |
|||
on:click={() => editLayoutNameModal.show()} /> |
|||
<DropdownItem |
|||
icon="ri-delete-bin-line" |
|||
title="Delete" |
|||
on:click={() => confirmDeleteDialog.show()} /> |
|||
</DropdownContainer> |
|||
</DropdownMenu> |
|||
</div> |
|||
<ConfirmDialog |
|||
bind:this={confirmDeleteDialog} |
|||
title="Confirm Deletion" |
|||
body={'Are you sure you wish to delete this layout?'} |
|||
okText="Delete Layout" |
|||
onOk={deleteLayout} /> |
|||
|
|||
<Modal bind:this={editLayoutNameModal}> |
|||
<ModalContent |
|||
title="Edit Layout Name" |
|||
confirmText="Save" |
|||
onConfirm={saveLayout} |
|||
disabled={!name}> |
|||
<Input thin type="text" label="Name" bind:value={name} /> |
|||
</ModalContent> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.icon i { |
|||
font-size: 16px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,41 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import { FrontendTypes } from "constants" |
|||
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte" |
|||
import LayoutDropdownMenu from "./ComponentNavigationTree/LayoutDropdownMenu.svelte" |
|||
import initDragDropStore from "./ComponentNavigationTree/dragDropStore" |
|||
import NavItem from "components/common/NavItem.svelte" |
|||
import { last } from "lodash/fp" |
|||
import { store, currentAsset, selectedComponent } from "builderStore" |
|||
import { writable } from "svelte/store" |
|||
|
|||
export let layout |
|||
|
|||
let confirmDeleteDialog |
|||
let componentToDelete = "" |
|||
|
|||
const dragDropStore = initDragDropStore() |
|||
|
|||
const selectLayout = () => { |
|||
store.actions.layouts.select(layout._id) |
|||
$goto(`./${layout._id}`) |
|||
} |
|||
</script> |
|||
|
|||
<NavItem |
|||
border={false} |
|||
icon="ri-layout-3-line" |
|||
text={layout.name} |
|||
withArrow |
|||
selected={$store.currentAssetId === layout._id} |
|||
opened={$store.currentAssetId === layout._id} |
|||
on:click={selectLayout}> |
|||
<LayoutDropdownMenu {layout} /> |
|||
</NavItem> |
|||
|
|||
{#if $store.currentAssetId === layout._id && layout.props?._children} |
|||
<ComponentTree |
|||
components={layout.props._children} |
|||
currentComponent={$selectedComponent} |
|||
{dragDropStore} /> |
|||
{/if} |
|||
@ -0,0 +1,13 @@ |
|||
<script> |
|||
import { store, currentAsset } from "builderStore" |
|||
import { Select } from "@budibase/bbui" |
|||
|
|||
export let value |
|||
</script> |
|||
|
|||
<Select bind:value extraThin secondary on:change> |
|||
<option value="">Choose an option</option> |
|||
{#each $store.layouts as layout} |
|||
<option value={layout._id}>{layout.name}</option> |
|||
{/each} |
|||
</Select> |
|||
@ -0,0 +1,26 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import api from "builderStore/api" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { store, backendUiStore, allScreens } from "builderStore" |
|||
import { Input, ModalContent } from "@budibase/bbui" |
|||
import analytics from "analytics" |
|||
|
|||
const CONTAINER = "@budibase/standard-components/container" |
|||
|
|||
let name = "" |
|||
|
|||
async function save() { |
|||
try { |
|||
await store.actions.layouts.save({ name }) |
|||
$goto(`./${$store.currentAssetId}`) |
|||
notifier.success(`Layout ${name} created successfully`) |
|||
} catch (err) { |
|||
notifier.danger(`Error creating layout ${name}.`) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent title="Create Layout" confirmText="Create" onConfirm={save}> |
|||
<Input thin label="Name" bind:value={name} /> |
|||
</ModalContent> |
|||
@ -1,44 +0,0 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte" |
|||
import NavItem from "components/common/NavItem.svelte" |
|||
import { last } from "lodash/fp" |
|||
import { store } from "builderStore" |
|||
import { writable } from "svelte/store" |
|||
|
|||
export let layout |
|||
|
|||
let confirmDeleteDialog |
|||
let componentToDelete = "" |
|||
|
|||
const dragDropStore = writable({}) |
|||
|
|||
const lastPartOfName = c => |
|||
c && last(c.name ? c.name.split("/") : c._component.split("/")) |
|||
|
|||
$: _layout = { |
|||
component: layout, |
|||
title: lastPartOfName(layout), |
|||
} |
|||
|
|||
const setCurrentScreenToLayout = () => { |
|||
store.actions.selectPageOrScreen("page") |
|||
$goto("./:page/page-layout") |
|||
} |
|||
</script> |
|||
|
|||
<NavItem |
|||
border={false} |
|||
icon="ri-layout-3-line" |
|||
text="Master Screen" |
|||
withArrow |
|||
selected={$store.currentComponentInfo?._id === _layout.component.props._id} |
|||
opened={$store.currentPreviewItem?.name === _layout.title} |
|||
on:click={setCurrentScreenToLayout} /> |
|||
|
|||
{#if $store.currentPreviewItem?.name === _layout.title && _layout.component.props._children} |
|||
<ComponentTree |
|||
components={_layout.component.props._children} |
|||
currentComponent={$store.currentComponentInfo} |
|||
{dragDropStore} /> |
|||
{/if} |
|||
@ -1,3 +0,0 @@ |
|||
<script> |
|||
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte" |
|||
</script> |
|||
@ -1,60 +0,0 @@ |
|||
import { isPlainObject, isArray, cloneDeep } from "lodash/fp" |
|||
import { getExactComponent } from "./searchComponents" |
|||
|
|||
export const rename = (pages, screens, oldname, newname) => { |
|||
pages = cloneDeep(pages) |
|||
screens = cloneDeep(screens) |
|||
const changedScreens = [] |
|||
|
|||
const existingWithNewName = getExactComponent(screens, newname, true) |
|||
if (existingWithNewName) |
|||
return { |
|||
components: screens, |
|||
pages, |
|||
error: "Component by that name already exists", |
|||
} |
|||
|
|||
const traverseProps = props => { |
|||
let hasEdited = false |
|||
if (props._component && props._component === oldname) { |
|||
props._component = newname |
|||
hasEdited = true |
|||
} |
|||
|
|||
for (let propName in props) { |
|||
const prop = props[propName] |
|||
if (isPlainObject(prop) && prop._component) { |
|||
hasEdited = traverseProps(prop) || hasEdited |
|||
} |
|||
if (isArray(prop)) { |
|||
for (let element of prop) { |
|||
hasEdited = traverseProps(element) || hasEdited |
|||
} |
|||
} |
|||
} |
|||
return hasEdited |
|||
} |
|||
|
|||
for (let screen of screens) { |
|||
let hasEdited = false |
|||
|
|||
if (screen.props.instanceName === oldname) { |
|||
screen.props.instanceName = newname |
|||
hasEdited = true |
|||
} |
|||
|
|||
hasEdited = traverseProps(screen.props) || hasEdited |
|||
|
|||
if (hasEdited && screen.props.instanceName !== newname) |
|||
changedScreens.push(screen.props.instanceName) |
|||
} |
|||
|
|||
for (let pageName in pages) { |
|||
const page = pages[pageName] |
|||
if (page.appBody === oldname) { |
|||
page.appBody = newname |
|||
} |
|||
} |
|||
|
|||
return { screens, pages, changedScreens } |
|||
} |
|||
@ -1,4 +1,6 @@ |
|||
<script> |
|||
import { store } from "builderStore" |
|||
import { params } from "@sveltech/routify" |
|||
store.actions.pages.select($params.page) |
|||
|
|||
store.actions.layouts.select($params.layout) |
|||
</script> |
|||
|
|||
@ -0,0 +1,63 @@ |
|||
<script> |
|||
import { params, leftover, goto } from "@sveltech/routify" |
|||
import { FrontendTypes } from "constants" |
|||
import { store, allScreens } from "builderStore" |
|||
|
|||
// Get any leftover params not caught by Routifys params store. |
|||
const componentIds = $leftover.split("/").filter(id => id !== "") |
|||
|
|||
const currentAssetId = decodeURI($params.asset) |
|||
|
|||
let assetList |
|||
let actions |
|||
|
|||
// Determine screens or layouts based on the URL |
|||
if ($params.assetType === FrontendTypes.SCREEN) { |
|||
assetList = $allScreens |
|||
actions = store.actions.screens |
|||
} else { |
|||
assetList = $store.layouts |
|||
actions = store.actions.layouts |
|||
} |
|||
|
|||
// select the screen or layout in the UI |
|||
actions.select(currentAssetId) |
|||
|
|||
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it. |
|||
if ($leftover) { |
|||
// Get the correct screen children. |
|||
const assetChildren = assetList.find( |
|||
asset => |
|||
asset._id === $params.asset || |
|||
asset._id === decodeURIComponent($params.asset) |
|||
).props._children |
|||
findComponent(componentIds, assetChildren) |
|||
} |
|||
// } |
|||
|
|||
// Find Component with ID and continue |
|||
function findComponent(ids, children) { |
|||
// Setup stuff |
|||
let componentToSelect |
|||
let currentChildren = children |
|||
|
|||
// Loop through each ID |
|||
ids.forEach(id => { |
|||
// Find ID |
|||
const component = currentChildren.find(child => child._id === id) |
|||
|
|||
// If it does not exist, ignore (use last valid route) |
|||
if (!component) return |
|||
|
|||
componentToSelect = component |
|||
|
|||
// Update childrens array to selected components children |
|||
currentChildren = componentToSelect._children |
|||
}) |
|||
|
|||
// Select Component! |
|||
if (componentToSelect) store.actions.components.select(componentToSelect) |
|||
} |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -0,0 +1,17 @@ |
|||
<script> |
|||
import { store, allScreens } from "builderStore" |
|||
import { FrontendTypes } from "constants" |
|||
import { goto, params } from "@sveltech/routify" |
|||
|
|||
// Go to first layout |
|||
if ($params.assetType === FrontendTypes.LAYOUT) { |
|||
$goto(`../${$store.layouts[0]?._id}`) |
|||
} |
|||
|
|||
// Go to first screen |
|||
if ($params.assetType === FrontendTypes.SCREEN) { |
|||
$goto(`../${$allScreens[0]?._id}`) |
|||
} |
|||
</script> |
|||
|
|||
<!-- routify:options index=false --> |
|||
@ -1,69 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { params, leftover, goto } from "@sveltech/routify" |
|||
import { store, allScreens } from "builderStore" |
|||
|
|||
// Get any leftover params not caught by Routifys params store. |
|||
const componentIds = $leftover.split("/").filter(id => id !== "") |
|||
|
|||
// It's a screen, set it to that screen |
|||
if ($params.screen !== "page-layout") { |
|||
const currentScreenName = decodeURI($params.screen) |
|||
const validScreen = |
|||
$allScreens.findIndex(screen => screen._id === currentScreenName) !== -1 |
|||
|
|||
if (!validScreen) { |
|||
// Go to main layout if URL set to invalid screen |
|||
store.actions.pages.select("main") |
|||
$goto("../../main") |
|||
} else { |
|||
// Otherwise proceed to set screen |
|||
store.actions.screens.select(currentScreenName) |
|||
|
|||
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it. |
|||
if ($leftover) { |
|||
// Get the correct screen children. |
|||
const screenChildren = $store.pages[$params.page]._screens.find( |
|||
screen => |
|||
screen._id === $params.screen || |
|||
screen._id === decodeURIComponent($params.screen) |
|||
).props._children |
|||
findComponent(componentIds, screenChildren) |
|||
} |
|||
} |
|||
} else { |
|||
// It's a page, so set the screentype to page. |
|||
store.actions.selectPageOrScreen("page") |
|||
|
|||
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it. |
|||
if ($leftover) { |
|||
findComponent(componentIds, $store.pages[$params.page].props._children) |
|||
} |
|||
} |
|||
|
|||
// Find Component with ID and continue |
|||
function findComponent(ids, children) { |
|||
// Setup stuff |
|||
let componentToSelect |
|||
let currentChildren = children |
|||
|
|||
// Loop through each ID |
|||
ids.forEach(id => { |
|||
// Find ID |
|||
const component = currentChildren.find(child => child._id === id) |
|||
|
|||
// If it does not exist, ignore (use last valid route) |
|||
if (!component) return |
|||
|
|||
componentToSelect = component |
|||
|
|||
// Update childrens array to selected components children |
|||
currentChildren = componentToSelect._children |
|||
}) |
|||
|
|||
// Select Component! |
|||
if (componentToSelect) store.actions.components.select(componentToSelect) |
|||
} |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -1,8 +0,0 @@ |
|||
<script> |
|||
import { params } from "@sveltech/routify" |
|||
import { store } from "builderStore" |
|||
|
|||
store.actions.pages.select($params.page) |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -1,4 +0,0 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
$goto("../page-layout") |
|||
</script> |
|||
@ -1,6 +1,8 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
$goto("../main") |
|||
import { FrontendTypes } from "constants" |
|||
|
|||
$goto(`../${FrontendTypes.SCREEN}`) |
|||
</script> |
|||
|
|||
<!-- routify:options index=false --> |
|||
<!-- routify:options index=1 --> |
|||
|
|||
@ -1,51 +0,0 @@ |
|||
import { |
|||
generate_css, |
|||
generate_screen_css, |
|||
} from "../src/builderStore/generate_css.js" |
|||
|
|||
describe("generate_css", () => { |
|||
|
|||
|
|||
test("Check how array styles are output", () => { |
|||
expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;") |
|||
}) |
|||
|
|||
test("Check handling of an array with empty string values", () => { |
|||
expect(generate_css({ padding: ["", "", "", ""] })).toBe("") |
|||
}) |
|||
|
|||
test("Check handling of an empty array", () => { |
|||
expect(generate_css({ margin: [] })).toBe("") |
|||
}) |
|||
|
|||
test("Check handling of valid font property", () => { |
|||
expect(generate_css({ "font-size": "10px" })).toBe("font-size: 10px;") |
|||
}) |
|||
}) |
|||
|
|||
|
|||
describe("generate_screen_css", () => { |
|||
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } } |
|||
|
|||
test("Test generation of normal css styles", () => { |
|||
expect(generate_screen_css([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } } |
|||
|
|||
test("Test generation of hover css styles", () => { |
|||
expect(generate_screen_css([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } } |
|||
|
|||
test("Test generation of selection css styles", () => { |
|||
expect(generate_screen_css([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } } |
|||
|
|||
test.only("Testing handling of empty component styles", () => { |
|||
expect(generate_screen_css([emptyComponent])).toBe("") |
|||
}) |
|||
}) |
|||
@ -0,0 +1,43 @@ |
|||
const { EMPTY_LAYOUT } = require("../../constants/layouts") |
|||
const CouchDB = require("../../db") |
|||
const { generateLayoutID, getScreenParams } = require("../../db/utils") |
|||
|
|||
exports.save = async function(ctx) { |
|||
const db = new CouchDB(ctx.user.appId) |
|||
let layout = ctx.request.body |
|||
|
|||
if (!layout.props) { |
|||
layout = { |
|||
...layout, |
|||
...EMPTY_LAYOUT, |
|||
} |
|||
} |
|||
|
|||
layout._id = layout._id || generateLayoutID() |
|||
const response = await db.put(layout) |
|||
layout._rev = response.rev |
|||
|
|||
ctx.body = layout |
|||
ctx.status = 200 |
|||
} |
|||
|
|||
exports.destroy = async function(ctx) { |
|||
const db = new CouchDB(ctx.user.appId) |
|||
const layoutId = ctx.params.layoutId, |
|||
layoutRev = ctx.params.layoutRev |
|||
|
|||
const layoutsUsedByScreens = ( |
|||
await db.allDocs( |
|||
getScreenParams(null, { |
|||
include_docs: true, |
|||
}) |
|||
) |
|||
).rows.map(element => element.doc.layoutId) |
|||
if (layoutsUsedByScreens.includes(layoutId)) { |
|||
ctx.throw(400, "Cannot delete a base layout") |
|||
} |
|||
|
|||
await db.remove(layoutId, layoutRev) |
|||
ctx.message = "Layout deleted successfully" |
|||
ctx.status = 200 |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
const CouchDB = require("../../db/client") |
|||
const { generatePageID } = require("../../db/utils") |
|||
const compileStaticAssetsForPage = require("../../utilities/builder/compileStaticAssetsForPage") |
|||
|
|||
exports.save = async function(ctx) { |
|||
const db = new CouchDB(ctx.user.appId) |
|||
|
|||
const appPackage = ctx.request.body |
|||
|
|||
const page = await db.get(ctx.params.pageId) |
|||
await compileStaticAssetsForPage(ctx.user.appId, page.name, ctx.request.body) |
|||
|
|||
// remove special doc props which couch will complain about
|
|||
delete appPackage.page._css |
|||
delete appPackage.page._screens |
|||
appPackage.page._id = appPackage.page._id || generatePageID() |
|||
ctx.body = await db.put(appPackage.page) |
|||
ctx.status = 200 |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
const Router = require("@koa/router") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { BUILDER } = require("../../utilities/security/permissions") |
|||
const controller = require("../controllers/layout") |
|||
|
|||
const router = Router() |
|||
|
|||
router |
|||
.post("/api/layouts", authorized(BUILDER), controller.save) |
|||
.delete( |
|||
"/api/layouts/:layoutId/:layoutRev", |
|||
authorized(BUILDER), |
|||
controller.destroy |
|||
) |
|||
|
|||
module.exports = router |
|||
@ -1,10 +0,0 @@ |
|||
const Router = require("@koa/router") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { BUILDER } = require("../../utilities/security/permissions") |
|||
const controller = require("../controllers/page") |
|||
|
|||
const router = Router() |
|||
|
|||
router.post("/api/pages/:pageId", authorized(BUILDER), controller.save) |
|||
|
|||
module.exports = router |
|||
@ -0,0 +1,46 @@ |
|||
const { generateAssetCss, generateCss } = require("../../../utilities/builder/generateCss") |
|||
|
|||
describe("generate_css", () => { |
|||
it("Check how array styles are output", () => { |
|||
expect(generateCss({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0 10 0 15;") |
|||
}) |
|||
|
|||
it("Check handling of an array with empty string values", () => { |
|||
expect(generateCss({ padding: ["", "", "", ""] })).toBe("") |
|||
}) |
|||
|
|||
it("Check handling of an empty array", () => { |
|||
expect(generateCss({ margin: [] })).toBe("") |
|||
}) |
|||
|
|||
it("Check handling of valid font property", () => { |
|||
expect(generateCss({ "font-size": "10px" })).toBe("font-size: 10px;") |
|||
}) |
|||
}) |
|||
|
|||
|
|||
describe("generate_screen_css", () => { |
|||
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } } |
|||
|
|||
it("Test generation of normal css styles", () => { |
|||
expect(generateAssetCss([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } } |
|||
|
|||
it("Test generation of hover css styles", () => { |
|||
expect(generateAssetCss([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } } |
|||
|
|||
it("Test generation of selection css styles", () => { |
|||
expect(generateAssetCss([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } } |
|||
|
|||
it("Testing handling of empty component styles", () => { |
|||
expect(generateAssetCss([emptyComponent])).toBe("") |
|||
}) |
|||
}) |
|||
@ -0,0 +1,238 @@ |
|||
const BASE_LAYOUT_PROP_IDS = { |
|||
PRIVATE: "layout_private_master", |
|||
PUBLIC: "layout_public_master", |
|||
} |
|||
|
|||
const EMPTY_LAYOUT = { |
|||
componentLibraries: ["@budibase/standard-components"], |
|||
title: "{{ name }}", |
|||
favicon: "./_shared/favicon.png", |
|||
stylesheets: [], |
|||
props: { |
|||
_id: "30b8822a-d07b-49f4-9531-551e37c6899b", |
|||
_component: "@budibase/standard-components/container", |
|||
_children: [ |
|||
{ |
|||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967", |
|||
_component: "##builtin/screenslot", |
|||
_styles: { |
|||
normal: {}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_children: [], |
|||
}, |
|||
], |
|||
type: "div", |
|||
_styles: { |
|||
active: {}, |
|||
hover: {}, |
|||
normal: {}, |
|||
selected: {}, |
|||
}, |
|||
className: "", |
|||
onLoad: [], |
|||
}, |
|||
} |
|||
|
|||
const BASE_LAYOUTS = [ |
|||
{ |
|||
_id: BASE_LAYOUT_PROP_IDS.PRIVATE, |
|||
componentLibraries: ["@budibase/standard-components"], |
|||
title: "{{ name }}", |
|||
favicon: "./_shared/favicon.png", |
|||
stylesheets: [], |
|||
name: "Top Navigation Layout", |
|||
props: { |
|||
_id: "4f569166-a4f3-47ea-a09e-6d218c75586f", |
|||
_component: "@budibase/standard-components/container", |
|||
_children: [ |
|||
{ |
|||
_id: "c74f07266980c4b6eafc33e2a6caa783d", |
|||
_component: "@budibase/standard-components/container", |
|||
_styles: { |
|||
normal: { |
|||
display: "flex", |
|||
"flex-direction": "row", |
|||
"justify-content": "flex-start", |
|||
"align-items": "flex-start", |
|||
background: "#fff", |
|||
width: "100%", |
|||
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", |
|||
}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
className: "", |
|||
onLoad: [], |
|||
type: "div", |
|||
_instanceName: "Header", |
|||
_children: [ |
|||
{ |
|||
_id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1", |
|||
_component: "@budibase/standard-components/navigation", |
|||
_styles: { |
|||
normal: { |
|||
"max-width": "1400px", |
|||
"margin-left": "auto", |
|||
"margin-right": "auto", |
|||
padding: "20px", |
|||
color: "#757575", |
|||
"font-weight": "400", |
|||
"font-size": "16px", |
|||
flex: "1 1 auto", |
|||
}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
logoUrl: |
|||
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg", |
|||
title: "", |
|||
backgroundColor: "", |
|||
color: "", |
|||
borderWidth: "", |
|||
borderColor: "", |
|||
borderStyle: "", |
|||
_instanceName: "Navigation", |
|||
_children: [ |
|||
{ |
|||
_id: "48b35328-4c91-4343-a6a3-1a1fd77b3386", |
|||
_component: "@budibase/standard-components/link", |
|||
_styles: { |
|||
normal: { |
|||
"font-family": "Inter", |
|||
"font-weight": "500", |
|||
color: "#000000", |
|||
"text-decoration-line": "none", |
|||
"font-size": "16px", |
|||
}, |
|||
hover: { |
|||
color: "#4285f4", |
|||
}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
url: "/", |
|||
openInNewTab: false, |
|||
text: "Home", |
|||
color: "", |
|||
hoverColor: "", |
|||
underline: false, |
|||
fontSize: "", |
|||
fontFamily: "initial", |
|||
_instanceName: "Home Link", |
|||
_children: [], |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967", |
|||
_component: "##builtin/screenslot", |
|||
_styles: { |
|||
normal: { |
|||
flex: "1 1 auto", |
|||
display: "flex", |
|||
"flex-direction": "column", |
|||
"justify-content": "flex-start", |
|||
"align-items": "stretch", |
|||
"max-width": "100%", |
|||
"margin-left": "20px", |
|||
"margin-right": "20px", |
|||
width: "1400px", |
|||
padding: "20px", |
|||
}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_children: [], |
|||
}, |
|||
], |
|||
type: "div", |
|||
_styles: { |
|||
active: {}, |
|||
hover: {}, |
|||
normal: { |
|||
display: "flex", |
|||
"flex-direction": "column", |
|||
"align-items": "center", |
|||
"justify-content": "flex-start", |
|||
"margin-right": "auto", |
|||
"margin-left": "auto", |
|||
"min-height": "100%", |
|||
"background-image": |
|||
"linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);", |
|||
}, |
|||
selected: {}, |
|||
}, |
|||
className: "", |
|||
onLoad: [], |
|||
}, |
|||
}, |
|||
{ |
|||
_id: BASE_LAYOUT_PROP_IDS.PUBLIC, |
|||
componentLibraries: ["@budibase/standard-components"], |
|||
title: "{{ name }}", |
|||
favicon: "./_shared/favicon.png", |
|||
stylesheets: [], |
|||
name: "Empty Layout", |
|||
props: { |
|||
_id: "3723ffa1-f9e0-4c05-8013-98195c788ed6", |
|||
_component: "@budibase/standard-components/container", |
|||
_children: [ |
|||
{ |
|||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967", |
|||
_component: "##builtin/screenslot", |
|||
_styles: { |
|||
normal: { |
|||
flex: "1 1 auto", |
|||
display: "flex", |
|||
"flex-direction": "column", |
|||
"justify-content": "flex-start", |
|||
"align-items": "stretch", |
|||
"max-width": "100%", |
|||
"margin-left": "20px", |
|||
"margin-right": "20px", |
|||
width: "1400px", |
|||
padding: "20px", |
|||
}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_children: [], |
|||
}, |
|||
], |
|||
type: "div", |
|||
_styles: { |
|||
active: {}, |
|||
hover: {}, |
|||
normal: { |
|||
display: "flex", |
|||
"flex-direction": "column", |
|||
"align-items": "center", |
|||
"justify-content": "center", |
|||
"margin-right": "auto", |
|||
"margin-left": "auto", |
|||
"min-height": "100%", |
|||
"background-image": |
|||
"linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);", |
|||
}, |
|||
selected: {}, |
|||
}, |
|||
className: "", |
|||
onLoad: [], |
|||
}, |
|||
}, |
|||
] |
|||
|
|||
module.exports = { |
|||
BASE_LAYOUTS, |
|||
BASE_LAYOUT_PROP_IDS, |
|||
EMPTY_LAYOUT, |
|||
} |
|||
@ -1,198 +0,0 @@ |
|||
const PageTypes = { |
|||
MAIN: "main", |
|||
UNAUTHENTICATED: "unauthenticated", |
|||
} |
|||
|
|||
const MAIN = { |
|||
componentLibraries: ["@budibase/standard-components"], |
|||
title: "{{ name }}", |
|||
favicon: "./_shared/favicon.png", |
|||
stylesheets: [], |
|||
name: PageTypes.MAIN, |
|||
props: { |
|||
_id: "private-master-root", |
|||
_component: "@budibase/standard-components/container", |
|||
_children: [ |
|||
{ |
|||
_id: "c74f07266980c4b6eafc33e2a6caa783d", |
|||
_component: "@budibase/standard-components/container", |
|||
_styles: { |
|||
normal: { |
|||
display: "flex", |
|||
"flex-direction": "row", |
|||
"justify-content": "flex-start", |
|||
"align-items": "flex-start", |
|||
background: "#fff", |
|||
width: "100%", |
|||
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", |
|||
}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_code: "", |
|||
type: "div", |
|||
_appId: "inst_app_80b_f158d4057d2c4bedb0042d42fda8abaf", |
|||
_instanceName: "Header", |
|||
_children: [ |
|||
{ |
|||
_id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1", |
|||
_component: "@budibase/standard-components/navigation", |
|||
_styles: { |
|||
normal: { |
|||
"max-width": "1400px", |
|||
"margin-left": "auto", |
|||
"margin-right": "auto", |
|||
padding: "20px", |
|||
color: "#757575", |
|||
"font-weight": "400", |
|||
"font-size": "16px", |
|||
flex: "1 1 auto", |
|||
}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_code: "", |
|||
logoUrl: |
|||
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg", |
|||
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394", |
|||
_instanceName: "Navigation", |
|||
_children: [ |
|||
{ |
|||
_id: "48b35328-4c91-4343-a6a3-1a1fd77b3386", |
|||
_component: "@budibase/standard-components/link", |
|||
_styles: { |
|||
normal: { |
|||
"font-family": "Inter", |
|||
"font-weight": "500", |
|||
color: "#000000", |
|||
"text-decoration-line": "none", |
|||
"font-size": "16px", |
|||
}, |
|||
hover: { |
|||
color: "#4285f4", |
|||
}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_code: "", |
|||
url: "/", |
|||
openInNewTab: false, |
|||
text: "Home", |
|||
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394", |
|||
_instanceName: "Home Link", |
|||
_children: [], |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967", |
|||
_component: "##builtin/screenslot", |
|||
_styles: { |
|||
normal: { |
|||
flex: "1 1 auto", |
|||
display: "flex", |
|||
"flex-direction": "column", |
|||
"justify-content": "flex-start", |
|||
"align-items": "stretch", |
|||
"max-width": "100%", |
|||
"margin-left": "20px", |
|||
"margin-right": "20px", |
|||
width: "1400px", |
|||
padding: "20px", |
|||
}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_code: "", |
|||
_children: [], |
|||
}, |
|||
], |
|||
type: "div", |
|||
_styles: { |
|||
active: {}, |
|||
hover: {}, |
|||
normal: { |
|||
display: "flex", |
|||
"flex-direction": "column", |
|||
"align-items": "center", |
|||
"justify-content": "flex-start", |
|||
"margin-right": "auto", |
|||
"margin-left": "auto", |
|||
"min-height": "100%", |
|||
"background-image": |
|||
"linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);", |
|||
}, |
|||
selected: {}, |
|||
}, |
|||
_code: "", |
|||
}, |
|||
} |
|||
|
|||
const UNAUTHENTICATED = { |
|||
componentLibraries: ["@budibase/standard-components"], |
|||
title: "{{ name }}", |
|||
favicon: "./_shared/favicon.png", |
|||
stylesheets: [], |
|||
name: PageTypes.UNAUTHENTICATED, |
|||
props: { |
|||
_id: "public-master-root", |
|||
_component: "@budibase/standard-components/container", |
|||
_children: [ |
|||
{ |
|||
_id: "686c252d-dbf2-4e28-9078-414ba4719759", |
|||
_component: "@budibase/standard-components/login", |
|||
_styles: { |
|||
normal: { |
|||
padding: "64px", |
|||
background: "rgba(255, 255, 255, 0.4)", |
|||
"border-radius": "0.5rem", |
|||
"margin-top": "0px", |
|||
margin: "0px", |
|||
"line-height": "1", |
|||
"box-shadow": |
|||
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)", |
|||
"font-size": "16px", |
|||
"font-family": "Inter", |
|||
flex: "0 1 auto", |
|||
transform: "0", |
|||
}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_code: "", |
|||
_instanceName: "Login", |
|||
_children: [], |
|||
title: "Log in to {{ name }}", |
|||
buttonText: "Log In", |
|||
logo: |
|||
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg", |
|||
}, |
|||
], |
|||
type: "div", |
|||
_styles: { |
|||
active: {}, |
|||
hover: {}, |
|||
normal: { |
|||
display: "flex", |
|||
"flex-direction": "column", |
|||
"align-items": "center", |
|||
"justify-content": "center", |
|||
"margin-right": "auto", |
|||
"margin-left": "auto", |
|||
"min-height": "100%", |
|||
"background-image": |
|||
"linear-gradient(135deg, rgba(252,215,212,1) 20%, rgba(207,218,255,1) 100%);", |
|||
}, |
|||
selected: {}, |
|||
}, |
|||
_code: "", |
|||
}, |
|||
} |
|||
|
|||
module.exports = { MAIN, UNAUTHENTICATED, PageTypes } |
|||
@ -0,0 +1,83 @@ |
|||
const { |
|||
ensureDir, |
|||
constants, |
|||
copyFile, |
|||
writeFile, |
|||
readdir, |
|||
readFile, |
|||
existsSync, |
|||
} = require("fs-extra") |
|||
const { join } = require("../centralPath") |
|||
const { budibaseAppsDir } = require("../budibaseDir") |
|||
|
|||
const CSS_DIRECTORY = "css" |
|||
|
|||
/** |
|||
* Compile all the non-db static web assets that are required for the running of |
|||
* a budibase application. This includes CSS, the JSON structure of the DOM and |
|||
* the client library, a script responsible for reading the JSON structure |
|||
* and rendering the application. |
|||
* @param {string} appId id of the application we want to compile static assets for |
|||
* @param {array|object} assets a list of screens or screen layouts for which the CSS should be extracted and stored. |
|||
*/ |
|||
module.exports = async (appId, assets) => { |
|||
const publicPath = join(budibaseAppsDir(), appId, "public") |
|||
await ensureDir(publicPath) |
|||
for (let asset of Array.isArray(assets) ? assets : [assets]) { |
|||
await buildCssBundle(publicPath, asset) |
|||
await copyClientLib(publicPath) |
|||
// remove props that shouldn't be present when written to DB
|
|||
if (asset._css) { |
|||
delete asset._css |
|||
} |
|||
} |
|||
return assets |
|||
} |
|||
|
|||
/** |
|||
* Reads the _css property of all screens and the screen layouts, and creates a singular CSS |
|||
* bundle for the app at <appId>/public/bundle.css |
|||
* @param {String} publicPath - path to the public assets directory of the budibase application |
|||
* @param {Object} asset a single screen or screen layout which is being updated |
|||
*/ |
|||
const buildCssBundle = async (publicPath, asset) => { |
|||
const cssPath = join(publicPath, CSS_DIRECTORY) |
|||
let cssString = "" |
|||
|
|||
// create a singular CSS file for this asset
|
|||
const assetCss = asset._css ? asset._css.trim() : "" |
|||
if (assetCss.length !== 0) { |
|||
await ensureDir(cssPath) |
|||
await writeFile(join(cssPath, asset._id), assetCss) |
|||
} |
|||
|
|||
// bundle up all the CSS in the directory into one top level CSS file
|
|||
if (existsSync(cssPath)) { |
|||
const cssFiles = await readdir(cssPath) |
|||
for (let filename of cssFiles) { |
|||
const css = await readFile(join(cssPath, filename)) |
|||
cssString += css |
|||
} |
|||
} |
|||
|
|||
await writeFile(join(publicPath, "bundle.css"), cssString) |
|||
} |
|||
|
|||
/** |
|||
* Copy the budibase client library and sourcemap from NPM to <appId>/public/. |
|||
* The client library is then served as a static asset when the budibase application |
|||
* is running in preview or prod |
|||
* @param {String} publicPath - path to write the client library to |
|||
*/ |
|||
const copyClientLib = async publicPath => { |
|||
const sourcepath = require.resolve("@budibase/client") |
|||
const destPath = join(publicPath, "budibase-client.js") |
|||
|
|||
await copyFile(sourcepath, destPath, constants.COPYFILE_FICLONE) |
|||
|
|||
await copyFile( |
|||
sourcepath + ".map", |
|||
destPath + ".map", |
|||
constants.COPYFILE_FICLONE |
|||
) |
|||
} |
|||
@ -1,102 +0,0 @@ |
|||
const { ensureDir, constants, copyFile, writeFile } = require("fs-extra") |
|||
const { join } = require("../centralPath") |
|||
const { budibaseAppsDir } = require("../budibaseDir") |
|||
|
|||
/** |
|||
* Compile all the non-db static web assets that are required for the running of |
|||
* a budibase application. This includes CSS, the JSON structure of the DOM and |
|||
* the client library, a script responsible for reading the JSON structure |
|||
* and rendering the application. |
|||
* @param {} appId - id of the application we want to compile static assets for |
|||
* @param {*} pageName - name of the page that the assets will be served for |
|||
* @param {*} pkg - app package information/metadata |
|||
*/ |
|||
module.exports = async (appId, pageName, pkg) => { |
|||
const pagePath = join(budibaseAppsDir(), appId, "public", pageName) |
|||
|
|||
pkg.screens = pkg.screens || [] |
|||
|
|||
await ensureDir(pagePath) |
|||
|
|||
await buildPageCssBundle(pagePath, pkg) |
|||
|
|||
await buildFrontendAppDefinition(pagePath, pkg) |
|||
|
|||
await copyClientLib(pagePath) |
|||
} |
|||
|
|||
/** |
|||
* Reads the _css property of a page and its screens, and creates a singular CSS |
|||
* bundle for the page at <appId>/public/<pageName>/bundle.css |
|||
* @param {String} publicPagePath - path to the public assets directory of the budibase application |
|||
* @param {Object} pkg - app package information |
|||
* @param {"main" | "unauthenticated"} pageName - the pagename of the page we are compiling CSS for. |
|||
*/ |
|||
const buildPageCssBundle = async (publicPagePath, pkg) => { |
|||
let cssString = "" |
|||
|
|||
for (let screen of pkg.screens || []) { |
|||
if (!screen._css) continue |
|||
if (screen._css.trim().length === 0) { |
|||
delete screen._css |
|||
continue |
|||
} |
|||
cssString += screen._css |
|||
} |
|||
|
|||
if (pkg.page._css) cssString += pkg.page._css |
|||
|
|||
writeFile(join(publicPagePath, "bundle.css"), cssString) |
|||
} |
|||
|
|||
/** |
|||
* Copy the budibase client library and sourcemap from NPM to <appId>/public/<pageName>. |
|||
* The client library is then served as a static asset when the budibase application |
|||
* is running in preview or prod |
|||
* @param {String} pagePath - path to write the client library to |
|||
*/ |
|||
const copyClientLib = async pagePath => { |
|||
const sourcepath = require.resolve("@budibase/client") |
|||
const destPath = join(pagePath, "budibase-client.js") |
|||
|
|||
await copyFile(sourcepath, destPath, constants.COPYFILE_FICLONE) |
|||
|
|||
await copyFile( |
|||
sourcepath + ".map", |
|||
destPath + ".map", |
|||
constants.COPYFILE_FICLONE |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* Build the frontend definition for a budibase application. This includes all page and screen information, |
|||
* and is injected into the budibase client library to tell it how to start constructing |
|||
* the DOM from components defined in the frontendDefinition. |
|||
* @param {String} pagePath - path to the public folder of the page where the definition will be written |
|||
* @param {Object} pkg - app package information from which the frontendDefinition will be built. |
|||
*/ |
|||
const buildFrontendAppDefinition = async (pagePath, pkg) => { |
|||
const filename = join(pagePath, "clientFrontendDefinition.js") |
|||
|
|||
// Delete CSS code from the page and screens so it's not injected
|
|||
delete pkg.page._css |
|||
|
|||
for (let screen of pkg.screens) { |
|||
if (screen._css) { |
|||
delete pkg.page._css |
|||
} |
|||
} |
|||
|
|||
const clientUiDefinition = JSON.stringify({ |
|||
page: pkg.page, |
|||
screens: pkg.screens, |
|||
libraries: ["@budibase/standard-components"], |
|||
}) |
|||
|
|||
await writeFile( |
|||
filename, |
|||
` |
|||
window['##BUDIBASE_FRONTEND_DEFINITION##'] = ${clientUiDefinition}; |
|||
` |
|||
) |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
const handlebars = require("handlebars") |
|||
|
|||
handlebars.registerHelper("object", value => { |
|||
return new handlebars.SafeString(JSON.stringify(value)) |
|||
}) |
|||
|
|||
/** |
|||
* When running mustache statements to execute on the context of the automation it possible user's may input mustache |
|||
* in a few different forms, some of which are invalid but are logically valid. An example of this would be the mustache |
|||
* statement "{{steps[0].revision}}" here it is obvious the user is attempting to access an array or object using array |
|||
* like operators. These are not supported by Mustache and therefore the statement will fail. This function will clean up |
|||
* the mustache statement so it instead reads as "{{steps.0.revision}}" which is valid and will work. It may also be expanded |
|||
* to include any other mustache statement cleanup that has been deemed necessary for the system. |
|||
* |
|||
* @param {string} string The string which *may* contain mustache statements, it is OK if it does not contain any. |
|||
* @returns {string} The string that was input with cleaned up mustache statements as required. |
|||
*/ |
|||
function cleanMustache(string) { |
|||
let charToReplace = { |
|||
"[": ".", |
|||
"]": "", |
|||
} |
|||
let regex = new RegExp(/{{[^}}]*}}/g) |
|||
let matches = string.match(regex) |
|||
if (matches == null) { |
|||
return string |
|||
} |
|||
for (let match of matches) { |
|||
let baseIdx = string.indexOf(match) |
|||
for (let key of Object.keys(charToReplace)) { |
|||
let idxChar = match.indexOf(key) |
|||
if (idxChar !== -1) { |
|||
string = |
|||
string.slice(baseIdx, baseIdx + idxChar) + |
|||
charToReplace[key] + |
|||
string.slice(baseIdx + idxChar + 1) |
|||
} |
|||
} |
|||
} |
|||
return string |
|||
} |
|||
|
|||
/** |
|||
* Given an input object this will recurse through all props to try and update |
|||
* any handlebars/mustache statements within. |
|||
* @param {object|array} inputs The input structure which is to be recursed, it is important to note that |
|||
* if the structure contains any cycles then this will fail. |
|||
* @param {object} context The context that handlebars should fill data from. |
|||
* @returns {object|array} The structure input, as fully updated as possible. |
|||
*/ |
|||
function recurseMustache(inputs, context) { |
|||
// JSON stringify will fail if there are any cycles, stops infinite recursion
|
|||
try { |
|||
JSON.stringify(inputs) |
|||
} catch (err) { |
|||
throw "Unable to process inputs to JSON, cannot recurse" |
|||
} |
|||
for (let key of Object.keys(inputs)) { |
|||
let val = inputs[key] |
|||
if (typeof val === "string") { |
|||
val = cleanMustache(inputs[key]) |
|||
const template = handlebars.compile(val) |
|||
inputs[key] = template(context) |
|||
} |
|||
// this covers objects and arrays
|
|||
else if (typeof val === "object") { |
|||
inputs[key] = recurseMustache(inputs[key], context) |
|||
} |
|||
} |
|||
return inputs |
|||
} |
|||
|
|||
exports.recurseMustache = recurseMustache |
|||
Loading…
Reference in new issue