forked from tsai/budibase
70 changed files with 1534 additions and 531 deletions
@ -0,0 +1,56 @@ |
|||
<script> |
|||
import { Button } from "@budibase/bbui" |
|||
export let remove = false |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="content"> |
|||
<div class="img"> |
|||
<img src="https://picsum.photos/60/60" alt="zoom" /> |
|||
</div> |
|||
<div class="body"> |
|||
<div class="title">Zoom</div> |
|||
<div class="description"> |
|||
Lorem, ipsum dolor sit amet consectetur adipisicing elit |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="footer"> |
|||
<Button wide error={remove} secondary={!remove} on:click> |
|||
<span>{remove ? 'Remove' : 'Add'}</span> |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
padding: 12px; |
|||
background: var(--light-grey); |
|||
grid-gap: 20px; |
|||
} |
|||
span { |
|||
font-size: 12px; |
|||
font-weight: bold; |
|||
} |
|||
.content { |
|||
display: grid; |
|||
grid-gap: 12px; |
|||
grid-template-columns: 60px auto; |
|||
} |
|||
.title { |
|||
font-size: 14px; |
|||
font-weight: bold; |
|||
} |
|||
.description { |
|||
font-size: 12px; |
|||
} |
|||
.img { |
|||
border-radius: 3px; |
|||
overflow: hidden; |
|||
} |
|||
img { |
|||
height: 60px; |
|||
width: 60px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,48 @@ |
|||
<script> |
|||
import Modal from "./Modal.svelte" |
|||
import { SettingsIcon } from "components/common/Icons/" |
|||
import { getContext } from "svelte" |
|||
import { isActive, goto, layout } from "@sveltech/routify" |
|||
|
|||
// Handle create app modal |
|||
const { open } = getContext("simple-modal") |
|||
|
|||
const showSettingsModal = () => { |
|||
open( |
|||
Modal, |
|||
{ |
|||
name: "Placeholder App Name", |
|||
description: "This is a hardcoded description that needs to change", |
|||
}, |
|||
{ |
|||
closeButton: false, |
|||
closeOnEsc: true, |
|||
styleContent: { padding: 0 }, |
|||
closeOnOuterClick: true, |
|||
} |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<span class="topnavitemright" on:click={showSettingsModal}> |
|||
<SettingsIcon /> |
|||
</span> |
|||
|
|||
<style> |
|||
span:first-letter { |
|||
text-transform: capitalize; |
|||
} |
|||
.topnavitemright { |
|||
cursor: pointer; |
|||
color: var(--ink-light); |
|||
margin: 0px 20px 0px 0px; |
|||
padding-top: 4px; |
|||
font-weight: 500; |
|||
font-size: 1rem; |
|||
height: 100%; |
|||
display: flex; |
|||
flex: 1; |
|||
align-items: center; |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,92 @@ |
|||
<script> |
|||
import { General, Users, DangerZone } from "./tabs" |
|||
|
|||
import { Input, TextArea, Button, Switcher } from "@budibase/bbui" |
|||
import { SettingsIcon, CloseIcon } from "components/common/Icons/" |
|||
import { getContext } from "svelte" |
|||
import { post } from "builderStore/api" |
|||
|
|||
const { open, close } = getContext("simple-modal") |
|||
export let name = "" |
|||
export let description = "" |
|||
const tabs = [ |
|||
{ |
|||
title: "General", |
|||
key: "GENERAL", |
|||
component: General, |
|||
}, |
|||
{ |
|||
title: "Users", |
|||
key: "USERS", |
|||
component: Users, |
|||
}, |
|||
{ |
|||
title: "Danger Zone", |
|||
key: "DANGERZONE", |
|||
component: DangerZone, |
|||
}, |
|||
] |
|||
let value = "GENERAL" |
|||
$: selectedTab = tabs.find(tab => tab.key === value).component |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="body"> |
|||
<div class="heading"> |
|||
<span class="icon"> |
|||
<SettingsIcon /> |
|||
</span> |
|||
<h3>Settings</h3> |
|||
</div> |
|||
<Switcher headings={tabs} bind:value> |
|||
<svelte:component this={selectedTab} /> |
|||
</Switcher> |
|||
</div> |
|||
<div class="close-button" on:click={close}> |
|||
<CloseIcon /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
position: relative; |
|||
} |
|||
|
|||
.close-button { |
|||
cursor: pointer; |
|||
position: absolute; |
|||
top: 20px; |
|||
right: 20px; |
|||
} |
|||
.close-button :global(svg) { |
|||
width: 24px; |
|||
height: 24px; |
|||
} |
|||
.heading { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
margin-bottom: 20px; |
|||
} |
|||
h3 { |
|||
margin: 0; |
|||
font-size: 24px; |
|||
font-weight: bold; |
|||
} |
|||
.icon { |
|||
display: grid; |
|||
border-radius: 3px; |
|||
align-content: center; |
|||
justify-content: center; |
|||
margin-right: 12px; |
|||
height: 20px; |
|||
width: 20px; |
|||
padding: 10px; |
|||
background-color: var(--blue-light); |
|||
} |
|||
.body { |
|||
padding: 40px 40px 80px 40px; |
|||
display: grid; |
|||
grid-gap: 20px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,13 @@ |
|||
<h3> |
|||
<slot /> |
|||
</h3> |
|||
|
|||
<style> |
|||
h3 { |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
color: var(--ink); |
|||
margin-top: 40px; |
|||
margin-bottom: 20px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,42 @@ |
|||
<script> |
|||
import { createEventDispatcher } from "svelte" |
|||
const dispatch = createEventDispatcher() |
|||
import { Input, Select, Button } from "@budibase/bbui" |
|||
export let user |
|||
|
|||
let editMode = false |
|||
</script> |
|||
|
|||
<div class="inputs"> |
|||
<Input |
|||
disabled |
|||
thin |
|||
bind:value={user.username} |
|||
name="Name" |
|||
placeholder="Username" /> |
|||
<Select disabled={!editMode} bind:value={user.accessLevelId} thin> |
|||
<option value="ADMIN">Admin</option> |
|||
<option value="POWER_USER">Power User</option> |
|||
</Select> |
|||
{#if editMode} |
|||
<Button |
|||
blue |
|||
on:click={() => { |
|||
dispatch('save', user) |
|||
editMode = false |
|||
}}> |
|||
Save |
|||
</Button> |
|||
{:else} |
|||
<Button secondary on:click={() => (editMode = true)}>Edit</Button> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.inputs { |
|||
display: grid; |
|||
justify-items: stretch; |
|||
grid-gap: 18px; |
|||
grid-template-columns: 1fr 1fr 1fr; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,46 @@ |
|||
<script> |
|||
import { Input, TextArea, Button } from "@budibase/bbui" |
|||
import Title from "../TabTitle.svelte" |
|||
|
|||
let value = "" |
|||
let loading = false |
|||
|
|||
const deleteApp = () => { |
|||
loading = true |
|||
// Do stuff here to delete app! |
|||
// Navigate to start |
|||
} |
|||
</script> |
|||
|
|||
<Title>Danger Zone</Title> |
|||
<div class="background"> |
|||
<Input |
|||
on:change={e => (value = e.target.value)} |
|||
on:input={e => (value = e.target.value)} |
|||
thin |
|||
disabled={loading} |
|||
placeholder="Enter your name" |
|||
label="Type DELETE into the textbox, then click the following button to |
|||
delete your web app:" /> |
|||
|
|||
<Button |
|||
disabled={value !== 'DELETE' || loading} |
|||
primary |
|||
wide |
|||
on:click={deleteApp}> |
|||
Delete Entire Web App |
|||
</Button> |
|||
</div> |
|||
|
|||
<style> |
|||
.background { |
|||
display: grid; |
|||
grid-gap: var(--space); |
|||
border-radius: 5px; |
|||
background-color: var(--light-grey); |
|||
padding: 12px 12px 18px 12px; |
|||
} |
|||
.background :global(button) { |
|||
max-width: 100%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,26 @@ |
|||
<script> |
|||
import { Input, TextArea, Button } from "@budibase/bbui" |
|||
import Title from "../TabTitle.svelte" |
|||
</script> |
|||
|
|||
<Title>General</Title> |
|||
<div class="container"> |
|||
<div class="background"> |
|||
<Input thin edit placeholder="Enter your name" label="Name" /> |
|||
</div> |
|||
<div class="background"> |
|||
<TextArea thin edit placeholder="Enter your name" label="Name" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
grid-gap: var(--space); |
|||
} |
|||
.background { |
|||
border-radius: 5px; |
|||
background-color: var(--light-grey); |
|||
padding: 12px 12px 18px 12px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,45 @@ |
|||
<script> |
|||
import Integration from "../Integration.svelte" |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="title">Your Integrations</div> |
|||
<div class="integrations"> |
|||
<Integration remove /> |
|||
<Integration remove /> |
|||
<Integration remove /> |
|||
<Integration remove /> |
|||
</div> |
|||
<div class="apps">Recommended apps</div> |
|||
<div class="integrations"> |
|||
<Integration /> |
|||
<Integration /> |
|||
<Integration /> |
|||
<Integration /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
grid-gap: 16px; |
|||
} |
|||
.integrations { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr; |
|||
grid-gap: 20px; |
|||
} |
|||
.title { |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
} |
|||
.apps { |
|||
font-size: 14px; |
|||
font-weight: bold; |
|||
} |
|||
.background { |
|||
border-radius: 5px; |
|||
background-color: var(--light-grey); |
|||
padding: 12px 12px 18px 12px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1 @@ |
|||
Permissions |
|||
@ -0,0 +1,135 @@ |
|||
<script> |
|||
import { Input, Select, Button } from "@budibase/bbui" |
|||
import Title from "../TabTitle.svelte" |
|||
import UserRow from "../UserRow.svelte" |
|||
|
|||
import { store, backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
// import * as api from "../api" |
|||
|
|||
let username = "" |
|||
let password = "" |
|||
let accessLevelId |
|||
|
|||
$: valid = username && password && accessLevelId |
|||
$: appId = $store.appId |
|||
|
|||
// Create user! |
|||
async function createUser() { |
|||
if (valid) { |
|||
const user = { name: username, username, password, accessLevelId } |
|||
const response = await api.post(`/api/users`, user) |
|||
const json = await response.json() |
|||
backendUiStore.actions.users.create(json) |
|||
fetchUsersPromise = fetchUsers() |
|||
} |
|||
} |
|||
|
|||
// Update user! |
|||
async function updateUser(event) { |
|||
let data = event.detail |
|||
delete data.password |
|||
const response = await api.put(`/api/users`, data) |
|||
const users = await response.json() |
|||
backendUiStore.update(state => { |
|||
state.users = users |
|||
return state |
|||
}) |
|||
fetchUsersPromise = fetchUsers() |
|||
} |
|||
|
|||
// Get users |
|||
async function fetchUsers() { |
|||
const response = await api.get(`/api/users`) |
|||
const users = await response.json() |
|||
backendUiStore.update(state => { |
|||
state.users = users |
|||
return state |
|||
}) |
|||
return users |
|||
} |
|||
|
|||
let fetchUsersPromise = fetchUsers() |
|||
</script> |
|||
|
|||
<Title>Users</Title> |
|||
<div class="container"> |
|||
<div class="background create"> |
|||
<div class="title">Create new user</div> |
|||
<div class="inputs"> |
|||
<Input thin bind:value={username} name="Name" placeholder="Username" /> |
|||
<Input |
|||
thin |
|||
bind:value={password} |
|||
name="Password" |
|||
placeholder="Password" /> |
|||
<Select bind:value={accessLevelId} thin> |
|||
<option value="ADMIN">Admin</option> |
|||
<option value="POWER_USER">Power User</option> |
|||
</Select> |
|||
</div> |
|||
<div class="create-button"> |
|||
<Button on:click={createUser} small blue>Create</Button> |
|||
</div> |
|||
</div> |
|||
<div class="background"> |
|||
<div class="title">Current Users</div> |
|||
{#await fetchUsersPromise} |
|||
Loading state! |
|||
{:then users} |
|||
<ul> |
|||
{#each users as user} |
|||
<li> |
|||
<UserRow {user} on:save={updateUser} /> |
|||
</li> |
|||
{:else} |
|||
<li>No Users found</li> |
|||
{/each} |
|||
</ul> |
|||
{:catch error} |
|||
err0r |
|||
{/await} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
grid-gap: 14px; |
|||
} |
|||
.background { |
|||
position: relative; |
|||
display: grid; |
|||
grid-gap: 12px; |
|||
border-radius: 5px; |
|||
background-color: var(--grey-2); |
|||
padding: 12px 12px 18px 12px; |
|||
} |
|||
.background.create { |
|||
background-color: var(--blue-light); |
|||
} |
|||
.inputs :global(select) { |
|||
padding: 12px 9px; |
|||
height: initial; |
|||
} |
|||
.create-button { |
|||
position: absolute; |
|||
top: 12px; |
|||
right: 12px; |
|||
} |
|||
.title { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
} |
|||
.inputs { |
|||
display: grid; |
|||
grid-gap: 18px; |
|||
grid-template-columns: 1fr 1fr 1fr; |
|||
} |
|||
ul { |
|||
list-style: none; |
|||
padding: 0; |
|||
display: grid; |
|||
grid-gap: 8px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,5 @@ |
|||
export { default as General } from "./General.svelte" |
|||
export { default as Integrations } from "./Integrations.svelte" |
|||
export { default as Permissions } from "./Permissions.svelte" |
|||
export { default as Users } from "./Users.svelte" |
|||
export { default as DangerZone } from "./DangerZone.svelte" |
|||
@ -0,0 +1,2 @@ |
|||
export { default as drag } from "./drag.js" |
|||
export { default as keyevents } from "./key-events.js" |
|||
@ -0,0 +1,41 @@ |
|||
//events: Array<{trigger: fn}>
|
|||
export default function(node, events = []) { |
|||
const ev = Object.entries(events) |
|||
let fns = [] |
|||
|
|||
for (let [trigger, fn] of ev) { |
|||
let f = addEvent(trigger, fn) |
|||
fns = [...fns, f] |
|||
} |
|||
|
|||
function _scaffold(trigger, fn) { |
|||
return () => { |
|||
let trig = parseInt(trigger) |
|||
if (trig) { |
|||
if (event.keyCode === trig) { |
|||
fn(event) |
|||
} |
|||
} else { |
|||
if (event.key === trigger) { |
|||
fn(event) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
function addEvent(trigger, fn) { |
|||
let f = _scaffold(trigger, fn) |
|||
node.addEventListener("keydown", f) |
|||
return f |
|||
} |
|||
|
|||
function removeEvents() { |
|||
fns.forEach(f => node.removeEventListener("keypress", f)) |
|||
} |
|||
|
|||
return { |
|||
destroy() { |
|||
removeEvents() |
|||
}, |
|||
} |
|||
} |
|||
@ -1,5 +1,5 @@ |
|||
<script> |
|||
import { buildStyle } from "./helpers.js" |
|||
import {buildStyle} from "../helpers.js" |
|||
import { fade } from "svelte/transition" |
|||
|
|||
export let backgroundSize = "10px" |
|||
@ -0,0 +1,37 @@ |
|||
<script> |
|||
import { onMount } from "svelte"; |
|||
|
|||
export let target = document.body; |
|||
|
|||
let targetEl; |
|||
let portal; |
|||
let componentInstance; |
|||
|
|||
onMount(() => { |
|||
if (typeof target === "string") { |
|||
targetEl = document.querySelector(target); |
|||
// Force exit |
|||
if (targetEl === null) { |
|||
return () => {}; |
|||
} |
|||
} else if (target instanceof HTMLElement) { |
|||
targetEl = target; |
|||
} else { |
|||
throw new TypeError( |
|||
`Unknown target type: ${typeof target}. Allowed types: String (CSS selector), HTMLElement.` |
|||
); |
|||
} |
|||
|
|||
portal = document.createElement("div"); |
|||
targetEl.appendChild(portal); |
|||
portal.appendChild(componentInstance); |
|||
|
|||
return () => { |
|||
targetEl.removeChild(portal); |
|||
}; |
|||
}); |
|||
</script> |
|||
|
|||
<div bind:this={componentInstance}> |
|||
<slot /> |
|||
</div> |
|||
@ -1,2 +1,2 @@ |
|||
import Colorpreview from "./Colorpreview.svelte" |
|||
import Colorpreview from "./components/Colorpreview.svelte" |
|||
export default Colorpreview |
|||
|
|||
@ -1 +0,0 @@ |
|||
<slot /> |
|||
@ -1,36 +0,0 @@ |
|||
<script> |
|||
import { store, backendUiStore } from "builderStore" |
|||
import { goto } from "@sveltech/routify" |
|||
import { onMount } from "svelte" |
|||
|
|||
$: instances = $store.appInstances |
|||
|
|||
async function selectDatabase(database) { |
|||
backendUiStore.actions.database.select(database) |
|||
} |
|||
|
|||
onMount(async () => { |
|||
if ($store.appInstances.length > 0) { |
|||
await selectDatabase($store.appInstances[0]) |
|||
$goto(`./${$backendUiStore.selectedDatabase._id}`) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<div class="node-view"> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
height: 100%; |
|||
position: relative; |
|||
} |
|||
|
|||
.node-view { |
|||
overflow-y: auto; |
|||
flex: 1 1 auto; |
|||
} |
|||
</style> |
|||
@ -1,20 +0,0 @@ |
|||
<script> |
|||
import { store, backendUiStore } from "builderStore" |
|||
import { goto } from "@sveltech/routify" |
|||
import { onMount } from "svelte" |
|||
|
|||
$: instances = $store.appInstances |
|||
|
|||
async function selectDatabase(database) { |
|||
backendUiStore.actions.database.select(database) |
|||
} |
|||
|
|||
onMount(async () => { |
|||
if ($store.appInstances.length > 0) { |
|||
await selectDatabase($store.appInstances[0]) |
|||
$goto(`../${$backendUiStore.selectedDatabase._id}`) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
Please select a database |
|||
@ -1,6 +1,6 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
$goto("../database") |
|||
$goto("../model") |
|||
</script> |
|||
|
|||
<!-- routify:options index=false --> |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
<script> |
|||
import { params } from "@sveltech/routify" |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
if ($params.selectedModel) { |
|||
const model = $backendUiStore.models.find(m => m._id === $params.selectedModel) |
|||
if (model) { |
|||
backendUiStore.actions.models.select(model) |
|||
} |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -0,0 +1,35 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { goto, leftover } from "@sveltech/routify" |
|||
import { onMount } from "svelte" |
|||
|
|||
async function selectModel(model) { |
|||
backendUiStore.actions.models.select(model) |
|||
} |
|||
|
|||
onMount(async () => { |
|||
// navigate to first model in list, if not already selected |
|||
// and this is the final url (i.e. no selectedModel) |
|||
if (!$leftover && $backendUiStore.models.length > 0 && (!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)) { |
|||
$goto(`./${$backendUiStore.models[0]._id}`) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<div class="node-view"> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
height: 100%; |
|||
position: relative; |
|||
} |
|||
|
|||
.node-view { |
|||
overflow-y: auto; |
|||
flex: 1 1 auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,24 @@ |
|||
<script> |
|||
import { store, backendUiStore } from "builderStore" |
|||
import { goto, leftover } from "@sveltech/routify" |
|||
import { onMount } from "svelte" |
|||
|
|||
async function selectModel(model) { |
|||
backendUiStore.actions.models.select(model) |
|||
} |
|||
|
|||
onMount(async () => { |
|||
// navigate to first model in list, if not already selected |
|||
// and this is the final url (i.e. no selectedModel) |
|||
if (!$leftover && $backendUiStore.models.length > 0 && (!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)) { |
|||
// this file routes as .../models/index, so, go up one. |
|||
$goto(`../${$backendUiStore.models[0]._id}`) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
{#if $backendUiStore.models.length === 0} |
|||
Please create a model |
|||
{:else} |
|||
Please select a model |
|||
{/if} |
|||
@ -0,0 +1,173 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
export let _bb |
|||
export let model |
|||
|
|||
const TYPE_MAP = { |
|||
string: "text", |
|||
boolean: "checkbox", |
|||
number: "number", |
|||
} |
|||
|
|||
let username |
|||
let password |
|||
let newModel = { |
|||
modelId: model, |
|||
} |
|||
let store = _bb.store |
|||
let schema = {} |
|||
let modelDef = {} |
|||
$: if (model && model.length !== 0) { |
|||
fetchModel() |
|||
} |
|||
$: fields = Object.keys(schema) |
|||
async function fetchModel() { |
|||
const FETCH_MODEL_URL = `/api/models/${model}` |
|||
const response = await _bb.api.get(FETCH_MODEL_URL) |
|||
modelDef = await response.json() |
|||
schema = modelDef.schema |
|||
} |
|||
async function save() { |
|||
const SAVE_RECORD_URL = `/api/${model}/records` |
|||
const response = await _bb.api.post(SAVE_RECORD_URL, newModel) |
|||
const json = await response.json() |
|||
store.update(state => { |
|||
state[model] = state[model] ? [...state[model], json] : [json] |
|||
return state |
|||
}) |
|||
} |
|||
const handleInput = field => event => { |
|||
let value |
|||
if (event.target.type === "checkbox") { |
|||
value = event.target.checked |
|||
newModel[field] = value |
|||
return |
|||
} |
|||
if (event.target.type === "number") { |
|||
value = parseInt(event.target.value) |
|||
newModel[field] = value |
|||
return |
|||
} |
|||
value = event.target.value |
|||
newModel[field] = value |
|||
} |
|||
</script> |
|||
|
|||
<form class="form" on:submit|preventDefault> |
|||
<h1>{modelDef.name} Form</h1> |
|||
<hr /> |
|||
<div class="form-content"> |
|||
{#each fields as field} |
|||
<div class="form-item"> |
|||
<label class="form-label" for="form-stacked-text">{field}</label> |
|||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion} |
|||
<select on:blur={handleInput(field)}> |
|||
{#each schema[field].constraints.inclusion as opt} |
|||
<option>{opt}</option> |
|||
{/each} |
|||
</select> |
|||
{:else} |
|||
<input |
|||
class="input" |
|||
type={TYPE_MAP[schema[field].type]} |
|||
on:change={handleInput(field)} /> |
|||
{/if} |
|||
</div> |
|||
<hr /> |
|||
{/each} |
|||
<div class="button-block"> |
|||
<button on:click={save}>Submit Form</button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
|
|||
<style> |
|||
.form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
margin: auto; |
|||
padding: 40px; |
|||
} |
|||
.form-content { |
|||
margin-bottom: 20px; |
|||
} |
|||
.input { |
|||
border-radius: 5px; |
|||
border: 1px solid #e6e6e6; |
|||
padding: 1em; |
|||
font-size: 16px; |
|||
} |
|||
|
|||
.form-item { |
|||
display: grid; |
|||
grid-template-columns: 30% 1fr; |
|||
margin-bottom: 16px; |
|||
align-items: center; |
|||
gap: 1em; |
|||
} |
|||
|
|||
hr { |
|||
border: 1px solid #f5f5f5; |
|||
margin: 40px 0px; |
|||
} |
|||
hr:nth-last-child(2) { |
|||
border: 1px solid #f5f5f5; |
|||
margin: 40px 0px; |
|||
} |
|||
.button-block { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
} |
|||
button { |
|||
font-size: 16px; |
|||
padding: 8px 16px; |
|||
box-sizing: border-box; |
|||
border-radius: 4px; |
|||
color: white; |
|||
background-color: black; |
|||
outline: none; |
|||
height: 40px; |
|||
cursor: pointer; |
|||
transition: all 0.2s ease 0s; |
|||
overflow: hidden; |
|||
outline: none; |
|||
user-select: none; |
|||
white-space: nowrap; |
|||
text-align: center; |
|||
} |
|||
|
|||
button:hover { |
|||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), |
|||
0 4px 6px -2px rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
input[type="checkbox"] { |
|||
transform: scale(2); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
select::-ms-expand { |
|||
display: none; |
|||
} |
|||
select { |
|||
cursor: pointer; |
|||
display: inline-block; |
|||
align-items: baseline; |
|||
box-sizing: border-box; |
|||
padding: 1em 1em; |
|||
border: 1px solid #eaeaea; |
|||
border-radius: 5px; |
|||
font: inherit; |
|||
line-height: inherit; |
|||
-webkit-appearance: none; |
|||
-moz-appearance: none; |
|||
-ms-appearance: none; |
|||
appearance: none; |
|||
background-repeat: no-repeat; |
|||
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), |
|||
linear-gradient(135deg, currentColor 50%, transparent 50%); |
|||
background-position: right 17px top 1.5em, right 10px top 1.5em; |
|||
background-size: 7px 7px, 7px 7px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,52 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
|
|||
export let _bb |
|||
export let model |
|||
|
|||
let headers = [] |
|||
let store = _bb.store |
|||
let target |
|||
|
|||
async function fetchFirstRecord() { |
|||
const FETCH_RECORDS_URL = `/api/views/all_${model}` |
|||
const response = await _bb.api.get(FETCH_RECORDS_URL) |
|||
if (response.status === 200) { |
|||
const allRecords = await response.json() |
|||
if (allRecords.length > 0) return allRecords[0] |
|||
} |
|||
} |
|||
|
|||
async function fetchData() { |
|||
const pathParts = window.location.pathname.split("/") |
|||
|
|||
let record |
|||
// if srcdoc, then we assume this is the builder preview |
|||
if(pathParts.length === 0 || pathParts[0] === "srcdoc") { |
|||
record = await fetchFirstRecord() |
|||
} else { |
|||
const id = pathParts[pathParts.length - 1] |
|||
const GET_RECORD_URL = `/api/${model}/records/${id}` |
|||
const response = await _bb.api.get(GET_RECORD_URL) |
|||
if (response.status === 200) { |
|||
record = await response.json() |
|||
} |
|||
} |
|||
|
|||
if (record) { |
|||
_bb.attachChildren(target, { |
|||
hydrate: false, |
|||
context: record, |
|||
}) |
|||
} else { |
|||
throw new Error("Failed to fetch record.", response) |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
await fetchData() |
|||
}) |
|||
</script> |
|||
|
|||
<section bind:this={target}> |
|||
</section> |
|||
Loading…
Reference in new issue