Merge branch 'master' of https://github.com/Budibase/budibase into britecharts/separate-components-2
This commit is contained in:
commit
bbf32cda75
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -63,7 +63,7 @@
|
|||
|
||||
.budibase__nav-item.selected {
|
||||
color: var(--ink);
|
||||
background: var(--blue-light);
|
||||
background: var(--grey-2);
|
||||
}
|
||||
|
||||
.budibase__nav-item:hover {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
|
@ -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 |
|
@ -60,7 +60,8 @@
|
|||
|
||||
<style>
|
||||
.fields.selected {
|
||||
background: var(--grey-1);
|
||||
background: var(--grey-2);
|
||||
border: var(--purple) 1px solid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
margin-right: 20px;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
.switcher > .selected {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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%;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>+</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>+</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>
|
|
@ -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>×</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>
|
|
@ -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)
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { default as drag } from "./drag.js"
|
||||
export { default as keyevents } from "./key-events.js"
|
|
@ -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()
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>×</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
import Colorpreview from "./components/Colorpreview.svelte"
|
||||
export default Colorpreview
|
|
@ -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))
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -53,7 +53,6 @@
|
|||
}
|
||||
|
||||
.label {
|
||||
flex: 0 0 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 = () => {}
|
||||
|
|
|
@ -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%" },
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
}
|
||||
|
||||
.workflow-item.selected {
|
||||
background: var(--blue-light);
|
||||
background: var(--grey-2);
|
||||
}
|
||||
|
||||
.new-workflow-button {
|
||||
|
|
|
@ -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 },
|
||||
// },
|
||||
// },
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<!-- routify:options index=4 -->
|
||||
<slot />
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())}`)
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -8,7 +8,7 @@
|
|||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Start Server",
|
||||
"program": "${workspaceFolder}/../cli/bin/budi"
|
||||
"program": "${workspaceFolder}/src/index.js"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue