This commit is contained in:
Martin McKeaveney 2020-06-01 16:27:27 +01:00
commit 398f200661
63 changed files with 1652 additions and 1125 deletions

View File

@ -39,6 +39,7 @@
}, },
"dependencies": { "dependencies": {
"@beyonk/svelte-notifications": "^2.0.3", "@beyonk/svelte-notifications": "^2.0.3",
"@budibase/bbui": "^0.3.5",
"@budibase/client": "^0.0.32", "@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0", "@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0", "codemirror": "^5.51.0",
@ -83,7 +84,7 @@
"rollup-plugin-svelte": "^5.0.3", "rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4", "rollup-plugin-terser": "^4.0.4",
"rollup-plugin-url": "^2.2.2", "rollup-plugin-url": "^2.2.2",
"svelte": "^3.0.0" "svelte": "3.23.x"
}, },
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072" "gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
} }

View File

@ -30,6 +30,4 @@
<!-- svelte-notifications --> <!-- svelte-notifications -->
<NotificationDisplay /> <NotificationDisplay />
<Modal> <Router {routes} />
<Router {routes} />
</Modal>

View File

@ -57,23 +57,23 @@
.budibase__nav-item { .budibase__nav-item {
cursor: pointer; cursor: pointer;
padding: 0 7px 0 3px; padding: 0 4px 0 2px;
height: 35px; height: 35px;
margin: 5px 20px 5px 0px; margin: 5px 0px 4px 0px;
border-radius: 0 5px 5px 0; border-radius: 0 5px 5px 0;
display: flex; display: flex;
align-items: center; align-items: center;
font-weight: 500; font-size: 14px;
font-size: 13px; transition: 0.2s;
} }
.budibase__nav-item.selected { .budibase__nav-item.selected {
color: var(--button-text); color: var(--ink);
background: #f1f4fc; background: var(--blue-light);
} }
.budibase__nav-item:hover { .budibase__nav-item:hover {
background: #fafafa; background: var(--grey-light);
} }
.budibase__input { .budibase__input {

View File

@ -1,4 +1,4 @@
import { cloneDeep, values } from "lodash/fp" import { values } from "lodash/fp"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend" import * as backendStoreActions from "./backend"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
@ -16,6 +16,14 @@ import { buildCodeForScreens } from "../buildCodeForScreens"
import { generate_screen_css } from "../generate_css" import { generate_screen_css } from "../generate_css"
import { insertCodeMetadata } from "../insertCodeMetadata" import { insertCodeMetadata } from "../insertCodeMetadata"
import { uuid } from "../uuid" import { uuid } from "../uuid"
import {
selectComponent as _selectComponent,
getParent,
walkProps,
savePage as _savePage,
saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
} from "../storeUtils"
export const getStore = () => { export const getStore = () => {
const initial = { const initial = {
@ -57,10 +65,6 @@ export const getStore = () => {
store.setComponentStyle = setComponentStyle(store) store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store) store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(store) store.setScreenType = setScreenType(store)
store.deleteComponent = deleteComponent(store)
store.moveUpComponent = moveUpComponent(store)
store.moveDownComponent = moveDownComponent(store)
store.copyComponent = copyComponent(store)
store.getPathToComponent = getPathToComponent(store) store.getPathToComponent = getPathToComponent(store)
store.addTemplatedComponent = addTemplatedComponent(store) store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store) store.setMetadataProp = setMetadataProp(store)
@ -69,6 +73,9 @@ export const getStore = () => {
export default getStore export default getStore
export const getComponentDefinition = (state, name) =>
name.startsWith("##") ? getBuiltin(name) : state.components[name]
const setPackage = (store, initial) => async pkg => { const setPackage = (store, initial) => async pkg => {
const [main_screens, unauth_screens] = await Promise.all([ const [main_screens, unauth_screens] = await Promise.all([
api api
@ -140,12 +147,6 @@ const _saveScreen = async (store, s, screen) => {
return s return s
} }
const _saveScreenApi = (screen, s) => {
api
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
.then(() => _savePage(s))
}
const createScreen = store => (screenName, route, layoutComponentName) => { const createScreen = store => (screenName, route, layoutComponentName) => {
store.update(state => { store.update(state => {
const rootComponent = state.components[layoutComponentName] const rootComponent = state.components[layoutComponentName]
@ -276,14 +277,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 },
screens: page._screens,
})
}
const setCurrentPage = store => pageName => { const setCurrentPage = store => pageName => {
store.update(state => { store.update(state => {
const current_screens = state.pages[pageName]._screens const current_screens = state.pages[pageName]._screens
@ -315,8 +308,6 @@ const setCurrentPage = store => pageName => {
}) })
} }
// const getComponentDefinition = (components, name) => components.find(c => c.name === name)
/** /**
* @param {string} componentToAdd - name of the component to add to the application * @param {string} componentToAdd - name of the component to add to the application
* @param {string} presetName - name of the component preset if defined * @param {string} presetName - name of the component preset if defined
@ -342,9 +333,7 @@ const addChildComponent = store => (componentToAdd, presetName) => {
return state return state
} }
const component = componentToAdd.startsWith("##") const component = getComponentDefinition(state, componentToAdd)
? getBuiltin(componentToAdd)
: state.components[componentToAdd]
const presetProps = presetName ? component.presets[presetName] : {} const presetProps = presetName ? component.presets[presetName] : {}
@ -398,12 +387,7 @@ const addTemplatedComponent = store => props => {
const selectComponent = store => component => { const selectComponent = store => component => {
store.update(state => { store.update(state => {
const componentDef = component._component.startsWith("##") return _selectComponent(state, component)
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
}) })
} }
@ -470,75 +454,6 @@ const setScreenType = store => type => {
}) })
} }
const deleteComponent = store => componentName => {
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, componentName)
if (parent) {
parent._children = parent._children.filter(
component => component !== componentName
)
}
_saveCurrentPreviewItem(state)
return state
})
}
const moveUpComponent = store => component => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === 0) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
_saveCurrentPreviewItem(s)
return s
})
}
const moveDownComponent = store => component => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === parent._children.length - 1) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
_saveCurrentPreviewItem(s)
return s
})
}
const copyComponent = store => component => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
const copiedComponent = cloneDeep(component)
walkProps(copiedComponent, p => {
p._id = uuid()
})
parent._children = [...parent._children, copiedComponent]
s.curren
_saveCurrentPreviewItem(s)
s.currentComponentInfo = copiedComponent
return s
})
}
const getPathToComponent = store => component => { const getPathToComponent = store => component => {
// Gets all the components to needed to construct a path. // Gets all the components to needed to construct a path.
const tempStore = get(store) const tempStore = get(store)
@ -570,39 +485,9 @@ const getPathToComponent = store => component => {
return path return path
} }
const getParent = (rootProps, child) => {
let parent
walkProps(rootProps, (p, breakWalk) => {
if (p._children && p._children.includes(child)) {
parent = p
breakWalk()
}
})
return parent
}
const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false }
action(props, () => {
cancelToken.cancelled = true
})
if (props._children) {
for (let child of props._children) {
if (cancelToken.cancelled) return
walkProps(child, action, cancelToken)
}
}
}
const setMetadataProp = store => (name, prop) => { const setMetadataProp = store => (name, prop) => {
store.update(s => { store.update(s => {
s.currentPreviewItem[name] = prop s.currentPreviewItem[name] = prop
return s return s
}) })
} }
const _saveCurrentPreviewItem = s =>
s.currentFrontEndType === "page"
? _savePage(s)
: _saveScreenApi(s.currentPreviewItem, s)

View File

@ -0,0 +1,59 @@
import { makePropsSafe } from "components/userInterface/pagesParsing/createProps"
import api from "./api"
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 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)
}
}
}

View File

@ -5,7 +5,7 @@
UIKit.notification({ UIKit.notification({
message: ` message: `
<div class="message-container"> <div class="message-container">
<i class="ri-information-fill information-icon"></i> <div class="information-icon">🤯</div>
<span class="notification-message"> <span class="notification-message">
${message} ${message}
</span> </span>
@ -21,6 +21,7 @@
<style> <style>
:global(.information-icon) { :global(.information-icon) {
font-size: 24px; font-size: 24px;
margin-right: 8px;
} }
:global(.uk-nofi) { :global(.uk-nofi) {
@ -31,10 +32,9 @@
} }
:global(.message-container) { :global(.message-container) {
display: grid; display: flex;
grid-template-columns: 40px 1fr auto;
grid-gap: 5px;
align-items: center; align-items: center;
justify-content: center;
} }
:global(.uk-notification) { :global(.uk-notification) {
@ -44,7 +44,6 @@
margin-right: auto !important; margin-right: auto !important;
margin-left: auto !important; margin-left: auto !important;
border-radius: 10px; border-radius: 10px;
box-shadow: 0px 3px 6px #00000029;
} }
:global(.uk-notification-message) { :global(.uk-notification-message) {
@ -56,21 +55,23 @@
} }
:global(.uk-notification-message-danger) { :global(.uk-notification-message-danger) {
background: #f2545b !important; background: var(--ink-light) !important;
color: #fff !important; color: #fff !important;
font-family: Roboto; font-family: Roboto;
font-size: 14px !important; font-size: 16px !important;
} }
:global(.refresh-page-button) { :global(.refresh-page-button) {
font-size: 13px; font-size: 14px;
font-weight: 600; border-radius: 3px;
border-radius: 5px;
border: none; border: none;
padding: 5px; padding: 8px 16px;
width: 91px; color: var(--ink);
height: 28px;
color: #f2545b;
background: #ffffff; background: #ffffff;
margin-left: 20px;
}
:global(.refresh-page-button):hover {
background: var(--grey-light);
} }
</style> </style>

View File

@ -0,0 +1,10 @@
<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="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414
1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
</svg>

After

Width:  |  Height:  |  Size: 292 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10
10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z" />
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

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

After

Width:  |  Height:  |  Size: 419 B

View File

@ -29,3 +29,6 @@ export { default as ContributionIcon } from "./Contribution.svelte"
export { default as BugIcon } from "./Bug.svelte" export { default as BugIcon } from "./Bug.svelte"
export { default as EmailIcon } from "./Email.svelte" export { default as EmailIcon } from "./Email.svelte"
export { default as TwitterIcon } from "./Twitter.svelte" export { default as TwitterIcon } from "./Twitter.svelte"
export { default as InfoIcon } from "./Info.svelte"
export { default as CloseIcon } from "./Close.svelte"
export { default as MoreIcon } from "./More.svelte"

View File

@ -0,0 +1,69 @@
<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);
}
.switcher {
display: flex;
margin: 0px 20px 20px 0px;
}
.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;
}
.switcher > .selected {
color: var(--ink);
}
</style>

View File

@ -15,13 +15,13 @@ export async function createDatabase(appname, instanceName) {
} }
export async function deleteRecord(record, instanceId) { export async function deleteRecord(record, instanceId) {
const DELETE_RECORDS_URL = `/api/${instanceId}/${record.modelId}/records/${record._id}/${record._rev}` const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}`
const response = await api.delete(DELETE_RECORDS_URL) const response = await api.delete(DELETE_RECORDS_URL)
return response return response
} }
export async function saveRecord(record, instanceId) { export async function saveRecord(record, instanceId, modelId) {
const SAVE_RECORDS_URL = `/api/${instanceId}/${record.modelId}/records` const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records`
const response = await api.post(SAVE_RECORDS_URL, record) const response = await api.post(SAVE_RECORDS_URL, record)
return await response.json() return await response.json()

View File

@ -13,17 +13,41 @@
const FIELD_TYPES = ["string", "number", "boolean"] const FIELD_TYPES = ["string", "number", "boolean"]
export let field = { type: "string" } export let field = {
type: "string",
constraints: { type: "string", presence: false },
}
export let schema export let schema
export let goBack export let goBack
let errors = [] let errors = []
let draftField = cloneDeep(field) let draftField = cloneDeep(field)
let type = field.type
let constraints = field.constraints
let required =
field.constraints.presence && !field.constraints.presence.allowEmpty
const save = () => { const save = () => {
constraints.presence = required ? { allowEmpty: false } : false
draftField.constraints = constraints
draftField.type = type
schema[field.name] = draftField schema[field.name] = draftField
goBack() goBack()
} }
$: constraints =
type === "string"
? { type: "string", length: {}, presence: false }
: type === "number"
? { type: "number", presence: false, numericality: {} }
: type === "boolean"
? { type: "boolean", presence: false }
: type === "datetime"
? { type: "date", datetime: {}, presence: false }
: type.startsWith("array")
? { type: "array", presence: false }
: { type: "string", presence: false }
</script> </script>
<div class="root"> <div class="root">
@ -32,32 +56,26 @@
<form on:submit|preventDefault class="uk-form-stacked"> <form on:submit|preventDefault class="uk-form-stacked">
<Textbox label="Name" bind:text={field.name} /> <Textbox label="Name" bind:text={field.name} />
<Dropdown <Dropdown label="Type" bind:selected={type} options={FIELD_TYPES} />
label="Type"
bind:selected={draftField.type}
options={FIELD_TYPES} />
{#if field.type === 'string'} <Checkbox label="Required" bind:checked={required} />
<NumberBox label="Max Length" bind:value={draftField.maxLength} />
<ValuesList label="Categories" bind:values={draftField.values} /> {#if type === 'string'}
{:else if field.type === 'boolean'} <NumberBox label="Max Length" bind:value={constraints.length.maximum} />
<!-- TODO: revisit and fix with JSON schema --> <ValuesList label="Categories" bind:values={constraints.inclusion} />
<Checkbox label="Allow Null" bind:checked={draftField.allowNulls} /> {:else if type === 'datetime'}
{:else if field.format === 'datetime'}
<!-- 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 --> <!-- TODO: revisit and fix with JSON schema -->
<DatePicker
label="Min Value"
bind:value={constraints.datetime.earliest} />
<DatePicker label="Max Value" bind:value={constraints.datetime.latest} />
{:else if type === 'number'}
<NumberBox <NumberBox
label="Min Length" label="Min Value"
bind:value={draftField.typeOptions.minLength} /> bind:value={constraints.numericality.greaterThanOrEqualTo} />
<NumberBox <NumberBox
label="Max Length" label="Max Value"
bind:value={draftField.typeOptions.maxLength} /> bind:value={constraints.numericality.lessThanOrEqualTo} />
{/if} {/if}
</form> </form>
</div> </div>

View File

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

View File

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

View File

@ -45,7 +45,6 @@
<DatabasesList /> <DatabasesList />
</div> </div>
</div> </div>
<hr />
{#if $backendUiStore.selectedDatabase._id} {#if $backendUiStore.selectedDatabase._id}
<div class="hierarchy"> <div class="hierarchy">
<div class="components-list-container"> <div class="components-list-container">

View File

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

View File

@ -0,0 +1,68 @@
<script>
import Button from "components/common/Button.svelte"
export let name,
description = `A minimalist CRM which removes the noise and allows you to focus
on your business.`,
_id
</script>
<div class="apps-card">
<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>
<style>
.apps-card {
background-color: var(--white);
padding: 20px;
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-medium);
}
.app-button:hover {
background-color: var(--grey-light);
text-decoration: none;
}
.app-title {
font-size: 18px;
font-weight: 700;
color: var(--ink);
text-transform: capitalize;
}
.app-desc {
color: var(--ink-light);
}
.card-footer {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
}
.modified-date {
font-size: 14px;
color: var(--ink-light);
}
.app-button {
background-color: var(--white);
color: var(--ink);
padding: 12px 20px;
border-radius: 5px;
border: 1px var(--grey) solid;
font-size: 14px;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
}
</style>

View File

@ -1,5 +1,5 @@
<script> <script>
import Button from "components/common/Button.svelte" import AppCard from "./AppCard.svelte"
export let apps export let apps
function myFunction() { function myFunction() {
@ -13,27 +13,23 @@
<div> <div>
<div> <div>
<div class="app-section-title">Your Web Apps</div> <div class="app-section-title">Your Web Apps</div>
{#each apps as app} <div class="apps">
<div class="apps-card"> {#each apps as app}
<h3 class="app-title">{app.name}</h3> <AppCard {...app} />
<p class="app-desc"> {/each}
A minimalist CRM which removes the noise and allows you to focus </div>
on your business.
</p>
<div class="card-footer">
<div class="modified-date">Last Edited - 25th May 2020</div>
<a href={`/_builder/${app._id}`} class="app-button">
Open Web App
</a>
</div>
</div>
{/each}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style> <style>
.apps {
display: grid;
grid-template-columns: repeat(auto-fill, 400px);
grid-gap: 40px 85px;
justify-content: start;
}
.root { .root {
margin: 40px 80px; margin: 40px 80px;
} }
@ -44,59 +40,4 @@
font-weight: 700; font-weight: 700;
margin-bottom: 20px; margin-bottom: 20px;
} }
.apps {
display: flex;
flex-wrap: wrap;
gap: 40px;
}
.apps-card {
background-color: var(--white);
padding: 20px;
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-dark);
}
.app-button:hover {
background-color: var(--grey-light);
text-decoration: none;
}
.app-title {
font-size: 18px;
font-weight: 700;
color: var(--ink);
text-transform: capitalize;
}
.app-desc {
color: var(--ink-light);
}
.card-footer {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
}
.modified-date {
font-size: 14px;
color: var(--ink-light);
}
.app-button {
background-color: var(--white);
color: var(--ink);
padding: 12px 20px;
border-radius: 5px;
border: 1px var(--grey) solid;
font-size: 14px;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
}
</style> </style>

View File

@ -0,0 +1,197 @@
<script>
import Spinner from "components/common/Spinner.svelte"
import { Input, TextArea, Button } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { fade } from "svelte/transition"
const { open, close } = getContext("simple-modal")
let name = ""
let description = ""
let loading = false
let error = {}
const createNewApp = async () => {
if ((name.length > 100 || name.length < 1) && description.length < 1) {
error = {
name: true,
description: true,
}
} else if (description.length < 1) {
error = {
name: false,
description: true,
}
} else if (name.length > 100 || name.length < 1) {
error = {
name: true,
}
} else {
error = {}
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 res = await response.json()
$goto(`./${res._id}`)
} catch (error) {
console.error(error)
}
}
}
let value
let onChange = () => {}
function _onCancel() {
close()
}
async function _onOkay() {
await createNewApp()
}
</script>
<div class="container">
<div class="body">
<div class="heading">
<span class="icon">
<AppsIcon />
</span>
<h3>Create new web app</h3>
</div>
<Input
name="name"
label="Name"
placeholder="Enter application name"
on:change={e => (name = e.target.value)}
on:input={e => (name = e.target.value)} />
{#if error.name}
<span class="error">You need to enter a name for your application.</span>
{/if}
<TextArea
bind:value={description}
name="description"
label="Description"
placeholder="Describe your application" />
{#if error.description}
<span class="error">
Please enter a short description of your application
</span>
{/if}
</div>
<div class="footer">
<a href="./#" class="info">
<InfoIcon />
How to get started
</a>
<Button outline thin on:click={_onCancel}>Cancel</Button>
<Button primary thin on:click={_onOkay}>Save</Button>
</div>
<div class="close-button" on:click={_onCancel}>
<CloseIcon />
</div>
{#if loading}
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
</div>
{/if}
</div>
<style>
.container {
position: relative;
}
.close-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
}
.heading {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
}
h3 {
margin: 0;
font-size: 24px;
font-weight: bold;
}
.icon {
display: grid;
border-radius: 3px;
align-content: center;
justify-content: center;
margin-right: 12px;
height: 20px;
width: 20px;
padding: 10px;
background-color: var(--blue-light);
}
.info {
color: var(--primary100);
text-decoration-color: var(--primary100);
}
.info :global(svg) {
fill: var(--primary100);
margin-right: 8px;
width: 24px;
height: 24px;
}
.body {
padding: 40px 40px 80px 40px;
display: grid;
grid-gap: 20px;
}
.footer {
display: grid;
grid-gap: 20px;
align-items: center;
grid-template-columns: 1fr auto auto;
padding: 30px 40px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-light);
}
.spinner-container {
background: white;
position: absolute;
border-radius: 5px;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: grid;
justify-items: center;
align-content: center;
grid-gap: 50px;
}
.spinner-text {
font-size: 2em;
}
.error {
color: var(--deletion100);
font-weight: bold;
font-size: 0.8em;
}
</style>

View File

@ -0,0 +1,232 @@
<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,
} 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 || $store.componentToPaste._id === component._id
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
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))
saveCurrentPreviewItem(s)
selectComponent(s, componentToPaste)
return s
})
}
</script>
<div class="root" on:click|stopPropagation={() => {}}>
<button>
<MoreIcon />
</button>
<ul class="menu" bind:this={dropdownEl} on:click={hideDropdown}>
<li on:click={() => confirmDeleteDialog.show()}>Delete</li>
<li on:click={moveUpComponent}>Move up</li>
<li on:click={moveDownComponent}>Move down</li>
<li on:click={copyComponent}>Duplicate</li>
<li on:click={() => storeComponentForCopy(true)}>Cut</li>
<li on:click={() => storeComponentForCopy(false)}>Copy</li>
<hr />
<li class:disabled={noPaste} on:click={() => pasteComponent('above')}>
Paste above
</li>
<li class:disabled={noPaste} on:click={() => pasteComponent('below')}>
Paste below
</li>
<li
class:disabled={noPaste || noChildrenAllowed}
on:click={() => pasteComponent('inside')}>
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(--button-text);
outline: none;
}
.menu {
z-index: 100000;
overflow: visible;
padding: 10px 0;
}
.menu li {
border-style: none;
background-color: transparent;
list-style-type: none;
padding: 4px 5px 4px 15px;
margin: 0;
width: 100%;
box-sizing: border-box;
}
.menu li:not(.disabled) {
cursor: pointer;
color: var(--ink);
}
.menu li:not(.disabled):hover {
color: var(--button-text);
background-color: var(--grey-light);
}
.disabled {
color: var(--grey-dark);
cursor: default;
}
</style>

View File

@ -107,11 +107,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: hidden; overflow-x: hidden;
padding: 20px;
} }
.title > div:nth-child(1) { .title > div:nth-child(1) {
grid-column-start: name; grid-column-start: name;
color: var(--secondary100); color: var(--ink);
} }
.title > div:nth-child(2) { .title > div:nth-child(2) {

View File

@ -1,4 +1,5 @@
<script> <script>
import { goto } from "@sveltech/routify"
import { splitName } from "./pagesParsing/splitRootComponentName.js" import { splitName } from "./pagesParsing/splitRootComponentName.js"
import components from "./temporaryPanelStructure.js" import components from "./temporaryPanelStructure.js"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -32,7 +33,14 @@
const onComponentChosen = component => { const onComponentChosen = component => {
store.addChildComponent(component._component) store.addChildComponent(component._component)
toggleTab()
toggleTab("Navigate")
// Get ID path
const path = store.getPathToComponent($store.currentComponentInfo)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
} }
</script> </script>
@ -52,32 +60,9 @@
</div> </div>
<style> <style>
.tabs {
display: flex;
justify-content: center;
list-style: none;
margin: 0 auto;
padding: 0 30px;
border-bottom: 1px solid #d8d8d8;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.14px;
}
li {
color: #808192;
margin: 0 5px;
padding: 0 8px;
cursor: pointer;
}
.panel { .panel {
padding: 20px; padding: 20px 0px;
} display: flex;
flex-wrap: wrap;
.active {
border-bottom: solid 3px #0055ff;
color: #393c44;
} }
</style> </style>

View File

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

View File

@ -3,6 +3,7 @@
import { store } from "builderStore" import { store } from "builderStore"
import { last } from "lodash/fp" import { last } from "lodash/fp"
import { pipe } from "components/common/core" import { pipe } from "components/common/core"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import { import {
XCircleIcon, XCircleIcon,
ChevronUpIcon, ChevronUpIcon,
@ -14,23 +15,12 @@
export let currentComponent export let currentComponent
export let onSelect = () => {} export let onSelect = () => {}
export let level = 0 export let level = 0
export let onDeleteComponent
export let onMoveUpComponent
export let onMoveDownComponent
export let onCopyComponent
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/"))) const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise]) const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const moveDownComponent = component => {
const c = component
return () => {
return onMoveDownComponent(c)
}
}
const selectComponent = component => { const selectComponent = component => {
// Set current component // Set current component
store.selectComponent(component) store.selectComponent(component)
@ -51,30 +41,9 @@
class:selected={currentComponent === component} class:selected={currentComponent === component}
style="padding-left: {level * 20 + 53}px"> style="padding-left: {level * 20 + 53}px">
<div>{get_capitalised_name(component._component)}</div> <div>{get_capitalised_name(component._component)}</div>
<div class="reorder-buttons"> <div class="actions">
{#if index > 0} <ComponentDropdownMenu {component} />
<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> </div>
<button
class="copy"
on:click|stopPropagation={() => onCopyComponent(component)}>
<CopyIcon />
</button>
<button on:click|stopPropagation={() => onDeleteComponent(component)}>
<XCircleIcon />
</button>
</div> </div>
{#if component._children} {#if component._children}
@ -82,11 +51,7 @@
components={component._children} components={component._children}
{currentComponent} {currentComponent}
{onSelect} {onSelect}
level={level + 1} level={level + 1} />
{onDeleteComponent}
{onMoveUpComponent}
{onMoveDownComponent}
{onCopyComponent} />
{/if} {/if}
</li> </li>
{/each} {/each}
@ -111,7 +76,7 @@
font-size: 13px; font-size: 13px;
} }
.item button { .actions {
display: none; display: none;
height: 20px; height: 20px;
width: 28px; width: 28px;
@ -120,37 +85,14 @@
border-style: none; border-style: none;
background: rgba(0, 0, 0, 0); background: rgba(0, 0, 0, 0);
cursor: pointer; cursor: pointer;
} position: relative;
.item button.copy {
width: 26px;
} }
.item:hover { .item:hover {
background: #fafafa; background: #fafafa;
cursor: pointer; cursor: pointer;
} }
.item:hover button { .item:hover .actions {
display: block; display: block;
} }
.item:hover button:hover {
color: var(--button-text);
}
.reorder-buttons {
display: flex;
flex-direction: column;
height: 100%;
}
.reorder-buttons > button {
flex: 1 1 auto;
width: 30px;
height: 15px;
}
.reorder-buttons > button.solo {
padding-top: 2px;
}
</style> </style>

View File

@ -55,7 +55,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 20px 20px; padding: 20px 20px;
border-left: solid 1px #e8e8ef; border-left: solid 1px var(--grey);
} }
.switcher { .switcher {

View File

@ -19,20 +19,20 @@
<style> <style>
.flatbutton { .flatbutton {
cursor: pointer; cursor: pointer;
padding: 8px 4px; padding: 8px 2px;
text-align: center; text-align: center;
background: #ffffff; background: #ffffff;
color: var(--ink-light); color: var(--ink-light);
border-radius: 5px; border-radius: 5px;
font-family: Roboto; font-family: Roboto;
font-size: 13px; font-size: 14px;
font-weight: 500; font-weight: 400;
transition: background 0.5s, color 0.5s ease; transition: all 0.3s;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
.selected { .selected {
background: #808192; background: var(--ink-light);
color: #ffffff; color: #ffffff;
} }
</style> </style>

View File

@ -1,6 +1,5 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import FlatButton from "./FlatButton.svelte" import FlatButton from "./FlatButton.svelte"
export let buttonProps = [] export let buttonProps = []
export let isMultiSelect = false export let isMultiSelect = false

View File

@ -0,0 +1,48 @@
<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}>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: 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);
}
</style>

View File

@ -1,66 +1,56 @@
<script> <script>
import { fly } from "svelte/transition"
export let item export let item
</script> </script>
<div class="item-item" on:click> <div class="item-item" in:fly={{ y: 100, duration: 1000 }} on:click>
<div class="item-icon"> <div class="item-icon">
<i class={item.icon} /> <i class={item.icon} />
</div> </div>
<div class="item-text"> <div class="item-text">
<div class="item-name">{item.name}</div> <div class="item-name">{item.name}</div>
<div class="item-description">
<p>{item.description}</p>
</div>
</div> </div>
</div> </div>
<style> <style>
.item-item { .item-item {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
padding: 10px 0px 8px 10px;
align-items: center;
cursor: pointer; cursor: pointer;
margin-bottom: 8px;
padding: 8px 0px 16px 0px;
width: 120px;
height: 80px;
justify-content: center;
align-items: center;
margin-right: 8px;
background-color: var(--grey-light);
border-radius: 3px;
} }
.item-item:hover { .item-item:hover {
background: #fbfbfb; background: var(--grey);
border-radius: 5px; border-radius: 3px;
transition: all 0.2s;
} }
.item-icon { .item-icon {
flex: 0 0 40px; border-radius: 3px;
background: #f1f4fc;
height: 40px;
border-radius: 5px;
display: flex; display: flex;
justify-content: center;
align-items: center;
} }
.item-text { .item-text {
display: flex; display: flex;
padding-left: 16px;
padding-top: 8px;
flex-direction: column; flex-direction: column;
} }
.item-name { .item-name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 400;
}
.item-description {
font-size: 12px;
color: #808192;
}
p {
line-height: 15px;
} }
i { i {
font-size: 24px; font-size: 24px;
color: #808192; color: var(--ink-light);
} }
</style> </style>

View File

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

View File

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

View File

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

View File

@ -27,11 +27,6 @@
settingsView.show() settingsView.show()
} }
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "") const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script> </script>
@ -42,7 +37,6 @@
<div class="pages-list-container"> <div class="pages-list-container">
<div class="nav-header"> <div class="nav-header">
<span class="navigator-title">Navigator</span> <span class="navigator-title">Navigator</span>
<div class="border-line" />
<span class="components-nav-page">Pages</span> <span class="components-nav-page">Pages</span>
</div> </div>
@ -52,12 +46,8 @@
</div> </div>
</div> </div>
<div class="border-line" />
<PageLayout layout={$store.pages[$store.currentPageName]} /> <PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="border-line" />
<div class="components-list-container"> <div class="components-list-container">
<div class="nav-group-header"> <div class="nav-group-header">
<span class="components-nav-header" style="margin-top: 0;"> <span class="components-nav-header" style="margin-top: 0;">
@ -91,13 +81,6 @@
<NewScreen bind:this={newScreenPicker} /> <NewScreen bind:this={newScreenPicker} />
<SettingsView bind:this={settingsView} /> <SettingsView bind:this={settingsView} />
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<style> <style>
button { button {
cursor: pointer; cursor: pointer;
@ -114,20 +97,10 @@
.root { .root {
display: grid; display: grid;
grid-template-columns: 275px 1fr 300px; grid-template-columns: 300px 1fr 300px;
height: 100%; height: 100%;
width: 100%; width: 100%;
background: #fafafa; background: #fbfbfb;
}
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr 300px;
height: 100%;
width: 100%;
background: #fafafa;
}
} }
.ui-nav { .ui-nav {
@ -135,7 +108,6 @@
background-color: var(--white); background-color: var(--white);
height: calc(100vh - 49px); height: calc(100vh - 49px);
padding: 0; padding: 0;
overflow: scroll;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -230,10 +202,6 @@
letter-spacing: 1px; letter-spacing: 1px;
} }
.border-line {
border-bottom: 1px solid #d8d8d8;
}
.components-list-container { .components-list-container {
padding: 20px 0px 0 0; padding: 20px 0px 0 0;
} }

View File

@ -7,42 +7,84 @@ import InputGroup from "../common/Inputs/InputGroup.svelte"
*/ */
export const layout = [ export const layout = [
{
label: "Display",
key: "display",
control: OptionSelect,
initialValue: "Select Option",
options: [
{ label: "Select Option", value: "" },
{ label: "Flex", value: "flex" },
{ label: "Inline Flex", value: "inline-flex" },
],
},
{ {
label: "Direction", label: "Direction",
key: "flex-direction", key: "flex-direction",
control: OptionSelect, control: OptionSelect,
initialValue: "columnReverse", initialValue: "Row",
options: [ options: [
{ label: "row" }, { label: "Row", value: "row" },
{ label: "row-reverse", value: "rowReverse" }, { label: "Row Reverse", value: "rowReverse" },
{ label: "column" }, { label: "column", value: "column" },
{ label: "column-reverse", value: "columnReverse" }, { label: "Column Reverse", value: "columnReverse" },
],
},
{
label: "Justify",
key: "justify-content",
control: OptionSelect,
initialValue: "Flex Start",
options: [
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
{ label: "Space Between", value: "space-between" },
{ label: "Space Around", value: "space-around" },
{ label: "Space Evenly", value: "space-evenly" },
],
},
{
label: "Align",
key: "align-items",
control: OptionSelect,
initialValue: "Flex Start",
options: [
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
{ label: "Baseline", value: "baseline" },
{ label: "Stretch", value: "stretch" },
], ],
}, },
{ label: "Justify", key: "justify-content", control: Input },
{ label: "Align", key: "align-items", control: Input },
{ {
label: "Wrap", label: "Wrap",
key: "flex-wrap", key: "flex-wrap",
control: OptionSelect, control: OptionSelect,
options: [{ label: "wrap" }, { label: "no wrap", value: "noWrap" }], initialValue: "NoWrap",
options: [
{ label: "No Wrap", value: "nowrap" },
{ label: "Wrap", value: "wrap" },
{ label: "Wrap Reverse", value: "wrap-reverse" },
],
}, },
] ]
const spacingMeta = [ const spacingMeta = [
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" }, { placeholder: "L" },
{ placeholder: "B" },
{ placeholder: "R" },
{ placeholder: "T" },
] ]
export const spacing = [ export const spacing = [
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
{ {
label: "Padding", label: "Padding",
key: "padding", key: "padding",
control: InputGroup, control: InputGroup,
meta: spacingMeta, meta: spacingMeta,
}, },
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
] ]
export const size = [ export const size = [
@ -59,14 +101,40 @@ export const position = [
label: "Position", label: "Position",
key: "position", key: "position",
control: OptionSelect, control: OptionSelect,
initialValue: "Wrap",
options: [ options: [
{ label: "static" }, { label: "Static", value: "static" },
{ label: "relative" }, { label: "Relative", value: "relative" },
{ label: "fixed" }, { label: "Fixed", value: "fixed" },
{ label: "absolute" }, { label: "Absolute", value: "absolute" },
{ label: "sticky" }, { label: "Sticky", value: "sticky" },
], ],
}, },
{
label: "Top",
key: "top",
control: Input,
},
{
label: "Right",
key: "right",
control: Input,
},
{
label: "Bottom",
key: "bottom",
control: Input,
},
{
label: "Left",
key: "left",
control: Input,
},
{
label: "Z-index",
key: "z-index",
control: Input,
},
] ]
export const typography = [ export const typography = [
@ -77,13 +145,21 @@ export const typography = [
defaultValue: "initial", defaultValue: "initial",
options: [ options: [
"initial", "initial",
"Times New Roman",
"Georgia",
"Arial", "Arial",
"Arial Black", "Arial Black",
"Cursive",
"Courier",
"Comic Sans MS", "Comic Sans MS",
"Helvetica",
"Impact", "Impact",
"Inter",
"Lucida Sans Unicode", "Lucida Sans Unicode",
"Open Sans",
"Playfair",
"Roboto",
"Roboto Mono",
"Times New Roman",
"Verdana",
], ],
styleBindingProperty: "font-family", styleBindingProperty: "font-family",
}, },
@ -92,10 +168,15 @@ export const typography = [
key: "font-weight", key: "font-weight",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "normal" }, { label: "100", value: "100" },
{ label: "bold" }, { label: "200", value: "200" },
{ label: "bolder" }, { label: "300", value: "300" },
{ label: "lighter" }, { label: "400", value: "400" },
{ label: "500", value: "500" },
{ label: "600", value: "600" },
{ label: "700", value: "700" },
{ label: "800", value: "800" },
{ label: "900", value: "900" },
], ],
}, },
{ label: "size", key: "font-size", defaultValue: "", control: Input }, { label: "size", key: "font-size", defaultValue: "", control: Input },
@ -103,8 +184,7 @@ export const typography = [
{ {
label: "Color", label: "Color",
key: "color", key: "color",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"],
}, },
{ {
label: "align", label: "align",
@ -112,6 +192,20 @@ export const typography = [
control: OptionSelect, control: OptionSelect,
options: ["initial", "left", "right", "center", "justify"], options: ["initial", "left", "right", "center", "justify"],
}, //custom }, //custom
{
label: "Decoration",
key: "text-decoration-line",
control: OptionSelect,
defaultValue: "None",
options: [
{ label: "None", value: "none" },
{ label: "Underline", value: "underline" },
{ label: "Overline", value: "overline" },
{ label: "Line-through", value: "line-through" },
{ label: "Under Over", value: "underline overline" },
],
},
{ label: "transform", key: "text-transform", control: Input }, //custom { label: "transform", key: "text-transform", control: Input }, //custom
{ label: "style", key: "font-style", control: Input }, //custom { label: "style", key: "font-style", control: Input }, //custom
] ]
@ -120,8 +214,7 @@ export const background = [
{ {
label: "Background", label: "Background",
key: "background", key: "background",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"],
}, },
{ label: "Image", key: "image", control: Input }, //custom { label: "Image", key: "image", control: Input }, //custom
] ]
@ -132,15 +225,45 @@ export const border = [
{ {
label: "Color", label: "Color",
key: "border-color", key: "border-color",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"], },
{
label: "Style",
key: "border-style",
control: OptionSelect,
options: [
"none",
"hidden",
"dotted",
"dashed",
"solid",
"double",
"groove",
"ridge",
"inset",
"outset",
],
}, },
{ label: "Style", key: "border-style", control: Input },
] ]
export const effects = [ export const effects = [
{ label: "Opacity", key: "opacity", control: Input }, { label: "Opacity", key: "opacity", control: Input },
{ label: "Rotate", key: "transform", control: Input }, //needs special control {
label: "Rotate",
key: "transform",
control: OptionSelect,
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)" },
],
}, //needs special control
{ label: "Shadow", key: "box-shadow", control: Input }, { label: "Shadow", key: "box-shadow", control: Input },
] ]

View File

@ -10,15 +10,6 @@ export default {
name: "Basic", name: "Basic",
isCategory: true, isCategory: true,
children: [ children: [
{
_component: "##builtin/screenslot",
name: "Screenslot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
icon: "ri-crop-2-line",
commonProps: {},
children: [],
},
{ {
_component: "@budibase/standard-components/container", _component: "@budibase/standard-components/container",
name: "Container", name: "Container",
@ -119,7 +110,7 @@ export default {
{ {
name: "Input", name: "Input",
description: "These components handle user input.", description: "These components handle user input.",
icon: "ri-edit-box-line", icon: "ri-edit-box-fill",
commonProps: {}, commonProps: {},
children: [ children: [
{ {
@ -127,7 +118,7 @@ export default {
name: "Textfield", name: "Textfield",
description: description:
"A textfield component that allows the user to input text.", "A textfield component that allows the user to input text.",
icon: "ri-edit-box-line", icon: "ri-edit-box-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [ settings: [
@ -145,7 +136,7 @@ export default {
_component: "@budibase/standard-components/checkbox", _component: "@budibase/standard-components/checkbox",
name: "Checkbox", name: "Checkbox",
description: "A selectable checkbox component", description: "A selectable checkbox component",
icon: "ri-checkbox-line", icon: "ri-checkbox-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [{ label: "Label", key: "label", control: Input }], settings: [{ label: "Label", key: "label", control: Input }],
@ -166,7 +157,7 @@ export default {
name: "Select", name: "Select",
description: description:
"A select component for choosing from different options", "A select component for choosing from different options",
icon: "ri-file-list-line", icon: "ri-file-list-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [], settings: [],
@ -236,7 +227,7 @@ export default {
name: "Card", name: "Card",
description: description:
"A basic card component that can contain content and actions.", "A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-line", icon: "ri-layout-bottom-fill",
children: [], children: [],
properties: { design: { ...all } }, properties: { design: { ...all } },
}, },
@ -248,21 +239,6 @@ export default {
children: [], children: [],
properties: { design: { ...all } }, properties: { design: { ...all } },
}, },
{
name: "Navigation Bar",
_component: "@budibase/standard-components/Navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
children: [],
properties: { design: { ...all } },
},
],
},
{
name: "Data",
isCategory: true,
children: [
{ {
name: "Table", name: "Table",
description: "A component that generates a table from your data.", description: "A component that generates a table from your data.",
@ -283,27 +259,11 @@ export default {
}, },
children: [], children: [],
}, },
{
_component: "@budibase/standard-components/datatable",
name: "DataTable",
description: "A table for displaying data from the backend.",
icon: "ri-archive-drawer-fill",
properties: { design: { ...all } },
children: [],
},
{
_component: "@budibase/standard-components/dataform",
name: "DataForm",
description: "Form stuff",
icon: "ri-file-edit-fill",
properties: { design: { ...all } },
children: [],
},
{ {
name: "Chart", name: "Chart",
_component: "@budibase/standard-components/datachart", _component: "@budibase/standard-components/datachart",
description: "Shiny chart", description: "Shiny chart",
icon: "ri-bar-chart-line", icon: "ri-bar-chart-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
@ -311,7 +271,7 @@ export default {
name: "List", name: "List",
_component: "@budibase/standard-components/datalist", _component: "@budibase/standard-components/datalist",
description: "Shiny list", description: "Shiny list",
icon: "ri-file-list-line", icon: "ri-file-list-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
@ -319,11 +279,36 @@ export default {
name: "Map", name: "Map",
_component: "@budibase/standard-components/datamap", _component: "@budibase/standard-components/datamap",
description: "Shiny map", description: "Shiny map",
icon: "ri-map-pin-line", icon: "ri-map-pin-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
], ],
}, },
{
name: "Layouts",
isCategory: true,
children: [
{
_component: "##builtin/screenslot",
name: "Screenslot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
icon: "ri-crop-2-fill",
properties: { design: { ...all } },
commonProps: {},
children: [],
},
{
name: "Nav Bar",
_component: "@budibase/standard-components/Navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
children: [],
properties: { design: { ...all } },
},
],
},
], ],
} }

View File

@ -13,6 +13,7 @@
--grey: #F2F2F2; --grey: #F2F2F2;
--grey-light: #FBFBFB; --grey-light: #FBFBFB;
--grey-medium: #e8e8ef;
--grey-dark: #E6E6E6; --grey-dark: #E6E6E6;
--primary100: #0055ff; --primary100: #0055ff;
@ -136,6 +137,10 @@ h5 {
color: var(--darkslate); color: var(--darkslate);
} }
textarea {
font-family: var(--fontnormal);
}
.hoverable:hover { .hoverable:hover {
cursor: pointer; cursor: pointer;
} }

View File

@ -1,4 +1,5 @@
<script> <script>
import Modal from "svelte-simple-modal"
import { store } from "builderStore" import { store } from "builderStore"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
@ -25,55 +26,58 @@
} }
</script> </script>
<div class="root"> <Modal>
<div class="root">
<div class="top-nav"> <div class="top-nav">
<div class="topleftnav"> <div class="topleftnav">
<button class="home-logo"> <button class="home-logo">
<img <img
src="/_builder/assets/bb-logo.svg" src="/_builder/assets/bb-logo.svg"
alt="budibase icon" alt="budibase icon"
on:click={() => $goto(`/`)} /> on:click={() => $goto(`/`)} />
</button> </button>
<!-- This gets all indexable subroutes and sticks them in the top nav. --> <!-- This gets all indexable subroutes and sticks them in the top nav. -->
{#each $layout.children as { path, title }} {#each $layout.children as { path, title }}
<span <span
class:active={$isActive(path)} class:active={$isActive(path)}
class="topnavitem" class="topnavitem"
on:click={() => $goto(path)}> on:click={() => $goto(path)}>
{title} {title}
</span> </span>
{/each} {/each}
<!-- <IconButton icon="home" <!-- <IconButton icon="home"
color="var(--slate)" color="var(--slate)"
hoverColor="var(--secondary75)"/> --> hoverColor="var(--secondary75)"/> -->
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<span <span
class:active={$isActive(`/settings`)} class:active={$isActive(`/settings`)}
class="topnavitemright" class="topnavitemright"
on:click={() => $goto(`/settings`)}> on:click={() => $goto(`/settings`)}>
<SettingsIcon /> <SettingsIcon />
</span> </span>
<span class:active={false} class="topnavitemright"> <span
<a href={`/${application}`} target="_blank"> class:active={false}
class="topnavitemright"
on:click={() => (location = `/${application}`)}>
<PreviewIcon /> <PreviewIcon />
</a> </span>
</span> </div>
</div> </div>
{#await promise}
<!-- This should probably be some kind of loading state? -->
<div />
{:then}
<slot />
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
</div> </div>
</Modal>
{#await promise}
<!-- This should probably be some kind of loading state? -->
<div />
{:then}
<slot />
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
</div>
<style> <style>
.root { .root {
@ -151,7 +155,7 @@
} }
.topnavitemright:hover { .topnavitemright:hover {
color: rgb(255, 255, 255, 0.8); color: var(--ink);
font-weight: 500; font-weight: 500;
} }

View File

@ -34,16 +34,7 @@
.nav { .nav {
overflow: auto; overflow: auto;
flex: 0 1 auto; flex: 0 1 auto;
width: 275px; width: 300px;
height: 100%; height: 100%;
} }
@media only screen and (min-width: 1800px) {
.nav {
overflow: auto;
flex: 0 1 auto;
width: 300px;
height: 100%;
}
}
</style> </style>

View File

@ -2,18 +2,17 @@
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { onMount } from "svelte" import { onMount } from "svelte"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import ComponentsHierarchyChildren from "components/userInterface/ComponentsHierarchyChildren.svelte" import ComponentsHierarchyChildren from "components/userInterface/ComponentsHierarchyChildren.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import IconButton from "components/common/IconButton.svelte" import IconButton from "components/common/IconButton.svelte"
import NewScreen from "components/userInterface/NewScreen.svelte"
import CurrentItemPreview from "components/userInterface/AppPreview" import CurrentItemPreview from "components/userInterface/AppPreview"
import PageView from "components/userInterface/PageView.svelte" import PageView from "components/userInterface/PageView.svelte"
import ComponentsPaneSwitcher from "components/userInterface/ComponentsPaneSwitcher.svelte" import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
import Switcher from "components/common/Switcher.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp" import { last } from "lodash/fp"
import { AddIcon } from "components/common/Icons" import { AddIcon } from "components/common/Icons"
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
$: instances = $store.appInstances $: instances = $store.appInstances
@ -27,23 +26,15 @@
} }
}) })
let newScreenPicker
let confirmDeleteDialog let confirmDeleteDialog
let componentToDelete = "" let componentToDelete = ""
const newScreen = () => {
newScreenPicker.show()
}
let settingsView let settingsView
const settings = () => { const settings = () => {
settingsView.show() settingsView.show()
} }
const confirmDeleteComponent = component => { let leftNavSwitcher
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "") const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script> </script>
@ -52,102 +43,49 @@
<div class="ui-nav"> <div class="ui-nav">
<div class="pages-list-container"> <Switcher bind:this={leftNavSwitcher} tabs={['Navigate', 'Add']}>
<div class="nav-header"> <div slot="0">
<span class="navigator-title">Navigate</span> <FrontendNavigatePane />
<span class="components-nav-page">Pages</span>
</div> </div>
<div slot="1">
<div class="nav-items-container"> <ComponentSelectionList toggleTab={leftNavSwitcher.selectTab} />
<PagesList />
</div> </div>
</div> </Switcher>
<div class="border-line" />
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="border-line" />
<div class="components-list-container">
<div class="nav-group-header">
<span class="components-nav-header" style="margin-top: 0;">
Screens
</span>
<div>
<button on:click={newScreen}>
<AddIcon />
</button>
</div>
</div>
<div class="nav-items-container">
<ComponentsHierarchy screens={$store.screens} />
</div>
</div>
</div> </div>
<div class="preview-pane"> <div class="preview-pane">
<CurrentItemPreview /> {#if $store.currentPageName && $store.currentPageName.length > 0}
<CurrentItemPreview />
{/if}
</div> </div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'} {#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
<div class="components-pane"> <div class="components-pane">
<ComponentsPaneSwitcher /> <ComponentPropertiesPanel />
</div> </div>
{/if} {/if}
</div> </div>
<NewScreen bind:this={newScreenPicker} />
<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)} />
<slot /> <slot />
<style> <style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
width: 20px;
padding-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
.root { .root {
display: grid; display: grid;
grid-template-columns: 275px 1fr 275px; grid-template-columns: 300px 1fr 300px;
width: 100%; width: 100%;
background: var(--grey-light); background: var(--grey-light);
} }
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr 300px;
width: 100%;
background: var(--grey-light);
}
}
.ui-nav { .ui-nav {
grid-column: 1; grid-column: 1;
background-color: var(--white); background-color: var(--white);
height: calc(100vh - 49px); height: calc(100vh - 49px);
padding: 0; padding: 0;
overflow: scroll;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 5;
} }
.preview-pane { .preview-pane {
@ -162,44 +100,6 @@
background-color: var(--white); background-color: var(--white);
} }
.components-nav-page {
font-size: 13px;
color: var(--ink);
padding-left: 20px;
margin-top: 20px;
font-weight: 600;
opacity: 0.4;
letter-spacing: 1px;
}
.components-nav-header {
font-size: 13px;
color: var(--ink);
margin-top: 20px;
font-weight: 600;
opacity: 0.4;
letter-spacing: 1px;
}
.nav-header {
display: flex;
flex-direction: column;
margin-top: 20px;
}
.nav-items-container {
padding: 1rem 0rem 0rem 0rem;
}
.nav-group-header {
display: flex;
padding: 0px 20px 0px 20px;
font-size: 0.9rem;
font-weight: bold;
justify-content: space-between;
align-items: center;
}
.nav-group-header > div:nth-child(1) { .nav-group-header > div:nth-child(1) {
padding: 0rem 0.5rem 0rem 0rem; padding: 0rem 0.5rem 0rem 0rem;
vertical-align: bottom; vertical-align: bottom;
@ -207,13 +107,6 @@
margin-right: 5px; margin-right: 5px;
} }
.nav-group-header > span:nth-child(3) {
margin-left: 5px;
vertical-align: bottom;
grid-column-start: title;
margin-top: auto;
}
.nav-group-header > div:nth-child(3) { .nav-group-header > div:nth-child(3) {
vertical-align: bottom; vertical-align: bottom;
grid-column-start: button; grid-column-start: button;
@ -224,19 +117,4 @@
.nav-group-header > div:nth-child(3):hover { .nav-group-header > div:nth-child(3):hover {
color: var(--primary75); color: var(--primary75);
} }
.navigator-title {
font-size: 18px;
color: var(--ink);
font-weight: bold;
padding: 0 20px 20px 20px;
}
.border-line {
border-bottom: 1px solid #d8d8d8;
}
.components-list-container {
padding: 20px 0px 0 0;
}
</style> </style>

View File

@ -0,0 +1,207 @@
<script>
import Modal from "svelte-simple-modal"
import {
SettingsIcon,
AppsIcon,
UpdatesIcon,
HostingIcon,
DocumentationIcon,
TutorialsIcon,
CommunityIcon,
ContributionIcon,
BugIcon,
EmailIcon,
TwitterIcon,
} from "components/common/Icons/"
</script>
<Modal>
<div class="root">
<div class="ui-nav">
<div class="home-logo">
<img src="/_builder/assets/bb-logo.svg" alt="Budibase icon" />
</div>
<div class="nav-section">
<div class="nav-section-title">Build</div>
<div class="nav-item-home">
<span class="nav-item-icon">
<AppsIcon />
</span>
<div class="nav-item-title">Apps</div>
</div>
<div class="nav-item">
<span class="nav-item-icon">
<SettingsIcon />
</span>
<div class="nav-item-title">Settings</div>
</div>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<UpdatesIcon />
</span>
<div class="nav-item-title">Updates</div>
</a>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<HostingIcon />
</span>
<div class="nav-item-title">Hosting</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Learn</div>
<a href="https://docs.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<DocumentationIcon />
</span>
<div class="nav-item-title">Documentation</div>
</a>
<a
href="https://docs.budibase.com/tutorial/quick-start"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<TutorialsIcon />
</span>
<div class="nav-item-title">Tutorials</div>
</a>
<a href="https://forum.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<CommunityIcon />
</span>
<div class="nav-item-title">Community</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Contact</div>
<a
href="https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<ContributionIcon />
</span>
<div class="nav-item-title">Contribute to our product</div>
</a>
<a
href="https://github.com/Budibase/budibase/issues"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<BugIcon />
</span>
<div class="nav-item-title">Report bug</div>
</a>
<a href="mailto:support@budibase.com" target="_blank" class="nav-item">
<span class="nav-item-icon">
<EmailIcon />
</span>
<div class="nav-item-title">Email</div>
</a>
<a href="https://twitter.com/budibase" target="_blank" class="nav-item">
<span class="nav-item-icon">
<TwitterIcon />
</span>
<div class="nav-item-title">Twitter</div>
</a>
</div>
</div>
<div class="main">
<slot />
</div>
</div>
</Modal>
<style>
.root {
display: grid;
grid-template-columns: 300px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
.main {
grid-column: 2;
}
.ui-nav {
grid-column: 1;
background-color: var(--white);
padding: 20px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--grey-medium);
}
.home-logo {
cursor: pointer;
height: 40px;
margin-bottom: 20px;
}
.home-logo img {
height: 40px;
}
.nav-section {
margin: 20px 0px;
display: flex;
flex-direction: column;
}
.nav-section-title {
font-size: 20px;
color: var(--ink);
font-weight: 700;
margin-bottom: 12px;
}
.nav-item {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
}
.nav-item-home {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
background-color: var(--blue-light);
}
.nav-item:hover {
background-color: var(--grey-light);
border-radius: 3px;
}
.nav-item::selection {
background-color: var(--blue-light);
border-radius: 3px;
}
.nav-item-title {
font-size: 14px;
color: var(--ink);
font-weight: 500;
margin-left: 12px;
}
.nav-item-icon {
color: var(--ink-light);
}
</style>

View File

@ -1,23 +1,13 @@
<script> <script>
import { getContext } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
import AppList from "components/start/AppList.svelte" import AppList from "components/start/AppList.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import IconButton from "components/common/IconButton.svelte" import IconButton from "components/common/IconButton.svelte"
import {
SettingsIcon,
AppsIcon,
UpdatesIcon,
HostingIcon,
DocumentationIcon,
TutorialsIcon,
CommunityIcon,
ContributionIcon,
BugIcon,
EmailIcon,
TwitterIcon,
} from "components/common/Icons/"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
let promise = getApps() let promise = getApps()
@ -31,229 +21,54 @@
throw new Error(json) throw new Error(json)
} }
} }
// Handle create app modal
const { open } = getContext("simple-modal")
const showCreateAppModal = () => {
open(
CreateAppModal,
{
message: "What is your name?",
hasForm: true,
},
{
closeButton: false,
closeOnEsc: false,
closeOnOuterClick: false,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
}
</script> </script>
<div class="root"> <div class="welcome">Welcome to Budibase</div>
<div class="ui-nav"> <div class="banner">
<div class="home-logo"> <div class="banner-content">
<img src="/_builder/assets/bb-logo.svg" alt="Budibase icon" /> <div class="banner-header">
</div> Every accomplishment starts with a decision to try.
<div class="nav-section">
<div class="nav-section-title">Build</div>
<div class="nav-item-home">
<span class="nav-item-icon">
<AppsIcon />
</span>
<div class="nav-item-title">Apps</div>
</div>
<div class="nav-item">
<span class="nav-item-icon">
<SettingsIcon />
</span>
<div class="nav-item-title">Settings</div>
</div>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<UpdatesIcon />
</span>
<div class="nav-item-title">Updates</div>
</a>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<HostingIcon />
</span>
<div class="nav-item-title">Hosting</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Learn</div>
<a href="https://docs.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<DocumentationIcon />
</span>
<div class="nav-item-title">Documentation</div>
</a>
<a
href="https://docs.budibase.com/tutorial/quick-start"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<TutorialsIcon />
</span>
<div class="nav-item-title">Tutorials</div>
</a>
<a href="https://forum.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<CommunityIcon />
</span>
<div class="nav-item-title">Community</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Contact</div>
<a
href="https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<ContributionIcon />
</span>
<div class="nav-item-title">Contribute to our product</div>
</a>
<a
href="https://github.com/Budibase/budibase/issues"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<BugIcon />
</span>
<div class="nav-item-title">Report bug</div>
</a>
<a href="mailto:support@budibase.com" target="_blank" class="nav-item">
<span class="nav-item-icon">
<EmailIcon />
</span>
<div class="nav-item-title">Email</div>
</a>
<a href="https://twitter.com/budibase" target="_blank" class="nav-item">
<span class="nav-item-icon">
<TwitterIcon />
</span>
<div class="nav-item-title">Twitter</div>
</a>
</div> </div>
<button class="banner-button" type="button" on:click={showCreateAppModal}>
<i class="ri-add-circle-fill" />
Create New Web App
</button>
</div> </div>
<div class="banner-image">
<div class="main"> <img src="/_builder/assets/banner-image.png" alt="Bannerimage" />
<div class="welcome">Welcome to Budibase</div>
<div class="banner">
<div class="banner-content">
<div class="banner-header">
Every accomplishment starts with a decision to try.
</div>
<button class="banner-button" type="button">
<i class="ri-add-circle-fill" />
Create New Web App
</button>
</div>
<div class="banner-image">
<img src="/_builder/assets/banner-image.png" alt="Bannerimage" />
</div>
</div>
{#await promise}
<div class="spinner-container">
<Spinner />
</div>
{:then result}
<AppList apps={result} />
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
</div> </div>
</div> </div>
{#await promise}
<div class="spinner-container">
<Spinner />
</div>
{:then result}
<AppList apps={result} />
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
<style> <style>
.root {
display: grid;
grid-template-columns: 275px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
}
.main {
grid-column: 2;
}
.ui-nav {
grid-column: 1;
background-color: var(--white);
padding: 20px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--grey-dark);
}
.home-logo {
cursor: pointer;
height: 40px;
margin-bottom: 20px;
}
.home-logo img {
height: 40px;
}
.nav-section {
margin: 20px 0px;
display: flex;
flex-direction: column;
}
.nav-section-title {
font-size: 20px;
color: var(--ink);
font-weight: 700;
margin-bottom: 12px;
}
.nav-item {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
}
.nav-item-home {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
background-color: var(--blue-light);
}
.nav-item:hover {
background-color: var(--grey-light);
border-radius: 3px;
}
.nav-item::selection {
background-color: var(--blue-light);
border-radius: 3px;
}
.nav-item-title {
font-size: 14px;
color: var(--ink);
font-weight: 500;
margin-left: 12px;
}
.nav-item-icon {
color: var(--ink-light);
}
.welcome { .welcome {
margin: 60px 80px 0px 80px; margin: 60px 80px 0px 80px;
font-size: 42px; font-size: 42px;

View File

@ -12,4 +12,7 @@ ADMIN_SECRET={{adminSecret}}
JWT_SECRET={{cookieKey1}} JWT_SECRET={{cookieKey1}}
# port to run http server on # port to run http server on
PORT=4001 PORT=4001
# error level for koa-pino
LOG_LEVEL=error

View File

@ -45,7 +45,6 @@
"@budibase/core": "^0.0.32", "@budibase/core": "^0.0.32",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sendgrid/mail": "^7.1.1", "@sendgrid/mail": "^7.1.1",
"ajv": "^6.12.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"electron-is-dev": "^1.2.0", "electron-is-dev": "^1.2.0",
@ -68,6 +67,7 @@
"squirrelly": "^7.5.0", "squirrelly": "^7.5.0",
"tar-fs": "^2.0.0", "tar-fs": "^2.0.0",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"validate.js": "^0.13.1",
"yargs": "^13.2.4", "yargs": "^13.2.4",
"zlib": "^1.0.5" "zlib": "^1.0.5"
}, },

View File

@ -5,3 +5,4 @@ process.env.JWT_SECRET = "test-jwtsecret"
process.env.CLIENT_ID = "test-client-id" process.env.CLIENT_ID = "test-client-id"
process.env.BUDIBASE_DIR = tmpdir("budibase-unittests") process.env.BUDIBASE_DIR = tmpdir("budibase-unittests")
process.env.ADMIN_SECRET = "test-admin-secret" process.env.ADMIN_SECRET = "test-admin-secret"
process.env.LOG_LEVEL = "silent"

View File

@ -3,6 +3,10 @@ const ClientDb = require("../../db/clientDb")
const { getPackageForBuilder } = require("../../utilities/builder") const { getPackageForBuilder } = require("../../utilities/builder")
const newid = require("../../db/newid") const newid = require("../../db/newid")
const env = require("../../environment") const env = require("../../environment")
const instanceController = require("./instance")
const { resolve, join } = require("path")
const { copy, readJSON, writeJSON, exists } = require("fs-extra")
const { exec } = require("child_process")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const db = new CouchDB(ClientDb.name(env.CLIENT_ID)) const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
@ -32,12 +36,77 @@ exports.create = async function(ctx) {
"@budibase/standard-components", "@budibase/standard-components",
"@budibase/materialdesign-components", "@budibase/materialdesign-components",
], ],
...ctx.request.body, name: ctx.request.body.name,
description: ctx.request.body.description,
} }
const { rev } = await db.post(newApplication) const { rev } = await db.post(newApplication)
newApplication._rev = rev newApplication._rev = rev
const createInstCtx = {
params: {
clientId: env.CLIENT_ID,
applicationId: newApplication._id,
},
request: {
body: { name: `dev-${env.CLIENT_ID}` },
},
}
await instanceController.create(createInstCtx)
if (ctx.isDev) {
const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
await runNpmInstall(newAppFolder)
}
ctx.body = newApplication ctx.body = newApplication
ctx.message = `Application ${ctx.request.body.name} created successfully` ctx.message = `Application ${ctx.request.body.name} created successfully`
} }
const createEmptyAppPackage = async (ctx, app) => {
const templateFolder = resolve(
__dirname,
"..",
"..",
"utilities",
"appDirectoryTemplate"
)
const appsFolder = env.BUDIBASE_DIR
const newAppFolder = resolve(appsFolder, app._id)
if (await exists(newAppFolder)) {
ctx.throw(400, "App folder already exists for this application")
return
}
await copy(templateFolder, newAppFolder)
const packageJsonPath = join(appsFolder, app._id, "package.json")
const packageJson = await readJSON(packageJsonPath)
packageJson.name = npmFriendlyAppName(app.name)
await writeJSON(packageJsonPath, packageJson)
return newAppFolder
}
const runNpmInstall = async newAppFolder => {
return new Promise((resolve, reject) => {
const cmd = `cd ${newAppFolder} && npm install`
exec(cmd, (error, stdout, stderr) => {
if (error) {
reject(error)
}
resolve(stdout ? stdout : stderr)
})
})
}
const npmFriendlyAppName = name =>
name
.replace(/_/g, "")
.replace(/./g, "")
.replace(/ /g, "")
.toLowerCase()

View File

@ -1,9 +1,7 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const Ajv = require("ajv") const validateJs = require("validate.js")
const newid = require("../../db/newid") const newid = require("../../db/newid")
const ajv = new Ajv()
exports.save = async function(ctx) { exports.save = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
const record = ctx.request.body const record = ctx.request.body
@ -13,18 +11,18 @@ exports.save = async function(ctx) {
record._id = newid() record._id = newid()
} }
// validation with ajv
const model = await db.get(record.modelId) const model = await db.get(record.modelId)
const validate = ajv.compile({
properties: model.schema,
})
const valid = validate(record)
if (!valid) { const validateResult = await validate({
record,
model,
})
if (!validateResult.valid) {
ctx.status = 400 ctx.status = 400
ctx.body = { ctx.body = {
status: 400, status: 400,
errors: validate.errors, errors: validateResult.errors,
} }
return return
} }
@ -90,3 +88,29 @@ exports.destroy = async function(ctx) {
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId) ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
ctx.eventEmitter.emit(`record:delete`, record) ctx.eventEmitter.emit(`record:delete`, record)
} }
exports.validate = async function(ctx) {
const errors = await validate({
instanceId: ctx.params.instanceId,
modelId: ctx.params.modelId,
record: ctx.request.body,
})
ctx.status = 200
ctx.body = errors
}
async function validate({ instanceId, modelId, record, model }) {
if (!model) {
const db = new CouchDB(instanceId)
model = await db.get(modelId)
}
const errors = {}
for (let fieldName in model.schema) {
const res = validateJs.single(
record[fieldName],
model.schema[fieldName].constraints
)
if (res) errors[fieldName] = res
}
return { valid: Object.keys(errors).length === 0, errors }
}

View File

@ -61,7 +61,7 @@ exports.create = async function(ctx) {
} }
} }
exports.update = async function(ctx) {} exports.update = async function() {}
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId) const database = new CouchDB(ctx.params.instanceId)

View File

@ -1,8 +1,5 @@
const CouchDB = require("../../../db") const CouchDB = require("../../db")
const Ajv = require("ajv") const newid = require("../../db/newid")
const newid = require("../../../db/newid")
const ajv = new Ajv()
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
@ -19,8 +16,7 @@ exports.create = async function(ctx) {
message: "Workflow created successfully", message: "Workflow created successfully",
workflow: { workflow: {
...workflow, ...workflow,
_rev: response.rev, ...response,
_id: response.id,
}, },
} }
} }

View File

@ -28,6 +28,11 @@ router
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.save recordController.save
) )
.post(
"/api/:instanceId/:modelId/records/validate",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.validate
)
.delete( .delete(
"/api/:instanceId/:modelId/records/:recordId/:revId", "/api/:instanceId/:modelId/records/:recordId/:revId",
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),

View File

@ -30,7 +30,12 @@ exports.createModel = async (request, instanceId, model) => {
type: "model", type: "model",
key: "name", key: "name",
schema: { schema: {
name: { type: "string" }, name: {
type: "text",
constraints: {
type: "string",
},
},
}, },
} }
@ -41,19 +46,6 @@ exports.createModel = async (request, instanceId, model) => {
return res.body return res.body
} }
exports.createRecord = async ({ request, instanceId, modelId, record }) => {
record = record || {
modelId,
name: "test name",
}
const res = await request
.post(`/api/${instanceId}/${modelId}/records`)
.send(record)
.set(exports.defaultHeaders)
return res.body
}
exports.createView = async (request, instanceId, view) => { exports.createView = async (request, instanceId, view) => {
view = view || { view = view || {
map: "function(doc) { emit(doc[doc.key], doc._id); } ", map: "function(doc) { emit(doc[doc.key], doc._id); } ",

View File

@ -197,4 +197,31 @@ describe("/records", () => {
}) })
}) })
describe("validate", () => {
it("should return no errors on valid record", async () => {
const result = await request
.post(`/api/${instance._id}/${model._id}/records/validate`)
.send({ name: "ivan" })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(result.body.valid).toBe(true)
expect(Object.keys(result.body.errors)).toEqual([])
})
it("should errors on invalid record", async () => {
const result = await request
.post(`/api/${instance._id}/${model._id}/records/validate`)
.send({ name: 1 })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(result.body.valid).toBe(false)
expect(Object.keys(result.body.errors)).toEqual(["name"])
})
})
}) })

View File

@ -8,7 +8,7 @@ const router = Router()
router router
.get( .get(
"/api/:instanceId/views/:viewName", "/api/:instanceId/view/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName), authorized(READ_VIEW, ctx => ctx.params.viewName),
recordController.fetchView recordController.fetchView
) )

View File

@ -16,7 +16,7 @@ app.use(
prettyPrint: { prettyPrint: {
levelFirst: true, levelFirst: true,
}, },
level: process.env.NODE_ENV === "jest" ? "silent" : "info", level: env.LOG_LEVEL || "error",
}) })
) )

View File

@ -1,40 +0,0 @@
const WORKFLOW_SCHEMA = {
properties: {
type: "workflow",
pageId: {
type: "string",
},
screenId: {
type: "string",
},
live: {
type: "boolean",
},
uiTree: {
type: "object",
},
definition: {
type: "object",
properties: {
triggers: { type: "array" },
steps: { type: "array" },
// next: {
// type: "object",
// properties: {
// environment: { environment: "string" },
// type: { type: "string" },
// actionId: { type: "string" },
// args: { type: "object" },
// conditions: { type: "array" },
// errorHandling: { type: "object" },
// next: { type: "object" },
// },
// },
},
},
},
}
module.exports = {
WORKFLOW_SCHEMA,
}

View File

@ -0,0 +1 @@
dist/

View File

@ -0,0 +1,11 @@
{
"name": "name",
"version": "1.0.0",
"description": "",
"author": "",
"license": "ISC",
"dependencies": {
"@budibase/standard-components": "0.x",
"@budibase/materialdesign-components": "0.x"
}
}

View File

@ -0,0 +1,19 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
"props" : {
"_component": "@budibase/standard-components/container",
"_children": [],
"_id": 0,
"type": "div",
"_styles": {
"layout": {},
"position": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

View File

@ -0,0 +1,19 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
"props" : {
"_component": "@budibase/standard-components/container",
"_children": [],
"_id": 1,
"type": "div",
"_styles": {
"layout": {},
"position": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

View File

@ -0,0 +1 @@
module.exports = () => ({})

View File

@ -693,7 +693,7 @@ ajv-keywords@^3.4.1:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.2: ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0:
version "6.12.2" version "6.12.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
@ -6659,6 +6659,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0" spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0" spdx-expression-parse "^3.0.0"
validate.js@^0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/validate.js/-/validate.js-0.13.1.tgz#b58bfac04a0f600a340f62e5227e70d95971e92a"
integrity sha512-PnFM3xiZ+kYmLyTiMgTYmU7ZHkjBZz2/+F0DaALc/uUtVzdCt1wAosvYJ5hFQi/hz8O4zb52FQhHZRC+uVkJ+g==
vary@^1.1.2: vary@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"