Merge branch 'master' of https://github.com/Budibase/budibase into britecharts/separate-components-2

This commit is contained in:
cmack 2020-08-04 10:06:10 +01:00
commit bbf32cda75
152 changed files with 3529 additions and 3912 deletions

18
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,18 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- roadmap
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@ -28,12 +28,17 @@ jobs:
- run: yarn lint
- run: yarn bootstrap
- run: yarn build
env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
- run: yarn test
- name: Prepare for app notarization (macOS)
if: startsWith(matrix.os, 'macos')
# Import Apple API key for app notarization on macOS
run: |
xattr -cr *
mkdir -p ~/private_keys/
echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8

11
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"editor.formatOnSave": true,
"eslint.format.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
},
"[svelte]": {
"editor.defaultFormatter": "JamesBirtles.svelte-vscode"
},
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

View File

@ -27,15 +27,15 @@ A client represents a single budibase customer. Each budibase client will have 1
### App
A client can have one or more budibase applications. Think of a budibase application as a tree. Budibase applications have one definition of what the front end will look like,
A client can have one or more budibase applications. Budibase applications would be things like "Developer Inventory Management" or "Goat Herder CRM". Think of a budibase application as a tree.
### Database
An App can have one or more databases. Keeping with our [dendrology](https://en.wikipedia.org/wiki/Dendrology) analogy - think of an database as a branch on the tree. Databases are used to keep data separate for different instances of your app. For example, if you had a CRM app, you may create a database for your US office, and a database for your Australian office. Databases allow us to support [multitenancy](https://www.gartner.com/en/information-technology/glossary/multitenancy) in budibase applications.
### Model
### Table
Models in budibase are almost akin to tables in relational databases. A model may be a "Car" or an "Employee". They are the main building blocks for the creation and management of backend data in budibase.
Tables in budibase are almost akin to tables in relational databases. A table may be a "Car" or an "Employee". They are the main building blocks for the creation and management of backend data in budibase.
### View
@ -95,7 +95,7 @@ then `cd ` into your local copy.
### 4. Initialising Budibase and Creating a Budibase App
`yarn initialise` will initialise your budibase installation. A Budibase apps folder will have been created in `~/.budibase`.
`yarn initialise` will initialise your budibase installation. A Budibase apps folder will have been created in `~/.budibase`. You can also just start up the budibase electron app and it should initialise budibase for you.
This is a blank apps folder, so you will need to create yourself an app.
@ -149,7 +149,25 @@ The backend schema, models and records are stored using PouchDB when developing
### Publishing Budibase to NPM
You can publish all the latest versions of the monorepo packages by running:
#### Testing In Electron
At budibase, we pride ourselves on giving our users a fast, native and slick local development experience. As a result, we use the electron to provide a native GUI for the budibase builder. In order to release budibase out into the wild, you should test your changes in a packaged electron application. To do this, first build budibase from the root directory.
```
yarn build
```
Now everything is built, you can package up your electron application.
```
cd packages/server
yarn build:electron
```
Your new electron application will be stored in `packages/server/dist/<operating-system>`. Open up the executable and make sure everything is working smoothly.
#### Publishing to NPM
Once you are happy that your changes work in electron, you can publish all the latest versions of the monorepo packages by running:
```
yarn publishnpm
@ -157,6 +175,10 @@ yarn publishnpm
from your root directory.
#### CI Release
After NPM has successfully published the budibase packages, a new tag will be pushed to master. This will kick off a github action (can be found at `.github/workflows/release.yml`) this will build and package the electron application for every OS (Windows, Mac, Linux). The binaries will be stored under the new tag on the [budibase releases page](https://github.com/Budibase/budibase/releases).
### Troubleshooting
Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again:
@ -186,4 +208,4 @@ Or if you are in the builder you can run `yarn cy:test`.
* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE).
* We use the [C4 (Collective Code Construction Contract)](https://rfc.zeromq.org/spec:42/C4/) process for contributions.
Please read this if you are unfamiliar with it.
Please read this if you are unfamiliar with it.

View File

@ -1,5 +1,5 @@
{
"version": "0.0.32",
"version": "0.1.13",
"npmClient": "yarn",
"packages": [
"packages/*"

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.0.32",
"version": "0.1.13",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -55,10 +55,13 @@
]
},
"dependencies": {
"@budibase/bbui": "^1.15.4",
"@budibase/client": "^0.0.32",
"@budibase/bbui": "^1.18.0",
"@budibase/client": "^0.1.1",
"@budibase/colorpicker": "^1.0.1",
"@nx-js/compiler-util": "^2.0.0",
"britecharts": "^2.16.0",
"@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "^0.7.0",
"codemirror": "^5.51.0",
"d3-selection": "^1.4.1",
"date-fns": "^1.29.0",
@ -66,15 +69,16 @@
"feather-icons": "^4.21.0",
"flatpickr": "^4.5.7",
"lodash": "^4.17.13",
"logrocket": "^1.0.6",
"lunr": "^2.3.5",
"mustache": "^4.0.1",
"posthog-js": "^1.3.1",
"safe-buffer": "^5.1.2",
"shortid": "^2.2.15",
"string_decoder": "^1.2.0",
"svelte-portal": "^0.1.0",
"svelte-simple-modal": "^0.4.2",
"uikit": "^3.1.7"
"uikit": "^3.1.7",
"yup": "^0.29.2"
},
"devDependencies": {
"@babel/core": "^7.5.5",
@ -112,4 +116,4 @@
"svelte-jester": "^1.0.6"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
}
}

View File

@ -180,6 +180,9 @@ export default {
"process.env.NODE_ENV": JSON.stringify(
production ? "production" : "development"
),
"process.env.POSTHOG_TOKEN": JSON.stringify(process.env.POSTHOG_TOKEN),
"process.env.POSTHOG_URL": JSON.stringify(process.env.POSTHOG_URL),
"process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN),
}),
svelte({

View File

@ -5,17 +5,9 @@
import { routes } from "../routify/routes"
import { store, initialise } from "builderStore"
import NotificationDisplay from "components/common/Notification/NotificationDisplay.svelte"
import { notifier } from "builderStore/store/notifications"
function showErrorBanner() {
notifier.danger(
"Whoops! Looks like we're having trouble. Please refresh the page."
)
}
onMount(async () => {
window.addEventListener("error", showErrorBanner)
window.addEventListener("unhandledrejection", showErrorBanner)
await initialise()
})
$basepath = "/_builder"

View File

@ -0,0 +1,24 @@
import * as Sentry from "@sentry/browser"
import posthog from "posthog-js"
function activate() {
Sentry.init({ dsn: process.env.SENTRY_DSN })
posthog.init(process.env.POSTHOG_TOKEN, {
api_host: process.env.POSTHOG_URL,
})
}
function captureException(err) {
Sentry.captureException(err)
}
function captureEvent(event) {
if (process.env.NODE_ENV !== "production") return
posthog.capture(event)
}
export default {
activate,
captureException,
captureEvent,
}

View File

@ -63,7 +63,7 @@
.budibase__nav-item.selected {
color: var(--ink);
background: var(--blue-light);
background: var(--grey-2);
}
.budibase__nav-item:hover {

View File

@ -1,7 +1,7 @@
import { getStore } from "./store"
import { getBackendUiStore } from "./store/backend"
import { getWorkflowStore } from "./store/workflow/"
import LogRocket from "logrocket"
import analytics from "../analytics"
export const store = getStore()
export const backendUiStore = getBackendUiStore()
@ -10,7 +10,7 @@ export const workflowStore = getWorkflowStore()
export const initialise = async () => {
try {
if (process.env.NODE_ENV === "production") {
LogRocket.init("knlald/budibase")
analytics.activate()
}
} catch (err) {
console.log(err)

View File

@ -2,23 +2,24 @@ import { writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import api from "../api"
export const getBackendUiStore = () => {
const INITIAL_BACKEND_UI_STATE = {
models: [],
views: [],
users: [],
selectedDatabase: {},
selectedModel: {},
draftModel: {},
tabs: {
SETUP_PANEL: "SETUP",
NAVIGATION_PANEL: "NAVIGATE",
},
}
const INITIAL_BACKEND_UI_STATE = {
models: [],
views: [],
users: [],
selectedDatabase: {},
selectedModel: {},
draftModel: {},
tabs: {
SETUP_PANEL: "SETUP",
NAVIGATION_PANEL: "NAVIGATE",
},
}
const store = writable(INITIAL_BACKEND_UI_STATE)
export const getBackendUiStore = () => {
const store = writable({ ...INITIAL_BACKEND_UI_STATE })
store.actions = {
reset: () => store.set({ ...INITIAL_BACKEND_UI_STATE }),
database: {
select: async db => {
const modelsResponse = await api.get(`/api/models`)
@ -78,7 +79,6 @@ export const getBackendUiStore = () => {
}
const SAVE_MODEL_URL = `/api/models`
console.log(updatedModel)
const response = await api.post(SAVE_MODEL_URL, updatedModel)
const savedModel = await response.json()
await store.actions.models.fetch()

View File

@ -53,7 +53,6 @@ export const getStore = () => {
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
store.saveScreen = saveScreen(store)
store.deleteScreen = deleteScreen(store)
store.setCurrentScreen = setCurrentScreen(store)
store.setCurrentPage = setCurrentPage(store)
store.createScreen = createScreen(store)
@ -162,6 +161,7 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
props: createProps(rootComponent).props,
}
newScreen.route = route
newScreen.name = newScreen.props._id
newScreen.props._instanceName = screenName || ""
state.currentPreviewItem = newScreen
state.currentComponentInfo = newScreen.props
@ -191,24 +191,6 @@ const setCurrentScreen = store => screenName => {
})
}
const deleteScreen = store => name => {
store.update(s => {
const components = s.components.filter(c => c.name !== name)
const screens = s.screens.filter(c => c.name !== name)
s.components = components
s.screens = screens
if (s.currentPreviewItem.name === name) {
s.currentPreviewItem = null
s.currentFrontEndType = ""
}
api.delete(`/_builder/api/${s.appId}/screen/${name}`)
return s
})
}
const savePage = store => async page => {
store.update(state => {
if (state.currentFrontEndType !== "page" || !state.currentPageName) {

View File

@ -28,12 +28,15 @@
}
i {
font-size: 30px;
font-size: 24px;
color: var(--grey-7);
}
span {
font-size: 14px;
text-align: center;
margin-top: 8px;
line-height: 1.25;
}
div:hover {

View File

@ -1,116 +0,0 @@
<script>
import { onMount } from "svelte"
// import { HsvPicker } from "svelte-color-picker"
// export let initialValue = "#ffffff"
export let onChange = color => {}
export let open = false
let value = "#ffffff"
let _justMounted = true //see onColorChange
let pickerHeight = 275
let colorPreview
let pickerTopPosition = null
function rbgaToHexa({ r, g, b, a }) {
r = r.toString(16)
g = g.toString(16)
b = b.toString(16)
a = Math.round(a * 255).toString(16)
if (r.length == 1) r = "0" + r
if (g.length == 1) g = "0" + g
if (b.length == 1) b = "0" + b
if (a.length == 1) a = "0" + a
return "#" + r + g + b + a
}
function onColourChange(rgba) {
value = rbgaToHexa(rgba.detail)
//Hack: so that color change doesn't fire onMount
if (!_justMounted) {
// onChange(value)
}
_justMounted = false
}
function toggleColorpicker(isOpen) {
if (isOpen) {
const {
y: previewYPosition,
height: previewHeight,
} = colorPreview.getBoundingClientRect()
let wiggleRoom = window.innerHeight - previewYPosition
let displayTop = wiggleRoom < pickerHeight
if (displayTop) {
pickerTopPosition = previewYPosition - (pickerHeight - window.scrollY)
} else {
pickerTopPosition = null
}
}
open = isOpen
}
$: style = open ? "display: block;" : "display: none;"
$: pickerStyle = pickerTopPosition ? `top: ${pickerTopPosition}px;` : ""
</script>
<div
bind:this={colorPreview}
on:click={() => toggleColorpicker(true)}
class="color-preview"
style={`background: ${value}`} />
<div class="colorpicker" {style}>
<div class="overlay" on:click|self={() => toggleColorpicker(false)} />
<div class="cp" style={pickerStyle}>
<!-- <HsvPicker on:colorChange={onColourChange} startColor={value} /> -->
</div>
</div>
<!--
OLD LOCAL STORAGE OPTIONS. INCLUDING FOR ADDING LATER
function getRecentColors() {
let colorStore = localStorage.getItem("bb:recentColors")
if (!!colorStore) {
swatches = JSON.parse(colorStore)
}
}
function setRecentColor(color) {
if (swatches.length >= 15) {
swatches.splice(0, 1)
picker.removeSwatch(0)
}
if (!swatches.includes(color)) {
swatches = [...swatches, color]
picker.addSwatch(color)
localStorage.setItem("bb:recentColors", JSON.stringify(swatches))
}
} -->
<style>
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* background: rgba(5, 5, 5, 0.25); */
}
.cp {
position: absolute;
right: 25px;
}
.color-preview {
height: 30px;
width: 100%;
margin: 5px;
cursor: pointer;
border: 1px solid gainsboro;
}
</style>

View File

@ -3,9 +3,12 @@
viewBox="0 0 24 24"
width="24"
height="24">
<path d="M0 0h24v24H0z" fill="none" />
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 1l9.5 5.5v11L12 23l-9.5-5.5v-11L12 1zm0 14a3 3 0 1 0 0-6 3 3 0 0 0 0
6z"
fill="currentColor" />
fill="currentColor"
d="M8.686 4l2.607-2.607a1 1 0 0 1 1.414 0L15.314 4H19a1 1 0 0 1 1
1v3.686l2.607 2.607a1 1 0 0 1 0 1.414L20 15.314V19a1 1 0 0 1-1
1h-3.686l-2.607 2.607a1 1 0 0 1-1.414 0L8.686 20H5a1 1 0 0
1-1-1v-3.686l-2.607-2.607a1 1 0 0 1 0-1.414L4 8.686V5a1 1 0 0 1
1-1h3.686zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" />
</svg>

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 494 B

View File

@ -60,7 +60,8 @@
<style>
.fields.selected {
background: var(--grey-1);
background: var(--grey-2);
border: var(--purple) 1px solid;
}
h3 {

View File

@ -65,6 +65,7 @@
margin-right: 20px;
background: none;
outline: none;
font-family: Inter;
}
.switcher > .selected {

View File

@ -113,15 +113,15 @@
on:click={() => {
editRecord(row)
}}>
<div>Edit</div>
<i class="ri-edit-line" />
<div class="label">Edit</div>
</li>
<li>
<div
on:click={() => {
deleteRecord(row)
}}>
Delete
</div>
<li
on:click={() => {
deleteRecord(row)
}}>
<i class="ri-delete-bin-2-line" />
<div class="label">Delete</div>
</li>
</ul>
</div>
@ -146,6 +146,9 @@
</section>
<style>
section {
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: 600;
@ -177,7 +180,7 @@
border-bottom: 1px solid var(--grey-4);
transition: 0.3s background-color;
color: var(--ink);
font-size: 14px;
font-size: 12px;
}
tbody tr:hover {
@ -204,4 +207,28 @@
display: flex;
align-items: center;
}
li {
display: flex;
align-items: center;
border-radius: 5px;
}
i {
color: var(--grey-7);
margin-right: 8px;
font-size: 20px;
}
.label {
color: var(--grey-7);
font-size: 14px;
font-family: inter;
font-weight: 400;
margin: 12px 0px;
}
.label:hover {
color: var(--ink);
cursor: pointer;
}
</style>

View File

@ -3,7 +3,7 @@
import { store, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { compose, map, get, flatten } from "lodash/fp"
import { Button } from "@budibase/bbui"
import { Input, TextArea, Button } from "@budibase/bbui"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import Select from "components/common/Select.svelte"
import RecordFieldControl from "./RecordFieldControl.svelte"
@ -70,7 +70,7 @@
<div class="actions">
<header>
<i class="ri-file-user-fill" />
<h4 class="budibase__title--4">Create / Edit Record</h4>
<h4>Create / Edit Record</h4>
</header>
<ErrorsBox {errors} />
<form on:submit|preventDefault class="uk-form-stacked">
@ -117,15 +117,16 @@
align-items: center;
justify-content: center;
background: var(--blue-light);
color: var(--ink);
color: var(--grey-7);
font-size: 20px;
border-radius: 3px;
border-radius: 5px;
}
h4 {
display: inline-block;
font-size: 24px;
font-weight: bold;
font-weight: 600;
font-family: sans-serif;
color: var(--ink);
margin: 0;
}

View File

@ -36,7 +36,7 @@
class={determineClassName(type)}
bind:value
class:uk-form-danger={errors.length > 0}>
<option></option>
<option />
{#each options as opt}
<option value={opt}>{opt}</option>
{/each}

View File

@ -52,17 +52,28 @@
}
span {
cursor: pointer;
display: grid;
justify-content: center;
align-content: center;
padding: 0px 16px;
height: 36px;
text-align: center;
padding: 10px;
font-weight: 500;
border-radius: 3px;
color: var(--ink-lighter);
background: #ffffff;
color: var(--grey-7);
border-radius: 5px;
font-family: inter;
font-size: 14px;
background: var(--grey-1);
font-weight: 400;
transition: all 0.3s;
text-rendering: optimizeLegibility;
border: none !important;
transition: 0.2s;
outline: none;
}
span:hover {
background: var(--blue-light);
color: var(--ink);
cursor: pointer;
}

View File

@ -31,11 +31,11 @@
}
.selected {
background-color: var(--blue-light);
background-color: var(--grey-2);
}
div:hover {
background-color: var(--blue-light);
background-color: var(--grey-1);
cursor: pointer;
}

View File

@ -68,7 +68,7 @@
<ListItem
selected={model._id === $backendUiStore.selectedModel._id && fieldName === $backendUiStore.selectedField}
indented
icon="ri-layout-column-fill"
icon="ri-layout-column-line"
title={model.schema[fieldName].name}
on:click={() => selectModel(model, fieldName)} />
{/each}

View File

@ -21,7 +21,7 @@
$: required =
field.constraints &&
field.constraints.presence &&
!constraints.presence.allowEmpty
!field.constraints.presence.allowEmpty
</script>
<div class="info">
@ -41,7 +41,10 @@
<div class="info">
<div class="field">
<label>Required</label>
<input type="checkbox" />
<input
type="checkbox"
bind:checked={required}
on:change={() => (field.constraints.presence.allowEmpty = required)} />
</div>
{#if field.type === 'string'}

View File

@ -59,7 +59,9 @@
if (field.name.startsWith("_")) {
errors.push(`field '${field.name}' - name cannot begin with '_''`)
} else if (restrictedFieldNames.includes(field.name)) {
errors.push(`field '${field.name}' - is a restricted name, please rename`)
errors.push(
`field '${field.name}' - is a restricted name, please rename`
)
} else if (!field.name || !field.name.trim()) {
errors.push("field name cannot be blank")
}
@ -75,9 +77,7 @@
async function saveModel() {
const errors = validate()
if (errors.length > 0) {
notifier.danger(
errors.join("/n")
)
notifier.danger(errors.join("/n"))
return
}
@ -103,10 +103,12 @@
class="budibase__input"
bind:value={$backendUiStore.draftModel.name} />
</div>
<!-- dont have this capability yet..
<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}>

View File

@ -34,7 +34,7 @@
}
.topnavitemright {
cursor: pointer;
color: var(--ink-light);
color: var(--grey-7);
margin: 0px 20px 0px 0px;
padding-top: 4px;
font-weight: 500;

View File

@ -1,5 +1,5 @@
<script>
import { General, Users, DangerZone } from "./tabs"
import { General, Users, DangerZone, APIKeys } from "./tabs"
import { Input, TextArea, Button, Switcher } from "@budibase/bbui"
import { SettingsIcon, CloseIcon } from "components/common/Icons/"
@ -20,6 +20,11 @@
key: "USERS",
component: Users,
},
{
title: "API Keys",
key: "API_KEYS",
component: APIKeys,
},
{
title: "Danger Zone",
key: "DANGERZONE",
@ -50,6 +55,7 @@
<style>
.container {
position: relative;
height: 36rem;
}
.close-button {
@ -83,9 +89,10 @@
width: 20px;
padding: 10px;
background-color: var(--blue-light);
color: var(--grey-7);
}
.body {
padding: 40px 40px 80px 40px;
padding: 40px 40px 40px 40px;
display: grid;
grid-gap: 20px;
}

View File

@ -39,4 +39,16 @@
grid-gap: 18px;
grid-template-columns: 1fr 1fr 1fr;
}
.inputs :global(input) {
padding: 10px 12px;
border-radius: var(--rounded-small);
}
.inputs :global(select) {
padding: 9px 12px;
border-radius: var(--rounded-small);
}
.inputs :global(button) {
border-radius: var(--rounded-small);
height: initial;
}
</style>

View File

@ -0,0 +1,54 @@
<script>
import { Input, Button } from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
import posthog from "posthog-js"
let keys = { budibase: "", sendGrid: "" }
async function updateKey([key, value]) {
const response = await api.put(`/api/keys/${key}`, { value })
const res = await response.json()
if (key === "budibase") posthog.identify(value)
keys = { ...keys, ...res }
}
// Get Keys
async function fetchKeys() {
const response = await api.get(`/api/keys/`)
const res = await response.json()
keys = res
}
fetchKeys()
</script>
<div class="container">
<div class="background">
<Input
on:save={e => updateKey(['budibase', e.detail])}
thin
edit
value={keys.budibase}
label="Budibase" />
</div>
<div class="background">
<Input
on:save={e => updateKey(['sendgrid', e.detail])}
thin
edit
value={keys.sendgrid}
label="Sendgrid" />
</div>
</div>
<style>
.container {
display: grid;
grid-gap: var(--space);
}
.background {
border-radius: 5px;
padding: 12px 0px;
}
</style>

View File

@ -1,31 +1,42 @@
<script>
import { params, goto } from "@sveltech/routify"
import { Input, TextArea, Button } from "@budibase/bbui"
import Title from "../TabTitle.svelte"
import { del } from "builderStore/api"
let value = ""
let loading = false
const deleteApp = () => {
async function deleteApp() {
loading = true
// Do stuff here to delete app!
// Navigate to start
const id = $params.application
const res = await del(`/api/${id}`)
const json = await res.json()
loading = false
if (res.ok) {
$goto("/")
return json
} else {
throw new Error(json)
}
}
</script>
<Title>Danger Zone</Title>
<div class="background">
<p>
Type DELETE into the textbox, then click the following button to delete your
web app:
</p>
<Input
on:change={e => (value = e.target.value)}
on:input={e => (value = e.target.value)}
thin
disabled={loading}
placeholder="Enter your name"
label="Type DELETE into the textbox, then click the following button to
delete your web app:" />
placeholder="" />
<Button
disabled={value !== 'DELETE' || loading}
primary
red
wide
on:click={deleteApp}>
Delete Entire Web App
@ -35,10 +46,12 @@
<style>
.background {
display: grid;
grid-gap: var(--space);
grid-gap: 16px;
border-radius: 5px;
background-color: var(--light-grey);
padding: 12px 12px 18px 12px;
padding: 12px 0px;
}
p {
margin: 0;
}
.background :global(button) {
max-width: 100%;

View File

@ -2,7 +2,6 @@
import { Input, TextArea, Button } from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
import Title from "../TabTitle.svelte"
async function updateApplication(data) {
const response = await api.put(`/api/${$store.appId}`, data)
@ -17,34 +16,25 @@
}
</script>
<Title>General</Title>
<div class="container">
<div class="background">
<Input
on:save={e => updateApplication({ name: e.detail })}
thin
edit
value={$store.name}
label="Name" />
</div>
<div class="background">
<TextArea
on:save={e => updateApplication({ description: e.detail })}
thin
edit
value={$store.description}
label="Name" />
</div>
<Input
on:save={e => updateApplication({ name: e.detail })}
thin
edit
value={$store.name}
label="Name" />
<TextArea
on:save={e => updateApplication({ description: e.detail })}
thin
edit
value={$store.description}
label="Description" />
</div>
<style>
.container {
display: grid;
grid-gap: var(--space);
}
.background {
border-radius: 5px;
background-color: var(--light-grey);
padding: 12px 12px 18px 12px;
grid-gap: 32px;
margin-top: 32px;
}
</style>

View File

@ -1,6 +1,5 @@
<script>
import { Input, Select, Button } from "@budibase/bbui"
import Title from "../TabTitle.svelte"
import UserRow from "../UserRow.svelte"
import { store, backendUiStore } from "builderStore"
@ -52,9 +51,8 @@
let fetchUsersPromise = fetchUsers()
</script>
<Title>Users</Title>
<div class="container">
<div class="background create">
<div class="background">
<div class="title">Create new user</div>
<div class="inputs">
<Input thin bind:value={username} name="Name" placeholder="Username" />
@ -69,10 +67,10 @@
</Select>
</div>
<div class="create-button">
<Button on:click={createUser} small blue>Create</Button>
<Button on:click={createUser} small primary>Create</Button>
</div>
</div>
<div class="background">
<div class="background-users">
<div class="title">Current Users</div>
{#await fetchUsersPromise}
Loading state!
@ -87,7 +85,8 @@
{/each}
</ul>
{:catch error}
err0r
Something went wrong when trying to fetch users. Please refresh (CMD + R /
CTRL + R) the page and try again.
{/await}
</div>
</div>
@ -95,27 +94,32 @@
<style>
.container {
display: grid;
grid-gap: 14px;
grid-gap: 32px;
margin-top: 32px;
}
.background {
position: relative;
display: grid;
grid-gap: 12px;
border-radius: 5px;
background-color: var(--grey-2);
padding: 12px 12px 18px 12px;
}
.background.create {
background-color: var(--blue-light);
}
.inputs :global(select) {
padding: 12px 9px;
height: initial;
.background-users {
position: relative;
display: grid;
grid-gap: 12px;
border-radius: 5px;
}
.create-button {
position: absolute;
top: 12px;
right: 12px;
top: 0px;
right: 0px;
}
.create-button :global(button) {
font-size: var(--font-size-sm);
min-width: 100px;
border-radius: var(--rounded-small);
}
.title {
font-size: 14px;
@ -123,13 +127,24 @@
}
.inputs {
display: grid;
margin-top: 12px;
grid-gap: 18px;
grid-template-columns: 1fr 1fr 1fr;
}
.inputs :global(input) {
padding: 10px 12px;
border-radius: var(--rounded-small);
}
.inputs :global(select) {
padding: 10px 12px;
border-radius: var(--rounded-small);
background-color: var(--grey-2);
}
ul {
list-style: none;
padding: 0;
display: grid;
grid-gap: 8px;
margin-top: 0;
}
</style>

View File

@ -2,4 +2,5 @@ export { default as General } from "./General.svelte"
export { default as Integrations } from "./Integrations.svelte"
export { default as Permissions } from "./Permissions.svelte"
export { default as Users } from "./Users.svelte"
export { default as APIKeys } from "./APIKeys.svelte"
export { default as DangerZone } from "./DangerZone.svelte"

View File

@ -1,27 +1,24 @@
<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
export let name, _id
</script>
<div class="apps-card">
<h3 class="app-title">{name}</h3>
<p class="app-desc">{description}</p>
<div class="card-footer">
<a href={`/_builder/${_id}`} class="app-button">Open Web App</a>
<a href={`/_builder/${_id}`} class="app-button">Open {name}</a>
</div>
</div>
<style>
.apps-card {
background-color: var(--white);
padding: 20px 20px 30px 20px;
padding: 20px 20px 20px 20px;
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-4);
font-family: Inter;
}
.app-button:hover {
@ -34,12 +31,15 @@
font-weight: 600;
color: var(--ink);
text-transform: capitalize;
font-family: Inter;
}
.app-desc {
color: var(--grey-7);
font-family: Inter;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.card-footer {

View File

@ -1,51 +1,174 @@
<script>
import { writable } from "svelte/store"
import { store, workflowStore, backendUiStore } from "builderStore"
import { string, object } from "yup"
import api, { get } from "builderStore/api"
import Form from "@svelteschool/svelte-forms"
import Spinner from "components/common/Spinner.svelte"
import { API, Info, User } from "./Steps"
import Indicator from "./Indicator.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"
import { post } from "builderStore/api"
import analytics from "../../analytics"
const { open, close } = getContext("simple-modal")
//Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
const createAppStore = writable({ currentStep: 0, values: {} })
let name = ""
let description = ""
let loading = false
let error = {}
export let hasKey
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 post("/api/applications", data)
let submitting = false
let errors = {}
let validationErrors = {}
let validationSchemas = [
{
apiKey: string().required("Please enter your API key."),
},
{
applicationName: string().required("Your application must have a name."),
},
{
username: string().required("Your application needs a first user."),
password: string().required(
"Please enter a password for your first user."
),
accessLevelId: string().required(
"You need to select an access level for your user."
),
},
]
const res = await response.json()
let steps = [
{
component: API,
errors,
},
{
component: Info,
errors,
},
{
component: User,
errors,
},
]
$goto(`./${res._id}`)
} catch (error) {
console.error(error)
}
if (hasKey) {
validationSchemas.shift()
validationSchemas = validationSchemas
steps.shift()
steps = steps
}
// Handles form navigation
const back = () => {
if ($createAppStore.currentStep > 0) {
$createAppStore.currentStep -= 1
}
}
const next = () => {
$createAppStore.currentStep += 1
}
// $: errors = validationSchemas.validate(values);
$: getErrors(
$createAppStore.values,
validationSchemas[$createAppStore.currentStep]
)
async function getErrors(values, schema) {
try {
validationErrors = {}
await object(schema).validate(values, { abortEarly: false })
} catch (error) {
validationErrors = extractErrors(error)
}
}
let value
const checkValidity = async (values, currentStep) => {
const validity = await object()
.shape(validationSchemas[currentStep])
.isValid(values)
currentStepIsValid = validity
// Check full form on last step
if (currentStep === steps.length - 1) {
// Make one big schema from all the small ones
const fullSchema = Object.assign({}, ...validationSchemas)
// Check full form schema
const formIsValid = await object()
.shape(fullSchema)
.isValid(values)
fullFormIsValid = formIsValid
}
}
async function signUp() {
submitting = true
try {
// Add API key if there is none.
if (!hasKey) {
await updateKey(["budibase", $createAppStore.values.apiKey])
}
// Create App
const appResp = await post("/api/applications", {
name: $createAppStore.values.applicationName,
})
const appJson = await appResp.json()
analytics.captureEvent("web_app_created", {
name,
appId: appJson._id,
})
// Select Correct Application/DB in prep for creating user
const applicationPkg = await get(`/api/${appJson._id}/appPackage`)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
backendUiStore.actions.reset()
await store.setPackage(pkg)
workflowStore.actions.fetch()
} else {
throw new Error(pkg)
}
// Create user
const user = {
name: $createAppStore.values.username,
username: $createAppStore.values.username,
password: $createAppStore.values.password,
accessLevelId: $createAppStore.values.accessLevelId,
}
const userResp = await api.post(`/api/users`, user)
const json = await userResp.json()
$goto(`./${appJson._id}`)
} catch (error) {
console.error(error)
}
}
async function updateKey([key, value]) {
const response = await api.put(`/api/keys/${key}`, { value })
const res = await response.json()
return res
}
function extractErrors({ inner }) {
return inner.reduce((acc, err) => {
return { ...acc, [err.path]: err.message }
}, {})
}
let currentStepIsValid = false
let fullFormIsValid = false
$: checkValidity($createAppStore.values, $createAppStore.currentStep)
let onChange = () => {}
function _onCancel() {
@ -58,45 +181,55 @@
</script>
<div class="container">
<div class="sidebar">
{#each steps as { active, done }, i}
<Indicator
active={$createAppStore.currentStep === i}
done={i < $createAppStore.currentStep}
step={i + 1} />
{/each}
</div>
<div class="body">
<div class="heading">
<span class="icon">
<AppsIcon />
</span>
<h3 class="header">Create new web app</h3>
<h3 class="header">Get Started with Budibase</h3>
</div>
<div class="step">
<Form bind:values={$createAppStore.values}>
{#each steps as step, i (i)}
<div class:hidden={$createAppStore.currentStep !== i}>
<svelte:component
this={step.component}
{validationErrors}
options={step.options}
name={step.name} />
</div>
{/each}
</Form>
</div>
<div class="footer">
{#if $createAppStore.currentStep > 0}
<Button secondary on:click={back}>Back</Button>
{/if}
{#if $createAppStore.currentStep < steps.length - 1}
<Button secondary on:click={next} disabled={!currentStepIsValid}>
Next
</Button>
{/if}
{#if $createAppStore.currentStep === steps.length - 1}
<Button
secondary
on:click={signUp}
disabled={!fullFormIsValid || submitting}>
{submitting ? 'Loading...' : 'Submit'}
</Button>
{/if}
</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 secondary 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}
<img src="/_builder/assets/bb-logo.svg" alt="budibase icon" />
{#if submitting}
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
@ -106,9 +239,19 @@
<style>
.container {
min-height: 600px;
display: grid;
grid-template-columns: 80px 1fr;
position: relative;
}
.sidebar {
display: grid;
border-bottom-left-radius: 0.5rem;
border-top-left-radius: 0.5rem;
grid-gap: 30px;
align-content: center;
background: #f5f5f5;
}
.close-button {
cursor: pointer;
position: absolute;
@ -129,43 +272,18 @@
margin: 0;
font-size: 24px;
font-weight: 600;
font-family: inter;
}
.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(--blue);
text-decoration-color: var(--blue);
}
.info :global(svg) {
fill: var(--blue);
margin-right: 8px;
width: 24px;
height: 24px;
}
.body {
padding: 40px 40px 80px 40px;
padding: 40px 60px 60px 60px;
display: grid;
grid-gap: 20px;
align-items: center;
grid-template-rows: auto 1fr auto;
}
.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-1);
grid-gap: 15px;
grid-template-columns: auto auto;
justify-content: end;
}
.spinner-container {
background: white;
@ -183,9 +301,14 @@
.spinner-text {
font-size: 2em;
}
.error {
color: var(--red);
font-weight: bold;
font-size: 0.8em;
.hidden {
display: none;
}
img {
position: absolute;
top: 20px;
left: 20px;
height: 40px;
}
</style>

View File

@ -0,0 +1,80 @@
<script>
export let step, done, active
</script>
<div class="container" class:active class:done>
<div class="circle" class:active class:done>
{#if done}
<svg
width="12"
height="10"
viewBox="0 0 12 10"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.1212 0.319527C10.327 0.115582 10.6047 0.000803464 10.8944
4.20219e-06C11.1841 -0.00079506 11.4624 0.11245 11.6693
0.315256C11.8762 0.518062 11.9949 0.794134 11.9998 1.08379C12.0048
1.37344 11.8955 1.65339 11.6957 1.86313L5.82705 9.19893C5.72619
9.30757 5.60445 9.39475 5.46913 9.45527C5.3338 9.51578 5.18766 9.54839
5.03944 9.55113C4.89123 9.55388 4.74398 9.52671 4.60651
9.47124C4.46903 9.41578 4.34416 9.33316 4.23934 9.22833L0.350925
5.33845C0.242598 5.23751 0.155712 5.11578 0.0954499 4.98054C0.0351876
4.84529 0.00278364 4.69929 0.00017159 4.55124C-0.00244046 4.4032
0.024793 4.25615 0.0802466 4.11886C0.1357 3.98157 0.218238 3.85685
0.322937 3.75215C0.427636 3.64746 0.55235 3.56492 0.68964
3.50946C0.82693 3.45401 0.973983 3.42678 1.12203 3.42939C1.27007 3.432
1.41607 3.46441 1.55132 3.52467C1.68657 3.58493 1.80829 3.67182
1.90923 3.78014L4.98762 6.85706L10.0933 0.35187C10.1024 0.340482
10.1122 0.329679 10.1227 0.319527H10.1212Z"
fill="white" />
</svg>
{:else}{step}{/if}
</div>
</div>
<style>
.container::before {
content: "";
position: absolute;
top: -30px;
width: 1px;
height: 30px;
background: #bdbdbd;
}
.container:first-child::before {
display: none;
}
.container {
position: relative;
height: 45px;
display: grid;
place-items: center;
}
.container.active {
box-shadow: inset 3px 0 0 0 #4285f4;
}
.circle.active {
background: #4285f4;
color: white;
border: none;
}
.circle.done {
background: #bdbdbd;
color: white;
border: none;
}
.circle {
color: #bdbdbd;
font-size: 14px;
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid #bdbdbd;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,25 @@
<script>
import { Input } from "@budibase/bbui"
export let validationErrors
let blurred = { api: false }
</script>
<h2>Setup your API Key</h2>
<div class="container">
<Input
on:input={() => (blurred.api = true)}
label="API Key"
name="apiKey"
placeholder="Enter your API Key"
type="password"
error={blurred.api && validationErrors.apiKey} />
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>
</div>
<style>
.container {
display: grid;
grid-gap: 40px;
}
</style>

View File

@ -0,0 +1,24 @@
<script>
import { Input } from "@budibase/bbui"
export let validationErrors
let blurred = { appName: false }
</script>
<h2>Create your first web app</h2>
<div class="container">
<Input
on:input={() => (blurred.appName = true)}
label="Web app name"
name="applicationName"
placeholder="Enter name of your web application"
type="name"
error={blurred.appName && validationErrors.applicationName} />
</div>
<style>
.container {
display: grid;
grid-gap: 40px;
}
</style>

View File

@ -0,0 +1,35 @@
<script>
import { Input, Select } from "@budibase/bbui"
export let validationErrors
let blurred = { username: false, password: false }
</script>
<h2>Create new user</h2>
<div class="container">
<Input
on:input={() => (blurred.username = true)}
label="Username"
name="username"
placeholder="Username"
type="name"
error={blurred.username && validationErrors.username} />
<Input
on:input={() => (blurred.password = true)}
label="Password"
name="password"
placeholder="Password"
type="pasword"
error={blurred.password && validationErrors.password} />
<Select name="accessLevelId">
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</Select>
</div>
<style>
.container {
display: grid;
grid-gap: 40px;
}
</style>

View File

@ -0,0 +1,3 @@
export { default as API } from "./API.svelte"
export { default as Info } from "./Info.svelte"
export { default as User } from "./User.svelte"

View File

@ -25,103 +25,105 @@
name: "Screen Placeholder",
route: "*",
props: {
"_id": "screenslot-placeholder",
"_component": "@budibase/standard-components/container",
"_styles": {
"normal": {},
"hover": {},
"active": {},
"selected": {}
_id: "screenslot-placeholder",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
"_code": "",
"className": "",
"onLoad": [],
"type": "div",
"_children": [
_code: "",
className: "",
onLoad: [],
type: "div",
_children: [
{
"_id": "51a1b494-0fa4-49c3-90cc-c2a6c7a3f888",
"_component": "@budibase/standard-components/container",
"_styles": {
"normal": {
"display": "flex",
"flex-direction": "column",
"align-items": "center"
_id: "51a1b494-0fa4-49c3-90cc-c2a6c7a3f888",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "column",
"align-items": "center",
},
hover: {},
active: {},
selected: {},
},
"hover": {},
"active": {},
"selected": {}
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
_instanceName: "Container",
_children: [
{
_id: "90a52cd0-f215-46c1-b29b-e28f9e7edf72",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {
width: "500px",
padding: "8px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
text: "Screen Slot",
type: "h1",
_instanceId: "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
_instanceName: "Heading",
_children: [],
},
{
_id: "71a3da65-72c6-4c43-8c6a-49871c07b77d",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
"max-width": "",
"text-align": "left",
width: "500px",
padding: "8px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text:
"The screens that you create will be displayed inside this box.",
type: "none",
_instanceId: "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
_instanceName: "Text",
},
{
_id: "8af80374-460d-497b-a5d8-7dd2ec4a7bbc",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
"max-width": "",
"text-align": "left",
width: "500px",
padding: "8px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text:
"This box is just a placeholder, to show you the position of screens.",
type: "none",
_instanceId: "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
_instanceName: "Text",
},
],
},
"_code": "",
"className": "",
"onLoad": [],
"type": "div",
"_instanceId": "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
"_instanceName": "Container",
"_children": [
{
"_id": "90a52cd0-f215-46c1-b29b-e28f9e7edf72",
"_component": "@budibase/standard-components/heading",
"_styles": {
"normal": {
"width": "500px",
"padding": "8px"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"className": "",
"text": "Screenslot",
"type": "h1",
"_instanceId": "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
"_instanceName": "Heading",
"_children": []
},
{
"_id": "71a3da65-72c6-4c43-8c6a-49871c07b77d",
"_component": "@budibase/standard-components/text",
"_styles": {
"normal": {
"max-width": "",
"text-align": "left",
"width": "500px",
"padding": "8px"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"text": "The screens that you create will be displayed inside this box.",
"type": "none",
"_instanceId": "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
"_instanceName": "Text"
},
{
"_id": "8af80374-460d-497b-a5d8-7dd2ec4a7bbc",
"_component": "@budibase/standard-components/text",
"_styles": {
"normal": {
"max-width": "",
"text-align": "left",
"width": "500px",
"padding": "8px"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"text": "This box is just a placeholder, to show you the position of screens.",
"type": "none",
"_instanceId": "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
"_instanceName": "Text"
}
]
}
],
"_instanceName": "Content Placeholder"
_instanceName: "Content Placeholder",
},
}

View File

@ -5,7 +5,8 @@ export default `<html>
<style>
body, html {
height: 100%!important;
font-family: Roboto !important;
font-family: Inter !important;
margin: 0px!important;
}
*, *:before, *:after {
box-sizing: border-box;
@ -26,13 +27,21 @@ export default `<html>
}
.container-screenslot-placeholder {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
border-style: dashed !important;
border-width: 1px;
color: #806fde;
background: #e9e6ff44;
height: 100%;
color: #000000;
background: #fafafa;
height: 94%;
}
.container-screenslot-placeholder span {
display: block;
margin-bottom: 10px;
}
</style>
<script src='/assets/budibase-client.js'></script>

View File

@ -1,184 +1,241 @@
<script>
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 { 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 {
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";
import Portal from "./Portal.svelte";
import { keyevents } from "../actions";
convertHsvaToFormat,
} from "./utils.js"
import Slider from "./Slider.svelte"
import Palette from "./Palette.svelte"
import ButtonGroup from "./ButtonGroup.svelte"
import Input from "./Input.svelte"
import Portal from "./Portal.svelte"
export let value = "#3ec1d3ff";
export let open = false;
export let swatches = [];
export let value = "#3ec1d3ff"
export let open = false
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console
export let disableSwatches = false
export let format = "hexa"
export let style = ""
export let pickerHeight = 0
export let pickerWidth = 0
export let disableSwatches = false;
export let format = "hexa";
export let style = "";
export let pickerHeight = 0;
export let pickerWidth = 0;
let colorPicker = null
let adder = null
let colorPicker = null;
let adder = null;
let swatchesSetFromLocalStore = false;
let h = null
let s = null
let v = null
let a = null
let h = 0;
let s = 0;
let v = 0;
let a = 0;
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
onMount(() => {
if (!swatches.length > 0) {
//Don't use locally stored recent colors if swatches have been passed as props
swatchesSetFromLocalStore = true;
swatches = getRecentColors() || [];
}
if (swatches.length > 12) {
console.warn(
`Colorpicker - ${swatches.length} swatches were provided. Only the first 12 swatches will be displayed.`
);
getRecentColors()
}
if (colorPicker) {
colorPicker.focus();
colorPicker.focus()
}
if (format) {
convertAndSetHSVA();
convertAndSetHSVA()
}
});
})
function getRecentColors() {
let colorStore = localStorage.getItem("cp:recent-colors");
let colorStore = localStorage.getItem("cp:recent-colors")
if (colorStore) {
return JSON.parse(colorStore);
swatches = JSON.parse(colorStore)
}
}
function handleEscape() {
if (open) {
open = false;
function handleEscape(e) {
if (open && e.key === "Escape") {
open = false
}
}
function setRecentColors(color) {
const s = swatchesSetFromLocalStore
? swatches
: [...getRecentColors(), color];
localStorage.setItem("cp:recent-colors", JSON.stringify(s));
function setRecentColor(color) {
if (swatches.length === 12) {
swatches.splice(0, 1)
}
if (!swatches.includes(color)) {
swatches = [...swatches, color]
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
}
}
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);
dispatchValue();
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);
h = color
value = convertHsvaToFormat([h, s, v, a], format)
if (!isDrag) {
dispatchValue();
dispatchValue()
}
}
function setAlpha({ color, isDrag }) {
a = color === "1.00" ? "1" : color;
value = convertHsvaToFormat([h, s, v, a], format);
a = color === "1.00" ? "1" : color
value = convertHsvaToFormat([h, s, v, a], format)
if (!isDrag) {
dispatchValue();
dispatchValue()
}
}
function dispatchValue() {
dispatch("change", value);
dispatch("change", value)
}
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);
let format = getColorFormat(text)
if (format) {
value = text;
convertAndSetHSVA();
value = text
convertAndSetHSVA()
}
}
function dispatchInputChange() {
if (format) {
dispatchValue();
dispatchValue()
}
}
function addSwatch() {
if (format) {
if (swatches.length === 12) {
swatches.splice(0, 1);
}
if (!swatches.includes(value)) {
swatches = [...swatches, value];
setRecentColors(value);
}
dispatch("addswatch", value);
dispatch("addswatch", value)
setRecentColor(value)
}
}
function removeSwatch(idx) {
let [removedSwatch] = swatches.splice(idx, 1);
swatches = swatches;
dispatch("removeswatch", removedSwatch);
if (swatchesSetFromLocalStore) {
//as could be a swatch not present in local storage
setRecentColors();
}
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) {
format = getColorFormat(color);
format = getColorFormat(color)
if (format) {
value = color;
convertAndSetHSVA();
dispatchValue();
value = color
convertAndSetHSVA()
dispatchValue()
}
}
}
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : "";
$: selectedColorStyle = buildStyle({ background: value, border });
$: hasSwatches = swatches.length > 0;
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : ""
$: selectedColorStyle = buildStyle({ background: value, border })
$: shrink = swatches.length > 0
</script>
<Portal>
<div
class="colorpicker-container"
transition:fade
bind:this={colorPicker}
{style}
tabindex="0"
on:keydown={handleEscape}
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={selectedColorStyle} />
</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>
</Portal>
<style>
.colorpicker-container {
position: absolute;
@ -187,7 +244,6 @@
display: flex;
font-size: 11px;
font-weight: 400;
transition: top 0.1s, left 0.1s;
flex-direction: column;
margin: 5px 0px;
height: auto;
@ -239,7 +295,7 @@
flex: 1;
height: 20px;
display: flex;
transition: flex 0.3s;
transition: flex 0.5s;
justify-content: center;
align-items: center;
background: #f1f3f4;
@ -249,8 +305,6 @@
margin-left: 5px;
margin-top: 3px;
font-weight: 500;
outline-color: #003cb0;
outline-width: thin;
}
.shrink {
@ -264,86 +318,3 @@
padding-top: 3px;
}
</style>
<Portal>
<div
class="colorpicker-container"
use:keyevents={{ Escape: handleEscape }}
transition:fade
bind:this={colorPicker}
{style}
tabindex="0"
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"
title={value}
style={selectedColorStyle} />
</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 hasSwatches}
{#each swatches as color, idx}
{#if idx < 12}
<Swatch
{color}
on:click={() => applySwatch(color)}
on:removeswatch={() => removeSwatch(idx)} />
{/if}
{/each}
{/if}
{#if swatches.length < 12}
<div
tabindex="0"
title="Add Swatch"
use:keyevents={{ Enter: addSwatch }}
bind:this={adder}
transition:fade
class="adder"
class:shrink={hasSwatches}
on:click={addSwatch}>
<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>
</Portal>

View File

@ -0,0 +1,157 @@
<script>
import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import { createEventDispatcher, beforeUpdate } from "svelte"
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 width = "25px"
export let height = "25px"
let format = "hexa"
let dimensions = { top: 0, bottom: 0, right: 0, left: 0 }
let positionSide = "top"
let colorPreview = null
let previewHeight = null
let previewWidth = null
let pickerWidth = 0
let pickerHeight = 0
let errorMsg = null
const dispatch = createEventDispatcher()
beforeUpdate(() => {
format = getColorFormat(value)
if (!format) {
errorMsg = `Colorpicker - ${value} is an unknown color format. Please use a hex, rgb or hsl value`
console.error(errorMsg)
} else {
errorMsg = null
}
})
function openColorpicker(event) {
if (colorPreview) {
open = true
}
}
function onColorChange(color) {
value = color.detail
dispatch("change", color.detail)
}
$: if (open && colorPreview) {
const {
top: spaceAbove,
width,
bottom,
right,
left,
} = colorPreview.getBoundingClientRect()
const spaceBelow = window.innerHeight - bottom
const previewCenter = previewWidth / 2
let y, x
if (spaceAbove > spaceBelow) {
positionSide = "bottom"
y = window.innerHeight - spaceAbove
} else {
positionSide = "top"
y = bottom
}
x = left + previewCenter - pickerWidth / 2
dimensions = { [positionSide]: y.toFixed(1), left: x.toFixed(1) }
}
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
[positionSide]: `${dimensions[positionSide]}px`,
left: `${dimensions.left}px`,
})
</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} />
</CheckedBackground>
{#if open}
<Colorpicker
style={pickerStyle}
on:change={onColorChange}
on:addswatch
on:removeswatch
bind:format
bind:value
bind:pickerHeight
bind:pickerWidth
bind:open
{swatches}
{disableSwatches} />
<div on:click|self={() => (open = false)} class="overlay" />
{/if}
{:else}
<div class="color-preview preview-error" style={errorPreviewStyle}>
<span>&times;</span>
</div>
{/if}
</div>
<style>
.color-preview-container {
display: flex;
flex-flow: row nowrap;
height: fit-content;
}
.color-preview {
cursor: pointer;
border-radius: 3px;
border: 1px solid #dedada;
}
.preview-error {
background: #cccccc;
color: #808080;
text-align: center;
font-size: 18px;
cursor: not-allowed;
}
/* .picker-container {
position: absolute;
z-index: 3;
width: fit-content;
height: fit-content;
} */
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
}
</style>

View File

@ -1,23 +0,0 @@
export default function(node) {
function handleMouseDown() {
window.addEventListener("mousemove", handleMouseMove)
window.addEventListener("mouseup", handleMouseUp)
}
function handleMouseMove(event) {
let mouseX = event.clientX
node.dispatchEvent(
new CustomEvent("drag", {
detail: mouseX,
})
)
}
function handleMouseUp() {
window.removeEventListener("mousedown", handleMouseDown)
window.removeEventListener("mousemove", handleMouseMove)
node.dispatchEvent(new CustomEvent("dragend"))
}
node.addEventListener("mousedown", handleMouseDown)
}

View File

@ -1,2 +0,0 @@
export { default as drag } from "./drag.js"
export { default as keyevents } from "./key-events.js"

View File

@ -1,41 +0,0 @@
//events: Array<{trigger: fn}>
export default function(node, events = []) {
const ev = Object.entries(events)
let fns = []
for (let [trigger, fn] of ev) {
let f = addEvent(trigger, fn)
fns = [...fns, f]
}
function _scaffold(trigger, fn) {
return () => {
let trig = parseInt(trigger)
if (trig) {
if (event.keyCode === trig) {
fn(event)
}
} else {
if (event.key === trigger) {
fn(event)
}
}
}
}
function addEvent(trigger, fn) {
let f = _scaffold(trigger, fn)
node.addEventListener("keydown", f)
return f
}
function removeEvents() {
fns.forEach(f => node.removeEventListener("keypress", f))
}
return {
destroy() {
removeEvents()
},
}
}

View File

@ -1,29 +0,0 @@
<script>
import FlatButton from "./FlatButton.svelte"
export let format = "hex"
export let onclick = format => {}
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;
display: flex;
flex-flow: row nowrap;
justify-content: center;
width: 170px;
height: 30px;
align-self: center;
}
</style>

View File

@ -1,26 +0,0 @@
<script>
import { buildStyle } from "../helpers.js"
import { fade } from "svelte/transition"
export let backgroundSize = "10px"
export let borderRadius = ""
export let height = ""
export let width = ""
export let 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>');
height: fit-content;
width: fit-content;
width: -moz-fit-content;
height: -moz-fit-content;
}
</style>

View File

@ -1,175 +0,0 @@
<script>
import Colorpicker from "./Colorpicker.svelte";
import CheckedBackground from "./CheckedBackground.svelte";
import { createEventDispatcher, beforeUpdate, onMount } from "svelte";
import { buildStyle, debounce } 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 width = "25px";
export let height = "25px";
let format = "hexa";
let dimensions = { top: 0, bottom: 0, right: 0, left: 0 };
let positionY = "top";
let positionX = "left";
let colorPreview = null;
let previewHeight = null;
let previewWidth = null;
let pickerWidth = 0;
let pickerHeight = 0;
let errorMsg = null;
const dispatch = createEventDispatcher();
beforeUpdate(() => {
format = getColorFormat(value);
if (!format) {
errorMsg = `Colorpicker - ${value} is an unknown color format. Please use a hex, rgb or hsl value`;
console.error(errorMsg);
} else {
errorMsg = null;
}
});
function openColorpicker(event) {
if (colorPreview) {
open = true;
}
}
function onColorChange(color) {
value = color.detail;
dispatch("change", color.detail);
}
function calculateDimensions() {
if (open) {
const {
top: spaceAbove,
width,
bottom,
right,
left
} = colorPreview.getBoundingClientRect();
const spaceBelow = window.innerHeight - bottom;
const previewCenter = previewWidth / 2;
let y, x;
if (spaceAbove > spaceBelow) {
positionY = "bottom";
y = window.innerHeight - spaceAbove;
} else {
positionY = "top";
y = bottom;
}
// Centre picker by default
x = left + previewCenter - 220 / 2;
const spaceRight = window.innerWidth - right;
//Position picker left or right if space not available for centering
if (left < 110 && spaceRight > 220) {
positionX = "left";
x = right;
} else if (spaceRight < 100 && left > 220) {
positionX = "right";
x = document.body.clientWidth - left;
}
dimensions = { [positionY]: y.toFixed(1), [positionX]: x.toFixed(1) };
}
}
$: if (open && colorPreview) {
calculateDimensions();
}
$: previewStyle = buildStyle({ width, height, background: value });
$: errorPreviewStyle = buildStyle({ width, height });
$: pickerStyle = buildStyle({
[positionY]: `${dimensions[positionY]}px`,
[positionX]: `${dimensions[positionX]}px`
});
</script>
<style>
.color-preview-container {
display: flex;
flex-flow: row nowrap;
height: fit-content;
}
.color-preview {
cursor: pointer;
border-radius: 3px;
border: 1px solid #dedada;
}
.preview-error {
background: #cccccc;
color: #808080;
text-align: center;
font-size: 18px;
cursor: not-allowed;
}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
}
</style>
<svelte:window on:resize={debounce(calculateDimensions, 200)} />
<div class="color-preview-container">
{#if !errorMsg}
<CheckedBackground borderRadius="3px" backgroundSize="8px">
<div
bind:this={colorPreview}
bind:clientHeight={previewHeight}
bind:clientWidth={previewWidth}
title={value}
class="color-preview"
style={previewStyle}
on:click={openColorpicker} />
</CheckedBackground>
{#if open}
<Colorpicker
style={pickerStyle}
on:change={onColorChange}
on:addswatch
on:removeswatch
bind:format
bind:value
bind:pickerHeight
bind:pickerWidth
bind:open
{swatches}
{disableSwatches} />
<div on:click|self={() => (open = false)} class="overlay" />
{/if}
{:else}
<div
class="color-preview preview-error"
title="Invalid Color"
style={errorPreviewStyle}>
<span>&times;</span>
</div>
{/if}
</div>

View File

@ -1,37 +0,0 @@
<script>
import {createEventDispatcher} from "svelte"
import {keyevents} from "../actions"
export let text = ""
export let selected = false
const dispatch = createEventDispatcher()
</script>
<div class="flatbutton" tabindex="0" use:keyevents={{"Enter": () => dispatch("click")}} class:selected on:click>{text}</div>
<style>
.flatbutton {
cursor: pointer;
border: 1px solid #d4d4d4;
border-radius: 8px;
text-transform: uppercase;
margin: 5px;
transition: all 0.3s;
font-size: 10px;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background: #f1f3f4;
outline-color: #003cb0;
outline-width: thin;
}
.selected {
color: #ffffff;
background-color: #003cb0;
border: none;
outline: none;
}
</style>

View File

@ -1,28 +0,0 @@
<script>
export let value = ""
</script>
<div>
<input on:input on:change type="text" {value} maxlength="25" />
</div>
<style>
div {
display: flex;
justify-content: center;
margin: 5px 0px;
}
input {
width: 175px;
font-size: 13px;
background: #f1f3f4;
border-radius: 8px;
height: 20px;
outline-color: #003cb0;
color: inherit;
text-align: center;
border: 1px solid #dadada;
font-weight: 550;
}
</style>

View File

@ -1,73 +0,0 @@
<script>
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
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
) {
let s = (clickX / paletteWidth) * 100
let v = 100 - (clickY / paletteHeight) * 100
dispatch("change", { s, v })
}
}
$: 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};`
$: 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;
width: 100%;
height: 140px;
cursor: crosshair;
overflow: hidden;
}
.picker {
position: absolute;
width: 10px;
height: 10px;
background: transparent;
border: 2px solid white;
border-radius: 50%;
}
</style>

View File

@ -1,111 +0,0 @@
<script>
import { onMount, createEventDispatcher } from "svelte"
import {drag, keyevents} from "../actions"
export let value = 1
export let type = "hue"
const dispatch = createEventDispatcher()
let slider
let sliderWidth = 0
let upperLimit = type === "hue" ? 360 : 1
let incrementFactor = type === "hue" ? 1 : 0.01
const isWithinLimit = value => value >= 0 && value <= upperLimit
function onSliderChange(mouseX, isDrag = false) {
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
dispatch("change", { color: value, isDrag })
}
}
function handleLeftKey() {
let v = value - incrementFactor
if(isWithinLimit(v)) {
value = v
dispatch("change", { color: value })
}
}
function handleRightKey() {
let v = value + incrementFactor
if(isWithinLimit(v)) {
value = v
dispatch("change", { color: value })
}
}
$: thumbPosition =
type === "hue" ? sliderWidth * (value / 360) : sliderWidth * value
$: style = `transform: translateX(${thumbPosition - 6}px);`
</script>
<div
tabindex="0"
bind:this={slider}
use:keyevents={{37: handleLeftKey, 39: handleRightKey}}
bind:clientWidth={sliderWidth}
on:click={event => onSliderChange(event.clientX)}
class="color-format-slider"
class:hue={type === 'hue'}
class:alpha={type === 'alpha'}>
<div
use:drag
on:drag={e => onSliderChange(e.detail, true)}
on:dragend
class="slider-thumb"
{style} />
</div>
<style>
.color-format-slider {
position: relative;
align-self: center;
height: 8px;
width: 150px;
border-radius: 10px;
margin: 10px 0px;
border: 1px solid #e8e8ef;
cursor: pointer;
outline-color: #003cb0;
outline-width: thin;
}
.hue {
background: linear-gradient(
to right,
hsl(0, 100%, 50%),
hsl(60, 100%, 50%),
hsl(120, 100%, 50%),
hsl(180, 100%, 50%),
hsl(240, 100%, 50%),
hsl(300, 100%, 50%),
hsl(360, 100%, 50%)
);
}
.alpha {
background: linear-gradient(to right, transparent, rgb(0 0 0));
}
.slider-thumb {
position: absolute;
bottom: -3px;
height: 12px;
width: 12px;
border: 1px solid #777676;
border-radius: 50%;
background-color: #ffffff;
cursor: grab;
}
</style>

View File

@ -1,68 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
import CheckedBackground from "./CheckedBackground.svelte";
import { keyevents } from "../actions";
export let hovered = false;
export let color = "#fff";
const dispatch = createEventDispatcher();
</script>
<style>
.swatch {
position: relative;
cursor: pointer;
border-radius: 6px;
border: 1px solid #dedada;
height: 20px;
width: 20px;
outline-color: #003cb0;
outline-width: thin;
}
.space {
padding: 3px 5px;
}
span {
cursor: pointer;
position: absolute;
top: -5px;
right: -4px;
background: #800000;
color: white;
font-size: 12px;
border-radius: 50%;
text-align: center;
width: 13px;
height: 13px;
}
span:after {
content: "\00d7";
position: relative;
left: 0;
bottom: 3px;
}
</style>
<div class="space">
<CheckedBackground borderRadius="6px">
<div
tabindex="0"
use:keyevents={{ Enter: () => dispatch('click') }}
in:fade
title={color}
class="swatch"
style={`background: ${color};`}
on:mouseover={() => (hovered = true)}
on:mouseleave={() => (hovered = false)}
on:click|self>
{#if hovered}
<span in:fade on:click={() => dispatch('removeswatch')} />
{/if}
</div>
</CheckedBackground>
</div>

View File

@ -1,20 +0,0 @@
export function buildStyle(styles) {
let str = ""
for (let s in styles) {
if (styles[s]) {
let key = convertCamel(s)
str += `${key}: ${styles[s]}; `
}
}
return str
}
export const convertCamel = str => {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
}
export const debounce = (fn, milliseconds) => {
return () => {
setTimeout(fn, milliseconds)
}
}

View File

@ -1,2 +0,0 @@
import Colorpreview from "./components/Colorpreview.svelte"
export default Colorpreview

View File

@ -1,281 +0,0 @@
export const isValidHex = str =>
/^#(?:[A-F0-9]{3}$|[A-F0-9]{4}$|[A-F0-9]{6}$|[A-F0-9]{8})$/gi.test(str)
const getHexaValues = hexString => {
if (hexString.length <= 5) {
let hexArr = hexString.match(/[A-F0-9]/gi)
let t = hexArr.map(c => (c += c))
return t
} else {
return hexString.match(/[A-F0-9]{2}/gi)
}
}
export const isValidRgb = str => {
const hasValidStructure = /^(?:rgba\(|rgb\()(?:[0-9,\s]|\.(?=\d))*\)$/gi.test(
str
)
if (hasValidStructure) {
return testRgbaValues(str.toLowerCase())
}
}
const findNonNumericChars = /[a-z()\s]/gi
export const getNumericValues = str =>
str
.replace(findNonNumericChars, "")
.split(",")
.map(v => (v !== "" ? v : undefined))
export const testRgbaValues = str => {
const rgba = getNumericValues(str)
const [r, g, b, a] = rgba
let isValidLengthRange =
(str.startsWith("rgb(") && rgba.length === 3) ||
(str.startsWith("rgba(") && rgba.length === 4)
let isValidColorRange = [r, g, b].every(v => v >= 0 && v <= 255)
let isValidAlphaRange = str.startsWith("rgba(")
? `${a}`.length <= 4 && a >= 0 && a <= 1
: true
return isValidLengthRange && isValidColorRange && isValidAlphaRange
}
export const isValidHsl = str => {
const hasValidStructure = /^(?:hsl\(|hsla\()(?:[0-9,%\s]|\.(?=\d))*\)$/gi.test(
str
)
if (hasValidStructure) {
return testHslaValues(str.toLowerCase())
}
}
export const testHslaValues = str => {
const hsla = getNumericValues(str)
const [h, s, l, a] = hsla
const isUndefined = [h, s, l].some(v => v === undefined)
if (isUndefined) return false
let isValidLengthRange =
(str.startsWith("hsl(") && hsla.length === 3) ||
(str.startsWith("hsla(") && hsla.length === 4)
let isValidHue = h >= 0 && h <= 360
let isValidSatLum = [s, l].every(
v => v.endsWith("%") && parseInt(v) >= 0 && parseInt(v) <= 100
)
let isValidAlphaRange = str.startsWith("hsla(")
? `${a}`.length <= 4 && a >= 0 && a <= 1
: true
return isValidLengthRange && isValidHue && isValidSatLum && isValidAlphaRange
}
export const getColorFormat = color => {
if (typeof color === "string") {
if (isValidHex(color)) {
return "hex"
} else if (isValidRgb(color)) {
return "rgb"
} else if (isValidHsl(color)) {
return "hsl"
}
}
}
export const convertToHSVA = (value, format) => {
switch (format) {
case "hex":
return getAndConvertHexa(value)
case "rgb":
return getAndConvertRgba(value)
case "hsl":
return getAndConvertHsla(value)
}
}
export const convertHsvaToFormat = (hsva, format) => {
switch (format) {
case "hex":
return hsvaToHexa(hsva, true)
case "rgb":
return hsvaToRgba(hsva, true)
case "hsl":
return hsvaToHsla(hsva)
}
}
export const getAndConvertHexa = color => {
let [rHex, gHex, bHex, aHex] = getHexaValues(color)
return hexaToHSVA([rHex, gHex, bHex], aHex)
}
export const getAndConvertRgba = color => {
let rgba = getNumericValues(color)
return rgbaToHSVA(rgba)
}
export const getAndConvertHsla = color => {
let hsla = getNumericValues(color)
return hslaToHSVA(hsla)
}
export const hexaToHSVA = (hex, alpha = "FF") => {
const rgba = hex
.map(v => parseInt(v, 16))
.concat(Number((parseInt(alpha, 16) / 255).toFixed(2)))
return rgbaToHSVA(rgba)
}
export const rgbaToHSVA = rgba => {
const [r, g, b, a = 1] = rgba
let hsv = _rgbToHSV([r, g, b])
return [...hsv, a].map(x => parseFloat(x))
}
export const hslaToHSVA = ([h, s, l, a = 1]) => {
let sat = s.replace(/%/, "")
let lum = l.replace(/%/, "")
let hsv = _hslToHSV([h, sat, lum])
return [...hsv, a].map(x => parseFloat(x))
}
export const hsvaToHexa = (hsva, asString = false) => {
const [r, g, b, a] = hsvaToRgba(hsva)
const padSingle = hex => (hex.length === 1 ? `0${hex}` : hex)
let hexa = [r, g, b].map(v => {
let hex = Math.round(v).toString(16)
return padSingle(hex)
})
let alpha = padSingle(Math.round(a * 255).toString(16))
hexa = [...hexa, alpha]
return asString ? `#${hexa.join("")}` : hexa
}
export const hsvaToRgba = ([h, s, v, a = 1], asString = false) => {
let rgb = _hsvToRgb([h, s, v]).map(x => Math.round(x))
let rgba = [...rgb, a < 1 ? _fixNum(a, 2) : a]
return asString ? `rgba(${rgba.join(",")})` : rgba
}
export const hsvaToHsla = ([h, s, v, a = 1]) => {
let [hue, sat, lum] = _hsvToHSL([h, s, v])
let hsla = [hue, sat + "%", lum + "%", a < 1 ? _fixNum(a, 2) : a]
return `hsla(${hsla.join(",")})`
}
export const _hslToHSV = hsl => {
const h = hsl[0]
let s = hsl[1] / 100
let l = hsl[2] / 100
let smin = s
const lmin = Math.max(l, 0.01)
l *= 2
s *= l <= 1 ? l : 2 - l
smin *= lmin <= 1 ? lmin : 2 - lmin
const v = (l + s) / 2
const sv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s)
return [h, sv * 100, v * 100]
}
//Credit : https://github.com/Qix-/color-convert
export const _rgbToHSV = rgb => {
let rdif
let gdif
let bdif
let h
let s
const r = rgb[0] / 255
const g = rgb[1] / 255
const b = rgb[2] / 255
const v = Math.max(r, g, b)
const diff = v - Math.min(r, g, b)
const diffc = function(c) {
return (v - c) / 6 / diff + 1 / 2
}
if (diff === 0) {
h = 0
s = 0
} else {
s = diff / v
rdif = diffc(r)
gdif = diffc(g)
bdif = diffc(b)
if (r === v) {
h = bdif - gdif
} else if (g === v) {
h = 1 / 3 + rdif - bdif
} else if (b === v) {
h = 2 / 3 + gdif - rdif
}
if (h < 0) {
h += 1
} else if (h > 1) {
h -= 1
}
}
const hsvResult = [h * 360, s * 100, v * 100].map(v => Math.round(v))
return hsvResult
}
//Credit : https://github.com/Qix-/color-convert
export const _hsvToRgb = hsv => {
const h = hsv[0] / 60
const s = hsv[1] / 100
let v = hsv[2] / 100
const hi = Math.floor(h) % 6
const f = h - Math.floor(h)
const p = 255 * v * (1 - s)
const q = 255 * v * (1 - s * f)
const t = 255 * v * (1 - s * (1 - f))
v *= 255
switch (hi) {
case 0:
return [v, t, p]
case 1:
return [q, v, p]
case 2:
return [p, v, t]
case 3:
return [p, q, v]
case 4:
return [t, p, v]
case 5:
return [v, p, q]
}
}
//Credit : https://github.com/Qix-/color-convert
export const _hsvToHSL = hsv => {
const h = hsv[0]
const s = hsv[1] / 100
const v = hsv[2] / 100
const vmin = Math.max(v, 0.01)
let sl
let l
l = (2 - s) * v
const lmin = (2 - s) * vmin
sl = s * vmin
sl /= lmin <= 1 ? lmin : 2 - lmin
sl = sl || 0
l /= 2
return [_fixNum(h, 0), _fixNum(sl * 100, 0), _fixNum(l * 100, 0)]
}
export const _fixNum = (value, decimalPlaces) =>
Number(parseFloat(value).toFixed(decimalPlaces))

View File

@ -1,106 +0,0 @@
import { getColorFormat, convertToHSVA, convertHsvaToFormat } from "./utils"
describe("convertToHSVA - convert to hsva from format", () => {
test("convert from hexa", () => {
expect(convertToHSVA("#f222d382", "hex")).toEqual([309, 86, 95, 0.51])
})
test("convert from hex", () => {
expect(convertToHSVA("#f222d3", "hex")).toEqual([309, 86, 95, 1])
})
test("convert from rgba", () => {
expect(convertToHSVA("rgba(242, 34, 211, 1)", "rgb")).toEqual([
309,
86,
95,
1,
])
})
test("convert from rgb", () => {
expect(convertToHSVA("rgb(150, 80, 255)", "rgb")).toEqual([264, 69, 100, 1])
})
test("convert from from hsl", () => {
expect(convertToHSVA("hsl(264, 100%, 65.7%)", "hsl")).toEqual([
264,
68.6,
100,
1,
])
})
test("convert from from hsla", () => {
expect(convertToHSVA("hsla(264, 100%, 65.7%, 0.51)", "hsl")).toEqual([
264,
68.6,
100,
0.51,
])
})
})
describe("convertHsvaToFormat - convert from hsva to format", () => {
test("Convert to hexa", () => {
expect(convertHsvaToFormat([264, 68.63, 100, 0.5], "hex")).toBe("#9650ff80")
})
test("Convert to rgba", () => {
expect(convertHsvaToFormat([264, 68.63, 100, 0.75], "rgb")).toBe(
"rgba(150,80,255,0.75)"
)
})
test("Convert to hsla", () => {
expect(convertHsvaToFormat([264, 68.63, 100, 1], "hsl")).toBe(
"hsla(264,100%,66%,1)"
)
})
})
describe("Get Color Format", () => {
test("Testing valid hex string", () => {
expect(getColorFormat("#FFF")).toBe("hex")
})
test("Testing invalid hex string", () => {
expect(getColorFormat("#FFZ")).toBeUndefined()
})
test("Testing valid hex with alpha", () => {
expect(getColorFormat("#FF00BB80")).toBe("hex")
})
test("Test valid rgb value", () => {
expect(getColorFormat("RGB(255, 20, 50)")).toBe("rgb")
})
test("Testing invalid rgb value", () => {
expect(getColorFormat("rgb(255, 0)")).toBeUndefined()
})
test("Testing rgb value with alpha", () => {
expect(getColorFormat("rgba(255, 0, 50, 0.5)")).toBe("rgb")
})
test("Testing rgb value with incorrectly provided alpha", () => {
expect(getColorFormat("rgb(255, 0, 50, 0.5)")).toBeUndefined()
})
test("Testing invalid hsl value", () => {
expect(getColorFormat("hsla(255, 0)")).toBeUndefined()
})
test("Testing hsla value with alpha", () => {
expect(getColorFormat("hsla(150, 60%, 50%, 0.5)")).toBe("hsl")
})
test("Testing hsl value with incorrectly provided alpha", () => {
expect(getColorFormat("hsl(150, 0, 50, 0.5)")).toBeUndefined()
})
test("Testing out of bounds hsl", () => {
expect(getColorFormat("hsl(375, 0, 50)")).toBeUndefined()
})
})

View File

@ -23,8 +23,8 @@
let codeEditor
let flattenedPanel = flattenComponents(panelStructure.categories)
let categories = [
{ value: "design", name: "Design" },
{ value: "settings", name: "Settings" },
{ value: "design", name: "Design" },
{ value: "events", name: "Events" },
]
let selectedCategory = categories[0]
@ -99,9 +99,7 @@
{selectedCategory} />
{#if displayName}
<div class="instance-name">
<strong>{componentInstance._instanceName}</strong>
</div>
<div class="instance-name">{componentInstance._instanceName}</div>
{/if}
<div class="component-props-container">
@ -142,14 +140,16 @@
}
.component-props-container {
margin-top: 10px;
margin-top: 16px;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
.instance-name {
margin-top: 10px;
font-size: 12px;
margin-top: 20px;
font-size: 14px;
font-weight: 500;
color: var(--grey-7);
}
</style>

View File

@ -61,8 +61,9 @@
<style>
.panel {
padding: 20px 0px;
display: flex;
flex-wrap: wrap;
margin-top: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
}
</style>

View File

@ -6,6 +6,7 @@
import { pipe } from "components/common/core"
import { store } from "builderStore"
import { ArrowDownIcon, ShapeIcon } from "components/common/Icons/"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
export let screens = []
@ -32,7 +33,7 @@
{#each screens as screen}
<div
class="budibase__nav-item component"
class="budibase__nav-item screen-header-row"
class:selected={$store.currentComponentInfo._id === screen.props._id}
on:click|stopPropagation={() => changeScreen(screen)}>
@ -47,6 +48,10 @@
<i class="ri-artboard-2-fill icon" />
<span class="title">{screen.props._instanceName}</span>
<div class="dropdown-menu">
<ScreenDropdownMenu {screen} />
</div>
</div>
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children}
@ -64,10 +69,16 @@
color: var(--ink);
}
.screen-header-row {
display: flex;
flex-direction: row;
}
.title {
margin-left: 14px;
font-size: 14px;
font-weight: 400;
flex: 1;
}
.icon {
@ -90,4 +101,20 @@
.rotate :global(svg) {
transform: rotate(-90deg);
}
.dropdown-menu {
display: none;
height: 24px;
width: 24px;
color: var(--ink);
padding: 0px 5px;
border-style: none;
background: rgba(0, 0, 0, 0);
cursor: pointer;
position: relative;
}
.budibase__nav-item:hover .dropdown-menu {
display: block;
}
</style>

View File

@ -42,7 +42,7 @@
class:selected={currentComponent === component}
style="padding-left: {level * 20 + 40}px">
<div class="nav-item">
<i class="icon ri-arrow-right-circle-fill" />
<i class="icon ri-arrow-right-circle-line" />
{isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
</div>
<div class="actions">
@ -73,7 +73,7 @@
grid-template-columns: 1fr auto auto auto;
padding: 0px 5px 0px 15px;
margin: auto 0px;
border-radius: 3px;
border-radius: 5px;
height: 36px;
align-items: center;
}

View File

@ -21,21 +21,17 @@
display: flex;
flex-direction: column;
cursor: pointer;
margin-bottom: 8px;
padding: 8px 0px 16px 0px;
width: 110px;
padding: 12px 16px 16px 16px;
height: 80px;
justify-content: center;
align-items: center;
margin-right: 8px;
background-color: var(--grey-1);
border-radius: 3px;
border-radius: 5px;
}
.item-item:hover {
background: var(--grey-2);
border-radius: 3px;
transition: all 0.2s;
transition: all 0.3s;
}
.item-icon {
@ -51,6 +47,7 @@
.item-name {
font-size: 14px;
font-weight: 400;
text-align: center;
}
i {

View File

@ -23,21 +23,26 @@
{#if !list.isCategory}
<button class="back-button" on:click={() => (list = category)}>Back</button>
{/if}
{#each list.children as item}
<Item {item} on:click={() => handleClick(item)} />
{/each}
<style>
.back-button {
font-size: 16px;
width: 100%;
grid-column: 1 / span 2;
font-size: 14px;
text-align: center;
height: 40px;
border-radius: 3px;
border: solid 1px #e8e8ef;
height: 36px;
border-radius: 5px;
border: solid 1px var(--grey-3);
background: white;
margin-bottom: 20px;
cursor: pointer;
font-weight: 500;
font-family: Inter;
transition: all 0.3ms;
}
.back-button:hover {
background: var(--grey-1);
}
</style>

View File

@ -53,7 +53,6 @@
}
.label {
flex: 0 0 50px;
display: flex;
align-items: center;
font-size: 12px;

View File

@ -0,0 +1,112 @@
<script>
import { MoreIcon } from "components/common/Icons"
import { store } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UIkit from "uikit"
import api from "builderStore/api"
import Portal from "svelte-portal"
import { DropdownMenu } from "@budibase/bbui"
export let screen
let confirmDeleteDialog
let dropdown
let buttonForDropdown
const hideDropdown = () => {
dropdown.hide()
}
const deleteScreen = () => {
store.update(s => {
const screens = s.screens.filter(c => c.name !== screen.name)
s.screens = screens
if (s.currentPreviewItem.name === screen.name) {
s.currentPreviewItem = s.pages[s.currentPageName]
s.currentFrontEndType = "page"
}
api.delete(
`/_builder/api/pages/${s.currentPageName}/screens/${screen.name}`
)
return s
})
}
</script>
<div class="root boundary" on:click|stopPropagation={() => {}}>
<button on:click={() => dropdown.show()} bind:this={buttonForDropdown}>
<MoreIcon />
</button>
<DropdownMenu bind:this={dropdown} anchor={buttonForDropdown}>
<ul class="menu" on:click={hideDropdown}>
<li class="item" on:click={() => confirmDeleteDialog.show()}>
<i class="icon ri-delete-bin-2-line" />
Delete
</li>
</ul>
</DropdownMenu>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete the screen '${screen.props._instanceName}' ?`}
okText="Delete Screen"
onOk={deleteScreen} />
<style>
.root {
overflow: hidden;
z-index: 9;
}
.root button {
border-style: none;
border-radius: 2px;
padding: 5px;
background: transparent;
cursor: pointer;
color: var(--ink);
outline: none;
}
.menu {
z-index: 100000;
overflow: visible;
padding: 12px 0px;
border-radius: 5px;
margin: 0;
}
.menu li {
border-style: none;
background-color: transparent;
list-style-type: none;
padding: 4px 16px;
margin: 0;
width: 100%;
box-sizing: border-box;
}
.item {
display: flex;
align-items: center;
font-size: 14px;
}
.icon {
margin-right: 8px;
}
.menu li:not(.disabled) {
cursor: pointer;
color: var(--grey-7);
}
.menu li:not(.disabled):hover {
color: var(--ink);
background-color: var(--grey-1);
}
</style>

View File

@ -2,7 +2,6 @@
import PropertyControl from "./PropertyControl.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte"
import Input from "../common/Input.svelte"
import Colorpicker from "../common/Colorpicker.svelte"
import { goto } from "@sveltech/routify"
import { excludeProps } from "./propertyCategories.js"

View File

@ -2,7 +2,6 @@
import { backendUiStore } from "builderStore"
import IconButton from "../common/IconButton.svelte"
import Input from "../common/Input.svelte"
import Colorpicker from "../common/Colorpicker.svelte"
export let value = ""
export let onChanged = () => {}

View File

@ -1,7 +1,7 @@
import Input from "../common/Input.svelte"
import OptionSelect from "./OptionSelect.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte"
import Colorpicker from "./Colorpicker"
import Colorpicker from "@budibase/colorpicker"
/*
TODO: Allow for default values for all properties
*/
@ -85,6 +85,8 @@ export const margin = [
{ label: "20px", value: "20px" },
{ label: "32px", value: "32px" },
{ label: "64px", value: "64px" },
{ label: "128px", value: "128px" },
{ label: "256px", value: "256px" },
{ label: "Auto", value: "auto" },
{ label: "100%", value: "100%" },
],
@ -101,6 +103,8 @@ export const margin = [
{ label: "20px", value: "20px" },
{ label: "32px", value: "32px" },
{ label: "64px", value: "64px" },
{ label: "128px", value: "128px" },
{ label: "256px", value: "256px" },
{ label: "Auto", value: "auto" },
{ label: "100%", value: "100%" },
],
@ -133,6 +137,8 @@ export const margin = [
{ label: "20px", value: "20px" },
{ label: "32px", value: "32px" },
{ label: "64px", value: "64px" },
{ label: "128px", value: "128px" },
{ label: "256px", value: "256px" },
{ label: "Auto", value: "auto" },
{ label: "100%", value: "100%" },
],
@ -149,6 +155,8 @@ export const margin = [
{ label: "20px", value: "20px" },
{ label: "32px", value: "32px" },
{ label: "64px", value: "64px" },
{ label: "128px", value: "128px" },
{ label: "256px", value: "256px" },
{ label: "Auto", value: "auto" },
{ label: "100%", value: "100%" },
],

View File

@ -16,23 +16,11 @@ export default {
name: "Basic",
isCategory: true,
children: [
{
_component: "@budibase/standard-components/embed",
icon: "ri-code-line",
name: "Embed",
description: "Embed content from 3rd party sources",
properties: {
design: {
...all,
},
settings: [{ label: "Embed", key: "embed", control: Input }],
},
},
{
_component: "@budibase/standard-components/container",
name: "Container",
description: "This component contains things within itself",
icon: "ri-layout-row-fill",
icon: "ri-layout-row-line",
commonProps: {},
children: [],
properties: {
@ -61,10 +49,22 @@ export default {
],
},
},
{
_component: "@budibase/standard-components/embed",
icon: "ri-code-line",
name: "Embed",
description: "Embed content from 3rd party sources",
properties: {
design: {
...all,
},
settings: [{ label: "Embed", key: "embed", control: Input }],
},
},
{
name: "Text",
description: "This is a simple text component",
icon: "ri-t-box-fill",
icon: "ri-t-box-line",
commonProps: {},
children: [
{
@ -128,7 +128,7 @@ export default {
{
name: "Input",
description: "These components handle user input.",
icon: "ri-edit-box-fill",
icon: "ri-edit-box-line",
commonProps: {},
children: [
{
@ -136,7 +136,7 @@ export default {
name: "Textfield",
description:
"A textfield component that allows the user to input text.",
icon: "ri-edit-box-fill",
icon: "ri-edit-box-line",
properties: {
design: { ...all },
settings: [
@ -154,7 +154,7 @@ export default {
_component: "@budibase/standard-components/checkbox",
name: "Checkbox",
description: "A selectable checkbox component",
icon: "ri-checkbox-fill",
icon: "ri-checkbox-line",
properties: {
design: { ...all },
settings: [{ label: "Label", key: "label", control: Input }],
@ -175,7 +175,7 @@ export default {
name: "Select",
description:
"A select component for choosing from different options",
icon: "ri-file-list-fill",
icon: "ri-file-list-line",
properties: {
design: { ...all },
settings: [],
@ -187,7 +187,7 @@ export default {
_component: "@budibase/standard-components/button",
name: "Button",
description: "A basic html button that is ready for styling",
icon: "ri-radio-button-fill",
icon: "ri-share-box-line",
children: [],
properties: {
design: {
@ -208,23 +208,23 @@ export default {
_component: "@budibase/standard-components/image",
name: "Image",
description: "A basic component for displaying images",
icon: "ri-image-fill",
icon: "ri-image-line",
children: [],
properties: {
design: { ...all },
settings: [{ label: "URL", key: "url", control: Input }],
},
},
{
_component: "@budibase/standard-components/icon",
name: "Icon",
description: "A basic component for displaying icons",
icon: "ri-sun-fill",
children: [],
properties: {
design: { ...all },
},
},
// {
// _component: "@budibase/standard-components/icon",
// name: "Icon",
// description: "A basic component for displaying icons",
// icon: "ri-sun-fill",
// children: [],
// properties: {
// design: { ...all },
// },
// },
{
_component: "@budibase/standard-components/link",
name: "Link",
@ -251,12 +251,72 @@ export default {
name: "Blocks",
isCategory: true,
children: [
{
name: "List",
_component: "@budibase/standard-components/list",
description: "Renders all children once per record, of a given table",
icon: "ri-file-list-line",
properties: {
design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }],
},
children: [],
},
{
_component: "@budibase/standard-components/stackedlist",
name: "Stacked List",
description:
"A basic card component that can contain content and actions.",
icon: "ri-archive-drawer-line",
children: [],
properties: {
design: { ...all },
settings: [
{
label: "Image",
key: "imageUrl",
control: Input,
placeholder: "{{{context.Image}}}",
},
{
label: "Heading",
key: "heading",
control: Input,
placeholder: "{{context.Heading}}",
},
{
label: "Text 1",
key: "text1",
control: Input,
placeholder: "{{context.Text 1}}",
},
{
label: "Text 2",
key: "text2",
control: Input,
placeholder: "{{context.Text 2}}",
},
{
label: "Text 3",
key: "text3",
control: Input,
placeholder: "{{context.Text 3}}",
},
{
label: "destinationUrl",
key: "destinationUrl",
control: Input,
placeholder: "/table/_id",
},
],
},
},
{
_component: "@budibase/materialdesign-components/BasicCard",
name: "Card",
description:
"A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-fill",
icon: "ri-layout-bottom-line",
children: [],
properties: {
design: { ...all },
@ -288,50 +348,34 @@ export default {
],
},
},
{
name: "Login",
_component: "@budibase/standard-components/login",
description:
"A component that automatically generates a login screen for your app.",
icon: "ri-login-box-fill",
children: [],
properties: {
design: { ...all },
settings: [
{
label: "Name",
key: "name",
control: Input,
},
{
label: "Logo",
key: "logo",
control: Input,
},
],
},
},
{
name: "Table",
_component: "@budibase/standard-components/datatable",
description: "A component that generates a table from your data.",
icon: "ri-archive-drawer-fill",
icon: "ri-archive-drawer-line",
properties: {
design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }],
settings: [
{ label: "Model", key: "model", control: ModelSelect },
{ label: "Stripe Color", key: "stripeColor", control: Input },
{ label: "Border Color", key: "borderColor", control: Input },
{ label: "TH Color", key: "backgroundColor", control: Input },
{ label: "TH Font Color", key: "color", control: Input },
{ label: "Table", key: "model", control: ModelSelect },
],
},
children: [],
},
{
name: "Form",
description: "A component that generates a form from your data.",
icon: "ri-file-edit-fill",
icon: "ri-file-edit-line",
commonProps: {},
children: [
{
_component: "@budibase/standard-components/dataform",
name: "Form Basic",
icon: "ri-file-edit-fill",
icon: "ri-file-edit-line",
properties: {
design: { ...all },
settings: [
@ -340,6 +384,16 @@ export default {
key: "model",
control: ModelSelect,
},
{
label: "Title",
key: "title",
control: Input,
},
{
label: "Button Text",
key: "buttonText",
control: Input,
},
],
},
template: {
@ -351,7 +405,7 @@ export default {
{
_component: "@budibase/standard-components/dataformwide",
name: "Form Wide",
icon: "ri-file-edit-fill",
icon: "ri-file-edit-line",
properties: {
design: { ...all },
settings: [
@ -360,6 +414,16 @@ export default {
key: "model",
control: ModelSelect,
},
{
label: "Title",
key: "title",
control: Input,
},
{
label: "Button Text",
key: "buttonText",
control: Input,
},
],
},
},
@ -1715,6 +1779,17 @@ export default {
},
],
},
// {
// name: "Data List",
// _component: "@budibase/standard-components/datalist",
// description: "Shiny list",
// icon: "ri-file-list-line",
// properties: {
// design: { ...all },
// settings: [{ label: "Table", key: "model", control: ModelSelect }],
// },
// children: [],
// },
{
name: "Record Detail",
_component: "@budibase/standard-components/recorddetail",
@ -1727,14 +1802,14 @@ export default {
},
children: [],
},
{
name: "Map",
_component: "@budibase/standard-components/datamap",
description: "Shiny map",
icon: "ri-map-pin-fill",
properties: { design: { ...all } },
children: [],
},
// {
// name: "Map",
// _component: "@budibase/standard-components/datamap",
// description: "Shiny map",
// icon: "ri-map-pin-line",
// properties: { design: { ...all } },
// children: [],
// },
],
},
{
@ -1743,10 +1818,10 @@ export default {
children: [
{
_component: "##builtin/screenslot",
name: "Screenslot",
name: "Screen Slot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
icon: "ri-crop-2-fill",
icon: "ri-crop-2-line",
properties: { design: { ...all } },
commonProps: {},
children: [],
@ -1756,7 +1831,7 @@ export default {
_component: "@budibase/standard-components/Navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
icon: "ri-navigation-line",
children: [],
properties: {
design: { ...all },
@ -1768,6 +1843,39 @@ export default {
],
},
},
{
name: "Login",
_component: "@budibase/standard-components/login",
description:
"A component that automatically generates a login screen for your app.",
icon: "ri-login-box-line",
children: [],
properties: {
design: { ...all },
settings: [
{
label: "Name",
key: "name",
control: Input,
},
{
label: "Logo",
key: "logo",
control: Input,
},
{
label: "Title",
key: "title",
control: Input,
},
{
label: "Button Text",
key: "buttonText",
control: Input,
},
],
},
},
],
},
],

View File

@ -91,7 +91,7 @@
}
.workflow-item.selected {
background: var(--blue-light);
background: var(--grey-2);
}
.new-workflow-button {

View File

@ -6,7 +6,7 @@ export const FIELDS = {
constraints: {
type: "string",
length: {},
presence: false,
presence: { allowEmpty: true },
},
},
NUMBER: {
@ -15,7 +15,7 @@ export const FIELDS = {
type: "number",
constraints: {
type: "number",
presence: false,
presence: { allowEmpty: true },
numericality: {},
},
},
@ -25,7 +25,7 @@ export const FIELDS = {
type: "boolean",
constraints: {
type: "boolean",
presence: false,
presence: { allowEmpty: true },
},
},
// OPTIONS: {
@ -34,7 +34,7 @@ export const FIELDS = {
// type: "options",
// constraints: {
// type: "string",
// presence: false,
// presence: { allowEmpty: true },
// },
// },
DATETIME: {
@ -44,7 +44,7 @@ export const FIELDS = {
constraints: {
type: "string",
length: {},
presence: false,
presence: { allowEmpty: true },
},
},
// IMAGE: {
@ -53,7 +53,7 @@ export const FIELDS = {
// type: "file",
// constraints: {
// type: "string",
// presence: false,
// presence: { allowEmpty: true },
// },
// },
// FILE: {
@ -62,11 +62,11 @@ export const FIELDS = {
// type: "file",
// constraints: {
// type: "string",
// presence: false,
// presence: { allowEmpty: true },
// },
// },
DATA_LINK: {
name: "Data Links",
LINKED_FIELDS: {
name: "Linked Fields",
icon: "ri-link",
type: "link",
modelId: null,
@ -84,16 +84,46 @@ export const BLOCKS = {
constraints: {
type: "string",
length: {},
presence: false,
presence: { allowEmpty: true },
},
},
COMPANY: {
name: "Company",
icon: "ri-store-line",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
EMAIL: {
name: "Email",
icon: "ri-mail-line",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
PHONE_NUMBER: {
name: "Phone Number",
icon: "ri-number-1",
name: "Phone No.",
icon: "ri-phone-line",
type: "number",
constraints: {
type: "number",
presence: false,
presence: { allowEmpty: true },
numericality: {},
},
},
VALUE: {
name: "Value",
icon: "ri-number-5",
type: "number",
constraints: {
type: "number",
presence: { allowEmpty: true },
numericality: {},
},
},
@ -103,7 +133,27 @@ export const BLOCKS = {
type: "boolean",
constraints: {
type: "boolean",
presence: false,
presence: { allowEmpty: true },
},
},
URL: {
name: "URL",
icon: "ri-link",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
IMAGE: {
name: "Image URL",
icon: "ri-image-line",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
// PRIORITY: {
@ -112,7 +162,7 @@ export const BLOCKS = {
// type: "options",
// constraints: {
// type: "string",
// presence: false,
// presence: { allowEmpty: true },
// inclusion: ["low", "medium", "high"],
// },
// },
@ -123,7 +173,7 @@ export const BLOCKS = {
constraints: {
type: "string",
length: {},
presence: false,
presence: { allowEmpty: true },
},
},
// AVATAR: {
@ -132,7 +182,7 @@ export const BLOCKS = {
// type: "image",
// constraints: {
// type: "string",
// presence: false,
// presence: { allowEmpty: true },
// },
// },
// PDF: {
@ -141,7 +191,7 @@ export const BLOCKS = {
// type: "file",
// constraints: {
// type: "string",
// presence: false,
// presence: { allowEmpty: true },
// },
// },
}

View File

@ -1,6 +1,6 @@
<script>
import Modal from "svelte-simple-modal"
import { store, workflowStore } from "builderStore"
import { store, workflowStore, backendUiStore } from "builderStore"
import SettingsLink from "components/settings/Link.svelte"
import { get } from "builderStore/api"
@ -20,6 +20,7 @@
const pkg = await res.json()
if (res.ok) {
backendUiStore.actions.reset()
await store.setPackage(pkg)
workflowStore.actions.fetch()
return pkg

View File

@ -22,7 +22,8 @@
<style>
.root {
height: 100%;
display: flex;
display: grid;
grid-template-columns: 300px minmax(0, 1fr) 300px;
background: var(--grey-1);
line-height: 1;
}

View File

@ -0,0 +1,2 @@
<!-- routify:options index=4 -->
<slot />

View File

@ -0,0 +1,90 @@
<script>
import { Button } from "@budibase/bbui"
import { store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import api from "builderStore/api"
import Spinner from "components/common/Spinner.svelte"
import analytics from "../../../analytics"
let deployed = false
let loading = false
$: appId = $store.appId
async function deployApp() {
loading = true
const DEPLOY_URL = `/deploy`
try {
notifier.info("Starting Deployment..")
const response = await api.post(DEPLOY_URL)
const json = await response.json()
if (response.status !== 200) {
throw new Error()
}
notifier.success(`Your Deployment is Complete.`)
deployed = true
loading = false
analytics.captureEvent("web_app_deployment", {
appId,
})
} catch (err) {
analytics.captureException(err)
notifier.danger("Deployment unsuccessful. Please try again later.")
loading = false
}
}
</script>
<section>
<div>
<h4>It's time to shine!</h4>
{#if deployed}
<a target="_blank" href={`https://${appId}.app.budi.live/${appId}`}>
View App
</a>
{:else}
<Button secondary medium on:click={deployApp}>
Deploy App
{#if loading}
<Spinner ratio={'0.5'} />
{/if}
</Button>
{/if}
</div>
<img src="/_builder/assets/deploy-rocket.jpg" />
</section>
<style>
img {
width: 100%;
height: 100%;
}
h4 {
color: var(--white);
font-size: 18px;
font-weight: bold;
margin-bottom: 30px;
}
section {
position: relative;
}
div {
position: absolute;
display: flex;
text-align: center;
flex-direction: column;
align-items: center;
justify-content: center;
left: 0;
right: 0;
top: 20%;
margin-left: auto;
margin-right: auto;
width: 50%;
}
</style>

View File

@ -14,9 +14,9 @@
if ($leftover) {
// Get the correct screen children.
const screenChildren = $store.pages[$params.page]._screens.find(
screen =>
(screen.props._instanceName === $params.screen
|| screen.props._instanceName === decodeURIComponent($params.screen))
screen =>
screen.props._instanceName === $params.screen ||
screen.props._instanceName === decodeURIComponent($params.screen)
).props._children
findComponent(componentIds, screenChildren)
}
@ -38,15 +38,20 @@
// Loop through each ID
ids.forEach(id => {
// Find ID and select it
componentToSelect = currentChildren.find(child => child._id === id)
// Find ID
const component = currentChildren.find(child => child._id === id)
// If it does not exist, ignore (use last valid route)
if (!component) return
componentToSelect = component
// Update childrens array to selected components children
currentChildren = componentToSelect._children
})
// Select Component!
store.selectComponent(componentToSelect)
if (componentToSelect) store.selectComponent(componentToSelect)
}
</script>

View File

@ -27,7 +27,8 @@
.root {
height: 100%;
display: flex;
display: grid;
grid-template-columns: 300px minmax(0, 1fr) 300px;
background: var(--grey-1);
line-height: 1;
}

View File

@ -26,24 +26,39 @@
<div class="nav-section">
<div class="nav-section-title">Build</div>
<Link icon={AppsIcon} title="Apps" href="/" active />
<Link icon={SettingsIcon} title="Settings" href="/" />
<Link icon={UpdatesIcon} title="Updates" href="/" />
<Link icon={HostingIcon} title="Hosting" href="/" />
<Link
icon={HostingIcon}
title="Hosting"
href="https://portal.budi.live/" />
</div>
<div class="nav-section">
<div class="nav-section-title">Learn</div>
<Link icon={DocumentationIcon} title="Documentation" href="/" />
<Link icon={TutorialsIcon} title="Tutorials" href="/" />
<Link icon={CommunityIcon} title="Community" href="/" />
<Link
icon={DocumentationIcon}
title="Documentation"
href="https://docs.budibase.com/" />
<Link
icon={CommunityIcon}
title="Community"
href="https://forum.budibase.com/" />
</div>
<div class="nav-section">
<div class="nav-section-title">Contact</div>
<Link icon={ContributionIcon} title="Contribute" href="/" />
<Link icon={BugIcon} title="Report bug" href="/" />
<Link icon={EmailIcon} title="Email" href="/" />
<Link icon={TwitterIcon} title="Twitter" href="/" />
<Link
icon={ContributionIcon}
title="Contribute"
href="https://github.com/Budibase/budibase" />
<Link
icon={BugIcon}
title="Report bug"
href="https://github.com/Budibase/budibase/issues" />
<Link icon={EmailIcon} title="Email" href="mailto:hi@budibase.com" />
<Link
icon={TwitterIcon}
title="Twitter"
href="https://twitter.com/budibase" />
</div>
</div>

View File

@ -1,6 +1,7 @@
<script>
import { getContext } from "svelte"
import { store } from "builderStore"
import api from "builderStore/api"
import AppList from "components/start/AppList.svelte"
import { onMount } from "svelte"
import ActionButton from "components/common/ActionButton.svelte"
@ -23,6 +24,24 @@
}
}
let hasKey
async function fetchKeys() {
const response = await api.get(`/api/keys/`)
const res = await response.json()
return res.budibase
}
async function checkIfKeysAndApps() {
const key = await fetchKeys()
const apps = await getApps()
if (key) {
hasKey = true
} else {
showCreateAppModal()
}
}
// Handle create app modal
const { open } = getContext("simple-modal")
@ -30,8 +49,7 @@
open(
CreateAppModal,
{
message: "What is your name?",
hasForm: true,
hasKey,
},
{
closeButton: false,
@ -42,6 +60,8 @@
}
)
}
checkIfKeysAndApps()
</script>
<div class="header">

View File

@ -1,6 +1,6 @@
{
"name": "budibase",
"version": "0.0.32",
"version": "0.1.13",
"description": "Budibase CLI",
"repository": "https://github.com/Budibase/Budibase",
"homepage": "https://www.budibase.com",
@ -17,7 +17,7 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/server": "^0.0.32",
"@budibase/server": "^0.1.13",
"@inquirer/password": "^0.0.6-alpha.0",
"chalk": "^2.4.2",
"dotenv": "^8.2.0",
@ -29,5 +29,5 @@
"uuid": "^7.0.3",
"yargs": "^14.2.0"
},
"gitHead": "b1f4f90927d9e494e513220ef060af28d2d42455"
"gitHead": "eff4fa93ca1db11b97b5fdedc0c488413e277eb8"
}

View File

@ -16,8 +16,10 @@ const run = async opts => {
await createEmptyAppPackage(opts)
exec(`cd ${join(opts.dir, opts.applicationId)} && npm install`)
console.log(chalk.green(`Budibase app ${opts.name} created!`))
process.exit()
} catch (error) {
console.error(chalk.red("Error creating new app", error))
process.exit(1)
}
}

View File

@ -1,14 +1,14 @@
const { xPlatHomeDir } = require("../../common")
const { resolve } = require("path")
module.exports = ({ dir }) => {
module.exports = async ({ dir }) => {
dir = xPlatHomeDir(dir)
process.env.BUDIBASE_DIR = resolve(dir)
require("dotenv").config({ path: resolve(dir, ".env") })
// dont make this a variable or top level require
// ti will cause environment module to be loaded prematurely
require("@budibase/server/src/app")().then(server => {
// it will cause environment module to be loaded prematurely
return require("@budibase/server/src/app")().then(server => {
server.on("close", () => console.log("Server Closed"))
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
})

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.0.32",
"version": "0.1.1",
"license": "MPL-2.0",
"main": "dist/budibase-client.js",
"module": "dist/budibase-client.esm.mjs",
@ -60,5 +60,5 @@
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^4.0.4"
},
"gitHead": "b1f4f90927d9e494e513220ef060af28d2d42455"
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
}

View File

@ -2,7 +2,7 @@ import { attachChildren } from "./render/attachChildren"
import { createTreeNode } from "./render/prepareRenderComponent"
import { screenRouter } from "./render/screenRouter"
import { createStateManager } from "./state/stateManager"
import { getAppId } from "./render/getAppId"
import { parseAppIdFromCookie } from "./render/getAppId"
export const createApp = ({
componentLibraries,
@ -38,7 +38,7 @@ export const createApp = ({
window,
})
const fallbackPath = window.location.pathname.replace(
getAppId(window.document.cookie),
parseAppIdFromCookie(window.document.cookie),
""
)
routeTo(currentUrl || fallbackPath)

View File

@ -1,6 +1,6 @@
import { createApp } from "./createApp"
import { builtins, builtinLibName } from "./render/builtinComponents"
import { getAppId } from "./render/getAppId"
import { parseAppIdFromCookie } from "./render/getAppId"
/**
* create a web application from static budibase definition files.
@ -9,7 +9,7 @@ import { getAppId } from "./render/getAppId"
export const loadBudibase = async opts => {
const _window = (opts && opts.window) || window
// const _localStorage = (opts && opts.localStorage) || localStorage
const appId = getAppId(_window.document.cookie)
const appId = parseAppIdFromCookie(_window.document.cookie)
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
const user = {}

View File

@ -30,34 +30,33 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
}
}
const contextArray = Array.isArray(context) ? context : [context]
const childNodes = []
for (let childProps of treeNode.props._children) {
const { componentName, libName } = splitName(childProps._component)
if (!componentName || !libName) return
for (let context of contextArray) {
for (let childProps of treeNode.props._children) {
const { componentName, libName } = splitName(childProps._component)
const ComponentConstructor = componentLibraries[libName][componentName]
if (!componentName || !libName) return
const prepareNodes = ctx => {
const childNodesThisIteration = prepareRenderComponent({
props: childProps,
parentNode: treeNode,
ComponentConstructor,
htmlElement,
anchor,
context: ctx,
})
const ComponentConstructor = componentLibraries[libName][componentName]
for (let childNode of childNodesThisIteration) {
childNodes.push(childNode)
const prepareNodes = ctx => {
const childNodesThisIteration = prepareRenderComponent({
props: childProps,
parentNode: treeNode,
ComponentConstructor,
htmlElement,
anchor,
context: ctx,
})
for (let childNode of childNodesThisIteration) {
childNodes.push(childNode)
}
}
}
if (Array.isArray(context)) {
for (let singleCtx of context) {
prepareNodes(singleCtx)
}
} else {
prepareNodes(context)
}
}
@ -100,7 +99,10 @@ const areTreeNodesEqual = (children1, children2) => {
let isEqual = false
for (let i = 0; i < children1.length; i++) {
isEqual = deepEqual(children1[i].context, children2[i].context)
// same context and same children, then nothing has changed
isEqual =
deepEqual(children1[i].context, children2[i].context) &&
areTreeNodesEqual(children1[i].children, children2[i].children)
if (!isEqual) return false
if (isScreenSlot(children1[i].parentNode.props._component)) {
isEqual = deepEqual(children1[i].props, children2[i].props)

View File

@ -1,4 +1,4 @@
export const getAppId = docCookie => {
export const parseAppIdFromCookie = docCookie => {
const cookie =
docCookie.split(";").find(c => c.trim().startsWith("budibase:token")) ||
docCookie.split(";").find(c => c.trim().startsWith("builder:token"))

View File

@ -78,13 +78,14 @@ export const createTreeNode = () => ({
get destroy() {
const node = this
return () => {
if (node.unsubscribe) node.unsubscribe()
if (node.component && node.component.$destroy) node.component.$destroy()
if (node.children) {
// destroy children first - from leaf nodes up
for (let child of node.children) {
child.destroy()
}
}
if (node.unsubscribe) node.unsubscribe()
if (node.component && node.component.$destroy) node.component.$destroy()
for (let onDestroyItem of node.onDestroy) {
onDestroyItem()
}

View File

@ -1,6 +1,6 @@
import regexparam from "regexparam"
import { routerStore } from "../state/store"
import { getAppId } from "./getAppId"
import { appStore } from "../state/store"
import { parseAppIdFromCookie } from "./getAppId"
export const screenRouter = ({ screens, onScreenSelected, window }) => {
const makeRootedPath = url => {
@ -9,7 +9,7 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
(window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1")
) {
const appId = getAppId(window.document.cookie)
const appId = parseAppIdFromCookie(window.document.cookie)
if (url) {
if (url.startsWith(appId)) return url
return `/${appId}${url.startsWith("/") ? "" : "/"}${url}`
@ -49,7 +49,7 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
})
}
routerStore.update(state => {
appStore.update(state => {
state["##routeParams"] = params
return state
})

View File

@ -8,6 +8,7 @@ export const bbFactory = ({
store,
componentLibraries,
onScreenSlotRendered,
getCurrentState,
}) => {
const apiCall = method => (url, body) => {
return fetch(url, {
@ -53,6 +54,8 @@ export const bbFactory = ({
store: store,
api,
parent,
// these parameters are populated by screenRouter
routeParams: () => getCurrentState()["##routeParams"],
}
}
}

View File

@ -26,8 +26,14 @@ export const createStateManager = ({
routeTo,
}) => {
let handlerTypes = eventHandlers(routeTo)
let currentState
// creating a reference to the current state
// this avoids doing store.get() ... which is expensive on
// hot paths, according to the svelte docs.
// the state object reference never changes (although it's internals do)
// so this should work fine for us
let currentState
appStore.subscribe(s => (currentState = s))
const getCurrentState = () => currentState
const bb = bbFactory({

View File

@ -12,8 +12,8 @@
"dev:builder": "rollup -cw"
},
"devDependencies": {
"@budibase/client": "^0.0.32",
"@budibase/standard-components": "^0.0.32",
"@budibase/client": "^0.1.1",
"@budibase/standard-components": "^0.1.13",
"@material/button": "^4.0.0",
"@material/checkbox": "^4.0.0",
"@material/data-table": "4.0.0",
@ -50,9 +50,9 @@
"keywords": [
"svelte"
],
"version": "0.0.32",
"version": "0.1.13",
"license": "MIT",
"gitHead": "b1f4f90927d9e494e513220ef060af28d2d42455",
"gitHead": "eff4fa93ca1db11b97b5fdedc0c488413e277eb8",
"dependencies": {
"@material/card": "4.0.0"
}

View File

@ -12,4 +12,8 @@ JWT_SECRET={{cookieKey1}}
PORT=4001
# error level for koa-pino
LOG_LEVEL=error
LOG_LEVEL=error
DEPLOYMENT_CREDENTIALS_URL="https://dt4mpwwap8.execute-api.eu-west-1.amazonaws.com/prod/"
DEPLOYMENT_DB_URL="https://couchdb.budi.live:5984"
SENTRY_DSN=https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131

View File

@ -8,7 +8,7 @@
"type": "node",
"request": "launch",
"name": "Start Server",
"program": "${workspaceFolder}/../cli/bin/budi"
"program": "${workspaceFolder}/src/index.js"
},
{
"type": "node",

View File

@ -2,6 +2,9 @@ FROM node:12-alpine
WORKDIR /app
ENV CLOUD=1
ENV COUCH_DB_URL=https://couchdb.budi.live:5984
# copy files and install dependencies
COPY . ./
RUN yarn

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