Merge branch 'master' of github.com:Budibase/budibase into deployment
This commit is contained in:
commit
d176aa1d70
|
@ -55,7 +55,7 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.13.0",
|
||||
"@budibase/bbui": "^1.15.0",
|
||||
"@budibase/client": "^0.0.32",
|
||||
"@nx-js/compiler-util": "^2.0.0",
|
||||
"codemirror": "^5.51.0",
|
||||
|
@ -70,6 +70,7 @@
|
|||
"safe-buffer": "^5.1.2",
|
||||
"shortid": "^2.2.8",
|
||||
"string_decoder": "^1.2.0",
|
||||
"svelte-portal": "^0.1.0",
|
||||
"svelte-simple-modal": "^0.4.2",
|
||||
"uikit": "^3.1.7"
|
||||
},
|
||||
|
@ -108,4 +109,4 @@
|
|||
"svelte-jester": "^1.0.6"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
}
|
||||
}
|
|
@ -185,7 +185,11 @@ export default {
|
|||
svelte({
|
||||
// enable run-time checks when not in production
|
||||
dev: !production,
|
||||
include: ["src/**/*.svelte", "node_modules/**/*.svelte"],
|
||||
include: [
|
||||
"src/**/*.svelte",
|
||||
"node_modules/**/*.svelte",
|
||||
"../../../bbui/src/**/*.svelte",
|
||||
],
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file — better for performance
|
||||
css: css => {
|
||||
|
|
|
@ -27,9 +27,6 @@ export const getBackendUiStore = () => {
|
|||
const views = await viewsResponse.json()
|
||||
store.update(state => {
|
||||
state.selectedDatabase = db
|
||||
if (models && models.length > 0) {
|
||||
store.actions.models.select(models[0])
|
||||
}
|
||||
state.models = models
|
||||
state.views = views
|
||||
return state
|
||||
|
|
|
@ -109,8 +109,8 @@ const setPackage = (store, initial) => async pkg => {
|
|||
initial.builtins = [getBuiltin("##builtin/screenslot")]
|
||||
initial.appInstances = pkg.application.instances
|
||||
initial.appId = pkg.application._id
|
||||
|
||||
store.set(initial)
|
||||
await backendUiStore.actions.database.select(initial.appInstances[0])
|
||||
return initial
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
class={determineClassName(type)}
|
||||
bind:value
|
||||
class:uk-form-danger={errors.length > 0}>
|
||||
<option></option>
|
||||
{#each options as opt}
|
||||
<option value={opt}>{opt}</option>
|
||||
{/each}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
function selectModel(model, fieldId) {
|
||||
backendUiStore.actions.models.select(model)
|
||||
|
||||
$goto(`./model/${model._id}`)
|
||||
if (fieldId) {
|
||||
backendUiStore.update(state => {
|
||||
state.selectedField = fieldId
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<script>
|
||||
import { Button } from "@budibase/bbui"
|
||||
export let remove = false
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div class="img">
|
||||
<img src="https://picsum.photos/60/60" alt="zoom" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title">Zoom</div>
|
||||
<div class="description">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Button wide error={remove} secondary={!remove} on:click>
|
||||
<span>{remove ? 'Remove' : 'Add'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
padding: 12px;
|
||||
background: var(--light-grey);
|
||||
grid-gap: 20px;
|
||||
}
|
||||
span {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.content {
|
||||
display: grid;
|
||||
grid-gap: 12px;
|
||||
grid-template-columns: 60px auto;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.description {
|
||||
font-size: 12px;
|
||||
}
|
||||
.img {
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
img {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,48 @@
|
|||
<script>
|
||||
import Modal from "./Modal.svelte"
|
||||
import { SettingsIcon } from "components/common/Icons/"
|
||||
import { getContext } from "svelte"
|
||||
import { isActive, goto, layout } from "@sveltech/routify"
|
||||
|
||||
// Handle create app modal
|
||||
const { open } = getContext("simple-modal")
|
||||
|
||||
const showSettingsModal = () => {
|
||||
open(
|
||||
Modal,
|
||||
{
|
||||
name: "Placeholder App Name",
|
||||
description: "This is a hardcoded description that needs to change",
|
||||
},
|
||||
{
|
||||
closeButton: false,
|
||||
closeOnEsc: true,
|
||||
styleContent: { padding: 0 },
|
||||
closeOnOuterClick: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="topnavitemright" on:click={showSettingsModal}>
|
||||
<SettingsIcon />
|
||||
</span>
|
||||
|
||||
<style>
|
||||
span:first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.topnavitemright {
|
||||
cursor: pointer;
|
||||
color: var(--ink-light);
|
||||
margin: 0px 20px 0px 0px;
|
||||
padding-top: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,92 @@
|
|||
<script>
|
||||
import { General, Users, DangerZone } from "./tabs"
|
||||
|
||||
import { Input, TextArea, Button, Switcher } from "@budibase/bbui"
|
||||
import { SettingsIcon, CloseIcon } from "components/common/Icons/"
|
||||
import { getContext } from "svelte"
|
||||
import { post } from "builderStore/api"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
export let name = ""
|
||||
export let description = ""
|
||||
const tabs = [
|
||||
{
|
||||
title: "General",
|
||||
key: "GENERAL",
|
||||
component: General,
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
key: "USERS",
|
||||
component: Users,
|
||||
},
|
||||
{
|
||||
title: "Danger Zone",
|
||||
key: "DANGERZONE",
|
||||
component: DangerZone,
|
||||
},
|
||||
]
|
||||
let value = "GENERAL"
|
||||
$: selectedTab = tabs.find(tab => tab.key === value).component
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="body">
|
||||
<div class="heading">
|
||||
<span class="icon">
|
||||
<SettingsIcon />
|
||||
</span>
|
||||
<h3>Settings</h3>
|
||||
</div>
|
||||
<Switcher headings={tabs} bind:value>
|
||||
<svelte:component this={selectedTab} />
|
||||
</Switcher>
|
||||
</div>
|
||||
<div class="close-button" on:click={close}>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
.close-button :global(svg) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.heading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.icon {
|
||||
display: grid;
|
||||
border-radius: 3px;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
padding: 10px;
|
||||
background-color: var(--blue-light);
|
||||
}
|
||||
.body {
|
||||
padding: 40px 40px 80px 40px;
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,13 @@
|
|||
<h3>
|
||||
<slot />
|
||||
</h3>
|
||||
|
||||
<style>
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--ink);
|
||||
margin-top: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
import { Input, Select, Button } from "@budibase/bbui"
|
||||
export let user
|
||||
|
||||
let editMode = false
|
||||
</script>
|
||||
|
||||
<div class="inputs">
|
||||
<Input
|
||||
disabled
|
||||
thin
|
||||
bind:value={user.username}
|
||||
name="Name"
|
||||
placeholder="Username" />
|
||||
<Select disabled={!editMode} bind:value={user.accessLevelId} thin>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="POWER_USER">Power User</option>
|
||||
</Select>
|
||||
{#if editMode}
|
||||
<Button
|
||||
blue
|
||||
on:click={() => {
|
||||
dispatch('save', user)
|
||||
editMode = false
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
{:else}
|
||||
<Button secondary on:click={() => (editMode = true)}>Edit</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.inputs {
|
||||
display: grid;
|
||||
justify-items: stretch;
|
||||
grid-gap: 18px;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,46 @@
|
|||
<script>
|
||||
import { Input, TextArea, Button } from "@budibase/bbui"
|
||||
import Title from "../TabTitle.svelte"
|
||||
|
||||
let value = ""
|
||||
let loading = false
|
||||
|
||||
const deleteApp = () => {
|
||||
loading = true
|
||||
// Do stuff here to delete app!
|
||||
// Navigate to start
|
||||
}
|
||||
</script>
|
||||
|
||||
<Title>Danger Zone</Title>
|
||||
<div class="background">
|
||||
<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:" />
|
||||
|
||||
<Button
|
||||
disabled={value !== 'DELETE' || loading}
|
||||
primary
|
||||
wide
|
||||
on:click={deleteApp}>
|
||||
Delete Entire Web App
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.background {
|
||||
display: grid;
|
||||
grid-gap: var(--space);
|
||||
border-radius: 5px;
|
||||
background-color: var(--light-grey);
|
||||
padding: 12px 12px 18px 12px;
|
||||
}
|
||||
.background :global(button) {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,26 @@
|
|||
<script>
|
||||
import { Input, TextArea, Button } from "@budibase/bbui"
|
||||
import Title from "../TabTitle.svelte"
|
||||
</script>
|
||||
|
||||
<Title>General</Title>
|
||||
<div class="container">
|
||||
<div class="background">
|
||||
<Input thin edit placeholder="Enter your name" label="Name" />
|
||||
</div>
|
||||
<div class="background">
|
||||
<TextArea thin edit placeholder="Enter your name" label="Name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-gap: var(--space);
|
||||
}
|
||||
.background {
|
||||
border-radius: 5px;
|
||||
background-color: var(--light-grey);
|
||||
padding: 12px 12px 18px 12px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import Integration from "../Integration.svelte"
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="title">Your Integrations</div>
|
||||
<div class="integrations">
|
||||
<Integration remove />
|
||||
<Integration remove />
|
||||
<Integration remove />
|
||||
<Integration remove />
|
||||
</div>
|
||||
<div class="apps">Recommended apps</div>
|
||||
<div class="integrations">
|
||||
<Integration />
|
||||
<Integration />
|
||||
<Integration />
|
||||
<Integration />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
}
|
||||
.integrations {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.apps {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.background {
|
||||
border-radius: 5px;
|
||||
background-color: var(--light-grey);
|
||||
padding: 12px 12px 18px 12px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1 @@
|
|||
Permissions
|
|
@ -0,0 +1,135 @@
|
|||
<script>
|
||||
import { Input, Select, Button } from "@budibase/bbui"
|
||||
import Title from "../TabTitle.svelte"
|
||||
import UserRow from "../UserRow.svelte"
|
||||
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
// import * as api from "../api"
|
||||
|
||||
let username = ""
|
||||
let password = ""
|
||||
let accessLevelId
|
||||
|
||||
$: valid = username && password && accessLevelId
|
||||
$: appId = $store.appId
|
||||
|
||||
// Create user!
|
||||
async function createUser() {
|
||||
if (valid) {
|
||||
const user = { name: username, username, password, accessLevelId }
|
||||
const response = await api.post(`/api/users`, user)
|
||||
const json = await response.json()
|
||||
backendUiStore.actions.users.create(json)
|
||||
fetchUsersPromise = fetchUsers()
|
||||
}
|
||||
}
|
||||
|
||||
// Update user!
|
||||
async function updateUser(event) {
|
||||
let data = event.detail
|
||||
delete data.password
|
||||
const response = await api.put(`/api/users`, data)
|
||||
const users = await response.json()
|
||||
backendUiStore.update(state => {
|
||||
state.users = users
|
||||
return state
|
||||
})
|
||||
fetchUsersPromise = fetchUsers()
|
||||
}
|
||||
|
||||
// Get users
|
||||
async function fetchUsers() {
|
||||
const response = await api.get(`/api/users`)
|
||||
const users = await response.json()
|
||||
backendUiStore.update(state => {
|
||||
state.users = users
|
||||
return state
|
||||
})
|
||||
return users
|
||||
}
|
||||
|
||||
let fetchUsersPromise = fetchUsers()
|
||||
</script>
|
||||
|
||||
<Title>Users</Title>
|
||||
<div class="container">
|
||||
<div class="background create">
|
||||
<div class="title">Create new user</div>
|
||||
<div class="inputs">
|
||||
<Input thin bind:value={username} name="Name" placeholder="Username" />
|
||||
<Input
|
||||
thin
|
||||
bind:value={password}
|
||||
name="Password"
|
||||
placeholder="Password" />
|
||||
<Select bind:value={accessLevelId} thin>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="POWER_USER">Power User</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="create-button">
|
||||
<Button on:click={createUser} small blue>Create</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="background">
|
||||
<div class="title">Current Users</div>
|
||||
{#await fetchUsersPromise}
|
||||
Loading state!
|
||||
{:then users}
|
||||
<ul>
|
||||
{#each users as user}
|
||||
<li>
|
||||
<UserRow {user} on:save={updateUser} />
|
||||
</li>
|
||||
{:else}
|
||||
<li>No Users found</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:catch error}
|
||||
err0r
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-gap: 14px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.create-button {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.inputs {
|
||||
display: grid;
|
||||
grid-gap: 18px;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,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 DangerZone } from "./DangerZone.svelte"
|
|
@ -25,38 +25,69 @@
|
|||
name: "Screen Placeholder",
|
||||
route: "*",
|
||||
props: {
|
||||
_component: "@budibase/standard-components/container",
|
||||
type: "div",
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/container",
|
||||
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
|
||||
_id: "__screenslot__text",
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
type: "div",
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/text",
|
||||
_styles: {
|
||||
normal: {},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_id: "__screenslot__text_2",
|
||||
_code: "",
|
||||
text: "content",
|
||||
font: "",
|
||||
color: "",
|
||||
textAlign: "inline",
|
||||
verticalAlign: "inline",
|
||||
formattingTag: "none",
|
||||
},
|
||||
],
|
||||
"_id": "49c3d0a2-7028-46f0-b004-7eddf62ad01c",
|
||||
"_component": "@budibase/standard-components/container",
|
||||
"_styles": {
|
||||
"normal": {
|
||||
"padding": "0px",
|
||||
"font-family": "Roboto",
|
||||
"border-width": "0",
|
||||
"border-style": "None",
|
||||
"text-align": "center"
|
||||
},
|
||||
"hover": {},
|
||||
"active": {},
|
||||
"selected": {}
|
||||
},
|
||||
"_code": "",
|
||||
"className": "",
|
||||
"onLoad": [],
|
||||
"type": "div",
|
||||
"_children": [
|
||||
{
|
||||
"_id": "335428f7-f9ca-4acd-9e76-71bc8ad27324",
|
||||
"_component": "@budibase/standard-components/container",
|
||||
"_styles": {
|
||||
"normal": {
|
||||
"padding": "16px",
|
||||
"border-style": "Dashed",
|
||||
"border-width": "2px",
|
||||
"border-color": "#8a8989fa"
|
||||
},
|
||||
"hover": {},
|
||||
"active": {},
|
||||
"selected": {}
|
||||
},
|
||||
"_code": "",
|
||||
"className": "",
|
||||
"onLoad": [],
|
||||
"type": "div",
|
||||
"_instanceId": "inst_b3b4e95_ab0df02dda3f4d8eb4b35eea2968bad3",
|
||||
"_instanceName": "Container",
|
||||
"_children": [
|
||||
{
|
||||
"_id": "ddb6a225-33ba-4ba8-91da-bc6a2697ebf9",
|
||||
"_component": "@budibase/standard-components/heading",
|
||||
"_styles": {
|
||||
"normal": {
|
||||
"font-family": "Roboto"
|
||||
},
|
||||
"hover": {},
|
||||
"active": {},
|
||||
"selected": {}
|
||||
},
|
||||
"_code": "",
|
||||
"className": "",
|
||||
"text": "Your screens go here",
|
||||
"type": "h1",
|
||||
"_instanceId": "inst_b3b4e95_ab0df02dda3f4d8eb4b35eea2968bad3",
|
||||
"_instanceName": "Heading",
|
||||
"_children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"_instanceName": "Content Placeholder"
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
export default `<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%!important;
|
||||
font-family: Roboto !important;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.lay-__screenslot__text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export { default as drag } from "./drag.js"
|
||||
export { default as keyevents } from "./key-events.js"
|
|
@ -0,0 +1,41 @@
|
|||
//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,5 +1,5 @@
|
|||
<script>
|
||||
import { buildStyle } from "./helpers.js"
|
||||
import {buildStyle} from "../helpers.js"
|
||||
import { fade } from "svelte/transition"
|
||||
|
||||
export let backgroundSize = "10px"
|
|
@ -3,32 +3,35 @@
|
|||
import { fade } from "svelte/transition"
|
||||
import Swatch from "./Swatch.svelte"
|
||||
import CheckedBackground from "./CheckedBackground.svelte"
|
||||
import { buildStyle } from "./helpers.js"
|
||||
import {buildStyle} from "../helpers.js"
|
||||
import {
|
||||
getColorFormat,
|
||||
convertToHSVA,
|
||||
convertHsvaToFormat,
|
||||
} from "./utils.js"
|
||||
} 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"
|
||||
|
||||
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 open = false
|
||||
|
||||
export let style = ""
|
||||
export let pickerHeight = 0
|
||||
export let pickerWidth = 0
|
||||
|
||||
let colorPicker = null
|
||||
let adder = null
|
||||
|
||||
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()
|
||||
|
||||
|
@ -38,6 +41,10 @@
|
|||
getRecentColors()
|
||||
}
|
||||
|
||||
if(colorPicker) {
|
||||
colorPicker.focus()
|
||||
}
|
||||
|
||||
if (format) {
|
||||
convertAndSetHSVA()
|
||||
}
|
||||
|
@ -50,6 +57,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleEscape() {
|
||||
if(open) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setRecentColor(color) {
|
||||
if (swatches.length === 12) {
|
||||
swatches.splice(0, 1)
|
||||
|
@ -145,85 +158,99 @@
|
|||
}
|
||||
|
||||
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : ""
|
||||
$: style = buildStyle({ background: value, border })
|
||||
$: selectedColorStyle = buildStyle({ background: value, border })
|
||||
$: shrink = swatches.length > 0
|
||||
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="colorpicker-container"
|
||||
bind:clientHeight={pickerHeight}
|
||||
bind:clientWidth={pickerWidth}>
|
||||
<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="palette-panel">
|
||||
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<div class="alpha-hue-panel">
|
||||
<div>
|
||||
<CheckedBackground borderRadius="50%" backgroundSize="8px">
|
||||
<div class="selected-color" {style} />
|
||||
</CheckedBackground>
|
||||
</div>
|
||||
<div>
|
||||
<Slider
|
||||
type="hue"
|
||||
value={h}
|
||||
on:change={hue => setHue(hue.detail)}
|
||||
on:dragend={dispatchValue} />
|
||||
|
||||
<CheckedBackground borderRadius="10px" backgroundSize="7px">
|
||||
<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="alpha"
|
||||
value={a}
|
||||
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
|
||||
type="hue"
|
||||
value={h}
|
||||
on:change={hue => setHue(hue.detail)}
|
||||
on:dragend={dispatchValue} />
|
||||
</CheckedBackground>
|
||||
|
||||
<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
|
||||
tabindex="0"
|
||||
use:keyevents={{"Enter": addSwatch}}
|
||||
bind:this={adder}
|
||||
transition:fade
|
||||
class="adder"
|
||||
class:shrink
|
||||
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>
|
||||
|
||||
{#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;
|
||||
outline: none;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
transition: top 0.1s, left 0.1s;
|
||||
flex-direction: column;
|
||||
/* height: 265px; */
|
||||
margin: 5px 0px;
|
||||
height: auto;
|
||||
width: 220px;
|
||||
background: #ffffff;
|
||||
|
@ -273,7 +300,7 @@
|
|||
flex: 1;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
transition: flex 0.5s;
|
||||
transition: flex 0.3s;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f1f3f4;
|
||||
|
@ -283,6 +310,8 @@
|
|||
margin-left: 5px;
|
||||
margin-top: 3px;
|
||||
font-weight: 500;
|
||||
outline-color: #003cb0;
|
||||
outline-width: thin;
|
||||
}
|
||||
|
||||
.shrink {
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import Colorpicker from "./Colorpicker.svelte"
|
||||
import CheckedBackground from "./CheckedBackground.svelte"
|
||||
import { createEventDispatcher, afterUpdate, beforeUpdate } from "svelte"
|
||||
import { createEventDispatcher, beforeUpdate, onMount } from "svelte"
|
||||
|
||||
import { buildStyle } from "./helpers.js"
|
||||
import {buildStyle, debounce} from "../helpers.js"
|
||||
import { fade } from "svelte/transition"
|
||||
import { getColorFormat } from "./utils.js"
|
||||
import { getColorFormat } from "../utils.js"
|
||||
|
||||
export let value = "#3ec1d3ff"
|
||||
export let swatches = []
|
||||
|
@ -15,7 +15,8 @@
|
|||
export let height = "25px"
|
||||
|
||||
let format = "hexa"
|
||||
let dimensions = { top: 0, left: 0 }
|
||||
let dimensions = { top: 0, bottom: 0, right: 0, left: 0 }
|
||||
let positionSide = "top"
|
||||
let colorPreview = null
|
||||
|
||||
let previewHeight = null
|
||||
|
@ -23,17 +24,8 @@
|
|||
let pickerWidth = 0
|
||||
let pickerHeight = 0
|
||||
|
||||
let anchorEl = null
|
||||
let parentNodes = []
|
||||
let errorMsg = null
|
||||
|
||||
$: previewStyle = buildStyle({ width, height, background: value })
|
||||
$: errorPreviewStyle = buildStyle({ width, height })
|
||||
$: pickerStyle = buildStyle({
|
||||
top: `${dimensions.top}px`,
|
||||
left: `${dimensions.left}px`,
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
beforeUpdate(() => {
|
||||
|
@ -46,25 +38,6 @@
|
|||
}
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
if (colorPreview && colorPreview.offsetParent && !anchorEl) {
|
||||
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
|
||||
anchorEl = colorPreview.offsetParent
|
||||
let curEl = colorPreview
|
||||
let els = []
|
||||
//Travel up dom tree from preview element to find parent elements that scroll
|
||||
while (!anchorEl.isSameNode(curEl)) {
|
||||
curEl = curEl.parentNode
|
||||
let elOverflow = window
|
||||
.getComputedStyle(curEl)
|
||||
.getPropertyValue("overflow")
|
||||
if (/scroll|auto/.test(elOverflow)) {
|
||||
els.push(curEl)
|
||||
}
|
||||
}
|
||||
parentNodes = els
|
||||
}
|
||||
})
|
||||
|
||||
function openColorpicker(event) {
|
||||
if (colorPreview) {
|
||||
|
@ -72,45 +45,53 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: if (open && colorPreview) {
|
||||
const {
|
||||
top: spaceAbove,
|
||||
width,
|
||||
bottom,
|
||||
right,
|
||||
left: spaceLeft,
|
||||
} = colorPreview.getBoundingClientRect()
|
||||
const { innerHeight, innerWidth } = window
|
||||
|
||||
const { offsetLeft, offsetTop } = colorPreview
|
||||
//get the scrollTop value for all scrollable parent elements
|
||||
let scrollTop = parentNodes.reduce(
|
||||
(scrollAcc, el) => (scrollAcc += el.scrollTop),
|
||||
0
|
||||
)
|
||||
|
||||
const spaceBelow = innerHeight - spaceAbove - previewHeight
|
||||
const top =
|
||||
spaceAbove > spaceBelow
|
||||
? offsetTop - pickerHeight - scrollTop
|
||||
: offsetTop + previewHeight - scrollTop
|
||||
|
||||
//TOO: Testing and Scroll Awareness for x Scroll
|
||||
const spaceRight = innerWidth - spaceLeft + previewWidth
|
||||
const left =
|
||||
spaceRight > spaceLeft
|
||||
? offsetLeft + previewWidth
|
||||
: offsetLeft - pickerWidth
|
||||
|
||||
dimensions = { top, left }
|
||||
}
|
||||
|
||||
function onColorChange(color) {
|
||||
value = color.detail
|
||||
dispatch("change", color.detail)
|
||||
}
|
||||
|
||||
function calculateDimensions() {
|
||||
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) - (220 / 2)
|
||||
|
||||
dimensions = { [positionSide]: y.toFixed(1), left: x.toFixed(1) }
|
||||
}
|
||||
|
||||
$: if(open && colorPreview) {
|
||||
calculateDimensions()
|
||||
}
|
||||
|
||||
$: previewStyle = buildStyle({ width, height, background: value })
|
||||
$: errorPreviewStyle = buildStyle({ width, height })
|
||||
$: pickerStyle = buildStyle({
|
||||
[positionSide]: `${dimensions[positionSide]}px`,
|
||||
left: `${dimensions.left}px`,
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={debounce(calculateDimensions, 200)} />
|
||||
|
||||
<div class="color-preview-container">
|
||||
{#if !errorMsg}
|
||||
<CheckedBackground borderRadius="3px" backgroundSize="8px">
|
||||
|
@ -124,19 +105,19 @@
|
|||
</CheckedBackground>
|
||||
|
||||
{#if open}
|
||||
<div transition:fade class="picker-container" style={pickerStyle}>
|
||||
<Colorpicker
|
||||
on:change={onColorChange}
|
||||
on:addswatch
|
||||
on:removeswatch
|
||||
bind:format
|
||||
bind:value
|
||||
bind:pickerHeight
|
||||
bind:pickerWidth
|
||||
{swatches}
|
||||
{disableSwatches}
|
||||
{open} />
|
||||
</div>
|
||||
<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}
|
||||
|
@ -154,6 +135,7 @@
|
|||
}
|
||||
|
||||
.color-preview {
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #dedada;
|
||||
}
|
||||
|
@ -166,12 +148,12 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.picker-container {
|
||||
/* .picker-container {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
}
|
||||
} */
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
|
@ -1,9 +1,14 @@
|
|||
<script>
|
||||
import {createEventDispatcher} from "svelte"
|
||||
import {keyevents} from "../actions"
|
||||
|
||||
export let text = ""
|
||||
export let selected = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="flatbutton" class:selected on:click>{text}</div>
|
||||
<div class="flatbutton" tabindex="0" use:keyevents={{"Enter": () => dispatch("click")}} class:selected on:click>{text}</div>
|
||||
|
||||
<style>
|
||||
.flatbutton {
|
||||
|
@ -19,11 +24,14 @@
|
|||
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>
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let target = document.body;
|
||||
|
||||
let targetEl;
|
||||
let portal;
|
||||
let componentInstance;
|
||||
|
||||
onMount(() => {
|
||||
if (typeof target === "string") {
|
||||
targetEl = document.querySelector(target);
|
||||
// Force exit
|
||||
if (targetEl === null) {
|
||||
return () => {};
|
||||
}
|
||||
} else if (target instanceof HTMLElement) {
|
||||
targetEl = target;
|
||||
} else {
|
||||
throw new TypeError(
|
||||
`Unknown target type: ${typeof target}. Allowed types: String (CSS selector), HTMLElement.`
|
||||
);
|
||||
}
|
||||
|
||||
portal = document.createElement("div");
|
||||
targetEl.appendChild(portal);
|
||||
portal.appendChild(componentInstance);
|
||||
|
||||
return () => {
|
||||
targetEl.removeChild(portal);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={componentInstance}>
|
||||
<slot />
|
||||
</div>
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import dragable from "./drag.js"
|
||||
import {drag, keyevents} from "../actions"
|
||||
|
||||
export let value = 1
|
||||
export let type = "hue"
|
||||
|
@ -9,6 +9,10 @@
|
|||
|
||||
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()
|
||||
|
@ -17,12 +21,29 @@
|
|||
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
|
||||
|
||||
|
@ -30,14 +51,16 @@
|
|||
</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:dragable
|
||||
use:drag
|
||||
on:drag={e => onSliderChange(e.detail, true)}
|
||||
on:dragend
|
||||
class="slider-thumb"
|
||||
|
@ -54,6 +77,8 @@
|
|||
margin: 10px 0px;
|
||||
border: 1px solid #e8e8ef;
|
||||
cursor: pointer;
|
||||
outline-color: #003cb0;
|
||||
outline-width: thin;
|
||||
}
|
||||
|
||||
.hue {
|
|
@ -2,6 +2,7 @@
|
|||
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"
|
||||
|
@ -12,6 +13,8 @@
|
|||
<div class="space">
|
||||
<CheckedBackground borderRadius="6px">
|
||||
<div
|
||||
tabindex="0"
|
||||
use:keyevents={{"Enter": () => dispatch("click")}}
|
||||
in:fade
|
||||
class="swatch"
|
||||
style={`background: ${color};`}
|
||||
|
@ -38,6 +41,8 @@
|
|||
border: 1px solid #dedada;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
outline-color: #003cb0;
|
||||
outline-width: thin;
|
||||
}
|
||||
|
||||
.space {
|
|
@ -1,4 +1,4 @@
|
|||
export const buildStyle = styles => {
|
||||
export function buildStyle(styles) {
|
||||
let str = ""
|
||||
for (let s in styles) {
|
||||
if (styles[s]) {
|
||||
|
@ -12,3 +12,9 @@ export const buildStyle = styles => {
|
|||
export const convertCamel = str => {
|
||||
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
|
||||
}
|
||||
|
||||
export const debounce = (fn, milliseconds) => {
|
||||
return () => {
|
||||
setTimeout(fn, milliseconds)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
import Colorpreview from "./Colorpreview.svelte"
|
||||
import Colorpreview from "./components/Colorpreview.svelte"
|
||||
export default Colorpreview
|
||||
|
|
|
@ -144,13 +144,15 @@ export const hslaToHSVA = ([h, s, l, a = 1]) => {
|
|||
|
||||
export const hsvaToHexa = (hsva, asString = false) => {
|
||||
const [r, g, b, a] = hsvaToRgba(hsva)
|
||||
const padSingle = hex => (hex.length === 1 ? `0${hex}` : hex)
|
||||
|
||||
const hexa = [r, g, b]
|
||||
.map(v => {
|
||||
let hex = Math.round(v).toString(16)
|
||||
return hex.length === 1 ? `0${hex}` : hex
|
||||
})
|
||||
.concat(Math.round(a * 255).toString(16))
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
const changeScreen = screen => {
|
||||
store.setCurrentScreen(screen.props._instanceName)
|
||||
$goto(`./:page/${screen.title}`)
|
||||
$goto(`./:page/${screen.props._instanceName}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,16 +1,28 @@
|
|||
<script>
|
||||
import {onMount} from "svelte"
|
||||
import PropertyGroup from "./PropertyGroup.svelte"
|
||||
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
||||
|
||||
|
||||
export let panelDefinition = {}
|
||||
export let componentInstance = {}
|
||||
export let componentDefinition = {}
|
||||
export let onStyleChanged = () => {}
|
||||
|
||||
let selectedCategory = "normal"
|
||||
let propGroup = null
|
||||
let currentGroup
|
||||
|
||||
const getProperties = name => panelDefinition[name]
|
||||
|
||||
onMount(() => {
|
||||
// if(propGroup) {
|
||||
// propGroup.addEventListener("scroll", function(e){
|
||||
// console.log("I SCROLLED", e.target.scrollTop)
|
||||
// })
|
||||
// }
|
||||
})
|
||||
|
||||
function onChange(category) {
|
||||
selectedCategory = category
|
||||
}
|
||||
|
@ -22,6 +34,7 @@
|
|||
]
|
||||
|
||||
$: propertyGroupNames = Object.keys(panelDefinition)
|
||||
|
||||
</script>
|
||||
|
||||
<div class="design-view-container">
|
||||
|
@ -31,7 +44,7 @@
|
|||
</div>
|
||||
|
||||
<div class="positioned-wrapper">
|
||||
<div class="design-view-property-groups">
|
||||
<div bind:this={propGroup} class="design-view-property-groups">
|
||||
{#if propertyGroupNames.length > 0}
|
||||
{#each propertyGroupNames as groupName}
|
||||
<PropertyGroup
|
||||
|
@ -40,7 +53,9 @@
|
|||
styleCategory={selectedCategory}
|
||||
{onStyleChanged}
|
||||
{componentDefinition}
|
||||
{componentInstance} />
|
||||
{componentInstance}
|
||||
open={currentGroup === groupName}
|
||||
on:open={() => currentGroup = groupName} />
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="no-design">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { onMount, beforeUpdate } from "svelte"
|
||||
import { onMount, beforeUpdate, afterUpdate } from "svelte"
|
||||
import Portal from "svelte-portal"
|
||||
import { buildStyle } from "../../helpers.js"
|
||||
export let options = []
|
||||
export let value = ""
|
||||
|
@ -12,69 +13,80 @@
|
|||
let selectMenu
|
||||
let icon
|
||||
|
||||
let selectYPosition = null
|
||||
let availableSpace = 0
|
||||
let selectAnchor = null;
|
||||
let dimensions = {top: 0, bottom: 0, left: 0}
|
||||
|
||||
let positionSide = "top"
|
||||
let maxHeight = null
|
||||
let menuHeight
|
||||
let maxHeight = 0
|
||||
let scrollTop = 0;
|
||||
let containerEl = null;
|
||||
|
||||
const handleStyleBind = value =>
|
||||
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {}
|
||||
|
||||
onMount(() => {
|
||||
if (select) {
|
||||
select.addEventListener("keydown", addSelectKeyEvents)
|
||||
select.addEventListener("keydown", handleEnter)
|
||||
}
|
||||
|
||||
return () => {
|
||||
select.removeEventListener("keydown", addSelectKeyEvents)
|
||||
select.removeEventListener("keydown", handleEnter)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
function checkPosition() {
|
||||
const { bottom, top: spaceAbove } = select.getBoundingClientRect()
|
||||
function handleEscape(e) {
|
||||
if(open && e.key === "Escape") {
|
||||
toggleSelect(false)
|
||||
}
|
||||
}
|
||||
|
||||
function getDimensions() {
|
||||
const { bottom, top: spaceAbove, left } = selectAnchor.getBoundingClientRect()
|
||||
const spaceBelow = window.innerHeight - bottom
|
||||
|
||||
let y;
|
||||
|
||||
if (spaceAbove > spaceBelow) {
|
||||
positionSide = "bottom"
|
||||
maxHeight = `${spaceAbove.toFixed(0) - 20}px`
|
||||
maxHeight = spaceAbove - 20
|
||||
y = (window.innerHeight - spaceAbove)
|
||||
} else {
|
||||
positionSide = "top"
|
||||
maxHeight = `${spaceBelow.toFixed(0) - 20}px`
|
||||
y = bottom
|
||||
maxHeight = spaceBelow - 20
|
||||
}
|
||||
|
||||
dimensions = {[positionSide]: y, left}
|
||||
}
|
||||
|
||||
function addSelectKeyEvents(e) {
|
||||
if (e.key === "Enter") {
|
||||
if (!open) {
|
||||
function handleEnter(e) {
|
||||
if (!open && e.key === "Enter") {
|
||||
toggleSelect(true)
|
||||
}
|
||||
} else if (e.key === "Escape") {
|
||||
if (open) {
|
||||
toggleSelect(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelect(isOpen) {
|
||||
checkPosition()
|
||||
getDimensions()
|
||||
if (isOpen) {
|
||||
icon.style.transform = "rotate(180deg)"
|
||||
icon.style.transform = "rotate(180deg)"
|
||||
} else {
|
||||
icon.style.transform = "rotate(0deg)"
|
||||
}
|
||||
open = isOpen
|
||||
}
|
||||
|
||||
|
||||
function handleClick(val) {
|
||||
value = val
|
||||
onChange(value)
|
||||
}
|
||||
|
||||
$: menuStyle = buildStyle({
|
||||
"max-height": maxHeight,
|
||||
"max-height": `${maxHeight.toFixed(0)}px`,
|
||||
"transform-origin": `center ${positionSide}`,
|
||||
[positionSide]: "32px",
|
||||
[positionSide]: `${dimensions[positionSide]}px`,
|
||||
"left": `${dimensions.left.toFixed(0)}px`,
|
||||
})
|
||||
|
||||
$: isOptionsObject = options.every(o => typeof o === "object")
|
||||
|
@ -83,6 +95,10 @@
|
|||
? options.find(o => o.value === value)
|
||||
: {}
|
||||
|
||||
$: if(open && selectMenu) {
|
||||
selectMenu.focus()
|
||||
}
|
||||
|
||||
$: displayLabel =
|
||||
selectedOption && selectedOption.label ? selectedOption.label : value || ""
|
||||
</script>
|
||||
|
@ -92,45 +108,49 @@
|
|||
bind:this={select}
|
||||
class="bb-select-container"
|
||||
on:click={() => toggleSelect(!open)}>
|
||||
<div class="bb-select-anchor selected">
|
||||
<div bind:this={selectAnchor} class="bb-select-anchor selected">
|
||||
<span>{displayLabel}</span>
|
||||
<i bind:this={icon} class="ri-arrow-down-s-fill" />
|
||||
</div>
|
||||
<div
|
||||
bind:this={selectMenu}
|
||||
style={menuStyle}
|
||||
class="bb-select-menu"
|
||||
class:open>
|
||||
<ul>
|
||||
{#if isOptionsObject}
|
||||
{#each options as { value: v, label }}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{label}
|
||||
</li>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each options as v}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{v}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
{#if open}
|
||||
<Portal>
|
||||
<div
|
||||
tabindex="0"
|
||||
class:open
|
||||
bind:this={selectMenu}
|
||||
style={menuStyle}
|
||||
on:keydown={handleEscape}
|
||||
class="bb-select-menu">
|
||||
<ul>
|
||||
{#if isOptionsObject}
|
||||
{#each options as { value: v, label }}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{label}
|
||||
</li>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each options as v}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{v}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<div on:click|self={() => toggleSelect(false)} class="overlay" />
|
||||
</Portal>
|
||||
{/if}
|
||||
</div>
|
||||
{#if open}
|
||||
<div on:click|self={() => toggleSelect(false)} class="overlay" />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.overlay {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
@ -139,7 +159,6 @@
|
|||
}
|
||||
|
||||
.bb-select-container {
|
||||
position: relative;
|
||||
outline: none;
|
||||
width: 160px;
|
||||
height: 36px;
|
||||
|
@ -180,6 +199,7 @@
|
|||
.bb-select-menu {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
opacity: 0;
|
||||
|
@ -190,9 +210,6 @@
|
|||
height: fit-content !important;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-right: 1px solid var(--grey-4);
|
||||
border-left: 1px solid var(--grey-4);
|
||||
border-bottom: 1px solid var(--grey-4);
|
||||
background-color: var(--grey-2);
|
||||
transform: scale(0);
|
||||
transition: opacity 0.13s linear, transform 0.12s cubic-bezier(0, 0, 0.2, 1);
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
export let properties = []
|
||||
export let componentInstance = {}
|
||||
export let onStyleChanged = () => {}
|
||||
export let open = false
|
||||
|
||||
$: style = componentInstance["_styles"][styleCategory] || {}
|
||||
</script>
|
||||
|
||||
<DetailSummary {name}>
|
||||
<DetailSummary {name} on:open show={open} >
|
||||
{#each properties as props}
|
||||
<PropertyControl
|
||||
label={props.label}
|
||||
|
|
|
@ -123,7 +123,7 @@ export const margin = [
|
|||
},
|
||||
{
|
||||
label: "Bottom",
|
||||
key: "padding-bottom",
|
||||
key: "margin-bottom",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
|
@ -352,7 +352,6 @@ export const typography = [
|
|||
"Inter",
|
||||
"Lucida Sans Unicode",
|
||||
"Open Sans",
|
||||
"Playfair",
|
||||
"Roboto",
|
||||
"Roboto Mono",
|
||||
"Times New Roman",
|
||||
|
|
|
@ -321,17 +321,44 @@ export default {
|
|||
name: "Form",
|
||||
description: "A component that generates a form from your data.",
|
||||
icon: "ri-file-edit-fill",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [{ label: "Model", key: "model", control: ModelSelect }],
|
||||
},
|
||||
_component: "@budibase/standard-components/dataform",
|
||||
template: {
|
||||
component: "@budibase/materialdesign-components/Form",
|
||||
description: "Form for saving a record",
|
||||
name: "@budibase/materialdesign-components/recordForm",
|
||||
},
|
||||
children: [],
|
||||
commonProps: {},
|
||||
children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/dataform",
|
||||
name: "Form Basic",
|
||||
icon: "ri-file-edit-fill",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [
|
||||
{
|
||||
label: "Model",
|
||||
key: "model",
|
||||
control: ModelSelect,
|
||||
},
|
||||
],
|
||||
},
|
||||
template: {
|
||||
component: "@budibase/materialdesign-components/Form",
|
||||
description: "Form for saving a record",
|
||||
name: "@budibase/materialdesign-components/recordForm",
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "@budibase/standard-components/dataformwide",
|
||||
name: "Form Wide",
|
||||
icon: "ri-file-edit-fill",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [
|
||||
{
|
||||
label: "Model",
|
||||
key: "model",
|
||||
control: ModelSelect,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Chart",
|
||||
|
@ -379,7 +406,7 @@ export default {
|
|||
{
|
||||
name: "List",
|
||||
_component: "@budibase/standard-components/list",
|
||||
description: "Shiny list",
|
||||
description: "Renders all children once per record, of a given model",
|
||||
icon: "ri-file-list-fill",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
|
@ -387,6 +414,18 @@ export default {
|
|||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "Record Detail",
|
||||
_component: "@budibase/standard-components/recorddetail",
|
||||
description:
|
||||
"Loads a record, using an id from the URL, which can be used with {{ context }}, in children",
|
||||
icon: "ri-profile-line",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [{ label: "Model", key: "model", control: ModelSelect }],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "Map",
|
||||
_component: "@budibase/standard-components/datamap",
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { notifier } from "builderStore/store/notifications"
|
||||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
||||
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
|
@ -90,26 +91,21 @@
|
|||
{testResult}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="workflow-button hoverable" on:click={testWorkflow}>
|
||||
Test
|
||||
</button>
|
||||
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedTab === 'SETUP'}
|
||||
{#if workflowBlock}
|
||||
<WorkflowBlockSetup {workflowBlock} />
|
||||
<div class="buttons">
|
||||
<button
|
||||
<Button
|
||||
green
|
||||
wide
|
||||
data-cy="save-workflow-setup"
|
||||
class="workflow-button hoverable"
|
||||
on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
</button>
|
||||
<button
|
||||
class="delete-workflow-button hoverable"
|
||||
on:click={deleteWorkflowBlock}>
|
||||
Delete Block
|
||||
</button>
|
||||
</Button>
|
||||
<Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button>
|
||||
</div>
|
||||
{:else if $workflowStore.currentWorkflow}
|
||||
<div class="panel">
|
||||
|
@ -141,15 +137,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
<Button
|
||||
green
|
||||
wide
|
||||
data-cy="save-workflow-setup"
|
||||
class="workflow-button hoverable"
|
||||
on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
</button>
|
||||
<button class="delete-workflow-button" on:click={deleteWorkflow}>
|
||||
Delete Workflow
|
||||
</button>
|
||||
</Button>
|
||||
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -175,11 +170,12 @@
|
|||
}
|
||||
|
||||
header {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-family: inter;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
|
@ -190,13 +186,12 @@
|
|||
.block-label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--ink);
|
||||
margin: 0px 0px 16px 0px;
|
||||
color: var(--grey-7);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
margin: 0px 0px 4px 0px;
|
||||
padding: 12px 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.budibase_input {
|
||||
|
@ -214,6 +209,7 @@
|
|||
header > span {
|
||||
color: var(--grey-5);
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form {
|
||||
|
@ -228,52 +224,10 @@
|
|||
|
||||
.buttons {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.delete-workflow-button {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--red);
|
||||
border-radius: 3px;
|
||||
width: 260px;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--white);
|
||||
color: var(--red);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 2ms;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.delete-workflow-button:hover {
|
||||
background: var(--red);
|
||||
border: 1px solid var(--red);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.workflow-button {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--grey-4);
|
||||
border-radius: 3px;
|
||||
bottom: 20px;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: white;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 2ms;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.workflow-button:hover {
|
||||
background: var(--grey-1);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.access-level {
|
||||
|
@ -301,7 +255,7 @@
|
|||
}
|
||||
|
||||
.passed {
|
||||
background: #84c991;
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.failed {
|
||||
|
|
|
@ -13,85 +13,84 @@
|
|||
: []
|
||||
</script>
|
||||
|
||||
<label class="uk-form-label">{workflowBlock.type}: {workflowBlock.name}</label>
|
||||
<label class="selected-label">{workflowBlock.type}: {workflowBlock.name}</label>
|
||||
{#each workflowParams as [parameter, type]}
|
||||
<div class="block-field">
|
||||
<label class="uk-form-label">{parameter}</label>
|
||||
<div class="uk-form-controls">
|
||||
{#if Array.isArray(type)}
|
||||
<select
|
||||
class="budibase_input"
|
||||
bind:value={workflowBlock.args[parameter]}>
|
||||
{#each type as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if type === 'component'}
|
||||
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'accessLevel'}
|
||||
<select
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]}>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="POWER_USER">Power User</option>
|
||||
</select>
|
||||
{:else if type === 'password'}
|
||||
<input
|
||||
type="password"
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'number'}
|
||||
<input
|
||||
type="number"
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'longText'}
|
||||
<textarea
|
||||
type="text"
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'model'}
|
||||
<ModelSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'record'}
|
||||
<RecordSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'string'}
|
||||
<input
|
||||
type="text"
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{/if}
|
||||
</div>
|
||||
<label class="label">{parameter}</label>
|
||||
{#if Array.isArray(type)}
|
||||
<select class="budibase_input" bind:value={workflowBlock.args[parameter]}>
|
||||
{#each type as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if type === 'component'}
|
||||
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'accessLevel'}
|
||||
<select class="budibase_input" bind:value={workflowBlock.args[parameter]}>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="POWER_USER">Power User</option>
|
||||
</select>
|
||||
{:else if type === 'password'}
|
||||
<input
|
||||
type="password"
|
||||
class="budibase_input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'number'}
|
||||
<input
|
||||
type="number"
|
||||
class="budibase_input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'longText'}
|
||||
<textarea
|
||||
type="text"
|
||||
class="budibase_input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'model'}
|
||||
<ModelSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'record'}
|
||||
<RecordSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'string'}
|
||||
<input
|
||||
type="text"
|
||||
class="budibase_input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.block-field {
|
||||
border-radius: 3px;
|
||||
background: var(--grey-1);
|
||||
padding: 12px;
|
||||
margin: 0px 0px 4px 0px;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.budibase_input {
|
||||
height: 36px;
|
||||
width: 220px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--grey-4);
|
||||
border-radius: 5px;
|
||||
background-color: var(--grey-2);
|
||||
border: 1px solid var(--grey-2);
|
||||
text-align: left;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
padding-left: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
text-transform: capitalize;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.selected-label {
|
||||
text-transform: capitalize;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
font-family: inherit;
|
||||
padding: 5px;
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -81,7 +81,7 @@
|
|||
}
|
||||
|
||||
.play-button.highlighted {
|
||||
background: var(--primary);
|
||||
background: var(--purple);
|
||||
}
|
||||
|
||||
.stop-button.highlighted {
|
||||
|
|
|
@ -67,6 +67,10 @@
|
|||
color: var(--ink);
|
||||
}
|
||||
|
||||
p {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
div:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { onMount, getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
|
@ -27,10 +28,7 @@
|
|||
</script>
|
||||
|
||||
<section>
|
||||
<button class="new-workflow-button hoverable" on:click={newWorkflow}>
|
||||
<i class="icon ri-add-circle-fill" />
|
||||
Create New Workflow
|
||||
</button>
|
||||
<Button purple wide on:click{newWorkflow}>Create New Workflow</Button>
|
||||
<ul>
|
||||
{#each $workflowStore.workflows as workflow}
|
||||
<li
|
||||
|
@ -74,10 +72,10 @@
|
|||
|
||||
.workflow-item {
|
||||
display: flex;
|
||||
border-radius: 3px;
|
||||
border-radius: 5px;
|
||||
padding-left: 12px;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
height: 36px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
import Modal from "svelte-simple-modal"
|
||||
import { store, workflowStore } from "builderStore"
|
||||
import SettingsLink from "components/settings/Link.svelte"
|
||||
import { get } from "builderStore/api"
|
||||
|
||||
import { fade } from "svelte/transition"
|
||||
import { isActive, goto, layout } from "@sveltech/routify"
|
||||
import { isActive, goto, layout, url } from "@sveltech/routify"
|
||||
|
||||
import { SettingsIcon, PreviewIcon } from "components/common/Icons/"
|
||||
import IconButton from "components/common/IconButton.svelte"
|
||||
|
@ -26,6 +27,22 @@
|
|||
throw new Error(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
// handles navigation between frontend, backend, workflow.
|
||||
// this remembers your last place on each of the sections
|
||||
// e.g. if one of your screens is selected on front end, then
|
||||
// you browse to backend, when you click fronend, you will be
|
||||
// brought back to the same screen
|
||||
const topItemNavigate = path => () => {
|
||||
const activeTopNav = $layout.children.find(c => $isActive(c.path))
|
||||
if (!activeTopNav) return
|
||||
store.update(state => {
|
||||
if (!state.previousTopNavPath) state.previousTopNavPath = {}
|
||||
state.previousTopNavPath[activeTopNav.path] = window.location.pathname.replace("/_builder", "")
|
||||
$goto(state.previousTopNavPath[path] || path)
|
||||
return state
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
|
@ -45,7 +62,7 @@
|
|||
<span
|
||||
class:active={$isActive(path)}
|
||||
class="topnavitem"
|
||||
on:click={() => $goto(path)}>
|
||||
on:click={topItemNavigate(path)}>
|
||||
{title}
|
||||
</span>
|
||||
{/each}
|
||||
|
@ -54,12 +71,7 @@
|
|||
hoverColor="var(--secondary75)"/> -->
|
||||
</div>
|
||||
<div class="toprightnav">
|
||||
<span
|
||||
class:active={$isActive(`/settings`)}
|
||||
class="topnavitemright"
|
||||
on:click={() => $goto(`/settings`)}>
|
||||
<SettingsIcon />
|
||||
</span>
|
||||
<SettingsLink />
|
||||
<span
|
||||
class:active={false}
|
||||
class="topnavitemright"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<slot />
|
|
@ -1,36 +0,0 @@
|
|||
<script>
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
$: instances = $store.appInstances
|
||||
|
||||
async function selectDatabase(database) {
|
||||
backendUiStore.actions.database.select(database)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($store.appInstances.length > 0) {
|
||||
await selectDatabase($store.appInstances[0])
|
||||
$goto(`./${$backendUiStore.selectedDatabase._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="node-view">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.node-view {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
|
@ -1,20 +0,0 @@
|
|||
<script>
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
$: instances = $store.appInstances
|
||||
|
||||
async function selectDatabase(database) {
|
||||
backendUiStore.actions.database.select(database)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($store.appInstances.length > 0) {
|
||||
await selectDatabase($store.appInstances[0])
|
||||
$goto(`../${$backendUiStore.selectedDatabase._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Please select a database
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { goto } from "@sveltech/routify"
|
||||
$goto("../database")
|
||||
$goto("../model")
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=false -->
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<script>
|
||||
import { params } from "@sveltech/routify"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
if ($params.selectedModel) {
|
||||
const model = $backendUiStore.models.find(m => m._id === $params.selectedModel)
|
||||
if (model) {
|
||||
backendUiStore.actions.models.select(model)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -3,7 +3,7 @@
|
|||
import { Button } from "@budibase/bbui"
|
||||
import EmptyModel from "components/nav/ModelNavigator/EmptyModel.svelte"
|
||||
import ModelDataTable from "components/database/ModelDataTable"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import * as api from "components/database/ModelDataTable/api"
|
||||
import { CreateEditRecordModal } from "components/database/ModelDataTable/modals"
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { goto, leftover } from "@sveltech/routify"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
async function selectModel(model) {
|
||||
backendUiStore.actions.models.select(model)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// navigate to first model in list, if not already selected
|
||||
// and this is the final url (i.e. no selectedModel)
|
||||
if (!$leftover && $backendUiStore.models.length > 0 && (!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)) {
|
||||
$goto(`./${$backendUiStore.models[0]._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="node-view">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.node-view {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { goto, leftover } from "@sveltech/routify"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
async function selectModel(model) {
|
||||
backendUiStore.actions.models.select(model)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// navigate to first model in list, if not already selected
|
||||
// and this is the final url (i.e. no selectedModel)
|
||||
if (!$leftover && $backendUiStore.models.length > 0 && (!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)) {
|
||||
// this file routes as .../models/index, so, go up one.
|
||||
$goto(`../${$backendUiStore.models[0]._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $backendUiStore.models.length === 0}
|
||||
Please create a model
|
||||
{:else}
|
||||
Please select a model
|
||||
{/if}
|
|
@ -14,7 +14,7 @@
|
|||
if ($leftover) {
|
||||
// Get the correct screen children.
|
||||
const screenChildren = $store.pages[$params.page]._screens.find(
|
||||
screen => screen.name === $params.screen
|
||||
screen => screen.props._instanceName === $params.screen
|
||||
).props._children
|
||||
findComponent(componentIds, screenChildren)
|
||||
}
|
||||
|
|
|
@ -88,7 +88,6 @@
|
|||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.preview-pane {
|
||||
|
|
|
@ -56,13 +56,13 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
|||
|
||||
const screenIndex = current !== -1 ? current : fallback
|
||||
|
||||
onScreenSelected(screens[screenIndex], _url)
|
||||
|
||||
try {
|
||||
!url.state && history.pushState(_url, null, _url)
|
||||
} catch (_) {
|
||||
// ignoring an exception here as the builder runs an iframe, which does not like this
|
||||
}
|
||||
|
||||
onScreenSelected(screens[screenIndex], _url)
|
||||
}
|
||||
|
||||
function click(e) {
|
||||
|
|
|
@ -59,7 +59,7 @@ const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
|
|||
const propValue = props[propName]
|
||||
|
||||
// A little bit of a hack - won't bind if the string doesn't start with {{
|
||||
const isBound = typeof propValue === "string" && propValue.startsWith("{{")
|
||||
const isBound = typeof propValue === "string" && propValue.includes("{{")
|
||||
|
||||
if (isBound) {
|
||||
initialProps[propName] = mustache.render(propValue, {
|
||||
|
|
|
@ -63,7 +63,19 @@ exports.create = async function(ctx) {
|
|||
}
|
||||
}
|
||||
|
||||
exports.update = async function() {}
|
||||
exports.update = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const user = ctx.request.body
|
||||
const dbUser = db.get(ctx.request.body._id)
|
||||
const newData = { ...dbUser, ...user }
|
||||
|
||||
const response = await db.put(newData)
|
||||
user._rev = response.rev
|
||||
|
||||
ctx.status = 200
|
||||
ctx.message = `User ${ctx.request.body.username} updated successfully.`
|
||||
ctx.body = response
|
||||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const database = new CouchDB(ctx.user.instanceId)
|
||||
|
|
|
@ -8,6 +8,7 @@ const router = Router()
|
|||
router
|
||||
.get("/api/users", authorized(LIST_USERS), controller.fetch)
|
||||
.get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find)
|
||||
.put("/api/users/", authorized(USER_MANAGEMENT), controller.update)
|
||||
.post("/api/users", authorized(USER_MANAGEMENT), controller.create)
|
||||
.delete(
|
||||
"/api/users/:username",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"stylesheets": [],
|
||||
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
|
||||
"props" : {
|
||||
"_id": "private-master-root",
|
||||
"_component": "@budibase/standard-components/container",
|
||||
"_children": [
|
||||
{
|
||||
|
@ -19,7 +20,6 @@
|
|||
"_children": []
|
||||
}
|
||||
],
|
||||
"_id": 0,
|
||||
"type": "div",
|
||||
"_styles": {
|
||||
"active": {},
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"favicon": "./_shared/favicon.png",
|
||||
"stylesheets": [],
|
||||
"props": {
|
||||
"_id": "public-master-root",
|
||||
"_component": "@budibase/standard-components/container",
|
||||
"_children": [
|
||||
{
|
||||
|
@ -31,7 +32,6 @@
|
|||
"logo": ""
|
||||
}
|
||||
],
|
||||
"_id": 1,
|
||||
"type": "div",
|
||||
"_styles": {
|
||||
"layout": {},
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{ each(options.stylesheets) }}
|
||||
|
@ -31,6 +34,9 @@
|
|||
<link rel='stylesheet' href='/assets{{ pageStyle }}'>
|
||||
{{ /if }}
|
||||
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
|
||||
|
||||
<script src='/assets/clientFrontendDefinition.js'></script>
|
||||
<script src='/assets/budibase-client.js'></script>
|
||||
|
||||
|
|
|
@ -209,6 +209,13 @@
|
|||
"model": "models"
|
||||
}
|
||||
},
|
||||
"dataformwide": {
|
||||
"description": "an HTML table that fetches data from a model or view and displays it.",
|
||||
"data": true,
|
||||
"props": {
|
||||
"model": "models"
|
||||
}
|
||||
},
|
||||
"datalist": {
|
||||
"description": "A configurable data list that attaches to your backend models.",
|
||||
"data": true,
|
||||
|
@ -228,15 +235,14 @@
|
|||
"description": "A configurable data list that attaches to your backend models.",
|
||||
"data": true,
|
||||
"props": {
|
||||
"model": "models",
|
||||
"layout": {
|
||||
"type": "options",
|
||||
"default": "list",
|
||||
"options": [
|
||||
"list",
|
||||
"grid"
|
||||
]
|
||||
}
|
||||
"model": "models"
|
||||
}
|
||||
},
|
||||
"recorddetail": {
|
||||
"description": "Loads a record, using an ID in the url",
|
||||
"data": true,
|
||||
"props": {
|
||||
"model": "models"
|
||||
}
|
||||
},
|
||||
"datamap": {
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
export let _bb
|
||||
export let model
|
||||
|
||||
const TYPE_MAP = {
|
||||
string: "text",
|
||||
boolean: "checkbox",
|
||||
number: "number",
|
||||
}
|
||||
|
||||
let username
|
||||
let password
|
||||
let newModel = {
|
||||
|
@ -59,15 +65,23 @@
|
|||
|
||||
<form class="form" on:submit|preventDefault>
|
||||
<h1>{modelDef.name} Form</h1>
|
||||
<hr />
|
||||
<div class="form-content">
|
||||
{#each fields as field}
|
||||
<div class="form-item">
|
||||
<label class="form-label" for="form-stacked-text">{field}</label>
|
||||
<input
|
||||
class="input"
|
||||
placeholder={field}
|
||||
type={schema[field].type === 'string' ? 'text' : schema[field].type}
|
||||
on:change={handleInput(field)} />
|
||||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
|
||||
<select on:blur={handleInput(field)}>
|
||||
{#each schema[field].constraints.inclusion as opt}
|
||||
<option>{opt}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<input
|
||||
class="input"
|
||||
type={TYPE_MAP[schema[field].type]}
|
||||
on:change={handleInput(field)} />
|
||||
{/if}
|
||||
</div>
|
||||
<hr />
|
||||
{/each}
|
||||
|
@ -143,8 +157,42 @@
|
|||
}
|
||||
|
||||
button:hover {
|
||||
background-color: white;
|
||||
border-color: #393c44;
|
||||
color: #393c44;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
vertical-align: bottom;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
*overflow: hidden;
|
||||
}
|
||||
|
||||
select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
select {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
align-items: baseline;
|
||||
box-sizing: border-box;
|
||||
padding: 1em 1em;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 5px;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
-ms-appearance: none;
|
||||
appearance: none;
|
||||
background-repeat: no-repeat;
|
||||
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
|
||||
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
||||
background-position: right 17px top 1.5em, right 10px top 1.5em;
|
||||
background-size: 7px 7px, 7px 7px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
export let _bb
|
||||
export let model
|
||||
|
||||
const TYPE_MAP = {
|
||||
string: "text",
|
||||
boolean: "checkbox",
|
||||
number: "number",
|
||||
}
|
||||
|
||||
let username
|
||||
let password
|
||||
let newModel = {
|
||||
modelId: model,
|
||||
}
|
||||
let store = _bb.store
|
||||
let schema = {}
|
||||
let modelDef = {}
|
||||
$: if (model && model.length !== 0) {
|
||||
fetchModel()
|
||||
}
|
||||
$: fields = Object.keys(schema)
|
||||
async function fetchModel() {
|
||||
const FETCH_MODEL_URL = `/api/models/${model}`
|
||||
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||
modelDef = await response.json()
|
||||
schema = modelDef.schema
|
||||
}
|
||||
async function save() {
|
||||
const SAVE_RECORD_URL = `/api/${model}/records`
|
||||
const response = await _bb.api.post(SAVE_RECORD_URL, newModel)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
state[model] = state[model] ? [...state[model], json] : [json]
|
||||
return state
|
||||
})
|
||||
}
|
||||
const handleInput = field => event => {
|
||||
let value
|
||||
if (event.target.type === "checkbox") {
|
||||
value = event.target.checked
|
||||
newModel[field] = value
|
||||
return
|
||||
}
|
||||
if (event.target.type === "number") {
|
||||
value = parseInt(event.target.value)
|
||||
newModel[field] = value
|
||||
return
|
||||
}
|
||||
value = event.target.value
|
||||
newModel[field] = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="form" on:submit|preventDefault>
|
||||
<h1>{modelDef.name} Form</h1>
|
||||
<hr />
|
||||
<div class="form-content">
|
||||
{#each fields as field}
|
||||
<div class="form-item">
|
||||
<label class="form-label" for="form-stacked-text">{field}</label>
|
||||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
|
||||
<select on:blur={handleInput(field)}>
|
||||
{#each schema[field].constraints.inclusion as opt}
|
||||
<option>{opt}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<input
|
||||
class="input"
|
||||
type={TYPE_MAP[schema[field].type]}
|
||||
on:change={handleInput(field)} />
|
||||
{/if}
|
||||
</div>
|
||||
<hr />
|
||||
{/each}
|
||||
<div class="button-block">
|
||||
<button on:click={save}>Submit Form</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
padding: 40px;
|
||||
}
|
||||
.form-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.input {
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e6e6e6;
|
||||
padding: 1em;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: grid;
|
||||
grid-template-columns: 30% 1fr;
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #f5f5f5;
|
||||
margin: 40px 0px;
|
||||
}
|
||||
hr:nth-last-child(2) {
|
||||
border: 1px solid #f5f5f5;
|
||||
margin: 40px 0px;
|
||||
}
|
||||
.button-block {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
button {
|
||||
font-size: 16px;
|
||||
padding: 8px 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
background-color: black;
|
||||
outline: none;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease 0s;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
transform: scale(2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
select {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
align-items: baseline;
|
||||
box-sizing: border-box;
|
||||
padding: 1em 1em;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 5px;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
-ms-appearance: none;
|
||||
appearance: none;
|
||||
background-repeat: no-repeat;
|
||||
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
|
||||
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
||||
background-position: right 17px top 1.5em, right 10px top 1.5em;
|
||||
background-size: 7px 7px, 7px 7px;
|
||||
}
|
||||
</style>
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
export let _bb
|
||||
export let model
|
||||
export let layout = "list"
|
||||
|
||||
let headers = []
|
||||
let store = _bb.store
|
||||
|
@ -33,39 +32,5 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<section class:grid={layout === 'grid'} class:list={layout === 'list'}>
|
||||
<div class="data-card" bind:this={target} />
|
||||
<section bind:this={target}>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 5px 0 5px 0;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.data-key {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
|
||||
export let _bb
|
||||
export let model
|
||||
|
||||
let headers = []
|
||||
let store = _bb.store
|
||||
let target
|
||||
|
||||
async function fetchFirstRecord() {
|
||||
const FETCH_RECORDS_URL = `/api/views/all_${model}`
|
||||
const response = await _bb.api.get(FETCH_RECORDS_URL)
|
||||
if (response.status === 200) {
|
||||
const allRecords = await response.json()
|
||||
if (allRecords.length > 0) return allRecords[0]
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
const pathParts = window.location.pathname.split("/")
|
||||
|
||||
let record
|
||||
// if srcdoc, then we assume this is the builder preview
|
||||
if(pathParts.length === 0 || pathParts[0] === "srcdoc") {
|
||||
record = await fetchFirstRecord()
|
||||
} else {
|
||||
const id = pathParts[pathParts.length - 1]
|
||||
const GET_RECORD_URL = `/api/${model}/records/${id}`
|
||||
const response = await _bb.api.get(GET_RECORD_URL)
|
||||
if (response.status === 200) {
|
||||
record = await response.json()
|
||||
}
|
||||
}
|
||||
|
||||
if (record) {
|
||||
_bb.attachChildren(target, {
|
||||
hydrate: false,
|
||||
context: record,
|
||||
})
|
||||
} else {
|
||||
throw new Error("Failed to fetch record.", response)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<section bind:this={target}>
|
||||
</section>
|
|
@ -16,9 +16,11 @@ export { default as icon } from "./Icon.svelte"
|
|||
export { default as Navigation } from "./Navigation.svelte"
|
||||
export { default as datatable } from "./DataTable.svelte"
|
||||
export { default as dataform } from "./DataForm.svelte"
|
||||
export { default as dataformwide } from "./DataFormWide.svelte"
|
||||
export { default as datachart } from "./DataChart.svelte"
|
||||
export { default as datalist } from "./DataList.svelte"
|
||||
export { default as list } from "./List.svelte"
|
||||
export { default as datasearch } from "./DataSearch.svelte"
|
||||
export { default as datamap } from "./DataMap.svelte"
|
||||
export { default as embed } from "./Embed.svelte"
|
||||
export { default as recorddetail } from "./RecordDetail.svelte"
|
||||
|
|
Loading…
Reference in New Issue