merge
This commit is contained in:
commit
fe0b95d627
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -30,6 +30,4 @@
|
|||
<!-- svelte-notifications -->
|
||||
<NotificationDisplay />
|
||||
|
||||
<Modal>
|
||||
<Router {routes} />
|
||||
</Modal>
|
||||
<Router {routes} />
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -45,7 +45,6 @@
|
|||
<DatabasesList />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{#if $backendUiStore.selectedDatabase._id}
|
||||
<div class="hierarchy">
|
||||
<div class="components-list-container">
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
function selectModel(model) {
|
||||
backendUiStore.update(state => {
|
||||
state.selectedModel = model
|
||||
state.selectedView = `all_${model._id}`
|
||||
state.selectedView = `${model._id}`
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 20px;
|
||||
border-left: solid 1px #e8e8ef;
|
||||
border-left: solid 1px var(--grey);
|
||||
}
|
||||
|
||||
.switcher {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
|
||||
import FlatButton from "./FlatButton.svelte"
|
||||
export let buttonProps = []
|
||||
export let isMultiSelect = false
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
import Item from "./Item.svelte"
|
||||
import { store } from "builderStore"
|
||||
export let list
|
||||
|
||||
let category = list
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
]
|
||||
|
||||
|
|
|
@ -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 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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); } ",
|
||||
|
|
|
@ -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"])
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -16,7 +16,7 @@ app.use(
|
|||
prettyPrint: {
|
||||
levelFirst: true,
|
||||
},
|
||||
level: process.env.NODE_ENV === "jest" ? "silent" : "info",
|
||||
level: env.LOG_LEVEL || "error",
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
dist/
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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": ""
|
||||
}
|
|
@ -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": ""
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
module.exports = () => ({})
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue