Merge branch 'master' of https://github.com/Budibase/budibase into property-panel/colorpicker

This commit is contained in:
Conor_Mack 2020-06-16 09:58:48 +01:00
commit dbc490994b
218 changed files with 6510 additions and 3668 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#6A78D1;}
.st2{fill:#49C39E;}
.st3{fill:#F2545B;}
.st4{fill:#F5C26B;}
</style>
<g>
<g>
<path class="st0" d="M215,14.1l136.2,79.8c9.3,5.4,15,15.5,15,26.4v159.5c0,10.9-5.7,20.9-15,26.4L215,385.9
c-9.3,5.4-20.7,5.4-30,0L48.8,306.2c-9.3-5.4-15-15.5-15-26.4V120.2c0-10.9,5.7-20.9,15-26.4L185,14.1
C194.3,8.6,205.7,8.6,215,14.1z"/>
</g>
<g>
<path class="st1" d="M288.8,273.7l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6
c3.6-1.9,8-1.9,11.6,0l83,43.6C297.1,258.3,297.1,269.4,288.8,273.7z"/>
<path class="st2" d="M288.8,231.2l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6
c3.6-1.9,8-1.9,11.6,0l83,43.6C297.1,215.7,297.1,226.9,288.8,231.2z"/>
<path class="st3" d="M288.8,188.6l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6
c3.6-1.9,8-1.9,11.6,0l83,43.6C297.1,173.1,297.1,184.3,288.8,188.6z"/>
<path class="st4" d="M288.8,146l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6c3.6-1.9,8-1.9,11.6,0
l83,43.6C297.1,130.6,297.1,141.7,288.8,146z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 115 40" style="enable-background:new 0 0 115 40;" xml:space="preserve">
<style type="text/css">
.st0{fill:#393C44;}
.st1{fill:#FFFFFF;}
</style>
<path class="st0" d="M111.16,40H3.91c-2.15,0-3.89-1.74-3.89-3.89V4.04c0-2.15,1.74-3.89,3.89-3.89h107.25
c2.15,0,3.89,1.74,3.89,3.89v32.07C115.05,38.26,113.31,40,111.16,40z"/>
<path class="st1" d="M10.37,10.03v8.57c0.93-1.26,2.33-1.67,3.61-1.67c1.58,0,3.01,0.59,4.02,1.54c1.12,1.05,1.82,2.62,1.82,4.53
c0,1.78-0.62,3.42-1.82,4.61c-1.01,1.03-2.26,1.57-3.97,1.57c-2.05,0-3.09-0.95-3.66-1.78v1.39H6.63V10.03H10.37z M10.97,20.98
c-0.44,0.46-0.82,1.13-0.82,2.11c0,0.95,0.41,1.64,0.85,2.05c0.59,0.57,1.41,0.85,2.11,0.85c0.64,0,1.36-0.26,1.93-0.8
c0.54-0.51,0.9-1.26,0.9-2.11c0-0.92-0.36-1.67-0.9-2.18c-0.59-0.57-1.23-0.77-1.98-0.77C12.26,20.14,11.56,20.37,10.97,20.98z"/>
<path class="st1" d="M25.08,17.35v6.32c0,0.51,0.05,1.31,0.64,1.85c0.26,0.23,0.72,0.54,1.54,0.54c0.69,0,1.23-0.23,1.56-0.54
c0.54-0.51,0.62-1.28,0.62-1.85v-6.32h3.74v6.68c0,1.31-0.13,2.54-1.28,3.67c-1.31,1.28-3.23,1.49-4.59,1.49
c-1.41,0-3.31-0.21-4.62-1.49c-1.05-1.03-1.26-2.18-1.26-3.44v-6.91H25.08z"/>
<path class="st1" d="M47.88,28.79h-3.74V27.4c-0.57,0.82-1.61,1.78-3.66,1.78c-1.71,0-2.96-0.54-3.97-1.57
c-1.19-1.18-1.82-2.83-1.82-4.61c0-1.9,0.7-3.47,1.82-4.53c1.01-0.95,2.44-1.54,4.02-1.54c1.27,0,2.67,0.41,3.61,1.67v-8.57h3.74
V28.79z M39.53,20.91c-0.54,0.51-0.9,1.26-0.9,2.18c0,0.85,0.36,1.59,0.9,2.11c0.57,0.54,1.28,0.8,1.93,0.8
c0.69,0,1.52-0.28,2.11-0.85c0.44-0.41,0.85-1.1,0.85-2.05c0-0.98-0.39-1.64-0.82-2.11c-0.59-0.62-1.28-0.85-2.08-0.85
C40.77,20.14,40.12,20.34,39.53,20.91z"/>
<path class="st1" d="M52.32,10.3c1.21,0,2.16,0.95,2.16,2.16c0,1.21-0.95,2.16-2.16,2.16c-1.21,0-2.16-0.95-2.16-2.16
C50.17,11.25,51.12,10.3,52.32,10.3z M54.19,17.35v11.44h-3.74V17.35H54.19z"/>
<path class="st1" d="M60.49,10.03v8.57c0.93-1.26,2.33-1.67,3.61-1.67c1.58,0,3.01,0.59,4.02,1.54c1.12,1.05,1.82,2.62,1.82,4.53
c0,1.78-0.62,3.42-1.82,4.61c-1.01,1.03-2.26,1.57-3.97,1.57c-2.05,0-3.09-0.95-3.66-1.78v1.39h-3.74V10.03H60.49z M61.06,20.98
c-0.44,0.46-0.82,1.13-0.82,2.11c0,0.95,0.41,1.64,0.85,2.05c0.59,0.57,1.41,0.85,2.11,0.85c0.64,0,1.36-0.26,1.93-0.8
c0.54-0.51,0.9-1.26,0.9-2.11c0-0.92-0.36-1.67-0.9-2.18c-0.59-0.57-1.23-0.77-1.98-0.77C62.34,20.14,61.65,20.37,61.06,20.98z"/>
<path class="st1" d="M80.26,17.35H84v11.44h-3.74v-1.39c-1.01,1.54-2.46,1.77-3.42,1.77c-1.66,0-3.06-0.41-4.33-1.74
c-1.22-1.28-1.69-2.77-1.69-4.28c0-1.92,0.73-3.57,1.79-4.62c1.01-1,2.41-1.56,4.02-1.56c0.99,0,2.57,0.23,3.63,1.67V17.35z
M75.57,20.96c-0.39,0.39-0.85,1.05-0.85,2.08c0,1.03,0.44,1.7,0.77,2.05c0.51,0.54,1.31,0.9,2.18,0.9c0.74,0,1.44-0.31,1.93-0.8
c0.49-0.46,0.9-1.18,0.9-2.16c0-0.82-0.31-1.59-0.85-2.11c-0.57-0.54-1.39-0.8-2.05-0.8C76.8,20.14,76.06,20.47,75.57,20.96z"/>
<path class="st1" d="M93.21,20.26c-0.57-0.33-1.31-0.64-2.03-0.64c-0.39,0-0.82,0.1-1.05,0.33c-0.13,0.13-0.23,0.33-0.23,0.51
c0,0.26,0.18,0.41,0.36,0.51c0.26,0.15,0.64,0.23,1.1,0.39l0.98,0.31c0.64,0.21,1.31,0.46,1.9,1c0.67,0.62,0.9,1.31,0.9,2.18
c0,1.52-0.67,2.49-1.18,3.01c-1.13,1.13-2.52,1.31-3.72,1.31c-1.54,0-3.21-0.33-4.7-1.64l1.57-2.49c0.36,0.31,0.87,0.67,1.26,0.85
c0.51,0.26,1.05,0.36,1.54,0.36c0.23,0,0.82,0,1.16-0.26c0.23-0.18,0.39-0.46,0.39-0.74c0-0.21-0.08-0.46-0.41-0.67
c-0.26-0.15-0.59-0.26-1.13-0.41l-0.92-0.28c-0.67-0.21-1.36-0.57-1.85-1.05c-0.54-0.57-0.82-1.21-0.82-2.08
c0-1.1,0.44-2.03,1.1-2.65c1.03-0.95,2.41-1.16,3.47-1.16c1.7,0,2.88,0.44,3.8,0.98L93.21,20.26z"/>
<path class="st1" d="M108.43,23.73h-8.55c0,0.62,0.23,1.44,0.69,1.95c0.57,0.62,1.34,0.72,1.9,0.72c0.54,0,1.1-0.1,1.49-0.33
c0.05-0.03,0.49-0.31,0.8-0.95l3.49,0.36c-0.51,1.62-1.54,2.47-2.21,2.88c-1.1,0.67-2.34,0.85-3.62,0.85
c-1.72,0-3.24-0.31-4.57-1.64c-1-1-1.72-2.52-1.72-4.42c0-1.64,0.59-3.34,1.75-4.52c1.39-1.39,3.11-1.64,4.39-1.64
c1.28,0,3.13,0.23,4.55,1.72c1.36,1.44,1.62,3.24,1.62,4.65V23.73z M105.03,21.48c-0.03-0.1-0.21-0.82-0.74-1.34
c-0.41-0.39-1-0.64-1.75-0.64c-0.95,0-1.52,0.39-1.87,0.74c-0.28,0.31-0.54,0.72-0.64,1.23H105.03z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@ -38,20 +38,23 @@
]
},
"dependencies": {
"@budibase/bbui": "^0.3.5",
"@beyonk/svelte-notifications": "^2.0.3",
"@budibase/bbui": "^1.1.1",
"@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0",
"date-fns": "^1.29.0",
"deepmerge": "^4.2.2",
"feather-icons": "^4.21.0",
"flatpickr": "^4.5.7",
"lodash": "^4.17.13",
"logrocket": "^1.0.6",
"lunr": "^2.3.5",
"mustache": "^4.0.1",
"safe-buffer": "^5.1.2",
"shortid": "^2.2.8",
"string_decoder": "^1.2.0",
"svelte-simple-modal": "^0.3.0",
"svelte-simple-modal": "^0.4.2",
"uikit": "^3.1.7"
},
"devDependencies": {
@ -83,4 +86,4 @@
"svelte": "3.23.x"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
}
}

View File

@ -7,6 +7,7 @@
import AppNotification, {
showAppNotification,
} from "components/common/AppNotification.svelte"
import { NotificationDisplay } from "@beyonk/svelte-notifications"
function showErrorBanner() {
showAppNotification({
@ -26,4 +27,7 @@
<AppNotification />
<!-- svelte-notifications -->
<NotificationDisplay />
<Router {routes} />

View File

@ -1,8 +1,7 @@
/* Budibase Component Styles */
.header {
font-size: 0.75rem;
color: #000333;
opacity: 0.4;
color: var(--ink);
text-transform: uppercase;
margin-top: 1rem;
font-weight: 500;
@ -57,35 +56,34 @@
.budibase__nav-item {
cursor: pointer;
padding: 0 7px 0 3px;
padding: 0 4px 0 2px;
height: 35px;
margin: 5px 20px 5px 0px;
margin: 5px 0px 4px 0px;
border-radius: 0 5px 5px 0;
display: flex;
align-items: center;
font-weight: 500;
font-size: 13px;
font-size: 14px;
transition: 0.2s;
}
.budibase__nav-item.selected {
color: var(--button-text);
background: #f1f4fc;
color: var(--ink);
background: var(--blue-light);
}
.budibase__nav-item:hover {
background: #fafafa;
background: var(--grey-light);
}
.budibase__input {
width: 250px;
height: 35px;
width: 220px;
border-radius: 3px;
border: 1px solid #DBDBDB;
border: 1px solid var(--grey-dark);
text-align: left;
letter-spacing: 0.7px;
color: #000333;
font-size: 16px;
padding-left: 5px;
color: var(--ink);
font-size: 14px;
padding-left: 12px;
}
.uk-text-right {
@ -102,27 +100,32 @@
}
.budibase__table {
border: 1px solid #ccc;
border: 1px solid var(--grey-dark);
background: #fff;
border-radius: 2px;
}
.budibase__table thead {
background: #fafafa;
background: var(--blue-light);
}
.budibase__table thead > tr > th {
color: var(--button-text);
color: var(--ink);
text-transform: capitalize;
font-weight: 500;
}
.budibase__table tr {
border-bottom: 1px solid #ccc;
border-bottom: 1px solid var(--grey-light);
}
.button--toggled {
background: #fafafa;
color: var(--button-text);
padding: 10px;
background: var(--blue-light);
color: var(--ink-light);
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}

View File

@ -3,6 +3,7 @@ const apiCall = method => async (url, body) => {
method: method,
headers: {
"Content-Type": "application/json",
"x-user-agent": "Budibase Builder",
},
body: body && JSON.stringify(body),
})
@ -14,14 +15,16 @@ const apiCall = method => async (url, body) => {
return response
}
const post = apiCall("POST")
const get = apiCall("GET")
const patch = apiCall("PATCH")
const del = apiCall("DELETE")
export const post = apiCall("POST")
export const get = apiCall("GET")
export const patch = apiCall("PATCH")
export const del = apiCall("DELETE")
export const put = apiCall("PUT")
export default {
post,
get,
patch,
delete: del,
put,
}

View File

@ -17,33 +17,22 @@ export const generate_screen_css = component_arr => {
export const generate_css = style => {
let cssString = Object.entries(style).reduce((str, [key, value]) => {
//TODO Handle arrays and objects here also
if (typeof value === "string") {
if (value) {
return (str += `${key}: ${value};\n`)
}
} else if (Array.isArray(value)) {
if (value.length > 0 && !value.every(v => v === "")) {
return (str += `${key}: ${value
.map(generate_array_styles)
.join(" ")};\n`)
return (str += `${key}: ${value.join(" ")};\n`)
}
}
return str
}, "")
return (cssString || "").trim()
}
export const generate_array_styles = item => {
let safeItem = item === "" ? 0 : item
let hasPx = new RegExp("px$")
if (!hasPx.test(safeItem)) {
return `${safeItem}px`
} else {
return safeItem
}
}
export const apply_class = (id, name = "element", styles, selector) => {
if (selector === "normal") {
return `.${name}-${id} {\n${styles}\n}`

View File

@ -1,9 +1,11 @@
import { getStore } from "./store"
import { getBackendUiStore } from "./store/backend"
import { getWorkflowStore } from "./store/workflow/"
import LogRocket from "logrocket"
export const store = getStore()
export const backendUiStore = getBackendUiStore()
export const workflowStore = getWorkflowStore()
export const initialise = async () => {
try {

View File

@ -1,3 +1,5 @@
import { get } from "builderStore/api"
/**
* Fetches the definitions for component library components. This includes
* their props and other metadata from components.json.
@ -6,7 +8,7 @@
export const fetchComponentLibDefinitions = async appId => {
const LIB_DEFINITION_URL = `/${appId}/components/definitions`
try {
const libDefinitionResponse = await fetch(LIB_DEFINITION_URL)
const libDefinitionResponse = await get(LIB_DEFINITION_URL)
return await libDefinitionResponse.json()
} catch (err) {
console.error(`Error fetching component definitions for ${appId}`, err)

View File

@ -1,5 +1,13 @@
import { writable } from "svelte/store"
import api from "../api"
import { getContext } from "svelte"
/** TODO: DEMO SOLUTION
* this section should not be here, it is a quick fix for a demo
* when we reorg the backend UI, this should disappear
* **/
import { CreateEditModelModal } from "components/database/ModelDataTable/modals"
/** DEMO SOLUTION END **/
export const getBackendUiStore = () => {
const INITIAL_BACKEND_UI_STATE = {
@ -22,11 +30,27 @@ export const getBackendUiStore = () => {
const views = await viewsResponse.json()
store.update(state => {
state.selectedDatabase = db
if (models && models.length > 0) {
state.selectedModel = models[0]
state.selectedView = `all_${models[0]._id}`
}
state.breadcrumbs = [db.name]
state.models = models
state.views = views
return state
})
/** TODO: DEMO SOLUTION**/
if (!models || models.length === 0) {
const { open, close } = getContext("simple-modal")
open(
CreateEditModelModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
/** DEMO SOLUTION END **/
},
},
records: {
@ -51,6 +75,8 @@ export const getBackendUiStore = () => {
store.update(state => {
state.models.push(model)
state.models = state.models
state.selectedModel = model
state.selectedView = `all_${model._id}`
return state
}),
},

View File

@ -1,11 +1,10 @@
import { cloneDeep, values } from "lodash/fp"
import { values } from "lodash/fp"
import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend"
import { writable, get } from "svelte/store"
import api from "../api"
import { DEFAULT_PAGES_OBJECT } from "../../constants"
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
import { rename } from "components/userInterface/pagesParsing/renameScreen"
import {
createProps,
makePropsSafe,
@ -16,6 +15,16 @@ import { buildCodeForScreens } from "../buildCodeForScreens"
import { generate_screen_css } from "../generate_css"
import { insertCodeMetadata } from "../insertCodeMetadata"
import { uuid } from "../uuid"
import {
selectComponent as _selectComponent,
getParent,
walkProps,
savePage as _savePage,
saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
regenerateCssForCurrentScreen,
renameCurrentScreen,
} from "../storeUtils"
export const getStore = () => {
const initial = {
@ -43,7 +52,6 @@ export const getStore = () => {
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
store.saveScreen = saveScreen(store)
store.renameScreen = renameScreen(store)
store.deleteScreen = deleteScreen(store)
store.setCurrentScreen = setCurrentScreen(store)
store.setCurrentPage = setCurrentPage(store)
@ -54,13 +62,10 @@ export const getStore = () => {
store.addChildComponent = addChildComponent(store)
store.selectComponent = selectComponent(store)
store.setComponentProp = setComponentProp(store)
store.setPageOrScreenProp = setPageOrScreenProp(store)
store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(store)
store.deleteComponent = deleteComponent(store)
store.moveUpComponent = moveUpComponent(store)
store.moveDownComponent = moveDownComponent(store)
store.copyComponent = copyComponent(store)
store.getPathToComponent = getPathToComponent(store)
store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store)
@ -69,6 +74,9 @@ export const getStore = () => {
export default getStore
export const getComponentDefinition = (state, name) =>
name.startsWith("##") ? getBuiltin(name) : state.components[name]
const setPackage = (store, initial) => async pkg => {
const [main_screens, unauth_screens] = await Promise.all([
api
@ -140,12 +148,6 @@ const _saveScreen = async (store, s, screen) => {
return s
}
const _saveScreenApi = (screen, s) => {
api
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
.then(() => _savePage(s))
}
const createScreen = store => (screenName, route, layoutComponentName) => {
store.update(state => {
const rootComponent = state.components[layoutComponentName]
@ -155,7 +157,6 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
description: "",
url: "",
_css: "",
uiFunctions: "",
props: createProps(rootComponent).props,
}
@ -173,11 +174,10 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
const setCurrentScreen = store => screenName => {
store.update(s => {
const screen = getExactComponent(s.screens, screenName)
screen._css = generate_screen_css([screen.props])
s.currentPreviewItem = screen
s.currentFrontEndType = "screen"
s.currentView = "detail"
regenerateCssForCurrentScreen(s)
const safeProps = makePropsSafe(
s.components[screen.props._component],
screen.props
@ -207,46 +207,6 @@ const deleteScreen = store => name => {
})
}
const renameScreen = store => (oldname, newname) => {
store.update(s => {
const { screens, pages, error, changedScreens } = rename(
s.pages,
s.screens,
oldname,
newname
)
if (error) {
// should really do something with this
return s
}
s.screens = screens
s.pages = pages
if (s.currentPreviewItem.name === oldname)
s.currentPreviewItem.name = newname
const saveAllChanged = async () => {
for (let screenName of changedScreens) {
const changedScreen = getExactComponent(screens, screenName)
await api.post(`/_builder/api/${s.appId}/screen`, changedScreen)
}
}
api
.patch(`/_builder/api/${s.appId}/screen`, {
oldname,
newname,
})
.then(() => saveAllChanged())
.then(() => {
_savePage(s)
})
return s
})
}
const savePage = store => async page => {
store.update(state => {
if (state.currentFrontEndType !== "page" || !state.currentPageName) {
@ -277,15 +237,6 @@ const removeStylesheet = store => stylesheet => {
})
}
const _savePage = async s => {
const page = s.pages[s.currentPageName]
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, {
page: { componentLibraries: s.pages.componentLibraries, ...page },
uiFunctions: s.currentPageFunctions,
screens: page._screens,
})
}
const setCurrentPage = store => pageName => {
store.update(state => {
const current_screens = state.pages[pageName]._screens
@ -304,9 +255,7 @@ const setCurrentPage = store => pageName => {
state.currentComponentInfo = safeProps
currentPage.props = safeProps
state.currentPreviewItem = state.pages[pageName]
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
regenerateCssForCurrentScreen(state)
for (let screen of state.screens) {
screen._css = generate_screen_css([screen.props])
@ -317,8 +266,6 @@ const setCurrentPage = store => pageName => {
})
}
// const getComponentDefinition = (components, name) => components.find(c => c.name === name)
/**
* @param {string} componentToAdd - name of the component to add to the application
* @param {string} presetName - name of the component preset if defined
@ -344,9 +291,7 @@ const addChildComponent = store => (componentToAdd, presetName) => {
return state
}
const component = componentToAdd.startsWith("##")
? getBuiltin(componentToAdd)
: state.components[componentToAdd]
const component = getComponentDefinition(state, componentToAdd)
const presetProps = presetName ? component.presets[presetName] : {}
@ -379,6 +324,7 @@ const addChildComponent = store => (componentToAdd, presetName) => {
/**
* @param {string} props - props to add, as child of current component
*/
const addTemplatedComponent = store => props => {
store.update(state => {
walkProps(props, p => {
@ -387,9 +333,7 @@ const addTemplatedComponent = store => props => {
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
props
)
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
regenerateCssForCurrentScreen(state)
setCurrentPageFunctions(state)
_saveCurrentPreviewItem(state)
@ -400,12 +344,7 @@ const addTemplatedComponent = store => props => {
const selectComponent = store => component => {
store.update(state => {
const componentDef = component._component.startsWith("##")
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
return _selectComponent(state, component)
})
}
@ -421,6 +360,18 @@ const setComponentProp = store => (name, value) => {
})
}
const setPageOrScreenProp = store => (name, value) => {
store.update(state => {
if (name === "name" && state.currentFrontEndType === "screen") {
state = renameCurrentScreen(value, state)
} else {
state.currentPreviewItem[name] = value
_saveCurrentPreviewItem(state)
}
return state
})
}
const setComponentStyle = store => (type, name, value) => {
store.update(state => {
if (!state.currentComponentInfo._styles) {
@ -428,9 +379,7 @@ const setComponentStyle = store => (type, name, value) => {
}
state.currentComponentInfo._styles[type][name] = value
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
regenerateCssForCurrentScreen(state)
// save without messing with the store
_saveCurrentPreviewItem(state)
@ -472,75 +421,6 @@ const setScreenType = store => type => {
})
}
const deleteComponent = store => componentName => {
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, componentName)
if (parent) {
parent._children = parent._children.filter(
component => component !== componentName
)
}
_saveCurrentPreviewItem(state)
return state
})
}
const moveUpComponent = store => component => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === 0) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
_saveCurrentPreviewItem(s)
return s
})
}
const moveDownComponent = store => component => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === parent._children.length - 1) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
_saveCurrentPreviewItem(s)
return s
})
}
const copyComponent = store => component => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
const copiedComponent = cloneDeep(component)
walkProps(copiedComponent, p => {
p._id = uuid()
})
parent._children = [...parent._children, copiedComponent]
s.curren
_saveCurrentPreviewItem(s)
s.currentComponentInfo = copiedComponent
return s
})
}
const getPathToComponent = store => component => {
// Gets all the components to needed to construct a path.
const tempStore = get(store)
@ -572,39 +452,9 @@ const getPathToComponent = store => component => {
return path
}
const getParent = (rootProps, child) => {
let parent
walkProps(rootProps, (p, breakWalk) => {
if (p._children && p._children.includes(child)) {
parent = p
breakWalk()
}
})
return parent
}
const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false }
action(props, () => {
cancelToken.cancelled = true
})
if (props._children) {
for (let child of props._children) {
if (cancelToken.cancelled) return
walkProps(child, action, cancelToken)
}
}
}
const setMetadataProp = store => (name, prop) => {
store.update(s => {
s.currentPreviewItem[name] = prop
return s
})
}
const _saveCurrentPreviewItem = s =>
s.currentFrontEndType === "page"
? _savePage(s)
: _saveScreenApi(s.currentPreviewItem, s)

View File

@ -0,0 +1,95 @@
import mustache from "mustache"
import blockDefinitions from "components/workflow/WorkflowPanel/blockDefinitions"
import { generate } from "shortid"
/**
* Class responsible for the traversing of the workflow definition.
* Workflow definitions are stored in linked lists.
*/
export default class Workflow {
constructor(workflow) {
this.workflow = workflow
}
hasTrigger() {
return this.workflow.definition.trigger
}
addBlock(block) {
// Make sure to add trigger if doesn't exist
if (!this.hasTrigger() && block.type === "TRIGGER") {
this.workflow.definition.trigger = { id: generate(), ...block }
return
}
this.workflow.definition.steps.push({
id: generate(),
...block,
})
}
updateBlock(updatedBlock, id) {
const { steps, trigger } = this.workflow.definition
if (trigger && trigger.id === id) {
this.workflow.definition.trigger = null
return
}
const stepIdx = steps.findIndex(step => step.id === id)
if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1, updatedBlock)
}
deleteBlock(id) {
const { steps, trigger } = this.workflow.definition
if (trigger && trigger.id === id) {
this.workflow.definition.trigger = null
return
}
const stepIdx = steps.findIndex(step => step.id === id)
if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1)
}
createUiTree() {
if (!this.workflow.definition) return []
return Workflow.buildUiTree(this.workflow.definition)
}
static buildUiTree(definition) {
const steps = []
if (definition.trigger) steps.push(definition.trigger)
return [...steps, ...definition.steps].map(step => {
// The client side display definition for the block
const definition = blockDefinitions[step.type][step.actionId]
if (!definition) {
throw new Error(
`No block definition exists for the chosen block. Check there's an entry in the block definitions for ${step.actionId}`
)
}
if (!definition.params) {
throw new Error(
`Blocks should always have parameters. Ensure that the block definition is correct for ${step.actionId}`
)
}
const tagline = definition.tagline || ""
const args = step.args || {}
return {
id: step.id,
type: step.type,
params: step.params,
args,
heading: step.actionId,
body: mustache.render(tagline, args),
name: definition.name,
}
})
}
}

View File

@ -0,0 +1,106 @@
import { writable } from "svelte/store"
import api from "../../api"
import Workflow from "./Workflow"
const workflowActions = store => ({
fetch: async instanceId => {
const WORKFLOWS_URL = `/api/${instanceId}/workflows`
const workflowResponse = await api.get(WORKFLOWS_URL)
const json = await workflowResponse.json()
store.update(state => {
state.workflows = json
return state
})
},
create: async ({ instanceId, name }) => {
const workflow = {
name,
definition: {
steps: [],
},
}
const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
const json = await response.json()
store.update(state => {
state.workflows = state.workflows.concat(json.workflow)
state.currentWorkflow = new Workflow(json.workflow)
return state
})
},
save: async ({ instanceId, workflow }) => {
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
const json = await response.json()
store.update(state => {
const existingIdx = state.workflows.findIndex(
existing => existing._id === workflow._id
)
state.workflows.splice(existingIdx, 1, json.workflow)
state.workflows = state.workflows
state.currentWorkflow = new Workflow(json.workflow)
return state
})
},
update: async ({ instanceId, workflow }) => {
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
const json = await response.json()
store.update(state => {
const existingIdx = state.workflows.findIndex(
existing => existing._id === workflow._id
)
state.workflows.splice(existingIdx, 1, json.workflow)
state.workflows = state.workflows
return state
})
},
delete: async ({ instanceId, workflow }) => {
const { _id, _rev } = workflow
const DELETE_WORKFLOW_URL = `/api/${instanceId}/workflows/${_id}/${_rev}`
await api.delete(DELETE_WORKFLOW_URL)
store.update(state => {
const existingIdx = state.workflows.findIndex(
existing => existing._id === _id
)
state.workflows.splice(existingIdx, 1)
state.workflows = state.workflows
state.currentWorkflow = null
return state
})
},
select: workflow => {
store.update(state => {
state.currentWorkflow = new Workflow(workflow)
state.selectedWorkflowBlock = null
return state
})
},
addBlockToWorkflow: block => {
store.update(state => {
state.currentWorkflow.addBlock(block)
state.selectedWorkflowBlock = block
return state
})
},
deleteWorkflowBlock: block => {
store.update(state => {
state.currentWorkflow.deleteBlock(block.id)
state.selectedWorkflowBlock = null
return state
})
},
})
export const getWorkflowStore = () => {
const INITIAL_WORKFLOW_STATE = {
workflows: [],
}
const store = writable(INITIAL_WORKFLOW_STATE)
store.actions = workflowActions(store)
return store
}

View File

@ -0,0 +1,57 @@
import Workflow from "../Workflow";
import TEST_WORKFLOW from "./testWorkflow";
const TEST_BLOCK = {
id: "VFWeZcIPx",
name: "Update UI State",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
icon: "ri-refresh-line",
description: "Update your User Interface with some data.",
environment: "CLIENT",
params: {
path: "string",
value: "longText",
},
args: {
path: "foo",
value: "started...",
},
actionId: "SET_STATE",
type: "ACTION",
}
describe("Workflow Data Object", () => {
let workflow
beforeEach(() => {
workflow = new Workflow({ ...TEST_WORKFLOW });
});
it("adds a workflow block to the workflow", () => {
workflow.addBlock(TEST_BLOCK);
expect(workflow.workflow.definition)
})
it("updates a workflow block with new attributes", () => {
const firstBlock = workflow.workflow.definition.steps[0];
const updatedBlock = {
...firstBlock,
name: "UPDATED"
};
workflow.updateBlock(updatedBlock, firstBlock.id);
expect(workflow.workflow.definition.steps[0]).toEqual(updatedBlock)
})
it("deletes a workflow block successfully", () => {
const { steps } = workflow.workflow.definition
const originalLength = steps.length
const lastBlock = steps[steps.length - 1];
workflow.deleteBlock(lastBlock.id);
expect(workflow.workflow.definition.steps.length).toBeLessThan(originalLength);
})
it("builds a tree that gets rendered in the flowchart builder", () => {
expect(Workflow.buildUiTree(TEST_WORKFLOW.definition)).toMatchSnapshot();
})
})

View File

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Workflow Data Object builds a tree that gets rendered in the flowchart builder 1`] = `
Array [
Object {
"args": Object {
"time": 3000,
},
"body": "Delay for <b>3000</b> milliseconds",
"heading": "DELAY",
"id": "zJQcZUgDS",
"name": "Delay",
"params": Object {
"time": "number",
},
"type": "LOGIC",
},
Object {
"args": Object {
"path": "foo",
"value": "finished",
},
"body": "Update <b>foo</b> to <b>finished</b>",
"heading": "SET_STATE",
"id": "3RSTO7BMB",
"name": "Update UI State",
"params": Object {
"path": "string",
"value": "longText",
},
"type": "ACTION",
},
Object {
"args": Object {
"path": "foo",
"value": "started...",
},
"body": "Update <b>foo</b> to <b>started...</b>",
"heading": "SET_STATE",
"id": "VFWeZcIPx",
"name": "Update UI State",
"params": Object {
"path": "string",
"value": "longText",
},
"type": "ACTION",
},
]
`;

View File

@ -0,0 +1,63 @@
export default {
_id: "53b6148c65d1429c987e046852d11611",
_rev: "4-02c6659734934895812fa7be0215ee59",
name: "Test Workflow",
definition: {
steps: [
{
id: "VFWeZcIPx",
name: "Update UI State",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
icon: "ri-refresh-line",
description: "Update your User Interface with some data.",
environment: "CLIENT",
params: {
path: "string",
value: "longText",
},
args: {
path: "foo",
value: "started...",
},
actionId: "SET_STATE",
type: "ACTION",
},
{
id: "zJQcZUgDS",
name: "Delay",
icon: "ri-time-fill",
tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Delay the workflow until an amount of time has passed.",
environment: "CLIENT",
params: {
time: "number",
},
args: {
time: 3000,
},
actionId: "DELAY",
type: "LOGIC",
},
{
id: "3RSTO7BMB",
name: "Update UI State",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
icon: "ri-refresh-line",
description: "Update your User Interface with some data.",
environment: "CLIENT",
params: {
path: "string",
value: "longText",
},
args: {
path: "foo",
value: "finished",
},
actionId: "SET_STATE",
type: "ACTION",
},
],
},
type: "workflow",
live: true,
}

View File

@ -0,0 +1,80 @@
import { makePropsSafe } from "components/userInterface/pagesParsing/createProps"
import api from "./api"
import { generate_screen_css } from "./generate_css"
export const selectComponent = (state, component) => {
const componentDef = component._component.startsWith("##")
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
}
export const getParent = (rootProps, child) => {
let parent
walkProps(rootProps, (p, breakWalk) => {
if (
p._children &&
(p._children.includes(child) || p._children.some(c => c._id === child))
) {
parent = p
breakWalk()
}
})
return parent
}
export const saveCurrentPreviewItem = s =>
s.currentFrontEndType === "page"
? savePage(s)
: saveScreenApi(s.currentPreviewItem, s)
export const savePage = async s => {
const page = s.pages[s.currentPageName]
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, {
page: { componentLibraries: s.pages.componentLibraries, ...page },
uiFunctions: s.currentPageFunctions,
screens: page._screens,
})
}
export const saveScreenApi = (screen, s) => {
api
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
.then(() => savePage(s))
}
export const renameCurrentScreen = (newname, state) => {
const oldname = state.currentPreviewItem.name
state.currentPreviewItem.name = newname
api.patch(
`/_builder/api/${state.appId}/pages/${state.currentPageName}/screen`,
{
oldname,
newname,
}
)
return state
}
export const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false }
action(props, () => {
cancelToken.cancelled = true
})
if (props._children) {
for (let child of props._children) {
if (cancelToken.cancelled) return
walkProps(child, action, cancelToken)
}
}
}
export const regenerateCssForCurrentScreen = state => {
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
return state
}

View File

@ -1,6 +1,7 @@
<script>
export let disabled = false
export let hidden = false
export let secondary = false
export let primary = true
export let cancel = false
export let alert = false
@ -11,6 +12,7 @@
on:click
class="button"
class:hidden
class:secondary
class:primary
class:alert
class:cancel
@ -22,12 +24,14 @@
<style>
.primary {
color: #ffffff;
background: #0055ff;
background: var(--blue);
border: solid 1px var(--blue);
}
.alert {
color: rgba(255, 0, 31, 1);
background: rgba(255, 0, 31, 0.1);
color: white;
background: #e26d69;
border: solid 1px #e26d69;
}
.cancel {
@ -35,18 +39,22 @@
background: none;
}
.secondary {
color: var(--ink);
border: solid 1px var(--grey-dark);
background: white;
}
.button {
font-size: 14px;
font-weight: 600;
border-radius: 5px;
border: none;
font-weight: 500;
border-radius: 3px;
padding: 10px 20px;
height: 45px;
height: 40px;
}
.button:hover {
cursor: pointer;
font-weight: 600;
filter: saturate(90%);
}

View File

@ -71,7 +71,7 @@
margin-left: 20px;
}
:global(.refresh-page-button):hover{
:global(.refresh-page-button):hover {
background: var(--grey-light);
}
</style>

View File

@ -14,8 +14,7 @@
background: var(--secondary80);
color: var(--white);
font-family: "Courier New", Courier, monospace;
width: 95%;
height: 100px;
height: 200px;
border-radius: 5px;
}
</style>

View File

@ -0,0 +1,12 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6
12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21
12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5
1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z" />
</svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@ -31,3 +31,4 @@ export { default as EmailIcon } from "./Email.svelte"
export { default as TwitterIcon } from "./Twitter.svelte"
export { default as InfoIcon } from "./Info.svelte"
export { default as CloseIcon } from "./Close.svelte"
export { default as MoreIcon } from "./More.svelte"

View File

@ -1,30 +1,61 @@
<script>
import { onMount } from "svelte"
import { buildStyle } from "../../helpers.js"
export let value = ""
export let width = ""
export let textAlign = "left"
export let width = "160px"
export let placeholder = ""
export let suffix = ""
export let onChange = val => {}
let style = { width }
let centerPlaceholder = textAlign === "center"
let style = buildStyle({ width, textAlign })
function handleChange(val) {
value = val
let _value = value !== "auto" ? value + suffix : value
onChange(_value)
}
$: displayValue =
suffix && value && value.endsWith(suffix)
? value.replace(new RegExp(`${suffix}$`), "")
: value || ""
</script>
<input type="text" style={`width: ${width};`} on:change bind:value />
<input
class:centerPlaceholder
type="text"
value={displayValue}
{placeholder}
{style}
on:change={e => handleChange(e.target.value)} />
<style>
input {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--ink);
line-height: 1.3;
padding: 12px;
width: 164px;
float: right;
max-width: 100%;
box-sizing: border-box;
margin: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background: #fff;
border: 1px solid var(--grey-dark);
/* width: 32px; */
height: 32px;
font-size: 12px;
font-weight: 700;
margin: 0px 0px 0px 1px;
color: var(--ink);
opacity: 0.7;
padding: 0px 4px;
line-height: 1.3;
/* padding: 12px; */
width: 164px;
box-sizing: border-box;
border: 1px solid var(--grey);
border-radius: 2px;
outline: none;
}
input::placeholder {
text-align: left;
}
.centerPlaceholder::placeholder {
text-align: center;
}
</style>

View File

@ -1,74 +1,50 @@
<script>
import { onMount } from "svelte"
import Input from "../Input.svelte"
export let meta = []
export let label = ""
export let value = [0, 0, 0, 0]
export let type = "number"
export let value = ["0", "0", "0", "0"]
export let suffix = ""
export let onChange = () => {}
function handleChange(val, idx) {
value.splice(idx, 1, val)
value.splice(idx, 1, val !== "auto" && suffix ? val + suffix : val)
value = value
onChange(value)
let _value = value.map(v =>
suffix && !v.endsWith(suffix) && v !== "auto" ? v + suffix : v
)
onChange(_value)
}
$: displayValues =
value && suffix
? value.map(v => v.replace(new RegExp(`${suffix}$`), ""))
: value || []
</script>
<div class="input-container">
<div class="label">{label}</div>
<div class="inputs">
{#each meta as { placeholder }, i}
<input
{type}
placeholder={placeholder || ''}
value={!value || value[i] === 0 ? '' : value[i]}
on:change={e => handleChange(e.target.value || 0, i)} />
<div class="inputs-group">
{#each meta as m, i}
<Input
width="37px"
textAlign="center"
placeholder={m.placeholder || ''}
value={!displayValues || displayValues[i] === '0' ? '' : displayValues[i]}
onChange={value => handleChange(value || 0, i)} />
{/each}
</div>
</div>
<style>
.input-container {
}
.label {
flex: 0;
}
.inputs {
.inputs-group {
flex: 1;
}
input {
width: 40px;
height: 32px;
font-size: 12px;
font-weight: 700;
margin: 0px 0px 0px 1px;
text-align: center;
color: var(--ink);
opacity: 0.7;
padding: 0px 4px;
box-sizing: border-box;
border: 1px solid var(--grey);
border-radius: 2px;
outline: none;
float: right;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
input::placeholder {
text-align: center;
}
</style>

View File

@ -50,10 +50,10 @@
<style>
.uk-modal-dialog {
border-radius: 0.3rem;
width: 60%;
width: 520px;
height: 80vh;
display: flex;
flex-direction: column;
padding: 0;
padding: 40px;
}
</style>

View File

@ -20,11 +20,8 @@
<style>
.select-container {
font-size: 14px;
color: var(--secondary60);
font-weight: bold;
position: relative;
max-width: 400px;
min-width: 275px;
border: var(--grey-dark) 1px solid;
}
.adjusted {
@ -43,7 +40,7 @@
font-family: sans-serif;
font-weight: 400;
font-size: 14px;
color: #000333;
color: var(--ink);
padding: 0 40px 0px 20px;
width: 100%;
max-width: 100%;
@ -63,6 +60,6 @@
width: 30px;
height: 30px;
pointer-events: none;
color: var(--secondary100);
color: var(--ink);
}
</style>

View File

@ -0,0 +1,78 @@
<script>
export let tabs = []
export const selectTab = tabName => {
selected = tabName
selectedIndex = tabs.indexOf(selected)
}
let selected = tabs.length > 0 && tabs[0]
let selectedIndex = 0
const isSelected = tab => selected === tab
</script>
<div class="root">
<div class="switcher">
{#each tabs as tab}
<button class:selected={selected === tab} on:click={() => selectTab(tab)}>
{tab}
</button>
{/each}
</div>
<div class="panel">
{#if selectedIndex === 0}
<slot name="0" />
{:else if selectedIndex === 1}
<slot name="1" />
{:else if selectedIndex === 2}
<slot name="2" />
{:else if selectedIndex === 3}
<slot name="3" />
{/if}
</div>
</div>
<style>
.root {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px 20px;
border-left: solid 1px var(--grey);
box-sizing: border-box;
}
.switcher {
display: flex;
margin: 0px 20px 20px 0px;
box-sizing: border-box;
}
.switcher > button {
display: inline-block;
border: none;
margin: 0;
padding: 0;
cursor: pointer;
font-size: 18px;
font-weight: 700;
color: var(--ink-lighter);
margin-right: 20px;
background: none;
}
.switcher > .selected {
color: var(--ink);
}
.panel {
min-height: 0;
height: 100%;
overflow-y: auto;
}
</style>

View File

@ -1,31 +0,0 @@
import { isString } from "lodash/fp"
import {
BB_STATE_BINDINGPATH,
BB_STATE_FALLBACK,
BB_STATE_BINDINGSOURCE,
isBound,
parseBinding,
} from "@budibase/client/src/state/parseBinding"
export const isBinding = isBound
export const setBinding = ({ path, fallback, source }, binding = {}) => {
if (isNonEmptyString(path)) binding[BB_STATE_BINDINGPATH] = path
if (isNonEmptyString(fallback)) binding[BB_STATE_FALLBACK] = fallback
binding[BB_STATE_BINDINGSOURCE] = source || "store"
return binding
}
export const getBinding = val => {
const binding = parseBinding(val)
return binding
? binding
: {
path: "",
source: "store",
fallback: "",
}
}
const isNonEmptyString = s => isString(s) && s.length > 0

View File

@ -1,13 +1,8 @@
import { eventHandlers } from "../../../../client/src/state/eventHandlers"
import { writable } from "svelte/store"
export { EVENT_TYPE_MEMBER_NAME } from "../../../../client/src/state/eventHandlers"
export const allHandlers = user => {
const store = writable({
_bbuser: user,
})
const handlersObj = eventHandlers(store)
export const allHandlers = () => {
const handlersObj = eventHandlers()
const handlers = Object.keys(handlersObj).map(name => ({
name,

View File

@ -43,13 +43,6 @@
)
}
async function selectRecord(record) {
return await api.loadRecord(record.key, {
appname: $store.appname,
instanceId: $backendUiStore.selectedDatabase._id,
})
}
const ITEMS_PER_PAGE = 10
// Internal headers we want to hide from the user
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
@ -152,19 +145,19 @@
}
table {
border: 1px solid #ccc;
border: 1px solid var(--grey-dark);
background: #fff;
border-radius: 3px;
border-collapse: collapse;
}
thead {
background: #f9f9f9;
border: 1px solid #ccc;
background: var(--blue-light);
border: 1px solid var(--grey-dark);
}
thead th {
color: var(--button-text);
color: var(--ink);
text-transform: capitalize;
font-weight: 500;
font-size: 14px;
@ -173,14 +166,14 @@
}
tbody tr {
border-bottom: 1px solid #ccc;
border-bottom: 1px solid var(--grey-dark);
transition: 0.3s background-color;
color: var(--secondary100);
color: var(--ink);
font-size: 14px;
}
tbody tr:hover {
background: #fafafa;
background: var(--grey-light);
}
.table-controls {

View File

@ -1,6 +1,6 @@
import api from "builderStore/api"
export async function createUser(user, appId, instanceId) {
export async function createUser(user, instanceId) {
const CREATE_USER_URL = `/api/${instanceId}/users`
const response = await api.post(CREATE_USER_URL, user)
return await response.json()
@ -28,7 +28,7 @@ export async function saveRecord(record, instanceId, modelId) {
}
export async function fetchDataForView(viewName, instanceId) {
const FETCH_RECORDS_URL = `/api/${instanceId}/${viewName}/records`
const FETCH_RECORDS_URL = `/api/${instanceId}/views/${viewName}`
const response = await api.get(FETCH_RECORDS_URL)
return await response.json()

View File

@ -35,7 +35,7 @@
}
</script>
<heading>
<div class="heading">
{#if !showFieldView}
<i class="ri-list-settings-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit Model</h3>
@ -43,22 +43,20 @@
<i class="ri-file-list-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit Field</h3>
{/if}
</heading>
</div>
{#if !showFieldView}
<div class="padding">
<h4 class="budibase__label--big">Settings</h4>
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
<Textbox label="Name" bind:text={model.name} />
<div class="textbox">
<Textbox label="Name" bind:text={model.name} />
</div>
<div class="table-controls">
<span class="budibase__label--big">Fields</span>
<h4 class="hoverable new-field" on:click={() => (showFieldView = true)}>
<span class="label">Fields</span>
<div class="hoverable new-field" on:click={() => (showFieldView = true)}>
Add new field
</h4>
</div>
</div>
<table class="uk-table fields-table budibase__table">
@ -67,7 +65,6 @@
<th>Edit</th>
<th>Name</th>
<th>Type</th>
<th>Values</th>
<th />
</tr>
</thead>
@ -90,9 +87,9 @@
{/each}
</tbody>
</table>
<div class="uk-margin">
<footer>
<ActionButton color="secondary" on:click={saveModel}>Save</ActionButton>
</div>
</footer>
</div>
{:else}
<FieldView
@ -104,41 +101,63 @@
<style>
.padding {
padding: 20px;
padding-top: 40px;
}
.label {
font-size: 14px;
font-weight: 500;
}
.textbox {
margin: 0px 40px 0px 40px;
font-size: 14px;
font-weight: 500;
}
.new-field {
font-size: 16px;
font-weight: bold;
color: var(--button-text);
color: var(--blue);
}
.fields-table {
margin: 1rem 1rem 0rem 0rem;
margin: 8px 40px 0px 40px;
border-collapse: collapse;
width: 88%;
}
tbody > tr:hover {
background-color: var(--primary10);
background-color: var(--grey-light);
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0px 40px;
}
.ri-more-line:hover {
cursor: pointer;
}
heading {
padding: 20px 20px 0 20px;
.heading {
padding: 40px 40px 0 40px;
display: flex;
align-items: center;
}
h3 {
margin: 0 0 0 10px;
color: var(--ink);
}
footer {
background-color: var(--grey-light);
margin-top: 40px;
padding: 20px 40px 20px 40px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -13,17 +13,41 @@
const FIELD_TYPES = ["string", "number", "boolean"]
export let field = { type: "string" }
export let field = {
type: "string",
constraints: { type: "string", presence: false },
}
export let schema
export let goBack
let errors = []
let draftField = cloneDeep(field)
let type = field.type
let constraints = field.constraints
let required =
field.constraints.presence && !field.constraints.presence.allowEmpty
const save = () => {
constraints.presence = required ? { allowEmpty: false } : false
draftField.constraints = constraints
draftField.type = type
schema[field.name] = draftField
goBack()
}
$: constraints =
type === "string"
? { type: "string", length: {}, presence: false }
: type === "number"
? { type: "number", presence: false, numericality: {} }
: type === "boolean"
? { type: "boolean", presence: false }
: type === "datetime"
? { type: "date", datetime: {}, presence: false }
: type.startsWith("array")
? { type: "array", presence: false }
: { type: "string", presence: false }
</script>
<div class="root">
@ -32,49 +56,51 @@
<form on:submit|preventDefault class="uk-form-stacked">
<Textbox label="Name" bind:text={field.name} />
<Dropdown
label="Type"
bind:selected={draftField.type}
options={FIELD_TYPES} />
<Dropdown label="Type" bind:selected={type} options={FIELD_TYPES} />
{#if field.type === 'string'}
<NumberBox label="Max Length" bind:value={draftField.maxLength} />
<ValuesList label="Categories" bind:values={draftField.values} />
{:else if field.type === 'boolean'}
<!-- TODO: revisit and fix with JSON schema -->
<Checkbox label="Allow Null" bind:checked={draftField.allowNulls} />
{:else if field.format === 'datetime'}
<!-- TODO: revisit and fix with JSON schema -->
<DatePicker label="Min Value" bind:value={draftField.minValue} />
<DatePicker label="Max Value" bind:value={draftField.maxValue} />
{:else if field.type === 'number'}
<NumberBox label="Min Value" bind:value={draftField.minimum} />
<NumberBox label="Max Value" bind:value={draftField.maximum} />
{:else if draftField.type.startsWith('array')}
<!-- TODO: revisit and fix with JSON schema -->
<Checkbox label="Required" bind:checked={required} />
{#if type === 'string'}
<NumberBox label="Max Length" bind:value={constraints.length.maximum} />
<ValuesList label="Categories" bind:values={constraints.inclusion} />
{:else if type === 'datetime'}
<DatePicker
label="Min Value"
bind:value={constraints.datetime.earliest} />
<DatePicker label="Max Value" bind:value={constraints.datetime.latest} />
{:else if type === 'number'}
<NumberBox
label="Min Length"
bind:value={draftField.typeOptions.minLength} />
label="Min Value"
bind:value={constraints.numericality.greaterThanOrEqualTo} />
<NumberBox
label="Max Length"
bind:value={draftField.typeOptions.maxLength} />
label="Max Value"
bind:value={constraints.numericality.lessThanOrEqualTo} />
{/if}
</form>
</div>
<footer>
<div class="button">
<ActionButton secondary on:click={goBack}>Cancel</ActionButton>
</div>
<ActionButton primary on:click={save}>Save</ActionButton>
<ActionButton alert on:click={goBack}>Cancel</ActionButton>
</footer>
<style>
.root {
margin: 20px;
margin: 40px;
}
footer {
padding: 20px;
padding: 20px 40px;
border-radius: 0 0 5px 5px;
bottom: 0;
left: 0;
background: #fafafa;
background: var(--grey-light);
display: flex;
align-items: center;
justify-content: flex-end;
}
.button {
margin-right: 20px;
}
</style>

View File

@ -8,10 +8,6 @@
import * as api from "../api"
import ErrorsBox from "components/common/ErrorsBox.svelte"
const CLASS_NAME_MAP = {
boolean: "uk-checkbox",
}
export let record = {}
export let onClosed
@ -28,14 +24,25 @@
onClosed()
}
const isSelect = meta =>
meta.type === "string" &&
meta.constraints &&
meta.constraints.inclusion &&
meta.constraints.inclusion.length > 0
function determineInputType(meta) {
if (meta.type === "datetime") return "date"
if (meta.type === "number") return "number"
if (meta.type === "boolean") return "checkbox"
if (isSelect(meta)) return "select"
return "text"
}
function determineOptions(meta) {
return isSelect(meta) ? meta.constraints.inclusion : []
}
async function saveRecord() {
const recordResponse = await api.saveRecord(
{
@ -46,7 +53,9 @@
$backendUiStore.selectedModel._id
)
if (recordResponse.errors) {
errors = recordResponse.errors
errors = Object.keys(recordResponse.errors)
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
.flat()
return
}
@ -65,8 +74,8 @@
{#each modelSchema as [key, meta]}
<div class="uk-margin">
<RecordFieldControl
className={CLASS_NAME_MAP[meta.type]}
type={determineInputType(meta)}
options={determineOptions(meta)}
label={key}
bind:value={record[key]} />
</div>

View File

@ -39,51 +39,49 @@
}
</script>
<heading>
<div class="header">
<i class="ri-eye-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit View</h3>
</heading>
</div>
<form on:submit|preventDefault class="uk-form-stacked root">
<h4 class="budibase__label--big">Settings</h4>
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-2@s">
<Textbox bind:text={view.name} label="Name" />
<div class="main">
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-2@s">
<Textbox bind:text={view.name} label="Name" />
</div>
</div>
<div class="code-snippets">
{#each Object.values(SNIPPET_EDITORS) as snippetType}
<span
class="snippet-selector__heading hoverable"
class:highlighted={currentSnippetEditor === snippetType}
on:click={() => (currentSnippetEditor = snippetType)}>
{snippetType}
</span>
{/each}
{#if currentSnippetEditor === SNIPPET_EDITORS.MAP}
<CodeArea bind:text={view.map} label="Map" />
{:else if currentSnippetEditor === SNIPPET_EDITORS.FILTER}
<CodeArea bind:text={view.filter} label="Filter" />
{:else if currentSnippetEditor === SNIPPET_EDITORS.REDUCE}
<CodeArea bind:text={view.reduce} label="Reduce" />
{/if}
</div>
</div>
<h4 class="budibase__label--big">Snippets</h4>
{#each Object.values(SNIPPET_EDITORS) as snippetType}
<span
class="snippet-selector__heading hoverable"
class:highlighted={currentSnippetEditor === snippetType}
on:click={() => (currentSnippetEditor = snippetType)}>
{snippetType}
</span>
{/each}
{#if currentSnippetEditor === SNIPPET_EDITORS.MAP}
<CodeArea bind:text={view.map} label="Map" />
{:else if currentSnippetEditor === SNIPPET_EDITORS.FILTER}
<CodeArea bind:text={view.filter} label="Filter" />
{:else if currentSnippetEditor === SNIPPET_EDITORS.REDUCE}
<CodeArea bind:text={view.reduce} label="Reduce" />
{/if}
<ActionButton color="secondary" on:click={saveView}>Save</ActionButton>
<ActionButton alert on:click={deleteView}>Delete</ActionButton>
<div class="buttons">
<div class="button">
<ActionButton secondary on:click={deleteView}>Delete</ActionButton>
</div>
<ActionButton color="secondary" on:click={saveView}>Save</ActionButton>
</div>
</form>
<style>
.root {
height: 100%;
padding: 15px;
}
.snippet-selector__heading {
margin-right: 20px;
opacity: 0.7;
}
.highlighted {
@ -92,11 +90,38 @@
h3 {
margin: 0 0 0 10px;
color: var(--ink);
}
heading {
padding: 20px 20px 0 20px;
.snippet-selector__heading {
margin-right: 20px;
font-size: 14px;
color: var(--ink-lighter);
}
.header {
padding: 20px 40px 0 40px;
display: flex;
align-items: center;
}
.main {
margin: 20px 40px 0px 40px;
}
.code-snippets {
margin: 20px 0px 20px 0px;
}
.buttons {
display: flex;
justify-content: flex-end;
background-color: var(--grey-light);
margin: 0 40px;
padding: 20px 0;
}
.button {
margin-right: 20px;
}
</style>

View File

@ -7,21 +7,26 @@
let username
let password
let accessLevelId
$: valid = username && password
$: valid = username && password && accessLevelId
$: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId
async function createUser() {
const user = { name: username, username, password }
const response = await api.createUser(user, appId, instanceId)
const user = { name: username, username, password, accessLevelId }
const response = await api.createUser(user, instanceId)
backendUiStore.actions.users.create(response)
onClosed()
}
</script>
<form on:submit|preventDefault class="uk-form-stacked">
<div>
<div class="main">
<div class="heading">
<i class="ri-list-settings-line button--toggled" />
<div class="title">Create User</div>
</div>
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Username</label>
<input class="uk-input" type="text" bind:value={username} />
@ -30,20 +35,50 @@
<label class="uk-form-label" for="form-stacked-text">Password</label>
<input class="uk-input" type="password" bind:value={password} />
</div>
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Access Level</label>
<select class="uk-select" bind:value={accessLevelId}>
<option value="" />
<option value="POWER_USER">Power User</option>
<option value="ADMIN">Admin</option>
</select>
</div>
</div>
<footer>
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
<div class="button">
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
</div>
<ActionButton disabled={!valid} on:click={createUser}>Save</ActionButton>
</footer>
</form>
<style>
div {
padding: 30px;
.main {
padding: 40px 40px 20px 40px;
}
.title {
font-size: 24px;
font-weight: 700;
color: var(--ink);
margin-left: 12px;
}
.heading {
display: flex;
align-items: baseline;
}
footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 20px;
background: #fafafa;
border-radius: 0.5rem;
background: var(--grey-light);
border-radius: 0 0 5px 5px;
}
.button {
margin-right: 20px;
}
</style>

View File

@ -3,10 +3,16 @@
export let value = ""
export let label
export let errors = []
export let className = "uk-input"
export let options = []
let checked = type === "checkbox" ? value : false
const determineClassName = type => {
if (type === "checkbox") return "uk-checkbox"
if (type === "select") return "uk-select"
return "uk-input"
}
const handleInput = event => {
if (event.target.type === "checkbox") {
value = event.target.checked
@ -23,11 +29,23 @@
</script>
<label>{label}</label>
<input
class={className}
class:uk-form-danger={errors.length > 0}
{checked}
{type}
{value}
on:input={handleInput}
on:change={handleInput} />
{#if type === 'select'}
<select
class={determineClassName(type)}
bind:value
class:uk-form-danger={errors.length > 0}>
{#each options as opt}
<option value={opt}>{opt}</option>
{/each}
</select>
{:else}
<input
class={determineClassName(type)}
class:uk-form-danger={errors.length > 0}
{checked}
{type}
{value}
on:input={handleInput}
on:change={handleInput} />
{/if}

View File

@ -33,18 +33,7 @@
</script>
<div class="items-root">
<div class="hierarchy">
<div class="components-list-container">
<div class="nav-group-header">
<div class="hierarchy-title">Databases</div>
<i class="ri-add-line hoverable" on:click={openDatabaseCreator} />
</div>
</div>
<div class="hierarchy-items-container">
<DatabasesList />
</div>
</div>
<div class="hierarchy" />
{#if $backendUiStore.selectedDatabase._id}
<div class="hierarchy">
<div class="components-list-container">

View File

@ -46,7 +46,7 @@
function selectModel(model) {
backendUiStore.update(state => {
state.selectedModel = model
state.selectedView = `${model._id}`
state.selectedView = `all_${model._id}`
return state
})
}

View File

@ -10,7 +10,6 @@
<h3 class="app-title">{name}</h3>
<p class="app-desc">{description}</p>
<div class="card-footer">
<div class="modified-date">Last Edited - 25th May 2020</div>
<a href={`/_builder/${_id}`} class="app-button">Open Web App</a>
</div>
</div>
@ -18,7 +17,7 @@
<style>
.apps-card {
background-color: var(--white);
padding: 20px;
padding: 20px 20px 30px 20px;
max-width: 400px;
max-height: 150px;
border-radius: 5px;
@ -48,14 +47,13 @@
justify-content: space-between;
}
.modified-date {
font-size: 14px;
color: var(--ink-light);
}
.app-button {
align-items: center;
display: flex;
background-color: var(--white);
color: var(--ink);
width: 100%;
justify-content: center;
padding: 12px 20px;
border-radius: 5px;
border: 1px var(--grey) solid;

View File

@ -12,7 +12,6 @@
<div class="inner">
<div>
<div>
<div class="app-section-title">Your Web Apps</div>
<div class="apps">
{#each apps as app}
<AppCard {...app} />
@ -26,18 +25,12 @@
<style>
.apps {
display: grid;
grid-template-columns: repeat(auto-fill, 400px);
grid-gap: 40px 85px;
grid-template-columns: repeat(auto-fill, 380px);
grid-gap: 20px 40px;
justify-content: start;
}
.root {
margin: 40px 80px;
}
.app-section-title {
font-size: 20px;
color: var(--ink);
font-weight: 700;
margin-bottom: 20px;
.root {
margin: 20px 80px;
}
</style>

View File

@ -5,6 +5,7 @@
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { fade } from "svelte/transition"
import { post } from "builderStore/api"
const { open, close } = getContext("simple-modal")
@ -33,15 +34,7 @@
const data = { name, description }
loading = true
try {
const response = await fetch("/api/applications", {
method: "POST", // *GET, POST, PUT, DELETE, etc.
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
// 'Content-Type': 'application/x-www-form-urlencoded',
},
body: JSON.stringify(data), // body data type must match "Content-Type" header
})
const response = await post("/api/applications", data)
const res = await response.json()

View File

@ -21,14 +21,47 @@
return componentName || "element"
}
$: iframe &&
console.log(
iframe.contentDocument.head.insertAdjacentHTML(
"beforeend",
`<\style></style>`
)
)
const screenPlaceholder = {
name: "Screen Placeholder",
route: "*",
props: {
_component: "@budibase/standard-components/container",
type: "div",
_children: [
{
_component: "@budibase/standard-components/container",
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
_id: "__screenslot__text",
_code: "",
className: "",
onLoad: [],
type: "div",
_children: [
{
_component: "@budibase/standard-components/text",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_id: "__screenslot__text_2",
_code: "",
text: "content",
font: "",
color: "",
textAlign: "inline",
verticalAlign: "inline",
formattingTag: "none",
},
],
},
],
},
}
$: hasComponent = !!$store.currentPreviewItem
$: {
styles = ""
// Apply the CSS from the currently selected page and its screens
@ -52,49 +85,12 @@
$: frontendDefinition = {
appId: $store.appId,
libraries: $store.libraries,
page: $store.currentPreviewItem,
screens: screensExist
? $store.currentPreviewItem._screens
: [
{
name: "Screen Placeholder",
route: "*",
props: {
_component: "@budibase/standard-components/container",
type: "div",
_children: [
{
_component: "@budibase/standard-components/container",
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
_id: "__screenslot__text",
_code: "",
className: "",
onLoad: [],
type: "div",
_children: [
{
_component: "@budibase/standard-components/text",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_id: "__screenslot__text_2",
_code: "",
text: "content",
font: "",
color: "",
textAlign: "inline",
verticalAlign: "inline",
formattingTag: "none",
},
],
},
],
},
},
],
page: $store.pages[$store.currentPageName],
screens: [
$store.currentFrontEndType === "page"
? screenPlaceholder
: $store.currentPreviewItem,
],
appRootPath: "",
}
@ -103,6 +99,27 @@
$: selectedComponentId = $store.currentComponentInfo
? $store.currentComponentInfo._id
: ""
const refreshContent = () => {
iframe.contentWindow.postMessage(
JSON.stringify({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
})
)
}
$: if (iframe)
iframe.contentWindow.addEventListener("bb-ready", refreshContent, {
once: true,
})
$: if (iframe && frontendDefinition) {
refreshContent()
}
</script>
<div class="component-container">
@ -111,14 +128,7 @@
style="height: 100%; width: 100%"
title="componentPreview"
bind:this={iframe}
srcdoc={iframeTemplate({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition: JSON.stringify(frontendDefinition),
currentPageFunctions: $store.currentPageFunctions,
})} />
srcdoc={iframeTemplate} />
{/if}
</div>

View File

@ -1,23 +1,9 @@
export default ({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
currentPageFunctions,
}) => `<html>
export default `<html>
<head>
${stylesheetLinks}
<style>
${styles || ""}
.${selectedComponentType}-${selectedComponentId} {
border: 2px solid #0055ff;
}
body, html {
height: 100%!important;
font-family: Roboto !important;
}
.lay-__screenslot__text {
width: 100%;
@ -35,13 +21,50 @@ export default ({
}
</style>
<script>
window["##BUDIBASE_FRONTEND_DEFINITION##"] = ${frontendDefinition};
window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = ${currentPageFunctions};
function receiveMessage(event) {
import('/_builder/budibase-client.esm.mjs')
.then(module => {
module.loadBudibase({ window, localStorage });
})
if (!event.data) return
const data = JSON.parse(event.data)
try {
if (styles) document.head.removeChild(styles)
} catch(_) { }
try {
if (selectedComponentStyle) document.head.removeChild(selectedComponentStyle)
} catch(_) { }
selectedComponentStyle = document.createElement('style');
document.head.appendChild(selectedComponentStyle)
var selectedCss = '.' + data.selectedComponentType + '-' + data.selectedComponentId + '{ border: 2px solid #0055ff; }'
selectedComponentStyle.appendChild(document.createTextNode(selectedCss))
styles = document.createElement('style')
document.head.appendChild(styles)
styles.appendChild(document.createTextNode(data.styles))
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
if (clientModule) {
clientModule.loadBudibase({ window, localStorage })
}
}
let clientModule
let styles
let selectedComponentStyle
document.addEventListener("click", function(e) {
e.preventDefault()
e.stopPropagation()
return false;
}, true)
import('/_builder/budibase-client.esm.mjs')
.then(module => {
clientModule = module
window.addEventListener('message', receiveMessage)
window.dispatchEvent(new Event('bb-ready'))
})
</script>
</head>
<body>

View File

@ -0,0 +1,277 @@
<script>
import { MoreIcon } from "components/common/Icons"
import { store } from "builderStore"
import { getComponentDefinition } from "builderStore/store"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last, cloneDeep } from "lodash/fp"
import UIkit from "uikit"
import {
selectComponent,
getParent,
walkProps,
saveCurrentPreviewItem,
regenerateCssForCurrentScreen,
} from "builderStore/storeUtils"
import { uuid } from "builderStore/uuid"
export let component
let confirmDeleteDialog
let dropdownEl
$: dropdown = UIkit.dropdown(dropdownEl, {
mode: "click",
offset: 0,
pos: "bottom-right",
"delay-hide": 0,
animation: false,
})
$: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false))
$: noChildrenAllowed =
!component ||
getComponentDefinition($store, component._component).children === false
$: noPaste = !$store.componentToPaste
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
const hideDropdown = () => {
dropdown.hide()
}
const moveUpComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === 0) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
saveCurrentPreviewItem(s)
return s
})
}
const moveDownComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === parent._children.length - 1) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
saveCurrentPreviewItem(s)
return s
})
}
const copyComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
const copiedComponent = cloneDeep(component)
walkProps(copiedComponent, p => {
p._id = uuid()
})
parent._children = [...parent._children, copiedComponent]
saveCurrentPreviewItem(s)
s.currentComponentInfo = copiedComponent
regenerateCssForCurrentScreen(s)
return s
})
}
const deleteComponent = () => {
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component)
if (parent) {
parent._children = parent._children.filter(c => c !== component)
}
saveCurrentPreviewItem(state)
return state
})
}
const generateNewIdsForComponent = c =>
walkProps(c, p => {
p._id = uuid()
})
const storeComponentForCopy = (cut = false) => {
store.update(s => {
const copiedComponent = cloneDeep(component)
s.componentToPaste = copiedComponent
if (cut) {
const parent = getParent(s.currentPreviewItem.props, component._id)
parent._children = parent._children.filter(c => c._id !== component._id)
selectComponent(s, parent)
}
return s
})
}
const pasteComponent = mode => {
store.update(s => {
if (!s.componentToPaste) return s
const componentToPaste = cloneDeep(s.componentToPaste)
generateNewIdsForComponent(componentToPaste)
delete componentToPaste._cutId
if (mode === "inside") {
component._children.push(componentToPaste)
return s
}
const parent = getParent(s.currentPreviewItem.props, component)
const targetIndex = parent._children.indexOf(component)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
regenerateCssForCurrentScreen(s)
saveCurrentPreviewItem(s)
selectComponent(s, componentToPaste)
return s
})
}
</script>
<div class="root boundary" on:click|stopPropagation={() => {}}>
<button>
<MoreIcon />
</button>
<ul class="menu" bind:this={dropdownEl} on:click={hideDropdown}>
<li class="item" on:click={() => confirmDeleteDialog.show()}>
<i class="icon ri-delete-bin-2-line" />
Delete
</li>
<li class="item" on:click={moveUpComponent}>
<i class="icon ri-arrow-up-line" />
Move up
</li>
<li class="item" on:click={moveDownComponent}>
<i class="icon ri-arrow-down-line" />
Move down
</li>
<li class="item" on:click={copyComponent}>
<i class="icon ri-repeat-one-line" />
Duplicate
</li>
<li class="item" on:click={() => storeComponentForCopy(true)}>
<i class="icon ri-scissors-cut-line" />
Cut
</li>
<li class="item" on:click={() => storeComponentForCopy(false)}>
<i class="icon ri-file-copy-line" />
Copy
</li>
<hr class="hr-style" />
<li
class="item"
class:disabled={noPaste}
on:click={() => pasteComponent('above')}>
<i class="icon ri-insert-row-top" />
Paste above
</li>
<li
class="item"
class:disabled={noPaste}
on:click={() => pasteComponent('below')}>
<i class="icon ri-insert-row-bottom" />
Paste below
</li>
<li
class="item"
class:disabled={noPaste || noChildrenAllowed}
on:click={() => pasteComponent('inside')}>
<i class="icon ri-insert-column-right" />
Paste inside
</li>
</ul>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`}
okText="Delete Component"
onOk={deleteComponent} />
<style>
.root {
overflow: hidden;
z-index: 9;
}
.root button {
border-style: none;
border-radius: 2px;
padding: 5px;
background: transparent;
cursor: pointer;
color: var(--ink);
outline: none;
}
.menu {
z-index: 100000;
overflow: visible;
padding: 12px 0px;
border-radius: 5px;
}
.menu li {
border-style: none;
background-color: transparent;
list-style-type: none;
padding: 4px 16px;
margin: 0;
width: 100%;
box-sizing: border-box;
}
.item {
display: flex;
align-items: center;
font-size: 14px;
}
.icon {
margin-right: 8px;
}
.menu li:not(.disabled) {
cursor: pointer;
color: var(--ink-light);
}
.menu li:not(.disabled):hover {
color: var(--ink);
background-color: var(--grey-light);
}
.disabled {
color: var(--grey-dark);
cursor: default;
}
.hr-style {
margin: 8px 0;
color: var(--grey-dark);
}
</style>

View File

@ -13,7 +13,6 @@
import CodeEditor from "./CodeEditor.svelte"
import LayoutEditor from "./LayoutEditor.svelte"
import EventsEditor from "./EventsEditor"
import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte"
@ -25,7 +24,7 @@
let categories = [
{ value: "design", name: "Design" },
{ value: "settings", name: "Settings" },
{ value: "actions", name: "Actions" },
{ value: "events", name: "Events" },
]
let selectedCategory = categories[0]
@ -38,15 +37,10 @@
c => c._component === componentInstance._component
) || {}
$: panelDefinition = componentPropDefinition.properties
? componentPropDefinition.properties[selectedCategory.value]
: {}
let panelDefinition = {}
// SCREEN PROPS =============================================
$: screen_props =
$store.currentFrontEndType === "page"
? getProps($store.currentPreviewItem, ["name", "favicon"])
: getProps($store.currentPreviewItem, ["name", "description", "route"])
$: panelDefinition = componentPropDefinition.properties &&
componentPropDefinition.properties[selectedCategory.value]
const onStyleChanged = store.setComponentStyle
const onPropChanged = store.setComponentProp
@ -92,7 +86,11 @@
{componentInstance}
{componentDefinition}
{panelDefinition}
onChange={onPropChanged} />
onChange={onPropChanged}
onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== "component" && $store.currentPreviewItem} />
{:else if selectedCategory.value === 'events'}
<EventsEditor component={componentInstance} />
{/if}
</div>
@ -105,6 +103,9 @@
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: hidden;
padding: 20px;
box-sizing: border-box;
}
.title > div:nth-child(1) {
@ -119,5 +120,7 @@
.component-props-container {
margin-top: 20px;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
</style>

View File

@ -1,4 +1,5 @@
<script>
import { goto } from "@sveltech/routify"
import { splitName } from "./pagesParsing/splitRootComponentName.js"
import components from "./temporaryPanelStructure.js"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -32,7 +33,14 @@
const onComponentChosen = component => {
store.addChildComponent(component._component)
toggleTab()
toggleTab("Navigate")
// Get ID path
const path = store.getPathToComponent($store.currentComponentInfo)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
}
</script>

View File

@ -1,7 +1,6 @@
<script>
import { params, goto } from "@sveltech/routify"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import { last, sortBy, map, trimCharsStart, trimChars, join } from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { pipe } from "components/common/core"
@ -36,11 +35,6 @@
sortBy("title"),
])
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const changeScreen = screen => {
store.setCurrentScreen(screen.title)
$goto(`./:page/${screen.title}`)
@ -62,9 +56,7 @@
{/if}
</span>
<span class="icon">
<ShapeIcon />
</span>
<i class="ri-artboard-2-fill icon" />
<span class="title">{screen.title}</span>
</div>
@ -72,41 +64,32 @@
{#if $store.currentPreviewItem.name === screen.title && screen.component.props._children}
<ComponentsHierarchyChildren
components={screen.component.props._children}
currentComponent={$store.currentComponentInfo}
onDeleteComponent={confirmDeleteComponent}
onMoveUpComponent={store.moveUpComponent}
onMoveDownComponent={store.moveDownComponent}
onCopyComponent={store.copyComponent} />
currentComponent={$store.currentComponentInfo} />
{/if}
{/each}
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component?`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<style>
.root {
font-weight: 400;
color: #000333;
color: var(--ink);
}
.title {
margin-left: 10px;
margin-top: 2px;
font-size: 13px;
font-size: 14px;
font-weight: 400;
}
.icon {
display: inline-block;
transition: 0.2s;
font-size: 24px;
width: 20px;
margin-top: 2px;
color: #333;
color: var(--ink-light);
}
.icon:nth-of-type(2) {

View File

@ -3,6 +3,7 @@
import { store } from "builderStore"
import { last } from "lodash/fp"
import { pipe } from "components/common/core"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import {
XCircleIcon,
ChevronUpIcon,
@ -14,23 +15,12 @@
export let currentComponent
export let onSelect = () => {}
export let level = 0
export let onDeleteComponent
export let onMoveUpComponent
export let onMoveDownComponent
export let onCopyComponent
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const moveDownComponent = component => {
const c = component
return () => {
return onMoveDownComponent(c)
}
}
const selectComponent = component => {
// Set current component
store.selectComponent(component)
@ -50,31 +40,13 @@
class="budibase__nav-item item"
class:selected={currentComponent === component}
style="padding-left: {level * 20 + 53}px">
<div>{get_capitalised_name(component._component)}</div>
<div class="reorder-buttons">
{#if index > 0}
<button
class:solo={index === components.length - 1}
on:click|stopPropagation={() => onMoveUpComponent(component)}>
<ChevronUpIcon />
</button>
{/if}
{#if index < components.length - 1}
<button
class:solo={index === 0}
on:click|stopPropagation={moveDownComponent(component)}>
<ChevronDownIcon />
</button>
{/if}
<div class="nav-item">
<i class="icon ri-arrow-right-circle-fill" />
{get_capitalised_name(component._component)}
</div>
<div class="actions">
<ComponentDropdownMenu {component} />
</div>
<button
class="copy"
on:click|stopPropagation={() => onCopyComponent(component)}>
<CopyIcon />
</button>
<button on:click|stopPropagation={() => onDeleteComponent(component)}>
<XCircleIcon />
</button>
</div>
{#if component._children}
@ -82,11 +54,7 @@
components={component._children}
{currentComponent}
{onSelect}
level={level + 1}
{onDeleteComponent}
{onMoveUpComponent}
{onMoveDownComponent}
{onCopyComponent} />
level={level + 1} />
{/if}
</li>
{/each}
@ -107,50 +75,37 @@
border-radius: 3px;
height: 35px;
align-items: center;
font-weight: 400;
font-size: 13px;
}
.item button {
.actions {
display: none;
height: 20px;
width: 28px;
color: var(--slate);
height: 24px;
width: 24px;
color: var(--ink);
padding: 0px 5px;
border-style: none;
background: rgba(0, 0, 0, 0);
cursor: pointer;
}
.item button.copy {
width: 26px;
position: relative;
}
.item:hover {
background: #fafafa;
background: var(--grey-light);
cursor: pointer;
}
.item:hover button {
.item:hover .actions {
display: block;
}
.item:hover button:hover {
color: var(--button-text);
}
.reorder-buttons {
.nav-item {
display: flex;
flex-direction: column;
height: 100%;
align-items: center;
font-size: 14px;
color: var(--ink);
}
.reorder-buttons > button {
flex: 1 1 auto;
width: 30px;
height: 15px;
}
.reorder-buttons > button.solo {
padding-top: 2px;
.icon {
color: var(--ink-light);
margin-right: 8px;
}
</style>

View File

@ -54,7 +54,7 @@
height: 100%;
display: flex;
flex-direction: column;
padding: 20px 20px;
padding: 20px 5px 20px 10px;
border-left: solid 1px var(--grey);
}
@ -78,4 +78,8 @@
.switcher > .selected {
color: var(--ink);
}
.panel {
height: 100%;
}
</style>

View File

@ -55,6 +55,7 @@
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.design-view-state-categories {
@ -63,6 +64,9 @@
.design-view-property-groups {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
}
.no-design {

View File

@ -1,5 +1,6 @@
<script>
import { store } from "builderStore"
import { Button } from "@budibase/bbui"
import Modal from "../../common/Modal.svelte"
import HandlerSelector from "./HandlerSelector.svelte"
import IconButton from "../../common/IconButton.svelte"
@ -8,12 +9,12 @@
import Select from "../../common/Select.svelte"
import Input from "../../common/Input.svelte"
import getIcon from "../../common/icon"
import { CloseIcon } from "components/common/Icons/"
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
export let event
export let eventOptions = []
export let open
export let onClose
let eventType = ""
@ -62,105 +63,111 @@
}
</script>
<Modal bind:isOpen={open} onClosed={closeModal}>
<h2>
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
</h2>
<a href="https://docs.budibase.com/" target="_blank">
Click here to learn more about component events
</a>
<div class="container">
<div class="body">
<div class="heading">
<h3>
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
</h3>
</div>
<div class="event-options">
<div class="section">
<h4>Event Type</h4>
<Select bind:value={eventType}>
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}
</Select>
</div>
</div>
<div class="event-options">
<div>
<header>
<h5>Event Type</h5>
{@html getIcon('info', 20)}
</header>
<Select bind:value={eventType}>
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}
</Select>
<div class="section">
<h4>Event Action(s)</h4>
<HandlerSelector
newHandler
onChanged={updateDraftEventHandler}
onCreate={() => {
createNewEventHandler(draftEventHandler)
draftEventHandler = { parameters: [] }
}}
handler={draftEventHandler} />
</div>
{#if eventData}
{#each eventData.handlers as handler, index}
<HandlerSelector
{index}
onChanged={updateEventHandler}
onRemoved={() => deleteEventHandler(index)}
{handler} />
{/each}
{/if}
</div>
<div class="footer">
{#if eventData.name}
<Button
outline
on:click={deleteEvent}
disabled={eventData.handlers.length === 0}>
Delete
</Button>
{/if}
<div class="save">
<Button
primary
on:click={saveEventData}
disabled={eventData.handlers.length === 0}>
Save
</Button>
</div>
</div>
<header>
<h5>Event Action(s)</h5>
{@html getIcon('info', 20)}
</header>
<HandlerSelector
newHandler
onChanged={updateDraftEventHandler}
onCreate={() => {
createNewEventHandler(draftEventHandler)
draftEventHandler = { parameters: [] }
}}
handler={draftEventHandler} />
{#if eventData}
{#each eventData.handlers as handler, index}
<HandlerSelector
{index}
onChanged={updateEventHandler}
onRemoved={() => deleteEventHandler(index)}
{handler} />
{/each}
{/if}
<div class="actions">
<ActionButton
alert
disabled={eventData.handlers.length === 0}
hidden={!eventData.name}
on:click={deleteEvent}>
Delete
</ActionButton>
<ActionButton
disabled={eventData.handlers.length === 0}
on:click={saveEventData}>
Save
</ActionButton>
<div class="close-button" on:click={closeModal}>
<CloseIcon />
</div>
</Modal>
</div>
<style>
h2 {
color: var(--primary100);
font-size: 20px;
font-weight: bold;
margin-bottom: 0;
.container {
position: relative;
}
.heading {
margin-bottom: 20px;
}
h5 {
color: rgba(22, 48, 87, 0.6);
font-size: 15px;
margin: 0;
.close-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
}
.event-options {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
}
.actions,
header {
display: flex;
justify-content: space-between;
align-items: center;
}
.actions {
margin-top: auto;
}
header {
margin-top: 30px;
h4 {
margin-bottom: 10px;
}
a {
color: rgba(22, 48, 87, 0.6);
font-size: 13px;
margin-top: 0;
h3 {
margin: 0;
font-size: 24px;
font-weight: bold;
}
.body {
padding: 40px;
display: grid;
grid-gap: 20px;
}
.footer {
display: flex;
justify-content: flex-end;
padding: 30px 40px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-light);
}
.save {
margin-left: 20px;
}
</style>

View File

@ -1,4 +1,5 @@
<script>
import { getContext } from "svelte"
import {
keys,
map,
@ -17,7 +18,6 @@
import PlusButton from "components/common/PlusButton.svelte"
import IconButton from "components/common/IconButton.svelte"
import EventEditorModal from "./EventEditorModal.svelte"
import HandlerSelector from "./HandlerSelector.svelte"
import { PencilIcon } from "components/common/Icons"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
@ -25,37 +25,49 @@
export const EVENT_TYPE = "event"
export let component
export let components
let modalOpen = false
let events = []
let selectedEvent = null
$: {
const componentDefinition = components[component._component]
events = Object.keys(componentDefinition.props)
.filter(propName => componentDefinition.props[propName] === EVENT_TYPE)
events = Object.keys(component)
// TODO: use real events
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
.map(propName => ({
name: propName,
handlers: component[propName] || [],
}))
}
// Handle create app modal
const { open, close } = getContext("simple-modal")
const openModal = event => {
selectedEvent = event
modalOpen = true
}
const closeModal = () => {
selectedEvent = null
modalOpen = false
open(
EventEditorModal,
{
eventOptions: events,
event: selectedEvent,
onClose: () => {
close()
selectedEvent = null
},
},
{
closeButton: false,
closeOnEsc: false,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
}
</script>
<header>
<h3>Events</h3>
<PlusButton on:click={() => openModal()} />
</header>
<button class="newevent" on:click={() => openModal()}>
<i class="icon ri-add-circle-fill" />
Create New Event
</button>
<div class="root">
<form on:submit|preventDefault class="uk-form-stacked form-root">
@ -72,26 +84,40 @@
{/each}
</form>
</div>
<EventEditorModal
open={modalOpen}
onClose={closeModal}
eventOptions={events}
event={selectedEvent} />
<style>
h3 {
text-transform: uppercase;
font-size: 13px;
font-weight: 700;
color: #8997ab;
margin-bottom: 10px;
}
.root {
font-size: 10pt;
width: 100%;
}
.newevent {
cursor: pointer;
border: 1px solid var(--grey-dark);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
margin: 0px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
}
.newevent:hover {
background: var(--grey-light);
}
.icon {
color: var(--ink);
font-size: 16px;
margin-right: 4px;
}
.form-root {
display: flex;
flex-wrap: wrap;

View File

@ -1,4 +1,5 @@
<script>
import { Button } from "@budibase/bbui"
import IconButton from "components/common/IconButton.svelte"
import PlusButton from "components/common/PlusButton.svelte"
import Select from "components/common/Select.svelte"
@ -85,27 +86,28 @@
</Select>
</div>
{#if parameters}
<br />
{#each parameters as parameter, idx}
<StateBindingCascader onChange={onParameterChanged(idx)} {parameter} />
<StateBindingCascader on:change={onParameterChanged(idx)} {parameter} />
{/each}
{/if}
</div>
<div class="event-action-button">
{#if parameters.length > 0}
{#if newHandler}
<PlusButton on:click={onCreate} />
{:else}
<IconButton icon="x" on:click={onRemoved} />
{/if}
<div class="button-container">
{#if newHandler}
<Button primary thin on:click={onCreate}>Add Action</Button>
{:else}
<Button outline thin on:click={onRemoved}>Remove Action</Button>
{/if}
</div>
{/if}
</div>
</div>
<style>
.type-selector-container {
display: flex;
justify-content: space-between;
align-items: center;
display: grid;
grid-gap: 20px;
width: 100%;
background: rgba(223, 223, 223, 0.5);
border: 1px solid #dfdfdf;
margin-bottom: 18px;
@ -122,17 +124,19 @@
.handler-controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
grid-template-columns: 1fr;
grid-gap: 20px;
padding: 22px;
}
.event-action-button {
margin-right: 20px;
.button-container {
display: grid;
justify-items: end;
}
span {
font-size: 13px;
margin-bottom: 5px;
font-size: 18px;
margin-bottom: 10px;
font-weight: 500;
}
</style>

View File

@ -1,73 +1,52 @@
<script>
import { Input } from "@budibase/bbui"
import IconButton from "components/common/IconButton.svelte"
import PlusButton from "components/common/PlusButton.svelte"
import Select from "components/common/Select.svelte"
import Input from "components/common/Input.svelte"
import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe } from "components/common/core"
import {
EVENT_TYPE_MEMBER_NAME,
allHandlers,
} from "components/common/eventHandlers"
import { store } from "builderStore"
import StateBindingOptions from "../PropertyCascader/StateBindingOptions.svelte"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
import { store, workflowStore } from "builderStore"
import { ArrowDownIcon } from "components/common/Icons/"
export let parameter
export let onChange
let isOpen = false
const capitalize = s => {
if (typeof s !== "string") return ""
return s.charAt(0).toUpperCase() + s.slice(1)
}
</script>
<div class="handler-option">
<span>{parameter.name}</span>
<div class="handler-input">
<Input on:change={onChange} value={parameter.value} />
<button on:click={() => (isOpen = !isOpen)}>
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
<ArrowDownIcon size={36} />
</div>
</button>
{#if isOpen}
<StateBindingOptions
onSelect={option => {
onChange(option)
isOpen = false
}} />
{/if}
</div>
{#if parameter.name === 'workflow'}
<span>{parameter.name}</span>
{/if}
{#if parameter.name === 'workflow'}
<Select on:change bind:value={parameter.value}>
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
<option value={workflow._id}>{workflow.name}</option>
{/each}
</Select>
{:else}
<Input
name={parameter.name}
label={capitalize(parameter.name)}
on:change
value={parameter.value} />
{/if}
</div>
<style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
background: rgba(249, 249, 249, 1);
font-size: 1.6rem;
font-weight: 700;
color: rgba(22, 48, 87, 1);
margin-left: 5px;
}
.icon {
width: 24px;
}
.handler-option {
display: flex;
flex-direction: column;
}
.handler-input {
position: relative;
display: flex;
}
span {
font-size: 13px;
margin-bottom: 5px;
font-size: 18px;
margin-bottom: 10px;
font-weight: 500;
}
</style>

View File

@ -1,18 +1,28 @@
<script>
import { buildStyle } from "../../helpers.js"
export let value = ""
export let text = ""
export let icon = ""
export let padding = "8px 5px;"
export let onClick = value => {}
export let selected = false
export let fontWeight = ""
$: style = buildStyle({ padding, fontWeight })
$: useIcon = !!icon
</script>
<div class="flatbutton" class:selected on:click={() => onClick(value || text)}>
<div
class="flatbutton"
{style}
class:selected
on:click={() => onClick(value || text)}>
{#if useIcon}
<i class={icon} />
{:else}
<span>{text}</span>
<span>
{@html text}
</span>
{/if}
</div>
@ -28,6 +38,7 @@
font-size: 14px;
font-weight: 400;
transition: all 0.3s;
margin-left: 5px;
text-rendering: optimizeLegibility;
}
@ -35,4 +46,8 @@
background: var(--ink-light);
color: #ffffff;
}
i {
font-size: 20px;
}
</style>

View File

@ -27,13 +27,16 @@
}
onChange(val)
}
const checkSelected = val =>
isMultiSelect ? value.includes(val) : value === val
</script>
<div class="flatbutton-group">
{#each buttonProps as props}
<div class="button-container">
<FlatButton
selected={value.includes(props.value)}
selected={isMultiSelect ? value.includes(props.value) : value === props.value}
onClick={onButtonClicked}
{...props} />
</div>

View File

@ -0,0 +1,57 @@
<script>
import { store, backendUiStore } from "builderStore"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import NewScreen from "components/userInterface/NewScreen.svelte"
const newScreen = () => {
newScreenPicker.show()
}
let newScreenPicker
</script>
<PagesList />
<button class="newscreen" on:click={newScreen}>
<i class="icon ri-add-circle-fill" />
Create New Screen
</button>
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="nav-items-container">
<ComponentsHierarchy screens={$store.screens} />
</div>
<NewScreen bind:this={newScreenPicker} />
<style>
.newscreen {
cursor: pointer;
border: 1px solid var(--grey-dark);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
margin: 20px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
}
.newscreen:hover {
background: var(--grey-light);
}
.icon {
color: var(--ink);
font-size: 16px;
margin-right: 4px;
}
</style>

View File

@ -3,7 +3,7 @@
export let item
</script>
<div class="item-item" transition:fly={{ y: 100, duration: 1000 }} on:click>
<div class="item-item" in:fly={{ y: 100, duration: 1000 }} on:click>
<div class="item-icon">
<i class={item.icon} />
</div>
@ -19,7 +19,7 @@
cursor: pointer;
margin-bottom: 8px;
padding: 8px 0px 16px 0px;
width: 120px;
width: 110px;
height: 80px;
justify-content: center;
align-items: center;

View File

@ -3,7 +3,6 @@
const dispatch = createEventDispatcher()
import Item from "./Item.svelte"
import { store } from "builderStore"
export let list
let category = list

View File

@ -0,0 +1,16 @@
<script>
import { backendUiStore } from "builderStore"
export let value
</script>
<div class="uk-margin block-field">
<div class="uk-form-controls">
<select class="budibase__input" on:change {value}>
<option value="" />
{#each $backendUiStore.models as model}
<option value={model._id}>{model.name}</option>
{/each}
</select>
</div>
</div>

View File

@ -1,36 +1,223 @@
<script>
import { onMount } from "svelte"
export let value = ""
export let onChange = value => {}
import { onMount, beforeUpdate } from "svelte"
import { buildStyle } from "../../helpers.js"
export let options = []
export let initialValue = ""
export let styleBindingProperty = ""
export let value = ""
export let styleBindingProperty
export let onChange = value => {}
let open = null
let rotate = ""
let select
let selectMenu
let icon
let selectYPosition = null
let availableSpace = 0
let positionSide = "top"
let maxHeight = null
let menuHeight
const handleStyleBind = value =>
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {}
$: isOptionsObject = options.every(o => typeof o === "object")
onMount(() => {
if (!value && !!initialValue) {
value = initialValue
if (select) {
select.addEventListener("keydown", addSelectKeyEvents)
}
return () => {
select.removeEventListener("keydown", addSelectKeyEvents)
}
})
function checkPosition() {
const { bottom, top: spaceAbove } = select.getBoundingClientRect()
const spaceBelow = window.innerHeight - bottom
if (spaceAbove > spaceBelow) {
positionSide = "bottom"
maxHeight = `${spaceAbove.toFixed(0) - 20}px`
} else {
positionSide = "top"
maxHeight = `${spaceBelow.toFixed(0) - 20}px`
}
}
function addSelectKeyEvents(e) {
if (e.key === "Enter") {
if (!open) {
toggleSelect(true)
}
} else if (e.key === "Escape") {
if (open) {
toggleSelect(false)
}
}
}
function toggleSelect(isOpen) {
checkPosition()
if (isOpen) {
icon.style.transform = "rotate(180deg)"
} else {
icon.style.transform = "rotate(0deg)"
}
open = isOpen
}
function handleClick(val) {
value = val
onChange(value)
}
$: menuStyle = buildStyle({
"max-height": maxHeight,
"transform-origin": `center ${positionSide}`,
[positionSide]: "32px",
})
$: isOptionsObject = options.every(o => typeof o === "object")
$: selectedOption = isOptionsObject
? options.find(o => o.value === value)
: {}
$: displayLabel =
selectedOption && selectedOption.label ? selectedOption.label : value || ""
</script>
<select
class="uk-select uk-form-small"
{value}
on:change={ev => onChange(ev.target.value)}>
{#if isOptionsObject}
{#each options as { value, label }}
<option {...handleStyleBind(value || label)} value={value || label}>
{label}
</option>
{/each}
{:else}
{#each options as value}
<option {...handleStyleBind(value)} {value}>{value}</option>
{/each}
{/if}
</select>
<div
tabindex="0"
bind:this={select}
class="bb-select-container"
on:click={() => toggleSelect(!open)}>
<div class="bb-select-anchor selected">
<span>{displayLabel}</span>
<i bind:this={icon} class="ri-arrow-down-s-fill" />
</div>
<div
bind:this={selectMenu}
style={menuStyle}
class="bb-select-menu"
class:open>
<ul>
{#if isOptionsObject}
{#each options as { value: v, label }}
<li
{...handleStyleBind(v)}
on:click|self={handleClick(v)}
class:selected={value === v}>
{label}
</li>
{/each}
{:else}
{#each options as v}
<li
{...handleStyleBind(v)}
on:click|self={handleClick(v)}
class:selected={value === v}>
{v}
</li>
{/each}
{/if}
</ul>
</div>
</div>
{#if open}
<div on:click|self={() => toggleSelect(false)} class="overlay" />
{/if}
<style>
.overlay {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 1;
}
.bb-select-container {
position: relative;
outline: none;
width: 160px;
height: 32px;
cursor: pointer;
font-size: 12px;
}
.bb-select-anchor {
cursor: pointer;
display: flex;
padding: 5px 10px;
background-color: #f2f2f2;
border-radius: 2px;
border: 1px solid var(--grey-dark);
align-items: center;
}
.bb-select-anchor > span {
color: #565a66;
font-weight: 500;
width: 140px;
overflow-x: hidden;
}
.bb-select-anchor > i {
transition: transform 0.13s ease;
transform-origin: center;
width: 20px;
height: 20px;
text-align: center;
}
.selected {
color: #565a66;
font-weight: 500;
}
.bb-select-menu {
position: absolute;
display: flex;
box-sizing: border-box;
flex-direction: column;
opacity: 0;
width: 160px;
z-index: 2;
color: #808192;
font-weight: 500;
height: fit-content !important;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
border-right: 1px solid var(--grey-dark);
border-left: 1px solid var(--grey-dark);
border-bottom: 1px solid var(--grey-dark);
background-color: #f2f2f2;
transform: scale(0);
transition: opacity 0.13s linear, transform 0.12s cubic-bezier(0, 0, 0.2, 1);
overflow-y: auto;
}
.open {
transform: scale(1);
opacity: 1;
}
ul {
list-style-type: none;
margin: 0;
padding: 5px 0px;
}
li {
height: auto;
padding: 5px 0px;
cursor: pointer;
padding-left: 10px;
}
li:hover {
background-color: #e6e6e6;
}
</style>

View File

@ -0,0 +1,33 @@
<script>
import { onMount } from "svelte"
export let value = ""
export let onChange = value => {}
export let options = []
export let initialValue = ""
export let styleBindingProperty = ""
const handleStyleBind = value =>
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {}
$: isOptionsObject = options.every(o => typeof o === "object")
onMount(() => {
if (!value && !!initialValue) {
value = initialValue
}
})
</script>
<select {value} on:change={ev => onChange(ev.target.value)}>
{#if isOptionsObject}
{#each options as { value, label }}
<option {...handleStyleBind(value || label)} value={value || label}>
{label}
</option>
{/each}
{:else}
{#each options as value}
<option {...handleStyleBind(value)} {value}>{value}</option>
{/each}
{/if}
</select>

View File

@ -34,86 +34,51 @@
title: lastPartOfName(layout),
}
const confirmDeleteComponent = async component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const setCurrentScreenToLayout = () => {
store.setScreenType("page")
$goto("./:page/page-layout")
}
</script>
<div class="pagelayoutSection">
<div class="components-nav-page">Page Layout</div>
<div
class="budibase__nav-item root"
class:selected={$store.currentComponentInfo._id === _layout.component.props._id}
on:click|stopPropagation={setCurrentScreenToLayout}>
<span
class="icon"
class:rotate={$store.currentPreviewItem.name !== _layout.title}>
<ArrowDownIcon />
</span>
<span class="icon">
<GridIcon />
</span>
<span class="title">Page Layout</span>
</div>
{#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
<ComponentsHierarchyChildren
thisComponent={_layout.component.props}
components={_layout.component.props._children}
currentComponent={$store.currentComponentInfo}
onDeleteComponent={confirmDeleteComponent}
onMoveUpComponent={store.moveUpComponent}
onMoveDownComponent={store.moveDownComponent}
onCopyComponent={store.copyComponent} />
{/if}
<div
class="budibase__nav-item root"
class:selected={$store.currentComponentInfo._id === _layout.component.props._id}
on:click|stopPropagation={setCurrentScreenToLayout}>
<span
class="icon"
class:rotate={$store.currentPreviewItem.name !== _layout.title}>
<ArrowDownIcon />
</span>
<i class="ri-layout-3-fill icon-big" />
<span class="title">Master Screen</span>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component?`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
{#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
<ComponentsHierarchyChildren
thisComponent={_layout.component.props}
components={_layout.component.props._children}
currentComponent={$store.currentComponentInfo} />
{/if}
<style>
.components-nav-page {
font-size: 13px;
color: #000333;
text-transform: uppercase;
margin-bottom: 10px;
padding-left: 20px;
font-weight: 600;
opacity: 0.4;
letter-spacing: 1px;
}
.pagelayoutSection {
margin: 20px 0px 20px 0px;
}
.title {
margin-left: 10px;
font-size: 13px;
font-size: 14px;
font-weight: 400;
color: var(--ink);
}
.icon {
width: 24px;
display: inline-block;
transition: 0.2s;
width: 20px;
margin-top: 2px;
color: #000333;
color: var(--ink-light);
}
.icon:nth-of-type(2) {
width: 14px;
margin: 0 0 0 5px;
.icon-big {
font-size: 24px;
color: var(--ink-light);
}
:global(svg) {

View File

@ -1,8 +1,6 @@
<script>
import { params, goto } from "@sveltech/routify"
import { store } from "builderStore"
import getIcon from "components/common/icon"
import { CheckIcon } from "components/common/Icons"
const getPage = (s, name) => {
const props = s.pages[name]
@ -11,16 +9,17 @@
const pages = [
{
title: "Main",
title: "Private",
id: "main",
},
{
title: "Login",
title: "Public",
id: "unauthenticated",
},
]
store.setCurrentPage($params.page ? $params.page : "main")
if (!$store.currentPageName)
store.setCurrentPage($params.page ? $params.page : "main")
const changePage = id => {
store.setCurrentPage(id)
@ -29,63 +28,37 @@
</script>
<div class="root">
<ul>
{#each pages as { title, id }}
<li>
<span class="icon">
{#if id === $params.page}
<CheckIcon />
{/if}
</span>
<button
class:active={id === $params.page}
on:click={() => changePage(id)}>
{title}
</button>
</li>
{/each}
</ul>
{#each pages as { title, id }}
<button class:active={id === $params.page} on:click={() => changePage(id)}>
{title}
</button>
{/each}
</div>
<style>
.root {
padding-bottom: 10px;
font-size: 0.9rem;
color: #000333;
font-weight: bold;
position: relative;
padding-left: 1.8rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
margin: 0.5rem 0;
display: flex;
flex-direction: row;
}
button {
margin: 0 0 0 6px;
padding: 0;
border: none;
font-family: Roboto;
font-size: 13px;
outline: none;
cursor: pointer;
background: rgba(0, 0, 0, 0);
padding: 8px 16px;
text-align: center;
background: #ffffff;
color: var(--ink-light);
border-radius: 5px;
font-family: Roboto;
font-size: 14px;
font-weight: 400;
transition: all 0.3s;
text-rendering: optimizeLegibility;
border: none !important;
transition: 0.2s;
}
.active {
font-weight: 500;
}
.icon {
display: inline-block;
width: 14px;
color: #000333;
background: var(--ink-light);
color: var(--white);
}
</style>

View File

@ -1,121 +0,0 @@
<script>
import { ArrowDownIcon } from "components/common/Icons/"
import { store } from "builderStore"
import { buildStateOrigins } from "builderStore/buildStateOrigins"
import { isBinding, getBinding, setBinding } from "components/common/binding"
import StateBindingOptions from "./StateBindingOptions.svelte"
export let onChanged = () => {}
export let value = ""
let isOpen = false
let stateBindings = []
let bindingPath = ""
let bindingFallbackValue = ""
let bindingSource = "store"
let bindingValue = ""
const bindValueToSource = (path, fallback, source) => {
if (!path) {
onChanged(fallback)
return
}
const binding = setBinding({ path, fallback, source })
onChanged(binding)
}
const setBindingPath = value =>
bindValueToSource(value, bindingFallbackValue, bindingSource)
const setBindingFallback = value =>
bindValueToSource(bindingPath, value, bindingSource)
const setBindingSource = source =>
bindValueToSource(bindingPath, bindingFallbackValue, source)
$: {
const binding = getBinding(value)
if (bindingPath !== binding.path) isOpen = false
bindingPath = binding.path
bindingValue = typeof value === "object" ? "" : value
bindingFallbackValue = binding.fallback || bindingValue
const currentScreen = $store.screens.find(
({ name }) => name === $store.currentPreviewItem.name
)
stateBindings = currentScreen
? Object.keys(buildStateOrigins(currentScreen))
: []
}
</script>
<div class="cascader">
<div class="input-box">
<input
class:bold={!bindingFallbackValue && bindingPath}
class="uk-input uk-form-small"
value={bindingFallbackValue || bindingPath}
on:change={e => {
setBindingFallback(e.target.value)
onChanged(e.target.value)
}} />
<button on:click={() => (isOpen = !isOpen)}>
<div
class="icon"
class:highlighted={bindingPath}
style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
<ArrowDownIcon size={36} />
</div>
</button>
</div>
{#if isOpen}
<StateBindingOptions
onSelect={option => {
onChanged(option)
isOpen = false
}} />
{/if}
</div>
<style>
.bold {
font-weight: bold;
}
.highlighted {
color: rgba(0, 85, 255, 0.8);
}
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 3px;
font-size: 1.6rem;
font-weight: 700;
color: rgba(22, 48, 87, 1);
}
.cascader {
position: relative;
width: 100%;
}
.input-box {
display: flex;
align-items: center;
}
input {
margin-right: 5px;
border: 1px solid #dbdbdb;
border-radius: 2px;
opacity: 0.5;
height: 40px;
}
.icon {
width: 24px;
}
</style>

View File

@ -1,63 +0,0 @@
<script>
export let onSelect = () => {}
let options = [
{
name: "state",
description: "Front-end client state.",
},
{
name: "context",
description: "The component context object.",
},
{
name: "event",
description: "DOM event handler arguments.",
},
]
</script>
<ul class="options">
{#each options as option}
<li on:click={() => onSelect(`${option.name}.`)}>
<span class="name">{option.name}</span>
<span class="description">{option.description}</span>
</li>
{/each}
</ul>
<style>
.options {
width: 172px;
margin: 0;
position: absolute;
top: 35px;
padding: 10px;
z-index: 1;
background: rgba(249, 249, 249, 1);
min-height: 50px;
border-radius: 2px;
}
.description {
font-size: 0.8em;
}
.name {
color: rgba(22, 48, 87, 0.6);
font-size: 13px;
font-weight: bold;
text-transform: uppercase;
margin-top: 5px;
display: block;
}
.name:hover {
cursor: pointer;
font-weight: 800;
}
li {
list-style-type: none;
}
</style>

View File

@ -1 +0,0 @@
export { default } from "./PropertyCascader.svelte"

View File

@ -50,6 +50,9 @@
.label {
flex: 0 0 50px;
display: flex;
align-items: center;
padding: 0px 5px;
font-size: 12px;
font-weight: 400;
text-align: left;
@ -60,6 +63,7 @@
.control {
flex: 1;
display: flex;
padding-left: 2px;
max-width: 164px;
}

View File

@ -1,6 +1,7 @@
<script>
import { excludeProps } from "./propertyCategories.js"
import PropertyControl from "./PropertyControl.svelte"
import { DetailSummary } from "@budibase/bbui"
export let name = ""
export let styleCategory = "normal"
@ -8,75 +9,17 @@
export let componentInstance = {}
export let onStyleChanged = () => {}
export let show = false
const capitalize = name => name[0].toUpperCase() + name.slice(1)
$: icon = show ? "ri-arrow-down-s-fill" : "ri-arrow-right-s-fill"
$: style = componentInstance["_styles"][styleCategory] || {}
</script>
<div class="property-group-container">
<div class="property-group-name" on:click={() => (show = !show)}>
<div class="icon">
<i class={icon} />
</div>
<div class="name">{capitalize(name)}</div>
</div>
<div class="property-panel" class:show>
{#each properties as props}
<PropertyControl
label={props.label}
control={props.control}
key={props.key}
value={style[props.key]}
onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
props={{ ...excludeProps(props, ['control', 'label']) }} />
{/each}
</div>
</div>
<style>
.property-group-container {
display: flex;
flex-direction: column;
height: auto;
background: var(--grey-light);
margin: 0px 0px 4px 0px;
padding: 8px 12px;
justify-content: center;
border-radius: 4px;
}
.property-group-name {
cursor: pointer;
display: flex;
flex-flow: row nowrap;
}
.name {
flex: 1;
text-align: left;
padding-top: 2px;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.14px;
color: var(--ink);
}
.icon {
flex: 0 0 20px;
text-align: center;
}
.property-panel {
height: 0px;
overflow: hidden;
}
.show {
overflow: auto;
height: auto;
}
</style>
<DetailSummary {name}>
{#each properties as props}
<PropertyControl
label={props.label}
control={props.control}
key={props.key}
value={style[props.key]}
onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
props={{ ...excludeProps(props, ['control', 'label']) }} />
{/each}
</DetailSummary>

View File

@ -2,21 +2,61 @@
import PropertyControl from "./PropertyControl.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte"
import Colorpicker from "../common/Colorpicker.svelte"
import { goto } from "@sveltech/routify"
import { excludeProps } from "./propertyCategories.js"
import Input from "../common/Input.svelte"
export let panelDefinition = []
export let componentDefinition = {}
export let componentInstance = {}
export let onChange = () => {}
export let onScreenPropChange = () => {}
export let screenOrPageInstance
const propExistsOnComponentDef = prop => prop in componentDefinition.props
function handleChange(key, data) {
data.target ? onChange(key, data.target.value) : onChange(key, data)
}
function handleScreenPropChange (name, value) {
onScreenPropChange(name,value)
if(!isPage && name === "name") {
// screen name is changed... change URL
$goto(`./:page/${value}`)
}
}
const screenDefinition = [
{ key: "name", label: "Name", control: Input },
{ key: "description", label: "Description", control: Input },
{ key: "route", label: "Route", control: Input },
]
const pageDefinition = [
{ key: "title", label: "Title", control: Input },
{ key: "favicon", label: "Favicon", control: Input },
]
$: isPage = screenOrPageInstance && screenOrPageInstance.favicon
$: screenOrPageDefinition = isPage ? pageDefinition : screenDefinition
</script>
{#if panelDefinition.length > 0}
{#if screenOrPageInstance}
{#each screenOrPageDefinition as def}
<PropertyControl
control={def.control}
label={def.label}
key={def.key}
value={screenOrPageInstance[def.key]}
onChange={handleScreenPropChange}
props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each}
<hr/>
{/if}
{#if panelDefinition && panelDefinition.length > 0}
{#each panelDefinition as definition}
{#if propExistsOnComponentDef(definition.key)}
<PropertyControl

View File

@ -2,8 +2,6 @@
import { backendUiStore } from "builderStore"
import IconButton from "../common/IconButton.svelte"
import Input from "../common/Input.svelte"
import PropertyCascader from "./PropertyCascader"
import { isBinding, getBinding, setBinding } from "../common/binding"
import Colorpicker from "../common/Colorpicker.svelte"
export let value = ""
@ -49,8 +47,6 @@
{/if}
{/each}
</select>
{:else}
<PropertyCascader {onChanged} {value} />
{/if}
</div>

View File

@ -27,11 +27,6 @@
settingsView.show()
}
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script>
@ -86,13 +81,6 @@
<NewScreen bind:this={newScreenPicker} />
<SettingsView bind:this={settingsView} />
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<style>
button {
cursor: pointer;
@ -120,7 +108,6 @@
background-color: var(--white);
height: calc(100vh - 49px);
padding: 0;
overflow: scroll;
display: flex;
flex-direction: column;
}
@ -136,8 +123,7 @@
.components-pane {
grid-column: 3;
background-color: var(--white);
height: 100vh;
overflow-y: scroll;
height: calc(100vh - 49px);
}
.components-nav-page {
@ -215,10 +201,6 @@
letter-spacing: 1px;
}
.border-line {
border-bottom: 1px solid #d8d8d8;
}
.components-list-container {
padding: 20px 0px 0 0;
}

View File

@ -1,6 +1,7 @@
import Input from "../common/Input.svelte"
import OptionSelect from "./OptionSelect.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte"
// import Colorpicker from "../common/Colorpicker.svelte"
/*
TODO: Allow for default values for all properties
@ -11,8 +12,9 @@ export const layout = [
label: "Display",
key: "display",
control: OptionSelect,
initialValue: "Flex",
initialValue: "",
options: [
{ label: "", value: "" },
{ label: "Flex", value: "flex" },
{ label: "Inline Flex", value: "inline-flex" },
],
@ -20,13 +22,16 @@ export const layout = [
{
label: "Direction",
key: "flex-direction",
control: OptionSelect,
initialValue: "Row",
options: [
{ label: "Row", value: "row" },
{ label: "Row Reverse", value: "rowReverse" },
{ label: "column", value: "column" },
{ label: "Column Reverse", value: "columnReverse" },
control: FlatButtonGroup,
buttonProps: [
{ icon: "ri-arrow-right-line", padding: "0px 5px", value: "row" },
{ icon: "ri-arrow-left-line", padding: "0px 5px", value: "rowReverse" },
{ icon: "ri-arrow-down-line", padding: "0px 5px", value: "column" },
{
icon: "ri-arrow-up-line",
padding: "0px 5px",
value: "columnReverse",
},
],
},
{
@ -35,6 +40,7 @@ export const layout = [
control: OptionSelect,
initialValue: "Flex Start",
options: [
{ label: "", value: "" },
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
@ -60,39 +66,86 @@ export const layout = [
label: "Wrap",
key: "flex-wrap",
control: OptionSelect,
initialValue: "Wrap",
options: [
{ label: "Wrap", value: "wrap" },
{ label: "No Wrap", value: "nowrap" },
{ label: "Wrap Reverse", value: "wrap-reverse" },
{ label: "wrap", value: "wrap" },
{ label: "no wrap", value: "noWrap" },
],
},
]
const spacingMeta = [
{ placeholder: "L" },
{ placeholder: "B" },
{ placeholder: "R" },
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" },
]
export const spacing = [
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
{
label: "Margin",
key: "margin",
control: InputGroup,
meta: spacingMeta,
defaultValue: ["0", "0", "0", "0"],
},
{
label: "Padding",
key: "padding",
control: InputGroup,
meta: spacingMeta,
defaultValue: ["0", "0", "0", "0"],
},
]
export const size = [
{ label: "Width", key: "width", control: Input },
{ label: "Height", key: "height", control: Input },
{ label: "Min W", key: "min-width", control: Input },
{ label: "Min H", key: "min-height", control: Input },
{ label: "Max W", key: "max-width", control: Input },
{ label: "Max H", key: "max-height", control: Input },
{
label: "Width",
key: "width",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Height",
key: "height",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Min W",
key: "min-width",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Min H",
key: "min-height",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Max W",
key: "max-width",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Max H",
key: "max-height",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
]
export const position = [
@ -113,26 +166,41 @@ export const position = [
label: "Top",
key: "top",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Right",
key: "right",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Bottom",
key: "bottom",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Left",
key: "left",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Z-index",
key: "z-index",
control: Input,
placeholder: "num",
width: "48px",
textAlign: "center",
},
]
@ -166,31 +234,58 @@ export const typography = [
label: "Weight",
key: "font-weight",
control: OptionSelect,
options: [
{ label: "100", value: "100" },
{ label: "200", value: "200" },
{ label: "300", value: "300" },
{ label: "400", value: "400" },
{ label: "500", value: "500" },
{ label: "600", value: "600" },
{ label: "700", value: "700" },
{ label: "800", value: "800" },
{ label: "900", value: "900" },
],
options: ["200", "300", "400", "500", "600", "700", "800", "900"],
},
{
label: "size",
key: "font-size",
defaultValue: "",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Line H",
key: "line-height",
control: Input,
placeholder: "lh",
width: "48px",
textAlign: "center",
},
{ label: "size", key: "font-size", defaultValue: "", control: Input },
{ label: "Line H", key: "line-height", control: Input },
{
label: "Color",
key: "color",
control: Input,
placeholder: "hex",
},
{
label: "align",
key: "text-align",
control: OptionSelect,
options: ["initial", "left", "right", "center", "justify"],
}, //custom
control: FlatButtonGroup,
buttonProps: [
{ icon: "ri-align-left", padding: "0px 5px", value: "left" },
{ icon: "ri-align-center", padding: "0px 5px", value: "center" },
{ icon: "ri-align-right", padding: "0px 5px", value: "right" },
{ icon: "ri-align-justify", padding: "0px 5px", value: "justify" },
],
},
{
label: "transform",
key: "text-transform",
control: FlatButtonGroup,
buttonProps: [
{ text: "BB", padding: "0px 5px", fontWeight: 500, value: "uppercase" },
{ text: "Bb", padding: "0px 5px", fontWeight: 500, value: "capitalize" },
{ text: "bb", padding: "0px 5px", fontWeight: 500, value: "lowercase" },
{
text: "&times;",
padding: "0px 5px",
fontWeight: 500,
value: "none",
},
],
},
{
label: "Decoration",
key: "text-decoration-line",
@ -204,23 +299,51 @@ export const typography = [
{ label: "Under Over", value: "underline overline" },
],
},
{ label: "transform", key: "text-transform", control: Input }, //custom
{ label: "style", key: "font-style", control: Input }, //custom
]
export const background = [
{
label: "Background",
label: "Color",
key: "background",
control: Input,
},
{ label: "Image", key: "image", control: Input }, //custom
{
label: "Image",
key: "background-image",
control: Input,
placeholder: "src",
},
]
export const border = [
{ label: "Radius", key: "border-radius", control: Input },
{ label: "Width", key: "border-width", control: Input }, //custom
{
label: "Radius",
key: "border-radius",
control: OptionSelect,
defaultValue: "None",
options: [
{ label: "None", value: "0" },
{ label: "small", value: "0.125rem" },
{ label: "Medium", value: "0.25rem;" },
{ label: "Large", value: "0.375rem" },
{ label: "Extra large", value: "0.5rem" },
{ label: "Full", value: "5678px" },
],
},
{
label: "Width",
key: "border-width",
control: OptionSelect,
defaultValue: "None",
options: [
{ label: "None", value: "0" },
{ label: "Extra small", value: "0.5px" },
{ label: "Small", value: "1px" },
{ label: "Medium", value: "2px" },
{ label: "Large", value: "4px" },
{ label: "Extra large", value: "8px" },
],
},
{
label: "Color",
key: "border-color",
@ -230,6 +353,7 @@ export const border = [
label: "Style",
key: "border-style",
control: OptionSelect,
defaultValue: "None",
options: [
"none",
"hidden",
@ -246,30 +370,98 @@ export const border = [
]
export const effects = [
{ label: "Opacity", key: "opacity", control: Input },
{
label: "Opacity",
key: "opacity",
control: Input,
width: "48px",
textAlign: "center",
placeholder: "%",
},
{
label: "Rotate",
key: "transform",
key: "transform-rotate",
control: OptionSelect,
defaultValue: "0",
options: [
{ label: "None", value: "rotate(0deg)" },
{ label: "45 degrees", value: "rotate(45deg)" },
{ label: "90 degrees", value: "rotate(90deg)" },
{ label: "135 degrees", value: "rotate(135deg)" },
{ label: "180 degrees", value: "rotate(180deg)" },
{ label: "225 degrees", value: "rotate(225deg)" },
{ label: "270 degrees", value: "rotate(270deg)" },
{ label: "315 degrees", value: "rotate(315deg)" },
{ label: "360 degrees", value: "rotate(360deg)" },
"0",
"45deg",
"90deg",
"90deg",
"135deg",
"180deg",
"225deg",
"270deg",
"315dev",
],
}, //needs special control
{ label: "Shadow", key: "box-shadow", control: Input },
{
label: "Shadow",
key: "box-shadow",
control: OptionSelect,
defaultValue: "None",
options: [
{ label: "None", value: "none" },
{ label: "Extra small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
{
label: "Small",
value:
"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
},
{
label: "Medium",
value:
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
},
{
label: "Large",
value:
"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
},
{
label: "Extra large",
value:
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
},
],
},
]
export const transitions = [
{ label: "Property", key: "transition-property", control: Input },
{ label: "Duration", key: "transition-timing-function", control: Input },
{ label: "Ease", key: "transition-ease", control: Input },
{
label: "Property",
key: "transition-property",
control: OptionSelect,
options: [
"None",
"All",
"Background Color",
"Color",
"Font Size",
"Font Weight",
"Height",
"Margin",
"Opacity",
"Padding",
"Rotate",
"Shadow",
"Width",
],
},
{
label: "Duration",
key: "transition-duration",
control: Input,
width: "48px",
textAlign: "center",
placeholder: "sec",
},
{
label: "Ease",
key: "transition-timing-function:",
control: OptionSelect,
options: ["linear", "ease", "ease-in", "ease-out", "ease-in-out"],
},
]
export const all = {

View File

@ -1,6 +1,7 @@
import Input from "../common/Input.svelte"
import OptionSelect from "./OptionSelect.svelte"
import Checkbox from "../common/Checkbox.svelte"
import ModelSelect from "components/userInterface/ModelSelect.svelte"
import { all } from "./propertyCategories.js"
@ -10,6 +11,18 @@ export default {
name: "Basic",
isCategory: true,
children: [
{
_component: "@budibase/standard-components/embed",
icon: "ri-code-line",
name: "Embed",
description: "Embed content from 3rd party sources",
properties: {
design: {
...all,
},
settings: [{ label: "Embed", key: "embed", control: Input }],
},
},
{
_component: "@budibase/standard-components/container",
name: "Container",
@ -186,6 +199,17 @@ export default {
],
},
},
{
_component: "@budibase/standard-components/image",
name: "Image",
description: "A basic component for displaying images",
icon: "ri-image-fill",
children: [],
properties: {
design: { ...all },
settings: [{ label: "URL", key: "url", control: Input }],
},
},
{
_component: "@budibase/standard-components/icon",
name: "Icon",
@ -229,29 +253,79 @@ export default {
"A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-fill",
children: [],
properties: { design: { ...all } },
properties: {
design: { ...all },
settings: [
{
label: "Heading",
key: "heading",
control: Input,
placeholder: "text",
},
{
label: "Subheading",
key: "subheading",
control: Input,
placeholder: "text",
},
{
label: "Content",
key: "content",
control: Input,
placeholder: "text",
},
{
label: "Image",
key: "imageUrl",
control: Input,
placeholder: "src",
},
],
},
},
{
name: "Login",
_component: "@budibase/standard-components/login",
description:
"A component that automatically generates a login screen for your app.",
icon: "ri-login-box-fill",
children: [],
properties: { design: { ...all } },
properties: {
design: { ...all },
settings: [
{
label: "Name",
key: "name",
control: Input,
},
{
label: "Logo",
key: "logo",
control: Input,
},
],
},
},
{
name: "Table",
_component: "@budibase/standard-components/datatable",
description: "A component that generates a table from your data.",
icon: "ri-archive-drawer-fill",
properties: { design: { ...all } },
properties: {
design: { ...all },
settings: [{ label: "Model", key: "model", control: ModelSelect }],
},
children: [],
},
{
name: "Form",
description: "A component that generates a form from your data.",
icon: "ri-file-edit-fill",
properties: { design: { ...all } },
_component: "@budibase/materialdesign-components/Form",
properties: {
design: { ...all },
settings: [{ label: "Model", key: "model", control: ModelSelect }],
},
_component: "@budibase/standard-components/dataform",
template: {
component: "@budibase/materialdesign-components/Form",
description: "Form for saving a record",
@ -264,15 +338,53 @@ export default {
_component: "@budibase/standard-components/datachart",
description: "Shiny chart",
icon: "ri-bar-chart-fill",
properties: { design: { ...all } },
properties: {
design: { ...all },
settings: [
{ label: "Model", key: "model", control: ModelSelect },
{
label: "Chart Type",
key: "type",
control: OptionSelect,
options: [
"column2d",
"column3d",
"line",
"area2d",
"bar2d",
"bar3d",
"pie2d",
"pie3d",
"doughnut2d",
"doughnut3d",
"pareto2d",
"pareto3d",
],
},
],
},
children: [],
},
{
name: "Data List",
_component: "@budibase/standard-components/datalist",
description: "Shiny list",
icon: "ri-file-list-fill",
properties: {
design: { ...all },
settings: [{ label: "Model", key: "model", control: ModelSelect }],
},
children: [],
},
{
name: "List",
_component: "@budibase/standard-components/datalist",
_component: "@budibase/standard-components/list",
description: "Shiny list",
icon: "ri-file-list-fill",
properties: { design: { ...all } },
properties: {
design: { ...all },
settings: [{ label: "Model", key: "model", control: ModelSelect }],
},
children: [],
},
{
@ -306,7 +418,15 @@ export default {
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
children: [],
properties: { design: { ...all } },
properties: {
design: { ...all },
settings: [
{ label: "Logo URL", key: "logoUrl", control: Input },
{ label: "Title", key: "title", control: Input },
{ label: "Color", key: "color", control: Input },
{ label: "Background", key: "backgroundColor", control: Input },
],
},
},
],
},

View File

@ -0,0 +1,89 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications"
import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte"
export let onClosed
let name
$: valid = !!name
$: instanceId = $backendUiStore.selectedDatabase._id
async function deleteWorkflow() {
await workflowStore.actions.delete({
instanceId,
workflow: $workflowStore.currentWorkflow.workflow,
})
onClosed()
notifier.danger("Workflow deleted.")
}
</script>
<header>
<i class="ri-stackshare-line" />
Delete Workflow
</header>
<div>
<p>
Are you sure you want to delete this workflow? This action can't be undone.
</p>
</div>
<footer>
<a href="https://docs.budibase.com">
<i class="ri-information-line" />
Learn about workflows
</a>
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton alert on:click={deleteWorkflow}>Delete</ActionButton>
</footer>
<style>
header {
font-size: 24px;
color: var(--font);
font-weight: bold;
padding: 30px;
}
header i {
margin-right: 10px;
font-size: 20px;
background: var(--secondary);
color: var(--dark-grey);
padding: 8px;
}
div {
padding: 0 30px 30px 30px;
}
label {
font-size: 18px;
font-weight: 500;
}
footer {
display: grid;
grid-auto-flow: column;
grid-gap: 5px;
grid-auto-columns: 3fr 1fr 1fr;
padding: 20px;
background: #fafafa;
border-radius: 0.5rem;
}
footer a {
color: var(--primary);
font-size: 14px;
vertical-align: middle;
display: flex;
align-items: center;
}
footer i {
font-size: 20px;
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,45 @@
<script>
import { store } from "builderStore"
import deepmerge from "deepmerge"
export let value
let pages = []
let components = []
let pageName
let selectedPage
let selectedScreen
$: pages = $store.pages
$: selectedPage = pages[pageName]
$: screens = selectedPage ? selectedPage._screens : []
$: if (selectedPage) {
let result = selectedPage
for (screen of screens) {
result = deepmerge(result, screen)
}
components = result.props._children
}
</script>
<div class="uk-margin block-field">
<label class="uk-form-label">Page</label>
<div class="uk-form-controls">
<select class="budibase__input" bind:value={pageName}>
{#each Object.keys(pages) as page}
<option value={page}>{page}</option>
{/each}
</select>
</div>
{#if components.length > 0}
<label class="uk-form-label">Component</label>
<div class="uk-form-controls">
<select class="budibase__input" bind:value>
{#each components as component}
<option value={component._id}>{component._id}</option>
{/each}
</select>
</div>
{/if}
</div>

View File

@ -0,0 +1,16 @@
<script>
import { backendUiStore } from "builderStore"
export let value
</script>
<div class="uk-margin block-field">
<div class="uk-form-controls">
<select class="budibase__input" bind:value>
<option value="" />
{#each $backendUiStore.models as model}
<option value={model}>{model.name}</option>
{/each}
</select>
</div>
</div>

View File

@ -0,0 +1,33 @@
<script>
import { backendUiStore } from "builderStore"
export let value
</script>
<div class="uk-margin block-field">
<div class="uk-form-controls">
<select class="budibase__input" bind:value={value.model}>
{#each $backendUiStore.models as model}
<option value={model}>{model.name}</option>
{/each}
</select>
</div>
</div>
{#if value.model}
<div class="uk-margin block-field">
<label class="uk-form-label fields">Fields</label>
{#each Object.keys(value.model.schema) as field}
<div class="uk-form-controls uk-margin">
<label class="uk-form-label">{field}</label>
<input type="text" class="budibase__input" bind:value={value[field]} />
</div>
{/each}
</div>
{/if}
<style>
.fields {
font-weight: 500;
}
</style>

View File

@ -0,0 +1,294 @@
<script>
import { fade } from "svelte/transition"
import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications"
import api from "builderStore/api"
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
const { open, close } = getContext("simple-modal")
const ACCESS_LEVELS = [
{
name: "Admin",
key: "ADMIN",
},
{
name: "Power User",
key: "POWER_USER",
},
]
let selectedTab = "SETUP"
let testResult
$: workflow =
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow
$: workflowBlock = $workflowStore.selectedWorkflowBlock
function deleteWorkflow() {
open(
DeleteWorkflowModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
function deleteWorkflowBlock() {
workflowStore.actions.deleteWorkflowBlock(workflowBlock)
notifier.info("Workflow block deleted.")
}
function testWorkflow() {
testResult = "PASSED"
}
async function saveWorkflow() {
const workflow = $workflowStore.currentWorkflow.workflow
await workflowStore.actions.save({
instanceId: $backendUiStore.selectedDatabase._id,
workflow,
})
notifier.success(`Workflow ${workflow.name} saved.`)
}
</script>
<section>
<header>
<span
class="hoverable"
class:selected={selectedTab === 'SETUP'}
on:click={() => {
selectedTab = 'SETUP'
testResult = null
}}>
Setup
</span>
{#if !workflowBlock}
<span
class="test-tab"
class:selected={selectedTab === 'TEST'}
on:click={() => (selectedTab = 'TEST')}>
Test
</span>
{/if}
</header>
{#if selectedTab === 'TEST'}
<div class="uk-margin config-item">
{#if testResult}
<button
transition:fade
class:passed={testResult === 'PASSED'}
class:failed={testResult === 'FAILED'}
class="test-result">
{testResult}
</button>
{/if}
<button class="workflow-button hoverable" on:click={testWorkflow}>
Test
</button>
</div>
{/if}
{#if selectedTab === 'SETUP'}
{#if workflowBlock}
<WorkflowBlockSetup {workflowBlock} />
<div class="buttons">
<button class="workflow-button hoverable" on:click={saveWorkflow}>
Save Workflow
</button>
<button
class="delete-workflow-button hoverable"
on:click={deleteWorkflowBlock}>
Delete Block
</button>
</div>
{:else if $workflowStore.currentWorkflow}
<div class="panel">
<div class="panel-body">
<div class="block-label">Workflow: {workflow.name}</div>
<div class="config-item">
<label>Name</label>
<div class="form">
<input
type="text"
class="budibase_input"
bind:value={workflow.name} />
</div>
</div>
<div class="config-item">
<label class="uk-form-label">User Access</label>
<div class="access-levels">
{#each ACCESS_LEVELS as { name, key }}
<span class="access-level">
<label>{name}</label>
<input class="uk-checkbox" type="checkbox" />
</span>
{/each}
</div>
</div>
</div>
<div class="buttons">
<button class="delete-workflow-button" on:click={deleteWorkflow}>
Delete Workflow
</button>
</div>
</div>
{/if}
{/if}
</section>
<style>
section {
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
}
.panel-body {
flex: 1;
}
.panel {
display: flex;
flex-direction: column;
justify-content: space-between;
}
header {
font-size: 20px;
font-weight: 700;
display: flex;
align-items: center;
margin-bottom: 18px;
color: var(--ink);
}
.selected {
color: var(--ink);
}
.block-label {
font-weight: 500;
font-size: 14px;
color: var(--ink);
margin: 0px 0px 16px 0px;
}
.config-item {
margin: 0px 0px 4px 0px;
padding: 12px;
background: var(--light-grey);
}
.budibase_input {
height: 35px;
width: 220px;
border-radius: 3px;
border: 1px solid var(--grey-dark);
text-align: left;
color: var(--ink);
font-size: 14px;
padding-left: 12px;
}
header > span {
color: var(--ink-lighter);
margin-right: 20px;
}
.form {
margin-top: 12px;
}
label {
font-weight: 500;
font-size: 14px;
color: var(--ink);
}
.buttons {
position: absolute;
bottom: 10px;
}
.delete-workflow-button {
cursor: pointer;
border: 1px solid var(--red);
border-radius: 3px;
width: 260px;
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
background: var(--white);
color: var(--red);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
align-self: flex-end;
margin-bottom: 10px;
}
.delete-workflow-button:hover {
background: var(--red);
border: 1px solid var(--red);
color: var(--white);
}
.workflow-button {
cursor: pointer;
border: 1px solid var(--grey-dark);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
margin-bottom: 10px;
}
.workflow-button:hover {
background: var(--grey-light);
}
.access-level {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20px;
}
.access-level label {
font-weight: normal;
color: var(--ink);
}
.test-result {
border: none;
width: 100%;
border-radius: 3px;
height: 32px;
font-size: 14px;
font-weight: 500;
color: var(--white);
text-align: center;
margin-bottom: 10px;
}
.passed {
background: #84c991;
}
.failed {
background: var(--red);
}
</style>

View File

@ -0,0 +1,97 @@
<script>
import { backendUiStore, store } from "builderStore"
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte"
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
export let workflowBlock
let params
$: workflowParams = workflowBlock.params
? Object.entries(workflowBlock.params)
: []
</script>
<label class="uk-form-label">{workflowBlock.type}: {workflowBlock.name}</label>
{#each workflowParams as [parameter, type]}
<div class="block-field">
<label class="uk-form-label">{parameter}</label>
<div class="uk-form-controls">
{#if Array.isArray(type)}
<select
class="budibase_input"
bind:value={workflowBlock.args[parameter]}>
{#each type as option}
<option value={option}>{option}</option>
{/each}
</select>
{:else if type === 'component'}
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'accessLevel'}
<select
class="budibase__input"
bind:value={workflowBlock.args[parameter]}>
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</select>
{:else if type === 'password'}
<input
type="password"
class="budibase__input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'number'}
<input
type="number"
class="budibase__input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'longText'}
<textarea
type="text"
class="budibase__input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'model'}
<ModelSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'record'}
<RecordSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'string'}
<input
type="text"
class="budibase__input"
bind:value={workflowBlock.args[parameter]} />
{/if}
</div>
</div>
{/each}
<style>
.block-field {
border-radius: 3px;
background: var(--grey-light);
padding: 12px;
margin: 0px 0px 4px 0px;
}
.budibase_input {
height: 35px;
width: 220px;
border-radius: 3px;
border: 1px solid var(--grey-dark);
text-align: left;
color: var(--ink);
font-size: 14px;
padding-left: 12px;
}
label {
text-transform: capitalize;
font-size: 14px;
font-weight: 500;
}
textarea {
min-height: 150px;
font-family: inherit;
padding: 5px;
}
</style>

View File

@ -0,0 +1 @@
export { default as SetupPanel } from "./SetupPanel.svelte"

View File

@ -0,0 +1,90 @@
<script>
import { onMount } from "svelte"
import { workflowStore, backendUiStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications"
import Flowchart from "./flowchart/FlowChart.svelte"
import api from "builderStore/api"
let selectedWorkflow
let uiTree
let instanceId = $backendUiStore.selectedDatabase._id
$: selectedWorkflow = $workflowStore.currentWorkflow
$: workflowLive = selectedWorkflow && selectedWorkflow.workflow.live
$: uiTree = selectedWorkflow ? selectedWorkflow.createUiTree() : []
$: instanceId = $backendUiStore.selectedDatabase._id
function onSelect(block) {
workflowStore.update(state => {
state.selectedWorkflowBlock = block
return state
})
}
function setWorkflowLive(live) {
const { workflow } = selectedWorkflow
workflow.live = live
workflowStore.actions.save({ instanceId, workflow })
if (live) {
notifier.info(`Workflow ${workflow.name} enabled.`)
} else {
notifier.danger(`Workflow ${workflow.name} disabled.`)
}
}
</script>
<section>
<Flowchart blocks={uiTree} {onSelect} />
<footer>
{#if selectedWorkflow}
<button
class:highlighted={workflowLive}
class:hoverable={workflowLive}
class="stop-button hoverable">
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
</button>
<button
class:highlighted={!workflowLive}
class:hoverable={!workflowLive}
class="play-button hoverable"
on:click={() => setWorkflowLive(true)}>
<i class="ri-play-fill" />
</button>
{/if}
</footer>
</section>
<style>
footer {
position: absolute;
bottom: 0;
right: 0;
display: flex;
align-items: flex-end;
}
footer > button {
border-radius: 100%;
color: var(--white);
width: 76px;
height: 76px;
border: none;
background: #adaec4;
font-size: 45px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24px;
}
.play-button.highlighted {
background: var(--primary);
}
.stop-button.highlighted {
background: var(--coral);
}
</style>

View File

@ -0,0 +1,9 @@
<svg
width="9"
height="75"
viewBox="0 0 9 75"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M5.0625 70H9L4.5 75L0 70H3.9375V65H5.0625V70Z" fill="#ADAEC4" />
<rect x="4" width="1" height="65" fill="#ADAEC4" />
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@ -0,0 +1,24 @@
<script>
import FlowItem from "./FlowItem.svelte"
import Arrow from "./Arrow.svelte"
export let blocks = []
export let onSelect
</script>
<section class="canvas">
{#each blocks as block, idx}
<FlowItem {onSelect} {block} />
{#if idx !== blocks.length - 1}
<Arrow />
{/if}
{/each}
</section>
<style>
.canvas {
display: flex;
align-items: center;
flex-direction: column;
}
</style>

View File

@ -0,0 +1,73 @@
<script>
import { fade } from "svelte/transition"
export let onSelect
export let block
function selectBlock() {
onSelect(block)
}
</script>
<div transition:fade class={`${block.type} hoverable`} on:click={selectBlock}>
<header>
{#if block.type === 'TRIGGER'}
<i class="ri-lightbulb-fill" />
When this happens...
{:else if block.type === 'ACTION'}
<i class="ri-flashlight-fill" />
Do this...
{:else if block.type === 'LOGIC'}
<i class="ri-pause-fill" />
Only continue if...
{/if}
</header>
<hr />
<p>
{@html block.body}
</p>
</div>
<style>
div {
width: 320px;
padding: 20px;
border-radius: 5px;
transition: 0.3s all;
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08);
background-color: var(--font);
font-size: 16px;
color: var(--white);
}
header {
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
}
header i {
font-size: 20px;
margin-right: 5px;
}
.ACTION {
background-color: var(--white);
color: var(--font);
}
.TRIGGER {
background-color: var(--font);
color: var(--white);
}
.LOGIC {
background-color: var(--secondary);
color: var(--font);
}
div:hover {
transform: scale(1.05);
}
</style>

View File

@ -0,0 +1,81 @@
<script>
import { onMount } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import { WorkflowList } from "../"
import WorkflowBlock from "./WorkflowBlock.svelte"
import api from "builderStore/api"
import blockDefinitions from "../blockDefinitions"
let selectedTab = "TRIGGER"
let definitions = []
$: definitions = Object.entries(blockDefinitions[selectedTab])
$: {
if (
$workflowStore.currentWorkflow.hasTrigger() &&
selectedTab === "TRIGGER"
) {
selectedTab = "ACTION"
}
}
</script>
<section>
<div class="subtabs">
{#if !$workflowStore.currentWorkflow.hasTrigger()}
<span
class="hoverable"
class:selected={'TRIGGER' === selectedTab}
on:click={() => (selectedTab = 'TRIGGER')}>
Triggers
</span>
{/if}
<span
class="hoverable"
class:selected={'ACTION' === selectedTab}
on:click={() => (selectedTab = 'ACTION')}>
Actions
</span>
<span
class="hoverable"
class:selected={'LOGIC' === selectedTab}
on:click={() => (selectedTab = 'LOGIC')}>
Logic
</span>
</div>
<div id="blocklist">
{#each definitions as [actionId, blockDefinition]}
<WorkflowBlock {blockDefinition} {actionId} blockType={selectedTab} />
{/each}
</div>
</section>
<style>
.subtabs {
margin-top: 27px;
display: grid;
grid-gap: 5px;
grid-auto-flow: column;
grid-auto-columns: 1fr 1fr 1fr;
margin-bottom: 10px;
}
.subtabs span {
transition: 0.3s all;
text-align: center;
color: var(--dark-grey);
font-weight: 500;
padding: 10px;
}
.subtabs span.selected {
background: var(--dark-grey);
color: var(--white);
border-radius: 2px;
}
.subtabs span:not(.selected) {
color: var(--dark-grey);
}
</style>

View File

@ -0,0 +1,56 @@
<script>
import { workflowStore } from "builderStore"
export let blockType
export let blockDefinition
export let actionId
function addBlockToWorkflow() {
workflowStore.actions.addBlockToWorkflow({
...blockDefinition,
args: blockDefinition.args || {},
actionId,
type: blockType,
})
}
</script>
<div class="workflow-block hoverable" on:click={addBlockToWorkflow}>
<div>
<i class={blockDefinition.icon} />
</div>
<div class="workflow-text">
<h4>{blockDefinition.name}</h4>
<p>{blockDefinition.description}</p>
</div>
</div>
<style>
.workflow-block {
display: flex;
padding: 20px;
align-items: center;
}
.workflow-text {
margin-left: 10px;
}
i {
background: var(--secondary);
color: var(--dark-grey);
padding: 10px;
}
h4 {
font-size: 14px;
font-weight: 500;
margin-bottom: 5px;
}
p {
font-size: 12px;
color: var(--dark-grey);
margin: 0;
}
</style>

View File

@ -0,0 +1,89 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications"
import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte"
export let onClosed
let name
$: valid = !!name
$: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId
async function createWorkflow() {
await workflowStore.actions.create({
name,
instanceId,
})
onClosed()
notifier.success(`Workflow ${name} created.`)
}
</script>
<header>
<i class="ri-stackshare-line" />
Create Workflow
</header>
<div>
<label class="uk-form-label" for="form-stacked-text">Name</label>
<input class="uk-input" type="text" bind:value={name} />
</div>
<footer>
<a href="https://docs.budibase.com">
<i class="ri-information-line" />
Learn about workflows
</a>
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
<ActionButton disabled={!valid} on:click={createWorkflow}>Save</ActionButton>
</footer>
<style>
header {
font-size: 24px;
color: var(--font);
font-weight: bold;
padding: 30px;
}
header i {
margin-right: 10px;
font-size: 20px;
background: var(--secondary);
color: var(--dark-grey);
padding: 8px;
}
div {
padding: 0 30px 30px 30px;
}
label {
font-size: 18px;
font-weight: 500;
}
footer {
display: grid;
grid-auto-flow: column;
grid-gap: 5px;
grid-auto-columns: 3fr 1fr 1fr;
padding: 20px;
background: #fafafa;
border-radius: 0.5rem;
}
footer a {
color: var(--primary);
font-size: 14px;
vertical-align: middle;
display: flex;
align-items: center;
}
footer i {
font-size: 20px;
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,125 @@
<script>
import Modal from "svelte-simple-modal"
import { notifier } from "@beyonk/svelte-notifications"
import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import api from "builderStore/api"
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
const { open, close } = getContext("simple-modal")
$: currentWorkflowId =
$workflowStore.currentWorkflow &&
$workflowStore.currentWorkflow.workflow._id
function newWorkflow() {
open(
CreateWorkflowModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
onMount(() => {
workflowStore.actions.fetch($backendUiStore.selectedDatabase._id)
})
</script>
<section>
<button class="new-workflow-button hoverable" on:click={newWorkflow}>
<i class="icon ri-add-circle-fill" />
Create New Workflow
</button>
<ul>
{#each $workflowStore.workflows as workflow}
<li
class="workflow-item"
class:selected={workflow._id === currentWorkflowId}
on:click={() => workflowStore.actions.select(workflow)}>
<i class="ri-stackshare-line" class:live={workflow.live} />
{workflow.name}
</li>
{/each}
</ul>
</section>
<style>
section {
display: flex;
flex-direction: column;
}
i {
color: #adaec4;
}
i:hover {
cursor: pointer;
}
ul {
list-style-type: none;
padding: 0;
flex: 1;
}
.live {
color: var(--primary);
}
li {
font-size: 14px;
}
.workflow-item {
display: flex;
border-radius: 3px;
padding-left: 12px;
align-items: center;
height: 40px;
margin-bottom: 4px;
color: var(--ink);
}
.workflow-item i {
font-size: 24px;
margin-right: 10px;
}
.workflow-item:hover {
cursor: pointer;
background: var(--grey-light);
}
.workflow-item.selected {
background: var(--blue-light);
}
.new-workflow-button {
cursor: pointer;
border: 1px solid var(--grey-dark);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
}
.new-workflow-button:hover {
background: var(--grey-light);
}
.icon {
color: var(--ink);
font-size: 16px;
margin-right: 4px;
}
</style>

View File

@ -0,0 +1,55 @@
<script>
import { onMount } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import { WorkflowList, BlockList } from "./"
import api from "builderStore/api"
import blockDefinitions from "./blockDefinitions"
let selectedTab = "WORKFLOWS"
let definitions = []
</script>
<header>
<span
class="hoverable workflow-header"
class:selected={selectedTab === 'WORKFLOWS'}
on:click={() => (selectedTab = 'WORKFLOWS')}>
Workflows
</span>
{#if $workflowStore.currentWorkflow}
<span
class="hoverable"
class:selected={selectedTab === 'ADD'}
on:click={() => (selectedTab = 'ADD')}>
Add
</span>
{/if}
</header>
{#if selectedTab === 'WORKFLOWS'}
<WorkflowList />
{:else if selectedTab === 'ADD'}
<BlockList />
{/if}
<style>
header {
font-size: 18px;
font-weight: 700;
background: none;
display: flex;
align-items: center;
margin-bottom: 20px;
}
.workflow-header {
margin-right: 20px;
}
span:not(.selected) {
color: var(--ink-lighter);
}
span:not(.selected):hover {
color: var(--ink);
}
</style>

View File

@ -0,0 +1,170 @@
const ACTION = {
SET_STATE: {
name: "Update UI State",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
icon: "ri-refresh-line",
description: "Update your User Interface with some data.",
environment: "CLIENT",
params: {
path: "string",
value: "longText",
},
},
NAVIGATE: {
name: "Navigate",
tagline: "Navigate to <b>{{url}}</b>",
icon: "ri-navigation-line",
description: "Navigate to another page.",
environment: "CLIENT",
params: {
url: "string",
},
},
SAVE_RECORD: {
name: "Save Record",
tagline: "<b>Save</b> a <b>{{record.model.name}}</b> record",
icon: "ri-save-3-fill",
description: "Save a record to your database.",
environment: "SERVER",
params: {
record: "record",
},
args: {
record: {},
},
},
DELETE_RECORD: {
description: "Delete a record from your database.",
icon: "ri-delete-bin-line",
name: "Delete Record",
tagline: "<b>Delete</b> a <b>{{record.model.name}}</b> record",
environment: "SERVER",
params: {
record: "record",
},
args: {
record: {},
},
},
// FIND_RECORD: {
// description: "Find a record in your database.",
// tagline: "<b>Find</b> a <b>{{record.model.name}}</b> record",
// icon: "ri-search-line",
// name: "Find Record",
// environment: "SERVER",
// params: {
// record: "string",
// },
// },
CREATE_USER: {
description: "Create a new user.",
tagline: "Create user <b>{{username}}</b>",
icon: "ri-user-add-fill",
name: "Create User",
environment: "SERVER",
params: {
username: "string",
password: "password",
accessLevelId: "accessLevel",
},
},
SEND_EMAIL: {
description: "Send an email.",
tagline: "Send email to <b>{{to}}</b>",
icon: "ri-mail-open-fill",
name: "Send Email",
environment: "SERVER",
params: {
to: "string",
from: "string",
subject: "longText",
text: "longText",
},
},
}
const TRIGGER = {
RECORD_SAVED: {
name: "Record Saved",
event: "record:save",
icon: "ri-save-line",
tagline: "Record is added to <b>{{model.name}}</b>",
description: "Save a record to your database.",
environment: "SERVER",
params: {
model: "model",
},
},
RECORD_DELETED: {
name: "Record Deleted",
event: "record:delete",
icon: "ri-delete-bin-line",
tagline: "Record is deleted from <b>{{model.name}}</b>",
description: "Fired when a record is deleted from your database.",
environment: "SERVER",
params: {
model: "model",
},
},
// CLICK: {
// name: "Click",
// icon: "ri-cursor-line",
// tagline: "{{component}} is clicked",
// description: "Trigger when you click on an element in the UI.",
// environment: "CLIENT",
// params: {
// component: "component"
// }
// },
// LOAD: {
// name: "Load",
// icon: "ri-loader-line",
// tagline: "{{component}} is loaded",
// description: "Trigger an element has finished loading.",
// environment: "CLIENT",
// params: {
// component: "component"
// }
// },
// INPUT: {
// name: "Input",
// icon: "ri-text",
// tagline: "Text entered into {{component}",
// description: "Trigger when you type into an input box.",
// environment: "CLIENT",
// params: {
// component: "component"
// }
// },
}
const LOGIC = {
FILTER: {
name: "Filter",
tagline: "{{field}} <b>{{condition}}</b> {{value}}",
icon: "ri-git-branch-line",
description: "Filter any workflows which do not meet certain conditions.",
environment: "CLIENT",
params: {
filter: "string",
condition: ["equals"],
value: "string",
},
},
DELAY: {
name: "Delay",
icon: "ri-time-fill",
tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Delay the workflow until an amount of time has passed.",
environment: "CLIENT",
params: {
time: "number",
},
},
}
export default {
ACTION,
TRIGGER,
LOGIC,
}

Some files were not shown because too many files have changed in this diff Show More