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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
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)
return response
}
export async function saveRecord(record, instanceId) {
const SAVE_RECORDS_URL = `/api/${instanceId}/${record.modelId}/records`
export async function saveRecord(record, instanceId, modelId) {
const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records`
const response = await api.post(SAVE_RECORDS_URL, record)
return await response.json()

View File

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

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@
function selectModel(model) {
backendUiStore.update(state => {
state.selectedModel = model
state.selectedView = `all_${model._id}`
state.selectedView = `${model._id}`
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>
import Button from "components/common/Button.svelte"
import AppCard from "./AppCard.svelte"
export let apps
function myFunction() {
@ -13,27 +13,23 @@
<div>
<div>
<div class="app-section-title">Your Web Apps</div>
{#each apps as app}
<div class="apps-card">
<h3 class="app-title">{app.name}</h3>
<p class="app-desc">
A minimalist CRM which removes the noise and allows you to focus
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 class="apps">
{#each apps as app}
<AppCard {...app} />
{/each}
</div>
</div>
</div>
</div>
</div>
<style>
.apps {
display: grid;
grid-template-columns: repeat(auto-fill, 400px);
grid-gap: 40px 85px;
justify-content: start;
}
.root {
margin: 40px 80px;
}
@ -44,59 +40,4 @@
font-weight: 700;
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>

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;
flex-direction: column;
overflow-x: hidden;
padding: 20px;
}
.title > div:nth-child(1) {
grid-column-start: name;
color: var(--secondary100);
color: var(--ink);
}
.title > div:nth-child(2) {

View File

@ -1,4 +1,5 @@
<script>
import { goto } from "@sveltech/routify"
import { splitName } from "./pagesParsing/splitRootComponentName.js"
import components from "./temporaryPanelStructure.js"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -32,7 +33,14 @@
const onComponentChosen = component => {
store.addChildComponent(component._component)
toggleTab()
toggleTab("Navigate")
// Get ID path
const path = store.getPathToComponent($store.currentComponentInfo)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
}
</script>
@ -52,32 +60,9 @@
</div>
<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 {
padding: 20px;
}
.active {
border-bottom: solid 3px #0055ff;
color: #393c44;
padding: 20px 0px;
display: flex;
flex-wrap: wrap;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,42 +7,84 @@ import InputGroup from "../common/Inputs/InputGroup.svelte"
*/
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",
key: "flex-direction",
control: OptionSelect,
initialValue: "columnReverse",
initialValue: "Row",
options: [
{ label: "row" },
{ label: "row-reverse", value: "rowReverse" },
{ label: "column" },
{ label: "column-reverse", value: "columnReverse" },
{ label: "Row", value: "row" },
{ label: "Row Reverse", value: "rowReverse" },
{ label: "column", value: "column" },
{ 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",
key: "flex-wrap",
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 = [
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" },
{ placeholder: "B" },
{ placeholder: "R" },
{ placeholder: "T" },
]
export const spacing = [
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
{
label: "Padding",
key: "padding",
control: InputGroup,
meta: spacingMeta,
},
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
]
export const size = [
@ -59,14 +101,40 @@ export const position = [
label: "Position",
key: "position",
control: OptionSelect,
initialValue: "Wrap",
options: [
{ label: "static" },
{ label: "relative" },
{ label: "fixed" },
{ label: "absolute" },
{ label: "sticky" },
{ label: "Static", value: "static" },
{ label: "Relative", value: "relative" },
{ label: "Fixed", value: "fixed" },
{ label: "Absolute", value: "absolute" },
{ 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 = [
@ -77,13 +145,21 @@ export const typography = [
defaultValue: "initial",
options: [
"initial",
"Times New Roman",
"Georgia",
"Arial",
"Arial Black",
"Cursive",
"Courier",
"Comic Sans MS",
"Helvetica",
"Impact",
"Inter",
"Lucida Sans Unicode",
"Open Sans",
"Playfair",
"Roboto",
"Roboto Mono",
"Times New Roman",
"Verdana",
],
styleBindingProperty: "font-family",
},
@ -92,10 +168,15 @@ export const typography = [
key: "font-weight",
control: OptionSelect,
options: [
{ label: "normal" },
{ label: "bold" },
{ label: "bolder" },
{ label: "lighter" },
{ label: "100", value: "100" },
{ label: "200", value: "200" },
{ label: "300", value: "300" },
{ label: "400", value: "400" },
{ label: "500", value: "500" },
{ label: "600", value: "600" },
{ label: "700", value: "700" },
{ label: "800", value: "800" },
{ label: "900", value: "900" },
],
},
{ label: "size", key: "font-size", defaultValue: "", control: Input },
@ -103,8 +184,7 @@ export const typography = [
{
label: "Color",
key: "color",
control: OptionSelect,
options: ["black", "white", "red", "blue", "green"],
control: Input,
},
{
label: "align",
@ -112,6 +192,20 @@ export const typography = [
control: OptionSelect,
options: ["initial", "left", "right", "center", "justify"],
}, //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: "style", key: "font-style", control: Input }, //custom
]
@ -120,8 +214,7 @@ export const background = [
{
label: "Background",
key: "background",
control: OptionSelect,
options: ["black", "white", "red", "blue", "green"],
control: Input,
},
{ label: "Image", key: "image", control: Input }, //custom
]
@ -132,15 +225,45 @@ export const border = [
{
label: "Color",
key: "border-color",
control: OptionSelect,
options: ["black", "white", "red", "blue", "green"],
control: Input,
},
{
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 = [
{ 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 },
]

View File

@ -10,15 +10,6 @@ export default {
name: "Basic",
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-line",
commonProps: {},
children: [],
},
{
_component: "@budibase/standard-components/container",
name: "Container",
@ -119,7 +110,7 @@ export default {
{
name: "Input",
description: "These components handle user input.",
icon: "ri-edit-box-line",
icon: "ri-edit-box-fill",
commonProps: {},
children: [
{
@ -127,7 +118,7 @@ export default {
name: "Textfield",
description:
"A textfield component that allows the user to input text.",
icon: "ri-edit-box-line",
icon: "ri-edit-box-fill",
properties: {
design: { ...all },
settings: [
@ -145,7 +136,7 @@ export default {
_component: "@budibase/standard-components/checkbox",
name: "Checkbox",
description: "A selectable checkbox component",
icon: "ri-checkbox-line",
icon: "ri-checkbox-fill",
properties: {
design: { ...all },
settings: [{ label: "Label", key: "label", control: Input }],
@ -166,7 +157,7 @@ export default {
name: "Select",
description:
"A select component for choosing from different options",
icon: "ri-file-list-line",
icon: "ri-file-list-fill",
properties: {
design: { ...all },
settings: [],
@ -236,7 +227,7 @@ export default {
name: "Card",
description:
"A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-line",
icon: "ri-layout-bottom-fill",
children: [],
properties: { design: { ...all } },
},
@ -248,21 +239,6 @@ export default {
children: [],
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",
description: "A component that generates a table from your data.",
@ -283,27 +259,11 @@ export default {
},
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",
_component: "@budibase/standard-components/datachart",
description: "Shiny chart",
icon: "ri-bar-chart-line",
icon: "ri-bar-chart-fill",
properties: { design: { ...all } },
children: [],
},
@ -311,7 +271,7 @@ export default {
name: "List",
_component: "@budibase/standard-components/datalist",
description: "Shiny list",
icon: "ri-file-list-line",
icon: "ri-file-list-fill",
properties: { design: { ...all } },
children: [],
},
@ -319,11 +279,36 @@ export default {
name: "Map",
_component: "@budibase/standard-components/datamap",
description: "Shiny map",
icon: "ri-map-pin-line",
icon: "ri-map-pin-fill",
properties: { design: { ...all } },
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-light: #FBFBFB;
--grey-medium: #e8e8ef;
--grey-dark: #E6E6E6;
--primary100: #0055ff;
@ -136,6 +137,10 @@ h5 {
color: var(--darkslate);
}
textarea {
font-family: var(--fontnormal);
}
.hoverable:hover {
cursor: pointer;
}

View File

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

View File

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

View File

@ -2,18 +2,17 @@
import { store, backendUiStore } from "builderStore"
import { goto } from "@sveltech/routify"
import { onMount } from "svelte"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.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 NewScreen from "components/userInterface/NewScreen.svelte"
import CurrentItemPreview from "components/userInterface/AppPreview"
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 { last } from "lodash/fp"
import { AddIcon } from "components/common/Icons"
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
$: instances = $store.appInstances
@ -27,23 +26,15 @@
}
})
let newScreenPicker
let confirmDeleteDialog
let componentToDelete = ""
const newScreen = () => {
newScreenPicker.show()
}
let settingsView
const settings = () => {
settingsView.show()
}
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
let leftNavSwitcher
const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script>
@ -52,102 +43,49 @@
<div class="ui-nav">
<div class="pages-list-container">
<div class="nav-header">
<span class="navigator-title">Navigate</span>
<span class="components-nav-page">Pages</span>
<Switcher bind:this={leftNavSwitcher} tabs={['Navigate', 'Add']}>
<div slot="0">
<FrontendNavigatePane />
</div>
<div class="nav-items-container">
<PagesList />
<div slot="1">
<ComponentSelectionList toggleTab={leftNavSwitcher.selectTab} />
</div>
</div>
<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>
</Switcher>
</div>
<div class="preview-pane">
<CurrentItemPreview />
{#if $store.currentPageName && $store.currentPageName.length > 0}
<CurrentItemPreview />
{/if}
</div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
<div class="components-pane">
<ComponentsPaneSwitcher />
<ComponentPropertiesPanel />
</div>
{/if}
</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 />
<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 {
display: grid;
grid-template-columns: 275px 1fr 275px;
grid-template-columns: 300px 1fr 300px;
width: 100%;
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 {
grid-column: 1;
background-color: var(--white);
height: calc(100vh - 49px);
padding: 0;
overflow: scroll;
display: flex;
flex-direction: column;
z-index: 5;
}
.preview-pane {
@ -162,44 +100,6 @@
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) {
padding: 0rem 0.5rem 0rem 0rem;
vertical-align: bottom;
@ -207,13 +107,6 @@
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) {
vertical-align: bottom;
grid-column-start: button;
@ -224,19 +117,4 @@
.nav-group-header > div:nth-child(3):hover {
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>

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>
import { getContext } from "svelte"
import { store } from "builderStore"
import AppList from "components/start/AppList.svelte"
import { onMount } from "svelte"
import ActionButton from "components/common/ActionButton.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 CreateAppModal from "components/start/CreateAppModal.svelte"
let promise = getApps()
@ -31,229 +21,54 @@
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>
<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 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" on:click={showCreateAppModal}>
<i class="ri-add-circle-fill" />
Create New Web App
</button>
</div>
<div class="main">
<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 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}
<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 {
margin: 60px 80px 0px 80px;
font-size: 42px;

View File

@ -12,4 +12,7 @@ ADMIN_SECRET={{adminSecret}}
JWT_SECRET={{cookieKey1}}
# 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",
"@koa/router": "^8.0.0",
"@sendgrid/mail": "^7.1.1",
"ajv": "^6.12.2",
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"electron-is-dev": "^1.2.0",
@ -68,6 +67,7 @@
"squirrelly": "^7.5.0",
"tar-fs": "^2.0.0",
"uuid": "^3.3.2",
"validate.js": "^0.13.1",
"yargs": "^13.2.4",
"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.BUDIBASE_DIR = tmpdir("budibase-unittests")
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 newid = require("../../db/newid")
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) {
const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
@ -32,12 +36,77 @@ exports.create = async function(ctx) {
"@budibase/standard-components",
"@budibase/materialdesign-components",
],
...ctx.request.body,
name: ctx.request.body.name,
description: ctx.request.body.description,
}
const { rev } = await db.post(newApplication)
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.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 Ajv = require("ajv")
const validateJs = require("validate.js")
const newid = require("../../db/newid")
const ajv = new Ajv()
exports.save = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const record = ctx.request.body
@ -13,18 +11,18 @@ exports.save = async function(ctx) {
record._id = newid()
}
// validation with ajv
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.body = {
status: 400,
errors: validate.errors,
errors: validateResult.errors,
}
return
}
@ -90,3 +88,29 @@ exports.destroy = async function(ctx) {
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
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) {
const database = new CouchDB(ctx.params.instanceId)

View File

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

View File

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

View File

@ -30,7 +30,12 @@ exports.createModel = async (request, instanceId, model) => {
type: "model",
key: "name",
schema: {
name: { type: "string" },
name: {
type: "text",
constraints: {
type: "string",
},
},
},
}
@ -41,19 +46,6 @@ exports.createModel = async (request, instanceId, model) => {
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) => {
view = view || {
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
.get(
"/api/:instanceId/views/:viewName",
"/api/:instanceId/view/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName),
recordController.fetchView
)

View File

@ -16,7 +16,7 @@ app.use(
prettyPrint: {
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"
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"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
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-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:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"