forked from tsai/budibase
292 changed files with 11516 additions and 7594 deletions
@ -1,43 +0,0 @@ |
|||||
<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 secondary> |
|
||||
<option value="">Choose an option</option> |
|
||||
<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: var(--spacing-m); |
|
||||
grid-template-columns: 1fr 1fr 140px; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,114 +0,0 @@ |
|||||
<script> |
|
||||
import { Input, Select, Button, Label } from "@budibase/bbui" |
|
||||
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 = "ADMIN" |
|
||||
|
|
||||
$: 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> |
|
||||
|
|
||||
<div class="container"> |
|
||||
<div> |
|
||||
<Label extraSmall grey>Create New User</Label> |
|
||||
<div class="inputs"> |
|
||||
<Input thin bind:value={username} name="Name" placeholder="Username" /> |
|
||||
<Input |
|
||||
thin |
|
||||
type="password" |
|
||||
bind:value={password} |
|
||||
name="Password" |
|
||||
placeholder="Password" /> |
|
||||
<Select secondary bind:value={accessLevelId} thin> |
|
||||
<option value="">Choose an option</option> |
|
||||
<option value="ADMIN">Admin</option> |
|
||||
<option value="POWER_USER">Power User</option> |
|
||||
</Select> |
|
||||
<Button on:click={createUser} primary>Create</Button> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div> |
|
||||
<Label extraSmall grey>Current Users</Label> |
|
||||
{#await fetchUsersPromise} |
|
||||
Loading... |
|
||||
{:then users} |
|
||||
<ul> |
|
||||
{#each users as user} |
|
||||
<li> |
|
||||
<UserRow {user} on:save={updateUser} /> |
|
||||
</li> |
|
||||
{:else} |
|
||||
<li>No Users found</li> |
|
||||
{/each} |
|
||||
</ul> |
|
||||
{:catch err} |
|
||||
Something went wrong when trying to fetch users. Please refresh (CMD + R / |
|
||||
CTRL + R) the page and try again. |
|
||||
{/await} |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.container { |
|
||||
display: grid; |
|
||||
grid-gap: var(--spacing-xl); |
|
||||
} |
|
||||
|
|
||||
.inputs { |
|
||||
display: grid; |
|
||||
justify-items: stretch; |
|
||||
grid-gap: var(--spacing-m); |
|
||||
grid-template-columns: 1fr 1fr 1fr 140px; |
|
||||
} |
|
||||
|
|
||||
ul { |
|
||||
list-style: none; |
|
||||
padding: 0; |
|
||||
display: grid; |
|
||||
grid-gap: var(--spacing-m); |
|
||||
margin: 0; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,6 +1,5 @@ |
|||||
export { default as General } from "./General.svelte" |
export { default as General } from "./General.svelte" |
||||
export { default as Integrations } from "./Integrations.svelte" |
export { default as Integrations } from "./Integrations.svelte" |
||||
export { default as Permissions } from "./Permissions.svelte" |
export { default as Permissions } from "./Permissions.svelte" |
||||
export { default as Users } from "./Users.svelte" |
|
||||
export { default as APIKeys } from "./APIKeys.svelte" |
export { default as APIKeys } from "./APIKeys.svelte" |
||||
export { default as DangerZone } from "./DangerZone.svelte" |
export { default as DangerZone } from "./DangerZone.svelte" |
||||
|
|||||
@ -0,0 +1,52 @@ |
|||||
|
<html> |
||||
|
<head> |
||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"> |
||||
|
<style> |
||||
|
body, html { |
||||
|
height: 100% !important; |
||||
|
font-family: Inter, sans-serif !important; |
||||
|
margin: 0 !important; |
||||
|
} |
||||
|
*, *:before, *:after { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
</style> |
||||
|
<script src='/assets/budibase-client.js'></script> |
||||
|
<script> |
||||
|
function receiveMessage(event) { |
||||
|
if (!event.data) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Extract data from message |
||||
|
const { selectedComponentId, layout, screen } = JSON.parse(event.data) |
||||
|
|
||||
|
// Set some flags so the app knows we're in the builder |
||||
|
window["##BUDIBASE_IN_BUILDER##"] = true |
||||
|
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout |
||||
|
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen |
||||
|
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId |
||||
|
window["##BUDIBASE_PREVIEW_ID##"] = Math.random() |
||||
|
|
||||
|
// Initialise app |
||||
|
if (window.loadBudibase) { |
||||
|
loadBudibase() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Ignore clicks |
||||
|
["click", "mousedown"].forEach(type => { |
||||
|
document.addEventListener(type, function(e) { |
||||
|
e.preventDefault() |
||||
|
e.stopPropagation() |
||||
|
return false |
||||
|
}, true) |
||||
|
}) |
||||
|
|
||||
|
window.addEventListener("message", receiveMessage) |
||||
|
window.dispatchEvent(new Event("bb-ready")) |
||||
|
</script> |
||||
|
</head> |
||||
|
<body> |
||||
|
</body> |
||||
|
</html> |
||||
@ -1,77 +1 @@ |
|||||
export default `<html>
|
export { default } from "./iframeTemplate.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: Inter !important; |
|
||||
margin: 0px!important; |
|
||||
} |
|
||||
*, *:before, *:after { |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
.container-screenslot-placeholder { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
padding: 20px; |
|
||||
text-align: center; |
|
||||
border-style: dashed !important; |
|
||||
border-width: 1px; |
|
||||
color: #000000; |
|
||||
background-color: rgba(0, 0, 0, 0.05); |
|
||||
flex: 1 1 auto; |
|
||||
} |
|
||||
.container-screenslot-placeholder span { |
|
||||
display: block; |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
</style> |
|
||||
<script src='/assets/budibase-client.js'></script> |
|
||||
<script> |
|
||||
function receiveMessage(event) { |
|
||||
|
|
||||
if (!event.data) return |
|
||||
|
|
||||
const data = JSON.parse(event.data) |
|
||||
|
|
||||
try { |
|
||||
if (styles) document.head.removeChild(styles) |
|
||||
} catch(_) { } |
|
||||
|
|
||||
try { |
|
||||
if (selectedComponentStyle) document.head.removeChild(selectedComponentStyle) |
|
||||
} catch(_) { } |
|
||||
|
|
||||
selectedComponentStyle = document.createElement('style'); |
|
||||
document.head.appendChild(selectedComponentStyle) |
|
||||
var selectedCss = '.' + data.selectedComponentType + '-' + data.selectedComponentId + '{ border: 2px solid #0055ff; }' |
|
||||
selectedComponentStyle.appendChild(document.createTextNode(selectedCss)) |
|
||||
|
|
||||
styles = document.createElement('style') |
|
||||
document.head.appendChild(styles) |
|
||||
styles.appendChild(document.createTextNode(data.styles)) |
|
||||
|
|
||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition; |
|
||||
if (window.loadBudibase) { |
|
||||
loadBudibase({ window, localStorage }) |
|
||||
} |
|
||||
} |
|
||||
let styles |
|
||||
let selectedComponentStyle |
|
||||
|
|
||||
document.addEventListener("click", function(e) { |
|
||||
e.preventDefault() |
|
||||
e.stopPropagation() |
|
||||
return false; |
|
||||
}, true) |
|
||||
|
|
||||
window.addEventListener('message', receiveMessage) |
|
||||
window.dispatchEvent(new Event('bb-ready')) |
|
||||
|
|
||||
</script> |
|
||||
</head> |
|
||||
<body> |
|
||||
</body> |
|
||||
</html>` |
|
||||
|
|||||
@ -0,0 +1,77 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@sveltech/routify" |
||||
|
import { store } from "builderStore" |
||||
|
import { notifier } from "builderStore/store/notifications" |
||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
||||
|
import { DropdownMenu, Modal, ModalContent, Input } from "@budibase/bbui" |
||||
|
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" |
||||
|
import { cloneDeep } from "lodash/fp" |
||||
|
|
||||
|
export let layout |
||||
|
|
||||
|
let confirmDeleteDialog |
||||
|
let editLayoutNameModal |
||||
|
let dropdown |
||||
|
let anchor |
||||
|
let name = layout.name |
||||
|
|
||||
|
const deleteLayout = async () => { |
||||
|
try { |
||||
|
await store.actions.layouts.delete(layout) |
||||
|
notifier.success(`Layout ${layout.name} deleted successfully.`) |
||||
|
} catch (err) { |
||||
|
notifier.danger(`Error deleting layout: ${err.message}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const saveLayout = async () => { |
||||
|
try { |
||||
|
const layoutToSave = cloneDeep(layout) |
||||
|
layoutToSave.name = name |
||||
|
await store.actions.layouts.save(layoutToSave) |
||||
|
notifier.success(`Layout saved successfully.`) |
||||
|
} catch (err) { |
||||
|
notifier.danger(`Error saving layout: ${err.message}`) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div bind:this={anchor} on:click|stopPropagation> |
||||
|
<div class="icon" on:click={() => dropdown.show()}> |
||||
|
<i class="ri-more-line" /> |
||||
|
</div> |
||||
|
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
||||
|
<DropdownContainer> |
||||
|
<DropdownItem |
||||
|
icon="ri-pencil-line" |
||||
|
title="Edit" |
||||
|
on:click={() => editLayoutNameModal.show()} /> |
||||
|
<DropdownItem |
||||
|
icon="ri-delete-bin-line" |
||||
|
title="Delete" |
||||
|
on:click={() => confirmDeleteDialog.show()} /> |
||||
|
</DropdownContainer> |
||||
|
</DropdownMenu> |
||||
|
</div> |
||||
|
<ConfirmDialog |
||||
|
bind:this={confirmDeleteDialog} |
||||
|
title="Confirm Deletion" |
||||
|
body={'Are you sure you wish to delete this layout?'} |
||||
|
okText="Delete Layout" |
||||
|
onOk={deleteLayout} /> |
||||
|
|
||||
|
<Modal bind:this={editLayoutNameModal}> |
||||
|
<ModalContent |
||||
|
title="Edit Layout Name" |
||||
|
confirmText="Save" |
||||
|
onConfirm={saveLayout} |
||||
|
disabled={!name}> |
||||
|
<Input thin type="text" label="Name" bind:value={name} /> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
|
||||
|
<style> |
||||
|
.icon i { |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
</style> |
||||
@ -1,127 +0,0 @@ |
|||||
<script> |
|
||||
import { keys, map, includes, filter } from "lodash/fp" |
|
||||
import EventEditorModal from "./EventEditorModal.svelte" |
|
||||
import { Modal } from "@budibase/bbui" |
|
||||
|
|
||||
export const EVENT_TYPE = "event" |
|
||||
export let component |
|
||||
|
|
||||
let events = [] |
|
||||
let selectedEvent = null |
|
||||
let modal |
|
||||
|
|
||||
$: { |
|
||||
events = Object.keys(component) |
|
||||
// TODO: use real events |
|
||||
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName)) |
|
||||
.map(propName => ({ |
|
||||
name: propName, |
|
||||
handlers: component[propName] || [], |
|
||||
})) |
|
||||
} |
|
||||
|
|
||||
const openModal = event => { |
|
||||
selectedEvent = event |
|
||||
modal.show() |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<button class="newevent" on:click={() => openModal()}> |
|
||||
<i class="icon ri-add-circle-fill" /> |
|
||||
Create New Event |
|
||||
</button> |
|
||||
|
|
||||
<div class="root"> |
|
||||
<form on:submit|preventDefault class="form-root"> |
|
||||
{#each events as event, index} |
|
||||
{#if event.handlers.length > 0} |
|
||||
<div |
|
||||
class:selected={selectedEvent && selectedEvent.index === index} |
|
||||
class="handler-container budibase__nav-item" |
|
||||
on:click={() => openModal({ ...event, index })}> |
|
||||
<span class="event-name">{event.name}</span> |
|
||||
<span class="edit-text">EDIT</span> |
|
||||
</div> |
|
||||
{/if} |
|
||||
{/each} |
|
||||
</form> |
|
||||
</div> |
|
||||
|
|
||||
<Modal bind:this={modal} width="600px"> |
|
||||
<EventEditorModal eventOptions={events} event={selectedEvent} /> |
|
||||
</Modal> |
|
||||
|
|
||||
<style> |
|
||||
.root { |
|
||||
font-size: 10pt; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
.newevent { |
|
||||
cursor: pointer; |
|
||||
border: 1px solid var(--grey-4); |
|
||||
border-radius: 3px; |
|
||||
width: 100%; |
|
||||
padding: 8px 16px; |
|
||||
margin: 0px 0px 12px 0px; |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
background: var(--background); |
|
||||
color: var(--ink); |
|
||||
font-size: 14px; |
|
||||
font-weight: 500; |
|
||||
transition: all 2ms; |
|
||||
} |
|
||||
|
|
||||
.newevent:hover { |
|
||||
background: var(--grey-1); |
|
||||
} |
|
||||
|
|
||||
.icon { |
|
||||
color: var(--ink); |
|
||||
font-size: 16px; |
|
||||
margin-right: 4px; |
|
||||
} |
|
||||
|
|
||||
.form-root { |
|
||||
display: flex; |
|
||||
flex-wrap: wrap; |
|
||||
} |
|
||||
|
|
||||
header { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
justify-content: space-between; |
|
||||
} |
|
||||
|
|
||||
.handler-container { |
|
||||
display: grid; |
|
||||
grid-template-columns: repeat(2, 1fr); |
|
||||
border: 2px solid var(--grey-1); |
|
||||
height: 80px; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
.event-name { |
|
||||
margin-top: 5px; |
|
||||
font-weight: bold; |
|
||||
font-size: 16px; |
|
||||
color: rgba(22, 48, 87, 0.6); |
|
||||
align-self: end; |
|
||||
} |
|
||||
|
|
||||
.edit-text { |
|
||||
font-family: Arial, Helvetica, sans-serif; |
|
||||
font-weight: bold; |
|
||||
align-self: end; |
|
||||
justify-self: end; |
|
||||
font-size: 10px; |
|
||||
color: rgba(35, 65, 105, 0.4); |
|
||||
} |
|
||||
|
|
||||
.selected { |
|
||||
color: var(--blue); |
|
||||
background: var(--grey-1) !important; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,53 +0,0 @@ |
|||||
<script> |
|
||||
import { Input, DataList, Select } from "@budibase/bbui" |
|
||||
import { automationStore, allScreens } from "builderStore" |
|
||||
|
|
||||
export let parameter |
|
||||
|
|
||||
let isOpen = false |
|
||||
|
|
||||
const capitalize = s => { |
|
||||
if (typeof s !== "string") return "" |
|
||||
return s.charAt(0).toUpperCase() + s.slice(1) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="handler-option"> |
|
||||
{#if parameter.name === 'automation'}<span>{parameter.name}</span>{/if} |
|
||||
{#if parameter.name === 'automation'} |
|
||||
<Select on:change bind:value={parameter.value}> |
|
||||
<option value="" /> |
|
||||
{#each $automationStore.automations.filter(wf => wf.live) as automation} |
|
||||
<option value={automation._id}>{automation.name}</option> |
|
||||
{/each} |
|
||||
</Select> |
|
||||
{:else if parameter.name === 'url'} |
|
||||
<DataList on:change bind:value={parameter.value}> |
|
||||
<option value="" /> |
|
||||
{#each $allScreens as screen} |
|
||||
<option value={screen.routing.route}> |
|
||||
{screen.props._instanceName} |
|
||||
</option> |
|
||||
{/each} |
|
||||
</DataList> |
|
||||
{:else} |
|
||||
<Input |
|
||||
name={parameter.name} |
|
||||
label={capitalize(parameter.name)} |
|
||||
on:change |
|
||||
value={parameter.value} /> |
|
||||
{/if} |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.handler-option { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
} |
|
||||
|
|
||||
span { |
|
||||
font-size: 18px; |
|
||||
margin-bottom: 10px; |
|
||||
font-weight: 500; |
|
||||
} |
|
||||
</style> |
|
||||
@ -0,0 +1,41 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@sveltech/routify" |
||||
|
import { FrontendTypes } from "constants" |
||||
|
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte" |
||||
|
import LayoutDropdownMenu from "./ComponentNavigationTree/LayoutDropdownMenu.svelte" |
||||
|
import initDragDropStore from "./ComponentNavigationTree/dragDropStore" |
||||
|
import NavItem from "components/common/NavItem.svelte" |
||||
|
import { last } from "lodash/fp" |
||||
|
import { store, currentAsset, selectedComponent } from "builderStore" |
||||
|
import { writable } from "svelte/store" |
||||
|
|
||||
|
export let layout |
||||
|
|
||||
|
let confirmDeleteDialog |
||||
|
let componentToDelete = "" |
||||
|
|
||||
|
const dragDropStore = initDragDropStore() |
||||
|
|
||||
|
const selectLayout = () => { |
||||
|
store.actions.layouts.select(layout._id) |
||||
|
$goto(`./${layout._id}`) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<NavItem |
||||
|
border={false} |
||||
|
icon="ri-layout-3-line" |
||||
|
text={layout.name} |
||||
|
withArrow |
||||
|
selected={$store.currentAssetId === layout._id} |
||||
|
opened={$store.currentAssetId === layout._id} |
||||
|
on:click={selectLayout}> |
||||
|
<LayoutDropdownMenu {layout} /> |
||||
|
</NavItem> |
||||
|
|
||||
|
{#if $store.currentAssetId === layout._id && layout.props?._children} |
||||
|
<ComponentTree |
||||
|
components={layout.props._children} |
||||
|
currentComponent={$selectedComponent} |
||||
|
{dragDropStore} /> |
||||
|
{/if} |
||||
@ -0,0 +1,13 @@ |
|||||
|
<script> |
||||
|
import { store, currentAsset } from "builderStore" |
||||
|
import { Select } from "@budibase/bbui" |
||||
|
|
||||
|
export let value |
||||
|
</script> |
||||
|
|
||||
|
<Select bind:value extraThin secondary on:change> |
||||
|
<option value="">Choose an option</option> |
||||
|
{#each $store.layouts as layout} |
||||
|
<option value={layout._id}>{layout.name}</option> |
||||
|
{/each} |
||||
|
</Select> |
||||
@ -0,0 +1,26 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@sveltech/routify" |
||||
|
import api from "builderStore/api" |
||||
|
import { notifier } from "builderStore/store/notifications" |
||||
|
import { store, backendUiStore, allScreens } from "builderStore" |
||||
|
import { Input, ModalContent } from "@budibase/bbui" |
||||
|
import analytics from "analytics" |
||||
|
|
||||
|
const CONTAINER = "@budibase/standard-components/container" |
||||
|
|
||||
|
let name = "" |
||||
|
|
||||
|
async function save() { |
||||
|
try { |
||||
|
await store.actions.layouts.save({ name }) |
||||
|
$goto(`./${$store.currentAssetId}`) |
||||
|
notifier.success(`Layout ${name} created successfully`) |
||||
|
} catch (err) { |
||||
|
notifier.danger(`Error creating layout ${name}.`) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent title="Create Layout" confirmText="Create" onConfirm={save}> |
||||
|
<Input thin label="Name" bind:value={name} /> |
||||
|
</ModalContent> |
||||
@ -1,44 +0,0 @@ |
|||||
<script> |
|
||||
import { goto } from "@sveltech/routify" |
|
||||
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte" |
|
||||
import NavItem from "components/common/NavItem.svelte" |
|
||||
import { last } from "lodash/fp" |
|
||||
import { store } from "builderStore" |
|
||||
import { writable } from "svelte/store" |
|
||||
|
|
||||
export let layout |
|
||||
|
|
||||
let confirmDeleteDialog |
|
||||
let componentToDelete = "" |
|
||||
|
|
||||
const dragDropStore = writable({}) |
|
||||
|
|
||||
const lastPartOfName = c => |
|
||||
c && last(c.name ? c.name.split("/") : c._component.split("/")) |
|
||||
|
|
||||
$: _layout = { |
|
||||
component: layout, |
|
||||
title: lastPartOfName(layout), |
|
||||
} |
|
||||
|
|
||||
const setCurrentScreenToLayout = () => { |
|
||||
store.actions.selectPageOrScreen("page") |
|
||||
$goto("./:page/page-layout") |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<NavItem |
|
||||
border={false} |
|
||||
icon="ri-layout-3-line" |
|
||||
text="Master Screen" |
|
||||
withArrow |
|
||||
selected={$store.currentComponentInfo?._id === _layout.component.props._id} |
|
||||
opened={$store.currentPreviewItem?.name === _layout.title} |
|
||||
on:click={setCurrentScreenToLayout} /> |
|
||||
|
|
||||
{#if $store.currentPreviewItem?.name === _layout.title && _layout.component.props._children} |
|
||||
<ComponentTree |
|
||||
components={_layout.component.props._children} |
|
||||
currentComponent={$store.currentComponentInfo} |
|
||||
{dragDropStore} /> |
|
||||
{/if} |
|
||||
@ -1,3 +0,0 @@ |
|||||
<script> |
|
||||
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte" |
|
||||
</script> |
|
||||
@ -1,65 +0,0 @@ |
|||||
<script> |
|
||||
import { params, goto } from "@sveltech/routify" |
|
||||
import { store } from "builderStore" |
|
||||
|
|
||||
const getPage = (state, name) => { |
|
||||
const props = state.pages[name] |
|
||||
return { name, props } |
|
||||
} |
|
||||
|
|
||||
const pages = [ |
|
||||
{ |
|
||||
title: "Private", |
|
||||
id: "main", |
|
||||
}, |
|
||||
{ |
|
||||
title: "Public", |
|
||||
id: "unauthenticated", |
|
||||
}, |
|
||||
] |
|
||||
|
|
||||
if (!$store.currentPageName) |
|
||||
store.actions.pages.select($params.page ? $params.page : "main") |
|
||||
|
|
||||
const changePage = id => { |
|
||||
store.actions.pages.select(id) |
|
||||
$goto(`./${id}/page-layout`) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="root"> |
|
||||
{#each pages as { title, id }} |
|
||||
<button class:active={id === $params.page} on:click={() => changePage(id)}> |
|
||||
{title} |
|
||||
</button> |
|
||||
{/each} |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.root { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
} |
|
||||
|
|
||||
button { |
|
||||
cursor: pointer; |
|
||||
padding: 0 var(--spacing-m); |
|
||||
height: 32px; |
|
||||
text-align: center; |
|
||||
background: var(--background); |
|
||||
color: var(--grey-7); |
|
||||
border-radius: 5px; |
|
||||
font-size: var(--font-size-xs); |
|
||||
font-weight: 500; |
|
||||
transition: all 0.3s; |
|
||||
text-rendering: optimizeLegibility; |
|
||||
border: none !important; |
|
||||
outline: none; |
|
||||
font-family: var(--font-sans); |
|
||||
} |
|
||||
|
|
||||
.active { |
|
||||
background: var(--grey-2); |
|
||||
color: var(--ink); |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,60 +0,0 @@ |
|||||
import { isPlainObject, isArray, cloneDeep } from "lodash/fp" |
|
||||
import { getExactComponent } from "./searchComponents" |
|
||||
|
|
||||
export const rename = (pages, screens, oldname, newname) => { |
|
||||
pages = cloneDeep(pages) |
|
||||
screens = cloneDeep(screens) |
|
||||
const changedScreens = [] |
|
||||
|
|
||||
const existingWithNewName = getExactComponent(screens, newname, true) |
|
||||
if (existingWithNewName) |
|
||||
return { |
|
||||
components: screens, |
|
||||
pages, |
|
||||
error: "Component by that name already exists", |
|
||||
} |
|
||||
|
|
||||
const traverseProps = props => { |
|
||||
let hasEdited = false |
|
||||
if (props._component && props._component === oldname) { |
|
||||
props._component = newname |
|
||||
hasEdited = true |
|
||||
} |
|
||||
|
|
||||
for (let propName in props) { |
|
||||
const prop = props[propName] |
|
||||
if (isPlainObject(prop) && prop._component) { |
|
||||
hasEdited = traverseProps(prop) || hasEdited |
|
||||
} |
|
||||
if (isArray(prop)) { |
|
||||
for (let element of prop) { |
|
||||
hasEdited = traverseProps(element) || hasEdited |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
return hasEdited |
|
||||
} |
|
||||
|
|
||||
for (let screen of screens) { |
|
||||
let hasEdited = false |
|
||||
|
|
||||
if (screen.props.instanceName === oldname) { |
|
||||
screen.props.instanceName = newname |
|
||||
hasEdited = true |
|
||||
} |
|
||||
|
|
||||
hasEdited = traverseProps(screen.props) || hasEdited |
|
||||
|
|
||||
if (hasEdited && screen.props.instanceName !== newname) |
|
||||
changedScreens.push(screen.props.instanceName) |
|
||||
} |
|
||||
|
|
||||
for (let pageName in pages) { |
|
||||
const page = pages[pageName] |
|
||||
if (page.appBody === oldname) { |
|
||||
page.appBody = newname |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return { screens, pages, changedScreens } |
|
||||
} |
|
||||
@ -1,16 +1,20 @@ |
|||||
export const DEFAULT_PAGES_OBJECT = { |
export const TableNames = { |
||||
main: { |
USERS: "ta_users", |
||||
props: { |
} |
||||
_component: "@budibase/standard-components/container", |
|
||||
}, |
export const FrontendTypes = { |
||||
_screens: {}, |
PAGE: "page", |
||||
}, |
SCREEN: "screen", |
||||
unauthenticated: { |
LAYOUT: "layout", |
||||
props: { |
NONE: "none", |
||||
_component: "@budibase/standard-components/container", |
} |
||||
}, |
|
||||
_screens: {}, |
// fields on the user table that cannot be edited
|
||||
|
export const UNEDITABLE_USER_FIELDS = ["email", "password", "roleId"] |
||||
|
|
||||
|
export const LAYOUT_NAMES = { |
||||
|
MASTER: { |
||||
|
PRIVATE: "layout_private_master", |
||||
|
PUBLIC: "layout_private_master", |
||||
}, |
}, |
||||
componentLibraries: [], |
|
||||
stylesheets: [], |
|
||||
} |
} |
||||
|
|||||
@ -1,4 +1,6 @@ |
|||||
<script> |
<script> |
||||
|
import { store } from "builderStore" |
||||
import { params } from "@sveltech/routify" |
import { params } from "@sveltech/routify" |
||||
store.actions.pages.select($params.page) |
|
||||
|
store.actions.layouts.select($params.layout) |
||||
</script> |
</script> |
||||
|
|||||
@ -0,0 +1,63 @@ |
|||||
|
<script> |
||||
|
import { params, leftover, goto } from "@sveltech/routify" |
||||
|
import { FrontendTypes } from "constants" |
||||
|
import { store, allScreens } from "builderStore" |
||||
|
|
||||
|
// Get any leftover params not caught by Routifys params store. |
||||
|
const componentIds = $leftover.split("/").filter(id => id !== "") |
||||
|
|
||||
|
const currentAssetId = decodeURI($params.asset) |
||||
|
|
||||
|
let assetList |
||||
|
let actions |
||||
|
|
||||
|
// Determine screens or layouts based on the URL |
||||
|
if ($params.assetType === FrontendTypes.SCREEN) { |
||||
|
assetList = $allScreens |
||||
|
actions = store.actions.screens |
||||
|
} else { |
||||
|
assetList = $store.layouts |
||||
|
actions = store.actions.layouts |
||||
|
} |
||||
|
|
||||
|
// select the screen or layout in the UI |
||||
|
actions.select(currentAssetId) |
||||
|
|
||||
|
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it. |
||||
|
if ($leftover) { |
||||
|
// Get the correct screen children. |
||||
|
const assetChildren = assetList.find( |
||||
|
asset => |
||||
|
asset._id === $params.asset || |
||||
|
asset._id === decodeURIComponent($params.asset) |
||||
|
).props._children |
||||
|
findComponent(componentIds, assetChildren) |
||||
|
} |
||||
|
// } |
||||
|
|
||||
|
// Find Component with ID and continue |
||||
|
function findComponent(ids, children) { |
||||
|
// Setup stuff |
||||
|
let componentToSelect |
||||
|
let currentChildren = children |
||||
|
|
||||
|
// Loop through each ID |
||||
|
ids.forEach(id => { |
||||
|
// Find ID |
||||
|
const component = currentChildren.find(child => child._id === id) |
||||
|
|
||||
|
// If it does not exist, ignore (use last valid route) |
||||
|
if (!component) return |
||||
|
|
||||
|
componentToSelect = component |
||||
|
|
||||
|
// Update childrens array to selected components children |
||||
|
currentChildren = componentToSelect._children |
||||
|
}) |
||||
|
|
||||
|
// Select Component! |
||||
|
if (componentToSelect) store.actions.components.select(componentToSelect) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<slot /> |
||||
@ -0,0 +1,17 @@ |
|||||
|
<script> |
||||
|
import { store, allScreens } from "builderStore" |
||||
|
import { FrontendTypes } from "constants" |
||||
|
import { goto, params } from "@sveltech/routify" |
||||
|
|
||||
|
// Go to first layout |
||||
|
if ($params.assetType === FrontendTypes.LAYOUT) { |
||||
|
$goto(`../${$store.layouts[0]?._id}`) |
||||
|
} |
||||
|
|
||||
|
// Go to first screen |
||||
|
if ($params.assetType === FrontendTypes.SCREEN) { |
||||
|
$goto(`../${$allScreens[0]?._id}`) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<!-- routify:options index=false --> |
||||
@ -1,69 +0,0 @@ |
|||||
<script> |
|
||||
import { onMount } from "svelte" |
|
||||
import { params, leftover, goto } from "@sveltech/routify" |
|
||||
import { store, allScreens } from "builderStore" |
|
||||
|
|
||||
// Get any leftover params not caught by Routifys params store. |
|
||||
const componentIds = $leftover.split("/").filter(id => id !== "") |
|
||||
|
|
||||
// It's a screen, set it to that screen |
|
||||
if ($params.screen !== "page-layout") { |
|
||||
const currentScreenName = decodeURI($params.screen) |
|
||||
const validScreen = |
|
||||
$allScreens.findIndex(screen => screen._id === currentScreenName) !== -1 |
|
||||
|
|
||||
if (!validScreen) { |
|
||||
// Go to main layout if URL set to invalid screen |
|
||||
store.actions.pages.select("main") |
|
||||
$goto("../../main") |
|
||||
} else { |
|
||||
// Otherwise proceed to set screen |
|
||||
store.actions.screens.select(currentScreenName) |
|
||||
|
|
||||
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it. |
|
||||
if ($leftover) { |
|
||||
// Get the correct screen children. |
|
||||
const screenChildren = $store.pages[$params.page]._screens.find( |
|
||||
screen => |
|
||||
screen._id === $params.screen || |
|
||||
screen._id === decodeURIComponent($params.screen) |
|
||||
).props._children |
|
||||
findComponent(componentIds, screenChildren) |
|
||||
} |
|
||||
} |
|
||||
} else { |
|
||||
// It's a page, so set the screentype to page. |
|
||||
store.actions.selectPageOrScreen("page") |
|
||||
|
|
||||
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it. |
|
||||
if ($leftover) { |
|
||||
findComponent(componentIds, $store.pages[$params.page].props._children) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Find Component with ID and continue |
|
||||
function findComponent(ids, children) { |
|
||||
// Setup stuff |
|
||||
let componentToSelect |
|
||||
let currentChildren = children |
|
||||
|
|
||||
// Loop through each ID |
|
||||
ids.forEach(id => { |
|
||||
// Find ID |
|
||||
const component = currentChildren.find(child => child._id === id) |
|
||||
|
|
||||
// If it does not exist, ignore (use last valid route) |
|
||||
if (!component) return |
|
||||
|
|
||||
componentToSelect = component |
|
||||
|
|
||||
// Update childrens array to selected components children |
|
||||
currentChildren = componentToSelect._children |
|
||||
}) |
|
||||
|
|
||||
// Select Component! |
|
||||
if (componentToSelect) store.actions.components.select(componentToSelect) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<slot /> |
|
||||
@ -1,8 +0,0 @@ |
|||||
<script> |
|
||||
import { params } from "@sveltech/routify" |
|
||||
import { store } from "builderStore" |
|
||||
|
|
||||
store.actions.pages.select($params.page) |
|
||||
</script> |
|
||||
|
|
||||
<slot /> |
|
||||
@ -1,4 +0,0 @@ |
|||||
<script> |
|
||||
import { goto } from "@sveltech/routify" |
|
||||
$goto("../page-layout") |
|
||||
</script> |
|
||||
@ -1,6 +1,8 @@ |
|||||
<script> |
<script> |
||||
import { goto } from "@sveltech/routify" |
import { goto } from "@sveltech/routify" |
||||
$goto("../main") |
import { FrontendTypes } from "constants" |
||||
|
|
||||
|
$goto(`../${FrontendTypes.SCREEN}`) |
||||
</script> |
</script> |
||||
|
|
||||
<!-- routify:options index=false --> |
<!-- routify:options index=1 --> |
||||
|
|||||
@ -1,51 +0,0 @@ |
|||||
import { |
|
||||
generate_css, |
|
||||
generate_screen_css, |
|
||||
} from "../src/builderStore/generate_css.js" |
|
||||
|
|
||||
describe("generate_css", () => { |
|
||||
|
|
||||
|
|
||||
test("Check how array styles are output", () => { |
|
||||
expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;") |
|
||||
}) |
|
||||
|
|
||||
test("Check handling of an array with empty string values", () => { |
|
||||
expect(generate_css({ padding: ["", "", "", ""] })).toBe("") |
|
||||
}) |
|
||||
|
|
||||
test("Check handling of an empty array", () => { |
|
||||
expect(generate_css({ margin: [] })).toBe("") |
|
||||
}) |
|
||||
|
|
||||
test("Check handling of valid font property", () => { |
|
||||
expect(generate_css({ "font-size": "10px" })).toBe("font-size: 10px;") |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
|
|
||||
describe("generate_screen_css", () => { |
|
||||
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } } |
|
||||
|
|
||||
test("Test generation of normal css styles", () => { |
|
||||
expect(generate_screen_css([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}") |
|
||||
}) |
|
||||
|
|
||||
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } } |
|
||||
|
|
||||
test("Test generation of hover css styles", () => { |
|
||||
expect(generate_screen_css([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}") |
|
||||
}) |
|
||||
|
|
||||
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } } |
|
||||
|
|
||||
test("Test generation of selection css styles", () => { |
|
||||
expect(generate_screen_css([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}") |
|
||||
}) |
|
||||
|
|
||||
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } } |
|
||||
|
|
||||
test.only("Testing handling of empty component styles", () => { |
|
||||
expect(generate_screen_css([emptyComponent])).toBe("") |
|
||||
}) |
|
||||
}) |
|
||||
File diff suppressed because it is too large
@ -1,6 +1,5 @@ |
|||||
.DS_Store |
.DS_Store |
||||
node_modules |
node_modules |
||||
package-lock.json |
package-lock.json |
||||
yarn.lock |
|
||||
release/ |
release/ |
||||
dist/ |
dist/ |
||||
@ -1,13 +0,0 @@ |
|||||
module.exports = { |
|
||||
presets: ["@babel/preset-env"], |
|
||||
sourceMaps: "inline", |
|
||||
retainLines: true, |
|
||||
plugins: [ |
|
||||
[ |
|
||||
"@babel/plugin-transform-runtime", |
|
||||
{ |
|
||||
regenerator: true, |
|
||||
}, |
|
||||
], |
|
||||
], |
|
||||
} |
|
||||
@ -0,0 +1,96 @@ |
|||||
|
import { getAppId } from "../utils/getAppId" |
||||
|
|
||||
|
/** |
||||
|
* API cache for cached request responses. |
||||
|
*/ |
||||
|
let cache = {} |
||||
|
|
||||
|
/** |
||||
|
* Makes a fully formatted URL based on the SDK configuration. |
||||
|
*/ |
||||
|
const makeFullURL = path => { |
||||
|
return `/${path}`.replace("//", "/") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handler for API errors. |
||||
|
*/ |
||||
|
const handleError = error => { |
||||
|
return { error } |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Performs an API call to the server. |
||||
|
* App ID header is always correctly set. |
||||
|
*/ |
||||
|
const makeApiCall = async ({ method, url, body, json = true }) => { |
||||
|
try { |
||||
|
const requestBody = json ? JSON.stringify(body) : body |
||||
|
let headers = { |
||||
|
Accept: "application/json", |
||||
|
"Content-Type": "application/json", |
||||
|
"x-budibase-app-id": getAppId(), |
||||
|
} |
||||
|
if (!window["##BUDIBASE_IN_BUILDER##"]) { |
||||
|
headers["x-budibase-type"] = "client" |
||||
|
} |
||||
|
const response = await fetch(url, { |
||||
|
method, |
||||
|
headers, |
||||
|
body: requestBody, |
||||
|
credentials: "same-origin", |
||||
|
}) |
||||
|
switch (response.status) { |
||||
|
case 200: |
||||
|
return response.json() |
||||
|
case 404: |
||||
|
return handleError(`${url}: Not Found`) |
||||
|
case 400: |
||||
|
return handleError(`${url}: Bad Request`) |
||||
|
case 403: |
||||
|
return handleError(`${url}: Forbidden`) |
||||
|
default: |
||||
|
if (response.status >= 200 && response.status < 400) { |
||||
|
return response.json() |
||||
|
} |
||||
|
return handleError(`${url} - ${response.statusText}`) |
||||
|
} |
||||
|
} catch (error) { |
||||
|
return handleError(error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Performs an API call to the server and caches the response. |
||||
|
* Future invocation for this URL will return the cached result instead of |
||||
|
* hitting the server again. |
||||
|
*/ |
||||
|
const makeCachedApiCall = async params => { |
||||
|
const identifier = params.url |
||||
|
if (!identifier) { |
||||
|
return null |
||||
|
} |
||||
|
if (!cache[identifier]) { |
||||
|
cache[identifier] = makeApiCall(params) |
||||
|
cache[identifier] = await cache[identifier] |
||||
|
} |
||||
|
return await cache[identifier] |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Constructs an API call function for a particular HTTP method. |
||||
|
*/ |
||||
|
const requestApiCall = method => async params => { |
||||
|
const { url, cache = false } = params |
||||
|
const fullURL = makeFullURL(url) |
||||
|
const enrichedParams = { ...params, method, url: fullURL } |
||||
|
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams) |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
post: requestApiCall("POST"), |
||||
|
get: requestApiCall("GET"), |
||||
|
patch: requestApiCall("PATCH"), |
||||
|
del: requestApiCall("DELETE"), |
||||
|
error: handleError, |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue