Merge branch 'master' into feature/binding-interface

This commit is contained in:
Michael Shanks 2020-08-07 10:43:07 +01:00
commit 25bb78156a
52 changed files with 834 additions and 528 deletions

View File

@ -32,4 +32,5 @@ jobs:
- run: yarn test
env:
CI: true
name: Budibase CI
name: Budibase CI
- run: yarn test:e2e:ci

View File

@ -1,5 +1,5 @@
{
"version": "0.1.12",
"version": "0.1.13",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -26,7 +26,8 @@
"lint": "eslint packages",
"lint:fix": "eslint --fix packages",
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"",
"test:e2e": "lerna run cy:test"
"test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci"
},
"dependencies": {
"@material/icon-button": "4.0.0",

View File

@ -1,4 +1,5 @@
{
"baseUrl": "http://localhost:4001/_builder/",
"video": false
}
"baseUrl": "http://localhost:4001/_builder/",
"video": true,
"projectId": "bmbemn"
}

View File

@ -0,0 +1,3 @@
{
"budibase": "CB373643-3FC4-4902-9E31-449C0ED066B6"
}

View File

@ -1,6 +1,7 @@
context('Create an Application', () => {
beforeEach(() => {
cy.server()
cy.visit('localhost:4001/_builder')
})

View File

@ -1,6 +1,7 @@
context('Create Components', () => {
xcontext('Create Components', () => {
before(() => {
cy.server()
cy.visit('localhost:4001/_builder')
// https://on.cypress.io/type
cy.createApp('Model App', 'Model App Description')
@ -22,8 +23,8 @@ context('Create Components', () => {
})
it('change the font size of the headline', () => {
cy.contains('Typography').click()
cy.get('input[name="font-size"]')
.type('60px')
cy.get('[data-cy=font-size-prop-control]').click()
cy.contains("60px").click()
cy.contains('Design').click()
getIframeBody().contains('An Amazing headline!').should('have.css', 'font-size', '60px')

View File

@ -1,4 +1,4 @@
context('Create a Model', () => {
xcontext('Create a Model', () => {
before(() => {
cy.visit('localhost:4001/_builder')

View File

@ -1,6 +1,7 @@
context('Create a User', () => {
before(() => {
cy.server()
cy.visit('localhost:4001/_builder')
// https://on.cypress.io/type
cy.createApp('User App', 'This app is used to test user creation')
@ -8,12 +9,9 @@ context('Create a User', () => {
// https://on.cypress.io/interacting-with-elements
it('should create a user', () => {
// Close Model modal that shows up after creating an app
cy.get('.close').click()
cy.createUser('bbuser', 'test', 'ADMIN')
// Check to make sure user was created!
cy.contains('bbuser').should('have.text', 'bbuser')
cy.get("input[disabled]").should('have.value', 'bbuser')
})
})

View File

@ -1,6 +1,7 @@
context('Create a workflow', () => {
xcontext('Create a workflow', () => {
before(() => {
cy.server()
cy.visit('localhost:4001/_builder')
cy.createApp('Workflow Test App', 'This app is used to test that workflows do in fact work!')
@ -9,11 +10,9 @@ context('Create a workflow', () => {
// https://on.cypress.io/interacting-with-elements
it('should create a workflow', () => {
cy.createModel('dog', 'name', 'age')
cy.createUser('bbuser', 'test', 'ADMIN')
cy.contains('workflow').click()
cy.get('.new-workflow-button').click()
cy.contains('Create New Workflow').click()
cy.get('input').type('Add Record')
cy.contains('Save').click()
@ -28,14 +27,13 @@ context('Create a workflow', () => {
cy.get(':nth-child(3) > .budibase__input').type('11')
// Save
cy.get('[data-cy=save-workflow-setup]').click()
cy.get('.workflow-button').click()
cy.contains('Save Workflow').click()
// Activate Workflow
cy.get('[data-cy=activate-workflow]').click()
})
it('should add record when a new record is added', () => {
xit('should add record when a new record is added', () => {
cy.contains('backend').click()
cy.addRecord('bob', '15')

View File

@ -1,13 +1,14 @@
context('Screen Tests', () => {
before(() => {
cy.server()
cy.visit('localhost:4001/_builder')
cy.createApp('Conor Cy App', 'Model App Description')
cy.navigateToFrontend()
})
it('Should successful create a screen', () => {
cy.createScreen("test Screen")
cy.createScreen("test Screen", "/test")
})
it('Should rename a screen', () => {

View File

@ -19,4 +19,5 @@
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
require("cypress-terminal-report/src/installLogsPrinter")(on)
}

View File

@ -1,17 +1,23 @@
// What this script does:
// 1. Removes the old test folder if it exists (.budibase-cypress)
// 2. Initialises using `.budibase-cypress`
// 1. Removes the old test folder if it exists (.budibase)
// 2. Initialises using `.budibase`
// 3. Runs the server using said folder
const rimraf = require("rimraf")
const { join } = require("path")
const homedir = join(require("os").homedir(), ".budibase-cypress")
const init = require("../../cli/src/commands/init/initHandler")
const run = require("../../cli/src/commands/run/runHandler")
const initialiseBudibase = require("../../server/src/utilities/initialiseBudibase")
const homedir = join(require("os").homedir(), ".budibase")
rimraf.sync(homedir)
init({ dir: homedir, clientId: "cypress-test" }).then(() => {
delete require.cache[require.resolve("../../server/src/environment")]
run({ dir: homedir })
})
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
process.env.NODE_ENV = "cypress"
initialiseBudibase({ dir: homedir, clientId: "cypress-test" })
.then(() => {
delete require.cache[require.resolve("../../server/src/environment")]
run({ dir: homedir })
})
.catch(e => console.error(e))

View File

@ -24,69 +24,81 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add("createApp", (name, description) => {
cy.get(".banner-button")
.click()
.get('input[name="name"]')
.type(name)
.should("have.value", name)
Cypress.Commands.add("createApp", name => {
cy.contains("Create New Web App").click()
cy.get('textarea[name="description"]')
.type(description)
.should("have.value", description)
cy.get("body")
.then($body => {
if ($body.find("input[name=apiKey]").length) {
// input was found, do something else here
cy.get("input[name=apiKey]")
.type(name)
.should("have.value", name)
cy.contains("Next").click()
}
})
.then(() => {
cy.get("input[name=applicationName]")
.type(name)
.should("have.value", name)
cy.contains("Save").click()
cy.contains("Next").click()
cy.get("input[name=username]")
.click()
.type("test")
cy.get("input[name=password]")
.click()
.type("test")
cy.contains("Submit").click()
cy.contains("Create New Table", {
timeout: 10000,
}).should("be.visible")
})
})
Cypress.Commands.add("createModel", (modelName, firstField, secondField) => {
Cypress.Commands.add("createModel", modelName => {
// Enter model name
cy.get("[data-cy=Name]")
.click()
.type(modelName)
cy.contains("Create New Table").click()
cy.get("[data-cy=table-name-input]").type(modelName)
// Add 'name' field
cy.get("[data-cy=add-new-model-field]").click()
cy.get("[data-cy=Name]")
.click()
.type(firstField)
cy.contains("Save").click()
cy.contains("Add").click()
cy.contains("Plain Text").click()
// Add 'age' field
cy.get("[data-cy=add-new-model-field]").click()
cy.contains("Add").click()
cy.contains("Number").click()
cy.get("[data-cy=Name]")
.click()
.type(secondField)
cy.get("select").select("number")
cy.contains("Save").click()
cy.contains(secondField).should("exist")
// Save model
cy.contains("Save").click()
cy.contains(modelName).click()
})
Cypress.Commands.add("addRecord", (firstField, secondField) => {
cy.contains("Create new record").click()
cy.contains("Create New Record").click()
cy.get("[data-cy=name-input]")
.click()
.type(firstField)
cy.get("[data-cy=age-input]")
.click()
.type(secondField)
cy.get("[data-cy='Plain Text-input']").type(firstField)
cy.get("[data-cy=Number-input]").type(secondField)
// Save
cy.contains("Save").click()
})
Cypress.Commands.add("createUser", (username, password, level) => {
Cypress.Commands.add("createUser", (username, password) => {
// Create User
cy.get(".nav-group-header > .ri-add-line").click()
cy.get(".toprightnav > .settings").click()
cy.contains("Users").click()
cy.get("[data-cy=username]").type(username)
cy.get("[data-cy=password]").type(password)
cy.get("[data-cy=accessLevel]").select(level)
cy.get("[name=Name]")
.first()
.type(username)
cy.get("[name=Password]")
.first()
.type(password)
// Save
cy.contains("Save").click()
cy.get(".create-button").click()
})
Cypress.Commands.add("addHeadlineComponent", text => {
@ -95,7 +107,8 @@ Cypress.Commands.add("addHeadlineComponent", text => {
cy.get("[data-cy=Text]").click()
cy.get("[data-cy=Headline]").click()
cy.get(".tabs > :nth-child(2)").click()
cy.get('input[type="text"]').type(text)
cy.contains("Settings").click()
cy.get('input[name="text"]').type(text)
cy.contains("Design").click()
})
Cypress.Commands.add("addButtonComponent", () => {
@ -105,9 +118,7 @@ Cypress.Commands.add("addButtonComponent", () => {
})
Cypress.Commands.add("navigateToFrontend", () => {
cy.get(".close", { timeout: 10000 }).click()
cy.contains("frontend").click()
cy.get(".close", { timeout: 10000 }).click()
})
Cypress.Commands.add("createScreen", (screenName, route) => {

View File

@ -19,3 +19,4 @@ import "./commands"
// Alternatively you can use CommonJS syntax:
// require('./commands')
require("cypress-terminal-report/src/installLogsCollector")()

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.1.11",
"version": "0.1.13",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -13,7 +13,9 @@
"cy:setup": "node ./cypress/setup.js",
"cy:run": "cypress run",
"cy:open": "cypress open",
"cy:test": "start-server-and-test cy:setup http://localhost:4001/_builder cy:run"
"cy:run:ci": "cypress run --browser electron --record --key f308590b-6070-41af-b970-794a3823d451",
"cy:test": "start-server-and-test cy:setup http://localhost:4001/_builder cy:run",
"cy:ci": "start-server-and-test cy:setup http://localhost:4001/_builder cy:run:ci"
},
"jest": {
"globals": {
@ -55,11 +57,12 @@
]
},
"dependencies": {
"@budibase/bbui": "^1.16.0",
"@budibase/bbui": "^1.18.0",
"@budibase/client": "^0.1.1",
"@budibase/colorpicker": "^1.0.1",
"@nx-js/compiler-util": "^2.0.0",
"@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "^0.7.0",
"codemirror": "^5.51.0",
"date-fns": "^1.29.0",
"deepmerge": "^4.2.2",
@ -74,7 +77,8 @@
"string_decoder": "^1.2.0",
"svelte-portal": "^0.1.0",
"svelte-simple-modal": "^0.4.2",
"uikit": "^3.1.7"
"uikit": "^3.1.7",
"yup": "^0.29.2"
},
"devDependencies": {
"@babel/core": "^7.5.5",
@ -89,6 +93,7 @@
"babel-jest": "^26.2.2",
"browser-sync": "^2.26.7",
"cypress": "^4.8.0",
"cypress-terminal-report": "^1.4.1",
"eslint-plugin-cypress": "^2.11.1",
"http-proxy-middleware": "^0.19.1",
"jest": "^26.2.2",

View File

@ -3,6 +3,7 @@ import posthog from "posthog-js"
function activate() {
Sentry.init({ dsn: process.env.SENTRY_DSN })
if (!process.env.POSTHOG_TOKEN) return
posthog.init(process.env.POSTHOG_TOKEN, {
api_host: process.env.POSTHOG_URL,
})
@ -13,7 +14,7 @@ function captureException(err) {
}
function captureEvent(event) {
if (process.env.NODE_ENV !== "production") return
if (!process.env.POSTHOG_TOKEN) return
posthog.capture(event)
}

View File

@ -99,6 +99,7 @@
<div class="titled-input">
<header>Name</header>
<input
data-cy="table-name-input"
type="text"
class="budibase__input"
bind:value={$backendUiStore.draftModel.name} />

View File

@ -24,7 +24,7 @@
}
</script>
<span class="topnavitemright" on:click={showSettingsModal}>
<span class="topnavitemright settings" on:click={showSettingsModal}>
<SettingsIcon />
</span>

View File

@ -1,14 +1,10 @@
<script>
import Button from "components/common/Button.svelte"
export let name,
description = `A minimalist CRM which removes the noise and allows you to focus
on your business.`,
_id
export let name, _id
</script>
<div class="apps-card">
<h3 class="app-title">{name}</h3>
<p class="app-desc">{description}</p>
<div class="card-footer">
<a href={`/_builder/${_id}`} class="app-button">Open {name}</a>
</div>

View File

@ -1,5 +1,13 @@
<script>
import { writable } from "svelte/store"
import { store, workflowStore, backendUiStore } from "builderStore"
import { string, object } from "yup"
import api, { get } from "builderStore/api"
import Form from "@svelteschool/svelte-forms"
import Spinner from "components/common/Spinner.svelte"
import { API, Info, User } from "./Steps"
import Indicator from "./Indicator.svelte"
import { Input, TextArea, Button } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
@ -9,49 +17,158 @@
import analytics from "../../analytics"
const { open, close } = getContext("simple-modal")
//Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
const createAppStore = writable({ currentStep: 0, values: {} })
let name = ""
let description = ""
let loading = false
let error = {}
export let hasKey
const createNewApp = async () => {
if ((name.length > 100 || name.length < 1) && description.length < 1) {
error = {
name: true,
description: true,
}
} else if (description.length < 1) {
error = {
name: false,
description: true,
}
} else if (name.length > 100 || name.length < 1) {
error = {
name: true,
}
} else {
error = {}
const data = { name, description }
loading = true
try {
const response = await post("/api/applications", data)
let submitting = false
let errors = {}
let validationErrors = {}
let validationSchemas = [
{
apiKey: string().required("Please enter your API key."),
},
{
applicationName: string().required("Your application must have a name."),
},
{
username: string().required("Your application needs a first user."),
password: string().required(
"Please enter a password for your first user."
),
accessLevelId: string().required(
"You need to select an access level for your user."
),
},
]
const res = await response.json()
let steps = [
{
component: API,
errors,
},
{
component: Info,
errors,
},
{
component: User,
errors,
},
]
analytics.captureEvent("web_app_created", {
name,
description,
appId: res._id,
})
$goto(`./${res._id}`)
} catch (error) {
console.error(error)
}
if (hasKey) {
validationSchemas.shift()
validationSchemas = validationSchemas
steps.shift()
steps = steps
}
// Handles form navigation
const back = () => {
if ($createAppStore.currentStep > 0) {
$createAppStore.currentStep -= 1
}
}
const next = () => {
$createAppStore.currentStep += 1
}
// $: errors = validationSchemas.validate(values);
$: getErrors(
$createAppStore.values,
validationSchemas[$createAppStore.currentStep]
)
async function getErrors(values, schema) {
try {
validationErrors = {}
await object(schema).validate(values, { abortEarly: false })
} catch (error) {
validationErrors = extractErrors(error)
}
}
let value
const checkValidity = async (values, currentStep) => {
const validity = await object()
.shape(validationSchemas[currentStep])
.isValid(values)
currentStepIsValid = validity
// Check full form on last step
if (currentStep === steps.length - 1) {
// Make one big schema from all the small ones
const fullSchema = Object.assign({}, ...validationSchemas)
// Check full form schema
const formIsValid = await object()
.shape(fullSchema)
.isValid(values)
fullFormIsValid = formIsValid
}
}
async function signUp() {
submitting = true
try {
// Add API key if there is none.
if (!hasKey) {
await updateKey(["budibase", $createAppStore.values.apiKey])
}
// Create App
const appResp = await post("/api/applications", {
name: $createAppStore.values.applicationName,
})
const appJson = await appResp.json()
analytics.captureEvent("web_app_created", {
name,
appId: appJson._id,
})
// Select Correct Application/DB in prep for creating user
const applicationPkg = await get(`/api/${appJson._id}/appPackage`)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
backendUiStore.actions.reset()
await store.setPackage(pkg)
workflowStore.actions.fetch()
} else {
throw new Error(pkg)
}
// Create user
const user = {
name: $createAppStore.values.username,
username: $createAppStore.values.username,
password: $createAppStore.values.password,
accessLevelId: $createAppStore.values.accessLevelId,
}
const userResp = await api.post(`/api/users`, user)
const json = await userResp.json()
$goto(`./${appJson._id}`)
} catch (error) {
console.error(error)
}
}
async function updateKey([key, value]) {
const response = await api.put(`/api/keys/${key}`, { value })
const res = await response.json()
return res
}
function extractErrors({ inner }) {
return inner.reduce((acc, err) => {
return { ...acc, [err.path]: err.message }
}, {})
}
let currentStepIsValid = false
let fullFormIsValid = false
$: checkValidity($createAppStore.values, $createAppStore.currentStep)
let onChange = () => {}
function _onCancel() {
@ -64,45 +181,55 @@
</script>
<div class="container">
<div class="sidebar">
{#each steps as { active, done }, i}
<Indicator
active={$createAppStore.currentStep === i}
done={i < $createAppStore.currentStep}
step={i + 1} />
{/each}
</div>
<div class="body">
<div class="heading">
<span class="icon">
<AppsIcon />
</span>
<h3 class="header">Create new web app</h3>
<h3 class="header">Get Started with Budibase</h3>
</div>
<div class="step">
<Form bind:values={$createAppStore.values}>
{#each steps as step, i (i)}
<div class:hidden={$createAppStore.currentStep !== i}>
<svelte:component
this={step.component}
{validationErrors}
options={step.options}
name={step.name} />
</div>
{/each}
</Form>
</div>
<div class="footer">
{#if $createAppStore.currentStep > 0}
<Button secondary on:click={back}>Back</Button>
{/if}
{#if $createAppStore.currentStep < steps.length - 1}
<Button secondary on:click={next} disabled={!currentStepIsValid}>
Next
</Button>
{/if}
{#if $createAppStore.currentStep === steps.length - 1}
<Button
secondary
on:click={signUp}
disabled={!fullFormIsValid || submitting}>
{submitting ? 'Loading...' : 'Submit'}
</Button>
{/if}
</div>
<Input
name="name"
label="Name"
placeholder="Enter application name"
on:change={e => (name = e.target.value)}
on:input={e => (name = e.target.value)} />
{#if error.name}
<span class="error">You need to enter a name for your application.</span>
{/if}
<TextArea
bind:value={description}
name="description"
label="Description"
placeholder="Describe your application" />
{#if error.description}
<span class="error">
Please enter a short description of your application
</span>
{/if}
</div>
<div class="footer">
<a href="./#" class="info">
<InfoIcon />
How to get started
</a>
<Button secondary thin on:click={_onCancel}>Cancel</Button>
<Button primary thin on:click={_onOkay}>Save</Button>
</div>
<div class="close-button" on:click={_onCancel}>
<CloseIcon />
</div>
{#if loading}
<img src="/_builder/assets/bb-logo.svg" alt="budibase icon" />
{#if submitting}
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
@ -112,9 +239,19 @@
<style>
.container {
min-height: 600px;
display: grid;
grid-template-columns: 80px 1fr;
position: relative;
}
.sidebar {
display: grid;
border-bottom-left-radius: 0.5rem;
border-top-left-radius: 0.5rem;
grid-gap: 30px;
align-content: center;
background: #f5f5f5;
}
.close-button {
cursor: pointer;
position: absolute;
@ -135,43 +272,18 @@
margin: 0;
font-size: 24px;
font-weight: 600;
font-family: inter;
}
.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);
}
.info {
color: var(--blue);
text-decoration-color: var(--blue);
}
.info :global(svg) {
fill: var(--blue);
margin-right: 8px;
width: 24px;
height: 24px;
}
.body {
padding: 40px 40px 80px 40px;
padding: 40px 60px 60px 60px;
display: grid;
grid-gap: 20px;
align-items: center;
grid-template-rows: auto 1fr auto;
}
.footer {
display: grid;
grid-gap: 20px;
align-items: center;
grid-template-columns: 1fr auto auto;
padding: 30px 40px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-1);
grid-gap: 15px;
grid-template-columns: auto auto;
justify-content: end;
}
.spinner-container {
background: white;
@ -189,9 +301,14 @@
.spinner-text {
font-size: 2em;
}
.error {
color: var(--red);
font-weight: bold;
font-size: 0.8em;
.hidden {
display: none;
}
img {
position: absolute;
top: 20px;
left: 20px;
height: 40px;
}
</style>

View File

@ -0,0 +1,80 @@
<script>
export let step, done, active
</script>
<div class="container" class:active class:done>
<div class="circle" class:active class:done>
{#if done}
<svg
width="12"
height="10"
viewBox="0 0 12 10"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.1212 0.319527C10.327 0.115582 10.6047 0.000803464 10.8944
4.20219e-06C11.1841 -0.00079506 11.4624 0.11245 11.6693
0.315256C11.8762 0.518062 11.9949 0.794134 11.9998 1.08379C12.0048
1.37344 11.8955 1.65339 11.6957 1.86313L5.82705 9.19893C5.72619
9.30757 5.60445 9.39475 5.46913 9.45527C5.3338 9.51578 5.18766 9.54839
5.03944 9.55113C4.89123 9.55388 4.74398 9.52671 4.60651
9.47124C4.46903 9.41578 4.34416 9.33316 4.23934 9.22833L0.350925
5.33845C0.242598 5.23751 0.155712 5.11578 0.0954499 4.98054C0.0351876
4.84529 0.00278364 4.69929 0.00017159 4.55124C-0.00244046 4.4032
0.024793 4.25615 0.0802466 4.11886C0.1357 3.98157 0.218238 3.85685
0.322937 3.75215C0.427636 3.64746 0.55235 3.56492 0.68964
3.50946C0.82693 3.45401 0.973983 3.42678 1.12203 3.42939C1.27007 3.432
1.41607 3.46441 1.55132 3.52467C1.68657 3.58493 1.80829 3.67182
1.90923 3.78014L4.98762 6.85706L10.0933 0.35187C10.1024 0.340482
10.1122 0.329679 10.1227 0.319527H10.1212Z"
fill="white" />
</svg>
{:else}{step}{/if}
</div>
</div>
<style>
.container::before {
content: "";
position: absolute;
top: -30px;
width: 1px;
height: 30px;
background: #bdbdbd;
}
.container:first-child::before {
display: none;
}
.container {
position: relative;
height: 45px;
display: grid;
place-items: center;
}
.container.active {
box-shadow: inset 3px 0 0 0 #4285f4;
}
.circle.active {
background: #4285f4;
color: white;
border: none;
}
.circle.done {
background: #bdbdbd;
color: white;
border: none;
}
.circle {
color: #bdbdbd;
font-size: 14px;
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid #bdbdbd;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,25 @@
<script>
import { Input } from "@budibase/bbui"
export let validationErrors
let blurred = { api: false }
</script>
<h2>Setup your API Key</h2>
<div class="container">
<Input
on:input={() => (blurred.api = true)}
label="API Key"
name="apiKey"
placeholder="Enter your API Key"
type="password"
error={blurred.api && validationErrors.apiKey} />
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>
</div>
<style>
.container {
display: grid;
grid-gap: 40px;
}
</style>

View File

@ -0,0 +1,24 @@
<script>
import { Input } from "@budibase/bbui"
export let validationErrors
let blurred = { appName: false }
</script>
<h2>Create your first web app</h2>
<div class="container">
<Input
on:input={() => (blurred.appName = true)}
label="Web app name"
name="applicationName"
placeholder="Enter name of your web application"
type="name"
error={blurred.appName && validationErrors.applicationName} />
</div>
<style>
.container {
display: grid;
grid-gap: 40px;
}
</style>

View File

@ -0,0 +1,35 @@
<script>
import { Input, Select } from "@budibase/bbui"
export let validationErrors
let blurred = { username: false, password: false }
</script>
<h2>Create new user</h2>
<div class="container">
<Input
on:input={() => (blurred.username = true)}
label="Username"
name="username"
placeholder="Username"
type="name"
error={blurred.username && validationErrors.username} />
<Input
on:input={() => (blurred.password = true)}
label="Password"
name="password"
placeholder="Password"
type="pasword"
error={blurred.password && validationErrors.password} />
<Select name="accessLevelId">
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</Select>
</div>
<style>
.container {
display: grid;
grid-gap: 40px;
}
</style>

View File

@ -0,0 +1,3 @@
export { default as API } from "./API.svelte"
export { default as Info } from "./Info.svelte"
export { default as User } from "./User.svelte"

View File

@ -14,14 +14,6 @@
const getProperties = name => panelDefinition[name]
onMount(() => {
// if(propGroup) {
// propGroup.addEventListener("scroll", function(e){
// console.log("I SCROLLED", e.target.scrollTop)
// })
// }
})
function onChange(category) {
selectedCategory = category
}

View File

@ -32,7 +32,7 @@
<div class="property-control">
<div class="label">{label}</div>
<div class="control">
<div data-cy={`${key}-prop-control`} class="control">
<svelte:component
this={control}
{...handlevalueKey(value)}

View File

@ -246,6 +246,66 @@ export default {
name: "Blocks",
isCategory: true,
children: [
{
name: "List",
_component: "@budibase/standard-components/list",
description: "Renders all children once per record, of a given table",
icon: "ri-file-list-line",
properties: {
design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }],
},
children: [],
},
{
_component: "@budibase/standard-components/stackedlist",
name: "Stacked List",
description:
"A basic card component that can contain content and actions.",
icon: "ri-archive-drawer-line",
children: [],
properties: {
design: { ...all },
settings: [
{
label: "Image",
key: "imageUrl",
control: Input,
placeholder: "{{{context.Image}}}",
},
{
label: "Heading",
key: "heading",
control: Input,
placeholder: "{{context.Heading}}",
},
{
label: "Text 1",
key: "text1",
control: Input,
placeholder: "{{context.Text 1}}",
},
{
label: "Text 2",
key: "text2",
control: Input,
placeholder: "{{context.Text 2}}",
},
{
label: "Text 3",
key: "text3",
control: Input,
placeholder: "{{context.Text 3}}",
},
{
label: "destinationUrl",
key: "destinationUrl",
control: Input,
placeholder: "/table/_id",
},
],
},
},
{
_component: "@budibase/materialdesign-components/BasicCard",
name: "Card",
@ -407,17 +467,6 @@ export default {
// },
// children: [],
// },
{
name: "List",
_component: "@budibase/standard-components/list",
description: "Renders all children once per record, of a given table",
icon: "ri-file-list-line",
properties: {
design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }],
},
children: [],
},
{
name: "Record Detail",
_component: "@budibase/standard-components/recorddetail",

View File

@ -1,6 +1,7 @@
<script>
import { getContext } from "svelte"
import { store } from "builderStore"
import api from "builderStore/api"
import AppList from "components/start/AppList.svelte"
import { onMount } from "svelte"
import ActionButton from "components/common/ActionButton.svelte"
@ -23,6 +24,24 @@
}
}
let hasKey
async function fetchKeys() {
const response = await api.get(`/api/keys/`)
const res = await response.json()
return res.budibase
}
async function checkIfKeysAndApps() {
const key = await fetchKeys()
const apps = await getApps()
if (key) {
hasKey = true
} else {
showCreateAppModal()
}
}
// Handle create app modal
const { open } = getContext("simple-modal")
@ -30,8 +49,7 @@
open(
CreateAppModal,
{
message: "What is your name?",
hasForm: true,
hasKey,
},
{
closeButton: false,
@ -42,6 +60,8 @@
}
)
}
checkIfKeysAndApps()
</script>
<div class="header">

View File

@ -1,6 +1,6 @@
{
"name": "budibase",
"version": "0.1.12",
"version": "0.1.13",
"description": "Budibase CLI",
"repository": "https://github.com/Budibase/Budibase",
"homepage": "https://www.budibase.com",
@ -17,7 +17,7 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/server": "^0.1.12",
"@budibase/server": "^0.1.13",
"@inquirer/password": "^0.0.6-alpha.0",
"chalk": "^2.4.2",
"dotenv": "^8.2.0",
@ -29,5 +29,5 @@
"uuid": "^7.0.3",
"yargs": "^14.2.0"
},
"gitHead": "eff4fa93ca1db11b97b5fdedc0c488413e277eb8"
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691"
}

View File

@ -8,8 +8,6 @@ module.exports = async ({ dir }) => {
// dont make this a variable or top level require
// it will cause environment module to be loaded prematurely
return require("@budibase/server/src/app")().then(server => {
server.on("close", () => console.log("Server Closed"))
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
})
const server = require("@budibase/server/src/app")
server.on("close", () => console.log("Server Closed"))
}

View File

@ -13,7 +13,7 @@
},
"devDependencies": {
"@budibase/client": "^0.1.1",
"@budibase/standard-components": "^0.1.10",
"@budibase/standard-components": "^0.1.13",
"@material/button": "^4.0.0",
"@material/checkbox": "^4.0.0",
"@material/data-table": "4.0.0",
@ -50,9 +50,9 @@
"keywords": [
"svelte"
],
"version": "0.1.10",
"version": "0.1.13",
"license": "MIT",
"gitHead": "eff4fa93ca1db11b97b5fdedc0c488413e277eb8",
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
"dependencies": {
"@material/card": "4.0.0"
}

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/server",
"version": "0.1.12",
"version": "0.1.13",
"description": "Budibase Web Server",
"main": "src/electron.js",
"repository": {
@ -97,5 +97,5 @@
"./scripts/jestSetup.js"
]
},
"gitHead": "eff4fa93ca1db11b97b5fdedc0c488413e277eb8"
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691"
}

View File

@ -69,7 +69,6 @@ exports.create = async function(ctx) {
"@budibase/materialdesign-components",
],
name: ctx.request.body.name,
description: ctx.request.body.description,
}
const { rev } = await db.put(newApplication)
@ -208,7 +207,7 @@ const getClientId = ctx => {
env.CLIENT_ID
if (!clientId) {
ctx.throw(400, "ClientId not suplied")
ctx.throw(400, "ClientId not supplied")
}
return clientId
}

View File

@ -68,7 +68,9 @@ exports.save = async function(ctx) {
ctx.eventEmitter &&
ctx.eventEmitter.emit(`record:save`, {
record,
args: {
record,
},
instanceId: ctx.user.instanceId,
})
ctx.body = record

View File

@ -1,17 +1,17 @@
const recordController = require("../../record")
module.exports = async function saveRecord({ args, instanceId }) {
module.exports = async function saveRecord({ args, context }) {
const { model, ...record } = args.record
const ctx = {
params: {
instanceId,
instanceId: context.instanceId,
modelId: model._id,
},
request: {
body: record,
},
user: { instanceId },
user: { instanceId: context.instanceId },
}
await recordController.save(ctx)

View File

@ -44,7 +44,9 @@ router
useAppRootPath: true,
}
ctx.isDev =
process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "jest"
process.env.NODE_ENV !== "production" &&
process.env.NODE_ENV !== "jest" &&
process.env.NODE_ENV !== "cypress"
await next()
})
.use(authenticated)

View File

@ -18,7 +18,7 @@ async function executeRelevantWorkflows(event, eventType) {
workflowOrchestrator.strategy = serverStrategy
for (let workflow of workflows) {
workflowOrchestrator.execute(workflow)
workflowOrchestrator.execute(workflow, event)
}
}

View File

@ -12,9 +12,9 @@ exports.Orchestrator = class Orchestrator {
this._strategy = strategy()
}
async execute(workflow) {
async execute(workflow, context) {
if (workflow.live) {
this._strategy.run(workflow.definition)
this._strategy.run(workflow.definition, context)
}
}
}
@ -35,12 +35,15 @@ exports.serverStrategy = () => ({
return mappedArgs
},
run: async function(workflow) {
run: async function(workflow, context) {
for (let block of workflow.steps) {
if (block.type === "CLIENT") continue
const action = require(`../api/controllers/workflow/actions/${block.actionId}`)
const response = await action({ args: this.bindContextArgs(block.args) })
const response = await action({
args: this.bindContextArgs(block.args),
context,
})
this.context = {
...this.context,

View File

@ -5,6 +5,8 @@ const fetch = require("node-fetch")
const tar = require("tar-fs")
const zlib = require("zlib")
const { promisify } = require("util")
const packageJson = require("../../package.json")
const streamPipeline = promisify(stream.pipeline)
exports.appPackageFolder = (config, appname) =>
@ -16,8 +18,7 @@ exports.downloadExtractComponentLibraries = async appFolder => {
// Need to download tarballs directly from NPM as our users may not have node on their machine
for (let lib of LIBRARIES) {
// download tarball
// TODO: make sure the latest version is always downloaded
const registryUrl = `https://registry.npmjs.org/@budibase/${lib}/-/${lib}-0.1.2.tgz`
const registryUrl = `https://registry.npmjs.org/@budibase/${lib}/-/${lib}-${packageJson.version}.tgz`
const response = await fetch(registryUrl)
if (!response.ok)
throw new Error(`unexpected response ${response.statusText}`)

View File

@ -172,21 +172,6 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@budibase/client@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.1.tgz#6df65bef641d956dea6d648e5dcd7e95622a24fc"
integrity sha512-z5CYj/4wahV9Kg7GwYi1knRZdm60uspkjOX7b3GXy85Zam1+m4ObSQ1aT6857WCXgl2+CQ3hIb2QGqSZfOTFuA==
dependencies:
"@nx-js/compiler-util" "^2.0.0"
bcryptjs "^2.4.3"
deep-equal "^2.0.1"
lodash "^4.17.15"
lunr "^2.3.5"
mustache "^4.0.1"
regexparam "^1.3.0"
shortid "^2.2.8"
svelte "^3.9.2"
"@cnakazawa/watch@^1.0.3":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
@ -369,11 +354,6 @@
path-to-regexp "1.x"
urijs "^1.19.2"
"@nx-js/compiler-util@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@nx-js/compiler-util/-/compiler-util-2.0.0.tgz#c74c12165fa2f017a292bb79af007e8fce0af297"
integrity sha512-AxSQbwj9zqt8DYPZ6LwZdytqnwfiOEdcFdq4l8sdjkZmU2clTht7RDLCI8xvkp7KqgcNaOGlTeCM55TULWruyQ==
"@sendgrid/client@^7.1.1":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.1.1.tgz#09a25e58ac7e5321d66807e7110ff0fb61bb534f"
@ -842,11 +822,6 @@ array-equal@^1.0.0:
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
array-filter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
array-unique@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
@ -906,13 +881,6 @@ atomic-sleep@^1.0.0:
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
dependencies:
array-filter "^1.0.0"
aws-sdk@^2.706.0:
version "2.706.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953"
@ -1584,26 +1552,6 @@ decompress-response@^3.3.0:
dependencies:
mimic-response "^1.0.0"
deep-equal@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
dependencies:
es-abstract "^1.17.5"
es-get-iterator "^1.1.0"
is-arguments "^1.0.4"
is-date-object "^1.0.2"
is-regex "^1.0.5"
isarray "^2.0.5"
object-is "^1.1.2"
object-keys "^1.1.1"
object.assign "^4.1.0"
regexp.prototype.flags "^1.3.0"
side-channel "^1.0.2"
which-boxed-primitive "^1.0.1"
which-collection "^1.0.1"
which-typed-array "^1.1.2"
deep-equal@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@ -1932,36 +1880,6 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
string.prototype.trimleft "^2.1.1"
string.prototype.trimright "^2.1.1"
es-abstract@^1.17.4:
version "1.17.6"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
is-callable "^1.2.0"
is-regex "^1.1.0"
object-inspect "^1.7.0"
object-keys "^1.1.1"
object.assign "^4.1.0"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-get-iterator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
dependencies:
es-abstract "^1.17.4"
has-symbols "^1.0.1"
is-arguments "^1.0.4"
is-map "^2.0.1"
is-set "^2.0.1"
is-string "^1.0.5"
isarray "^2.0.5"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@ -2856,32 +2774,17 @@ is-accessor-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-arguments@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
is-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@ -2891,11 +2794,6 @@ is-callable@^1.1.4, is-callable@^1.1.5:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
is-callable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
@ -2918,7 +2816,7 @@ is-data-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-date-object@^1.0.1, is-date-object@^1.0.2:
is-date-object@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
@ -2981,21 +2879,11 @@ is-installed-globally@^0.3.1:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
is-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
is-npm@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==
is-number-object@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@ -3026,28 +2914,11 @@ is-regex@^1.0.5:
dependencies:
has "^1.0.3"
is-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff"
integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==
dependencies:
has-symbols "^1.0.1"
is-set@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
is-string@^1.0.4, is-string@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
is-symbol@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
@ -3062,31 +2933,11 @@ is-type-of@^1.0.0:
is-class-hotfix "~0.0.6"
isstream "~0.1.2"
is-typed-array@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
dependencies:
available-typed-arrays "^1.0.0"
es-abstract "^1.17.4"
foreach "^2.0.5"
has-symbols "^1.0.1"
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakset@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -3108,11 +2959,6 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isbinaryfile@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
@ -4009,14 +3855,10 @@ lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
lodash@^4.17.10:
lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
loose-envify@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -4058,11 +3900,6 @@ ltgt@~2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.1.3.tgz#10851a06d9964b971178441c23c9e52698eece34"
lunr@^2.3.5:
version "2.3.8"
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.8.tgz#a8b89c31f30b5a044b97d2d28e2da191b6ba2072"
integrity sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg==
make-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@ -4227,11 +4064,6 @@ nan@^2.12.1:
version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
nanoid@^2.1.0:
version "2.1.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@ -4381,14 +4213,6 @@ object-inspect@^1.7.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
object-is@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.5"
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@ -5038,19 +4862,6 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexp.prototype.flags@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.1"
regexparam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
regexpp@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
@ -5344,21 +5155,6 @@ shellwords@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
shortid@^2.2.8:
version "2.2.15"
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122"
integrity sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==
dependencies:
nanoid "^2.1.0"
side-channel@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"
integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==
dependencies:
es-abstract "^1.17.0-next.1"
object-inspect "^1.7.0"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -5575,7 +5371,7 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string.prototype.trimend@^1.0.0, string.prototype.trimend@^1.0.1:
string.prototype.trimend@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
dependencies:
@ -5598,7 +5394,7 @@ string.prototype.trimright@^2.1.1:
es-abstract "^1.17.5"
string.prototype.trimend "^1.0.0"
string.prototype.trimstart@^1.0.0, string.prototype.trimstart@^1.0.1:
string.prototype.trimstart@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
dependencies:
@ -5717,11 +5513,6 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
svelte@^3.9.2:
version "3.24.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.24.0.tgz#6565a42c9705796fa66c6abb4fedc09f4323a4a8"
integrity sha512-VFXom6EP2DK83kxy4ZlBbaZklSbZIrpNH3oNXlPYHJUuW4q1OuAr3ZoYbfIVTVYPDgrI7Yq0gQcOhDlAtO4qfw==
symbol-tree@^3.2.2:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -6156,44 +5947,11 @@ whatwg-url@^7.0.0:
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
which-boxed-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
dependencies:
is-bigint "^1.0.0"
is-boolean-object "^1.0.0"
is-number-object "^1.0.3"
is-string "^1.0.4"
is-symbol "^1.0.2"
which-collection@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies:
is-map "^2.0.1"
is-set "^2.0.1"
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-typed-array@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
dependencies:
available-typed-arrays "^1.0.2"
es-abstract "^1.17.5"
foreach "^2.0.5"
function-bind "^1.1.1"
has-symbols "^1.0.1"
is-typed-array "^1.1.3"
which@^1.2.9, which@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"

View File

@ -249,6 +249,18 @@
"model": "models"
}
},
"stackedlist": {
"name": "Stacked List",
"description": "A stacked list component for displaying information",
"props": {
"imageUrl": "string",
"heading": "string",
"text1": "string",
"text2": "string",
"text3": "string",
"destinationUrl": "string"
}
},
"recorddetail": {
"description": "Loads a record, using an ID in the url",
"data": true,

View File

@ -33,11 +33,12 @@
"keywords": [
"svelte"
],
"version": "0.1.10",
"version": "0.1.13",
"license": "MIT",
"gitHead": "eff4fa93ca1db11b97b5fdedc0c488413e277eb8",
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
"dependencies": {
"@beyonk/svelte-googlemaps": "^2.2.0",
"fast-sort": "^2.2.0",
"fusioncharts": "^3.15.1-sr.1",
"svelte-fusioncharts": "^1.0.0"
}

View File

@ -32,11 +32,35 @@
$: Object.values(inputElements).length && setForm(record)
const createBlankRecord = () => {
if (!schema) return
const newrecord = {
modelId: model,
}
for (let fieldName in schema) {
const field = schema[fieldName]
// defaulting to first one, as a blank value will fail validation
if (
field.type === "string" &&
field.constraints &&
field.constraints.inclusion &&
field.constraints.inclusion.length > 0
) {
newrecord[fieldName] = field.constraints.inclusion[0]
} else if (field.type === "number") newrecord[fieldName] = null
else if (field.type === "boolean") newrecord[fieldName] = false
else if (field.type === "link") newrecord[fieldName] = []
else newrecord[fieldName] = ""
}
return newrecord
}
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
record = createBlankRecord()
}
async function save() {
@ -81,9 +105,7 @@
el.checked = false
}
}
record = {
modelId: model,
}
record = createBlankRecord()
}
const setForm = rec => {
@ -123,7 +145,7 @@
isNew = !recordId || recordId === "new"
if (isNew) {
record = { modelId: model }
record = createBlankRecord()
} else {
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
_bb.api

View File

@ -32,11 +32,35 @@
$: Object.values(inputElements).length && setForm(record)
const createBlankRecord = () => {
if (!schema) return
const newrecord = {
modelId: model,
}
for (let fieldName in schema) {
const field = schema[fieldName]
// defaulting to first one, as a blank value will fail validation
if (
field.type === "string" &&
field.constraints &&
field.constraints.inclusion &&
field.constraints.inclusion.length > 0
) {
newrecord[fieldName] = field.constraints.inclusion[0]
} else if (field.type === "number") newrecord[fieldName] = null
else if (field.type === "boolean") newrecord[fieldName] = false
else if (field.type === "link") newrecord[fieldName] = []
else newrecord[fieldName] = ""
}
return newrecord
}
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
record = createBlankRecord()
}
async function save() {
@ -81,9 +105,7 @@
el.checked = false
}
}
record = {
modelId: model,
}
record = createBlankRecord()
}
const setForm = rec => {
@ -123,7 +145,7 @@
isNew = !recordId || recordId === "new"
if (isNew) {
record = { modelId: model }
record = createBlankRecord()
} else {
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
_bb.api

View File

@ -1,31 +0,0 @@
<script>
import { GoogleMap } from "@beyonk/svelte-googlemaps"
// export let _bb
// export let onLoad
// export let _instanceId
// export let model
// let mapComponent
// let headers = []
// let store = _bb.store
// $: data = $store[model._id] || []
// async function fetchData() {
// const FETCH_RECORDS_URL = `/api/${_instanceId}/all_${model._id}/records`
// const response = await _bb.api.get(FETCH_RECORDS_URL)
// if (response.status === 200) {
// const json = await response.json()
// store.update(state => {
// state[model._id] = json
// return state
// });
// } else {
// throw new Error("Failed to fetch records.", response)
// }
// }
</script>
<GoogleMap apiKey={'AIzaSyCPJ_eiSIbhRMmKBiVYXgh4HFHmbC4ZL5U'} />

View File

@ -1,6 +1,9 @@
<script>
import { onMount } from "svelte"
import { cssVars, createClasses } from "./cssVars"
import ArrowUp from "./icons/ArrowUp.svelte"
import ArrowDown from "./icons/ArrowDown.svelte"
import fsort from "fast-sort"
export let _bb
export let onLoad
@ -12,6 +15,8 @@
let headers = []
let store = _bb.store
let sort = {}
let sorted = []
$: cssVariables = {
backgroundColor,
@ -20,6 +25,10 @@
borderColor,
}
$: data = $store[model] || []
$: sorted = sort.direction ? fsort(data)[sort.direction](sort.column) : data
$: if (model) fetchData()
const shouldDisplayField = name => {
if (name.startsWith("_")) return false
// always 'record'
@ -48,8 +57,20 @@
}
}
$: data = $store[model] || []
$: if (model) fetchData()
function sortColumn(column) {
if (column === sort.column) {
sort = {
direction: sort.direction === "asc" ? "desc" : null,
column: sort.direction === "asc" ? sort.column : null,
}
return
}
sort = {
column,
direction: "asc",
}
}
onMount(async () => {
await fetchData()
@ -60,12 +81,21 @@
<thead>
<tr>
{#each headers as header}
<th>{header}</th>
<th on:click={() => sortColumn(header)}>
<span>
{header}
{#if sort.column === header}
<svelte:component
this={sort.direction === 'asc' ? ArrowDown : ArrowUp}
style="height: 1em;" />
{/if}
</span>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each data as row}
{#each sorted as row (row._id)}
<tr>
{#each headers as header}
{#if row[header]}
@ -95,6 +125,12 @@
color: var(--color);
font-weight: bold;
text-transform: capitalize;
cursor: pointer;
}
th span {
display: flex;
align-items: center;
}
td,

View File

@ -0,0 +1,89 @@
<script>
export let imageUrl = ""
export let heading = ""
export let text1 = ""
export let text2 = ""
export let text3 = ""
export let destinationUrl = ""
$: showImage = !!imageUrl
</script>
<div class="container">
<a href={destinationUrl}>
<div class="content">
{#if showImage}
<div class="image-block">
<img class="image" src={imageUrl} alt="" />
</div>
{/if}
<h2 class="heading">{heading}</h2>
<h4 class="text">{text1}</h4>
<h4 class="text">{text2}</h4>
<h4 class="text3">{text3}</h4>
</div>
</a>
</div>
<style>
a {
text-decoration: none;
color: inherit;
}
.container {
padding: 20px;
}
.content {
display: grid;
grid-template-columns: 120px 300px 1fr 1fr 1fr;
align-items: center;
gap: 20px;
min-height: 80px;
}
@media (max-width: 800px) {
.content {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
}
.image-block {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.image {
padding: 2px;
max-width: 80px;
max-height: 80px;
margin-right: 20px;
}
.heading {
font-size: 24px;
margin: 0;
min-width: 200px;
}
.text {
font-size: 16px;
font-weight: 400;
}
.text3 {
text-align: end;
font-size: 16px;
font-weight: 400;
}
@media (max-width: 800px) {
.text3 {
text-align: start;
}
}
</style>

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="18"
height="18">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M13 16.172l5.364-5.364 1.414 1.414L12 20l-7.778-7.778 1.414-1.414L11
16.172V4h2v12.172z" />
</svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="18"
height="18">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M13 7.828V20h-2V7.828l-5.364 5.364-1.414-1.414L12 4l7.778 7.778-1.414
1.414L13 7.828z" />
</svg>

After

Width:  |  Height:  |  Size: 249 B

View File

@ -21,6 +21,6 @@ export { default as datachart } from "./DataChart.svelte"
export { default as datalist } from "./DataList.svelte"
export { default as list } from "./List.svelte"
export { default as datasearch } from "./DataSearch.svelte"
export { default as datamap } from "./DataMap.svelte"
export { default as embed } from "./Embed.svelte"
export { default as stackedlist } from "./StackedList.svelte"
export { default as recorddetail } from "./RecordDetail.svelte"