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

View File

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

View File

@ -1,8 +1,7 @@
/* Budibase Component Styles */ /* Budibase Component Styles */
.header { .header {
font-size: 0.75rem; font-size: 0.75rem;
color: #000333; color: var(--ink);
opacity: 0.4;
text-transform: uppercase; text-transform: uppercase;
margin-top: 1rem; margin-top: 1rem;
font-weight: 500; font-weight: 500;
@ -57,35 +56,34 @@
.budibase__nav-item { .budibase__nav-item {
cursor: pointer; cursor: pointer;
padding: 0 7px 0 3px; padding: 0 4px 0 2px;
height: 35px; height: 35px;
margin: 5px 20px 5px 0px; margin: 5px 0px 4px 0px;
border-radius: 0 5px 5px 0; border-radius: 0 5px 5px 0;
display: flex; display: flex;
align-items: center; align-items: center;
font-weight: 500; font-size: 14px;
font-size: 13px; transition: 0.2s;
} }
.budibase__nav-item.selected { .budibase__nav-item.selected {
color: var(--button-text); color: var(--ink);
background: #f1f4fc; background: var(--blue-light);
} }
.budibase__nav-item:hover { .budibase__nav-item:hover {
background: #fafafa; background: var(--grey-light);
} }
.budibase__input { .budibase__input {
width: 250px;
height: 35px; height: 35px;
width: 220px;
border-radius: 3px; border-radius: 3px;
border: 1px solid #DBDBDB; border: 1px solid var(--grey-dark);
text-align: left; text-align: left;
letter-spacing: 0.7px; color: var(--ink);
color: #000333; font-size: 14px;
font-size: 16px; padding-left: 12px;
padding-left: 5px;
} }
.uk-text-right { .uk-text-right {
@ -102,27 +100,32 @@
} }
.budibase__table { .budibase__table {
border: 1px solid #ccc; border: 1px solid var(--grey-dark);
background: #fff; background: #fff;
border-radius: 2px; border-radius: 2px;
} }
.budibase__table thead { .budibase__table thead {
background: #fafafa; background: var(--blue-light);
} }
.budibase__table thead > tr > th { .budibase__table thead > tr > th {
color: var(--button-text); color: var(--ink);
text-transform: capitalize; text-transform: capitalize;
font-weight: 500; font-weight: 500;
} }
.budibase__table tr { .budibase__table tr {
border-bottom: 1px solid #ccc; border-bottom: 1px solid var(--grey-light);
} }
.button--toggled { .button--toggled {
background: #fafafa; background: var(--blue-light);
color: var(--button-text); color: var(--ink-light);
padding: 10px; 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, method: method,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"x-user-agent": "Budibase Builder",
}, },
body: body && JSON.stringify(body), body: body && JSON.stringify(body),
}) })
@ -14,14 +15,16 @@ const apiCall = method => async (url, body) => {
return response return response
} }
const post = apiCall("POST") export const post = apiCall("POST")
const get = apiCall("GET") export const get = apiCall("GET")
const patch = apiCall("PATCH") export const patch = apiCall("PATCH")
const del = apiCall("DELETE") export const del = apiCall("DELETE")
export const put = apiCall("PUT")
export default { export default {
post, post,
get, get,
patch, patch,
delete: del, delete: del,
put,
} }

View File

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

View File

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

View File

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

View File

@ -1,5 +1,13 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import api from "../api" 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 = () => { export const getBackendUiStore = () => {
const INITIAL_BACKEND_UI_STATE = { const INITIAL_BACKEND_UI_STATE = {
@ -22,11 +30,27 @@ export const getBackendUiStore = () => {
const views = await viewsResponse.json() const views = await viewsResponse.json()
store.update(state => { store.update(state => {
state.selectedDatabase = db state.selectedDatabase = db
if (models && models.length > 0) {
state.selectedModel = models[0]
state.selectedView = `all_${models[0]._id}`
}
state.breadcrumbs = [db.name] state.breadcrumbs = [db.name]
state.models = models state.models = models
state.views = views state.views = views
return state 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: { records: {
@ -51,6 +75,8 @@ export const getBackendUiStore = () => {
store.update(state => { store.update(state => {
state.models.push(model) state.models.push(model)
state.models = state.models state.models = state.models
state.selectedModel = model
state.selectedView = `all_${model._id}`
return state return state
}), }),
}, },

View File

@ -1,11 +1,10 @@
import { cloneDeep, values } from "lodash/fp" import { values } from "lodash/fp"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend" import * as backendStoreActions from "./backend"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import api from "../api" import api from "../api"
import { DEFAULT_PAGES_OBJECT } from "../../constants" import { DEFAULT_PAGES_OBJECT } from "../../constants"
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents" import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
import { rename } from "components/userInterface/pagesParsing/renameScreen"
import { import {
createProps, createProps,
makePropsSafe, makePropsSafe,
@ -16,6 +15,16 @@ import { buildCodeForScreens } from "../buildCodeForScreens"
import { generate_screen_css } from "../generate_css" import { generate_screen_css } from "../generate_css"
import { insertCodeMetadata } from "../insertCodeMetadata" import { insertCodeMetadata } from "../insertCodeMetadata"
import { uuid } from "../uuid" 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 = () => { export const getStore = () => {
const initial = { const initial = {
@ -43,7 +52,6 @@ export const getStore = () => {
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store) store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
store.saveScreen = saveScreen(store) store.saveScreen = saveScreen(store)
store.renameScreen = renameScreen(store)
store.deleteScreen = deleteScreen(store) store.deleteScreen = deleteScreen(store)
store.setCurrentScreen = setCurrentScreen(store) store.setCurrentScreen = setCurrentScreen(store)
store.setCurrentPage = setCurrentPage(store) store.setCurrentPage = setCurrentPage(store)
@ -54,13 +62,10 @@ export const getStore = () => {
store.addChildComponent = addChildComponent(store) store.addChildComponent = addChildComponent(store)
store.selectComponent = selectComponent(store) store.selectComponent = selectComponent(store)
store.setComponentProp = setComponentProp(store) store.setComponentProp = setComponentProp(store)
store.setPageOrScreenProp = setPageOrScreenProp(store)
store.setComponentStyle = setComponentStyle(store) store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store) store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(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.getPathToComponent = getPathToComponent(store)
store.addTemplatedComponent = addTemplatedComponent(store) store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store) store.setMetadataProp = setMetadataProp(store)
@ -69,6 +74,9 @@ export const getStore = () => {
export default getStore export default getStore
export const getComponentDefinition = (state, name) =>
name.startsWith("##") ? getBuiltin(name) : state.components[name]
const setPackage = (store, initial) => async pkg => { const setPackage = (store, initial) => async pkg => {
const [main_screens, unauth_screens] = await Promise.all([ const [main_screens, unauth_screens] = await Promise.all([
api api
@ -140,12 +148,6 @@ const _saveScreen = async (store, s, screen) => {
return s 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) => { const createScreen = store => (screenName, route, layoutComponentName) => {
store.update(state => { store.update(state => {
const rootComponent = state.components[layoutComponentName] const rootComponent = state.components[layoutComponentName]
@ -155,7 +157,6 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
description: "", description: "",
url: "", url: "",
_css: "", _css: "",
uiFunctions: "",
props: createProps(rootComponent).props, props: createProps(rootComponent).props,
} }
@ -173,11 +174,10 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
const setCurrentScreen = store => screenName => { const setCurrentScreen = store => screenName => {
store.update(s => { store.update(s => {
const screen = getExactComponent(s.screens, screenName) const screen = getExactComponent(s.screens, screenName)
screen._css = generate_screen_css([screen.props])
s.currentPreviewItem = screen s.currentPreviewItem = screen
s.currentFrontEndType = "screen" s.currentFrontEndType = "screen"
s.currentView = "detail" s.currentView = "detail"
regenerateCssForCurrentScreen(s)
const safeProps = makePropsSafe( const safeProps = makePropsSafe(
s.components[screen.props._component], s.components[screen.props._component],
screen.props 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 => { const savePage = store => async page => {
store.update(state => { store.update(state => {
if (state.currentFrontEndType !== "page" || !state.currentPageName) { 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 => { const setCurrentPage = store => pageName => {
store.update(state => { store.update(state => {
const current_screens = state.pages[pageName]._screens const current_screens = state.pages[pageName]._screens
@ -304,9 +255,7 @@ const setCurrentPage = store => pageName => {
state.currentComponentInfo = safeProps state.currentComponentInfo = safeProps
currentPage.props = safeProps currentPage.props = safeProps
state.currentPreviewItem = state.pages[pageName] state.currentPreviewItem = state.pages[pageName]
state.currentPreviewItem._css = generate_screen_css([ regenerateCssForCurrentScreen(state)
state.currentPreviewItem.props,
])
for (let screen of state.screens) { for (let screen of state.screens) {
screen._css = generate_screen_css([screen.props]) 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} componentToAdd - name of the component to add to the application
* @param {string} presetName - name of the component preset if defined * @param {string} presetName - name of the component preset if defined
@ -344,9 +291,7 @@ const addChildComponent = store => (componentToAdd, presetName) => {
return state return state
} }
const component = componentToAdd.startsWith("##") const component = getComponentDefinition(state, componentToAdd)
? getBuiltin(componentToAdd)
: state.components[componentToAdd]
const presetProps = presetName ? component.presets[presetName] : {} 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 * @param {string} props - props to add, as child of current component
*/ */
const addTemplatedComponent = store => props => { const addTemplatedComponent = store => props => {
store.update(state => { store.update(state => {
walkProps(props, p => { walkProps(props, p => {
@ -387,9 +333,7 @@ const addTemplatedComponent = store => props => {
state.currentComponentInfo._children = state.currentComponentInfo._children.concat( state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
props props
) )
state.currentPreviewItem._css = generate_screen_css([ regenerateCssForCurrentScreen(state)
state.currentPreviewItem.props,
])
setCurrentPageFunctions(state) setCurrentPageFunctions(state)
_saveCurrentPreviewItem(state) _saveCurrentPreviewItem(state)
@ -400,12 +344,7 @@ const addTemplatedComponent = store => props => {
const selectComponent = store => component => { const selectComponent = store => component => {
store.update(state => { store.update(state => {
const componentDef = component._component.startsWith("##") return _selectComponent(state, component)
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
}) })
} }
@ -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) => { const setComponentStyle = store => (type, name, value) => {
store.update(state => { store.update(state => {
if (!state.currentComponentInfo._styles) { if (!state.currentComponentInfo._styles) {
@ -428,9 +379,7 @@ const setComponentStyle = store => (type, name, value) => {
} }
state.currentComponentInfo._styles[type][name] = value state.currentComponentInfo._styles[type][name] = value
state.currentPreviewItem._css = generate_screen_css([ regenerateCssForCurrentScreen(state)
state.currentPreviewItem.props,
])
// save without messing with the store // save without messing with the store
_saveCurrentPreviewItem(state) _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 => { const getPathToComponent = store => component => {
// Gets all the components to needed to construct a path. // Gets all the components to needed to construct a path.
const tempStore = get(store) const tempStore = get(store)
@ -572,39 +452,9 @@ const getPathToComponent = store => component => {
return path 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) => { const setMetadataProp = store => (name, prop) => {
store.update(s => { store.update(s => {
s.currentPreviewItem[name] = prop s.currentPreviewItem[name] = prop
return s 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> <script>
export let disabled = false export let disabled = false
export let hidden = false export let hidden = false
export let secondary = false
export let primary = true export let primary = true
export let cancel = false export let cancel = false
export let alert = false export let alert = false
@ -11,6 +12,7 @@
on:click on:click
class="button" class="button"
class:hidden class:hidden
class:secondary
class:primary class:primary
class:alert class:alert
class:cancel class:cancel
@ -22,12 +24,14 @@
<style> <style>
.primary { .primary {
color: #ffffff; color: #ffffff;
background: #0055ff; background: var(--blue);
border: solid 1px var(--blue);
} }
.alert { .alert {
color: rgba(255, 0, 31, 1); color: white;
background: rgba(255, 0, 31, 0.1); background: #e26d69;
border: solid 1px #e26d69;
} }
.cancel { .cancel {
@ -35,18 +39,22 @@
background: none; background: none;
} }
.secondary {
color: var(--ink);
border: solid 1px var(--grey-dark);
background: white;
}
.button { .button {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 500;
border-radius: 5px; border-radius: 3px;
border: none;
padding: 10px 20px; padding: 10px 20px;
height: 45px; height: 40px;
} }
.button:hover { .button:hover {
cursor: pointer; cursor: pointer;
font-weight: 600;
filter: saturate(90%); filter: saturate(90%);
} }

View File

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

View File

@ -14,8 +14,7 @@
background: var(--secondary80); background: var(--secondary80);
color: var(--white); color: var(--white);
font-family: "Courier New", Courier, monospace; font-family: "Courier New", Courier, monospace;
width: 95%; height: 200px;
height: 100px;
border-radius: 5px; border-radius: 5px;
} }
</style> </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 TwitterIcon } from "./Twitter.svelte"
export { default as InfoIcon } from "./Info.svelte" export { default as InfoIcon } from "./Info.svelte"
export { default as CloseIcon } from "./Close.svelte" export { default as CloseIcon } from "./Close.svelte"
export { default as MoreIcon } from "./More.svelte"

View File

@ -1,30 +1,61 @@
<script> <script>
import { onMount } from "svelte"
import { buildStyle } from "../../helpers.js"
export let value = "" 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> </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> <style>
input { input {
display: block; /* width: 32px; */
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);
height: 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> </style>

View File

@ -1,74 +1,50 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import Input from "../Input.svelte"
export let meta = [] export let meta = []
export let label = "" export let label = ""
export let value = [0, 0, 0, 0] export let value = ["0", "0", "0", "0"]
export let type = "number" export let suffix = ""
export let onChange = () => {} export let onChange = () => {}
function handleChange(val, idx) { function handleChange(val, idx) {
value.splice(idx, 1, val) value.splice(idx, 1, val !== "auto" && suffix ? val + suffix : val)
value = value 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> </script>
<div class="input-container"> <div class="input-container">
<div class="label">{label}</div> <div class="label">{label}</div>
<div class="inputs"> <div class="inputs-group">
{#each meta as { placeholder }, i} {#each meta as m, i}
<input <Input
{type} width="37px"
placeholder={placeholder || ''} textAlign="center"
value={!value || value[i] === 0 ? '' : value[i]} placeholder={m.placeholder || ''}
on:change={e => handleChange(e.target.value || 0, i)} /> value={!displayValues || displayValues[i] === '0' ? '' : displayValues[i]}
onChange={value => handleChange(value || 0, i)} />
{/each} {/each}
</div> </div>
</div> </div>
<style> <style>
.input-container {
}
.label { .label {
flex: 0; flex: 0;
} }
.inputs { .inputs-group {
flex: 1; 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> </style>

View File

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

View File

@ -20,11 +20,8 @@
<style> <style>
.select-container { .select-container {
font-size: 14px; font-size: 14px;
color: var(--secondary60);
font-weight: bold;
position: relative; position: relative;
max-width: 400px; border: var(--grey-dark) 1px solid;
min-width: 275px;
} }
.adjusted { .adjusted {
@ -43,7 +40,7 @@
font-family: sans-serif; font-family: sans-serif;
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
color: #000333; color: var(--ink);
padding: 0 40px 0px 20px; padding: 0 40px 0px 20px;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@ -63,6 +60,6 @@
width: 30px; width: 30px;
height: 30px; height: 30px;
pointer-events: none; pointer-events: none;
color: var(--secondary100); color: var(--ink);
} }
</style> </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 { eventHandlers } from "../../../../client/src/state/eventHandlers"
import { writable } from "svelte/store"
export { EVENT_TYPE_MEMBER_NAME } from "../../../../client/src/state/eventHandlers" export { EVENT_TYPE_MEMBER_NAME } from "../../../../client/src/state/eventHandlers"
export const allHandlers = user => { export const allHandlers = () => {
const store = writable({ const handlersObj = eventHandlers()
_bbuser: user,
})
const handlersObj = eventHandlers(store)
const handlers = Object.keys(handlersObj).map(name => ({ const handlers = Object.keys(handlersObj).map(name => ({
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 const ITEMS_PER_PAGE = 10
// Internal headers we want to hide from the user // Internal headers we want to hide from the user
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"] const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
@ -152,19 +145,19 @@
} }
table { table {
border: 1px solid #ccc; border: 1px solid var(--grey-dark);
background: #fff; background: #fff;
border-radius: 3px; border-radius: 3px;
border-collapse: collapse; border-collapse: collapse;
} }
thead { thead {
background: #f9f9f9; background: var(--blue-light);
border: 1px solid #ccc; border: 1px solid var(--grey-dark);
} }
thead th { thead th {
color: var(--button-text); color: var(--ink);
text-transform: capitalize; text-transform: capitalize;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
@ -173,14 +166,14 @@
} }
tbody tr { tbody tr {
border-bottom: 1px solid #ccc; border-bottom: 1px solid var(--grey-dark);
transition: 0.3s background-color; transition: 0.3s background-color;
color: var(--secondary100); color: var(--ink);
font-size: 14px; font-size: 14px;
} }
tbody tr:hover { tbody tr:hover {
background: #fafafa; background: var(--grey-light);
} }
.table-controls { .table-controls {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,21 +7,26 @@
let username let username
let password let password
let accessLevelId
$: valid = username && password $: valid = username && password && accessLevelId
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId $: appId = $store.appId
async function createUser() { async function createUser() {
const user = { name: username, username, password } const user = { name: username, username, password, accessLevelId }
const response = await api.createUser(user, appId, instanceId) const response = await api.createUser(user, instanceId)
backendUiStore.actions.users.create(response) backendUiStore.actions.users.create(response)
onClosed() onClosed()
} }
</script> </script>
<form on:submit|preventDefault class="uk-form-stacked"> <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"> <div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Username</label> <label class="uk-form-label" for="form-stacked-text">Username</label>
<input class="uk-input" type="text" bind:value={username} /> <input class="uk-input" type="text" bind:value={username} />
@ -30,20 +35,50 @@
<label class="uk-form-label" for="form-stacked-text">Password</label> <label class="uk-form-label" for="form-stacked-text">Password</label>
<input class="uk-input" type="password" bind:value={password} /> <input class="uk-input" type="password" bind:value={password} />
</div> </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> </div>
<footer> <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> <ActionButton disabled={!valid} on:click={createUser}>Save</ActionButton>
</footer> </footer>
</form> </form>
<style> <style>
div { .main {
padding: 30px; 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 { footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 20px; padding: 20px;
background: #fafafa; background: var(--grey-light);
border-radius: 0.5rem; border-radius: 0 0 5px 5px;
}
.button {
margin-right: 20px;
} }
</style> </style>

View File

@ -3,10 +3,16 @@
export let value = "" export let value = ""
export let label export let label
export let errors = [] export let errors = []
export let className = "uk-input" export let options = []
let checked = type === "checkbox" ? value : false 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 => { const handleInput = event => {
if (event.target.type === "checkbox") { if (event.target.type === "checkbox") {
value = event.target.checked value = event.target.checked
@ -23,11 +29,23 @@
</script> </script>
<label>{label}</label> <label>{label}</label>
<input
class={className} {#if type === 'select'}
class:uk-form-danger={errors.length > 0} <select
{checked} class={determineClassName(type)}
{type} bind:value
{value} class:uk-form-danger={errors.length > 0}>
on:input={handleInput} {#each options as opt}
on:change={handleInput} /> <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> </script>
<div class="items-root"> <div class="items-root">
<div class="hierarchy"> <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>
{#if $backendUiStore.selectedDatabase._id} {#if $backendUiStore.selectedDatabase._id}
<div class="hierarchy"> <div class="hierarchy">
<div class="components-list-container"> <div class="components-list-container">

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/" import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte" import { getContext } from "svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { post } from "builderStore/api"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
@ -33,15 +34,7 @@
const data = { name, description } const data = { name, description }
loading = true loading = true
try { try {
const response = await fetch("/api/applications", { const response = await post("/api/applications", data)
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 res = await response.json() const res = await response.json()

View File

@ -21,14 +21,47 @@
return componentName || "element" return componentName || "element"
} }
$: iframe && const screenPlaceholder = {
console.log( name: "Screen Placeholder",
iframe.contentDocument.head.insertAdjacentHTML( route: "*",
"beforeend", props: {
`<\style></style>` _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 $: hasComponent = !!$store.currentPreviewItem
$: { $: {
styles = "" styles = ""
// Apply the CSS from the currently selected page and its screens // Apply the CSS from the currently selected page and its screens
@ -52,49 +85,12 @@
$: frontendDefinition = { $: frontendDefinition = {
appId: $store.appId, appId: $store.appId,
libraries: $store.libraries, libraries: $store.libraries,
page: $store.currentPreviewItem, page: $store.pages[$store.currentPageName],
screens: screensExist screens: [
? $store.currentPreviewItem._screens $store.currentFrontEndType === "page"
: [ ? screenPlaceholder
{ : $store.currentPreviewItem,
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",
},
],
},
],
},
},
],
appRootPath: "", appRootPath: "",
} }
@ -103,6 +99,27 @@
$: selectedComponentId = $store.currentComponentInfo $: selectedComponentId = $store.currentComponentInfo
? $store.currentComponentInfo._id ? $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> </script>
<div class="component-container"> <div class="component-container">
@ -111,14 +128,7 @@
style="height: 100%; width: 100%" style="height: 100%; width: 100%"
title="componentPreview" title="componentPreview"
bind:this={iframe} bind:this={iframe}
srcdoc={iframeTemplate({ srcdoc={iframeTemplate} />
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition: JSON.stringify(frontendDefinition),
currentPageFunctions: $store.currentPageFunctions,
})} />
{/if} {/if}
</div> </div>

View File

@ -1,23 +1,9 @@
export default ({ export default `<html>
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
currentPageFunctions,
}) => `<html>
<head> <head>
${stylesheetLinks}
<style> <style>
${styles || ""}
.${selectedComponentType}-${selectedComponentId} {
border: 2px solid #0055ff;
}
body, html { body, html {
height: 100%!important; height: 100%!important;
font-family: Roboto !important;
} }
.lay-__screenslot__text { .lay-__screenslot__text {
width: 100%; width: 100%;
@ -35,13 +21,50 @@ export default ({
} }
</style> </style>
<script> <script>
window["##BUDIBASE_FRONTEND_DEFINITION##"] = ${frontendDefinition}; function receiveMessage(event) {
window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = ${currentPageFunctions};
import('/_builder/budibase-client.esm.mjs') if (!event.data) return
.then(module => {
module.loadBudibase({ window, localStorage }); 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> </script>
</head> </head>
<body> <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 CodeEditor from "./CodeEditor.svelte"
import LayoutEditor from "./LayoutEditor.svelte" import LayoutEditor from "./LayoutEditor.svelte"
import EventsEditor from "./EventsEditor" import EventsEditor from "./EventsEditor"
import panelStructure from "./temporaryPanelStructure.js" import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte" import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte" import DesignView from "./DesignView.svelte"
@ -25,7 +24,7 @@
let categories = [ let categories = [
{ value: "design", name: "Design" }, { value: "design", name: "Design" },
{ value: "settings", name: "Settings" }, { value: "settings", name: "Settings" },
{ value: "actions", name: "Actions" }, { value: "events", name: "Events" },
] ]
let selectedCategory = categories[0] let selectedCategory = categories[0]
@ -38,15 +37,10 @@
c => c._component === componentInstance._component c => c._component === componentInstance._component
) || {} ) || {}
$: panelDefinition = componentPropDefinition.properties let panelDefinition = {}
? componentPropDefinition.properties[selectedCategory.value]
: {}
// SCREEN PROPS ============================================= $: panelDefinition = componentPropDefinition.properties &&
$: screen_props = componentPropDefinition.properties[selectedCategory.value]
$store.currentFrontEndType === "page"
? getProps($store.currentPreviewItem, ["name", "favicon"])
: getProps($store.currentPreviewItem, ["name", "description", "route"])
const onStyleChanged = store.setComponentStyle const onStyleChanged = store.setComponentStyle
const onPropChanged = store.setComponentProp const onPropChanged = store.setComponentProp
@ -92,7 +86,11 @@
{componentInstance} {componentInstance}
{componentDefinition} {componentDefinition}
{panelDefinition} {panelDefinition}
onChange={onPropChanged} /> onChange={onPropChanged}
onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== "component" && $store.currentPreviewItem} />
{:else if selectedCategory.value === 'events'}
<EventsEditor component={componentInstance} />
{/if} {/if}
</div> </div>
@ -105,6 +103,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: hidden; overflow-x: hidden;
overflow-y: hidden;
padding: 20px;
box-sizing: border-box;
} }
.title > div:nth-child(1) { .title > div:nth-child(1) {
@ -119,5 +120,7 @@
.component-props-container { .component-props-container {
margin-top: 20px; margin-top: 20px;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
} }
</style> </style>

View File

@ -1,4 +1,5 @@
<script> <script>
import { goto } from "@sveltech/routify"
import { splitName } from "./pagesParsing/splitRootComponentName.js" import { splitName } from "./pagesParsing/splitRootComponentName.js"
import components from "./temporaryPanelStructure.js" import components from "./temporaryPanelStructure.js"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -32,7 +33,14 @@
const onComponentChosen = component => { const onComponentChosen = component => {
store.addChildComponent(component._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> </script>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
<script> <script>
import { store } from "builderStore" import { store } from "builderStore"
import { Button } from "@budibase/bbui"
import Modal from "../../common/Modal.svelte" import Modal from "../../common/Modal.svelte"
import HandlerSelector from "./HandlerSelector.svelte" import HandlerSelector from "./HandlerSelector.svelte"
import IconButton from "../../common/IconButton.svelte" import IconButton from "../../common/IconButton.svelte"
@ -8,12 +9,12 @@
import Select from "../../common/Select.svelte" import Select from "../../common/Select.svelte"
import Input from "../../common/Input.svelte" import Input from "../../common/Input.svelte"
import getIcon from "../../common/icon" import getIcon from "../../common/icon"
import { CloseIcon } from "components/common/Icons/"
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers" import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
export let event export let event
export let eventOptions = [] export let eventOptions = []
export let open
export let onClose export let onClose
let eventType = "" let eventType = ""
@ -62,105 +63,111 @@
} }
</script> </script>
<Modal bind:isOpen={open} onClosed={closeModal}> <div class="container">
<h2> <div class="body">
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'} <div class="heading">
</h2> <h3>
<a href="https://docs.budibase.com/" target="_blank"> {eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
Click here to learn more about component events </h3>
</a> </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 class="section">
<div> <h4>Event Action(s)</h4>
<header> <HandlerSelector
<h5>Event Type</h5> newHandler
{@html getIcon('info', 20)} onChanged={updateDraftEventHandler}
</header> onCreate={() => {
<Select bind:value={eventType}> createNewEventHandler(draftEventHandler)
{#each eventOptions as option} draftEventHandler = { parameters: [] }
<option value={option.name}>{option.name}</option> }}
{/each} handler={draftEventHandler} />
</Select> </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>
</div> </div>
<div class="close-button" on:click={closeModal}>
<header> <CloseIcon />
<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> </div>
</Modal> </div>
<style> <style>
h2 { .container {
color: var(--primary100); position: relative;
font-size: 20px; }
font-weight: bold; .heading {
margin-bottom: 0; margin-bottom: 20px;
} }
h5 { .close-button {
color: rgba(22, 48, 87, 0.6); cursor: pointer;
font-size: 15px; position: absolute;
margin: 0; top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
} }
.event-options { h4 {
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;
margin-bottom: 10px; margin-bottom: 10px;
} }
a { h3 {
color: rgba(22, 48, 87, 0.6); margin: 0;
font-size: 13px; font-size: 24px;
margin-top: 0; 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> </style>

View File

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

View File

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

View File

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

View File

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

View File

@ -27,13 +27,16 @@
} }
onChange(val) onChange(val)
} }
const checkSelected = val =>
isMultiSelect ? value.includes(val) : value === val
</script> </script>
<div class="flatbutton-group"> <div class="flatbutton-group">
{#each buttonProps as props} {#each buttonProps as props}
<div class="button-container"> <div class="button-container">
<FlatButton <FlatButton
selected={value.includes(props.value)} selected={isMultiSelect ? value.includes(props.value) : value === props.value}
onClick={onButtonClicked} onClick={onButtonClicked}
{...props} /> {...props} />
</div> </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 export let item
</script> </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"> <div class="item-icon">
<i class={item.icon} /> <i class={item.icon} />
</div> </div>
@ -19,7 +19,7 @@
cursor: pointer; cursor: pointer;
margin-bottom: 8px; margin-bottom: 8px;
padding: 8px 0px 16px 0px; padding: 8px 0px 16px 0px;
width: 120px; width: 110px;
height: 80px; height: 80px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

@ -3,7 +3,6 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
import Item from "./Item.svelte" import Item from "./Item.svelte"
import { store } from "builderStore"
export let list export let list
let category = 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> <script>
import { onMount } from "svelte" import { onMount, beforeUpdate } from "svelte"
export let value = "" import { buildStyle } from "../../helpers.js"
export let onChange = value => {}
export let options = [] export let options = []
export let initialValue = "" export let value = ""
export let styleBindingProperty = "" 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 => const handleStyleBind = value =>
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {} !!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {}
$: isOptionsObject = options.every(o => typeof o === "object")
onMount(() => { onMount(() => {
if (!value && !!initialValue) { if (select) {
value = initialValue 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> </script>
<select <div
class="uk-select uk-form-small" tabindex="0"
{value} bind:this={select}
on:change={ev => onChange(ev.target.value)}> class="bb-select-container"
{#if isOptionsObject} on:click={() => toggleSelect(!open)}>
{#each options as { value, label }} <div class="bb-select-anchor selected">
<option {...handleStyleBind(value || label)} value={value || label}> <span>{displayLabel}</span>
{label} <i bind:this={icon} class="ri-arrow-down-s-fill" />
</option> </div>
{/each} <div
{:else} bind:this={selectMenu}
{#each options as value} style={menuStyle}
<option {...handleStyleBind(value)} {value}>{value}</option> class="bb-select-menu"
{/each} class:open>
{/if} <ul>
</select> {#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), title: lastPartOfName(layout),
} }
const confirmDeleteComponent = async component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const setCurrentScreenToLayout = () => { const setCurrentScreenToLayout = () => {
store.setScreenType("page") store.setScreenType("page")
$goto("./:page/page-layout") $goto("./:page/page-layout")
} }
</script> </script>
<div class="pagelayoutSection"> <div
<div class="components-nav-page">Page Layout</div> class="budibase__nav-item root"
<div class:selected={$store.currentComponentInfo._id === _layout.component.props._id}
class="budibase__nav-item root" on:click|stopPropagation={setCurrentScreenToLayout}>
class:selected={$store.currentComponentInfo._id === _layout.component.props._id} <span
on:click|stopPropagation={setCurrentScreenToLayout}> class="icon"
<span class:rotate={$store.currentPreviewItem.name !== _layout.title}>
class="icon" <ArrowDownIcon />
class:rotate={$store.currentPreviewItem.name !== _layout.title}> </span>
<ArrowDownIcon /> <i class="ri-layout-3-fill icon-big" />
</span> <span class="title">Master Screen</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> </div>
<ConfirmDialog {#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
bind:this={confirmDeleteDialog} <ComponentsHierarchyChildren
title="Confirm Delete" thisComponent={_layout.component.props}
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component?`} components={_layout.component.props._children}
okText="Delete Component" currentComponent={$store.currentComponentInfo} />
onOk={() => store.deleteComponent(componentToDelete)} /> {/if}
<style> <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 { .title {
margin-left: 10px; margin-left: 10px;
font-size: 13px; font-size: 14px;
font-weight: 400;
color: var(--ink);
} }
.icon { .icon {
width: 24px;
display: inline-block; display: inline-block;
transition: 0.2s; transition: 0.2s;
width: 20px; width: 20px;
margin-top: 2px; color: var(--ink-light);
color: #000333;
} }
.icon:nth-of-type(2) { .icon-big {
width: 14px; font-size: 24px;
margin: 0 0 0 5px; color: var(--ink-light);
} }
:global(svg) { :global(svg) {

View File

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

View File

@ -1,6 +1,7 @@
<script> <script>
import { excludeProps } from "./propertyCategories.js" import { excludeProps } from "./propertyCategories.js"
import PropertyControl from "./PropertyControl.svelte" import PropertyControl from "./PropertyControl.svelte"
import { DetailSummary } from "@budibase/bbui"
export let name = "" export let name = ""
export let styleCategory = "normal" export let styleCategory = "normal"
@ -8,75 +9,17 @@
export let componentInstance = {} export let componentInstance = {}
export let onStyleChanged = () => {} 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] || {} $: style = componentInstance["_styles"][styleCategory] || {}
</script> </script>
<div class="property-group-container"> <DetailSummary {name}>
<div class="property-group-name" on:click={() => (show = !show)}> {#each properties as props}
<div class="icon"> <PropertyControl
<i class={icon} /> label={props.label}
</div> control={props.control}
<div class="name">{capitalize(name)}</div> key={props.key}
</div> value={style[props.key]}
<div class="property-panel" class:show> onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
props={{ ...excludeProps(props, ['control', 'label']) }} />
{#each properties as props} {/each}
<PropertyControl </DetailSummary>
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>

View File

@ -2,21 +2,61 @@
import PropertyControl from "./PropertyControl.svelte" import PropertyControl from "./PropertyControl.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte" import InputGroup from "../common/Inputs/InputGroup.svelte"
import Colorpicker from "../common/Colorpicker.svelte" import Colorpicker from "../common/Colorpicker.svelte"
import { goto } from "@sveltech/routify"
import { excludeProps } from "./propertyCategories.js" import { excludeProps } from "./propertyCategories.js"
import Input from "../common/Input.svelte"
export let panelDefinition = [] export let panelDefinition = []
export let componentDefinition = {} export let componentDefinition = {}
export let componentInstance = {} export let componentInstance = {}
export let onChange = () => {} export let onChange = () => {}
export let onScreenPropChange = () => {}
export let screenOrPageInstance
const propExistsOnComponentDef = prop => prop in componentDefinition.props const propExistsOnComponentDef = prop => prop in componentDefinition.props
function handleChange(key, data) { function handleChange(key, data) {
data.target ? onChange(key, data.target.value) : onChange(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> </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} {#each panelDefinition as definition}
{#if propExistsOnComponentDef(definition.key)} {#if propExistsOnComponentDef(definition.key)}
<PropertyControl <PropertyControl

View File

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

View File

@ -27,11 +27,6 @@
settingsView.show() settingsView.show()
} }
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "") const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script> </script>
@ -86,13 +81,6 @@
<NewScreen bind:this={newScreenPicker} /> <NewScreen bind:this={newScreenPicker} />
<SettingsView bind:this={settingsView} /> <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> <style>
button { button {
cursor: pointer; cursor: pointer;
@ -120,7 +108,6 @@
background-color: var(--white); background-color: var(--white);
height: calc(100vh - 49px); height: calc(100vh - 49px);
padding: 0; padding: 0;
overflow: scroll;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -136,8 +123,7 @@
.components-pane { .components-pane {
grid-column: 3; grid-column: 3;
background-color: var(--white); background-color: var(--white);
height: 100vh; height: calc(100vh - 49px);
overflow-y: scroll;
} }
.components-nav-page { .components-nav-page {
@ -215,10 +201,6 @@
letter-spacing: 1px; letter-spacing: 1px;
} }
.border-line {
border-bottom: 1px solid #d8d8d8;
}
.components-list-container { .components-list-container {
padding: 20px 0px 0 0; padding: 20px 0px 0 0;
} }

View File

@ -1,6 +1,7 @@
import Input from "../common/Input.svelte" import Input from "../common/Input.svelte"
import OptionSelect from "./OptionSelect.svelte" import OptionSelect from "./OptionSelect.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte" import InputGroup from "../common/Inputs/InputGroup.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte"
// import Colorpicker from "../common/Colorpicker.svelte" // import Colorpicker from "../common/Colorpicker.svelte"
/* /*
TODO: Allow for default values for all properties TODO: Allow for default values for all properties
@ -11,8 +12,9 @@ export const layout = [
label: "Display", label: "Display",
key: "display", key: "display",
control: OptionSelect, control: OptionSelect,
initialValue: "Flex", initialValue: "",
options: [ options: [
{ label: "", value: "" },
{ label: "Flex", value: "flex" }, { label: "Flex", value: "flex" },
{ label: "Inline Flex", value: "inline-flex" }, { label: "Inline Flex", value: "inline-flex" },
], ],
@ -20,13 +22,16 @@ export const layout = [
{ {
label: "Direction", label: "Direction",
key: "flex-direction", key: "flex-direction",
control: OptionSelect, control: FlatButtonGroup,
initialValue: "Row", buttonProps: [
options: [ { icon: "ri-arrow-right-line", padding: "0px 5px", value: "row" },
{ label: "Row", value: "row" }, { icon: "ri-arrow-left-line", padding: "0px 5px", value: "rowReverse" },
{ label: "Row Reverse", value: "rowReverse" }, { icon: "ri-arrow-down-line", padding: "0px 5px", value: "column" },
{ label: "column", value: "column" }, {
{ label: "Column Reverse", value: "columnReverse" }, icon: "ri-arrow-up-line",
padding: "0px 5px",
value: "columnReverse",
},
], ],
}, },
{ {
@ -35,6 +40,7 @@ export const layout = [
control: OptionSelect, control: OptionSelect,
initialValue: "Flex Start", initialValue: "Flex Start",
options: [ options: [
{ label: "", value: "" },
{ label: "Flex Start", value: "flex-start" }, { label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" }, { label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" }, { label: "Center", value: "center" },
@ -60,39 +66,86 @@ export const layout = [
label: "Wrap", label: "Wrap",
key: "flex-wrap", key: "flex-wrap",
control: OptionSelect, control: OptionSelect,
initialValue: "Wrap",
options: [ options: [
{ label: "Wrap", value: "wrap" }, { label: "wrap", value: "wrap" },
{ label: "No Wrap", value: "nowrap" }, { label: "no wrap", value: "noWrap" },
{ label: "Wrap Reverse", value: "wrap-reverse" },
], ],
}, },
] ]
const spacingMeta = [ const spacingMeta = [
{ placeholder: "L" },
{ placeholder: "B" },
{ placeholder: "R" },
{ placeholder: "T" }, { placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" },
] ]
export const spacing = [ 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", label: "Padding",
key: "padding", key: "padding",
control: InputGroup, control: InputGroup,
meta: spacingMeta, meta: spacingMeta,
defaultValue: ["0", "0", "0", "0"],
}, },
] ]
export const size = [ export const size = [
{ label: "Width", key: "width", control: Input }, {
{ label: "Height", key: "height", control: Input }, label: "Width",
{ label: "Min W", key: "min-width", control: Input }, key: "width",
{ label: "Min H", key: "min-height", control: Input }, control: Input,
{ label: "Max W", key: "max-width", control: Input }, placeholder: "px",
{ label: "Max H", key: "max-height", control: Input }, 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 = [ export const position = [
@ -113,26 +166,41 @@ export const position = [
label: "Top", label: "Top",
key: "top", key: "top",
control: Input, control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
}, },
{ {
label: "Right", label: "Right",
key: "right", key: "right",
control: Input, control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
}, },
{ {
label: "Bottom", label: "Bottom",
key: "bottom", key: "bottom",
control: Input, control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
}, },
{ {
label: "Left", label: "Left",
key: "left", key: "left",
control: Input, control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
}, },
{ {
label: "Z-index", label: "Z-index",
key: "z-index", key: "z-index",
control: Input, control: Input,
placeholder: "num",
width: "48px",
textAlign: "center",
}, },
] ]
@ -166,31 +234,58 @@ export const typography = [
label: "Weight", label: "Weight",
key: "font-weight", key: "font-weight",
control: OptionSelect, control: OptionSelect,
options: [ options: ["200", "300", "400", "500", "600", "700", "800", "900"],
{ label: "100", value: "100" }, },
{ label: "200", value: "200" }, {
{ label: "300", value: "300" }, label: "size",
{ label: "400", value: "400" }, key: "font-size",
{ label: "500", value: "500" }, defaultValue: "",
{ label: "600", value: "600" }, control: Input,
{ label: "700", value: "700" }, placeholder: "px",
{ label: "800", value: "800" }, width: "48px",
{ label: "900", value: "900" }, 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", label: "Color",
key: "color", key: "color",
control: Input, control: Input,
placeholder: "hex",
}, },
{ {
label: "align", label: "align",
key: "text-align", key: "text-align",
control: OptionSelect, control: FlatButtonGroup,
options: ["initial", "left", "right", "center", "justify"], buttonProps: [
}, //custom { 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", label: "Decoration",
key: "text-decoration-line", key: "text-decoration-line",
@ -204,23 +299,51 @@ export const typography = [
{ label: "Under Over", value: "underline overline" }, { 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 = [ export const background = [
{ {
label: "Background", label: "Color",
key: "background", key: "background",
control: Input, control: Input,
}, },
{ label: "Image", key: "image", control: Input }, //custom {
label: "Image",
key: "background-image",
control: Input,
placeholder: "src",
},
] ]
export const border = [ 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", label: "Color",
key: "border-color", key: "border-color",
@ -230,6 +353,7 @@ export const border = [
label: "Style", label: "Style",
key: "border-style", key: "border-style",
control: OptionSelect, control: OptionSelect,
defaultValue: "None",
options: [ options: [
"none", "none",
"hidden", "hidden",
@ -246,30 +370,98 @@ export const border = [
] ]
export const effects = [ export const effects = [
{ label: "Opacity", key: "opacity", control: Input }, {
label: "Opacity",
key: "opacity",
control: Input,
width: "48px",
textAlign: "center",
placeholder: "%",
},
{ {
label: "Rotate", label: "Rotate",
key: "transform", key: "transform-rotate",
control: OptionSelect, control: OptionSelect,
defaultValue: "0",
options: [ options: [
{ label: "None", value: "rotate(0deg)" }, "0",
{ label: "45 degrees", value: "rotate(45deg)" }, "45deg",
{ label: "90 degrees", value: "rotate(90deg)" }, "90deg",
{ label: "135 degrees", value: "rotate(135deg)" }, "90deg",
{ label: "180 degrees", value: "rotate(180deg)" }, "135deg",
{ label: "225 degrees", value: "rotate(225deg)" }, "180deg",
{ label: "270 degrees", value: "rotate(270deg)" }, "225deg",
{ label: "315 degrees", value: "rotate(315deg)" }, "270deg",
{ label: "360 degrees", value: "rotate(360deg)" }, "315dev",
], ],
}, //needs special control }, //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 = [ export const transitions = [
{ label: "Property", key: "transition-property", control: Input }, {
{ label: "Duration", key: "transition-timing-function", control: Input }, label: "Property",
{ label: "Ease", key: "transition-ease", control: Input }, 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 = { export const all = {

View File

@ -1,6 +1,7 @@
import Input from "../common/Input.svelte" import Input from "../common/Input.svelte"
import OptionSelect from "./OptionSelect.svelte" import OptionSelect from "./OptionSelect.svelte"
import Checkbox from "../common/Checkbox.svelte" import Checkbox from "../common/Checkbox.svelte"
import ModelSelect from "components/userInterface/ModelSelect.svelte"
import { all } from "./propertyCategories.js" import { all } from "./propertyCategories.js"
@ -10,6 +11,18 @@ export default {
name: "Basic", name: "Basic",
isCategory: true, isCategory: true,
children: [ 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", _component: "@budibase/standard-components/container",
name: "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", _component: "@budibase/standard-components/icon",
name: "Icon", name: "Icon",
@ -229,29 +253,79 @@ export default {
"A basic card component that can contain content and actions.", "A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-fill", icon: "ri-layout-bottom-fill",
children: [], 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", name: "Login",
_component: "@budibase/standard-components/login",
description: description:
"A component that automatically generates a login screen for your app.", "A component that automatically generates a login screen for your app.",
icon: "ri-login-box-fill", icon: "ri-login-box-fill",
children: [], children: [],
properties: { design: { ...all } }, properties: {
design: { ...all },
settings: [
{
label: "Name",
key: "name",
control: Input,
},
{
label: "Logo",
key: "logo",
control: Input,
},
],
},
}, },
{ {
name: "Table", name: "Table",
_component: "@budibase/standard-components/datatable",
description: "A component that generates a table from your data.", description: "A component that generates a table from your data.",
icon: "ri-archive-drawer-fill", icon: "ri-archive-drawer-fill",
properties: { design: { ...all } }, properties: {
design: { ...all },
settings: [{ label: "Model", key: "model", control: ModelSelect }],
},
children: [], children: [],
}, },
{ {
name: "Form", name: "Form",
description: "A component that generates a form from your data.", description: "A component that generates a form from your data.",
icon: "ri-file-edit-fill", icon: "ri-file-edit-fill",
properties: { design: { ...all } }, properties: {
_component: "@budibase/materialdesign-components/Form", design: { ...all },
settings: [{ label: "Model", key: "model", control: ModelSelect }],
},
_component: "@budibase/standard-components/dataform",
template: { template: {
component: "@budibase/materialdesign-components/Form", component: "@budibase/materialdesign-components/Form",
description: "Form for saving a record", description: "Form for saving a record",
@ -264,15 +338,53 @@ export default {
_component: "@budibase/standard-components/datachart", _component: "@budibase/standard-components/datachart",
description: "Shiny chart", description: "Shiny chart",
icon: "ri-bar-chart-fill", 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: [], children: [],
}, },
{ {
name: "List", name: "List",
_component: "@budibase/standard-components/datalist", _component: "@budibase/standard-components/list",
description: "Shiny list", description: "Shiny list",
icon: "ri-file-list-fill", icon: "ri-file-list-fill",
properties: { design: { ...all } }, properties: {
design: { ...all },
settings: [{ label: "Model", key: "model", control: ModelSelect }],
},
children: [], children: [],
}, },
{ {
@ -306,7 +418,15 @@ export default {
"A component for handling the navigation within your app.", "A component for handling the navigation within your app.",
icon: "ri-navigation-fill", icon: "ri-navigation-fill",
children: [], 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