pulling app list, re-adding code for automation blocks and external data source block lost in merge

This commit is contained in:
Martin McKeaveney 2021-05-06 18:31:03 +01:00
commit 062d8323f4
92 changed files with 679 additions and 3762 deletions

View File

@ -16,6 +16,11 @@ static_resources:
- name: local_services - name: local_services
domains: ["*"] domains: ["*"]
routes: routes:
# special case to redirect specifically the route path
# to the builder, if this were a prefix then it would break minio
- match: { path: "/" }
redirect: { path_redirect: "/app/" }
- match: { prefix: "/db/" } - match: { prefix: "/db/" }
route: route:
cluster: couchdb-service cluster: couchdb-service
@ -33,14 +38,18 @@ static_resources:
route: route:
cluster: server-dev cluster: server-dev
- match: { prefix: "/" } - match: { path: "/" }
route: route:
cluster: builder-dev cluster: builder-dev
- match: { prefix: "/builder" } - match: { prefix: "/app/" }
route: route:
cluster: builder-dev cluster: builder-dev
prefix_rewrite: "/builder/"
- match: { prefix: "/app" }
route:
cluster: builder-dev
prefix_rewrite: "/app/"
# minio is on the default route because this works # minio is on the default route because this works
# best, minio + AWS SDK doesn't handle path proxy # best, minio + AWS SDK doesn't handle path proxy

View File

@ -21,7 +21,6 @@ static_resources:
cluster: app-service cluster: app-service
prefix_rewrite: "/" prefix_rewrite: "/"
# special case for presenting our static self hosting page
- match: { path: "/" } - match: { path: "/" }
route: route:
cluster: app-service cluster: app-service

View File

@ -4,6 +4,7 @@
import Menu from "../Menu/Menu.svelte" import Menu from "../Menu/Menu.svelte"
export let disabled = false export let disabled = false
export let align = "left"
let anchor let anchor
let dropdown let dropdown
@ -31,7 +32,7 @@
<div use:getAnchor on:click={openMenu}> <div use:getAnchor on:click={openMenu}>
<slot name="control" /> <slot name="control" />
</div> </div>
<Popover bind:this={dropdown} {anchor} align="left"> <Popover bind:this={dropdown} {anchor} {align}>
<Menu> <Menu>
<slot /> <slot />
</Menu> </Menu>

View File

@ -19,6 +19,7 @@
export let getOptionValue = option => option export let getOptionValue = option => option
export let open = false export let open = false
export let readonly = false export let readonly = false
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onClick = e => { const onClick = e => {
@ -33,6 +34,7 @@
<button <button
{id} {id}
class="spectrum-Picker spectrum-Picker--sizeM" class="spectrum-Picker spectrum-Picker--sizeM"
class:spectrum-Picker--quiet={quiet}
{disabled} {disabled}
class:is-invalid={!!error} class:is-invalid={!!error}
class:is-open={open} class:is-open={open}

View File

@ -11,6 +11,7 @@
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
export let readonly = false export let readonly = false
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let open = false let open = false
@ -43,6 +44,7 @@
<Picker <Picker
on:click on:click
bind:open bind:open
{quiet}
{id} {id}
{error} {error}
{disabled} {disabled}

View File

@ -13,6 +13,7 @@
export let options = [] export let options = []
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -29,6 +30,7 @@
<Field {label} {labelPosition} {disabled} {error}> <Field {label} {labelPosition} {disabled} {error}>
<Select <Select
{quiet}
{error} {error}
{disabled} {disabled}
{readonly} {readonly}

View File

@ -1,62 +1,16 @@
<script> <script>
export let forAttr = "", import "@spectrum-css/fieldlabel/dist/index-vars.css"
extraSmall = false,
small = false, export let size = "M"
medium = false,
large = false,
extraLarge = false,
white = false,
grey = false,
black = false
</script> </script>
<label <label class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
class="bb-label"
class:extraSmall
class:small
class:medium
class:large
class:extraLarge
class:white
class:grey
class:black
for={forAttr}
>
<slot /> <slot />
</label> </label>
<style> <style>
.bb-label { label {
font-family: var(--font-sans); padding: 0;
font-weight: 500; white-space: nowrap;
text-rendering: var(--text-render);
color: var(--ink);
font-size: var(--font-size-s);
margin-bottom: var(--spacing-s);
display: block;
}
.extraSmall {
font-size: var(--font-size-xs);
}
.small {
font-size: var(--font-size-s);
}
.medium {
font-size: var(--font-size-m);
}
.large {
font-size: var(--font-size-l);
}
.extraLarge {
font-size: var(--font-size-xl);
}
.white {
color: white;
}
.grey {
color: var(--grey-6);
}
.black {
color: var(--ink);
} }
</style> </style>

View File

@ -44,6 +44,9 @@
padding-top: var(--spacing-l); padding-top: var(--spacing-l);
padding-bottom: var(--spacing-l); padding-bottom: var(--spacing-l);
} }
.gap-XS {
grid-gap: var(--spacing-s);
}
.gap-S { .gap-S {
grid-gap: var(--spectrum-alias-grid-gutter-xsmall); grid-gap: var(--spectrum-alias-grid-gutter-xsmall);
} }

View File

@ -27,7 +27,7 @@
> >
{#if icon} {#if icon}
<svg <svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Menu-itemIcon" class="spectrum-Icon spectrum-Icon--sizeS spectrum-Menu-itemIcon"
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
aria-label={icon} aria-label={icon}

View File

@ -3,11 +3,20 @@
export let size = "M" export let size = "M"
export let serif = false export let serif = false
export let noPadding = false
</script> </script>
<p <p
class="spectrum-Body class:spectrum-Body--size{size}" class:noPadding
class="spectrum-Body spectrum-Body--size{size}"
class:spectrum-Body--serif={serif} class:spectrum-Body--serif={serif}
> >
<slot /> <slot />
</p> </p>
<style>
.noPadding {
padding: 0;
margin: 0;
}
</style>

View File

@ -1,5 +1,5 @@
{ {
"baseUrl": "http://localhost:10000/builder/", "baseUrl": "http://localhost:10000/app/",
"video": true, "video": true,
"projectId": "bmbemn", "projectId": "bmbemn",
"env": { "env": {

View File

@ -0,0 +1,64 @@
export const gradient = (node, config = {}) => {
const defaultConfig = {
points: 10,
saturation: 0.8,
lightness: 0.75,
softness: 0.8,
}
// Applies a gradient background
const createGradient = config => {
config = {
...defaultConfig,
...config,
}
const { saturation, lightness, softness, points } = config
// Generates a random number between min and max
const rand = (min, max) => {
return Math.round(min + Math.random() * (max - min))
}
// Generates a random HSL colour using the options specified
const randomHSL = () => {
const lowerSaturation = Math.min(100, saturation * 100)
const upperSaturation = Math.min(100, (saturation + 0.2) * 100)
const lowerLightness = Math.min(100, lightness * 100)
const upperLightness = Math.min(100, (lightness + 0.2) * 100)
const hue = rand(0, 360)
const sat = `${rand(lowerSaturation, upperSaturation)}%`
const light = `${rand(lowerLightness, upperLightness)}%`
return `hsl(${hue},${sat},${light})`
}
// Generates a radial gradient stop point
const randomGradientPoint = () => {
const lowerTransparency = Math.min(100, softness * 100)
const upperTransparency = Math.min(100, (softness + 0.2) * 100)
const transparency = rand(lowerTransparency, upperTransparency)
return (
`radial-gradient(` +
`at ${rand(10, 90)}% ${rand(10, 90)}%,` +
`${randomHSL()} 0,` +
`transparent ${transparency}%)`
)
}
let css = `opacity:0.9;background-color:${randomHSL()};background-image:`
for (let i = 0; i < points - 1; i++) {
css += `${randomGradientPoint()},`
}
css += `${randomGradientPoint()};`
node.style = css
}
// Apply the initial gradient
createGradient(config)
return {
// Apply a new gradient
update: config => {
createGradient(config)
},
}
}

View File

@ -7,6 +7,10 @@
import WebhookDisplay from "../Shared/WebhookDisplay.svelte" import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import CodeEditorModal from "./CodeEditorModal.svelte"
import QuerySelector from "./QuerySelector.svelte"
import QueryParamSelector from "./QueryParamSelector.svelte"
import Editor from "components/integration/QueryEditor.svelte"
export let block export let block
export let webhookModal export let webhookModal
@ -70,6 +74,10 @@
on:change={e => (block.inputs[key] = e.detail)} on:change={e => (block.inputs[key] = e.detail)}
{bindings} {bindings}
/> />
{:else if value.customType === 'query'}
<QuerySelector bind:value={block.inputs[key]} />
{:else if value.customType === 'queryParams'}
<QueryParamSelector bind:value={block.inputs[key]} {bindings} />
{:else if value.customType === "table"} {:else if value.customType === "table"}
<TableSelector bind:value={block.inputs[key]} /> <TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === "row"} {:else if value.customType === "row"}
@ -78,6 +86,17 @@
<WebhookDisplay value={block.inputs[key]} /> <WebhookDisplay value={block.inputs[key]} />
{:else if value.customType === "triggerSchema"} {:else if value.customType === "triggerSchema"}
<SchemaSetup bind:value={block.inputs[key]} /> <SchemaSetup bind:value={block.inputs[key]} />
{:else if value.customType === "code"}
<CodeEditorModal>
<pre>{JSON.stringify(bindings, null, 2)}</pre>
<Editor
mode="javascript"
on:change={e => {
block.inputs[key] = e.detail.value
}}
value={block.inputs[key]}
/>
</CodeEditorModal>
{:else if value.type === "string" || value.type === "number"} {:else if value.type === "string" || value.type === "number"}
<DrawerBindableInput <DrawerBindableInput
panel={AutomationBindingPanel} panel={AutomationBindingPanel}

View File

@ -11,12 +11,12 @@
} }
</script> </script>
<Modal bind:this={modal} width="60%"> <Modal bind:this={modal}>
<ModalContent <ModalContent
size="XL"
title="Edit Code" title="Edit Code"
showConfirmButton={false} showConfirmButton={false}
showCancelButton={false} showCancelButton={false}>
>
<div class="container"> <div class="container">
<slot /> <slot />
</div> </div>

View File

@ -1,8 +1,8 @@
<script> <script>
import { queries } from "stores/backend" import { queries } from "stores/backend"
import { Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import DrawerBindableInput from "../../common/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "./AutomationBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
export let value export let value
export let bindings export let bindings
@ -13,7 +13,6 @@
// Ensure any nullish queryId values get set to empty string so // Ensure any nullish queryId values get set to empty string so
// that the select works // that the select works
$: if (value?.queryId == null) value = { queryId: "" } $: if (value?.queryId == null) value = { queryId: "" }
$: console.log("daValuz", value)
</script> </script>
<div class="block-field"> <div class="block-field">

View File

@ -1,7 +1,7 @@
<script> <script>
import { Input, Select, DatePicker, Toggle, TextArea } from "@budibase/bbui" import { Input, Select, DatePicker, Toggle, TextArea } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte" import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers" import { capitalise } from "helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let defaultValue export let defaultValue

View File

@ -59,7 +59,7 @@
const selectRelationship = ({ tableId, rowId, fieldName }) => { const selectRelationship = ({ tableId, rowId, fieldName }) => {
$goto( $goto(
`/builder/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}` `/app/builder/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
) )
} }

View File

@ -8,7 +8,7 @@
Body, Body,
ModalContent, ModalContent,
} from "@budibase/bbui" } from "@budibase/bbui"
import { capitalise } from "../../../../helpers" import { capitalise } from "helpers"
export let resourceId export let resourceId
export let permissions export let permissions

View File

@ -1,7 +1,7 @@
<script> <script>
import { Label, Input, Layout } from "@budibase/bbui" import { Label, Input, Layout } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { capitalise } from "../../../../helpers" import { capitalise } from "helpers"
export let integration export let integration
export let schema export let schema

View File

@ -14,7 +14,7 @@
getBindableProperties, getBindableProperties,
readableToRuntimeBinding, readableToRuntimeBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset, store } from "../../../builderStore" import { currentAsset, store } from "builderStore"
import { handlebarsCompletions } from "constants/completions" import { handlebarsCompletions } from "constants/completions"
import { addToText } from "./utils" import { addToText } from "./utils"

View File

@ -10,7 +10,7 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { isValid } from "@budibase/string-templates" import { isValid } from "@budibase/string-templates"
import { handlebarsCompletions } from "constants/completions" import { handlebarsCompletions } from "constants/completions"
import { readableToRuntimeBinding } from "../../../builderStore/dataBinding" import { readableToRuntimeBinding } from "builderStore/dataBinding"
import { addToText } from "./utils" import { addToText } from "./utils"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()

View File

@ -3,7 +3,7 @@
import { store, currentAsset, selectedComponent } from "builderStore" import { store, currentAsset, selectedComponent } from "builderStore"
import iframeTemplate from "./iframeTemplate" import iframeTemplate from "./iframeTemplate"
import { Screen } from "builderStore/store/screenTemplates/utils/Screen" import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
import { FrontendTypes } from "../../../constants" import { FrontendTypes } from "constants"
let iframe let iframe
let layout let layout
@ -82,7 +82,8 @@
style="height: 100%; width: 100%" style="height: 100%; width: 100%"
title="componentPreview" title="componentPreview"
bind:this={iframe} bind:this={iframe}
srcdoc={template} /> srcdoc={template}
/>
</div> </div>
<style> <style>

View File

@ -7,7 +7,7 @@
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte" import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { capitalise } from "../../../../helpers" import { capitalise } from "helpers"
export let label = "" export let label = ""
export let bindable = true export let bindable = true

View File

@ -1,7 +1,7 @@
<script> <script>
import { goto } from "@roxi/routify"
import { import {
notifications, notifications,
Button,
Link, Link,
Input, Input,
Modal, Modal,
@ -18,27 +18,18 @@
username, username,
password, password,
}) })
notifications.success("Logged in successfully.") notifications.success("Logged in successfully")
$goto("../portal")
} catch (err) { } catch (err) {
console.error(err) console.error(err)
notifications.error("Invalid credentials") notifications.error("Invalid credentials")
} }
} }
async function createTestUser() {
try {
await auth.firstUser()
notifications.success("Test user created")
} catch (err) {
console.error(err)
notifications.error("Could not create test user")
}
}
</script> </script>
<Modal fixed> <Modal fixed>
<ModalContent <ModalContent
size="L" size="M"
title="Log In" title="Log In"
onConfirm={login} onConfirm={login}
confirmText="Log In" confirmText="Log In"
@ -51,7 +42,6 @@
<Link target="_blank" href="/api/admin/auth/google"> <Link target="_blank" href="/api/admin/auth/google">
Sign In With Google Sign In With Google
</Link> </Link>
<Button secondary on:click={createTestUser}>Create Test User</Button>
</div> </div>
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -11,7 +11,7 @@
const id = $params.application const id = $params.application
await del(`/api/applications/${id}`) await del(`/api/applications/${id}`)
loading = false loading = false
$goto("/builder/") $goto("/app")
} }
</script> </script>

View File

@ -1,11 +1,19 @@
<script> <script>
import { goto } from "@roxi/routify" import {
import { ActionButton, Heading } from "@budibase/bbui" Heading,
import { notifications } from "@budibase/bbui" Icon,
import Spinner from "components/common/Spinner.svelte" Body,
Layout,
ActionMenu,
MenuItem,
Link,
notifications,
} from "@budibase/bbui"
import download from "downloadjs" import download from "downloadjs"
import { gradient } from "actions"
export let name, _id export let name
export let _id
let appExportLoading = false let appExportLoading = false
@ -15,58 +23,60 @@
download( download(
`/api/backups/export?appId=${_id}&appname=${encodeURIComponent(name)}` `/api/backups/export?appId=${_id}&appname=${encodeURIComponent(name)}`
) )
notifications.success("App Export Complete.") notifications.success("App export complete")
} catch (err) { } catch (err) {
console.error(err) console.error(err)
notifications.error("App Export Failed.") notifications.error("App export failed")
} finally { } finally {
appExportLoading = false appExportLoading = false
} }
} }
</script> </script>
<div class="apps-card"> <Layout noPadding gap="XS">
<Heading size="S">{name}</Heading> <div class="preview" use:gradient />
<div class="card-footer" data-cy={`app-${name}`}> <div class="title">
<ActionButton on:click={() => $goto(`/builder/${_id}`)}> <Link href={`/app/builder/${_id}`}>
Open <Heading size="XS">
{name} {name}
</Heading>
</ActionButton> </Link>
{#if appExportLoading} <ActionMenu>
<Spinner size="10" /> <Icon slot="control" name="More" hoverable />
{:else} <MenuItem on:click={exportApp} icon="Download">Export</MenuItem>
<ActionButton icon="Download" quiet /> </ActionMenu>
</div>
<div class="status">
<Body noPadding size="S">
Edited {Math.floor(1 + Math.random() * 10)} months ago
</Body>
{#if Math.random() > 0.5}
<Icon name="LockClosed" />
{/if} {/if}
</div> </div>
</div> </Layout>
<style> <style>
.apps-card { .preview {
background-color: var(--background); height: 135px;
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-xl) border-radius: var(--border-radius-s);
var(--spacing-xl); margin-bottom: var(--spacing-s);
max-width: 300px;
max-height: 150px;
border-radius: var(--border-radius-m);
border: var(--border-dark);
} }
.card-footer { .title,
.status {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center;
justify-content: space-between; justify-content: space-between;
margin-top: var(--spacing-m); align-items: center;
} }
i { .title :global(a) {
font-size: var(--font-size-l); text-decoration: none;
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer; cursor: pointer;
transition: 0.2s all; transition: color 130ms ease;
}
i:hover {
color: var(--blue);
} }
</style> </style>

View File

@ -1,50 +1,25 @@
<script> <script>
import { onMount } from "svelte"
import AppCard from "./AppCard.svelte" import AppCard from "./AppCard.svelte"
import { Heading, Divider } from "@budibase/bbui" import { apps } from "stores/portal"
import Spinner from "components/common/Spinner.svelte"
import { get } from "builderStore/api"
let promise = getApps() onMount(apps.load)
async function getApps() {
const res = await get("/api/applications")
const json = await res.json()
if (res.ok) {
return json
} else {
throw new Error(json)
}
}
</script> </script>
<div class="root"> {#if $apps.length}
<Heading size="M">Your Apps</Heading> <div class="appList">
<Divider size="M" /> {#each $apps as app}
{#await promise} <AppCard {...app} />
<div class="spinner-container"> {/each}
<Spinner size="30" /> </div>
</div> {:else}
{:then apps} <div>No apps</div>
<div class="apps"> {/if}
{#each apps as app}
<AppCard {...app} />
{/each}
</div>
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
</div>
<style> <style>
.root { .appList {
margin-top: 10px;
}
.apps {
margin-top: var(--layout-m);
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-gap: 50px;
grid-gap: var(--layout-s); grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
justify-content: start;
} }
</style> </style>

View File

@ -1,15 +0,0 @@
<script>
import { Button, Modal } from "@budibase/bbui"
import BuilderSettingsModal from "./BuilderSettingsModal.svelte"
let modal
</script>
<div>
<Button primary quiet icon="Settings" text on:click={modal.show}>
Settings
</Button>
</div>
<Modal bind:this={modal} width="30%">
<BuilderSettingsModal />
</Modal>

View File

@ -1,6 +0,0 @@
<script>
import { Button } from "@budibase/bbui"
import { auth } from "stores/backend"
</script>
<Button primary quiet text icon="LogOut" on:click={auth.logout}>Log Out</Button>

View File

@ -1,29 +0,0 @@
<script>
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import {
SideNavigation as Navigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
import { admin } from "stores/portal"
import LoginForm from "components/login/LoginForm.svelte"
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
import LogoutButton from "components/start/LogoutButton.svelte"
import Logo from "/assets/budibase-logo.svg"
import api from "builderStore/api"
let checklist
onMount(async () => {
await admin.init()
if (!$admin?.checklist?.adminUser) {
$goto("./admin")
} else {
$goto("./portal")
}
})
</script>
{#if $admin.checklist}
<slot />
{/if}

View File

@ -0,0 +1,27 @@
<script>
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import { auth } from "stores/backend"
import { admin } from "stores/portal"
let checked = false
onMount(async () => {
await admin.init()
await auth.checkAuth()
if (!$admin?.checklist?.adminUser) {
$goto("./admin")
}
checked = true
})
$: {
if (checked && !$auth.user) {
$goto("./auth/login")
}
}
</script>
{#if checked}
<slot />
{/if}

View File

@ -31,21 +31,24 @@
<section> <section>
<div class="container"> <div class="container">
<header> <Layout gap="XS">
<Heading size="M">Create an admin user</Heading> <img src="https://i.imgur.com/ZKyklgF.png" />
<Body size="S">The admin user has access to everything in budibase.</Body> </Layout>
</header> <div class="center">
<div class="config-form"> <Layout gap="XS">
<Layout gap="S"> <Heading size="M">Create an admin user</Heading>
<Input label="email" bind:value={adminUser.email} /> <Body size="M"
<Input >The admin user has access to everything in Budibase.</Body
label="password" >
type="password"
bind:value={adminUser.password}
/>
<Button cta on:click={save}>Create super admin user</Button>
</Layout> </Layout>
</div> </div>
<Layout gap="XS">
<Input label="Email" bind:value={adminUser.email} />
<Input label="Password" type="password" bind:value={adminUser.password} />
</Layout>
<Layout gap="S">
<Button cta on:click={save}>Create super admin user</Button>
</Layout>
</div> </div>
</section> </section>
@ -56,14 +59,19 @@
justify-content: center; justify-content: center;
height: 100%; height: 100%;
} }
.container {
header { margin: 0 auto;
width: 260px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.center {
text-align: center; text-align: center;
width: 80%; }
img {
width: 40px;
margin: 0 auto; margin: 0 auto;
} }
.config-form {
margin-bottom: 42px;
}
</style> </style>

View File

@ -0,0 +1,4 @@
<script>
import { goto } from "@roxi/routify"
$goto("./login")
</script>

View File

@ -0,0 +1,5 @@
<script>
import LoginForm from "components/login/LoginForm.svelte"
</script>
<LoginForm />

View File

@ -6,9 +6,9 @@
import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte" import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte" import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte"
import { get } from "builderStore/api" import { get } from "builderStore/api"
import { isActive, goto, layout, params } from "@roxi/routify" import { isActive, goto, layout } from "@roxi/routify"
import Logo from "/assets/bb-logo.svg" import Logo from "/assets/bb-logo.svg"
import { capitalise } from "../../../helpers" import { capitalise } from "helpers"
// Get Package and set store // Get Package and set store
export let application export let application
@ -60,7 +60,7 @@
<img <img
src={Logo} src={Logo}
alt="budibase icon" alt="budibase icon"
on:click={() => $goto(`/builder/`)} on:click={() => $goto(`../../portal/`)}
/> />
</button> </button>

View File

@ -5,7 +5,7 @@
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte" import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
import { capitalise } from "../../../../../../helpers" import { capitalise } from "helpers"
let unsaved = false let unsaved = false

View File

@ -0,0 +1,4 @@
<script>
import { goto } from "@roxi/routify"
$goto("../portal")
</script>

View File

@ -0,0 +1,4 @@
<script>
import { goto } from "@roxi/routify"
$goto("./portal")
</script>

View File

@ -0,0 +1,159 @@
<script>
import { isActive, goto } from "@roxi/routify"
import { onMount } from "svelte"
import {
Icon,
Avatar,
Search,
Layout,
SideNavigation as Navigation,
SideNavigationItem as Item,
ActionMenu,
MenuItem,
Modal,
} from "@budibase/bbui"
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
import { organisation, apps } from "stores/portal"
import { auth } from "stores/backend"
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
organisation.init()
apps.load()
let orgName
let orgLogo
let user
let oldSettingsModal
async function getInfo() {
// fetch orgInfo
orgName = "ACME Inc."
orgLogo = "https://via.placeholder.com/150"
user = { name: "John Doe" }
}
onMount(getInfo)
let menu = [
{ title: "Apps", href: "/app/portal/apps" },
{ title: "Drafts", href: "/app/portal/drafts" },
{ title: "Users", href: "/app/portal/users", heading: "Manage" },
{ title: "Groups", href: "/app/portal/groups" },
{ title: "Auth", href: "/app/portal/oauth" },
{ title: "Email", href: "/app/portal/email" },
{
title: "General",
href: "/app/portal/settings/general",
heading: "Settings",
},
{ title: "Theming", href: "/app/portal/theming" },
{ title: "Account", href: "/app/portal/account" },
]
</script>
<div class="container">
<div class="nav">
<Layout paddingX="L" paddingY="L">
<div class="branding">
<div class="name" on:click={() => $goto("./apps")}>
<img
src={$organisation?.logoUrl || "https://i.imgur.com/ZKyklgF.png"}
alt="Logotype"
/>
<span>{$organisation?.company || "Budibase"}</span>
</div>
<div class="onboarding">
<ConfigChecklist />
</div>
</div>
<div class="menu">
<Navigation>
{#each menu as { title, href, heading }}
<Item selected={$isActive(href)} {href} {heading}>{title}</Item>
{/each}
</Navigation>
</div>
</Layout>
</div>
<div class="main">
<div class="toolbar">
<Search placeholder="Global search" />
<ActionMenu align="right">
<div slot="control" class="avatar">
<Avatar size="M" name="John Doe" />
<Icon size="XL" name="ChevronDown" />
</div>
<MenuItem icon="Settings" on:click={oldSettingsModal.show}>
Old settings
</MenuItem>
<MenuItem icon="LogOut" on:click={auth.logout}>Log out</MenuItem>
</ActionMenu>
</div>
<div>
<slot />
</div>
</div>
</div>
<Modal bind:this={oldSettingsModal} width="30%">
<BuilderSettingsModal />
</Modal>
<style>
.container {
min-height: 100vh;
display: grid;
grid-template-columns: 250px 1fr;
}
.nav {
background: var(--background);
border-right: var(--border-light);
}
.main {
display: grid;
grid-template-rows: auto 1fr;
}
.branding {
display: grid;
grid-gap: var(--spacing-s);
grid-template-columns: auto auto;
justify-content: space-between;
align-items: center;
}
.name {
display: grid;
grid-template-columns: auto auto;
grid-gap: var(--spacing-m);
align-items: center;
}
.name:hover {
cursor: pointer;
}
.avatar {
display: grid;
grid-template-columns: auto auto;
place-items: center;
grid-gap: var(--spacing-xs);
}
.avatar:hover {
cursor: pointer;
filter: brightness(110%);
}
.toolbar {
background: var(--background);
border-bottom: var(--border-light);
display: grid;
grid-template-columns: 250px auto;
justify-content: space-between;
padding: var(--spacing-m) calc(var(--spacing-xl) * 2);
align-items: center;
}
img {
width: 28px;
height: 28px;
}
span {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
</style>

View File

@ -0,0 +1,92 @@
<script>
import {
Heading,
Layout,
Button,
ActionButton,
ActionGroup,
ButtonGroup,
Select,
Modal,
} from "@budibase/bbui"
import AppList from "components/start/AppList.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import api from "builderStore/api"
import analytics from "analytics"
import { onMount } from "svelte"
let layout = "grid"
let modal
let template
async function checkKeys() {
const response = await api.get(`/api/keys/`)
const keys = await response.json()
if (keys.userId) {
analytics.identify(keys.userId)
}
}
function initiateAppImport() {
template = { fromFile: true }
modal.show()
}
onMount(checkKeys)
</script>
<Layout noPadding>
<div class="title">
<Heading>Apps</Heading>
<ButtonGroup>
<Button secondary on:click={initiateAppImport}>Import app</Button>
<Button cta on:click={modal.show}>Create new app</Button>
</ButtonGroup>
</div>
<div class="filter">
<div class="select">
<Select quiet placeholder="Filter by groups" />
</div>
<ActionGroup>
<ActionButton
on:click={() => (layout = "grid")}
selected={layout === "grid"}
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div>
{#if layout === "grid"}
<AppList />
{:else}
Table
{/if}
</Layout>
<Modal
bind:this={modal}
padding={false}
width="600px"
on:hide={() => (template = null)}
>
<CreateAppModal {template} />
</Modal>
<style>
.title,
.filter {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.select {
width: 110px;
}
</style>

View File

@ -60,47 +60,50 @@
</script> </script>
<Page> <Page>
<header> <Layout noPadding>
<Heading size="M">OAuth</Heading> <div>
<Body size="S"> <Heading size="M">OAuth</Heading>
Every budibase app comes with basic authentication (email/password) <Body>
included. You can add additional authentication methods from the options Every budibase app comes with basic authentication (email/password)
below. included. You can add additional authentication methods from the options
</Body> below.
</header> </Body>
<Divider /> </div>
{#if google} <Divider />
<div class="config-form"> {#if google}
<Layout gap="S"> <div>
<Heading size="S"> <Heading size="S">
<span> <span>
<GoogleLogo /> <GoogleLogo />
Google Google
</span> </span>
</Heading> </Heading>
{#each ConfigFields.Google as field} <Body>
<div class="form-row"> To allow users to authenticate using their Google accounts, fill out
<Label>{field}</Label> the fields below.
<Input bind:value={google.config[field]} /> </Body>
</div> </div>
{/each}
</Layout> {#each ConfigFields.Google as field}
<Button primary on:click={() => save(google)}>Save</Button> <div class="form-row">
</div> <Label size="L">{field}</Label>
<Divider /> <Input bind:value={google.config[field]} />
{/if} </div>
{/each}
<div>
<Button primary on:click={() => save(google)}>Save</Button>
</div>
<Divider />
{/if}
</Layout>
</Page> </Page>
<style> <style>
.config-form {
margin-top: 42px;
margin-bottom: 42px;
}
.form-row { .form-row {
display: grid; display: grid;
grid-template-columns: 20% 1fr; grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l); grid-gap: var(--spacing-l);
align-items: center;
} }
span { span {
@ -108,8 +111,4 @@
align-items: center; align-items: center;
gap: var(--spacing-s); gap: var(--spacing-s);
} }
header {
margin-bottom: 42px;
}
</style> </style>

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -45,12 +45,12 @@
<Layout noPadding> <Layout noPadding>
<div class="intro"> <div class="intro">
<Heading size="M">General</Heading> <Heading size="M">General</Heading>
<Body <Body>
>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Hic vero, aut Lorem ipsum, dolor sit amet consectetur adipisicing elit. Hic vero, aut
culpa provident sunt ratione! Voluptas doloremque, dicta nisi velit culpa provident sunt ratione! Voluptas doloremque, dicta nisi velit
perspiciatis, ratione vel blanditiis totam, nam voluptate repellat perspiciatis, ratione vel blanditiis totam, nam voluptate repellat
aperiam fuga!</Body aperiam fuga!
> </Body>
</div> </div>
<Divider size="S" /> <Divider size="S" />
<div class="information"> <div class="information">
@ -58,7 +58,7 @@
<Body>Here you can update your logo and organization name.</Body> <Body>Here you can update your logo and organization name.</Body>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label>Organization name</Label> <Label size="L">Organization name</Label>
<Input thin bind:value={company} /> <Input thin bind:value={company} />
</div> </div>
<!-- <div class="field"> <!-- <div class="field">
@ -72,13 +72,13 @@
<Divider size="S" /> <Divider size="S" />
<div class="analytics"> <div class="analytics">
<Heading size="S">Analytics</Heading> <Heading size="S">Analytics</Heading>
<Body <Body>
>If you would like to send analytics that help us make Budibase better, If you would like to send analytics that help us make Budibase better,
please let us know below.</Body please let us know below.
> </Body>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label>Send Analytics to Budibase</Label> <Label size="L">Send Analytics to Budibase</Label>
<Toggle text="" value={!analyticsDisabled} /> <Toggle text="" value={!analyticsDisabled} />
</div> </div>
</div> </div>
@ -97,7 +97,8 @@
} }
.field { .field {
display: grid; display: grid;
grid-template-columns: 30% 1fr; grid-template-columns: 32% 1fr;
align-items: center;
} }
.file { .file {
max-width: 30ch; max-width: 30ch;

View File

@ -1,108 +0,0 @@
<script>
import {
SideNavigation as Navigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
import { auth } from "stores/backend"
import LoginForm from "components/login/LoginForm.svelte"
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
import LogoutButton from "components/start/LogoutButton.svelte"
import Logo from "/assets/budibase-logo.svg"
let modal
</script>
<div class="root">
<div class="ui-nav">
<div class="home-logo">
<img src={Logo} alt="Budibase icon" />
</div>
<div class="nav-section">
<div class="nav-top">
<Navigation>
<Item href="/builder/" icon="Apps" selected>Apps</Item>
<Item external href="https://portal.budi.live/" icon="Servers">
Hosting
</Item>
<Item external href="https://docs.budibase.com/" icon="Book">
Documentation
</Item>
<Item
external
href="https://github.com/Budibase/budibase/discussions"
icon="PeopleGroup"
>
Community
</Item>
<Item
external
href="https://github.com/Budibase/budibase/issues/new/choose"
icon="Bug"
>
Raise an issue
</Item>
</Navigation>
</div>
<div class="nav-bottom">
<BuilderSettingsButton />
<LogoutButton />
</div>
</div>
</div>
<div class="main">
<slot />
</div>
</div>
<style>
.root {
display: grid;
grid-template-columns: 260px 1fr;
height: 100%;
width: 100%;
}
.login {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.main {
grid-column: 2;
overflow: auto;
}
.ui-nav {
grid-column: 1;
background-color: var(--background);
padding: 20px;
display: flex;
flex-direction: column;
border-right: var(--border-light);
}
.home-logo {
cursor: pointer;
height: 40px;
margin-bottom: 20px;
}
.home-logo img {
height: 40px;
}
.nav-section {
margin: 20px 0 0 0;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.nav-bottom :global(> *) {
margin-top: 5px;
}
</style>

View File

@ -1,123 +0,0 @@
<script>
import api from "builderStore/api"
import AppList from "components/start/AppList.svelte"
import { get } from "builderStore/api"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import { Button, Heading, Modal, ButtonGroup } from "@budibase/bbui"
import TemplateList from "components/start/TemplateList.svelte"
import analytics from "analytics"
import Banner from "/assets/orange-landscape.png"
let hasKey
let template
let modal
async function getApps() {
const res = await get("/api/applications")
const json = await res.json()
if (res.ok) {
return json
} else {
throw new Error(json)
}
}
async function fetchKeys() {
const response = await api.get(`/api/keys/`)
return await response.json()
}
async function checkIfKeysAndApps() {
const keys = await fetchKeys()
const apps = await getApps()
if (keys.userId) {
hasKey = true
analytics.identify(keys.userId)
}
}
function selectTemplate(newTemplate) {
template = newTemplate
modal.show()
}
function initiateAppImport() {
template = { fromFile: true }
modal.show()
}
function closeModal() {
template = null
modal.hide()
}
checkIfKeysAndApps()
</script>
<div class="container">
<div class="header">
<Heading size="M">Welcome to the Budibase Beta</Heading>
<ButtonGroup>
<Button secondary on:click={initiateAppImport}>Import Web App</Button>
<Button cta on:click={modal.show}>Create New Web App</Button>
</ButtonGroup>
</div>
<div class="banner">
<img src={Banner} alt="rocket" />
<div class="banner-content">
Every accomplishment starts with a decision to try.
</div>
</div>
<!-- <TemplateList onSelect={selectTemplate} /> -->
<AppList />
</div>
<Modal bind:this={modal} padding={false} width="600px" on:hide={closeModal}>
<CreateAppModal {hasKey} {template} />
</Modal>
<style>
.container {
display: grid;
gap: var(--spacing-xl);
margin: 40px 80px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.banner {
display: flex;
align-items: center;
justify-content: center;
position: relative;
text-align: center;
color: white;
border-radius: 16px;
}
.banner img {
height: 250px;
width: 100%;
border-radius: 5px;
}
.banner-content {
position: absolute;
font-size: 24px;
color: white;
font-weight: 500;
}
.button-group {
display: flex;
flex-direction: row;
}
</style>

View File

@ -1 +1,4 @@
Index route <script>
import { goto } from "@roxi/routify"
$goto("./app")
</script>

View File

@ -1,153 +0,0 @@
<script>
import { isActive, url, goto } from "@roxi/routify"
import { onMount } from "svelte"
import {
ActionMenu,
Checkbox,
MenuItem,
Icon,
Heading,
Avatar,
Search,
Layout,
ProgressCircle,
SideNavigation as Navigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
import LoginForm from "components/login/LoginForm.svelte"
import api from "builderStore/api"
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
import { auth } from "stores/backend"
import { organisation, admin } from "stores/portal"
organisation.init()
let orgName
let orgLogo
let user
async function getInfo() {
// fetch orgInfo
orgName = "ACME Inc."
orgLogo = "https://via.placeholder.com/150"
user = { name: "John Doe" }
}
onMount(getInfo)
let menu = [
{ title: "Apps", href: "/portal/apps" },
{ title: "Drafts", href: "/portal/drafts" },
{ title: "Users", href: "/portal/users", heading: "Manage" },
{ title: "Groups", href: "/portal/groups" },
{ title: "Auth", href: "/portal/oauth" },
{ title: "Email", href: "/portal/email" },
{ title: "General", href: "/portal/settings/general", heading: "Settings" },
{ title: "Theming", href: "/portal/theming" },
{ title: "Account", href: "/portal/account" },
]
</script>
{#if $auth}
{#if $auth.user}
<div class="container">
<div class="nav">
<Layout paddingX="L" paddingY="L">
<div class="branding">
<div class="name">
<img
src={$organisation?.logoUrl ||
"https://i.imgur.com/ZKyklgF.png"}
alt="Logotype"
/>
<span>{$organisation?.company || "Budibase"}</span>
</div>
<div class="onboarding">
<ConfigChecklist />
</div>
</div>
<div class="menu">
<Navigation>
{#each menu as { title, href, heading }}
<Item selected={$isActive(href)} {href} {heading}>{title}</Item>
{/each}
</Navigation>
</div>
</Layout>
</div>
<div class="main">
<div class="toolbar">
<Search placeholder="Global search" />
<div class="avatar">
<Avatar size="M" name="John Doe" />
<Icon size="XL" name="ChevronDown" />
</div>
</div>
<div class="content">
<slot />
</div>
</div>
</div>
{:else}
<section class="login">
<LoginForm />
</section>
{/if}
{/if}
<style>
.container {
min-height: 100vh;
display: grid;
grid-template-columns: 250px 1fr;
}
.nav {
background: var(--background);
border-right: var(--border-light);
}
.main {
display: grid;
grid-template-rows: auto 1fr;
}
.branding {
display: grid;
grid-gap: var(--spacing-s);
grid-template-columns: auto auto;
justify-content: space-between;
align-items: center;
}
.name {
display: grid;
grid-template-columns: auto auto;
grid-gap: var(--spacing-m);
align-items: center;
}
.avatar {
display: grid;
grid-template-columns: auto auto;
place-items: center;
grid-gap: var(--spacing-xs);
}
.avatar:hover {
cursor: pointer;
filter: brightness(110%);
}
.toolbar {
background: var(--background);
border-bottom: var(--border-light);
display: grid;
grid-template-columns: 250px auto;
justify-content: space-between;
padding: var(--spacing-m) calc(var(--spacing-xl) * 2);
}
img {
width: 28px;
height: 28px;
}
span {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
</style>

View File

@ -1,27 +0,0 @@
<script>
import { Heading, Layout } from "@budibase/bbui"
</script>
<Layout noPadding>
<div>
<Heading>Apps</Heading>
</div>
<div class="appList">
{#each new Array(10) as _}
<div class="app" />
{/each}
</div>
</Layout>
<style>
.appList {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, 300px);
}
.app {
height: 130px;
border-radius: 4px;
background-color: var(--spectrum-global-color-gray-200);
}
</style>

View File

@ -1,28 +1,25 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import api from "../../builderStore/api" import api from "../../builderStore/api"
async function checkAuth() {
const response = await api.get("/api/self")
const user = await response.json()
if (response.status === 200) return user
return null
}
export function createAuthStore() { export function createAuthStore() {
const { subscribe, set } = writable(null) const store = writable({ user: null })
checkAuth()
.then(user => set({ user }))
.catch(() => set({ user: null }))
return { return {
subscribe, subscribe: store.subscribe,
checkAuth: async () => {
const response = await api.get("/api/self")
const user = await response.json()
if (response.status === 200) {
store.update(state => ({ ...state, user }))
} else {
store.update(state => ({ ...state, user: null }))
}
},
login: async creds => { login: async creds => {
const response = await api.post(`/api/admin/auth`, creds) const response = await api.post(`/api/admin/auth`, creds)
const json = await response.json() const json = await response.json()
if (response.status === 200) { if (response.status === 200) {
set({ user: json.user }) store.update(state => ({ ...state, user: json.user }))
} else { } else {
throw "Invalid credentials" throw "Invalid credentials"
} }
@ -34,7 +31,7 @@ export function createAuthStore() {
throw "Unable to create logout" throw "Unable to create logout"
} }
await response.json() await response.json()
set({ user: null }) store.update(state => ({ ...state, user: null }))
}, },
createUser: async user => { createUser: async user => {
const response = await api.post(`/api/admin/users`, user) const response = await api.post(`/api/admin/users`, user)
@ -43,13 +40,6 @@ export function createAuthStore() {
} }
await response.json() await response.json()
}, },
firstUser: async () => {
const response = await api.post(`/api/admin/users/first`)
if (response.status !== 200) {
throw "Unable to create test user"
}
await response.json()
},
} }
} }

View File

@ -0,0 +1,27 @@
import { writable } from "svelte/store"
import { get } from "builderStore/api"
export function createAppStore() {
const store = writable([])
async function load() {
try {
const res = await get("/api/applications")
const json = await res.json()
if (res.ok && Array.isArray(json)) {
store.set(json)
} else {
store.set([])
}
} catch (error) {
store.set([])
}
}
return {
subscribe: store.subscribe,
load,
}
}
export const apps = createAppStore()

View File

@ -1,2 +1,3 @@
export { organisation } from "./organisation" export { organisation } from "./organisation"
export { admin } from "./admin" export { admin } from "./admin"
export { apps } from "./apps"

View File

@ -6,7 +6,7 @@ import path from "path"
export default ({ mode }) => { export default ({ mode }) => {
const isProduction = mode === "production" const isProduction = mode === "production"
return { return {
base: "/", base: "/app/",
build: { build: {
minify: isProduction, minify: isProduction,
outDir: "../server/builder", outDir: "../server/builder",
@ -52,6 +52,14 @@ export default ({ mode }) => {
find: "analytics", find: "analytics",
replacement: path.resolve("./src/analytics"), replacement: path.resolve("./src/analytics"),
}, },
{
find: "actions",
replacement: path.resolve("./src/actions"),
},
{
find: "helpers",
replacement: path.resolve("./src/helpers"),
},
], ],
}, },
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable, linkable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export const className = "" export const className = ""
@ -41,6 +41,7 @@
<footer> <footer>
<p class="subtext">{subtext}</p> <p class="subtext">{subtext}</p>
<a <a
use:linkable
style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}" style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}"
href={linkUrl || "/"}>{linkText}</a href={linkUrl || "/"}>{linkText}</a
> >

View File

@ -1,7 +1,7 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const { styleable } = getContext("sdk") const { styleable, linkable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let imageUrl = "" export let imageUrl = ""
@ -13,7 +13,7 @@
</script> </script>
<div class="container" use:styleable={$component.styles}> <div class="container" use:styleable={$component.styles}>
<a href={destinationUrl}> <a use:linkable href={destinationUrl}>
<div class="stackedlist"> <div class="stackedlist">
{#if showImage} {#if showImage}
<div class="image-block"> <div class="image-block">

File diff suppressed because it is too large Load Diff