Merge branch 'master' of github.com:Budibase/budibase into feature/settings-modal

This commit is contained in:
kevmodrome 2020-06-26 09:02:16 +02:00
commit bacc525596
179 changed files with 2829 additions and 2309 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
context('Screen Tests', () => {
before(() => {
cy.visit('localhost:4001/_builder')
cy.createApp('Conor Cy App', 'Model App Description')
cy.navigateToFrontend()
})
it('Should successful create a screen', () => {
cy.createScreen("test Screen")
})
it('Should rename a screen', () => {
cy.get(".components-pane").within(() => {
cy.contains("Settings").click()
cy.get("input[name=_instanceName]").clear().type("About Us").blur()
})
cy.get('.nav-items-container').within(() => {
cy.contains("About Us").should('exist')
})
})
})

View File

@ -103,3 +103,23 @@ Cypress.Commands.add("addButtonComponent", () => {
cy.get("[data-cy=Button]").click()
})
Cypress.Commands.add("navigateToFrontend", () => {
cy.get(".close", { timeout: 10000 }).click()
cy.contains("frontend").click()
cy.get(".close", { timeout: 10000 }).click()
})
Cypress.Commands.add("createScreen", (screenName, route) => {
cy.get(".newscreen").click()
cy.get(".uk-input:first").type(screenName)
if (route) {
cy.get(".uk-input:last").type(route)
}
cy.get(".uk-modal-footer").within(() => {
cy.contains("Create Screen").click()
})
cy.get(".nav-items-container").within(() => {
cy.contains(screenName).should("exist")
})
})

View File

@ -50,8 +50,7 @@
]
},
"dependencies": {
"@beyonk/svelte-notifications": "^2.0.3",
"@budibase/bbui": "^1.12.0",
"@budibase/bbui": "^1.13.0",
"@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0",

View File

@ -153,6 +153,10 @@ export default {
find: "builderStore",
replacement: path.resolve(projectRootDir, "src/builderStore"),
},
{
find: "constants",
replacement: path.resolve(projectRootDir, "src/constants"),
},
],
customResolver,
}),

View File

@ -4,30 +4,10 @@
import { Router, basepath } from "@sveltech/routify"
import { routes } from "../routify/routes"
import { store, initialise } from "builderStore"
import AppNotification, {
showAppNotification,
} from "components/common/AppNotification.svelte"
import { NotificationDisplay } from "@beyonk/svelte-notifications"
function showErrorBanner() {
showAppNotification({
status: "danger",
message:
"Whoops! Looks like we're having trouble. Please refresh the page.",
})
}
onMount(async () => {
window.addEventListener("error", showErrorBanner)
window.addEventListener("unhandledrejection", showErrorBanner)
})
import NotificationDisplay from "components/common/Notification/NotificationDisplay.svelte"
$basepath = "/_builder"
</script>
<AppNotification />
<!-- svelte-notifications -->
<NotificationDisplay />
<Router {routes} />

View File

@ -57,13 +57,14 @@
.budibase__nav-item {
cursor: pointer;
padding: 0 4px 0 2px;
height: 35px;
margin: 5px 0px 4px 0px;
border-radius: 0 5px 5px 0;
height: 36px;
margin: 0px 0px 0px 0px;
border-radius: 5px;
display: flex;
align-items: center;
font-size: 14px;
transition: 0.2s;
border-top: var(--grey-1) .5px solid;
}
.budibase__nav-item.selected {
@ -72,18 +73,22 @@
}
.budibase__nav-item:hover {
background: var(--grey-light);
background: var(--grey-1);
}
.budibase__input {
height: 35px;
width: 220px;
border-radius: 3px;
border: 1px solid var(--grey-dark);
height: 36px;
background-color: var(--grey-2);
border: none;
border-radius: 5px;
width: 100%;
text-align: left;
color: var(--ink);
font-size: 14px;
padding-left: 12px;
padding-left: 8px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.uk-text-right {
@ -100,7 +105,7 @@
}
.budibase__table {
border: 1px solid var(--grey-dark);
border: 1px solid var(--grey-4);
background: #fff;
border-radius: 2px;
}
@ -116,12 +121,12 @@
}
.budibase__table tr {
border-bottom: 1px solid var(--grey-light);
border-bottom: 1px solid var(--grey-1);
}
.button--toggled {
background: var(--blue-light);
color: var(--ink-light);
color: var(--grey-7);
width: 40px;
height: 40px;
display: flex;

View File

@ -1,22 +1,20 @@
import { writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import { uuid } from "builderStore/uuid"
import api from "../api"
import { getContext } from "svelte"
/** TODO: DEMO SOLUTION
* this section should not be here, it is a quick fix for a demo
* when we reorg the backend UI, this should disappear
* **/
import { CreateEditModelModal } from "components/database/ModelDataTable/modals"
/** DEMO SOLUTION END **/
export const getBackendUiStore = () => {
const INITIAL_BACKEND_UI_STATE = {
breadcrumbs: [],
models: [],
views: [],
users: [],
selectedDatabase: {},
selectedModel: {},
draftModel: {},
tabs: {
SETUP_PANEL: "SETUP",
NAVIGATION_PANEL: "NAVIGATE",
},
}
const store = writable(INITIAL_BACKEND_UI_STATE)
@ -31,26 +29,12 @@ export const getBackendUiStore = () => {
store.update(state => {
state.selectedDatabase = db
if (models && models.length > 0) {
state.selectedModel = models[0]
state.selectedView = `all_${models[0]._id}`
store.actions.models.select(models[0])
}
state.breadcrumbs = [db.name]
state.models = models
state.views = views
return state
})
/** TODO: DEMO SOLUTION**/
if (!models || models.length === 0) {
const { open, close } = getContext("simple-modal")
open(
CreateEditModelModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
/** DEMO SOLUTION END **/
},
},
records: {
@ -59,11 +43,6 @@ export const getBackendUiStore = () => {
state.selectedView = state.selectedView
return state
}),
view: record =>
store.update(state => {
state.breadcrumbs = [state.selectedDatabase.name, record._id]
return state
}),
select: record =>
store.update(state => {
state.selectedRecord = record
@ -71,14 +50,48 @@ export const getBackendUiStore = () => {
}),
},
models: {
create: model =>
fetch: async () => {
const modelsResponse = await api.get(`/api/models`)
const models = await modelsResponse.json()
store.update(state => {
state.models = models
return state
})
},
select: model =>
store.update(state => {
state.models.push(model)
state.models = state.models
state.selectedModel = model
state.draftModel = cloneDeep(model)
state.selectedField = ""
state.selectedView = `all_${model._id}`
state.tabs.SETUP_PANEL = "SETUP"
return state
}),
save: async ({ model }) => {
const updatedModel = cloneDeep(model)
const SAVE_MODEL_URL = `/api/models`
await api.post(SAVE_MODEL_URL, updatedModel)
await store.actions.models.fetch()
},
addField: field => {
store.update(state => {
if (!state.draftModel.schema) {
state.draftModel.schema = {}
}
const id = uuid()
state.draftModel.schema = {
...state.draftModel.schema,
[id]: field,
}
state.selectedField = id
state.tabs.NAVIGATION_PANEL = "NAVIGATE"
return state
})
},
},
views: {
select: view =>

View File

@ -1,4 +1,6 @@
import { values } from "lodash/fp"
import { get_capitalised_name } from "../../helpers"
import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend"
import { writable, get } from "svelte/store"
import api from "../api"
@ -22,7 +24,6 @@ import {
saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
regenerateCssForCurrentScreen,
renameCurrentScreen,
} from "../storeUtils"
export const getStore = () => {
@ -68,6 +69,7 @@ export const getStore = () => {
store.getPathToComponent = getPathToComponent(store)
store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store)
store.editPageOrScreen = editPageOrScreen(store)
return store
}
@ -152,14 +154,13 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
const rootComponent = state.components[layoutComponentName]
const newScreen = {
name: screenName || "",
description: "",
url: "",
_css: "",
props: createProps(rootComponent).props,
}
newScreen.route = route
newScreen.props._instanceName = screenName || ""
state.currentPreviewItem = newScreen
state.currentComponentInfo = newScreen.props
state.currentFrontEndType = "screen"
@ -172,7 +173,7 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
const setCurrentScreen = store => screenName => {
store.update(s => {
const screen = getExactComponent(s.screens, screenName)
const screen = getExactComponent(s.screens, screenName, true)
s.currentPreviewItem = screen
s.currentFrontEndType = "screen"
s.currentView = "detail"
@ -243,6 +244,7 @@ const setCurrentPage = store => pageName => {
const currentPage = state.pages[pageName]
state.currentFrontEndType = "page"
state.currentView = "detail"
state.currentPageName = pageName
state.screens = Array.isArray(current_screens)
? current_screens
@ -294,10 +296,15 @@ const addChildComponent = store => (componentToAdd, presetName) => {
const presetProps = presetName ? component.presets[presetName] : {}
const instanceId = get(backendUiStore).selectedDatabase._id
const instanceName = get_capitalised_name(componentToAdd)
const newComponent = createProps(
component,
{
...presetProps,
_instanceId: instanceId,
_instanceName: instanceName,
},
state
)
@ -346,24 +353,23 @@ const selectComponent = store => component => {
const setComponentProp = store => (name, value) => {
store.update(state => {
const current_component = state.currentComponentInfo
state.currentComponentInfo[name] = value
_saveCurrentPreviewItem(state)
let current_component = state.currentComponentInfo
current_component[name] = value
state.currentComponentInfo = current_component
_saveCurrentPreviewItem(state)
return state
})
}
const setPageOrScreenProp = store => (name, value) => {
store.update(state => {
if (name === "name" && state.currentFrontEndType === "screen") {
state = renameCurrentScreen(value, state)
if (name === "_instanceName" && state.currentFrontEndType === "screen") {
state.currentPreviewItem.props[name] = value
} else {
state.currentPreviewItem[name] = value
_saveCurrentPreviewItem(state)
}
_saveCurrentPreviewItem(state)
return state
})
}
@ -413,6 +419,18 @@ const setScreenType = store => type => {
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
state.currentView = "detail"
return state
})
}
const editPageOrScreen = store => (key, value, setOnComponent = false) => {
store.update(state => {
setOnComponent
? (state.currentPreviewItem.props[key] = value)
: (state.currentPreviewItem[key] = value)
_saveCurrentPreviewItem(state)
return state
})
}

View File

@ -0,0 +1,23 @@
import { writable } from "svelte/store"
import { generate } from "shortid"
export const notificationStore = writable({
notifications: [],
})
export function send(message, type = "default") {
notificationStore.update(state => {
state.notifications = [
...state.notifications,
{ id: generate(), type, message },
]
return state
})
}
export const notifier = {
danger: msg => send(msg, "danger"),
warning: msg => send(msg, "warning"),
info: msg => send(msg, "info"),
success: msg => send(msg, "success"),
}

View File

@ -46,8 +46,9 @@ export const saveScreenApi = (screen, s) => {
}
export const renameCurrentScreen = (newname, state) => {
const oldname = state.currentPreviewItem.name
state.currentPreviewItem.name = newname
const oldname = state.currentPreviewItem.props._instanceName
state.currentPreviewItem.props._instanceName = newname
api.patch(
`/_builder/api/${state.appId}/pages/${state.currentPageName}/screen`,
{

View File

@ -41,7 +41,7 @@
.secondary {
color: var(--ink);
border: solid 1px var(--grey-dark);
border: solid 1px var(--grey-4);
background: white;
}

View File

@ -1,77 +0,0 @@
<script context="module">
import UIKit from "uikit"
export function showAppNotification({ message, status }) {
UIKit.notification({
message: `
<div class="message-container">
<div class="information-icon">🤯</div>
<span class="notification-message">
${message}
</span>
<button class="hoverable refresh-page-button" onclick="window.location.reload()">Refresh Page</button>
</div>
`,
status,
timeout: 100000,
})
}
</script>
<style>
:global(.information-icon) {
font-size: 24px;
margin-right: 8px;
}
:global(.uk-nofi) {
display: grid;
grid-template-columns: 40px 1fr auto;
grid-gap: 5px;
align-items: center;
}
:global(.message-container) {
display: flex;
align-items: center;
justify-content: center;
}
:global(.uk-notification) {
width: 50% !important;
left: 0 !important;
right: 0 !important;
margin-right: auto !important;
margin-left: auto !important;
border-radius: 10px;
}
:global(.uk-notification-message) {
border-radius: 5px;
}
:global(.uk-notification-message:hover .uk-notification-close) {
visibility: hidden;
}
:global(.uk-notification-message-danger) {
background: var(--ink-light) !important;
color: #fff !important;
font-family: Roboto;
font-size: 16px !important;
}
:global(.refresh-page-button) {
font-size: 14px;
border-radius: 3px;
border: none;
padding: 8px 16px;
color: var(--ink);
background: #ffffff;
margin-left: 20px;
}
:global(.refresh-page-button):hover {
background: var(--grey-light);
}
</style>

View File

@ -0,0 +1,56 @@
<script>
export let title
export let icon
export let primary
export let secondary
export let tertiary
</script>
<div on:click class:primary class:secondary class:tertiary>
<i class={icon} />
<span>{title}</span>
</div>
<style>
div {
height: 80px;
border-radius: 5px;
color: var(--ink);
font-weight: 400;
padding: 15px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
transition: 0.3s transform;
background: var(--grey-1);
}
i {
font-size: 30px;
}
span {
font-size: 14px;
text-align: center;
}
div:hover {
cursor: pointer;
background: var(--grey-2);
}
.primary {
background: var(--ink);
color: var(--white);
}
.secondary {
background: var(--blue-light);
}
.tertiary {
background: var(--white);
}
</style>

View File

@ -42,14 +42,14 @@
/* ---- PRIMARY ----*/
.primary {
background-color: var(--primary100);
border-color: var(--primary100);
background-color: var(--blue);
border-color: var(--blue);
color: var(--white);
}
.primary:hover {
background-color: var(--primary75);
border-color: var(--primary75);
background-color: var(--blue);
border-color: var(--blue);
}
.primary:active {
@ -59,8 +59,8 @@
.primary-outline {
background-color: var(--white);
border-color: var(--primary100);
color: var(--primary100);
border-color: var(--blue);
color: var(--blue);
}
.primary-outline:hover {
@ -74,8 +74,8 @@
/* ---- secondary ----*/
.secondary {
background-color: var(--secondary100);
border-color: var(--secondary100);
background-color: var(--ink);
border-color: var(--ink);
color: var(--white);
}
@ -91,8 +91,8 @@
.secondary-outline {
background-color: var(--white);
border-color: var(--secondary100);
color: var(--secondary100);
border-color: var(--ink);
color: var(--ink);
}
.secondary-outline:hover {
@ -136,32 +136,36 @@
/* ---- deletion ----*/
.deletion {
background-color: var(--deletion100);
border-color: var(--deletion100);
background-color: var(--red);
border-color: var(--red);
color: var(--white);
}
.deletion:hover {
background-color: var(--deletion75);
border-color: var(--deletion75);
background-color: var(--red-light);
border-color: var(--red);
color: var(--red);
}
.deletion:pressed {
background-color: var(--deletiondark);
border-color: var(--deletiondark);
background-color: var(--red-dark);
border-color: var(--red-dark);
color: var(--white);
}
.deletion-outline {
background-color: var(--white);
border-color: var(--deletion100);
color: var(--deletion100);
border-color: var(--red);
color: var(--red);
}
.deletion-outline:hover {
background-color: var(--deletion10);
background-color: var(--red-light);
color: var(--red);
}
.deletion-outline:pressed {
background-color: var(--deletion25);
background-color: var(--red-dark);
color: var(--white);
}
</style>

View File

@ -3,8 +3,8 @@
export let label = ""
</script>
<input class="uk-checkbox" type="checkbox" bind:checked on:change />
{label}
<input class="uk-checkbox" type="checkbox" bind:checked on:change />
<style>
input {

View File

@ -11,7 +11,7 @@
padding: 10px;
margin-top: 5px;
margin-bottom: 10px;
background: var(--secondary80);
background: var(--grey-7);
color: var(--white);
font-family: "Courier New", Courier, monospace;
height: 200px;

View File

@ -54,7 +54,7 @@
<style>
.uk-modal-footer {
background: var(--lightslate);
background: var(--grey-1);
}
.uk-modal-dialog {

View File

@ -4,7 +4,7 @@
export let size = 18
export let icon = ""
export let style = ""
export let color = "var(--secondary100)"
export let color = "var(--ink)"
export let hoverColor = "var(--secondary75)"
export let attributes = {}

View File

@ -37,19 +37,18 @@
<style>
input {
/* width: 32px; */
height: 32px;
font-size: 12px;
font-weight: 700;
margin: 0px 0px 0px 1px;
height: 36px;
font-size: 14px;
font-weight: 400;
margin: 0px 0px 0px 2px;
color: var(--ink);
opacity: 0.7;
padding: 0px 4px;
line-height: 1.3;
/* padding: 12px; */
padding: 0px 8px;
font-family: inter;
width: 164px;
box-sizing: border-box;
border: 1px solid var(--grey);
border-radius: 2px;
background-color: var(--grey-2);
border-radius: 4px;
border: 1px solid var(--grey-2);
outline: none;
}

View File

@ -0,0 +1,101 @@
<script>
import { onMount } from "svelte"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
export let modelId
export let linkName
export let linked = []
let records = []
let model = {}
let linkedRecords = new Set(linked)
$: linked = [...linkedRecords]
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel._id]
$: schema = $backendUiStore.selectedModel.schema
async function fetchRecords() {
const FETCH_RECORDS_URL = `/api/${modelId}/records`
const response = await api.get(FETCH_RECORDS_URL)
const modelResponse = await api.get(`/api/models/${modelId}`)
model = await modelResponse.json()
records = await response.json()
}
function linkRecord(id) {
if (linkedRecords.has(id)) {
linkedRecords.delete(id)
} else {
linkedRecords.add(id)
}
linkedRecords = linkedRecords
}
onMount(() => {
fetchRecords()
})
</script>
<section>
<header>
<h3>{linkName}</h3>
</header>
{#each records as record}
<div class="linked-record" on:click={() => linkRecord(record._id)}>
<div class="fields" class:selected={linkedRecords.has(record._id)}>
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
<div class="field">
<span>{model.schema[key].name}</span>
<p>{record[key]}</p>
</div>
{/each}
</div>
</div>
{/each}
</section>
<style>
.fields.selected {
background: var(--light-grey);
}
h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
color: var(--ink);
}
.fields {
padding: 15px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
background: var(--white);
border: 1px solid var(--grey);
border-radius: 5px;
transition: 0.5s all;
margin-bottom: 8px;
}
.fields:hover {
cursor: pointer;
}
.field span {
color: var(--ink-lighter);
font-size: 12px;
}
.field p {
color: var(--ink);
font-size: 14px;
word-break: break-word;
font-weight: 500;
margin-top: 4px;
}
</style>

View File

@ -0,0 +1,65 @@
<script>
import { notificationStore } from "builderStore/store/notifications"
import { onMount, onDestroy } from "svelte"
import { fade } from "svelte/transition"
export let themes = {
danger: "#E26D69",
success: "#84C991",
warning: "#f0ad4e",
info: "#5bc0de",
default: "#aaaaaa",
}
export let timeout = 3000
$: if ($notificationStore.notifications.length) {
setTimeout(() => {
notificationStore.update(state => {
state.notifications.shift()
state.notifications = state.notifications
return state
})
}, timeout)
}
</script>
<ul class="notifications">
{#each $notificationStore.notifications as notification (notification.id)}
<li
class="toast"
style="background: {themes[notification.type]};"
transition:fade>
<div class="content">{notification.message}</div>
{#if notification.icon}
<i class={notification.icon} />
{/if}
</li>
{/each}
</ul>
<style>
.notifications {
width: 40vw;
list-style: none;
position: fixed;
top: 0;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
padding: 0;
z-index: 9999;
}
.toast {
margin-bottom: 10px;
}
.content {
padding: 10px;
display: block;
color: var(--white);
font-weight: 500;
}
</style>

View File

@ -13,13 +13,25 @@
let numberText = value === null || value === undefined ? "" : value.toString()
</script>
<div class="uk-margin">
<label class="uk-form-label">{label}</label>
<div class="uk-form-controls">
<div class="numberbox">
<label>{label}</label>
<input
class="budibase__input"
type="number"
{value}
on:change={inputChanged} />
</div>
</div>
<style>
.numberbox {
display: grid;
align-items: center;
margin-bottom: 16px;
}
label {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
</style>

View File

@ -17,7 +17,7 @@
align-items: center;
font-size: 1.2rem;
font-weight: 700;
color: var(--secondary100);
font-weight: 600;
color: var(--ink);
}
</style>

View File

@ -21,7 +21,7 @@
.select-container {
font-size: 14px;
position: relative;
border: var(--grey-dark) 1px solid;
border: var(--grey-4) 1px solid;
}
.adjusted {

View File

@ -43,7 +43,7 @@
display: flex;
flex-direction: column;
padding: 20px 20px;
border-left: solid 1px var(--grey);
border-left: solid 1px var(--grey-2);
box-sizing: border-box;
}
@ -60,10 +60,11 @@
padding: 0;
cursor: pointer;
font-size: 18px;
font-weight: 700;
color: var(--ink-lighter);
font-weight: 600;
color: var(--grey-5);
margin-right: 20px;
background: none;
outline: none;
}
.switcher > .selected {

View File

@ -15,16 +15,34 @@
$: valuesText = join("\n")(values)
</script>
<div class="uk-margin">
<label class="uk-form-label">{label}</label>
<div class="margin">
<label class="label">{label}</label>
<div class="uk-form-controls">
<textarea value={valuesText} on:change={inputChanged} />
</div>
</div>
<style>
.margin {
margin-bottom: 16px;
display: grid;
}
.label {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
textarea {
width: 300px;
height: 100px;
font-size: 14px;
height: 200px;
width: 100%;
border-radius: 5px;
border: none;
cursor: text;
background: var(--grey-2);
padding: 12px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,124 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
export let ids = []
export let field
let records = []
let open = false
let model
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel._id, field.modelId]
async function fetchRecords() {
const response = await api.post("/api/records/search", {
keys: ids,
})
const modelResponse = await api.get(`/api/models/${field.modelId}`)
records = await response.json()
model = await modelResponse.json()
}
$: ids && fetchRecords()
function toggleOpen() {
open = !open
}
onMount(() => {
fetchRecords()
})
</script>
<section>
<a on:click={toggleOpen}>{records.length}</a>
{#if open}
<div class="popover" transition:fade>
<header>
<h3>{field.name}</h3>
<i class="ri-close-circle-fill" on:click={toggleOpen} />
</header>
{#each records as record}
<div class="linked-record">
<div class="fields">
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
<div class="field">
<span>{model.schema[key].name}</span>
<p>{record[key]}</p>
</div>
{/each}
</div>
</div>
{/each}
</div>
{/if}
</section>
<style>
section {
display: relative;
}
header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
i {
font-size: 24px;
color: var(--ink-lighter);
}
i:hover {
cursor: pointer;
}
a {
font-size: 14px;
}
.popover {
width: 500px;
position: absolute;
right: 15%;
padding: 20px;
background: var(--light-grey);
border: 1px solid var(--grey);
}
h3 {
font-size: 20px;
font-weight: bold;
margin: 0;
}
.fields {
padding: 15px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
background: var(--white);
border: 1px solid var(--grey);
border-radius: 5px;
margin-bottom: 8px;
}
.field span {
color: var(--ink-lighter);
font-size: 12px;
}
.field p {
color: var(--ink);
font-size: 14px;
word-break: break-word;
font-weight: 500;
margin-top: 4px;
}
</style>

View File

@ -1,20 +1,10 @@
<script>
import { onMount, getContext } from "svelte"
import { store, backendUiStore } from "builderStore"
import {
tap,
get,
find,
last,
compose,
flatten,
map,
remove,
keys,
takeRight,
} from "lodash/fp"
import { Button } from "@budibase/bbui"
import Select from "components/common/Select.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import * as api from "./api"
@ -52,24 +42,38 @@
let headers = []
let views = []
let currentPage = 0
let search
$: {
if ($backendUiStore.selectedView) {
api
.fetchDataForView($backendUiStore.selectedView)
.then(records => {
api.fetchDataForView($backendUiStore.selectedView).then(records => {
data = records || []
headers = Object.keys($backendUiStore.selectedModel.schema).filter(
key => !INTERNAL_HEADERS.includes(key)
)
})
}
}
$: paginatedData = data.slice(
$: paginatedData = data
? data.slice(
currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
)
: []
$: headers = Object.keys($backendUiStore.selectedModel.schema).filter(
id => !INTERNAL_HEADERS.includes(id)
)
$: schema = $backendUiStore.selectedModel.schema
const createNewRecord = () => {
open(
CreateEditRecordModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
onMount(() => {
if (views.length) {
@ -81,13 +85,16 @@
<section>
<div class="table-controls">
<h2 class="title">{$backendUiStore.selectedModel.name}</h2>
<Button primary on:click={createNewRecord}>
<span class="button-inner">Create New Record</span>
</Button>
</div>
<table class="uk-table">
<thead>
<tr>
<th>Edit</th>
{#each headers as header}
<th>{header}</th>
<th>{$backendUiStore.selectedModel.schema[header].name}</th>
{/each}
</tr>
</thead>
@ -121,7 +128,11 @@
</div>
</td>
{#each headers as header}
<td>{row[header]}</td>
<td>
{#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} />
{:else}{row[header]}{/if}
</td>
{/each}
</tr>
{/each}
@ -143,7 +154,7 @@
}
table {
border: 1px solid var(--grey-dark);
border: 1px solid var(--grey-4);
background: #fff;
border-radius: 3px;
border-collapse: collapse;
@ -151,7 +162,7 @@
thead {
background: var(--blue-light);
border: 1px solid var(--grey-dark);
border: 1px solid var(--grey-4);
}
thead th {
@ -160,18 +171,17 @@
font-weight: 500;
font-size: 14px;
text-rendering: optimizeLegibility;
letter-spacing: 1px;
}
tbody tr {
border-bottom: 1px solid var(--grey-dark);
border-bottom: 1px solid var(--grey-4);
transition: 0.3s background-color;
color: var(--ink);
font-size: 14px;
}
tbody tr:hover {
background: var(--grey-light);
background: var(--grey-1);
}
.table-controls {
@ -189,4 +199,9 @@
.no-data {
padding: 20px;
}
.button-inner {
display: flex;
align-items: center;
}
</style>

View File

@ -58,20 +58,19 @@
padding: 10px;
margin: 0;
background: #fff;
border: 1px solid #ccc;
border: 1px solid var(--grey-4);
text-transform: capitalize;
border-radius: 3px;
font-family: Roboto;
min-width: 20px;
transition: 0.3s background-color;
}
.pagination__buttons button:hover {
cursor: pointer;
background-color: #fafafa;
background-color: var(--grey-1);
}
.selected {
color: var(--button-text);
color: var(--blue);
}
</style>

View File

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

View File

@ -34,7 +34,7 @@
}
footer {
padding: 20px;
background: #fafafa;
background: var(--grey-1);
border-radius: 0.5rem;
}
</style>

View File

@ -130,7 +130,7 @@
}
tbody > tr:hover {
background-color: var(--grey-light);
background-color: var(--grey-1);
}
.table-controls {
@ -156,7 +156,7 @@
}
footer {
background-color: var(--grey-light);
background-color: var(--grey-1);
margin-top: 40px;
padding: 20px 40px 20px 40px;
display: flex;

View File

@ -1,106 +0,0 @@
<script>
import Dropdown from "components/common/Dropdown.svelte"
import Textbox from "components/common/Textbox.svelte"
import Button from "components/common/Button.svelte"
import ButtonGroup from "components/common/ButtonGroup.svelte"
import NumberBox from "components/common/NumberBox.svelte"
import ValuesList from "components/common/ValuesList.svelte"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import Checkbox from "components/common/Checkbox.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import DatePicker from "components/common/DatePicker.svelte"
import { keys, cloneDeep } from "lodash/fp"
const FIELD_TYPES = ["string", "number", "boolean"]
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">
<ErrorsBox {errors} />
<form on:submit|preventDefault class="uk-form-stacked">
<Textbox label="Name" bind:text={field.name} />
<Dropdown label="Type" bind:selected={type} options={FIELD_TYPES} />
<Checkbox label="Required" bind:checked={required} />
{#if type === 'string'}
<NumberBox label="Max Length" bind:value={constraints.length.maximum} />
<ValuesList label="Categories" bind:values={constraints.inclusion} />
{:else if type === 'datetime'}
<DatePicker
label="Min Value"
bind:value={constraints.datetime.earliest} />
<DatePicker label="Max Value" bind:value={constraints.datetime.latest} />
{:else if type === 'number'}
<NumberBox
label="Min Value"
bind:value={constraints.numericality.greaterThanOrEqualTo} />
<NumberBox
label="Max Value"
bind:value={constraints.numericality.lessThanOrEqualTo} />
{/if}
</form>
</div>
<footer>
<div class="button">
<ActionButton secondary on:click={goBack}>Cancel</ActionButton>
</div>
<ActionButton primary on:click={save}>Save</ActionButton>
</footer>
<style>
.root {
margin: 40px;
}
footer {
padding: 20px 40px;
border-radius: 0 0 5px 5px;
bottom: 0;
left: 0;
background: var(--grey-light);
display: flex;
align-items: center;
justify-content: flex-end;
}
.button {
margin-right: 20px;
}
</style>

View File

@ -1,8 +1,10 @@
<script>
import { onMount } from "svelte"
import { store, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { compose, map, get, flatten } from "lodash/fp"
import ActionButton from "components/common/ActionButton.svelte"
import { Button } from "@budibase/bbui"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import Select from "components/common/Select.svelte"
import RecordFieldControl from "./RecordFieldControl.svelte"
import * as api from "../api"
@ -59,38 +61,96 @@
backendUiStore.update(state => {
state.selectedView = state.selectedView
onClosed()
notifier.success("Record created successfully.")
return state
})
}
</script>
<div class="actions">
<header>
<i class="ri-file-user-fill" />
<h4 class="budibase__title--4">Create / Edit Record</h4>
</header>
<ErrorsBox {errors} />
<form on:submit|preventDefault class="uk-form-stacked">
{#each modelSchema as [key, meta]}
<div class="uk-margin">
{#if meta.type === 'link'}
<LinkedRecordSelector
bind:linked={record[key]}
linkName={meta.name}
modelId={meta.modelId} />
{:else}
<RecordFieldControl
type={determineInputType(meta)}
options={determineOptions(meta)}
label={key}
label={meta.name}
bind:value={record[key]} />
{/if}
</div>
{/each}
</form>
</div>
<footer>
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
<ActionButton on:click={saveRecord}>Save</ActionButton>
<div class="button-margin-3">
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button blue on:click={saveRecord}>Save</Button>
</div>
</footer>
<style>
header {
margin-bottom: 40px;
display: grid;
grid-gap: 20px;
grid-template-columns: 40px 1fr;
align-items: center;
}
i {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--blue-light);
color: var(--ink);
font-size: 20px;
border-radius: 3px;
}
h4 {
display: inline-block;
font-size: 24px;
font-weight: bold;
color: var(--ink);
margin: 0;
}
.actions {
padding: 30px;
}
footer {
padding: 20px;
background: #fafafa;
border-radius: 0.5rem;
padding: 20px 30px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View File

@ -96,7 +96,7 @@
.snippet-selector__heading {
margin-right: 20px;
font-size: 14px;
color: var(--ink-lighter);
color: var(--grey-5);
}
.header {
@ -116,7 +116,7 @@
.buttons {
display: flex;
justify-content: flex-end;
background-color: var(--grey-light);
background-color: var(--grey-1);
margin: 0 40px;
padding: 20px 0;
}

View File

@ -69,7 +69,7 @@
.title {
font-size: 24px;
font-weight: 700;
font-weight: 600;
color: var(--ink);
margin-left: 12px;
}
@ -84,7 +84,7 @@
align-items: center;
justify-content: flex-end;
padding: 20px;
background: var(--grey-light);
background: var(--grey-1);
border-radius: 0 0 5px 5px;
}

View File

@ -1,11 +1,11 @@
<script>
import ActionButton from "components/common/ActionButton.svelte"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore } from "builderStore"
import * as api from "../api"
export let record
export let onClosed
</script>
<section>
@ -25,6 +25,7 @@
alert
on:click={async () => {
await api.deleteRecord(record)
notifier.danger('Record deleted')
backendUiStore.actions.records.delete(record)
onClosed()
}}>
@ -36,13 +37,13 @@
<style>
.alert {
color: rgba(255, 0, 31, 1);
background: #fafafa;
background: var(--grey-1);
padding: 5px;
}
.modal-actions {
padding: 10px;
background: #fafafa;
background: var(--grey-1);
border-top: 1px solid #ccc;
}

View File

@ -51,3 +51,16 @@
on:input={handleInput}
on:change={handleInput} />
{/if}
<style>
label {
display: block;
font-size: 18px;
font-weight: 500;
margin-bottom: 12px;
}
input {
color: var(--dark-grey);
}
</style>

View File

@ -1,6 +1,5 @@
export { default as DeleteRecordModal } from "./DeleteRecord.svelte"
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte"
export { default as CreateEditModelModal } from "./CreateEditModel/CreateEditModel.svelte"
export { default as CreateEditViewModal } from "./CreateEditView.svelte"
export { default as CreateDatabaseModal } from "./CreateDatabase.svelte"
export { default as CreateUserModal } from "./CreateUser.svelte"

View File

@ -1,87 +0,0 @@
<script>
import { getContext } from "svelte"
import { store, backendUiStore } from "builderStore"
import HierarchyRow from "./HierarchyRow.svelte"
import DatabasesList from "./DatabasesList.svelte"
import UsersList from "./UsersList.svelte"
import NavItem from "./NavItem.svelte"
import getIcon from "components/common/icon"
import {
CreateDatabaseModal,
CreateUserModal,
} from "components/database/ModelDataTable/modals"
const { open, close } = getContext("simple-modal")
const openDatabaseCreator = () => {
open(
CreateDatabaseModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
const openUserCreator = () => {
open(
CreateUserModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
</script>
<div class="items-root">
<div class="hierarchy" />
{#if $backendUiStore.selectedDatabase._id}
<div class="hierarchy">
<div class="components-list-container">
<div class="nav-group-header">
<div class="hierarchy-title">Users</div>
<i class="ri-add-line hoverable" on:click={openUserCreator} />
</div>
</div>
<div class="hierarchy-items-container">
<UsersList />
</div>
</div>
{/if}
</div>
<style>
.items-root {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background: var(--white);
}
.nav-group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 10px 20px;
}
.hierarchy-title {
align-items: center;
font-size: 18px;
font-weight: 700;
text-rendering: optimizeLegibility;
color: var(--ink);
}
.hierarchy {
display: flex;
flex-direction: column;
}
.hierarchy-items-container {
flex: 1 1 auto;
overflow-y: auto;
}
</style>

View File

@ -50,7 +50,7 @@
<style>
.root {
font-size: 13px;
color: var(--secondary100);
color: var(--ink);
position: relative;
padding-left: 20px;
}
@ -76,7 +76,6 @@
margin: 0 0 0 6px;
padding: 0;
border: none;
font-family: Roboto;
font-size: 13px;
outline: none;
cursor: pointer;

View File

@ -3,10 +3,8 @@
import { store, backendUiStore } from "builderStore"
import { cloneDeep } from "lodash/fp"
import getIcon from "../common/icon"
import {
CreateEditModelModal,
CreateEditViewModal,
} from "components/database/ModelDataTable/modals"
import { CreateEditViewModal } from "components/database/ModelDataTable/modals"
import api from "builderStore/api"
const { open, close } = getContext("simple-modal")
@ -30,14 +28,6 @@
class:selected={$backendUiStore.selectedView === `all_${node._id}`}>
<i class={ICON_MAP[type]} />
<span style="margin-left: 1rem">{node.name}</span>
<!-- <i
class="ri-edit-line hoverable"
on:click={editModel}
/>
<i
class="ri-delete-bin-7-line hoverable"
on:click={deleteModel}
/> -->
</div>
</div>

View File

@ -0,0 +1,80 @@
<script>
import * as blockDefinitions from "constants/backend"
import { backendUiStore } from "builderStore"
import Block from "components/common/Block.svelte"
const HEADINGS = [
{
title: "Fields",
key: "FIELDS",
},
{
title: "Blocks",
key: "BLOCKS",
},
]
let selectedTab = "FIELDS"
function addField(blockDefinition) {
backendUiStore.actions.models.addField(blockDefinition)
backendUiStore.actions.models.fetch()
}
</script>
<section>
<header>
{#each HEADINGS as tab}
<span
class:selected={selectedTab === tab.key}
on:click={() => (selectedTab = tab.key)}>
{tab.title}
</span>
{/each}
</header>
<div class="block-grid">
{#each Object.values(blockDefinitions[selectedTab]) as blockDefinition}
<Block
on:click={() => addField(blockDefinition)}
title={blockDefinition.name}
icon={blockDefinition.icon} />
{/each}
</div>
</section>
<style>
header {
margin-top: 20px;
margin-bottom: 20px;
display: grid;
grid-template-columns: repeat(3, 1fr);
}
span {
text-align: center;
padding: 10px;
font-weight: 500;
border-radius: 3px;
color: var(--ink-lighter);
font-size: 14px;
background: var(--light-grey);
}
span:hover {
background: var(--blue-light);
cursor: pointer;
}
.selected {
background: var(--grey-3);
color: var(--ink);
}
.block-grid {
margin-top: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
}
</style>

View File

@ -0,0 +1,112 @@
<script>
import { backendUiStore } from "builderStore"
import { uuid } from "builderStore/uuid"
import { fade } from "svelte/transition"
import { FIELDS, BLOCKS, MODELS } from "constants/backend"
import Block from "components/common/Block.svelte"
function addNewField(field) {
backendUiStore.actions.models.addField(field)
}
function createModel(model) {
const { schema, ...rest } = $backendUiStore.selectedModel
const newModel = { ...model, schema: {} }
// TODO: could be better
for (let key in model.schema) {
newModel.schema[uuid()] = model.schema[key]
}
backendUiStore.actions.models.save({
model: {
...newModel,
...rest,
},
})
}
</script>
<section transition:fade>
<header>
<h2>Create New Model</h2>
<p>Before you can view your model, you need to set it up.</p>
</header>
<div class="block-row">
<span class="block-row-title">Fields</span>
<p>Blocks are pre-made fields and help you build your model quicker.</p>
<div class="blocks">
{#each Object.values(FIELDS) as field}
<Block
primary
title={field.name}
icon={field.icon}
on:click={() => addNewField(field)} />
{/each}
</div>
</div>
<div class="block-row">
<span class="block-row-title">Blocks</span>
<p>Blocks are pre-made fields and help you build your model quicker.</p>
<div class="blocks">
{#each Object.values(BLOCKS) as field}
<Block
secondary
title={field.name}
icon={field.icon}
on:click={() => addNewField(field)} />
{/each}
</div>
</div>
<div class="block-row">
<span class="block-row-title">Models</span>
<p>Blocks are pre-made fields and help you build your model quicker.</p>
<div class="blocks">
{#each Object.values(MODELS) as model}
<Block
tertiary
title={model.name}
icon={model.icon}
on:click={() => createModel(model)} />
{/each}
</div>
</div>
</section>
<style>
section {
height: 100vh;
}
h2 {
font-size: 20px;
font-weight: bold;
margin: 0;
}
.block-row-title {
font-weight: 500;
font-size: 16px;
}
p {
margin-top: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.block-row {
margin-top: 40px;
}
.block-row .blocks {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 110px;
grid-gap: 20px;
}
</style>

View File

@ -0,0 +1,47 @@
<script>
export let icon
export let className
export let title
export let selected
export let indented
</script>
<div class:selected on:click class={className}>
<i class:indented class={icon} />
<span>{title}</span>
</div>
<style>
.indented {
margin-left: 10px;
}
div {
padding: 0 10px 0 10px;
height: 36px;
border-radius: 5px;
display: flex;
align-items: center;
transition: 0.3s background-color;
color: var(--ink);
font-weight: 400;
font-size: 14px;
margin-top: 4px;
margin-bottom: 4px;
}
.selected {
background-color: var(--blue-light);
}
div:hover {
background-color: var(--blue-light);
cursor: pointer;
}
i {
color: var(--grey-7);
font-size: 20px;
margin-right: 8px;
}
</style>

View File

@ -0,0 +1,107 @@
<script>
import { getContext } from "svelte"
import { slide } from "svelte/transition"
import { Switcher } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { store, backendUiStore } from "builderStore"
import BlockNavigator from "./BlockNavigator.svelte"
import ListItem from "./ListItem.svelte"
import { Button } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
let HEADINGS = [
{
title: "Navigate",
key: "NAVIGATE",
},
{
title: "Add",
key: "ADD",
},
]
$: selectedTab = $backendUiStore.tabs.NAVIGATION_PANEL
function selectModel(model, fieldId) {
backendUiStore.actions.models.select(model)
if (fieldId) {
backendUiStore.update(state => {
state.selectedField = fieldId
return state
})
}
}
function setupForNewModel() {
backendUiStore.update(state => {
state.selectedModel = {}
state.draftModel = { schema: {} }
state.tabs.SETUP_PANEL = "SETUP"
return state
})
}
</script>
<div class="items-root">
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
<div class="hierarchy">
<div class="components-list-container">
<Switcher
headings={HEADINGS}
bind:value={$backendUiStore.tabs.NAVIGATION_PANEL}>
{#if selectedTab === 'NAVIGATE'}
<Button purple wide on:click={setupForNewModel}>
Create New Model
</Button>
<div class="hierarchy-items-container">
{#each $backendUiStore.models as model}
<ListItem
selected={!$backendUiStore.selectedField && model._id === $backendUiStore.selectedModel._id}
title={model.name}
icon="ri-table-fill"
on:click={() => selectModel(model)} />
{#if model._id === $backendUiStore.selectedModel._id}
<div in:slide>
{#each Object.keys(model.schema) as fieldId}
<ListItem
selected={model._id === $backendUiStore.selectedModel._id && fieldId === $backendUiStore.selectedField}
indented
icon="ri-layout-column-fill"
title={model.schema[fieldId].name}
on:click={() => selectModel(model, fieldId)} />
{/each}
</div>
{/if}
{/each}
</div>
{:else if selectedTab === 'ADD'}
<BlockNavigator />
{/if}
</Switcher>
</div>
</div>
{/if}
</div>
<style>
.items-root {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background: var(--white);
padding: 20px;
}
.hierarchy {
display: flex;
flex-direction: column;
}
.hierarchy-items-container {
margin-top: 20px;
flex: 1 1 auto;
}
</style>

View File

@ -0,0 +1,132 @@
<script>
import { backendUiStore } from "builderStore"
import { Button } from "@budibase/bbui"
import Dropdown from "components/common/Dropdown.svelte"
import Textbox from "components/common/Textbox.svelte"
import ButtonGroup from "components/common/ButtonGroup.svelte"
import NumberBox from "components/common/NumberBox.svelte"
import ValuesList from "components/common/ValuesList.svelte"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import Checkbox from "components/common/Checkbox.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import DatePicker from "components/common/DatePicker.svelte"
import { keys, cloneDeep } from "lodash/fp"
const FIELD_TYPES = ["string", "number", "boolean", "link"]
let field = {}
$: field =
$backendUiStore.draftModel.schema[$backendUiStore.selectedField] || {}
$: required =
field.constraints &&
field.constraints.presence &&
!constraints.presence.allowEmpty
function attachModelIdToSchema(evt) {
const { draftModel } = $backendUiStore
if ($backendUiStore.selectedField !== evt.target.value) {
delete draftModel.schema[$backendUiStore.selectedField]
draftModel.schema[evt.target.value] = field
backendUiStore.update(state => {
state.selectedField = evt.target.value
return state
})
}
}
</script>
<div class="info">
<div class="field-box">
<header>Name</header>
<input class="budibase__input" type="text" bind:value={field.name} />
</div>
</div>
<div class="info">
<div class="field-box">
<header>Type</header>
<span>{field.type}</span>
</div>
</div>
<div class="info">
<div class="field">
<label>Required</label>
<input type="checkbox" />
</div>
{#if field.type === 'string'}
<NumberBox
label="Max Length"
bind:value={field.constraints.length.maximum} />
<ValuesList label="Categories" bind:values={field.constraints.inclusion} />
{:else if field.type === 'datetime'}
<DatePicker
label="Min Value"
bind:value={field.constraints.datetime.earliest} />
<DatePicker
label="Max Value"
bind:value={field.constraints.datetime.latest} />
{:else if field.type === 'number'}
<NumberBox
label="Min Value"
bind:value={field.constraints.numericality.greaterThanOrEqualTo} />
<NumberBox
label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'}
<div class="field">
<label>Link</label>
<select
class="budibase__input"
bind:value={field.modelId}
on:change={attachModelIdToSchema}>
<option value={''} />
{#each $backendUiStore.models as model}
{#if model._id !== $backendUiStore.draftModel._id}
<option value={model._id}>{model.name}</option>
{/if}
{/each}
</select>
</div>
{/if}
</div>
<style>
.info {
margin-bottom: 16px;
border-radius: 5px;
}
label {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.field {
display: grid;
align-items: center;
margin-bottom: 16px;
}
.field-box header {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.field-box span {
background: var(--grey-2);
color: var(--grey-6);
font-weight: 400;
height: 36px;
display: grid;
align-items: center;
padding-left: 12px;
text-transform: capitalize;
border-radius: 5px;
cursor: not-allowed;
}
</style>

View File

@ -0,0 +1,127 @@
<script>
import { getContext, onMount } from "svelte"
import { Button, Switcher } from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import ModelFieldEditor from "./ModelFieldEditor.svelte"
const { open, close } = getContext("simple-modal")
const ITEMS = [
{
title: "Setup",
key: "SETUP",
},
{
title: "Delete",
key: "DELETE",
},
]
let edited = false
$: selectedTab = $backendUiStore.tabs.SETUP_PANEL
$: edited =
$backendUiStore.selectedField ||
($backendUiStore.draftModel &&
$backendUiStore.draftModel.name !== $backendUiStore.selectedModel.name)
async function deleteModel() {
const model = $backendUiStore.selectedModel
const field = $backendUiStore.selectedField
if (field) {
const name = model.schema[field].name
delete model.schema[field]
backendUiStore.actions.models.save({ model })
notifier.danger(`Field ${name} deleted.`)
return
}
const DELETE_MODEL_URL = `/api/models/${model._id}/${model._rev}`
const response = await api.delete(DELETE_MODEL_URL)
backendUiStore.update(state => {
state.selectedView = null
state.selectedModel = {}
state.draftModel = {}
state.models = state.models.filter(({ _id }) => _id !== model._id)
notifier.danger(`${model.name} deleted successfully.`)
return state
})
}
async function saveModel() {
await backendUiStore.actions.models.save({
model: $backendUiStore.draftModel,
})
notifier.success(
"Success! Your changes have been saved. Please continue on with your greatness."
)
}
</script>
<div class="items-root">
<Switcher headings={ITEMS} bind:value={$backendUiStore.tabs.SETUP_PANEL}>
{#if selectedTab === 'SETUP'}
{#if $backendUiStore.selectedField}
<ModelFieldEditor />
{:else if $backendUiStore.draftModel.schema}
<div class="titled-input">
<header>Name</header>
<input
type="text"
class="budibase__input"
bind:value={$backendUiStore.draftModel.name} />
</div>
<div class="titled-input">
<header>Import Data</header>
<Button wide secondary>Import CSV</Button>
</div>
{/if}
<footer>
<Button disabled={!edited} green={edited} wide on:click={saveModel}>
Save
</Button>
</footer>
{:else if selectedTab === 'DELETE'}
<div class="titled-input">
<header>Danger Zone</header>
<Button red wide on:click={deleteModel}>Delete</Button>
</div>
{/if}
</Switcher>
</div>
<style>
header {
font-weight: 500;
}
footer {
width: 100%;
position: fixed;
bottom: 20px;
}
.items-root {
padding: 20px;
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background-color: var(--white);
}
.titled-input {
margin-bottom: 16px;
display: grid;
}
.titled-input header {
display: block;
font-size: 14px;
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1 @@
export { default as ModelSetupNav } from "./ModelSetupNav.svelte"

View File

@ -1,143 +0,0 @@
<script>
import { getContext, onMount } from "svelte"
import { store, backendUiStore } from "builderStore"
import HierarchyRow from "./HierarchyRow.svelte"
import NavItem from "./NavItem.svelte"
import getIcon from "components/common/icon"
import api from "builderStore/api"
import {
CreateEditModelModal,
CreateEditViewModal,
} from "components/database/ModelDataTable/modals"
const { open, close } = getContext("simple-modal")
function editModel() {
open(
CreateEditModelModal,
{
model: node,
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
function newModel() {
open(
CreateEditModelModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
function newView() {
open(
CreateEditViewModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
function selectModel(model) {
backendUiStore.update(state => {
state.selectedModel = model
state.selectedView = `all_${model._id}`
return state
})
}
async function deleteModel(modelToDelete) {
const DELETE_MODEL_URL = `/api/models/${node._id}/${node._rev}`
const response = await api.delete(DELETE_MODEL_URL)
backendUiStore.update(state => {
state.models = state.models.filter(
model => model._id !== modelToDelete._id
)
state.selectedView = {}
return state
})
}
function selectView(view) {
backendUiStore.update(state => {
state.selectedView = view.name
return state
})
}
</script>
<div class="items-root">
<div class="hierarchy">
<div class="components-list-container">
<div class="nav-group-header">
<div class="hierarchy-title">Models</div>
<div class="uk-inline">
<i class="ri-add-line hoverable" on:click={newModel} />
</div>
</div>
</div>
<div class="hierarchy-items-container">
{#each $backendUiStore.models as model}
<HierarchyRow onSelect={selectModel} node={model} type="model" />
{/each}
</div>
</div>
<div class="hierarchy">
<div class="components-list-container">
<div class="nav-group-header">
<div class="hierarchy-title">Views</div>
<div class="uk-inline">
<i class="ri-add-line hoverable" on:click={newView} />
</div>
</div>
</div>
<div class="hierarchy-items-container">
{#each $backendUiStore.views as view}
<HierarchyRow onSelect={selectView} node={view} type="view" />
{/each}
</div>
</div>
</div>
<style>
.items-root {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background-color: var(--white);
}
.nav-group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 10px 20px;
}
.hierarchy-title {
align-items: center;
font-size: 18px;
font-weight: 700;
text-rendering: optimizeLegibility;
color: var(--ink);
}
.hierarchy {
display: flex;
flex-direction: column;
}
.hierarchy-items-container {
flex: 1 1 auto;
overflow-y: auto;
}
</style>

View File

@ -64,7 +64,6 @@
margin: 0 0 0 6px;
padding: 0;
border: none;
font-family: Roboto;
font-size: 13px;
outline: none;
cursor: pointer;

View File

@ -21,23 +21,25 @@
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-medium);
border: 1px solid var(--grey-4);
}
.app-button:hover {
background-color: var(--grey-light);
background-color: var(--grey-1);
text-decoration: none;
}
.app-title {
font-size: 18px;
font-weight: 700;
font-weight: 600;
color: var(--ink);
text-transform: capitalize;
font-family: Inter;
}
.app-desc {
color: var(--ink-light);
color: var(--grey-7);
font-family: Inter;
}
.card-footer {
@ -56,7 +58,7 @@
justify-content: center;
padding: 12px 20px;
border-radius: 5px;
border: 1px var(--grey) solid;
border: 1px var(--grey-2) solid;
font-size: 14px;
font-weight: 400;
cursor: pointer;

View File

@ -142,11 +142,11 @@
background-color: var(--blue-light);
}
.info {
color: var(--primary100);
text-decoration-color: var(--primary100);
color: var(--blue);
text-decoration-color: var(--blue);
}
.info :global(svg) {
fill: var(--primary100);
fill: var(--blue);
margin-right: 8px;
width: 24px;
height: 24px;
@ -164,7 +164,7 @@
padding: 30px 40px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-light);
background-color: var(--grey-1);
}
.spinner-container {
background: white;
@ -183,7 +183,7 @@
font-size: 2em;
}
.error {
color: var(--deletion100);
color: var(--red);
font-weight: bold;
font-size: 0.8em;
}

View File

@ -21,11 +21,11 @@
display: flex;
list-style: none;
font-size: 18px;
font-weight: 700;
font-weight: 600;
}
li {
color: var(--ink-lighter);
color: var(--grey-5);
cursor: pointer;
margin-right: 20px;
}

View File

@ -1,12 +1,21 @@
<script>
import FlatButton from "./FlatButton.svelte";
import FlatButton from "./FlatButton.svelte"
export let format = "hex";
export let onclick = format => {};
export let format = "hex"
export let onclick = format => {}
let colorFormats = ["hex", "rgb", "hsl"];
let colorFormats = ["hex", "rgb", "hsl"]
</script>
<div class="flatbutton-group">
{#each colorFormats as text}
<FlatButton
selected={format === text}
{text}
on:click={() => onclick(text)} />
{/each}
</div>
<style>
.flatbutton-group {
font-weight: 500;
@ -18,12 +27,3 @@
align-self: center;
}
</style>
<div class="flatbutton-group">
{#each colorFormats as text}
<FlatButton
selected={format === text}
{text}
on:click={() => onclick(text)} />
{/each}
</div>

View File

@ -1,6 +1,6 @@
<script>
import {buildStyle} from "./helpers.js"
import {fade} from "svelte/transition"
import { buildStyle } from "./helpers.js"
import { fade } from "svelte/transition"
export let backgroundSize = "10px"
export let borderRadius = ""
@ -8,10 +8,13 @@
export let width = ""
export let margin = ""
$: style = buildStyle({backgroundSize, borderRadius, height, width, margin})
$: style = buildStyle({ backgroundSize, borderRadius, height, width, margin })
</script>
<div in:fade {style}>
<slot />
</div>
<style>
div {
background-image: url('data:image/svg+xml;utf8, <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="white" d="M1,0H2V1H1V0ZM0,1H1V2H0V1Z"/><path fill="gray" d="M0,0H1V1H0V0ZM1,1H2V2H1V1Z"/></svg>');
@ -19,7 +22,3 @@
width: fit-content;
}
</style>
<div in:fade {style}>
<slot />
</div>

View File

@ -1,39 +1,39 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
import { fade } from 'svelte/transition';
import Swatch from "./Swatch.svelte";
import { onMount, createEventDispatcher } from "svelte"
import { fade } from "svelte/transition"
import Swatch from "./Swatch.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import {buildStyle} from "./helpers.js"
import { buildStyle } from "./helpers.js"
import {
getColorFormat,
convertToHSVA,
convertHsvaToFormat
} from "./utils.js";
import Slider from "./Slider.svelte";
import Palette from "./Palette.svelte";
import ButtonGroup from "./ButtonGroup.svelte";
import Input from "./Input.svelte";
convertHsvaToFormat,
} from "./utils.js"
import Slider from "./Slider.svelte"
import Palette from "./Palette.svelte"
import ButtonGroup from "./ButtonGroup.svelte"
import Input from "./Input.svelte"
export let value = "#3ec1d3ff";
export let value = "#3ec1d3ff"
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console
export let disableSwatches = false
export let format = "hexa";
export let open = false;
export let format = "hexa"
export let open = false
export let pickerHeight = 0;
export let pickerWidth = 0;
export let pickerHeight = 0
export let pickerWidth = 0
let adder = null;
let adder = null
let h = null;
let s = null;
let v = null;
let a = null;
let h = null
let s = null
let v = null
let a = null
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
onMount(() => {
if(!swatches.length > 0) {
if (!swatches.length > 0) {
//Don't use locally stored recent colors if swatches have been passed as props
getRecentColors()
}
@ -41,7 +41,7 @@
if (format) {
convertAndSetHSVA()
}
});
})
function getRecentColors() {
let colorStore = localStorage.getItem("cp:recent-colors")
@ -61,37 +61,37 @@
}
function convertAndSetHSVA() {
let hsva = convertToHSVA(value, format);
setHSVA(hsva);
let hsva = convertToHSVA(value, format)
setHSVA(hsva)
}
function setHSVA([hue, sat, val, alpha]) {
h = hue;
s = sat;
v = val;
a = alpha;
h = hue
s = sat
v = val
a = alpha
}
//fired by choosing a color from the palette
function setSaturationAndValue({ detail }) {
s = detail.s;
v = detail.v;
value = convertHsvaToFormat([h, s, v, a], format);
s = detail.s
v = detail.v
value = convertHsvaToFormat([h, s, v, a], format)
dispatchValue()
}
function setHue({color, isDrag}) {
h = color;
value = convertHsvaToFormat([h, s, v, a], format);
if(!isDrag) {
function setHue({ color, isDrag }) {
h = color
value = convertHsvaToFormat([h, s, v, a], format)
if (!isDrag) {
dispatchValue()
}
}
function setAlpha({color, isDrag}) {
a = color === "1.00" ? "1" : color;
value = convertHsvaToFormat([h, s, v, a], format);
if(!isDrag) {
function setAlpha({ color, isDrag }) {
a = color === "1.00" ? "1" : color
value = convertHsvaToFormat([h, s, v, a], format)
if (!isDrag) {
dispatchValue()
}
}
@ -101,43 +101,42 @@
}
function changeFormatAndConvert(f) {
format = f;
value = convertHsvaToFormat([h, s, v, a], format);
format = f
value = convertHsvaToFormat([h, s, v, a], format)
}
function handleColorInput(text) {
let format = getColorFormat(text)
if(format) {
if (format) {
value = text
convertAndSetHSVA()
}
}
function dispatchInputChange() {
if(format) {
if (format) {
dispatchValue()
}
}
function addSwatch() {
if(format) {
if (format) {
dispatch("addswatch", value)
setRecentColor(value)
}
}
function removeSwatch(idx) {
let removedSwatch = swatches.splice(idx, 1);
let removedSwatch = swatches.splice(idx, 1)
swatches = swatches
dispatch("removeswatch", removedSwatch)
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
}
function applySwatch(color) {
if(value !== color) {
if (value !== color) {
format = getColorFormat(color)
if(format) {
if (format) {
value = color
convertAndSetHSVA()
dispatchValue()
@ -145,11 +144,112 @@
}
}
$: border = (v > 90 && s < 5) ? "1px dashed #dedada" : ""
$: style = buildStyle({background: value, border})
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : ""
$: style = buildStyle({ background: value, border })
$: shrink = swatches.length > 0
</script>
<div class="colorpicker-container">
<div class="palette-panel">
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
</div>
<div class="control-panel">
<div class="alpha-hue-panel">
<div>
<CheckedBackground borderRadius="50%" backgroundSize="8px">
<div class="selected-color" {style} />
</CheckedBackground>
</div>
<div>
<Slider type="hue" value={h} on:change={hue => setHue(hue.detail)} />
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<Slider
type="alpha"
value={a}
on:change={alpha => setAlpha(alpha.detail)} />
</CheckedBackground>
</div>
</div>
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input {value} on:input={event => handleColorInput(event.target.value)} />
</div>
</div>
</div>
<div
class="colorpicker-container"
bind:clientHeight={pickerHeight}
bind:clientWidth={pickerWidth}>
<div class="palette-panel">
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
</div>
<div class="control-panel">
<div class="alpha-hue-panel">
<div>
<CheckedBackground borderRadius="50%" backgroundSize="8px">
<div class="selected-color" {style} />
</CheckedBackground>
</div>
<div>
<Slider
type="hue"
value={h}
on:change={hue => setHue(hue.detail)}
on:dragend={dispatchValue} />
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<Slider
type="alpha"
value={a}
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
on:dragend={dispatchValue} />
</CheckedBackground>
</div>
</div>
{#if !disableSwatches}
<div transition:fade class="swatch-panel">
{#if swatches.length > 0}
{#each swatches as color, idx}
<Swatch
{color}
on:click={() => applySwatch(color)}
on:removeswatch={() => removeSwatch(idx)} />
{/each}
{/if}
{#if swatches.length !== 12}
<div
bind:this={adder}
transition:fade
class="adder"
on:click={addSwatch}
class:shrink>
<span>&plus;</span>
</div>
{/if}
</div>
{/if}
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input
{value}
on:input={event => handleColorInput(event.target.value)}
on:change={dispatchInputChange} />
</div>
</div>
</div>
<style>
.colorpicker-container {
display: flex;
@ -161,7 +261,8 @@
width: 220px;
background: #ffffff;
border-radius: 2px;
box-shadow: 0 0.15em 1.5em 0 rgba(0,0,0,.1), 0 0 1em 0 rgba(0,0,0,.03);
box-shadow: 0 0.15em 1.5em 0 rgba(0, 0, 0, 0.1),
0 0 1em 0 rgba(0, 0, 0, 0.03);
}
.palette-panel {
@ -228,54 +329,3 @@
padding-top: 3px;
}
</style>
<div class="colorpicker-container" bind:clientHeight={pickerHeight} bind:clientWidth={pickerWidth}>
<div class="palette-panel">
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
</div>
<div class="control-panel">
<div class="alpha-hue-panel">
<div>
<CheckedBackground borderRadius="50%" backgroundSize="8px">
<div class="selected-color" {style} />
</CheckedBackground>
</div>
<div>
<Slider type="hue" value={h} on:change={(hue) => setHue(hue.detail)} on:dragend={dispatchValue} />
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<Slider
type="alpha"
value={a}
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
on:dragend={dispatchValue}
/>
</CheckedBackground>
</div>
</div>
{#if !disableSwatches}
<div transition:fade class="swatch-panel">
{#if swatches.length > 0}
{#each swatches as color, idx}
<Swatch {color} on:click={() => applySwatch(color)} on:removeswatch={() => removeSwatch(idx)} />
{/each}
{/if}
{#if swatches.length !== 12}
<div bind:this={adder} transition:fade class="adder" on:click={addSwatch} class:shrink>
<span>&plus;</span>
</div>
{/if}
</div>
{/if}
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input {value} on:input={event => handleColorInput(event.target.value)} on:change={dispatchInputChange} />
</div>
</div>
</div>

View File

@ -1,21 +1,21 @@
<script>
import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import {createEventDispatcher, afterUpdate, beforeUpdate} from "svelte"
import { createEventDispatcher, afterUpdate, beforeUpdate } from "svelte"
import {buildStyle} from "./helpers.js"
import { fade } from 'svelte/transition';
import {getColorFormat} from "./utils.js"
import { buildStyle } from "./helpers.js"
import { fade } from "svelte/transition"
import { getColorFormat } from "./utils.js"
export let value = "#3ec1d3ff"
export let swatches = []
export let disableSwatches = false
export let open = false;
export let open = false
export let width = "25px"
export let height = "25px"
let format = "hexa";
let dimensions = {top: 0, left: 0}
let format = "hexa"
let dimensions = { top: 0, left: 0 }
let colorPreview = null
let previewHeight = null
@ -24,36 +24,41 @@
let pickerHeight = 0
let anchorEl = null
let parentNodes = [];
let parentNodes = []
let errorMsg = null
$: previewStyle = buildStyle({width, height, background: value})
$: errorPreviewStyle = buildStyle({width, height})
$: pickerStyle = buildStyle({top: `${dimensions.top}px`, left: `${dimensions.left}px`})
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
top: `${dimensions.top}px`,
left: `${dimensions.left}px`,
})
const dispatch = createEventDispatcher()
beforeUpdate(() => {
format = getColorFormat(value)
if(!format) {
if (!format) {
errorMsg = `Colorpicker - ${value} is an unknown color format. Please use a hex, rgb or hsl value`
console.error(errorMsg)
}else{
} else {
errorMsg = null
}
})
afterUpdate(() => {
if(colorPreview && colorPreview.offsetParent && !anchorEl) {
if (colorPreview && colorPreview.offsetParent && !anchorEl) {
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
anchorEl = colorPreview.offsetParent
let curEl = colorPreview
let els = []
//Travel up dom tree from preview element to find parent elements that scroll
while(!anchorEl.isSameNode(curEl)) {
while (!anchorEl.isSameNode(curEl)) {
curEl = curEl.parentNode
let elOverflow = window.getComputedStyle(curEl).getPropertyValue("overflow")
if(/scroll|auto/.test(elOverflow)) {
let elOverflow = window
.getComputedStyle(curEl)
.getPropertyValue("overflow")
if (/scroll|auto/.test(elOverflow)) {
els.push(curEl)
}
}
@ -61,49 +66,78 @@
}
})
function openColorpicker(event) {
if(colorPreview) {
open = true;
if (colorPreview) {
open = true
}
}
$: if(open && colorPreview) {
const {top: spaceAbove, width, bottom, right, left: spaceLeft} = colorPreview.getBoundingClientRect()
const {innerHeight, innerWidth} = window
$: if (open && colorPreview) {
const {
top: spaceAbove,
width,
bottom,
right,
left: spaceLeft,
} = colorPreview.getBoundingClientRect()
const { innerHeight, innerWidth } = window
const {offsetLeft, offsetTop} = colorPreview
const { offsetLeft, offsetTop } = colorPreview
//get the scrollTop value for all scrollable parent elements
let scrollTop = parentNodes.reduce((scrollAcc, el) => scrollAcc += el.scrollTop, 0);
let scrollTop = parentNodes.reduce(
(scrollAcc, el) => (scrollAcc += el.scrollTop),
0
)
const spaceBelow = (innerHeight - spaceAbove) - previewHeight
const top = spaceAbove > spaceBelow ? (offsetTop - pickerHeight) - scrollTop : (offsetTop + previewHeight) - scrollTop
const spaceBelow = innerHeight - spaceAbove - previewHeight
const top =
spaceAbove > spaceBelow
? offsetTop - pickerHeight - scrollTop
: offsetTop + previewHeight - scrollTop
//TOO: Testing and Scroll Awareness for x Scroll
const spaceRight = (innerWidth - spaceLeft) + previewWidth
const left = spaceRight > spaceLeft ? (offsetLeft + previewWidth) : offsetLeft - pickerWidth
const spaceRight = innerWidth - spaceLeft + previewWidth
const left =
spaceRight > spaceLeft
? offsetLeft + previewWidth
: offsetLeft - pickerWidth
dimensions = {top, left}
dimensions = { top, left }
}
function onColorChange(color) {
value = color.detail;
value = color.detail
dispatch("change", color.detail)
}
</script>
<div class="color-preview-container">
{#if !errorMsg}
<CheckedBackground borderRadius="3px" backgroundSize="8px">
<div bind:this={colorPreview} bind:clientHeight={previewHeight} bind:clientWidth={previewWidth} class="color-preview" style={previewStyle} on:click={openColorpicker} />
<div
bind:this={colorPreview}
bind:clientHeight={previewHeight}
bind:clientWidth={previewWidth}
class="color-preview"
style={previewStyle}
on:click={openColorpicker} />
</CheckedBackground>
{#if open}
<div transition:fade class="picker-container" style={pickerStyle}>
<Colorpicker on:change={onColorChange} on:addswatch on:removeswatch bind:format bind:value bind:pickerHeight bind:pickerWidth {swatches} {disableSwatches} {open} />
<Colorpicker
on:change={onColorChange}
on:addswatch
on:removeswatch
bind:format
bind:value
bind:pickerHeight
bind:pickerWidth
{swatches}
{disableSwatches}
{open} />
</div>
<div on:click|self={() => open = false} class="overlay"></div>
<div on:click|self={() => (open = false)} class="overlay" />
{/if}
{:else}
<div class="color-preview preview-error" style={errorPreviewStyle}>
@ -112,9 +146,8 @@
{/if}
</div>
<style>
.color-preview-container{
.color-preview-container {
display: flex;
flex-flow: row nowrap;
height: fit-content;
@ -140,7 +173,7 @@
height: fit-content;
}
.overlay{
.overlay {
position: fixed;
top: 0;
bottom: 0;
@ -149,4 +182,3 @@
z-index: 2;
}
</style>

View File

@ -1,8 +1,10 @@
<script>
export let text = "";
export let selected = false;
export let text = ""
export let selected = false
</script>
<div class="flatbutton" class:selected on:click>{text}</div>
<style>
.flatbutton {
cursor: pointer;
@ -25,5 +27,3 @@
border: none;
}
</style>
<div class="flatbutton" class:selected on:click>{text}</div>

View File

@ -1,7 +1,14 @@
<script>
export let value = "";
export let value = ""
</script>
<div>
<input on:input type="text" {value} maxlength="25" />
</div>
<div>
<input on:input on:change type="text" {value} maxlength="25" />
</div>
<style>
div {
display: flex;
@ -22,7 +29,3 @@
font-weight: 550;
}
</style>
<div>
<input on:input on:change type="text" {value} maxlength="25" />
</div>

View File

@ -1,42 +1,58 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
import { onMount, createEventDispatcher } from "svelte"
import CheckedBackground from "./CheckedBackground.svelte"
const dispatch = createEventDispatcher()
export let h = 0;
export let s = 0;
export let v = 0;
export let a = 1;
export let h = 0
export let s = 0
export let v = 0
export let a = 1
let palette;
let paletteHeight, paletteWidth = 0;
let palette
let paletteHeight,
paletteWidth = 0
function handleClick(event) {
const { left, top } = palette.getBoundingClientRect();
let clickX = (event.clientX - left)
let clickY = (event.clientY - top)
if((clickX > 0 && clickY > 0) && (clickX < paletteWidth && clickY < paletteHeight)) {
const { left, top } = palette.getBoundingClientRect()
let clickX = event.clientX - left
let clickY = event.clientY - top
if (
clickX > 0 &&
clickY > 0 &&
clickX < paletteWidth &&
clickY < paletteHeight
) {
let s = (clickX / paletteWidth) * 100
let v = 100 - ((clickY / paletteHeight) * 100)
dispatch("change", {s, v})
let v = 100 - (clickY / paletteHeight) * 100
dispatch("change", { s, v })
}
}
}
$: pickerX = (s * paletteWidth) / 100;
$: pickerX = (s * paletteWidth) / 100
$: pickerY = paletteHeight * ((100 - v) / 100)
$: paletteGradient = `linear-gradient(to top, rgba(0, 0, 0, 1), transparent),
linear-gradient(to left, hsla(${h}, 100%, 50%, ${a}), rgba(255, 255, 255, ${a}))
`;
$: style = `background: ${paletteGradient};`;
`
$: style = `background: ${paletteGradient};`
$: pickerStyle = `transform: translate(${pickerX - 8}px, ${pickerY - 8}px);`
</script>
<CheckedBackground width="100%">
<div
bind:this={palette}
bind:clientHeight={paletteHeight}
bind:clientWidth={paletteWidth}
on:click={handleClick}
class="palette"
{style}>
<div class="picker" style={pickerStyle} />
</div>
</CheckedBackground>
<style>
.palette {
position: relative;
@ -55,9 +71,3 @@
border-radius: 50%;
}
</style>
<CheckedBackground width="100%">
<div bind:this={palette} bind:clientHeight={paletteHeight} bind:clientWidth={paletteWidth} on:click={handleClick} class="palette" {style}>
<div class="picker" style={pickerStyle} />
</div>
</CheckedBackground>

View File

@ -1,37 +1,62 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
import dragable from "./drag.js";
import { onMount, createEventDispatcher } from "svelte"
import dragable from "./drag.js"
export let value = 1;
export let type = "hue";
export let value = 1
export let type = "hue"
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
let slider;
let sliderWidth = 0;
let slider
let sliderWidth = 0
function onSliderChange(mouseX, isDrag = false) {
const { left, width } = slider.getBoundingClientRect();
let clickPosition = mouseX - left;
const { left, width } = slider.getBoundingClientRect()
let clickPosition = mouseX - left
let percentageClick = (clickPosition / sliderWidth).toFixed(2)
if (percentageClick >= 0 && percentageClick <= 1) {
let value =
type === "hue"
? 360 * percentageClick
: percentageClick;
let value = type === "hue" ? 360 * percentageClick : percentageClick
dispatch("change", {color: value, isDrag});
dispatch("change", { color: value, isDrag })
}
}
$: thumbPosition =
type === "hue" ? sliderWidth * (value / 360) : sliderWidth * value;
type === "hue" ? sliderWidth * (value / 360) : sliderWidth * value
$: style = `transform: translateX(${thumbPosition - 6}px);`;
$: style = `transform: translateX(${thumbPosition - 6}px);`
</script>
<div
bind:this={slider}
bind:clientWidth={sliderWidth}
on:click={event => handleClick(event.clientX)}
class="color-format-slider"
class:hue={type === 'hue'}
class:alpha={type === 'alpha'}>
<div
use:dragable
on:drag={e => handleClick(e.detail)}
class="slider-thumb"
{style} />
</div>
<div
bind:this={slider}
bind:clientWidth={sliderWidth}
on:click={event => onSliderChange(event.clientX)}
class="color-format-slider"
class:hue={type === 'hue'}
class:alpha={type === 'alpha'}>
<div
use:dragable
on:drag={e => onSliderChange(e.detail, true)}
on:dragend
class="slider-thumb"
{style} />
</div>
<style>
.color-format-slider {
position: relative;
@ -69,21 +94,6 @@
border: 1px solid #777676;
border-radius: 50%;
background-color: #ffffff;
cursor:grab;
cursor: grab;
}
</style>
<div
bind:this={slider}
bind:clientWidth={sliderWidth}
on:click={event => onSliderChange(event.clientX)}
class="color-format-slider"
class:hue={type === 'hue'}
class:alpha={type === 'alpha'}>
<div
use:dragable
on:drag={e => onSliderChange(e.detail, true)}
on:dragend
class="slider-thumb"
{style} />
</div>

View File

@ -1,15 +1,35 @@
<script>
import {createEventDispatcher} from "svelte"
import { fade } from 'svelte/transition';
import { createEventDispatcher } from "svelte"
import { fade } from "svelte/transition"
import CheckedBackground from "./CheckedBackground.svelte"
export let hovered = false
export let color = "#fff"
const dispatch = createEventDispatcher()
</script>
<div class="space">
<CheckedBackground borderRadius="6px">
<div
in:fade
class="swatch"
style={`background: ${color};`}
on:click|self
on:mouseover={() => (hovered = true)}
on:mouseleave={() => (hovered = false)}>
{#if hovered}
<div
in:fade
class="remove-icon"
on:click|self={() => dispatch('removeswatch')}>
<span on:click|self={() => dispatch('removeswatch')}>&times;</span>
</div>
{/if}
</div>
</CheckedBackground>
</div>
<style>
.swatch {
position: relative;
@ -29,7 +49,7 @@
right: 0;
top: -5px;
right: -4px;
width:10px;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #800000;
@ -39,15 +59,3 @@
align-items: center;
}
</style>
<div class="space">
<CheckedBackground borderRadius="6px">
<div in:fade class="swatch" style={`background: ${color};`} on:click|self on:mouseover={() => hovered = true} on:mouseleave={() => hovered = false}>
{#if hovered}
<div in:fade class="remove-icon" on:click|self={()=> dispatch("removeswatch")}>
<span on:click|self={()=> dispatch("removeswatch")}>&times;</span>
</div>
{/if}
</div>
</CheckedBackground>
</div>

View File

@ -257,21 +257,21 @@
.menu li:not(.disabled) {
cursor: pointer;
color: var(--ink-light);
color: var(--grey-7);
}
.menu li:not(.disabled):hover {
color: var(--ink);
background-color: var(--grey-light);
background-color: var(--grey-1);
}
.disabled {
color: var(--grey-dark);
color: var(--grey-4);
cursor: default;
}
.hr-style {
margin: 8px 0;
color: var(--grey-dark);
color: var(--grey-4);
}
</style>

View File

@ -1,6 +1,7 @@
<script>
import { setContext, onMount } from "svelte"
import PropsView from "./PropsView.svelte"
import { store } from "builderStore"
import IconButton from "components/common/IconButton.svelte"
import {
@ -29,7 +30,10 @@
let selectedCategory = categories[0]
$: components = $store.components
$: componentInstance = $store.currentComponentInfo
$: componentInstance =
$store.currentView !== "component"
? { ...$store.currentPreviewItem, ...$store.currentComponentInfo }
: $store.currentComponentInfo
$: componentDefinition = $store.components[componentInstance._component]
$: componentPropDefinition =
flattenedPanel.find(
@ -44,7 +48,22 @@
componentPropDefinition.properties[selectedCategory.value]
const onStyleChanged = store.setComponentStyle
const onPropChanged = store.setComponentProp
function onPropChanged(key, value) {
if ($store.currentView !== "component") {
store.setPageOrScreenProp(key, value)
return
}
store.setComponentProp(key, value)
}
$: isComponentOrScreen =
$store.currentView === "component" ||
$store.currentFrontEndType === "screen"
$: isNotScreenslot = componentInstance._component !== "##builtin/screenslot"
$: displayName =
isComponentOrScreen && componentInstance._instanceName && isNotScreenslot
function walkProps(component, action) {
action(component)
@ -79,6 +98,12 @@
{categories}
{selectedCategory} />
{#if displayName}
<div class="instance-name">
<strong>{componentInstance._instanceName}</strong>
</div>
{/if}
<div class="component-props-container">
{#if selectedCategory.value === 'design'}
<DesignView {panelDefinition} {componentInstance} {onStyleChanged} />
@ -87,8 +112,8 @@
{componentInstance}
{componentDefinition}
{panelDefinition}
displayNameField={displayName}
onChange={onPropChanged}
onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{:else if selectedCategory.value === 'events'}
<EventsEditor component={componentInstance} />
@ -117,8 +142,13 @@
}
.component-props-container {
margin-top: 20px;
margin-top: 10px;
flex: 1 1 auto;
min-height: 0;
}
.instance-name {
margin-top: 10px;
font-size: 12px;
}
</style>

View File

@ -38,7 +38,7 @@
padding: 5px;
border-style: solid;
border-width: 0 0 1px 0;
border-color: var(--lightslate);
border-color: var(--grey-1);
cursor: pointer;
}
@ -48,12 +48,12 @@
.component > .title {
font-size: 13pt;
color: var(--secondary100);
color: var(--ink);
}
.component > .description {
font-size: 10pt;
color: var(--primary75);
color: var(--blue);
font-style: italic;
}
</style>

View File

@ -22,48 +22,35 @@
trimChars(" "),
])
const lastPartOfName = c => {
if (!c) return ""
const name = c.name ? c.name : c._component ? c._component : c
return last(name.split("/"))
}
const isComponentSelected = (current, comp) => current === comp
$: _screens = pipe(screens, [
map(c => ({ component: c, title: lastPartOfName(c) })),
sortBy("title"),
])
const changeScreen = screen => {
store.setCurrentScreen(screen.title)
store.setCurrentScreen(screen.props._instanceName)
$goto(`./:page/${screen.title}`)
}
</script>
<div class="root">
{#each _screens as screen}
{#each screens as screen}
<div
class="budibase__nav-item component"
class:selected={$store.currentComponentInfo._id === screen.component.props._id}
class:selected={$store.currentComponentInfo._id === screen.props._id}
on:click|stopPropagation={() => changeScreen(screen)}>
<span
class="icon"
class:rotate={$store.currentPreviewItem.name !== screen.title}>
{#if screen.component.props._children.length}
class:rotate={$store.currentPreviewItem.name !== screen.props._instanceName}>
{#if screen.props._children.length}
<ArrowDownIcon />
{/if}
</span>
<i class="ri-artboard-2-fill icon" />
<span class="title">{screen.title}</span>
<span class="title">{screen.props._instanceName}</span>
</div>
{#if $store.currentPreviewItem.name === screen.title && screen.component.props._children}
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children}
<ComponentsHierarchyChildren
components={screen.component.props._children}
components={screen.props._children}
currentComponent={$store.currentComponentInfo} />
{/if}
{/each}
@ -77,8 +64,7 @@
}
.title {
margin-left: 10px;
margin-top: 2px;
margin-left: 14px;
font-size: 14px;
font-weight: 400;
}
@ -87,9 +73,8 @@
display: inline-block;
transition: 0.2s;
font-size: 24px;
width: 20px;
margin-top: 2px;
color: var(--ink-light);
width: 18px;
color: var(--grey-7);
}
.icon:nth-of-type(2) {

View File

@ -20,6 +20,7 @@
const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const isScreenslot = name => name === "##builtin/screenslot"
const selectComponent = component => {
// Set current component
@ -39,10 +40,10 @@
<div
class="budibase__nav-item item"
class:selected={currentComponent === component}
style="padding-left: {level * 20 + 53}px">
style="padding-left: {level * 20 + 40}px">
<div class="nav-item">
<i class="icon ri-arrow-right-circle-fill" />
{get_capitalised_name(component._component)}
{isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
</div>
<div class="actions">
<ComponentDropdownMenu {component} />
@ -73,7 +74,7 @@
padding: 0px 5px 0px 15px;
margin: auto 0px;
border-radius: 3px;
height: 35px;
height: 36px;
align-items: center;
}
@ -90,7 +91,7 @@
}
.item:hover {
background: var(--grey-light);
background: var(--grey-1);
cursor: pointer;
}
.item:hover .actions {
@ -105,7 +106,7 @@
}
.icon {
color: var(--ink-light);
color: var(--grey-7);
margin-right: 8px;
}
</style>

View File

@ -55,7 +55,7 @@
display: flex;
flex-direction: column;
padding: 20px 5px 20px 10px;
border-left: solid 1px var(--grey);
border-left: solid 1px var(--grey-2);
}
.switcher {
@ -70,8 +70,8 @@
padding: 0;
cursor: pointer;
font-size: 18px;
font-weight: 700;
color: var(--ink-lighter);
font-weight: 600;
color: var(--grey-5);
margin-right: 20px;
}

View File

@ -19,7 +19,6 @@
{ value: "normal", text: "Normal" },
{ value: "hover", text: "Hover" },
{ value: "active", text: "Active" },
{ value: "selected", text: "Selected" },
]
$: propertyGroupNames = Object.keys(panelDefinition)
@ -31,7 +30,7 @@
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
</div>
<div class="positioned-wrapper">
<div class="positioned-wrapper">
<div class="design-view-property-groups">
{#if propertyGroupNames.length > 0}
{#each propertyGroupNames as groupName}
@ -49,7 +48,7 @@
</div>
{/if}
</div>
</div>
</div>
</div>
<style>
@ -64,7 +63,7 @@
flex: 0 0 50px;
}
.positioned-wrapper{
.positioned-wrapper {
position: relative;
display: flex;
min-height: 0;

View File

@ -53,7 +53,7 @@
.title > div:nth-child(1) {
grid-column-start: name;
color: var(--secondary100);
color: var(--ink);
}
.title > div:nth-child(2) {

View File

@ -165,7 +165,7 @@
padding: 30px 40px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-light);
background-color: var(--grey-1);
}
.save {
margin-left: 20px;

View File

@ -93,7 +93,7 @@
.newevent {
cursor: pointer;
border: 1px solid var(--grey-dark);
border: 1px solid var(--grey-4);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
@ -109,7 +109,7 @@
}
.newevent:hover {
background: var(--grey-light);
background: var(--grey-1);
}
.icon {
@ -155,7 +155,7 @@
}
.selected {
color: var(--button-text);
background: var(--background-button) !important;
color: var(--blue);
background: var(--grey-1) !important;
}
</style>

View File

@ -8,6 +8,7 @@
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
import { store, workflowStore } from "builderStore"
import { ArrowDownIcon } from "components/common/Icons/"
import { createEventDispatcher } from "svelte"
export let parameter
@ -25,6 +26,7 @@
{/if}
{#if parameter.name === 'workflow'}
<Select on:change bind:value={parameter.value}>
<option value="" />
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
<option value={workflow._id}>{workflow.name}</option>
{/each}

View File

@ -3,20 +3,13 @@
export let value = ""
export let text = ""
export let icon = ""
export let padding = "8px 5px;"
export let onClick = value => {}
export let selected = false
export let fontWeight = ""
$: style = buildStyle({ padding, fontWeight })
$: useIcon = !!icon
</script>
<div
class="flatbutton"
{style}
class:selected
on:click={() => onClick(value || text)}>
<div class="flatbutton" class:selected on:click={() => onClick(value || text)}>
{#if useIcon}
<i class={icon} />
{:else}
@ -29,25 +22,27 @@
<style>
.flatbutton {
cursor: pointer;
max-height: 36px;
padding: 8px 2px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background: #ffffff;
color: var(--ink-light);
color: var(--grey-7);
border-radius: 5px;
font-family: Roboto;
font-size: 14px;
font-weight: 400;
transition: all 0.3s;
margin-left: 5px;
text-rendering: optimizeLegibility;
}
.selected {
background: var(--ink-light);
color: #ffffff;
background: var(--grey-3);
color: var(--ink);
}
i {
font-size: 20px;
font-size: 16px;
}
</style>

View File

@ -46,6 +46,7 @@
<style>
.flatbutton-group {
display: flex;
width: 100%;
}
.button-container {

View File

@ -14,10 +14,7 @@
<PagesList />
<button class="newscreen" on:click={newScreen}>
<i class="icon ri-add-circle-fill" />
Create New Screen
</button>
<button class="newscreen" on:click={newScreen}>Create New Screen</button>
<PageLayout layout={$store.pages[$store.currentPageName]} />
@ -30,23 +27,26 @@
<style>
.newscreen {
cursor: pointer;
border: 1px solid var(--grey-dark);
border-radius: 3px;
border: 1px solid var(--purple);
border-radius: 5px;
width: 100%;
height: 36px;
padding: 8px 16px;
margin: 20px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
background: var(--purple);
color: var(--white);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
transition: all 3ms;
outline: none;
}
.newscreen:hover {
background: var(--grey-light);
background: var(--purple-light);
color: var(--purple);
}
.icon {

Some files were not shown because too many files have changed in this diff Show More