Merge branch 'master' into new-list-spike2
|
@ -1,29 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill:#FFFFFF;}
|
|
||||||
.st1{fill:#6A78D1;}
|
|
||||||
.st2{fill:#49C39E;}
|
|
||||||
.st3{fill:#F2545B;}
|
|
||||||
.st4{fill:#F5C26B;}
|
|
||||||
</style>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path class="st0" d="M215,14.1l136.2,79.8c9.3,5.4,15,15.5,15,26.4v159.5c0,10.9-5.7,20.9-15,26.4L215,385.9
|
|
||||||
c-9.3,5.4-20.7,5.4-30,0L48.8,306.2c-9.3-5.4-15-15.5-15-26.4V120.2c0-10.9,5.7-20.9,15-26.4L185,14.1
|
|
||||||
C194.3,8.6,205.7,8.6,215,14.1z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="st1" d="M288.8,273.7l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6
|
|
||||||
c3.6-1.9,8-1.9,11.6,0l83,43.6C297.1,258.3,297.1,269.4,288.8,273.7z"/>
|
|
||||||
<path class="st2" d="M288.8,231.2l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6
|
|
||||||
c3.6-1.9,8-1.9,11.6,0l83,43.6C297.1,215.7,297.1,226.9,288.8,231.2z"/>
|
|
||||||
<path class="st3" d="M288.8,188.6l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6
|
|
||||||
c3.6-1.9,8-1.9,11.6,0l83,43.6C297.1,173.1,297.1,184.3,288.8,188.6z"/>
|
|
||||||
<path class="st4" d="M288.8,146l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6c3.6-1.9,8-1.9,11.6,0
|
|
||||||
l83,43.6C297.1,130.6,297.1,141.7,288.8,146z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -3,6 +3,7 @@ const apiCall = method => async (url, body) => {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"x-user-agent": "Budibase Builder",
|
||||||
},
|
},
|
||||||
body: body && JSON.stringify(body),
|
body: body && JSON.stringify(body),
|
||||||
})
|
})
|
||||||
|
@ -14,11 +15,11 @@ const apiCall = method => async (url, body) => {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = apiCall("POST")
|
export const post = apiCall("POST")
|
||||||
const get = apiCall("GET")
|
export const get = apiCall("GET")
|
||||||
const patch = apiCall("PATCH")
|
export const patch = apiCall("PATCH")
|
||||||
const del = apiCall("DELETE")
|
export const del = apiCall("DELETE")
|
||||||
const put = apiCall("PUT")
|
export const put = apiCall("PUT")
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
post,
|
post,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { get } from "builderStore/api"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the definitions for component library components. This includes
|
* Fetches the definitions for component library components. This includes
|
||||||
* their props and other metadata from components.json.
|
* their props and other metadata from components.json.
|
||||||
|
@ -6,7 +8,7 @@
|
||||||
export const fetchComponentLibDefinitions = async appId => {
|
export const fetchComponentLibDefinitions = async appId => {
|
||||||
const LIB_DEFINITION_URL = `/${appId}/components/definitions`
|
const LIB_DEFINITION_URL = `/${appId}/components/definitions`
|
||||||
try {
|
try {
|
||||||
const libDefinitionResponse = await fetch(LIB_DEFINITION_URL)
|
const libDefinitionResponse = await get(LIB_DEFINITION_URL)
|
||||||
return await libDefinitionResponse.json()
|
return await libDefinitionResponse.json()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error fetching component definitions for ${appId}`, err)
|
console.error(`Error fetching component definitions for ${appId}`, err)
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: var(--grey-dark) 1px solid;
|
border: var(--grey-dark) 1px solid;
|
||||||
max-width: 256px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.adjusted {
|
.adjusted {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
|
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
|
import { post } from "builderStore/api"
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
const { open, close } = getContext("simple-modal")
|
||||||
|
|
||||||
|
@ -33,15 +34,7 @@
|
||||||
const data = { name, description }
|
const data = { name, description }
|
||||||
loading = true
|
loading = true
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/applications", {
|
const response = await post("/api/applications", data)
|
||||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
|
||||||
credentials: "same-origin", // include, *same-origin, omit
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data), // body data type must match "Content-Type" header
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await response.json()
|
const res = await response.json()
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export default ({
|
||||||
|
|
||||||
body, html {
|
body, html {
|
||||||
height: 100%!important;
|
height: 100%!important;
|
||||||
|
font-family: Roboto !important;
|
||||||
}
|
}
|
||||||
.lay-__screenslot__text {
|
.lay-__screenslot__text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
import { Button } from "@budibase/bbui"
|
||||||
import Modal from "../../common/Modal.svelte"
|
import Modal from "../../common/Modal.svelte"
|
||||||
import HandlerSelector from "./HandlerSelector.svelte"
|
import HandlerSelector from "./HandlerSelector.svelte"
|
||||||
import IconButton from "../../common/IconButton.svelte"
|
import IconButton from "../../common/IconButton.svelte"
|
||||||
|
@ -8,12 +9,12 @@
|
||||||
import Select from "../../common/Select.svelte"
|
import Select from "../../common/Select.svelte"
|
||||||
import Input from "../../common/Input.svelte"
|
import Input from "../../common/Input.svelte"
|
||||||
import getIcon from "../../common/icon"
|
import getIcon from "../../common/icon"
|
||||||
|
import { CloseIcon } from "components/common/Icons/"
|
||||||
|
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
|
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
export let eventOptions = []
|
export let eventOptions = []
|
||||||
export let open
|
|
||||||
export let onClose
|
export let onClose
|
||||||
|
|
||||||
let eventType = ""
|
let eventType = ""
|
||||||
|
@ -62,105 +63,111 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:isOpen={open} onClosed={closeModal}>
|
<div class="container">
|
||||||
<h2>
|
<div class="body">
|
||||||
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
|
<div class="heading">
|
||||||
</h2>
|
<h3>
|
||||||
<a href="https://docs.budibase.com/" target="_blank">
|
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
|
||||||
Click here to learn more about component events
|
</h3>
|
||||||
</a>
|
</div>
|
||||||
|
<div class="event-options">
|
||||||
|
<div class="section">
|
||||||
|
<h4>Event Type</h4>
|
||||||
|
<Select bind:value={eventType}>
|
||||||
|
{#each eventOptions as option}
|
||||||
|
<option value={option.name}>{option.name}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="event-options">
|
<div class="section">
|
||||||
<div>
|
<h4>Event Action(s)</h4>
|
||||||
<header>
|
<HandlerSelector
|
||||||
<h5>Event Type</h5>
|
newHandler
|
||||||
{@html getIcon('info', 20)}
|
onChanged={updateDraftEventHandler}
|
||||||
</header>
|
onCreate={() => {
|
||||||
<Select bind:value={eventType}>
|
createNewEventHandler(draftEventHandler)
|
||||||
{#each eventOptions as option}
|
draftEventHandler = { parameters: [] }
|
||||||
<option value={option.name}>{option.name}</option>
|
}}
|
||||||
{/each}
|
handler={draftEventHandler} />
|
||||||
</Select>
|
</div>
|
||||||
|
{#if eventData}
|
||||||
|
{#each eventData.handlers as handler, index}
|
||||||
|
<HandlerSelector
|
||||||
|
{index}
|
||||||
|
onChanged={updateEventHandler}
|
||||||
|
onRemoved={() => deleteEventHandler(index)}
|
||||||
|
{handler} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
{#if eventData.name}
|
||||||
|
<Button
|
||||||
|
outline
|
||||||
|
on:click={deleteEvent}
|
||||||
|
disabled={eventData.handlers.length === 0}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<div class="save">
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
on:click={saveEventData}
|
||||||
|
disabled={eventData.handlers.length === 0}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="close-button" on:click={closeModal}>
|
||||||
<header>
|
<CloseIcon />
|
||||||
<h5>Event Action(s)</h5>
|
|
||||||
{@html getIcon('info', 20)}
|
|
||||||
</header>
|
|
||||||
<HandlerSelector
|
|
||||||
newHandler
|
|
||||||
onChanged={updateDraftEventHandler}
|
|
||||||
onCreate={() => {
|
|
||||||
createNewEventHandler(draftEventHandler)
|
|
||||||
draftEventHandler = { parameters: [] }
|
|
||||||
}}
|
|
||||||
handler={draftEventHandler} />
|
|
||||||
{#if eventData}
|
|
||||||
{#each eventData.handlers as handler, index}
|
|
||||||
<HandlerSelector
|
|
||||||
{index}
|
|
||||||
onChanged={updateEventHandler}
|
|
||||||
onRemoved={() => deleteEventHandler(index)}
|
|
||||||
{handler} />
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<ActionButton
|
|
||||||
alert
|
|
||||||
disabled={eventData.handlers.length === 0}
|
|
||||||
hidden={!eventData.name}
|
|
||||||
on:click={deleteEvent}>
|
|
||||||
Delete
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
disabled={eventData.handlers.length === 0}
|
|
||||||
on:click={saveEventData}>
|
|
||||||
Save
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h2 {
|
.container {
|
||||||
color: var(--primary100);
|
position: relative;
|
||||||
font-size: 20px;
|
}
|
||||||
font-weight: bold;
|
.heading {
|
||||||
margin-bottom: 0;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
.close-button {
|
||||||
color: rgba(22, 48, 87, 0.6);
|
cursor: pointer;
|
||||||
font-size: 15px;
|
position: absolute;
|
||||||
margin: 0;
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
.close-button :global(svg) {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-options {
|
h4 {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions,
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
margin-top: 30px;
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
h3 {
|
||||||
color: rgba(22, 48, 87, 0.6);
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 24px;
|
||||||
margin-top: 0;
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
padding: 40px;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 20px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 30px 40px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom-right-radius: 50px;
|
||||||
|
background-color: var(--grey-light);
|
||||||
|
}
|
||||||
|
.save {
|
||||||
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
import {
|
import {
|
||||||
keys,
|
keys,
|
||||||
map,
|
map,
|
||||||
|
@ -17,7 +18,6 @@
|
||||||
import PlusButton from "components/common/PlusButton.svelte"
|
import PlusButton from "components/common/PlusButton.svelte"
|
||||||
import IconButton from "components/common/IconButton.svelte"
|
import IconButton from "components/common/IconButton.svelte"
|
||||||
import EventEditorModal from "./EventEditorModal.svelte"
|
import EventEditorModal from "./EventEditorModal.svelte"
|
||||||
import HandlerSelector from "./HandlerSelector.svelte"
|
|
||||||
|
|
||||||
import { PencilIcon } from "components/common/Icons"
|
import { PencilIcon } from "components/common/Icons"
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
||||||
|
@ -26,7 +26,6 @@
|
||||||
|
|
||||||
export let component
|
export let component
|
||||||
|
|
||||||
let modalOpen = false
|
|
||||||
let events = []
|
let events = []
|
||||||
let selectedEvent = null
|
let selectedEvent = null
|
||||||
|
|
||||||
|
@ -40,14 +39,28 @@
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle create app modal
|
||||||
|
const { open, close } = getContext("simple-modal")
|
||||||
|
|
||||||
const openModal = event => {
|
const openModal = event => {
|
||||||
selectedEvent = event
|
selectedEvent = event
|
||||||
modalOpen = true
|
open(
|
||||||
}
|
EventEditorModal,
|
||||||
|
{
|
||||||
const closeModal = () => {
|
eventOptions: events,
|
||||||
selectedEvent = null
|
event: selectedEvent,
|
||||||
modalOpen = false
|
onClose: () => {
|
||||||
|
close()
|
||||||
|
selectedEvent = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
closeButton: false,
|
||||||
|
closeOnEsc: false,
|
||||||
|
styleContent: { padding: 0 },
|
||||||
|
closeOnOuterClick: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -71,11 +84,6 @@
|
||||||
{/each}
|
{/each}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<EventEditorModal
|
|
||||||
open={modalOpen}
|
|
||||||
onClose={closeModal}
|
|
||||||
eventOptions={events}
|
|
||||||
event={selectedEvent} />
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h3 {
|
h3 {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { Button } from "@budibase/bbui"
|
||||||
import IconButton from "components/common/IconButton.svelte"
|
import IconButton from "components/common/IconButton.svelte"
|
||||||
import PlusButton from "components/common/PlusButton.svelte"
|
import PlusButton from "components/common/PlusButton.svelte"
|
||||||
import Select from "components/common/Select.svelte"
|
import Select from "components/common/Select.svelte"
|
||||||
|
@ -85,27 +86,28 @@
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{#if parameters}
|
{#if parameters}
|
||||||
|
<br />
|
||||||
{#each parameters as parameter, idx}
|
{#each parameters as parameter, idx}
|
||||||
<StateBindingCascader onChange={onParameterChanged(idx)} {parameter} />
|
<StateBindingCascader on:change={onParameterChanged(idx)} {parameter} />
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
<div class="event-action-button">
|
|
||||||
{#if parameters.length > 0}
|
{#if parameters.length > 0}
|
||||||
{#if newHandler}
|
<div class="button-container">
|
||||||
<PlusButton on:click={onCreate} />
|
{#if newHandler}
|
||||||
{:else}
|
<Button primary thin on:click={onCreate}>Add Action</Button>
|
||||||
<IconButton icon="x" on:click={onRemoved} />
|
{:else}
|
||||||
{/if}
|
<Button outline thin on:click={onRemoved}>Remove Action</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.type-selector-container {
|
.type-selector-container {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: space-between;
|
grid-gap: 20px;
|
||||||
align-items: center;
|
width: 100%;
|
||||||
background: rgba(223, 223, 223, 0.5);
|
background: rgba(223, 223, 223, 0.5);
|
||||||
border: 1px solid #dfdfdf;
|
border: 1px solid #dfdfdf;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
|
@ -122,17 +124,19 @@
|
||||||
|
|
||||||
.handler-controls {
|
.handler-controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: 1fr;
|
||||||
grid-gap: 10px;
|
grid-gap: 20px;
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-action-button {
|
.button-container {
|
||||||
margin-right: 20px;
|
display: grid;
|
||||||
|
justify-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 13px;
|
font-size: 18px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { Input } from "@budibase/bbui"
|
||||||
import IconButton from "components/common/IconButton.svelte"
|
import IconButton from "components/common/IconButton.svelte"
|
||||||
import PlusButton from "components/common/PlusButton.svelte"
|
import PlusButton from "components/common/PlusButton.svelte"
|
||||||
import Select from "components/common/Select.svelte"
|
import Select from "components/common/Select.svelte"
|
||||||
import Input from "components/common/Input.svelte"
|
|
||||||
import { find, map, keys, reduce, keyBy } from "lodash/fp"
|
import { find, map, keys, reduce, keyBy } from "lodash/fp"
|
||||||
import { pipe } from "components/common/core"
|
import { pipe } from "components/common/core"
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
||||||
|
@ -10,64 +10,43 @@
|
||||||
import { ArrowDownIcon } from "components/common/Icons/"
|
import { ArrowDownIcon } from "components/common/Icons/"
|
||||||
|
|
||||||
export let parameter
|
export let parameter
|
||||||
export let onChange
|
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
|
||||||
|
const capitalize = s => {
|
||||||
|
if (typeof s !== "string") return ""
|
||||||
|
return s.charAt(0).toUpperCase() + s.slice(1)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="handler-option">
|
<div class="handler-option">
|
||||||
<span>{parameter.name}</span>
|
{#if parameter.name === 'workflow'}
|
||||||
<div class="handler-input">
|
<span>{parameter.name}</span>
|
||||||
{#if parameter.name === 'workflow'}
|
{/if}
|
||||||
<select
|
{#if parameter.name === 'workflow'}
|
||||||
class="budibase__input"
|
<Select on:change bind:value={parameter.value}>
|
||||||
on:change={onChange}
|
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
|
||||||
bind:value={parameter.value}>
|
<option value={workflow._id}>{workflow.name}</option>
|
||||||
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
|
{/each}
|
||||||
<option value={workflow._id}>{workflow.name}</option>
|
</Select>
|
||||||
{/each}
|
{:else}
|
||||||
</select>
|
<Input
|
||||||
{:else}
|
name={parameter.name}
|
||||||
<Input on:change={onChange} value={parameter.value} />
|
label={capitalize(parameter.name)}
|
||||||
<button on:click={() => (isOpen = !isOpen)}>
|
on:change
|
||||||
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
|
value={parameter.value} />
|
||||||
<ArrowDownIcon size={36} />
|
{/if}
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: rgba(249, 249, 249, 1);
|
|
||||||
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: rgba(22, 48, 87, 1);
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.handler-option {
|
.handler-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handler-input {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 13px;
|
font-size: 18px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="uk-margin block-field">
|
<div class="uk-margin block-field">
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
<select class="budibase__input" on:change {value}>
|
<select class="budibase__input" on:change {value}>
|
||||||
<option value=""></option>
|
<option value="" />
|
||||||
{#each $backendUiStore.models as model}
|
{#each $backendUiStore.models as model}
|
||||||
<option value={model._id}>{model.name}</option>
|
<option value={model._id}>{model.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if panelDefinition.length > 0}
|
{#if panelDefinition && panelDefinition.length > 0}
|
||||||
{#each panelDefinition as definition}
|
{#each panelDefinition as definition}
|
||||||
{#if propExistsOnComponentDef(definition.key)}
|
{#if propExistsOnComponentDef(definition.key)}
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
|
|
|
@ -267,7 +267,21 @@ export default {
|
||||||
"A component that automatically generates a login screen for your app.",
|
"A component that automatically generates a login screen for your app.",
|
||||||
icon: "ri-login-box-fill",
|
icon: "ri-login-box-fill",
|
||||||
children: [],
|
children: [],
|
||||||
properties: { design: { ...all } },
|
properties: {
|
||||||
|
design: { ...all },
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
label: "Name",
|
||||||
|
key: "name",
|
||||||
|
control: Input,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Logo",
|
||||||
|
key: "logo",
|
||||||
|
control: Input,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Table",
|
name: "Table",
|
||||||
|
@ -303,7 +317,28 @@ export default {
|
||||||
icon: "ri-bar-chart-fill",
|
icon: "ri-bar-chart-fill",
|
||||||
properties: {
|
properties: {
|
||||||
design: { ...all },
|
design: { ...all },
|
||||||
settings: [{ label: "Model", key: "model", control: ModelSelect }],
|
settings: [
|
||||||
|
{ label: "Model", key: "model", control: ModelSelect },
|
||||||
|
{
|
||||||
|
label: "Chart Type",
|
||||||
|
key: "type",
|
||||||
|
control: OptionSelect,
|
||||||
|
options: [
|
||||||
|
"column2d",
|
||||||
|
"column3d",
|
||||||
|
"line",
|
||||||
|
"area2d",
|
||||||
|
"bar2d",
|
||||||
|
"bar3d",
|
||||||
|
"pie2d",
|
||||||
|
"pie3d",
|
||||||
|
"doughnut2d",
|
||||||
|
"doughnut3d",
|
||||||
|
"pareto2d",
|
||||||
|
"pareto3d",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
|
@ -360,7 +395,15 @@ export default {
|
||||||
"A component for handling the navigation within your app.",
|
"A component for handling the navigation within your app.",
|
||||||
icon: "ri-navigation-fill",
|
icon: "ri-navigation-fill",
|
||||||
children: [],
|
children: [],
|
||||||
properties: { design: { ...all } },
|
properties: {
|
||||||
|
design: { ...all },
|
||||||
|
settings: [
|
||||||
|
{ label: "Logo URL", key: "logoUrl", control: Input },
|
||||||
|
{ label: "Title", key: "title", control: Input },
|
||||||
|
{ label: "Color", key: "color", control: Input },
|
||||||
|
{ label: "Background", key: "backgroundColor", control: Input },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<div class="uk-margin block-field">
|
<div class="uk-margin block-field">
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
<select class="budibase__input" bind:value>
|
<select class="budibase__input" bind:value>
|
||||||
|
<option value="" />
|
||||||
{#each $backendUiStore.models as model}
|
{#each $backendUiStore.models as model}
|
||||||
<option value={model}>{model.name}</option>
|
<option value={model}>{model.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 6.7 KiB |
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Modal from "svelte-simple-modal"
|
import Modal from "svelte-simple-modal"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
import { get } from "builderStore/api"
|
||||||
|
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { isActive, goto, layout } from "@sveltech/routify"
|
import { isActive, goto, layout } from "@sveltech/routify"
|
||||||
|
@ -14,7 +15,7 @@
|
||||||
let promise = getPackage()
|
let promise = getPackage()
|
||||||
|
|
||||||
async function getPackage() {
|
async function getPackage() {
|
||||||
const res = await fetch(`/api/${application}/appPackage`)
|
const res = await get(`/api/${application}/appPackage`)
|
||||||
const pkg = await res.json()
|
const pkg = await res.json()
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
|
@ -5,14 +5,14 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import IconButton from "components/common/IconButton.svelte"
|
import IconButton from "components/common/IconButton.svelte"
|
||||||
|
import { get } from "builderStore/api"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
|
|
||||||
let promise = getApps()
|
let promise = getApps()
|
||||||
|
|
||||||
async function getApps() {
|
async function getApps() {
|
||||||
const res = await fetch(`/api/applications`)
|
const res = await get("/api/applications")
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.0.32",
|
"version": "0.0.32",
|
||||||
"description": "Budibase CLI",
|
"description": "Budibase CLI",
|
||||||
"repository": "https://github.com/Budibase/Budibase",
|
"repository": "https://github.com/Budibase/Budibase",
|
||||||
"homepage": "https://budibase.com",
|
"homepage": "https://www.budibase.com",
|
||||||
"main": "src/cli.js",
|
"main": "src/cli.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"budi": "bin/budi"
|
"budi": "bin/budi"
|
||||||
|
|
|
@ -56,8 +56,30 @@ export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function click(e) {
|
||||||
|
const x = e.target.closest("a")
|
||||||
|
const y = x && x.getAttribute("href")
|
||||||
|
|
||||||
|
if (
|
||||||
|
e.ctrlKey ||
|
||||||
|
e.metaKey ||
|
||||||
|
e.altKey ||
|
||||||
|
e.shiftKey ||
|
||||||
|
e.button ||
|
||||||
|
e.defaultPrevented
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
const target = x.target || "_self"
|
||||||
|
if (!y || target !== "_self" || x.host !== location.host) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
route(y)
|
||||||
|
}
|
||||||
|
|
||||||
addEventListener("popstate", route)
|
addEventListener("popstate", route)
|
||||||
addEventListener("pushstate", route)
|
addEventListener("pushstate", route)
|
||||||
|
addEventListener("click", click)
|
||||||
|
|
||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,11 @@ export const bbFactory = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiCall = method => (url, body) =>
|
const apiCall = method => (url, body) =>
|
||||||
fetch(relativeUrl(url), {
|
fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"x-user-agent": "Budibase Builder",
|
||||||
},
|
},
|
||||||
body: body && JSON.stringify(body),
|
body: body && JSON.stringify(body),
|
||||||
})
|
})
|
||||||
|
|
Before Width: | Height: | Size: 109 KiB |
|
@ -1,29 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill:#FFFFFF;}
|
|
||||||
.st1{fill:#6A78D1;}
|
|
||||||
.st2{fill:#49C39E;}
|
|
||||||
.st3{fill:#F2545B;}
|
|
||||||
.st4{fill:#F5C26B;}
|
|
||||||
</style>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<path class="st0" d="M215,14.1l136.2,79.8c9.3,5.4,15,15.5,15,26.4v159.5c0,10.9-5.7,20.9-15,26.4L215,385.9
|
|
||||||
c-9.3,5.4-20.7,5.4-30,0L48.8,306.2c-9.3-5.4-15-15.5-15-26.4V120.2c0-10.9,5.7-20.9,15-26.4L185,14.1
|
|
||||||
C194.3,8.6,205.7,8.6,215,14.1z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="st1" d="M288.8,273.7l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6
|
|
||||||
c3.6-1.9,8-1.9,11.6,0l83,43.6C297.1,258.3,297.1,269.4,288.8,273.7z"/>
|
|
||||||
<path class="st2" d="M288.8,231.2l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6
|
|
||||||
c3.6-1.9,8-1.9,11.6,0l83,43.6C297.1,215.7,297.1,226.9,288.8,231.2z"/>
|
|
||||||
<path class="st3" d="M288.8,188.6l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6
|
|
||||||
c3.6-1.9,8-1.9,11.6,0l83,43.6C297.1,173.1,297.1,184.3,288.8,188.6z"/>
|
|
||||||
<path class="st4" d="M288.8,146l-83,43.6c-3.6,1.9-8,1.9-11.6,0l-83-43.6c-8.2-4.3-8.2-15.5,0-19.8l83-43.6c3.6-1.9,8-1.9,11.6,0
|
|
||||||
l83,43.6C297.1,130.6,297.1,141.7,288.8,146z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 241 KiB |
Before Width: | Height: | Size: 381 KiB |
|
@ -5,8 +5,10 @@ const newid = require("../../db/newid")
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const instanceController = require("./instance")
|
const instanceController = require("./instance")
|
||||||
const { resolve, join } = require("path")
|
const { resolve, join } = require("path")
|
||||||
const { copy, readJSON, writeJSON, exists } = require("fs-extra")
|
const { copy, exists, readFile, writeFile } = require("fs-extra")
|
||||||
|
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
||||||
const { exec } = require("child_process")
|
const { exec } = require("child_process")
|
||||||
|
const sqrl = require("squirrelly")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
|
const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
|
||||||
|
@ -72,7 +74,7 @@ const createEmptyAppPackage = async (ctx, app) => {
|
||||||
"appDirectoryTemplate"
|
"appDirectoryTemplate"
|
||||||
)
|
)
|
||||||
|
|
||||||
const appsFolder = env.BUDIBASE_DIR
|
const appsFolder = budibaseAppsDir()
|
||||||
const newAppFolder = resolve(appsFolder, app._id)
|
const newAppFolder = resolve(appsFolder, app._id)
|
||||||
|
|
||||||
if (await exists(newAppFolder)) {
|
if (await exists(newAppFolder)) {
|
||||||
|
@ -82,16 +84,27 @@ const createEmptyAppPackage = async (ctx, app) => {
|
||||||
|
|
||||||
await copy(templateFolder, newAppFolder)
|
await copy(templateFolder, newAppFolder)
|
||||||
|
|
||||||
const packageJsonPath = join(appsFolder, app._id, "package.json")
|
await updateJsonFile(join(appsFolder, app._id, "package.json"), {
|
||||||
const packageJson = await readJSON(packageJsonPath)
|
name: npmFriendlyAppName(app.name),
|
||||||
|
})
|
||||||
packageJson.name = npmFriendlyAppName(app.name)
|
await updateJsonFile(
|
||||||
|
join(appsFolder, app._id, "pages", "main", "page.json"),
|
||||||
await writeJSON(packageJsonPath, packageJson)
|
app
|
||||||
|
)
|
||||||
|
await updateJsonFile(
|
||||||
|
join(appsFolder, app._id, "pages", "unauthenticated", "page.json"),
|
||||||
|
app
|
||||||
|
)
|
||||||
|
|
||||||
return newAppFolder
|
return newAppFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateJsonFile = async (filePath, app) => {
|
||||||
|
const json = await readFile(filePath, "utf8")
|
||||||
|
const newJson = sqrl.Render(json, app)
|
||||||
|
await writeFile(filePath, newJson, "utf8")
|
||||||
|
}
|
||||||
|
|
||||||
const runNpmInstall = async newAppFolder => {
|
const runNpmInstall = async newAppFolder => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const cmd = `cd ${newAppFolder} && npm install`
|
const cmd = `cd ${newAppFolder} && npm install`
|
||||||
|
|
|
@ -10,12 +10,9 @@ exports.authenticate = async ctx => {
|
||||||
if (!username) ctx.throw(400, "Username Required.")
|
if (!username) ctx.throw(400, "Username Required.")
|
||||||
if (!password) ctx.throw(400, "Password Required")
|
if (!password) ctx.throw(400, "Password Required")
|
||||||
|
|
||||||
// TODO: Don't use this. It can't be relied on
|
|
||||||
const referer = ctx.request.headers.referer.split("/")
|
|
||||||
const appId = referer[3]
|
|
||||||
|
|
||||||
// find the instance that the user is associated with
|
// find the instance that the user is associated with
|
||||||
const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
|
const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
|
||||||
|
const appId = ctx.params.appId
|
||||||
const app = await db.get(appId)
|
const app = await db.get(appId)
|
||||||
const instanceId = app.userInstanceMap[username]
|
const instanceId = app.userInstanceMap[username]
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,12 @@ exports.fetch = async function(ctx) {
|
||||||
ctx.body = body.rows.map(row => row.doc)
|
ctx.body = body.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.find = async function(ctx) {
|
||||||
|
const db = new CouchDB(ctx.params.instanceId)
|
||||||
|
const model = await db.get(ctx.params.id)
|
||||||
|
ctx.body = model
|
||||||
|
}
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.params.instanceId)
|
||||||
const newModel = {
|
const newModel = {
|
||||||
|
|
|
@ -13,7 +13,6 @@ exports.serveBuilder = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.serveApp = async function(ctx) {
|
exports.serveApp = async function(ctx) {
|
||||||
// TODO: update homedir stuff to wherever budi is run
|
|
||||||
// default to homedir
|
// default to homedir
|
||||||
const appPath = resolve(
|
const appPath = resolve(
|
||||||
budibaseAppsDir(),
|
budibaseAppsDir(),
|
||||||
|
@ -26,7 +25,6 @@ exports.serveApp = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.serveComponentLibrary = async function(ctx) {
|
exports.serveComponentLibrary = async function(ctx) {
|
||||||
// TODO: update homedir stuff to wherever budi is run
|
|
||||||
// default to homedir
|
// default to homedir
|
||||||
let componentLibraryPath = resolve(
|
let componentLibraryPath = resolve(
|
||||||
budibaseAppsDir(),
|
budibaseAppsDir(),
|
||||||
|
|
|
@ -38,6 +38,7 @@ router
|
||||||
ctx.config = {
|
ctx.config = {
|
||||||
latestPackagesFolder: budibaseAppsDir(),
|
latestPackagesFolder: budibaseAppsDir(),
|
||||||
jwtSecret: env.JWT_SECRET,
|
jwtSecret: env.JWT_SECRET,
|
||||||
|
useAppRootPath: true,
|
||||||
}
|
}
|
||||||
ctx.isDev = env.NODE_ENV !== "production" && env.NODE_ENV !== "jest"
|
ctx.isDev = env.NODE_ENV !== "production" && env.NODE_ENV !== "jest"
|
||||||
await next()
|
await next()
|
||||||
|
|
|
@ -3,6 +3,6 @@ const controller = require("../controllers/auth")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.post("/api/authenticate", controller.authenticate)
|
router.post("/:appId/api/authenticate", controller.authenticate)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -43,6 +43,7 @@ router
|
||||||
|
|
||||||
router
|
router
|
||||||
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
|
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
|
||||||
|
.get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find)
|
||||||
.post("/api/:instanceId/models", authorized(BUILDER), modelController.create)
|
.post("/api/:instanceId/models", authorized(BUILDER), modelController.create)
|
||||||
// .patch("/api/:instanceId/models", controller.update)
|
// .patch("/api/:instanceId/models", controller.update)
|
||||||
.delete(
|
.delete(
|
||||||
|
|
|
@ -22,6 +22,7 @@ exports.supertest = async () => {
|
||||||
exports.defaultHeaders = {
|
exports.defaultHeaders = {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
Cookie: ["builder:token=test-admin-secret"],
|
Cookie: ["builder:token=test-admin-secret"],
|
||||||
|
"x-user-agent": "Budibase Builder",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createModel = async (request, instanceId, model) => {
|
exports.createModel = async (request, instanceId, model) => {
|
||||||
|
@ -175,8 +176,7 @@ const createUserWithPermissions = async (
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get("_design/database")
|
||||||
|
|
||||||
const loginResult = await request
|
const loginResult = await request
|
||||||
.post(`/api/authenticate`)
|
.post(`/${designDoc.metadata.applicationId}/api/authenticate`)
|
||||||
.set("Referer", `http://localhost:4001/${designDoc.metadata.applicationId}`)
|
|
||||||
.send({ username, password })
|
.send({ username, password })
|
||||||
|
|
||||||
// returning necessary request headers
|
// returning necessary request headers
|
||||||
|
|
|
@ -13,23 +13,31 @@ module.exports = async (ctx, next) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.cookies.get("builder:token") === env.ADMIN_SECRET) {
|
const appToken = ctx.cookies.get("budibase:token")
|
||||||
ctx.isAuthenticated = true
|
const builderToken = ctx.cookies.get("builder:token")
|
||||||
ctx.isBuilder = true
|
const isBuilderAgent = ctx.headers["x-user-agent"] === "Budibase Builder"
|
||||||
|
|
||||||
|
// all admin api access should auth with buildertoken and 'Budibase Builder user agent
|
||||||
|
const shouldAuthAsBuilder = isBuilderAgent && builderToken
|
||||||
|
|
||||||
|
if (shouldAuthAsBuilder) {
|
||||||
|
const builderTokenValid = builderToken === env.ADMIN_SECRET
|
||||||
|
|
||||||
|
ctx.isAuthenticated = builderTokenValid
|
||||||
|
ctx.isBuilder = builderTokenValid
|
||||||
|
|
||||||
await next()
|
await next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = ctx.cookies.get("budibase:token")
|
if (!appToken) {
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
ctx.isAuthenticated = false
|
ctx.isAuthenticated = false
|
||||||
await next()
|
await next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
|
const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret)
|
||||||
|
|
||||||
ctx.user = {
|
ctx.user = {
|
||||||
...jwtPayload,
|
...jwtPayload,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "{{ name }}",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"title": "Test App",
|
"title": "{{ name }}",
|
||||||
"favicon": "./_shared/favicon.png",
|
"favicon": "./_shared/favicon.png",
|
||||||
"stylesheets": [],
|
"stylesheets": [],
|
||||||
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
|
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
|
||||||
|
|
|
@ -1,21 +1,45 @@
|
||||||
{
|
{
|
||||||
"title": "Test App",
|
"componentLibraries": [
|
||||||
"favicon": "./_shared/favicon.png",
|
"@budibase/standard-components",
|
||||||
"stylesheets": [],
|
"@budibase/materialdesign-components"
|
||||||
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
|
],
|
||||||
"props" : {
|
"title": "{{ name }}",
|
||||||
"_component": "@budibase/standard-components/container",
|
"favicon": "./_shared/favicon.png",
|
||||||
"_children": [],
|
"stylesheets": [],
|
||||||
"_id": 1,
|
"props": {
|
||||||
"type": "div",
|
"_component": "@budibase/standard-components/container",
|
||||||
"_styles": {
|
"_children": [
|
||||||
"active": {},
|
{
|
||||||
"hover": {},
|
"_id": "686c252d-dbf2-4e28-9078-414ba4719759",
|
||||||
"normal": {},
|
"_component": "@budibase/standard-components/login",
|
||||||
"selected": {}
|
"_styles": {
|
||||||
},
|
"normal": {},
|
||||||
"_code": ""
|
"hover": {},
|
||||||
},
|
"active": {},
|
||||||
"_css": "",
|
"selected": {}
|
||||||
"uiFunctions": ""
|
},
|
||||||
|
"_code": "",
|
||||||
|
"loginRedirect": "",
|
||||||
|
"usernameLabel": "Username",
|
||||||
|
"passwordLabel": "Password",
|
||||||
|
"loginButtonLabel": "Login",
|
||||||
|
"buttonClass": "",
|
||||||
|
"inputClass": "",
|
||||||
|
"_children": [],
|
||||||
|
"name": "{{ name }}",
|
||||||
|
"logo": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_id": 1,
|
||||||
|
"type": "div",
|
||||||
|
"_styles": {
|
||||||
|
"layout": {},
|
||||||
|
"position": {}
|
||||||
|
},
|
||||||
|
"_code": "",
|
||||||
|
"className": "",
|
||||||
|
"onLoad": []
|
||||||
|
},
|
||||||
|
"_css": "",
|
||||||
|
"uiFunctions": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,7 @@ module.exports = async (config, appId, pageName, pkg) => {
|
||||||
await savePageJson(appPath, pageName, pkg)
|
await savePageJson(appPath, pageName, pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootPath = (config, appname) =>
|
const rootPath = (config, appId) => (config.useAppRootPath ? `/${appId}` : "")
|
||||||
config.useAppRootPath ? `/${appname}` : ""
|
|
||||||
|
|
||||||
const copyClientLib = async (appPath, pageName) => {
|
const copyClientLib = async (appPath, pageName) => {
|
||||||
const sourcepath = require.resolve("@budibase/client")
|
const sourcepath = require.resolve("@budibase/client")
|
||||||
|
@ -46,7 +45,7 @@ const copyClientLib = async (appPath, pageName) => {
|
||||||
|
|
||||||
const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => {
|
const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => {
|
||||||
const appPublicPath = publicPath(appPath, pageName)
|
const appPublicPath = publicPath(appPath, pageName)
|
||||||
const appRootPath = appId
|
const appRootPath = rootPath(config, appId)
|
||||||
|
|
||||||
const stylesheetUrl = s =>
|
const stylesheetUrl = s =>
|
||||||
s.startsWith("http") ? s : `/${rootPath(config, appId)}/${s}`
|
s.startsWith("http") ? s : `/${rootPath(config, appId)}/${s}`
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
<meta charset='utf8'>
|
|
||||||
<meta name='viewport' content='width=device-width'>
|
|
||||||
|
|
||||||
<title>{{ title }}</title>
|
<head>
|
||||||
|
<meta charset='utf8'>
|
||||||
|
<meta name='viewport' content='width=device-width'>
|
||||||
|
|
||||||
|
<title>{{ title }}</title>
|
||||||
<link rel='icon' type='image/png' href='{{ favicon }}'>
|
<link rel='icon' type='image/png' href='{{ favicon }}'>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html,
|
||||||
|
body {
|
||||||
|
font-family: Roboto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
@ -39,4 +42,5 @@
|
||||||
loadBudibase();
|
loadBudibase();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -59,6 +59,7 @@
|
||||||
"props": {
|
"props": {
|
||||||
"logo": "asset",
|
"logo": "asset",
|
||||||
"loginRedirect": "string",
|
"loginRedirect": "string",
|
||||||
|
"name": "string",
|
||||||
"usernameLabel": {
|
"usernameLabel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "Username"
|
"default": "Username"
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
<button
|
<button
|
||||||
bind:this={theButton}
|
bind:this={theButton}
|
||||||
class={className}
|
class="default"
|
||||||
disabled={disabled || false}
|
disabled={disabled || false}
|
||||||
on:click={clickHandler}>
|
on:click={clickHandler}>
|
||||||
{#if !_bb.props._children || _bb.props._children.length === 0}{text}{/if}
|
{#if !_bb.props._children || _bb.props._children.length === 0}{text}{/if}
|
||||||
|
@ -25,23 +25,21 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.default {
|
.default {
|
||||||
font-family: inherit;
|
align-items: center;
|
||||||
font-size: inherit;
|
font-family: Inter;
|
||||||
padding: 0.4em;
|
font-size: 16px;
|
||||||
margin: 0 0 0.5em 0;
|
padding: 0px 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid #ccc;
|
border-radius: 4px;
|
||||||
border-radius: 2px;
|
|
||||||
color: #000333;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
.default:active {
|
transition: all 0.2s ease 0s;
|
||||||
background-color: #f9f9f9;
|
overflow: hidden;
|
||||||
}
|
outline: none;
|
||||||
|
user-select: none;
|
||||||
.default:focus {
|
white-space: nowrap;
|
||||||
border-color: #666;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
|
|
|
@ -8,19 +8,32 @@
|
||||||
let username
|
let username
|
||||||
let password
|
let password
|
||||||
let newModel = {
|
let newModel = {
|
||||||
modelId: model._id,
|
modelId: model,
|
||||||
}
|
}
|
||||||
let store = _bb.store
|
let store = _bb.store
|
||||||
|
let schema = {}
|
||||||
|
let modelDef = {}
|
||||||
|
|
||||||
$: fields = Object.keys(model.schema)
|
$: if (model && model.length !== 0) {
|
||||||
|
fetchModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: fields = Object.keys(schema)
|
||||||
|
|
||||||
|
async function fetchModel() {
|
||||||
|
const FETCH_MODEL_URL = `/api/${_instanceId}/models/${model}`
|
||||||
|
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||||
|
modelDef = await response.json()
|
||||||
|
schema = modelDef.schema
|
||||||
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
const SAVE_RECORD_URL = `/api/${_instanceId}/records`
|
const SAVE_RECORD_URL = `/api/${_instanceId}/${model}/records`
|
||||||
const response = await _bb.api.post(SAVE_RECORD_URL, newModel)
|
const response = await _bb.api.post(SAVE_RECORD_URL, newModel)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state[model._id] = [...state[model._id], json]
|
state[model._id] = [...state[model], json]
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -45,24 +58,95 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="uk-form" on:submit|preventDefault>
|
<form class="form" on:submit|preventDefault>
|
||||||
<h4>{model.name}</h4>
|
<div class="form-content">
|
||||||
<div>
|
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<div class="uk-margin">
|
<div class="form-item">
|
||||||
<label class="form-label" for="form-stacked-text">{field}</label>
|
<label class="form-label" for="form-stacked-text">{field}</label>
|
||||||
<input
|
<input
|
||||||
class="uk-input"
|
class="input"
|
||||||
type={model.schema[field].type === 'string' ? 'text' : model.schema[field].type}
|
placeholder={field}
|
||||||
|
type={schema[field].type === 'string' ? 'text' : schema[field].type}
|
||||||
on:change={handleInput(field)} />
|
on:change={handleInput(field)} />
|
||||||
</div>
|
</div>
|
||||||
|
<hr />
|
||||||
{/each}
|
{/each}
|
||||||
|
<div class="button-block">
|
||||||
|
<button on:click={save}>Submit Form</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button on:click={save}>SAVE</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.form {
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 600px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
padding: 6px 12px 6px 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 1px solid #fafafa;
|
||||||
|
margin: 20px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr:nth-last-child(2) {
|
||||||
|
border: 1px solid #fff;
|
||||||
|
margin: 20px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-block {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0.4em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
background-color: #393c44;
|
||||||
|
outline: none;
|
||||||
|
width: 300px;
|
||||||
|
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 {
|
||||||
|
background-color: white;
|
||||||
|
border-color: #393c44;
|
||||||
|
color: #393c44;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import Button from "./Button.svelte"
|
import Button from "./Button.svelte"
|
||||||
|
|
||||||
export let usernameLabel = "Username"
|
|
||||||
export let passwordLabel = "Password"
|
|
||||||
export let loginButtonLabel = "Login"
|
export let loginButtonLabel = "Login"
|
||||||
export let logo = ""
|
export let logo = ""
|
||||||
|
export let name = ""
|
||||||
export let buttonClass = ""
|
export let buttonClass = ""
|
||||||
export let inputClass = ""
|
export let inputClass = ""
|
||||||
|
|
||||||
|
@ -26,9 +25,16 @@
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
const response = await _bb.api.post("/api/authenticate", {
|
const response = await fetch(_bb.relativeUrl("/api/authenticate"), {
|
||||||
username,
|
body: JSON.stringify({
|
||||||
password,
|
username,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-user-agent": "Budibase Builder",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
|
@ -51,14 +57,23 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<h1 class="header-content">Log in to {name}</h1>
|
||||||
|
|
||||||
<div class="form-root">
|
<div class="form-root">
|
||||||
<div class="label">{usernameLabel}</div>
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input bind:value={username} type="text" class={_inputClass} />
|
<input
|
||||||
|
bind:value={username}
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
class={_inputClass} />
|
||||||
</div>
|
</div>
|
||||||
<div class="label">{passwordLabel}</div>
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input bind:value={password} type="password" class={_inputClass} />
|
<input
|
||||||
|
bind:value={password}
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
class={_inputClass} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -77,28 +92,42 @@
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: [left] 1fr [middle] auto [right] 1fr;
|
flex-direction: column;
|
||||||
grid-template-rows: [top] 1fr [center] auto [bottom] 1fr;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
grid-column-start: middle;
|
display: flex;
|
||||||
grid-row-start: center;
|
flex-direction: column;
|
||||||
width: 400px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container {
|
.logo-container {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container > img {
|
.logo-container > img {
|
||||||
max-width: 100%;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-button-container {
|
.login-button-container {
|
||||||
text-align: right;
|
margin-top: 6px;
|
||||||
margin-top: 20px;
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
font-family: Inter;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f1f1f;
|
||||||
|
font-size: 48px;
|
||||||
|
line-height: 72px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-feature-settings: "case" "rlig" "calt" 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.incorrect-details-panel {
|
.incorrect-details-panel {
|
||||||
|
@ -114,48 +143,55 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-root {
|
.form-root {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: [label] auto [control] 1fr; /* [overflow] auto;*/
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
|
||||||
grid-column-start: label;
|
|
||||||
padding: 5px 10px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.control {
|
.control {
|
||||||
grid-column-start: control;
|
padding: 6px 0px;
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-input {
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
padding: 0.4em;
|
|
||||||
margin: 0 0 0.5em 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 2px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-button {
|
.default-input {
|
||||||
font-family: inherit;
|
font-family: Inter;
|
||||||
font-size: inherit;
|
font-size: 14px;
|
||||||
padding: 0.4em;
|
color: #393c44;
|
||||||
|
padding: 2px 6px 2px 12px;
|
||||||
margin: 0 0 0.5em 0;
|
margin: 0 0 0.5em 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid #ccc;
|
border: 0.5px solid #d8d8d8;
|
||||||
border-radius: 2px;
|
border-radius: 4px;
|
||||||
color: #000333;
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
transition: border-color 100ms ease-in 0s;
|
||||||
|
outline-color: #797979;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-button {
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0.4em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
background-color: #393c44;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
width: 300px;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease 0s;
|
||||||
|
overflow: hidden;
|
||||||
|
outline: none;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-button:active {
|
.default-button:hover {
|
||||||
background-color: #f9f9f9;
|
background-color: white;
|
||||||
}
|
border-color: #393c44;
|
||||||
|
color: #393c44;
|
||||||
.default-button:focus {
|
|
||||||
border-color: #f9f9f9;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|