Merge branch 'master' of https://github.com/Budibase/budibase into property-panel/colorpicker
|
@ -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 |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -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 |
After Width: | Height: | Size: 201 KiB |
After Width: | Height: | Size: 105 KiB |
|
@ -38,20 +38,23 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.3.5",
|
||||
"@beyonk/svelte-notifications": "^2.0.3",
|
||||
"@budibase/bbui": "^1.1.1",
|
||||
"@budibase/client": "^0.0.32",
|
||||
"@nx-js/compiler-util": "^2.0.0",
|
||||
"codemirror": "^5.51.0",
|
||||
"date-fns": "^1.29.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"feather-icons": "^4.21.0",
|
||||
"flatpickr": "^4.5.7",
|
||||
"lodash": "^4.17.13",
|
||||
"logrocket": "^1.0.6",
|
||||
"lunr": "^2.3.5",
|
||||
"mustache": "^4.0.1",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"shortid": "^2.2.8",
|
||||
"string_decoder": "^1.2.0",
|
||||
"svelte-simple-modal": "^0.3.0",
|
||||
"svelte-simple-modal": "^0.4.2",
|
||||
"uikit": "^3.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -83,4 +86,4 @@
|
|||
"svelte": "3.23.x"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
import AppNotification, {
|
||||
showAppNotification,
|
||||
} from "components/common/AppNotification.svelte"
|
||||
import { NotificationDisplay } from "@beyonk/svelte-notifications"
|
||||
|
||||
function showErrorBanner() {
|
||||
showAppNotification({
|
||||
|
@ -26,4 +27,7 @@
|
|||
|
||||
<AppNotification />
|
||||
|
||||
<!-- svelte-notifications -->
|
||||
<NotificationDisplay />
|
||||
|
||||
<Router {routes} />
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
/* Budibase Component Styles */
|
||||
.header {
|
||||
font-size: 0.75rem;
|
||||
color: #000333;
|
||||
opacity: 0.4;
|
||||
color: var(--ink);
|
||||
text-transform: uppercase;
|
||||
margin-top: 1rem;
|
||||
font-weight: 500;
|
||||
|
@ -57,35 +56,34 @@
|
|||
|
||||
.budibase__nav-item {
|
||||
cursor: pointer;
|
||||
padding: 0 7px 0 3px;
|
||||
padding: 0 4px 0 2px;
|
||||
height: 35px;
|
||||
margin: 5px 20px 5px 0px;
|
||||
margin: 5px 0px 4px 0px;
|
||||
border-radius: 0 5px 5px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.budibase__nav-item.selected {
|
||||
color: var(--button-text);
|
||||
background: #f1f4fc;
|
||||
color: var(--ink);
|
||||
background: var(--blue-light);
|
||||
}
|
||||
|
||||
.budibase__nav-item:hover {
|
||||
background: #fafafa;
|
||||
background: var(--grey-light);
|
||||
}
|
||||
|
||||
.budibase__input {
|
||||
width: 250px;
|
||||
height: 35px;
|
||||
width: 220px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #DBDBDB;
|
||||
border: 1px solid var(--grey-dark);
|
||||
text-align: left;
|
||||
letter-spacing: 0.7px;
|
||||
color: #000333;
|
||||
font-size: 16px;
|
||||
padding-left: 5px;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.uk-text-right {
|
||||
|
@ -102,27 +100,32 @@
|
|||
}
|
||||
|
||||
.budibase__table {
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid var(--grey-dark);
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.budibase__table thead {
|
||||
background: #fafafa;
|
||||
background: var(--blue-light);
|
||||
}
|
||||
|
||||
.budibase__table thead > tr > th {
|
||||
color: var(--button-text);
|
||||
color: var(--ink);
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.budibase__table tr {
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-bottom: 1px solid var(--grey-light);
|
||||
}
|
||||
|
||||
.button--toggled {
|
||||
background: #fafafa;
|
||||
color: var(--button-text);
|
||||
padding: 10px;
|
||||
background: var(--blue-light);
|
||||
color: var(--ink-light);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
|
@ -3,6 +3,7 @@ const apiCall = method => async (url, body) => {
|
|||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-user-agent": "Budibase Builder",
|
||||
},
|
||||
body: body && JSON.stringify(body),
|
||||
})
|
||||
|
@ -14,14 +15,16 @@ const apiCall = method => async (url, body) => {
|
|||
return response
|
||||
}
|
||||
|
||||
const post = apiCall("POST")
|
||||
const get = apiCall("GET")
|
||||
const patch = apiCall("PATCH")
|
||||
const del = apiCall("DELETE")
|
||||
export const post = apiCall("POST")
|
||||
export const get = apiCall("GET")
|
||||
export const patch = apiCall("PATCH")
|
||||
export const del = apiCall("DELETE")
|
||||
export const put = apiCall("PUT")
|
||||
|
||||
export default {
|
||||
post,
|
||||
get,
|
||||
patch,
|
||||
delete: del,
|
||||
put,
|
||||
}
|
||||
|
|
|
@ -17,33 +17,22 @@ export const generate_screen_css = component_arr => {
|
|||
|
||||
export const generate_css = style => {
|
||||
let cssString = Object.entries(style).reduce((str, [key, value]) => {
|
||||
//TODO Handle arrays and objects here also
|
||||
if (typeof value === "string") {
|
||||
if (value) {
|
||||
return (str += `${key}: ${value};\n`)
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
if (value.length > 0 && !value.every(v => v === "")) {
|
||||
return (str += `${key}: ${value
|
||||
.map(generate_array_styles)
|
||||
.join(" ")};\n`)
|
||||
return (str += `${key}: ${value.join(" ")};\n`)
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}, "")
|
||||
|
||||
return (cssString || "").trim()
|
||||
}
|
||||
|
||||
export const generate_array_styles = item => {
|
||||
let safeItem = item === "" ? 0 : item
|
||||
let hasPx = new RegExp("px$")
|
||||
if (!hasPx.test(safeItem)) {
|
||||
return `${safeItem}px`
|
||||
} else {
|
||||
return safeItem
|
||||
}
|
||||
}
|
||||
|
||||
export const apply_class = (id, name = "element", styles, selector) => {
|
||||
if (selector === "normal") {
|
||||
return `.${name}-${id} {\n${styles}\n}`
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { getStore } from "./store"
|
||||
import { getBackendUiStore } from "./store/backend"
|
||||
import { getWorkflowStore } from "./store/workflow/"
|
||||
import LogRocket from "logrocket"
|
||||
|
||||
export const store = getStore()
|
||||
export const backendUiStore = getBackendUiStore()
|
||||
export const workflowStore = getWorkflowStore()
|
||||
|
||||
export const initialise = async () => {
|
||||
try {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { get } from "builderStore/api"
|
||||
|
||||
/**
|
||||
* Fetches the definitions for component library components. This includes
|
||||
* their props and other metadata from components.json.
|
||||
|
@ -6,7 +8,7 @@
|
|||
export const fetchComponentLibDefinitions = async appId => {
|
||||
const LIB_DEFINITION_URL = `/${appId}/components/definitions`
|
||||
try {
|
||||
const libDefinitionResponse = await fetch(LIB_DEFINITION_URL)
|
||||
const libDefinitionResponse = await get(LIB_DEFINITION_URL)
|
||||
return await libDefinitionResponse.json()
|
||||
} catch (err) {
|
||||
console.error(`Error fetching component definitions for ${appId}`, err)
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api from "../api"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
/** TODO: DEMO SOLUTION
|
||||
* this section should not be here, it is a quick fix for a demo
|
||||
* when we reorg the backend UI, this should disappear
|
||||
* **/
|
||||
import { CreateEditModelModal } from "components/database/ModelDataTable/modals"
|
||||
/** DEMO SOLUTION END **/
|
||||
|
||||
export const getBackendUiStore = () => {
|
||||
const INITIAL_BACKEND_UI_STATE = {
|
||||
|
@ -22,11 +30,27 @@ export const getBackendUiStore = () => {
|
|||
const views = await viewsResponse.json()
|
||||
store.update(state => {
|
||||
state.selectedDatabase = db
|
||||
if (models && models.length > 0) {
|
||||
state.selectedModel = models[0]
|
||||
state.selectedView = `all_${models[0]._id}`
|
||||
}
|
||||
state.breadcrumbs = [db.name]
|
||||
state.models = models
|
||||
state.views = views
|
||||
return state
|
||||
})
|
||||
/** TODO: DEMO SOLUTION**/
|
||||
if (!models || models.length === 0) {
|
||||
const { open, close } = getContext("simple-modal")
|
||||
open(
|
||||
CreateEditModelModal,
|
||||
{
|
||||
onClosed: close,
|
||||
},
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
/** DEMO SOLUTION END **/
|
||||
},
|
||||
},
|
||||
records: {
|
||||
|
@ -51,6 +75,8 @@ export const getBackendUiStore = () => {
|
|||
store.update(state => {
|
||||
state.models.push(model)
|
||||
state.models = state.models
|
||||
state.selectedModel = model
|
||||
state.selectedView = `all_${model._id}`
|
||||
return state
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { cloneDeep, values } from "lodash/fp"
|
||||
import { values } from "lodash/fp"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import * as backendStoreActions from "./backend"
|
||||
import { writable, get } from "svelte/store"
|
||||
import api from "../api"
|
||||
import { DEFAULT_PAGES_OBJECT } from "../../constants"
|
||||
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
|
||||
import { rename } from "components/userInterface/pagesParsing/renameScreen"
|
||||
import {
|
||||
createProps,
|
||||
makePropsSafe,
|
||||
|
@ -16,6 +15,16 @@ import { buildCodeForScreens } from "../buildCodeForScreens"
|
|||
import { generate_screen_css } from "../generate_css"
|
||||
import { insertCodeMetadata } from "../insertCodeMetadata"
|
||||
import { uuid } from "../uuid"
|
||||
import {
|
||||
selectComponent as _selectComponent,
|
||||
getParent,
|
||||
walkProps,
|
||||
savePage as _savePage,
|
||||
saveCurrentPreviewItem as _saveCurrentPreviewItem,
|
||||
saveScreenApi as _saveScreenApi,
|
||||
regenerateCssForCurrentScreen,
|
||||
renameCurrentScreen,
|
||||
} from "../storeUtils"
|
||||
|
||||
export const getStore = () => {
|
||||
const initial = {
|
||||
|
@ -43,7 +52,6 @@ export const getStore = () => {
|
|||
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
|
||||
|
||||
store.saveScreen = saveScreen(store)
|
||||
store.renameScreen = renameScreen(store)
|
||||
store.deleteScreen = deleteScreen(store)
|
||||
store.setCurrentScreen = setCurrentScreen(store)
|
||||
store.setCurrentPage = setCurrentPage(store)
|
||||
|
@ -54,13 +62,10 @@ export const getStore = () => {
|
|||
store.addChildComponent = addChildComponent(store)
|
||||
store.selectComponent = selectComponent(store)
|
||||
store.setComponentProp = setComponentProp(store)
|
||||
store.setPageOrScreenProp = setPageOrScreenProp(store)
|
||||
store.setComponentStyle = setComponentStyle(store)
|
||||
store.setComponentCode = setComponentCode(store)
|
||||
store.setScreenType = setScreenType(store)
|
||||
store.deleteComponent = deleteComponent(store)
|
||||
store.moveUpComponent = moveUpComponent(store)
|
||||
store.moveDownComponent = moveDownComponent(store)
|
||||
store.copyComponent = copyComponent(store)
|
||||
store.getPathToComponent = getPathToComponent(store)
|
||||
store.addTemplatedComponent = addTemplatedComponent(store)
|
||||
store.setMetadataProp = setMetadataProp(store)
|
||||
|
@ -69,6 +74,9 @@ export const getStore = () => {
|
|||
|
||||
export default getStore
|
||||
|
||||
export const getComponentDefinition = (state, name) =>
|
||||
name.startsWith("##") ? getBuiltin(name) : state.components[name]
|
||||
|
||||
const setPackage = (store, initial) => async pkg => {
|
||||
const [main_screens, unauth_screens] = await Promise.all([
|
||||
api
|
||||
|
@ -140,12 +148,6 @@ const _saveScreen = async (store, s, screen) => {
|
|||
return s
|
||||
}
|
||||
|
||||
const _saveScreenApi = (screen, s) => {
|
||||
api
|
||||
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
|
||||
.then(() => _savePage(s))
|
||||
}
|
||||
|
||||
const createScreen = store => (screenName, route, layoutComponentName) => {
|
||||
store.update(state => {
|
||||
const rootComponent = state.components[layoutComponentName]
|
||||
|
@ -155,7 +157,6 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
|
|||
description: "",
|
||||
url: "",
|
||||
_css: "",
|
||||
uiFunctions: "",
|
||||
props: createProps(rootComponent).props,
|
||||
}
|
||||
|
||||
|
@ -173,11 +174,10 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
|
|||
const setCurrentScreen = store => screenName => {
|
||||
store.update(s => {
|
||||
const screen = getExactComponent(s.screens, screenName)
|
||||
screen._css = generate_screen_css([screen.props])
|
||||
s.currentPreviewItem = screen
|
||||
s.currentFrontEndType = "screen"
|
||||
s.currentView = "detail"
|
||||
|
||||
regenerateCssForCurrentScreen(s)
|
||||
const safeProps = makePropsSafe(
|
||||
s.components[screen.props._component],
|
||||
screen.props
|
||||
|
@ -207,46 +207,6 @@ const deleteScreen = store => name => {
|
|||
})
|
||||
}
|
||||
|
||||
const renameScreen = store => (oldname, newname) => {
|
||||
store.update(s => {
|
||||
const { screens, pages, error, changedScreens } = rename(
|
||||
s.pages,
|
||||
s.screens,
|
||||
oldname,
|
||||
newname
|
||||
)
|
||||
|
||||
if (error) {
|
||||
// should really do something with this
|
||||
return s
|
||||
}
|
||||
|
||||
s.screens = screens
|
||||
s.pages = pages
|
||||
if (s.currentPreviewItem.name === oldname)
|
||||
s.currentPreviewItem.name = newname
|
||||
|
||||
const saveAllChanged = async () => {
|
||||
for (let screenName of changedScreens) {
|
||||
const changedScreen = getExactComponent(screens, screenName)
|
||||
await api.post(`/_builder/api/${s.appId}/screen`, changedScreen)
|
||||
}
|
||||
}
|
||||
|
||||
api
|
||||
.patch(`/_builder/api/${s.appId}/screen`, {
|
||||
oldname,
|
||||
newname,
|
||||
})
|
||||
.then(() => saveAllChanged())
|
||||
.then(() => {
|
||||
_savePage(s)
|
||||
})
|
||||
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
const savePage = store => async page => {
|
||||
store.update(state => {
|
||||
if (state.currentFrontEndType !== "page" || !state.currentPageName) {
|
||||
|
@ -277,15 +237,6 @@ const removeStylesheet = store => stylesheet => {
|
|||
})
|
||||
}
|
||||
|
||||
const _savePage = async s => {
|
||||
const page = s.pages[s.currentPageName]
|
||||
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, {
|
||||
page: { componentLibraries: s.pages.componentLibraries, ...page },
|
||||
uiFunctions: s.currentPageFunctions,
|
||||
screens: page._screens,
|
||||
})
|
||||
}
|
||||
|
||||
const setCurrentPage = store => pageName => {
|
||||
store.update(state => {
|
||||
const current_screens = state.pages[pageName]._screens
|
||||
|
@ -304,9 +255,7 @@ const setCurrentPage = store => pageName => {
|
|||
state.currentComponentInfo = safeProps
|
||||
currentPage.props = safeProps
|
||||
state.currentPreviewItem = state.pages[pageName]
|
||||
state.currentPreviewItem._css = generate_screen_css([
|
||||
state.currentPreviewItem.props,
|
||||
])
|
||||
regenerateCssForCurrentScreen(state)
|
||||
|
||||
for (let screen of state.screens) {
|
||||
screen._css = generate_screen_css([screen.props])
|
||||
|
@ -317,8 +266,6 @@ const setCurrentPage = store => pageName => {
|
|||
})
|
||||
}
|
||||
|
||||
// const getComponentDefinition = (components, name) => components.find(c => c.name === name)
|
||||
|
||||
/**
|
||||
* @param {string} componentToAdd - name of the component to add to the application
|
||||
* @param {string} presetName - name of the component preset if defined
|
||||
|
@ -344,9 +291,7 @@ const addChildComponent = store => (componentToAdd, presetName) => {
|
|||
return state
|
||||
}
|
||||
|
||||
const component = componentToAdd.startsWith("##")
|
||||
? getBuiltin(componentToAdd)
|
||||
: state.components[componentToAdd]
|
||||
const component = getComponentDefinition(state, componentToAdd)
|
||||
|
||||
const presetProps = presetName ? component.presets[presetName] : {}
|
||||
|
||||
|
@ -379,6 +324,7 @@ const addChildComponent = store => (componentToAdd, presetName) => {
|
|||
/**
|
||||
* @param {string} props - props to add, as child of current component
|
||||
*/
|
||||
|
||||
const addTemplatedComponent = store => props => {
|
||||
store.update(state => {
|
||||
walkProps(props, p => {
|
||||
|
@ -387,9 +333,7 @@ const addTemplatedComponent = store => props => {
|
|||
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
|
||||
props
|
||||
)
|
||||
state.currentPreviewItem._css = generate_screen_css([
|
||||
state.currentPreviewItem.props,
|
||||
])
|
||||
regenerateCssForCurrentScreen(state)
|
||||
|
||||
setCurrentPageFunctions(state)
|
||||
_saveCurrentPreviewItem(state)
|
||||
|
@ -400,12 +344,7 @@ const addTemplatedComponent = store => props => {
|
|||
|
||||
const selectComponent = store => component => {
|
||||
store.update(state => {
|
||||
const componentDef = component._component.startsWith("##")
|
||||
? component
|
||||
: state.components[component._component]
|
||||
state.currentComponentInfo = makePropsSafe(componentDef, component)
|
||||
state.currentView = "component"
|
||||
return state
|
||||
return _selectComponent(state, component)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -421,6 +360,18 @@ const setComponentProp = store => (name, value) => {
|
|||
})
|
||||
}
|
||||
|
||||
const setPageOrScreenProp = store => (name, value) => {
|
||||
store.update(state => {
|
||||
if (name === "name" && state.currentFrontEndType === "screen") {
|
||||
state = renameCurrentScreen(value, state)
|
||||
} else {
|
||||
state.currentPreviewItem[name] = value
|
||||
_saveCurrentPreviewItem(state)
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const setComponentStyle = store => (type, name, value) => {
|
||||
store.update(state => {
|
||||
if (!state.currentComponentInfo._styles) {
|
||||
|
@ -428,9 +379,7 @@ const setComponentStyle = store => (type, name, value) => {
|
|||
}
|
||||
state.currentComponentInfo._styles[type][name] = value
|
||||
|
||||
state.currentPreviewItem._css = generate_screen_css([
|
||||
state.currentPreviewItem.props,
|
||||
])
|
||||
regenerateCssForCurrentScreen(state)
|
||||
|
||||
// save without messing with the store
|
||||
_saveCurrentPreviewItem(state)
|
||||
|
@ -472,75 +421,6 @@ const setScreenType = store => type => {
|
|||
})
|
||||
}
|
||||
|
||||
const deleteComponent = store => componentName => {
|
||||
store.update(state => {
|
||||
const parent = getParent(state.currentPreviewItem.props, componentName)
|
||||
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(
|
||||
component => component !== componentName
|
||||
)
|
||||
}
|
||||
|
||||
_saveCurrentPreviewItem(state)
|
||||
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const moveUpComponent = store => component => {
|
||||
store.update(s => {
|
||||
const parent = getParent(s.currentPreviewItem.props, component)
|
||||
|
||||
if (parent) {
|
||||
const currentIndex = parent._children.indexOf(component)
|
||||
if (currentIndex === 0) return s
|
||||
|
||||
const newChildren = parent._children.filter(c => c !== component)
|
||||
newChildren.splice(currentIndex - 1, 0, component)
|
||||
parent._children = newChildren
|
||||
}
|
||||
s.currentComponentInfo = component
|
||||
_saveCurrentPreviewItem(s)
|
||||
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
const moveDownComponent = store => component => {
|
||||
store.update(s => {
|
||||
const parent = getParent(s.currentPreviewItem.props, component)
|
||||
|
||||
if (parent) {
|
||||
const currentIndex = parent._children.indexOf(component)
|
||||
if (currentIndex === parent._children.length - 1) return s
|
||||
|
||||
const newChildren = parent._children.filter(c => c !== component)
|
||||
newChildren.splice(currentIndex + 1, 0, component)
|
||||
parent._children = newChildren
|
||||
}
|
||||
s.currentComponentInfo = component
|
||||
_saveCurrentPreviewItem(s)
|
||||
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
const copyComponent = store => component => {
|
||||
store.update(s => {
|
||||
const parent = getParent(s.currentPreviewItem.props, component)
|
||||
const copiedComponent = cloneDeep(component)
|
||||
walkProps(copiedComponent, p => {
|
||||
p._id = uuid()
|
||||
})
|
||||
parent._children = [...parent._children, copiedComponent]
|
||||
s.curren
|
||||
_saveCurrentPreviewItem(s)
|
||||
s.currentComponentInfo = copiedComponent
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
const getPathToComponent = store => component => {
|
||||
// Gets all the components to needed to construct a path.
|
||||
const tempStore = get(store)
|
||||
|
@ -572,39 +452,9 @@ const getPathToComponent = store => component => {
|
|||
return path
|
||||
}
|
||||
|
||||
const getParent = (rootProps, child) => {
|
||||
let parent
|
||||
walkProps(rootProps, (p, breakWalk) => {
|
||||
if (p._children && p._children.includes(child)) {
|
||||
parent = p
|
||||
breakWalk()
|
||||
}
|
||||
})
|
||||
return parent
|
||||
}
|
||||
|
||||
const walkProps = (props, action, cancelToken = null) => {
|
||||
cancelToken = cancelToken || { cancelled: false }
|
||||
action(props, () => {
|
||||
cancelToken.cancelled = true
|
||||
})
|
||||
|
||||
if (props._children) {
|
||||
for (let child of props._children) {
|
||||
if (cancelToken.cancelled) return
|
||||
walkProps(child, action, cancelToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setMetadataProp = store => (name, prop) => {
|
||||
store.update(s => {
|
||||
s.currentPreviewItem[name] = prop
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
const _saveCurrentPreviewItem = s =>
|
||||
s.currentFrontEndType === "page"
|
||||
? _savePage(s)
|
||||
: _saveScreenApi(s.currentPreviewItem, s)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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();
|
||||
})
|
||||
})
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
export let disabled = false
|
||||
export let hidden = false
|
||||
export let secondary = false
|
||||
export let primary = true
|
||||
export let cancel = false
|
||||
export let alert = false
|
||||
|
@ -11,6 +12,7 @@
|
|||
on:click
|
||||
class="button"
|
||||
class:hidden
|
||||
class:secondary
|
||||
class:primary
|
||||
class:alert
|
||||
class:cancel
|
||||
|
@ -22,12 +24,14 @@
|
|||
<style>
|
||||
.primary {
|
||||
color: #ffffff;
|
||||
background: #0055ff;
|
||||
background: var(--blue);
|
||||
border: solid 1px var(--blue);
|
||||
}
|
||||
|
||||
.alert {
|
||||
color: rgba(255, 0, 31, 1);
|
||||
background: rgba(255, 0, 31, 0.1);
|
||||
color: white;
|
||||
background: #e26d69;
|
||||
border: solid 1px #e26d69;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
|
@ -35,18 +39,22 @@
|
|||
background: none;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--ink);
|
||||
border: solid 1px var(--grey-dark);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
border-radius: 3px;
|
||||
padding: 10px 20px;
|
||||
height: 45px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
filter: saturate(90%);
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
margin-left: 20px;
|
||||
}
|
||||
|
||||
:global(.refresh-page-button):hover{
|
||||
:global(.refresh-page-button):hover {
|
||||
background: var(--grey-light);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -14,8 +14,7 @@
|
|||
background: var(--secondary80);
|
||||
color: var(--white);
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
width: 95%;
|
||||
height: 100px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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 |
|
@ -31,3 +31,4 @@ export { default as EmailIcon } from "./Email.svelte"
|
|||
export { default as TwitterIcon } from "./Twitter.svelte"
|
||||
export { default as InfoIcon } from "./Info.svelte"
|
||||
export { default as CloseIcon } from "./Close.svelte"
|
||||
export { default as MoreIcon } from "./More.svelte"
|
||||
|
|
|
@ -1,30 +1,61 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { buildStyle } from "../../helpers.js"
|
||||
export let value = ""
|
||||
export let width = ""
|
||||
export let textAlign = "left"
|
||||
export let width = "160px"
|
||||
export let placeholder = ""
|
||||
export let suffix = ""
|
||||
export let onChange = val => {}
|
||||
|
||||
let style = { width }
|
||||
let centerPlaceholder = textAlign === "center"
|
||||
|
||||
let style = buildStyle({ width, textAlign })
|
||||
|
||||
function handleChange(val) {
|
||||
value = val
|
||||
let _value = value !== "auto" ? value + suffix : value
|
||||
onChange(_value)
|
||||
}
|
||||
|
||||
$: displayValue =
|
||||
suffix && value && value.endsWith(suffix)
|
||||
? value.replace(new RegExp(`${suffix}$`), "")
|
||||
: value || ""
|
||||
</script>
|
||||
|
||||
<input type="text" style={`width: ${width};`} on:change bind:value />
|
||||
<input
|
||||
class:centerPlaceholder
|
||||
type="text"
|
||||
value={displayValue}
|
||||
{placeholder}
|
||||
{style}
|
||||
on:change={e => handleChange(e.target.value)} />
|
||||
|
||||
<style>
|
||||
input {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
line-height: 1.3;
|
||||
padding: 12px;
|
||||
width: 164px;
|
||||
float: right;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: #fff;
|
||||
border: 1px solid var(--grey-dark);
|
||||
/* width: 32px; */
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin: 0px 0px 0px 1px;
|
||||
color: var(--ink);
|
||||
opacity: 0.7;
|
||||
padding: 0px 4px;
|
||||
line-height: 1.3;
|
||||
/* padding: 12px; */
|
||||
width: 164px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--grey);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.centerPlaceholder::placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,74 +1,50 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import Input from "../Input.svelte"
|
||||
|
||||
export let meta = []
|
||||
export let label = ""
|
||||
export let value = [0, 0, 0, 0]
|
||||
export let type = "number"
|
||||
export let value = ["0", "0", "0", "0"]
|
||||
export let suffix = ""
|
||||
|
||||
export let onChange = () => {}
|
||||
|
||||
function handleChange(val, idx) {
|
||||
value.splice(idx, 1, val)
|
||||
value.splice(idx, 1, val !== "auto" && suffix ? val + suffix : val)
|
||||
|
||||
value = value
|
||||
onChange(value)
|
||||
let _value = value.map(v =>
|
||||
suffix && !v.endsWith(suffix) && v !== "auto" ? v + suffix : v
|
||||
)
|
||||
onChange(_value)
|
||||
}
|
||||
|
||||
$: displayValues =
|
||||
value && suffix
|
||||
? value.map(v => v.replace(new RegExp(`${suffix}$`), ""))
|
||||
: value || []
|
||||
</script>
|
||||
|
||||
<div class="input-container">
|
||||
<div class="label">{label}</div>
|
||||
<div class="inputs">
|
||||
{#each meta as { placeholder }, i}
|
||||
<input
|
||||
{type}
|
||||
placeholder={placeholder || ''}
|
||||
value={!value || value[i] === 0 ? '' : value[i]}
|
||||
on:change={e => handleChange(e.target.value || 0, i)} />
|
||||
<div class="inputs-group">
|
||||
{#each meta as m, i}
|
||||
<Input
|
||||
width="37px"
|
||||
textAlign="center"
|
||||
placeholder={m.placeholder || ''}
|
||||
value={!displayValues || displayValues[i] === '0' ? '' : displayValues[i]}
|
||||
onChange={value => handleChange(value || 0, i)} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.input-container {
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
.inputs-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin: 0px 0px 0px 1px;
|
||||
text-align: center;
|
||||
color: var(--ink);
|
||||
opacity: 0.7;
|
||||
padding: 0px 4px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--grey);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
float: right;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -50,10 +50,10 @@
|
|||
<style>
|
||||
.uk-modal-dialog {
|
||||
border-radius: 0.3rem;
|
||||
width: 60%;
|
||||
width: 520px;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
padding: 40px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,11 +20,8 @@
|
|||
<style>
|
||||
.select-container {
|
||||
font-size: 14px;
|
||||
color: var(--secondary60);
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
min-width: 275px;
|
||||
border: var(--grey-dark) 1px solid;
|
||||
}
|
||||
|
||||
.adjusted {
|
||||
|
@ -43,7 +40,7 @@
|
|||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #000333;
|
||||
color: var(--ink);
|
||||
padding: 0 40px 0px 20px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
@ -63,6 +60,6 @@
|
|||
width: 30px;
|
||||
height: 30px;
|
||||
pointer-events: none;
|
||||
color: var(--secondary100);
|
||||
color: var(--ink);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
|
@ -1,13 +1,8 @@
|
|||
import { eventHandlers } from "../../../../client/src/state/eventHandlers"
|
||||
import { writable } from "svelte/store"
|
||||
export { EVENT_TYPE_MEMBER_NAME } from "../../../../client/src/state/eventHandlers"
|
||||
|
||||
export const allHandlers = user => {
|
||||
const store = writable({
|
||||
_bbuser: user,
|
||||
})
|
||||
|
||||
const handlersObj = eventHandlers(store)
|
||||
export const allHandlers = () => {
|
||||
const handlersObj = eventHandlers()
|
||||
|
||||
const handlers = Object.keys(handlersObj).map(name => ({
|
||||
name,
|
||||
|
|
|
@ -43,13 +43,6 @@
|
|||
)
|
||||
}
|
||||
|
||||
async function selectRecord(record) {
|
||||
return await api.loadRecord(record.key, {
|
||||
appname: $store.appname,
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
})
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 10
|
||||
// Internal headers we want to hide from the user
|
||||
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
|
||||
|
@ -152,19 +145,19 @@
|
|||
}
|
||||
|
||||
table {
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid var(--grey-dark);
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #ccc;
|
||||
background: var(--blue-light);
|
||||
border: 1px solid var(--grey-dark);
|
||||
}
|
||||
|
||||
thead th {
|
||||
color: var(--button-text);
|
||||
color: var(--ink);
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
|
@ -173,14 +166,14 @@
|
|||
}
|
||||
|
||||
tbody tr {
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-bottom: 1px solid var(--grey-dark);
|
||||
transition: 0.3s background-color;
|
||||
color: var(--secondary100);
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: #fafafa;
|
||||
background: var(--grey-light);
|
||||
}
|
||||
|
||||
.table-controls {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import api from "builderStore/api"
|
||||
|
||||
export async function createUser(user, appId, instanceId) {
|
||||
export async function createUser(user, instanceId) {
|
||||
const CREATE_USER_URL = `/api/${instanceId}/users`
|
||||
const response = await api.post(CREATE_USER_URL, user)
|
||||
return await response.json()
|
||||
|
@ -28,7 +28,7 @@ export async function saveRecord(record, instanceId, modelId) {
|
|||
}
|
||||
|
||||
export async function fetchDataForView(viewName, instanceId) {
|
||||
const FETCH_RECORDS_URL = `/api/${instanceId}/${viewName}/records`
|
||||
const FETCH_RECORDS_URL = `/api/${instanceId}/views/${viewName}`
|
||||
|
||||
const response = await api.get(FETCH_RECORDS_URL)
|
||||
return await response.json()
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<heading>
|
||||
<div class="heading">
|
||||
{#if !showFieldView}
|
||||
<i class="ri-list-settings-line button--toggled" />
|
||||
<h3 class="budibase__title--3">Create / Edit Model</h3>
|
||||
|
@ -43,22 +43,20 @@
|
|||
<i class="ri-file-list-line button--toggled" />
|
||||
<h3 class="budibase__title--3">Create / Edit Field</h3>
|
||||
{/if}
|
||||
</heading>
|
||||
</div>
|
||||
{#if !showFieldView}
|
||||
<div class="padding">
|
||||
<h4 class="budibase__label--big">Settings</h4>
|
||||
|
||||
{#if $store.errors && $store.errors.length > 0}
|
||||
<ErrorsBox errors={$store.errors} />
|
||||
{/if}
|
||||
|
||||
<Textbox label="Name" bind:text={model.name} />
|
||||
|
||||
<div class="textbox">
|
||||
<Textbox label="Name" bind:text={model.name} />
|
||||
</div>
|
||||
<div class="table-controls">
|
||||
<span class="budibase__label--big">Fields</span>
|
||||
<h4 class="hoverable new-field" on:click={() => (showFieldView = true)}>
|
||||
<span class="label">Fields</span>
|
||||
<div class="hoverable new-field" on:click={() => (showFieldView = true)}>
|
||||
Add new field
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="uk-table fields-table budibase__table">
|
||||
|
@ -67,7 +65,6 @@
|
|||
<th>Edit</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Values</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -90,9 +87,9 @@
|
|||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="uk-margin">
|
||||
<footer>
|
||||
<ActionButton color="secondary" on:click={saveModel}>Save</ActionButton>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
{:else}
|
||||
<FieldView
|
||||
|
@ -104,41 +101,63 @@
|
|||
|
||||
<style>
|
||||
.padding {
|
||||
padding: 20px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
margin: 0px 40px 0px 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.new-field {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: var(--button-text);
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.fields-table {
|
||||
margin: 1rem 1rem 0rem 0rem;
|
||||
margin: 8px 40px 0px 40px;
|
||||
border-collapse: collapse;
|
||||
width: 88%;
|
||||
}
|
||||
|
||||
tbody > tr:hover {
|
||||
background-color: var(--primary10);
|
||||
background-color: var(--grey-light);
|
||||
}
|
||||
|
||||
.table-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0px 40px;
|
||||
}
|
||||
|
||||
.ri-more-line:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
heading {
|
||||
padding: 20px 20px 0 20px;
|
||||
.heading {
|
||||
padding: 40px 40px 0 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 0 10px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: var(--grey-light);
|
||||
margin-top: 40px;
|
||||
padding: 20px 40px 20px 40px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,17 +13,41 @@
|
|||
|
||||
const FIELD_TYPES = ["string", "number", "boolean"]
|
||||
|
||||
export let field = { type: "string" }
|
||||
export let field = {
|
||||
type: "string",
|
||||
constraints: { type: "string", presence: false },
|
||||
}
|
||||
export let schema
|
||||
export let goBack
|
||||
|
||||
let errors = []
|
||||
let draftField = cloneDeep(field)
|
||||
|
||||
let type = field.type
|
||||
let constraints = field.constraints
|
||||
let required =
|
||||
field.constraints.presence && !field.constraints.presence.allowEmpty
|
||||
|
||||
const save = () => {
|
||||
constraints.presence = required ? { allowEmpty: false } : false
|
||||
draftField.constraints = constraints
|
||||
draftField.type = type
|
||||
schema[field.name] = draftField
|
||||
goBack()
|
||||
}
|
||||
|
||||
$: constraints =
|
||||
type === "string"
|
||||
? { type: "string", length: {}, presence: false }
|
||||
: type === "number"
|
||||
? { type: "number", presence: false, numericality: {} }
|
||||
: type === "boolean"
|
||||
? { type: "boolean", presence: false }
|
||||
: type === "datetime"
|
||||
? { type: "date", datetime: {}, presence: false }
|
||||
: type.startsWith("array")
|
||||
? { type: "array", presence: false }
|
||||
: { type: "string", presence: false }
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
|
@ -32,49 +56,51 @@
|
|||
|
||||
<form on:submit|preventDefault class="uk-form-stacked">
|
||||
<Textbox label="Name" bind:text={field.name} />
|
||||
<Dropdown
|
||||
label="Type"
|
||||
bind:selected={draftField.type}
|
||||
options={FIELD_TYPES} />
|
||||
<Dropdown label="Type" bind:selected={type} options={FIELD_TYPES} />
|
||||
|
||||
{#if field.type === 'string'}
|
||||
<NumberBox label="Max Length" bind:value={draftField.maxLength} />
|
||||
<ValuesList label="Categories" bind:values={draftField.values} />
|
||||
{:else if field.type === 'boolean'}
|
||||
<!-- TODO: revisit and fix with JSON schema -->
|
||||
<Checkbox label="Allow Null" bind:checked={draftField.allowNulls} />
|
||||
{:else if field.format === 'datetime'}
|
||||
<!-- TODO: revisit and fix with JSON schema -->
|
||||
<DatePicker label="Min Value" bind:value={draftField.minValue} />
|
||||
<DatePicker label="Max Value" bind:value={draftField.maxValue} />
|
||||
{:else if field.type === 'number'}
|
||||
<NumberBox label="Min Value" bind:value={draftField.minimum} />
|
||||
<NumberBox label="Max Value" bind:value={draftField.maximum} />
|
||||
{:else if draftField.type.startsWith('array')}
|
||||
<!-- TODO: revisit and fix with JSON schema -->
|
||||
<Checkbox label="Required" bind:checked={required} />
|
||||
|
||||
{#if type === 'string'}
|
||||
<NumberBox label="Max Length" bind:value={constraints.length.maximum} />
|
||||
<ValuesList label="Categories" bind:values={constraints.inclusion} />
|
||||
{:else if type === 'datetime'}
|
||||
<DatePicker
|
||||
label="Min Value"
|
||||
bind:value={constraints.datetime.earliest} />
|
||||
<DatePicker label="Max Value" bind:value={constraints.datetime.latest} />
|
||||
{:else if type === 'number'}
|
||||
<NumberBox
|
||||
label="Min Length"
|
||||
bind:value={draftField.typeOptions.minLength} />
|
||||
label="Min Value"
|
||||
bind:value={constraints.numericality.greaterThanOrEqualTo} />
|
||||
<NumberBox
|
||||
label="Max Length"
|
||||
bind:value={draftField.typeOptions.maxLength} />
|
||||
label="Max Value"
|
||||
bind:value={constraints.numericality.lessThanOrEqualTo} />
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
<footer>
|
||||
<div class="button">
|
||||
<ActionButton secondary on:click={goBack}>Cancel</ActionButton>
|
||||
</div>
|
||||
<ActionButton primary on:click={save}>Save</ActionButton>
|
||||
<ActionButton alert on:click={goBack}>Cancel</ActionButton>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
margin: 20px;
|
||||
margin: 40px;
|
||||
}
|
||||
footer {
|
||||
padding: 20px;
|
||||
padding: 20px 40px;
|
||||
border-radius: 0 0 5px 5px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #fafafa;
|
||||
background: var(--grey-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,10 +8,6 @@
|
|||
import * as api from "../api"
|
||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||
|
||||
const CLASS_NAME_MAP = {
|
||||
boolean: "uk-checkbox",
|
||||
}
|
||||
|
||||
export let record = {}
|
||||
export let onClosed
|
||||
|
||||
|
@ -28,14 +24,25 @@
|
|||
onClosed()
|
||||
}
|
||||
|
||||
const isSelect = meta =>
|
||||
meta.type === "string" &&
|
||||
meta.constraints &&
|
||||
meta.constraints.inclusion &&
|
||||
meta.constraints.inclusion.length > 0
|
||||
|
||||
function determineInputType(meta) {
|
||||
if (meta.type === "datetime") return "date"
|
||||
if (meta.type === "number") return "number"
|
||||
if (meta.type === "boolean") return "checkbox"
|
||||
if (isSelect(meta)) return "select"
|
||||
|
||||
return "text"
|
||||
}
|
||||
|
||||
function determineOptions(meta) {
|
||||
return isSelect(meta) ? meta.constraints.inclusion : []
|
||||
}
|
||||
|
||||
async function saveRecord() {
|
||||
const recordResponse = await api.saveRecord(
|
||||
{
|
||||
|
@ -46,7 +53,9 @@
|
|||
$backendUiStore.selectedModel._id
|
||||
)
|
||||
if (recordResponse.errors) {
|
||||
errors = recordResponse.errors
|
||||
errors = Object.keys(recordResponse.errors)
|
||||
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
|
||||
.flat()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -65,8 +74,8 @@
|
|||
{#each modelSchema as [key, meta]}
|
||||
<div class="uk-margin">
|
||||
<RecordFieldControl
|
||||
className={CLASS_NAME_MAP[meta.type]}
|
||||
type={determineInputType(meta)}
|
||||
options={determineOptions(meta)}
|
||||
label={key}
|
||||
bind:value={record[key]} />
|
||||
</div>
|
||||
|
|
|
@ -39,51 +39,49 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<heading>
|
||||
<div class="header">
|
||||
<i class="ri-eye-line button--toggled" />
|
||||
<h3 class="budibase__title--3">Create / Edit View</h3>
|
||||
</heading>
|
||||
</div>
|
||||
<form on:submit|preventDefault class="uk-form-stacked root">
|
||||
<h4 class="budibase__label--big">Settings</h4>
|
||||
{#if $store.errors && $store.errors.length > 0}
|
||||
<ErrorsBox errors={$store.errors} />
|
||||
{/if}
|
||||
<div class="uk-grid-small" uk-grid>
|
||||
<div class="uk-width-1-2@s">
|
||||
<Textbox bind:text={view.name} label="Name" />
|
||||
<div class="main">
|
||||
<div class="uk-grid-small" uk-grid>
|
||||
<div class="uk-width-1-2@s">
|
||||
<Textbox bind:text={view.name} label="Name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-snippets">
|
||||
{#each Object.values(SNIPPET_EDITORS) as snippetType}
|
||||
<span
|
||||
class="snippet-selector__heading hoverable"
|
||||
class:highlighted={currentSnippetEditor === snippetType}
|
||||
on:click={() => (currentSnippetEditor = snippetType)}>
|
||||
{snippetType}
|
||||
</span>
|
||||
{/each}
|
||||
{#if currentSnippetEditor === SNIPPET_EDITORS.MAP}
|
||||
<CodeArea bind:text={view.map} label="Map" />
|
||||
{:else if currentSnippetEditor === SNIPPET_EDITORS.FILTER}
|
||||
<CodeArea bind:text={view.filter} label="Filter" />
|
||||
{:else if currentSnippetEditor === SNIPPET_EDITORS.REDUCE}
|
||||
<CodeArea bind:text={view.reduce} label="Reduce" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="budibase__label--big">Snippets</h4>
|
||||
{#each Object.values(SNIPPET_EDITORS) as snippetType}
|
||||
<span
|
||||
class="snippet-selector__heading hoverable"
|
||||
class:highlighted={currentSnippetEditor === snippetType}
|
||||
on:click={() => (currentSnippetEditor = snippetType)}>
|
||||
{snippetType}
|
||||
</span>
|
||||
{/each}
|
||||
{#if currentSnippetEditor === SNIPPET_EDITORS.MAP}
|
||||
<CodeArea bind:text={view.map} label="Map" />
|
||||
{:else if currentSnippetEditor === SNIPPET_EDITORS.FILTER}
|
||||
<CodeArea bind:text={view.filter} label="Filter" />
|
||||
{:else if currentSnippetEditor === SNIPPET_EDITORS.REDUCE}
|
||||
<CodeArea bind:text={view.reduce} label="Reduce" />
|
||||
{/if}
|
||||
|
||||
<ActionButton color="secondary" on:click={saveView}>Save</ActionButton>
|
||||
<ActionButton alert on:click={deleteView}>Delete</ActionButton>
|
||||
<div class="buttons">
|
||||
<div class="button">
|
||||
<ActionButton secondary on:click={deleteView}>Delete</ActionButton>
|
||||
</div>
|
||||
<ActionButton color="secondary" on:click={saveView}>Save</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.snippet-selector__heading {
|
||||
margin-right: 20px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
|
@ -92,11 +90,38 @@
|
|||
|
||||
h3 {
|
||||
margin: 0 0 0 10px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
heading {
|
||||
padding: 20px 20px 0 20px;
|
||||
.snippet-selector__heading {
|
||||
margin-right: 20px;
|
||||
font-size: 14px;
|
||||
color: var(--ink-lighter);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 40px 0 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
margin: 20px 40px 0px 40px;
|
||||
}
|
||||
|
||||
.code-snippets {
|
||||
margin: 20px 0px 20px 0px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background-color: var(--grey-light);
|
||||
margin: 0 40px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,21 +7,26 @@
|
|||
|
||||
let username
|
||||
let password
|
||||
let accessLevelId
|
||||
|
||||
$: valid = username && password
|
||||
$: valid = username && password && accessLevelId
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
$: appId = $store.appId
|
||||
|
||||
async function createUser() {
|
||||
const user = { name: username, username, password }
|
||||
const response = await api.createUser(user, appId, instanceId)
|
||||
const user = { name: username, username, password, accessLevelId }
|
||||
const response = await api.createUser(user, instanceId)
|
||||
backendUiStore.actions.users.create(response)
|
||||
onClosed()
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault class="uk-form-stacked">
|
||||
<div>
|
||||
<div class="main">
|
||||
<div class="heading">
|
||||
<i class="ri-list-settings-line button--toggled" />
|
||||
<div class="title">Create User</div>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="form-stacked-text">Username</label>
|
||||
<input class="uk-input" type="text" bind:value={username} />
|
||||
|
@ -30,20 +35,50 @@
|
|||
<label class="uk-form-label" for="form-stacked-text">Password</label>
|
||||
<input class="uk-input" type="password" bind:value={password} />
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="form-stacked-text">Access Level</label>
|
||||
<select class="uk-select" bind:value={accessLevelId}>
|
||||
<option value="" />
|
||||
<option value="POWER_USER">Power User</option>
|
||||
<option value="ADMIN">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
|
||||
<div class="button">
|
||||
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
|
||||
</div>
|
||||
<ActionButton disabled={!valid} on:click={createUser}>Save</ActionButton>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: 30px;
|
||||
.main {
|
||||
padding: 40px 40px 20px 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--grey-light);
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,10 +3,16 @@
|
|||
export let value = ""
|
||||
export let label
|
||||
export let errors = []
|
||||
export let className = "uk-input"
|
||||
export let options = []
|
||||
|
||||
let checked = type === "checkbox" ? value : false
|
||||
|
||||
const determineClassName = type => {
|
||||
if (type === "checkbox") return "uk-checkbox"
|
||||
if (type === "select") return "uk-select"
|
||||
return "uk-input"
|
||||
}
|
||||
|
||||
const handleInput = event => {
|
||||
if (event.target.type === "checkbox") {
|
||||
value = event.target.checked
|
||||
|
@ -23,11 +29,23 @@
|
|||
</script>
|
||||
|
||||
<label>{label}</label>
|
||||
<input
|
||||
class={className}
|
||||
class:uk-form-danger={errors.length > 0}
|
||||
{checked}
|
||||
{type}
|
||||
{value}
|
||||
on:input={handleInput}
|
||||
on:change={handleInput} />
|
||||
|
||||
{#if type === 'select'}
|
||||
<select
|
||||
class={determineClassName(type)}
|
||||
bind:value
|
||||
class:uk-form-danger={errors.length > 0}>
|
||||
{#each options as opt}
|
||||
<option value={opt}>{opt}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<input
|
||||
class={determineClassName(type)}
|
||||
class:uk-form-danger={errors.length > 0}
|
||||
{checked}
|
||||
{type}
|
||||
{value}
|
||||
on:input={handleInput}
|
||||
on:change={handleInput} />
|
||||
{/if}
|
||||
|
|
|
@ -33,18 +33,7 @@
|
|||
</script>
|
||||
|
||||
<div class="items-root">
|
||||
<div class="hierarchy">
|
||||
<div class="components-list-container">
|
||||
<div class="nav-group-header">
|
||||
<div class="hierarchy-title">Databases</div>
|
||||
<i class="ri-add-line hoverable" on:click={openDatabaseCreator} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hierarchy-items-container">
|
||||
<DatabasesList />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hierarchy" />
|
||||
{#if $backendUiStore.selectedDatabase._id}
|
||||
<div class="hierarchy">
|
||||
<div class="components-list-container">
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
function selectModel(model) {
|
||||
backendUiStore.update(state => {
|
||||
state.selectedModel = model
|
||||
state.selectedView = `${model._id}`
|
||||
state.selectedView = `all_${model._id}`
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
<h3 class="app-title">{name}</h3>
|
||||
<p class="app-desc">{description}</p>
|
||||
<div class="card-footer">
|
||||
<div class="modified-date">Last Edited - 25th May 2020</div>
|
||||
<a href={`/_builder/${_id}`} class="app-button">Open Web App</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,7 +17,7 @@
|
|||
<style>
|
||||
.apps-card {
|
||||
background-color: var(--white);
|
||||
padding: 20px;
|
||||
padding: 20px 20px 30px 20px;
|
||||
max-width: 400px;
|
||||
max-height: 150px;
|
||||
border-radius: 5px;
|
||||
|
@ -48,14 +47,13 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modified-date {
|
||||
font-size: 14px;
|
||||
color: var(--ink-light);
|
||||
}
|
||||
|
||||
.app-button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
background-color: var(--white);
|
||||
color: var(--ink);
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 12px 20px;
|
||||
border-radius: 5px;
|
||||
border: 1px var(--grey) solid;
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
<div class="inner">
|
||||
<div>
|
||||
<div>
|
||||
<div class="app-section-title">Your Web Apps</div>
|
||||
<div class="apps">
|
||||
{#each apps as app}
|
||||
<AppCard {...app} />
|
||||
|
@ -26,18 +25,12 @@
|
|||
<style>
|
||||
.apps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 400px);
|
||||
grid-gap: 40px 85px;
|
||||
grid-template-columns: repeat(auto-fill, 380px);
|
||||
grid-gap: 20px 40px;
|
||||
justify-content: start;
|
||||
}
|
||||
.root {
|
||||
margin: 40px 80px;
|
||||
}
|
||||
|
||||
.app-section-title {
|
||||
font-size: 20px;
|
||||
color: var(--ink);
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
.root {
|
||||
margin: 20px 80px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
|
||||
import { getContext } from "svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
import { post } from "builderStore/api"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
|
@ -33,15 +34,7 @@
|
|||
const data = { name, description }
|
||||
loading = true
|
||||
try {
|
||||
const response = await fetch("/api/applications", {
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: JSON.stringify(data), // body data type must match "Content-Type" header
|
||||
})
|
||||
const response = await post("/api/applications", data)
|
||||
|
||||
const res = await response.json()
|
||||
|
||||
|
|
|
@ -21,14 +21,47 @@
|
|||
return componentName || "element"
|
||||
}
|
||||
|
||||
$: iframe &&
|
||||
console.log(
|
||||
iframe.contentDocument.head.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<\style></style>`
|
||||
)
|
||||
)
|
||||
const screenPlaceholder = {
|
||||
name: "Screen Placeholder",
|
||||
route: "*",
|
||||
props: {
|
||||
_component: "@budibase/standard-components/container",
|
||||
type: "div",
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/container",
|
||||
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
|
||||
_id: "__screenslot__text",
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
type: "div",
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/text",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_id: "__screenslot__text_2",
|
||||
_code: "",
|
||||
text: "content",
|
||||
font: "",
|
||||
color: "",
|
||||
textAlign: "inline",
|
||||
verticalAlign: "inline",
|
||||
formattingTag: "none",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
$: hasComponent = !!$store.currentPreviewItem
|
||||
|
||||
$: {
|
||||
styles = ""
|
||||
// Apply the CSS from the currently selected page and its screens
|
||||
|
@ -52,49 +85,12 @@
|
|||
$: frontendDefinition = {
|
||||
appId: $store.appId,
|
||||
libraries: $store.libraries,
|
||||
page: $store.currentPreviewItem,
|
||||
screens: screensExist
|
||||
? $store.currentPreviewItem._screens
|
||||
: [
|
||||
{
|
||||
name: "Screen Placeholder",
|
||||
route: "*",
|
||||
props: {
|
||||
_component: "@budibase/standard-components/container",
|
||||
type: "div",
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/container",
|
||||
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
|
||||
_id: "__screenslot__text",
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
type: "div",
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/text",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_id: "__screenslot__text_2",
|
||||
_code: "",
|
||||
text: "content",
|
||||
font: "",
|
||||
color: "",
|
||||
textAlign: "inline",
|
||||
verticalAlign: "inline",
|
||||
formattingTag: "none",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
page: $store.pages[$store.currentPageName],
|
||||
screens: [
|
||||
$store.currentFrontEndType === "page"
|
||||
? screenPlaceholder
|
||||
: $store.currentPreviewItem,
|
||||
],
|
||||
appRootPath: "",
|
||||
}
|
||||
|
||||
|
@ -103,6 +99,27 @@
|
|||
$: selectedComponentId = $store.currentComponentInfo
|
||||
? $store.currentComponentInfo._id
|
||||
: ""
|
||||
|
||||
const refreshContent = () => {
|
||||
iframe.contentWindow.postMessage(
|
||||
JSON.stringify({
|
||||
styles,
|
||||
stylesheetLinks,
|
||||
selectedComponentType,
|
||||
selectedComponentId,
|
||||
frontendDefinition,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
$: if (iframe)
|
||||
iframe.contentWindow.addEventListener("bb-ready", refreshContent, {
|
||||
once: true,
|
||||
})
|
||||
|
||||
$: if (iframe && frontendDefinition) {
|
||||
refreshContent()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="component-container">
|
||||
|
@ -111,14 +128,7 @@
|
|||
style="height: 100%; width: 100%"
|
||||
title="componentPreview"
|
||||
bind:this={iframe}
|
||||
srcdoc={iframeTemplate({
|
||||
styles,
|
||||
stylesheetLinks,
|
||||
selectedComponentType,
|
||||
selectedComponentId,
|
||||
frontendDefinition: JSON.stringify(frontendDefinition),
|
||||
currentPageFunctions: $store.currentPageFunctions,
|
||||
})} />
|
||||
srcdoc={iframeTemplate} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,23 +1,9 @@
|
|||
export default ({
|
||||
styles,
|
||||
stylesheetLinks,
|
||||
selectedComponentType,
|
||||
selectedComponentId,
|
||||
frontendDefinition,
|
||||
currentPageFunctions,
|
||||
}) => `<html>
|
||||
export default `<html>
|
||||
<head>
|
||||
${stylesheetLinks}
|
||||
|
||||
<style>
|
||||
${styles || ""}
|
||||
|
||||
.${selectedComponentType}-${selectedComponentId} {
|
||||
border: 2px solid #0055ff;
|
||||
}
|
||||
|
||||
body, html {
|
||||
height: 100%!important;
|
||||
font-family: Roboto !important;
|
||||
}
|
||||
.lay-__screenslot__text {
|
||||
width: 100%;
|
||||
|
@ -35,13 +21,50 @@ export default ({
|
|||
}
|
||||
</style>
|
||||
<script>
|
||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = ${frontendDefinition};
|
||||
window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = ${currentPageFunctions};
|
||||
function receiveMessage(event) {
|
||||
|
||||
import('/_builder/budibase-client.esm.mjs')
|
||||
.then(module => {
|
||||
module.loadBudibase({ window, localStorage });
|
||||
})
|
||||
if (!event.data) return
|
||||
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
try {
|
||||
if (styles) document.head.removeChild(styles)
|
||||
} catch(_) { }
|
||||
|
||||
try {
|
||||
if (selectedComponentStyle) document.head.removeChild(selectedComponentStyle)
|
||||
} catch(_) { }
|
||||
|
||||
selectedComponentStyle = document.createElement('style');
|
||||
document.head.appendChild(selectedComponentStyle)
|
||||
var selectedCss = '.' + data.selectedComponentType + '-' + data.selectedComponentId + '{ border: 2px solid #0055ff; }'
|
||||
selectedComponentStyle.appendChild(document.createTextNode(selectedCss))
|
||||
|
||||
styles = document.createElement('style')
|
||||
document.head.appendChild(styles)
|
||||
styles.appendChild(document.createTextNode(data.styles))
|
||||
|
||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
|
||||
if (clientModule) {
|
||||
clientModule.loadBudibase({ window, localStorage })
|
||||
}
|
||||
}
|
||||
let clientModule
|
||||
let styles
|
||||
let selectedComponentStyle
|
||||
|
||||
document.addEventListener("click", function(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false;
|
||||
}, true)
|
||||
|
||||
import('/_builder/budibase-client.esm.mjs')
|
||||
.then(module => {
|
||||
clientModule = module
|
||||
window.addEventListener('message', receiveMessage)
|
||||
window.dispatchEvent(new Event('bb-ready'))
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -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>
|
|
@ -13,7 +13,6 @@
|
|||
import CodeEditor from "./CodeEditor.svelte"
|
||||
import LayoutEditor from "./LayoutEditor.svelte"
|
||||
import EventsEditor from "./EventsEditor"
|
||||
|
||||
import panelStructure from "./temporaryPanelStructure.js"
|
||||
import CategoryTab from "./CategoryTab.svelte"
|
||||
import DesignView from "./DesignView.svelte"
|
||||
|
@ -25,7 +24,7 @@
|
|||
let categories = [
|
||||
{ value: "design", name: "Design" },
|
||||
{ value: "settings", name: "Settings" },
|
||||
{ value: "actions", name: "Actions" },
|
||||
{ value: "events", name: "Events" },
|
||||
]
|
||||
let selectedCategory = categories[0]
|
||||
|
||||
|
@ -38,15 +37,10 @@
|
|||
c => c._component === componentInstance._component
|
||||
) || {}
|
||||
|
||||
$: panelDefinition = componentPropDefinition.properties
|
||||
? componentPropDefinition.properties[selectedCategory.value]
|
||||
: {}
|
||||
let panelDefinition = {}
|
||||
|
||||
// SCREEN PROPS =============================================
|
||||
$: screen_props =
|
||||
$store.currentFrontEndType === "page"
|
||||
? getProps($store.currentPreviewItem, ["name", "favicon"])
|
||||
: getProps($store.currentPreviewItem, ["name", "description", "route"])
|
||||
$: panelDefinition = componentPropDefinition.properties &&
|
||||
componentPropDefinition.properties[selectedCategory.value]
|
||||
|
||||
const onStyleChanged = store.setComponentStyle
|
||||
const onPropChanged = store.setComponentProp
|
||||
|
@ -92,7 +86,11 @@
|
|||
{componentInstance}
|
||||
{componentDefinition}
|
||||
{panelDefinition}
|
||||
onChange={onPropChanged} />
|
||||
onChange={onPropChanged}
|
||||
onScreenPropChange={store.setPageOrScreenProp}
|
||||
screenOrPageInstance={$store.currentView !== "component" && $store.currentPreviewItem} />
|
||||
{:else if selectedCategory.value === 'events'}
|
||||
<EventsEditor component={componentInstance} />
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
@ -105,6 +103,9 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.title > div:nth-child(1) {
|
||||
|
@ -119,5 +120,7 @@
|
|||
.component-props-container {
|
||||
margin-top: 20px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { splitName } from "./pagesParsing/splitRootComponentName.js"
|
||||
import components from "./temporaryPanelStructure.js"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
@ -32,7 +33,14 @@
|
|||
|
||||
const onComponentChosen = component => {
|
||||
store.addChildComponent(component._component)
|
||||
toggleTab()
|
||||
|
||||
toggleTab("Navigate")
|
||||
|
||||
// Get ID path
|
||||
const path = store.getPathToComponent($store.currentComponentInfo)
|
||||
|
||||
// Go to correct URL
|
||||
$goto(`./:page/:screen/${path}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { params, goto } from "@sveltech/routify"
|
||||
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
|
||||
|
||||
import { last, sortBy, map, trimCharsStart, trimChars, join } from "lodash/fp"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { pipe } from "components/common/core"
|
||||
|
@ -36,11 +35,6 @@
|
|||
sortBy("title"),
|
||||
])
|
||||
|
||||
const confirmDeleteComponent = component => {
|
||||
componentToDelete = component
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
const changeScreen = screen => {
|
||||
store.setCurrentScreen(screen.title)
|
||||
$goto(`./:page/${screen.title}`)
|
||||
|
@ -62,9 +56,7 @@
|
|||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="icon">
|
||||
<ShapeIcon />
|
||||
</span>
|
||||
<i class="ri-artboard-2-fill icon" />
|
||||
|
||||
<span class="title">{screen.title}</span>
|
||||
</div>
|
||||
|
@ -72,41 +64,32 @@
|
|||
{#if $store.currentPreviewItem.name === screen.title && screen.component.props._children}
|
||||
<ComponentsHierarchyChildren
|
||||
components={screen.component.props._children}
|
||||
currentComponent={$store.currentComponentInfo}
|
||||
onDeleteComponent={confirmDeleteComponent}
|
||||
onMoveUpComponent={store.moveUpComponent}
|
||||
onMoveDownComponent={store.moveDownComponent}
|
||||
onCopyComponent={store.copyComponent} />
|
||||
currentComponent={$store.currentComponentInfo} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Delete"
|
||||
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component?`}
|
||||
okText="Delete Component"
|
||||
onOk={() => store.deleteComponent(componentToDelete)} />
|
||||
|
||||
<style>
|
||||
.root {
|
||||
font-weight: 400;
|
||||
color: #000333;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
transition: 0.2s;
|
||||
font-size: 24px;
|
||||
width: 20px;
|
||||
margin-top: 2px;
|
||||
color: #333;
|
||||
color: var(--ink-light);
|
||||
}
|
||||
|
||||
.icon:nth-of-type(2) {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { store } from "builderStore"
|
||||
import { last } from "lodash/fp"
|
||||
import { pipe } from "components/common/core"
|
||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||
import {
|
||||
XCircleIcon,
|
||||
ChevronUpIcon,
|
||||
|
@ -14,23 +15,12 @@
|
|||
export let currentComponent
|
||||
export let onSelect = () => {}
|
||||
export let level = 0
|
||||
export let onDeleteComponent
|
||||
export let onMoveUpComponent
|
||||
export let onMoveDownComponent
|
||||
export let onCopyComponent
|
||||
|
||||
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||
const get_name = s => (!s ? "" : last(s.split("/")))
|
||||
|
||||
const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
||||
|
||||
const moveDownComponent = component => {
|
||||
const c = component
|
||||
return () => {
|
||||
return onMoveDownComponent(c)
|
||||
}
|
||||
}
|
||||
|
||||
const selectComponent = component => {
|
||||
// Set current component
|
||||
store.selectComponent(component)
|
||||
|
@ -50,31 +40,13 @@
|
|||
class="budibase__nav-item item"
|
||||
class:selected={currentComponent === component}
|
||||
style="padding-left: {level * 20 + 53}px">
|
||||
<div>{get_capitalised_name(component._component)}</div>
|
||||
<div class="reorder-buttons">
|
||||
{#if index > 0}
|
||||
<button
|
||||
class:solo={index === components.length - 1}
|
||||
on:click|stopPropagation={() => onMoveUpComponent(component)}>
|
||||
<ChevronUpIcon />
|
||||
</button>
|
||||
{/if}
|
||||
{#if index < components.length - 1}
|
||||
<button
|
||||
class:solo={index === 0}
|
||||
on:click|stopPropagation={moveDownComponent(component)}>
|
||||
<ChevronDownIcon />
|
||||
</button>
|
||||
{/if}
|
||||
<div class="nav-item">
|
||||
<i class="icon ri-arrow-right-circle-fill" />
|
||||
{get_capitalised_name(component._component)}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ComponentDropdownMenu {component} />
|
||||
</div>
|
||||
<button
|
||||
class="copy"
|
||||
on:click|stopPropagation={() => onCopyComponent(component)}>
|
||||
<CopyIcon />
|
||||
</button>
|
||||
<button on:click|stopPropagation={() => onDeleteComponent(component)}>
|
||||
<XCircleIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if component._children}
|
||||
|
@ -82,11 +54,7 @@
|
|||
components={component._children}
|
||||
{currentComponent}
|
||||
{onSelect}
|
||||
level={level + 1}
|
||||
{onDeleteComponent}
|
||||
{onMoveUpComponent}
|
||||
{onMoveDownComponent}
|
||||
{onCopyComponent} />
|
||||
level={level + 1} />
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
@ -107,50 +75,37 @@
|
|||
border-radius: 3px;
|
||||
height: 35px;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.item button {
|
||||
.actions {
|
||||
display: none;
|
||||
height: 20px;
|
||||
width: 28px;
|
||||
color: var(--slate);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
color: var(--ink);
|
||||
padding: 0px 5px;
|
||||
border-style: none;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item button.copy {
|
||||
width: 26px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: #fafafa;
|
||||
background: var(--grey-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
.item:hover button {
|
||||
.item:hover .actions {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item:hover button:hover {
|
||||
color: var(--button-text);
|
||||
}
|
||||
|
||||
.reorder-buttons {
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.reorder-buttons > button {
|
||||
flex: 1 1 auto;
|
||||
width: 30px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.reorder-buttons > button.solo {
|
||||
padding-top: 2px;
|
||||
.icon {
|
||||
color: var(--ink-light);
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 20px;
|
||||
padding: 20px 5px 20px 10px;
|
||||
border-left: solid 1px var(--grey);
|
||||
}
|
||||
|
||||
|
@ -78,4 +78,8 @@
|
|||
.switcher > .selected {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.panel {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.design-view-state-categories {
|
||||
|
@ -63,6 +64,9 @@
|
|||
|
||||
.design-view-property-groups {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.no-design {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { Button } from "@budibase/bbui"
|
||||
import Modal from "../../common/Modal.svelte"
|
||||
import HandlerSelector from "./HandlerSelector.svelte"
|
||||
import IconButton from "../../common/IconButton.svelte"
|
||||
|
@ -8,12 +9,12 @@
|
|||
import Select from "../../common/Select.svelte"
|
||||
import Input from "../../common/Input.svelte"
|
||||
import getIcon from "../../common/icon"
|
||||
import { CloseIcon } from "components/common/Icons/"
|
||||
|
||||
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
|
||||
|
||||
export let event
|
||||
export let eventOptions = []
|
||||
export let open
|
||||
export let onClose
|
||||
|
||||
let eventType = ""
|
||||
|
@ -62,105 +63,111 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:isOpen={open} onClosed={closeModal}>
|
||||
<h2>
|
||||
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
|
||||
</h2>
|
||||
<a href="https://docs.budibase.com/" target="_blank">
|
||||
Click here to learn more about component events
|
||||
</a>
|
||||
<div class="container">
|
||||
<div class="body">
|
||||
<div class="heading">
|
||||
<h3>
|
||||
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="event-options">
|
||||
<div class="section">
|
||||
<h4>Event Type</h4>
|
||||
<Select bind:value={eventType}>
|
||||
{#each eventOptions as option}
|
||||
<option value={option.name}>{option.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="event-options">
|
||||
<div>
|
||||
<header>
|
||||
<h5>Event Type</h5>
|
||||
{@html getIcon('info', 20)}
|
||||
</header>
|
||||
<Select bind:value={eventType}>
|
||||
{#each eventOptions as option}
|
||||
<option value={option.name}>{option.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
<div class="section">
|
||||
<h4>Event Action(s)</h4>
|
||||
<HandlerSelector
|
||||
newHandler
|
||||
onChanged={updateDraftEventHandler}
|
||||
onCreate={() => {
|
||||
createNewEventHandler(draftEventHandler)
|
||||
draftEventHandler = { parameters: [] }
|
||||
}}
|
||||
handler={draftEventHandler} />
|
||||
</div>
|
||||
{#if eventData}
|
||||
{#each eventData.handlers as handler, index}
|
||||
<HandlerSelector
|
||||
{index}
|
||||
onChanged={updateEventHandler}
|
||||
onRemoved={() => deleteEventHandler(index)}
|
||||
{handler} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
<div class="footer">
|
||||
{#if eventData.name}
|
||||
<Button
|
||||
outline
|
||||
on:click={deleteEvent}
|
||||
disabled={eventData.handlers.length === 0}>
|
||||
Delete
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="save">
|
||||
<Button
|
||||
primary
|
||||
on:click={saveEventData}
|
||||
disabled={eventData.handlers.length === 0}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<h5>Event Action(s)</h5>
|
||||
{@html getIcon('info', 20)}
|
||||
</header>
|
||||
<HandlerSelector
|
||||
newHandler
|
||||
onChanged={updateDraftEventHandler}
|
||||
onCreate={() => {
|
||||
createNewEventHandler(draftEventHandler)
|
||||
draftEventHandler = { parameters: [] }
|
||||
}}
|
||||
handler={draftEventHandler} />
|
||||
{#if eventData}
|
||||
{#each eventData.handlers as handler, index}
|
||||
<HandlerSelector
|
||||
{index}
|
||||
onChanged={updateEventHandler}
|
||||
onRemoved={() => deleteEventHandler(index)}
|
||||
{handler} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="actions">
|
||||
<ActionButton
|
||||
alert
|
||||
disabled={eventData.handlers.length === 0}
|
||||
hidden={!eventData.name}
|
||||
on:click={deleteEvent}>
|
||||
Delete
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
disabled={eventData.handlers.length === 0}
|
||||
on:click={saveEventData}>
|
||||
Save
|
||||
</ActionButton>
|
||||
<div class="close-button" on:click={closeModal}>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
h2 {
|
||||
color: var(--primary100);
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
.heading {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: rgba(22, 48, 87, 0.6);
|
||||
font-size: 15px;
|
||||
margin: 0;
|
||||
.close-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
.close-button :global(svg) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.event-options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.actions,
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-top: 30px;
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(22, 48, 87, 0.6);
|
||||
font-size: 13px;
|
||||
margin-top: 0;
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.body {
|
||||
padding: 40px;
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 30px 40px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 50px;
|
||||
background-color: var(--grey-light);
|
||||
}
|
||||
.save {
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import {
|
||||
keys,
|
||||
map,
|
||||
|
@ -17,7 +18,6 @@
|
|||
import PlusButton from "components/common/PlusButton.svelte"
|
||||
import IconButton from "components/common/IconButton.svelte"
|
||||
import EventEditorModal from "./EventEditorModal.svelte"
|
||||
import HandlerSelector from "./HandlerSelector.svelte"
|
||||
|
||||
import { PencilIcon } from "components/common/Icons"
|
||||
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
||||
|
@ -25,37 +25,49 @@
|
|||
export const EVENT_TYPE = "event"
|
||||
|
||||
export let component
|
||||
export let components
|
||||
|
||||
let modalOpen = false
|
||||
let events = []
|
||||
let selectedEvent = null
|
||||
|
||||
$: {
|
||||
const componentDefinition = components[component._component]
|
||||
events = Object.keys(componentDefinition.props)
|
||||
.filter(propName => componentDefinition.props[propName] === EVENT_TYPE)
|
||||
events = Object.keys(component)
|
||||
// TODO: use real events
|
||||
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
|
||||
.map(propName => ({
|
||||
name: propName,
|
||||
handlers: component[propName] || [],
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle create app modal
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
const openModal = event => {
|
||||
selectedEvent = event
|
||||
modalOpen = true
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
selectedEvent = null
|
||||
modalOpen = false
|
||||
open(
|
||||
EventEditorModal,
|
||||
{
|
||||
eventOptions: events,
|
||||
event: selectedEvent,
|
||||
onClose: () => {
|
||||
close()
|
||||
selectedEvent = null
|
||||
},
|
||||
},
|
||||
{
|
||||
closeButton: false,
|
||||
closeOnEsc: false,
|
||||
styleContent: { padding: 0 },
|
||||
closeOnOuterClick: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<h3>Events</h3>
|
||||
<PlusButton on:click={() => openModal()} />
|
||||
</header>
|
||||
<button class="newevent" on:click={() => openModal()}>
|
||||
<i class="icon ri-add-circle-fill" />
|
||||
Create New Event
|
||||
</button>
|
||||
|
||||
<div class="root">
|
||||
<form on:submit|preventDefault class="uk-form-stacked form-root">
|
||||
|
@ -72,26 +84,40 @@
|
|||
{/each}
|
||||
</form>
|
||||
</div>
|
||||
<EventEditorModal
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
eventOptions={events}
|
||||
event={selectedEvent} />
|
||||
|
||||
<style>
|
||||
h3 {
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #8997ab;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.root {
|
||||
font-size: 10pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.newevent {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--grey-dark);
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
margin: 0px 0px 12px 0px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: white;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 2ms;
|
||||
}
|
||||
|
||||
.newevent:hover {
|
||||
background: var(--grey-light);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--ink);
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.form-root {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { Button } from "@budibase/bbui"
|
||||
import IconButton from "components/common/IconButton.svelte"
|
||||
import PlusButton from "components/common/PlusButton.svelte"
|
||||
import Select from "components/common/Select.svelte"
|
||||
|
@ -85,27 +86,28 @@
|
|||
</Select>
|
||||
</div>
|
||||
{#if parameters}
|
||||
<br />
|
||||
{#each parameters as parameter, idx}
|
||||
<StateBindingCascader onChange={onParameterChanged(idx)} {parameter} />
|
||||
<StateBindingCascader on:change={onParameterChanged(idx)} {parameter} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="event-action-button">
|
||||
{#if parameters.length > 0}
|
||||
{#if newHandler}
|
||||
<PlusButton on:click={onCreate} />
|
||||
{:else}
|
||||
<IconButton icon="x" on:click={onRemoved} />
|
||||
{/if}
|
||||
<div class="button-container">
|
||||
{#if newHandler}
|
||||
<Button primary thin on:click={onCreate}>Add Action</Button>
|
||||
{:else}
|
||||
<Button outline thin on:click={onRemoved}>Remove Action</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.type-selector-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
width: 100%;
|
||||
background: rgba(223, 223, 223, 0.5);
|
||||
border: 1px solid #dfdfdf;
|
||||
margin-bottom: 18px;
|
||||
|
@ -122,17 +124,19 @@
|
|||
|
||||
.handler-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 20px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.event-action-button {
|
||||
margin-right: 20px;
|
||||
.button-container {
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,73 +1,52 @@
|
|||
<script>
|
||||
import { Input } from "@budibase/bbui"
|
||||
import IconButton from "components/common/IconButton.svelte"
|
||||
import PlusButton from "components/common/PlusButton.svelte"
|
||||
import Select from "components/common/Select.svelte"
|
||||
import Input from "components/common/Input.svelte"
|
||||
import { find, map, keys, reduce, keyBy } from "lodash/fp"
|
||||
import { pipe } from "components/common/core"
|
||||
import {
|
||||
EVENT_TYPE_MEMBER_NAME,
|
||||
allHandlers,
|
||||
} from "components/common/eventHandlers"
|
||||
import { store } from "builderStore"
|
||||
import StateBindingOptions from "../PropertyCascader/StateBindingOptions.svelte"
|
||||
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
||||
import { store, workflowStore } from "builderStore"
|
||||
import { ArrowDownIcon } from "components/common/Icons/"
|
||||
|
||||
export let parameter
|
||||
export let onChange
|
||||
|
||||
let isOpen = false
|
||||
|
||||
const capitalize = s => {
|
||||
if (typeof s !== "string") return ""
|
||||
return s.charAt(0).toUpperCase() + s.slice(1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="handler-option">
|
||||
<span>{parameter.name}</span>
|
||||
<div class="handler-input">
|
||||
<Input on:change={onChange} value={parameter.value} />
|
||||
<button on:click={() => (isOpen = !isOpen)}>
|
||||
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
|
||||
<ArrowDownIcon size={36} />
|
||||
</div>
|
||||
</button>
|
||||
{#if isOpen}
|
||||
<StateBindingOptions
|
||||
onSelect={option => {
|
||||
onChange(option)
|
||||
isOpen = false
|
||||
}} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if parameter.name === 'workflow'}
|
||||
<span>{parameter.name}</span>
|
||||
{/if}
|
||||
{#if parameter.name === 'workflow'}
|
||||
<Select on:change bind:value={parameter.value}>
|
||||
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
|
||||
<option value={workflow._id}>{workflow.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else}
|
||||
<Input
|
||||
name={parameter.name}
|
||||
label={capitalize(parameter.name)}
|
||||
on:change
|
||||
value={parameter.value} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
button {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background: rgba(249, 249, 249, 1);
|
||||
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: rgba(22, 48, 87, 1);
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.handler-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.handler-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
<script>
|
||||
import { buildStyle } from "../../helpers.js"
|
||||
export let value = ""
|
||||
export let text = ""
|
||||
export let icon = ""
|
||||
export let padding = "8px 5px;"
|
||||
export let onClick = value => {}
|
||||
export let selected = false
|
||||
export let fontWeight = ""
|
||||
|
||||
$: style = buildStyle({ padding, fontWeight })
|
||||
$: useIcon = !!icon
|
||||
</script>
|
||||
|
||||
<div class="flatbutton" class:selected on:click={() => onClick(value || text)}>
|
||||
<div
|
||||
class="flatbutton"
|
||||
{style}
|
||||
class:selected
|
||||
on:click={() => onClick(value || text)}>
|
||||
{#if useIcon}
|
||||
<i class={icon} />
|
||||
{:else}
|
||||
<span>{text}</span>
|
||||
<span>
|
||||
{@html text}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -28,6 +38,7 @@
|
|||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
transition: all 0.3s;
|
||||
margin-left: 5px;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
|
@ -35,4 +46,8 @@
|
|||
background: var(--ink-light);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -27,13 +27,16 @@
|
|||
}
|
||||
onChange(val)
|
||||
}
|
||||
|
||||
const checkSelected = val =>
|
||||
isMultiSelect ? value.includes(val) : value === val
|
||||
</script>
|
||||
|
||||
<div class="flatbutton-group">
|
||||
{#each buttonProps as props}
|
||||
<div class="button-container">
|
||||
<FlatButton
|
||||
selected={value.includes(props.value)}
|
||||
selected={isMultiSelect ? value.includes(props.value) : value === props.value}
|
||||
onClick={onButtonClicked}
|
||||
{...props} />
|
||||
</div>
|
||||
|
|
|
@ -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>
|
|
@ -3,7 +3,7 @@
|
|||
export let item
|
||||
</script>
|
||||
|
||||
<div class="item-item" transition:fly={{ y: 100, duration: 1000 }} on:click>
|
||||
<div class="item-item" in:fly={{ y: 100, duration: 1000 }} on:click>
|
||||
<div class="item-icon">
|
||||
<i class={item.icon} />
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
|||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 0px 16px 0px;
|
||||
width: 120px;
|
||||
width: 110px;
|
||||
height: 80px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
import Item from "./Item.svelte"
|
||||
import { store } from "builderStore"
|
||||
export let list
|
||||
|
||||
let category = list
|
||||
|
|
|
@ -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>
|
|
@ -1,36 +1,223 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
export let value = ""
|
||||
export let onChange = value => {}
|
||||
import { onMount, beforeUpdate } from "svelte"
|
||||
import { buildStyle } from "../../helpers.js"
|
||||
export let options = []
|
||||
export let initialValue = ""
|
||||
export let styleBindingProperty = ""
|
||||
export let value = ""
|
||||
export let styleBindingProperty
|
||||
export let onChange = value => {}
|
||||
|
||||
let open = null
|
||||
let rotate = ""
|
||||
let select
|
||||
let selectMenu
|
||||
let icon
|
||||
|
||||
let selectYPosition = null
|
||||
let availableSpace = 0
|
||||
|
||||
let positionSide = "top"
|
||||
let maxHeight = null
|
||||
let menuHeight
|
||||
|
||||
const handleStyleBind = value =>
|
||||
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {}
|
||||
|
||||
$: isOptionsObject = options.every(o => typeof o === "object")
|
||||
|
||||
onMount(() => {
|
||||
if (!value && !!initialValue) {
|
||||
value = initialValue
|
||||
if (select) {
|
||||
select.addEventListener("keydown", addSelectKeyEvents)
|
||||
}
|
||||
return () => {
|
||||
select.removeEventListener("keydown", addSelectKeyEvents)
|
||||
}
|
||||
})
|
||||
|
||||
function checkPosition() {
|
||||
const { bottom, top: spaceAbove } = select.getBoundingClientRect()
|
||||
const spaceBelow = window.innerHeight - bottom
|
||||
|
||||
if (spaceAbove > spaceBelow) {
|
||||
positionSide = "bottom"
|
||||
maxHeight = `${spaceAbove.toFixed(0) - 20}px`
|
||||
} else {
|
||||
positionSide = "top"
|
||||
maxHeight = `${spaceBelow.toFixed(0) - 20}px`
|
||||
}
|
||||
}
|
||||
|
||||
function addSelectKeyEvents(e) {
|
||||
if (e.key === "Enter") {
|
||||
if (!open) {
|
||||
toggleSelect(true)
|
||||
}
|
||||
} else if (e.key === "Escape") {
|
||||
if (open) {
|
||||
toggleSelect(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelect(isOpen) {
|
||||
checkPosition()
|
||||
if (isOpen) {
|
||||
icon.style.transform = "rotate(180deg)"
|
||||
} else {
|
||||
icon.style.transform = "rotate(0deg)"
|
||||
}
|
||||
open = isOpen
|
||||
}
|
||||
|
||||
function handleClick(val) {
|
||||
value = val
|
||||
onChange(value)
|
||||
}
|
||||
|
||||
$: menuStyle = buildStyle({
|
||||
"max-height": maxHeight,
|
||||
"transform-origin": `center ${positionSide}`,
|
||||
[positionSide]: "32px",
|
||||
})
|
||||
|
||||
$: isOptionsObject = options.every(o => typeof o === "object")
|
||||
|
||||
$: selectedOption = isOptionsObject
|
||||
? options.find(o => o.value === value)
|
||||
: {}
|
||||
|
||||
$: displayLabel =
|
||||
selectedOption && selectedOption.label ? selectedOption.label : value || ""
|
||||
</script>
|
||||
|
||||
<select
|
||||
class="uk-select uk-form-small"
|
||||
{value}
|
||||
on:change={ev => onChange(ev.target.value)}>
|
||||
{#if isOptionsObject}
|
||||
{#each options as { value, label }}
|
||||
<option {...handleStyleBind(value || label)} value={value || label}>
|
||||
{label}
|
||||
</option>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each options as value}
|
||||
<option {...handleStyleBind(value)} {value}>{value}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<div
|
||||
tabindex="0"
|
||||
bind:this={select}
|
||||
class="bb-select-container"
|
||||
on:click={() => toggleSelect(!open)}>
|
||||
<div class="bb-select-anchor selected">
|
||||
<span>{displayLabel}</span>
|
||||
<i bind:this={icon} class="ri-arrow-down-s-fill" />
|
||||
</div>
|
||||
<div
|
||||
bind:this={selectMenu}
|
||||
style={menuStyle}
|
||||
class="bb-select-menu"
|
||||
class:open>
|
||||
<ul>
|
||||
{#if isOptionsObject}
|
||||
{#each options as { value: v, label }}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{label}
|
||||
</li>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each options as v}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{v}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{#if open}
|
||||
<div on:click|self={() => toggleSelect(false)} class="overlay" />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bb-select-container {
|
||||
position: relative;
|
||||
outline: none;
|
||||
width: 160px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bb-select-anchor {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
background-color: #f2f2f2;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--grey-dark);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bb-select-anchor > span {
|
||||
color: #565a66;
|
||||
font-weight: 500;
|
||||
width: 140px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.bb-select-anchor > i {
|
||||
transition: transform 0.13s ease;
|
||||
transform-origin: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: #565a66;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bb-select-menu {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
opacity: 0;
|
||||
width: 160px;
|
||||
z-index: 2;
|
||||
color: #808192;
|
||||
font-weight: 500;
|
||||
height: fit-content !important;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-right: 1px solid var(--grey-dark);
|
||||
border-left: 1px solid var(--grey-dark);
|
||||
border-bottom: 1px solid var(--grey-dark);
|
||||
background-color: #f2f2f2;
|
||||
transform: scale(0);
|
||||
transition: opacity 0.13s linear, transform 0.12s cubic-bezier(0, 0, 0.2, 1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.open {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
|
||||
li {
|
||||
height: auto;
|
||||
padding: 5px 0px;
|
||||
cursor: pointer;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
|
@ -34,86 +34,51 @@
|
|||
title: lastPartOfName(layout),
|
||||
}
|
||||
|
||||
const confirmDeleteComponent = async component => {
|
||||
componentToDelete = component
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
const setCurrentScreenToLayout = () => {
|
||||
store.setScreenType("page")
|
||||
$goto("./:page/page-layout")
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pagelayoutSection">
|
||||
<div class="components-nav-page">Page Layout</div>
|
||||
<div
|
||||
class="budibase__nav-item root"
|
||||
class:selected={$store.currentComponentInfo._id === _layout.component.props._id}
|
||||
on:click|stopPropagation={setCurrentScreenToLayout}>
|
||||
<span
|
||||
class="icon"
|
||||
class:rotate={$store.currentPreviewItem.name !== _layout.title}>
|
||||
<ArrowDownIcon />
|
||||
</span>
|
||||
|
||||
<span class="icon">
|
||||
<GridIcon />
|
||||
</span>
|
||||
|
||||
<span class="title">Page Layout</span>
|
||||
</div>
|
||||
|
||||
{#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
|
||||
<ComponentsHierarchyChildren
|
||||
thisComponent={_layout.component.props}
|
||||
components={_layout.component.props._children}
|
||||
currentComponent={$store.currentComponentInfo}
|
||||
onDeleteComponent={confirmDeleteComponent}
|
||||
onMoveUpComponent={store.moveUpComponent}
|
||||
onMoveDownComponent={store.moveDownComponent}
|
||||
onCopyComponent={store.copyComponent} />
|
||||
{/if}
|
||||
<div
|
||||
class="budibase__nav-item root"
|
||||
class:selected={$store.currentComponentInfo._id === _layout.component.props._id}
|
||||
on:click|stopPropagation={setCurrentScreenToLayout}>
|
||||
<span
|
||||
class="icon"
|
||||
class:rotate={$store.currentPreviewItem.name !== _layout.title}>
|
||||
<ArrowDownIcon />
|
||||
</span>
|
||||
<i class="ri-layout-3-fill icon-big" />
|
||||
<span class="title">Master Screen</span>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Delete"
|
||||
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component?`}
|
||||
okText="Delete Component"
|
||||
onOk={() => store.deleteComponent(componentToDelete)} />
|
||||
{#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
|
||||
<ComponentsHierarchyChildren
|
||||
thisComponent={_layout.component.props}
|
||||
components={_layout.component.props._children}
|
||||
currentComponent={$store.currentComponentInfo} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.components-nav-page {
|
||||
font-size: 13px;
|
||||
color: #000333;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
padding-left: 20px;
|
||||
font-weight: 600;
|
||||
opacity: 0.4;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.pagelayoutSection {
|
||||
margin: 20px 0px 20px 0px;
|
||||
}
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
transition: 0.2s;
|
||||
width: 20px;
|
||||
margin-top: 2px;
|
||||
color: #000333;
|
||||
color: var(--ink-light);
|
||||
}
|
||||
|
||||
.icon:nth-of-type(2) {
|
||||
width: 14px;
|
||||
margin: 0 0 0 5px;
|
||||
.icon-big {
|
||||
font-size: 24px;
|
||||
color: var(--ink-light);
|
||||
}
|
||||
|
||||
:global(svg) {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<script>
|
||||
import { params, goto } from "@sveltech/routify"
|
||||
import { store } from "builderStore"
|
||||
import getIcon from "components/common/icon"
|
||||
import { CheckIcon } from "components/common/Icons"
|
||||
|
||||
const getPage = (s, name) => {
|
||||
const props = s.pages[name]
|
||||
|
@ -11,16 +9,17 @@
|
|||
|
||||
const pages = [
|
||||
{
|
||||
title: "Main",
|
||||
title: "Private",
|
||||
id: "main",
|
||||
},
|
||||
{
|
||||
title: "Login",
|
||||
title: "Public",
|
||||
id: "unauthenticated",
|
||||
},
|
||||
]
|
||||
|
||||
store.setCurrentPage($params.page ? $params.page : "main")
|
||||
if (!$store.currentPageName)
|
||||
store.setCurrentPage($params.page ? $params.page : "main")
|
||||
|
||||
const changePage = id => {
|
||||
store.setCurrentPage(id)
|
||||
|
@ -29,63 +28,37 @@
|
|||
</script>
|
||||
|
||||
<div class="root">
|
||||
<ul>
|
||||
{#each pages as { title, id }}
|
||||
<li>
|
||||
<span class="icon">
|
||||
{#if id === $params.page}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class:active={id === $params.page}
|
||||
on:click={() => changePage(id)}>
|
||||
{title}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#each pages as { title, id }}
|
||||
<button class:active={id === $params.page} on:click={() => changePage(id)}>
|
||||
{title}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
padding-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
color: #000333;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
padding-left: 1.8rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 0 0 6px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-family: Roboto;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
padding: 8px 16px;
|
||||
text-align: center;
|
||||
background: #ffffff;
|
||||
color: var(--ink-light);
|
||||
border-radius: 5px;
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
transition: all 0.3s;
|
||||
text-rendering: optimizeLegibility;
|
||||
border: none !important;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.active {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
color: #000333;
|
||||
background: var(--ink-light);
|
||||
color: var(--white);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./PropertyCascader.svelte"
|
|
@ -50,6 +50,9 @@
|
|||
|
||||
.label {
|
||||
flex: 0 0 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px 5px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
|
@ -60,6 +63,7 @@
|
|||
|
||||
.control {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding-left: 2px;
|
||||
max-width: 164px;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { excludeProps } from "./propertyCategories.js"
|
||||
import PropertyControl from "./PropertyControl.svelte"
|
||||
import { DetailSummary } from "@budibase/bbui"
|
||||
|
||||
export let name = ""
|
||||
export let styleCategory = "normal"
|
||||
|
@ -8,75 +9,17 @@
|
|||
export let componentInstance = {}
|
||||
export let onStyleChanged = () => {}
|
||||
|
||||
export let show = false
|
||||
|
||||
const capitalize = name => name[0].toUpperCase() + name.slice(1)
|
||||
|
||||
$: icon = show ? "ri-arrow-down-s-fill" : "ri-arrow-right-s-fill"
|
||||
$: style = componentInstance["_styles"][styleCategory] || {}
|
||||
</script>
|
||||
|
||||
<div class="property-group-container">
|
||||
<div class="property-group-name" on:click={() => (show = !show)}>
|
||||
<div class="icon">
|
||||
<i class={icon} />
|
||||
</div>
|
||||
<div class="name">{capitalize(name)}</div>
|
||||
</div>
|
||||
<div class="property-panel" class:show>
|
||||
|
||||
{#each properties as props}
|
||||
<PropertyControl
|
||||
label={props.label}
|
||||
control={props.control}
|
||||
key={props.key}
|
||||
value={style[props.key]}
|
||||
onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
|
||||
props={{ ...excludeProps(props, ['control', 'label']) }} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.property-group-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
background: var(--grey-light);
|
||||
margin: 0px 0px 4px 0px;
|
||||
padding: 8px 12px;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.property-group-name {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
padding-top: 2px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: 0 0 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.property-panel {
|
||||
height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.show {
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
<DetailSummary {name}>
|
||||
{#each properties as props}
|
||||
<PropertyControl
|
||||
label={props.label}
|
||||
control={props.control}
|
||||
key={props.key}
|
||||
value={style[props.key]}
|
||||
onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
|
||||
props={{ ...excludeProps(props, ['control', 'label']) }} />
|
||||
{/each}
|
||||
</DetailSummary>
|
||||
|
|
|
@ -2,21 +2,61 @@
|
|||
import PropertyControl from "./PropertyControl.svelte"
|
||||
import InputGroup from "../common/Inputs/InputGroup.svelte"
|
||||
import Colorpicker from "../common/Colorpicker.svelte"
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { excludeProps } from "./propertyCategories.js"
|
||||
import Input from "../common/Input.svelte"
|
||||
|
||||
export let panelDefinition = []
|
||||
export let componentDefinition = {}
|
||||
export let componentInstance = {}
|
||||
export let onChange = () => {}
|
||||
export let onScreenPropChange = () => {}
|
||||
export let screenOrPageInstance
|
||||
|
||||
const propExistsOnComponentDef = prop => prop in componentDefinition.props
|
||||
|
||||
function handleChange(key, data) {
|
||||
data.target ? onChange(key, data.target.value) : onChange(key, data)
|
||||
}
|
||||
|
||||
function handleScreenPropChange (name, value) {
|
||||
onScreenPropChange(name,value)
|
||||
if(!isPage && name === "name") {
|
||||
// screen name is changed... change URL
|
||||
$goto(`./:page/${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
const screenDefinition = [
|
||||
{ key: "name", label: "Name", control: Input },
|
||||
{ key: "description", label: "Description", control: Input },
|
||||
{ key: "route", label: "Route", control: Input },
|
||||
]
|
||||
|
||||
const pageDefinition = [
|
||||
{ key: "title", label: "Title", control: Input },
|
||||
{ key: "favicon", label: "Favicon", control: Input },
|
||||
]
|
||||
|
||||
$: isPage = screenOrPageInstance && screenOrPageInstance.favicon
|
||||
$: screenOrPageDefinition = isPage ? pageDefinition : screenDefinition
|
||||
|
||||
</script>
|
||||
|
||||
{#if panelDefinition.length > 0}
|
||||
{#if screenOrPageInstance}
|
||||
{#each screenOrPageDefinition as def}
|
||||
<PropertyControl
|
||||
control={def.control}
|
||||
label={def.label}
|
||||
key={def.key}
|
||||
value={screenOrPageInstance[def.key]}
|
||||
onChange={handleScreenPropChange}
|
||||
props={{ ...excludeProps(def, ['control', 'label']) }} />
|
||||
{/each}
|
||||
<hr/>
|
||||
{/if}
|
||||
|
||||
{#if panelDefinition && panelDefinition.length > 0}
|
||||
{#each panelDefinition as definition}
|
||||
{#if propExistsOnComponentDef(definition.key)}
|
||||
<PropertyControl
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
import { backendUiStore } from "builderStore"
|
||||
import IconButton from "../common/IconButton.svelte"
|
||||
import Input from "../common/Input.svelte"
|
||||
import PropertyCascader from "./PropertyCascader"
|
||||
import { isBinding, getBinding, setBinding } from "../common/binding"
|
||||
import Colorpicker from "../common/Colorpicker.svelte"
|
||||
|
||||
export let value = ""
|
||||
|
@ -49,8 +47,6 @@
|
|||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<PropertyCascader {onChanged} {value} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -27,11 +27,6 @@
|
|||
settingsView.show()
|
||||
}
|
||||
|
||||
const confirmDeleteComponent = component => {
|
||||
componentToDelete = component
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
const lastPartOfName = c => (c ? last(c.split("/")) : "")
|
||||
</script>
|
||||
|
||||
|
@ -86,13 +81,6 @@
|
|||
<NewScreen bind:this={newScreenPicker} />
|
||||
<SettingsView bind:this={settingsView} />
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Delete"
|
||||
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component`}
|
||||
okText="Delete Component"
|
||||
onOk={() => store.deleteComponent(componentToDelete)} />
|
||||
|
||||
<style>
|
||||
button {
|
||||
cursor: pointer;
|
||||
|
@ -120,7 +108,6 @@
|
|||
background-color: var(--white);
|
||||
height: calc(100vh - 49px);
|
||||
padding: 0;
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -136,8 +123,7 @@
|
|||
.components-pane {
|
||||
grid-column: 3;
|
||||
background-color: var(--white);
|
||||
height: 100vh;
|
||||
overflow-y: scroll;
|
||||
height: calc(100vh - 49px);
|
||||
}
|
||||
|
||||
.components-nav-page {
|
||||
|
@ -215,10 +201,6 @@
|
|||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.border-line {
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
}
|
||||
|
||||
.components-list-container {
|
||||
padding: 20px 0px 0 0;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Input from "../common/Input.svelte"
|
||||
import OptionSelect from "./OptionSelect.svelte"
|
||||
import InputGroup from "../common/Inputs/InputGroup.svelte"
|
||||
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
||||
// import Colorpicker from "../common/Colorpicker.svelte"
|
||||
/*
|
||||
TODO: Allow for default values for all properties
|
||||
|
@ -11,8 +12,9 @@ export const layout = [
|
|||
label: "Display",
|
||||
key: "display",
|
||||
control: OptionSelect,
|
||||
initialValue: "Flex",
|
||||
initialValue: "",
|
||||
options: [
|
||||
{ label: "", value: "" },
|
||||
{ label: "Flex", value: "flex" },
|
||||
{ label: "Inline Flex", value: "inline-flex" },
|
||||
],
|
||||
|
@ -20,13 +22,16 @@ export const layout = [
|
|||
{
|
||||
label: "Direction",
|
||||
key: "flex-direction",
|
||||
control: OptionSelect,
|
||||
initialValue: "Row",
|
||||
options: [
|
||||
{ label: "Row", value: "row" },
|
||||
{ label: "Row Reverse", value: "rowReverse" },
|
||||
{ label: "column", value: "column" },
|
||||
{ label: "Column Reverse", value: "columnReverse" },
|
||||
control: FlatButtonGroup,
|
||||
buttonProps: [
|
||||
{ icon: "ri-arrow-right-line", padding: "0px 5px", value: "row" },
|
||||
{ icon: "ri-arrow-left-line", padding: "0px 5px", value: "rowReverse" },
|
||||
{ icon: "ri-arrow-down-line", padding: "0px 5px", value: "column" },
|
||||
{
|
||||
icon: "ri-arrow-up-line",
|
||||
padding: "0px 5px",
|
||||
value: "columnReverse",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -35,6 +40,7 @@ export const layout = [
|
|||
control: OptionSelect,
|
||||
initialValue: "Flex Start",
|
||||
options: [
|
||||
{ label: "", value: "" },
|
||||
{ label: "Flex Start", value: "flex-start" },
|
||||
{ label: "Flex End", value: "flex-end" },
|
||||
{ label: "Center", value: "center" },
|
||||
|
@ -60,39 +66,86 @@ export const layout = [
|
|||
label: "Wrap",
|
||||
key: "flex-wrap",
|
||||
control: OptionSelect,
|
||||
initialValue: "Wrap",
|
||||
options: [
|
||||
{ label: "Wrap", value: "wrap" },
|
||||
{ label: "No Wrap", value: "nowrap" },
|
||||
{ label: "Wrap Reverse", value: "wrap-reverse" },
|
||||
{ label: "wrap", value: "wrap" },
|
||||
{ label: "no wrap", value: "noWrap" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const spacingMeta = [
|
||||
{ placeholder: "L" },
|
||||
{ placeholder: "B" },
|
||||
{ placeholder: "R" },
|
||||
{ placeholder: "T" },
|
||||
{ placeholder: "R" },
|
||||
{ placeholder: "B" },
|
||||
{ placeholder: "L" },
|
||||
]
|
||||
|
||||
export const spacing = [
|
||||
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
|
||||
{
|
||||
label: "Margin",
|
||||
key: "margin",
|
||||
control: InputGroup,
|
||||
meta: spacingMeta,
|
||||
defaultValue: ["0", "0", "0", "0"],
|
||||
},
|
||||
{
|
||||
label: "Padding",
|
||||
key: "padding",
|
||||
control: InputGroup,
|
||||
meta: spacingMeta,
|
||||
defaultValue: ["0", "0", "0", "0"],
|
||||
},
|
||||
]
|
||||
|
||||
export const size = [
|
||||
{ label: "Width", key: "width", control: Input },
|
||||
{ label: "Height", key: "height", control: Input },
|
||||
{ label: "Min W", key: "min-width", control: Input },
|
||||
{ label: "Min H", key: "min-height", control: Input },
|
||||
{ label: "Max W", key: "max-width", control: Input },
|
||||
{ label: "Max H", key: "max-height", control: Input },
|
||||
{
|
||||
label: "Width",
|
||||
key: "width",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Height",
|
||||
key: "height",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Min W",
|
||||
key: "min-width",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Min H",
|
||||
key: "min-height",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Max W",
|
||||
key: "max-width",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Max H",
|
||||
key: "max-height",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
]
|
||||
|
||||
export const position = [
|
||||
|
@ -113,26 +166,41 @@ export const position = [
|
|||
label: "Top",
|
||||
key: "top",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Right",
|
||||
key: "right",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Bottom",
|
||||
key: "bottom",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Left",
|
||||
key: "left",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Z-index",
|
||||
key: "z-index",
|
||||
control: Input,
|
||||
placeholder: "num",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -166,31 +234,58 @@ export const typography = [
|
|||
label: "Weight",
|
||||
key: "font-weight",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "100", value: "100" },
|
||||
{ label: "200", value: "200" },
|
||||
{ label: "300", value: "300" },
|
||||
{ label: "400", value: "400" },
|
||||
{ label: "500", value: "500" },
|
||||
{ label: "600", value: "600" },
|
||||
{ label: "700", value: "700" },
|
||||
{ label: "800", value: "800" },
|
||||
{ label: "900", value: "900" },
|
||||
],
|
||||
options: ["200", "300", "400", "500", "600", "700", "800", "900"],
|
||||
},
|
||||
{
|
||||
label: "size",
|
||||
key: "font-size",
|
||||
defaultValue: "",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
label: "Line H",
|
||||
key: "line-height",
|
||||
control: Input,
|
||||
placeholder: "lh",
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
},
|
||||
{ label: "size", key: "font-size", defaultValue: "", control: Input },
|
||||
{ label: "Line H", key: "line-height", control: Input },
|
||||
{
|
||||
label: "Color",
|
||||
key: "color",
|
||||
control: Input,
|
||||
placeholder: "hex",
|
||||
},
|
||||
{
|
||||
label: "align",
|
||||
key: "text-align",
|
||||
control: OptionSelect,
|
||||
options: ["initial", "left", "right", "center", "justify"],
|
||||
}, //custom
|
||||
control: FlatButtonGroup,
|
||||
buttonProps: [
|
||||
{ icon: "ri-align-left", padding: "0px 5px", value: "left" },
|
||||
{ icon: "ri-align-center", padding: "0px 5px", value: "center" },
|
||||
{ icon: "ri-align-right", padding: "0px 5px", value: "right" },
|
||||
{ icon: "ri-align-justify", padding: "0px 5px", value: "justify" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "transform",
|
||||
key: "text-transform",
|
||||
control: FlatButtonGroup,
|
||||
buttonProps: [
|
||||
{ text: "BB", padding: "0px 5px", fontWeight: 500, value: "uppercase" },
|
||||
{ text: "Bb", padding: "0px 5px", fontWeight: 500, value: "capitalize" },
|
||||
{ text: "bb", padding: "0px 5px", fontWeight: 500, value: "lowercase" },
|
||||
{
|
||||
text: "×",
|
||||
padding: "0px 5px",
|
||||
fontWeight: 500,
|
||||
value: "none",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Decoration",
|
||||
key: "text-decoration-line",
|
||||
|
@ -204,23 +299,51 @@ export const typography = [
|
|||
{ label: "Under Over", value: "underline overline" },
|
||||
],
|
||||
},
|
||||
|
||||
{ label: "transform", key: "text-transform", control: Input }, //custom
|
||||
{ label: "style", key: "font-style", control: Input }, //custom
|
||||
]
|
||||
|
||||
export const background = [
|
||||
{
|
||||
label: "Background",
|
||||
label: "Color",
|
||||
key: "background",
|
||||
control: Input,
|
||||
},
|
||||
{ label: "Image", key: "image", control: Input }, //custom
|
||||
{
|
||||
label: "Image",
|
||||
key: "background-image",
|
||||
control: Input,
|
||||
placeholder: "src",
|
||||
},
|
||||
]
|
||||
|
||||
export const border = [
|
||||
{ label: "Radius", key: "border-radius", control: Input },
|
||||
{ label: "Width", key: "border-width", control: Input }, //custom
|
||||
{
|
||||
label: "Radius",
|
||||
key: "border-radius",
|
||||
control: OptionSelect,
|
||||
defaultValue: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "small", value: "0.125rem" },
|
||||
{ label: "Medium", value: "0.25rem;" },
|
||||
{ label: "Large", value: "0.375rem" },
|
||||
{ label: "Extra large", value: "0.5rem" },
|
||||
{ label: "Full", value: "5678px" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Width",
|
||||
key: "border-width",
|
||||
control: OptionSelect,
|
||||
defaultValue: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "Extra small", value: "0.5px" },
|
||||
{ label: "Small", value: "1px" },
|
||||
{ label: "Medium", value: "2px" },
|
||||
{ label: "Large", value: "4px" },
|
||||
{ label: "Extra large", value: "8px" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Color",
|
||||
key: "border-color",
|
||||
|
@ -230,6 +353,7 @@ export const border = [
|
|||
label: "Style",
|
||||
key: "border-style",
|
||||
control: OptionSelect,
|
||||
defaultValue: "None",
|
||||
options: [
|
||||
"none",
|
||||
"hidden",
|
||||
|
@ -246,30 +370,98 @@ export const border = [
|
|||
]
|
||||
|
||||
export const effects = [
|
||||
{ label: "Opacity", key: "opacity", control: Input },
|
||||
{
|
||||
label: "Opacity",
|
||||
key: "opacity",
|
||||
control: Input,
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
placeholder: "%",
|
||||
},
|
||||
{
|
||||
label: "Rotate",
|
||||
key: "transform",
|
||||
key: "transform-rotate",
|
||||
control: OptionSelect,
|
||||
defaultValue: "0",
|
||||
options: [
|
||||
{ label: "None", value: "rotate(0deg)" },
|
||||
{ label: "45 degrees", value: "rotate(45deg)" },
|
||||
{ label: "90 degrees", value: "rotate(90deg)" },
|
||||
{ label: "135 degrees", value: "rotate(135deg)" },
|
||||
{ label: "180 degrees", value: "rotate(180deg)" },
|
||||
{ label: "225 degrees", value: "rotate(225deg)" },
|
||||
{ label: "270 degrees", value: "rotate(270deg)" },
|
||||
{ label: "315 degrees", value: "rotate(315deg)" },
|
||||
{ label: "360 degrees", value: "rotate(360deg)" },
|
||||
"0",
|
||||
"45deg",
|
||||
"90deg",
|
||||
"90deg",
|
||||
"135deg",
|
||||
"180deg",
|
||||
"225deg",
|
||||
"270deg",
|
||||
"315dev",
|
||||
],
|
||||
}, //needs special control
|
||||
{ label: "Shadow", key: "box-shadow", control: Input },
|
||||
{
|
||||
label: "Shadow",
|
||||
key: "box-shadow",
|
||||
control: OptionSelect,
|
||||
defaultValue: "None",
|
||||
options: [
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "Extra small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
|
||||
{
|
||||
label: "Small",
|
||||
value:
|
||||
"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
|
||||
},
|
||||
{
|
||||
label: "Medium",
|
||||
value:
|
||||
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
|
||||
},
|
||||
{
|
||||
label: "Large",
|
||||
value:
|
||||
"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
{
|
||||
label: "Extra large",
|
||||
value:
|
||||
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const transitions = [
|
||||
{ label: "Property", key: "transition-property", control: Input },
|
||||
{ label: "Duration", key: "transition-timing-function", control: Input },
|
||||
{ label: "Ease", key: "transition-ease", control: Input },
|
||||
{
|
||||
label: "Property",
|
||||
key: "transition-property",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
"None",
|
||||
"All",
|
||||
"Background Color",
|
||||
"Color",
|
||||
"Font Size",
|
||||
"Font Weight",
|
||||
"Height",
|
||||
"Margin",
|
||||
"Opacity",
|
||||
"Padding",
|
||||
"Rotate",
|
||||
"Shadow",
|
||||
"Width",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Duration",
|
||||
key: "transition-duration",
|
||||
control: Input,
|
||||
width: "48px",
|
||||
textAlign: "center",
|
||||
placeholder: "sec",
|
||||
},
|
||||
{
|
||||
label: "Ease",
|
||||
key: "transition-timing-function:",
|
||||
control: OptionSelect,
|
||||
options: ["linear", "ease", "ease-in", "ease-out", "ease-in-out"],
|
||||
},
|
||||
]
|
||||
|
||||
export const all = {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Input from "../common/Input.svelte"
|
||||
import OptionSelect from "./OptionSelect.svelte"
|
||||
import Checkbox from "../common/Checkbox.svelte"
|
||||
import ModelSelect from "components/userInterface/ModelSelect.svelte"
|
||||
|
||||
import { all } from "./propertyCategories.js"
|
||||
|
||||
|
@ -10,6 +11,18 @@ export default {
|
|||
name: "Basic",
|
||||
isCategory: true,
|
||||
children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/embed",
|
||||
icon: "ri-code-line",
|
||||
name: "Embed",
|
||||
description: "Embed content from 3rd party sources",
|
||||
properties: {
|
||||
design: {
|
||||
...all,
|
||||
},
|
||||
settings: [{ label: "Embed", key: "embed", control: Input }],
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "@budibase/standard-components/container",
|
||||
name: "Container",
|
||||
|
@ -186,6 +199,17 @@ export default {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "@budibase/standard-components/image",
|
||||
name: "Image",
|
||||
description: "A basic component for displaying images",
|
||||
icon: "ri-image-fill",
|
||||
children: [],
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [{ label: "URL", key: "url", control: Input }],
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "@budibase/standard-components/icon",
|
||||
name: "Icon",
|
||||
|
@ -229,29 +253,79 @@ export default {
|
|||
"A basic card component that can contain content and actions.",
|
||||
icon: "ri-layout-bottom-fill",
|
||||
children: [],
|
||||
properties: { design: { ...all } },
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [
|
||||
{
|
||||
label: "Heading",
|
||||
key: "heading",
|
||||
control: Input,
|
||||
placeholder: "text",
|
||||
},
|
||||
{
|
||||
label: "Subheading",
|
||||
key: "subheading",
|
||||
control: Input,
|
||||
placeholder: "text",
|
||||
},
|
||||
{
|
||||
label: "Content",
|
||||
key: "content",
|
||||
control: Input,
|
||||
placeholder: "text",
|
||||
},
|
||||
{
|
||||
label: "Image",
|
||||
key: "imageUrl",
|
||||
control: Input,
|
||||
placeholder: "src",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Login",
|
||||
_component: "@budibase/standard-components/login",
|
||||
description:
|
||||
"A component that automatically generates a login screen for your app.",
|
||||
icon: "ri-login-box-fill",
|
||||
children: [],
|
||||
properties: { design: { ...all } },
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [
|
||||
{
|
||||
label: "Name",
|
||||
key: "name",
|
||||
control: Input,
|
||||
},
|
||||
{
|
||||
label: "Logo",
|
||||
key: "logo",
|
||||
control: Input,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Table",
|
||||
_component: "@budibase/standard-components/datatable",
|
||||
description: "A component that generates a table from your data.",
|
||||
icon: "ri-archive-drawer-fill",
|
||||
properties: { design: { ...all } },
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [{ label: "Model", key: "model", control: ModelSelect }],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "Form",
|
||||
description: "A component that generates a form from your data.",
|
||||
icon: "ri-file-edit-fill",
|
||||
properties: { design: { ...all } },
|
||||
_component: "@budibase/materialdesign-components/Form",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [{ label: "Model", key: "model", control: ModelSelect }],
|
||||
},
|
||||
_component: "@budibase/standard-components/dataform",
|
||||
template: {
|
||||
component: "@budibase/materialdesign-components/Form",
|
||||
description: "Form for saving a record",
|
||||
|
@ -264,15 +338,53 @@ export default {
|
|||
_component: "@budibase/standard-components/datachart",
|
||||
description: "Shiny chart",
|
||||
icon: "ri-bar-chart-fill",
|
||||
properties: { design: { ...all } },
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [
|
||||
{ label: "Model", key: "model", control: ModelSelect },
|
||||
{
|
||||
label: "Chart Type",
|
||||
key: "type",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
"column2d",
|
||||
"column3d",
|
||||
"line",
|
||||
"area2d",
|
||||
"bar2d",
|
||||
"bar3d",
|
||||
"pie2d",
|
||||
"pie3d",
|
||||
"doughnut2d",
|
||||
"doughnut3d",
|
||||
"pareto2d",
|
||||
"pareto3d",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "Data List",
|
||||
_component: "@budibase/standard-components/datalist",
|
||||
description: "Shiny list",
|
||||
icon: "ri-file-list-fill",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [{ label: "Model", key: "model", control: ModelSelect }],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "List",
|
||||
_component: "@budibase/standard-components/datalist",
|
||||
_component: "@budibase/standard-components/list",
|
||||
description: "Shiny list",
|
||||
icon: "ri-file-list-fill",
|
||||
properties: { design: { ...all } },
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [{ label: "Model", key: "model", control: ModelSelect }],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
@ -306,7 +418,15 @@ export default {
|
|||
"A component for handling the navigation within your app.",
|
||||
icon: "ri-navigation-fill",
|
||||
children: [],
|
||||
properties: { design: { ...all } },
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [
|
||||
{ label: "Logo URL", key: "logoUrl", control: Input },
|
||||
{ label: "Title", key: "title", control: Input },
|
||||
{ label: "Color", key: "color", control: Input },
|
||||
{ label: "Background", key: "backgroundColor", control: Input },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
export { default as SetupPanel } from "./SetupPanel.svelte"
|
|
@ -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>
|
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||
}
|