1
0
Fork 0
mirror of synced 2024-07-07 23:35:49 +12:00

merge master

This commit is contained in:
kevmodrome 2020-10-08 14:37:57 +02:00
commit 480a73997d
149 changed files with 3591 additions and 11248 deletions

View file

@ -15,8 +15,12 @@ context("Create a automation", () => {
cy.contains("automate").click() cy.contains("automate").click()
cy.contains("Create New Automation").click() cy.contains("Create New Automation").click()
cy.get("input").type("Add Record") cy.get(".modal").within(() => {
cy.contains("Save").click() cy.get("input").type("Add Record")
cy.get(".buttons")
.contains("Create")
.click()
})
// Add trigger // Add trigger
cy.get("[data-cy=add-automation-component]").click() cy.get("[data-cy=add-automation-component]").click()
@ -46,7 +50,6 @@ context("Create a automation", () => {
// Activate Automation // Activate Automation
cy.get("[data-cy=activate-automation]").click() cy.get("[data-cy=activate-automation]").click()
cy.contains("Add Record").should("be.visible")
cy.get(".stop-button.highlighted").should("be.visible") cy.get(".stop-button.highlighted").should("be.visible")
}) })

View file

@ -1,70 +1,65 @@
context('Create a Table', () => { context("Create a Table", () => {
before(() => { before(() => {
cy.visit('localhost:4001/_builder') cy.visit("localhost:4001/_builder")
cy.createApp('Table App', 'Table App Description') cy.createApp("Table App", "Table App Description")
}) })
it('should create a new Table', () => { it("should create a new Table", () => {
cy.createTable('dog') cy.createTable("dog")
// Check if Table exists // Check if Table exists
cy.get('.title').should('contain.text', 'dog') cy.get(".title span").should("have.text", "dog")
}) })
it('adds a new column to the table', () => { it("adds a new column to the table", () => {
cy.addColumn('dog', 'name', 'Plain Text') cy.addColumn("dog", "name", "Text")
cy.contains("name").should("be.visible")
})
cy.contains('name').should("be.visible") it("creates a record in the table", () => {
}) cy.addRecord(["Rover"])
cy.contains("Rover").should("be.visible")
})
it('creates a record in the table', () => { it("updates a column on the table", () => {
cy.addRecord(["Rover"]) cy.contains("name").click()
cy.get("[data-cy='edit-column-header']").click()
cy.get(".actions input")
.first()
.type("updated")
cy.get("select").select("Text")
cy.contains("Save Column").click()
cy.contains("nameupdated").should("have.text", "nameupdated")
})
cy.contains('Rover').should("be.visible") it("edits a record", () => {
}) cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=edit-row]").click()
cy.get(".modal input").type("Updated")
cy.contains("Save").click()
cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
})
it('updates a column on the table', () => { it("deletes a record", () => {
cy.contains("name").click() cy.get("tbody .ri-more-line").click()
cy.get("[data-cy='edit-column-header']").click() cy.get("[data-cy=delete-row]").click()
cy.contains("Delete Row").click()
cy.contains("RoverUpdated").should("not.exist")
})
cy.get("[placeholder=Name]").type("updated") it("deletes a column", () => {
cy.get("select").select("Plain Text") cy.contains("name").click()
cy.get("[data-cy='delete-column-header']").click()
cy.contains("Save Column").click() cy.contains("Delete Column").click()
cy.contains("nameupdated").should("not.exist")
cy.contains('nameupdated').should('have.text', 'nameupdated ') })
})
it('edits a record', () => {
cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=edit-row]").click()
cy.get(".actions input").type("Updated")
cy.contains("Save").click()
cy.contains('RoverUpdated').should('have.text', 'RoverUpdated')
})
it('deletes a record', () => {
cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=delete-row]").click()
cy.get(".modal-actions").contains("Delete").click()
cy.contains('RoverUpdated').should('not.exist')
})
it('deletes a column', () => {
cy.contains("name").click()
cy.get("[data-cy='delete-column-header']").click()
cy.contains('nameupdated').should('not.exist')
})
it('deletes a table', () => {
cy.contains("div", "dog").get(".ri-more-line").click()
cy.get("[data-cy=delete-table]").click()
cy.get(".modal-actions").contains("Delete").click()
cy.contains('dog').should('not.exist')
})
it("deletes a table", () => {
cy.contains("div", "dog")
.get(".ri-more-line")
.click()
cy.get("[data-cy=delete-table]").click()
cy.contains("Delete Table").click()
cy.contains("dog").should("not.exist")
})
}) })

View file

@ -1,91 +1,100 @@
context("Create a View", () => {
before(() => {
cy.visit("localhost:4001/_builder")
cy.createApp("View App", "View App Description")
cy.createTable("data")
cy.addColumn("data", "group", "Text")
cy.addColumn("data", "age", "Number")
cy.addColumn("data", "rating", "Number")
context('Create a View', () => { // 6 Records
before(() => { cy.addRecord(["Students", 25, 1])
cy.visit('localhost:4001/_builder') cy.addRecord(["Students", 20, 3])
cy.createApp('View App', 'View App Description') cy.addRecord(["Students", 18, 6])
cy.createTable('data') cy.addRecord(["Students", 25, 2])
cy.addColumn('data', 'group', 'Plain Text') cy.addRecord(["Teachers", 49, 5])
cy.addColumn('data', 'age', 'Number') cy.addRecord(["Teachers", 36, 3])
cy.addColumn('data', 'rating', 'Number') })
// 6 Records it("creates a view", () => {
cy.addRecord(["Students", 25, 1]) cy.contains("Create New View").click()
cy.addRecord(["Students", 20, 3]) cy.get(".menu-container").within(() => {
cy.addRecord(["Students", 18, 6]) cy.get("input").type("Test View")
cy.addRecord(["Students", 25, 2])
cy.addRecord(["Teachers", 49, 5])
cy.addRecord(["Teachers", 36, 3])
})
it('creates a view', () => {
cy.contains("Create New View").click()
cy.get("[placeholder='View Name']").type("Test View")
cy.contains("Save View").click() cy.contains("Save View").click()
cy.get(".title").contains("Test View")
cy.get("thead th").should(($headers) => {
expect($headers).to.have.length(3)
const headers = $headers.map((i, header) => Cypress.$(header).text())
expect(headers.get()).to.deep.eq([
"group",
"age",
"rating"
])
})
});
it('filters the view by age over 10', () => {
cy.contains("Filter").click()
cy.contains("Add Filter").click()
cy.get(".menu-container").find("select").first().select("age")
cy.get(".menu-container").find("select").eq(1).select("More Than")
cy.get("input[placeholder='age']").type(18)
cy.contains("Save").click()
cy.get("tbody tr").should(($values) => {
expect($values).to.have.length(5)
})
});
it('creates a stats calculation view based on age', () => {
cy.contains("Calculate").click()
// we may reinstate this - have commented this dropdown for now as there is only one option
//cy.get(".menu-container").find("select").first().select("Statistics")
cy.get(".menu-container").find("select").eq(0).select("age")
cy.contains("Save").click()
cy.get("thead th").should(($headers) => {
expect($headers).to.have.length(7)
const headers = $headers.map((i, header) => Cypress.$(header).text())
expect(headers.get()).to.deep.eq([
"field",
"sum",
"min",
"max",
"count",
"sumsqr",
"avg",
])
})
cy.get("tbody td").should(($values) => {
const values = $values.map((i, value) => Cypress.$(value).text())
expect(values.get()).to.deep.eq([
"age",
"155",
"20",
"49",
"5",
"5347",
"31"
])
})
}) })
cy.get(".title").contains("Test View")
cy.get("thead th div").should($headers => {
expect($headers).to.have.length(3)
const headers = $headers.map((i, header) => Cypress.$(header).text())
expect(headers.get()).to.deep.eq(["group", "age", "rating"])
})
})
it('groups the view by group', () => { it("filters the view by age over 10", () => {
cy.contains("Group By").click() cy.contains("Filter").click()
cy.get("select").select("group") cy.contains("Add Filter").click()
cy.contains("Save").click() cy.get(".menu-container")
cy.contains("Students").should("be.visible") .find("select")
cy.contains("Teachers").should("be.visible") .first()
.select("age")
cy.get(".menu-container")
.find("select")
.eq(1)
.select("More Than")
cy.get("input").type(18)
cy.contains("Save").click()
cy.get("tbody tr").should($values => {
expect($values).to.have.length(5)
})
})
cy.get("tbody tr").first().find("td").should(($values) => { it("creates a stats calculation view based on age", () => {
cy.contains("Calculate").click()
// we may reinstate this - have commented this dropdown for now as there is only one option
//cy.get(".menu-container").find("select").first().select("Statistics")
cy.get(".menu-container")
.find("select")
.eq(0)
.select("age")
cy.contains("Save").click()
cy.get("thead th div").should($headers => {
expect($headers).to.have.length(7)
const headers = $headers.map((i, header) => Cypress.$(header).text())
expect(headers.get()).to.deep.eq([
"field",
"sum",
"min",
"max",
"count",
"sumsqr",
"avg",
])
})
cy.get("tbody td").should($values => {
const values = $values.map((i, value) => Cypress.$(value).text())
expect(values.get()).to.deep.eq([
"age",
"155",
"20",
"49",
"5",
"5347",
"31",
])
})
})
it("groups the view by group", () => {
cy.contains("Group By").click()
cy.get("select").select("group")
cy.contains("Save").click()
cy.contains("Students").should("be.visible")
cy.contains("Teachers").should("be.visible")
cy.get("tbody tr")
.first()
.find("td")
.should($values => {
const values = $values.map((i, value) => Cypress.$(value).text()) const values = $values.map((i, value) => Cypress.$(value).text())
expect(values.get()).to.deep.eq([ expect(values.get()).to.deep.eq([
"Students", "Students",
@ -94,24 +103,30 @@ context('Create a View', () => {
"25", "25",
"3", "3",
"1650", "1650",
"23.333333333333332" "23.333333333333332",
]) ])
}) })
}) })
it('renames a view', () => { it("renames a view", () => {
cy.contains("[data-cy=model-nav-item]", "Test View").find(".ri-more-line").click() cy.contains("[data-cy=model-nav-item]", "Test View")
cy.contains("Edit").click() .find(".ri-more-line")
cy.get("[placeholder='View Name']").type(" Updated") .click()
cy.contains("Edit").click()
cy.get(".menu-container").within(() => {
cy.get("input").type(" Updated")
cy.contains("Save").click() cy.contains("Save").click()
cy.contains("Test View Updated").should("be.visible")
}) })
cy.contains("Test View Updated").should("be.visible")
})
it('deletes a view', () => { it("deletes a view", () => {
cy.contains("[data-cy=model-nav-item]", "Test View Updated").click() cy.contains("[data-cy=model-nav-item]", "Test View Updated").click()
cy.contains("[data-cy=model-nav-item]", "Test View Updated").find(".ri-more-line").click() cy.contains("[data-cy=model-nav-item]", "Test View Updated")
cy.contains("Delete").click() .find(".ri-more-line")
cy.get(".content").contains("button", "Delete").click() .click()
cy.contains("TestView Updated").should("not.be.visible") cy.contains("Delete").click()
}) cy.contains("Delete View").click()
cy.contains("TestView Updated").should("not.be.visible")
})
}) })

View file

@ -59,16 +59,21 @@ Cypress.Commands.add("createApp", name => {
Cypress.Commands.add("createTestTableWithData", () => { Cypress.Commands.add("createTestTableWithData", () => {
cy.createTable("dog") cy.createTable("dog")
cy.addColumn("dog", "name", "Plain Text") cy.addColumn("dog", "name", "Text")
cy.addColumn("dog", "age", "Number") cy.addColumn("dog", "age", "Number")
}) })
Cypress.Commands.add("createTable", tableName => { Cypress.Commands.add("createTable", tableName => {
// Enter model name // Enter model name
cy.contains("Create New Table").click() cy.contains("Create New Table").click()
cy.get("[placeholder='Table Name']").type(tableName) cy.get(".modal").within(() => {
cy.get("input")
cy.contains("Save").click() .first()
.type(tableName)
cy.get(".buttons")
.contains("Create")
.click()
})
cy.contains(tableName).should("be.visible") cy.contains(tableName).should("be.visible")
}) })
@ -77,25 +82,31 @@ Cypress.Commands.add("addColumn", (tableName, columnName, type) => {
cy.contains(tableName).click() cy.contains(tableName).click()
cy.contains("Create New Column").click() cy.contains("Create New Column").click()
cy.get("[placeholder=Name]").type(columnName) // Configure column
cy.get("select").select(type) cy.get(".menu-container").within(() => {
cy.get("input")
cy.contains("Save Column") .first()
.type(columnName)
cy.contains("Save").click() cy.get("select").select(type)
cy.contains("Save").click()
})
}) })
Cypress.Commands.add("addRecord", values => { Cypress.Commands.add("addRecord", values => {
cy.contains("Create New Row").click() cy.contains("Create New Row").click()
for (let i = 0; i < values.length; i++) { cy.get(".modal").within(() => {
cy.get(".actions input") for (let i = 0; i < values.length; i++) {
.eq(i) cy.get("input")
.type(values[i]) .eq(i)
} .type(values[i])
}
// Save // Save
cy.contains("Save").click() cy.get(".buttons")
.contains("Create")
.click()
})
}) })
Cypress.Commands.add("createUser", (username, password, accessLevel) => { Cypress.Commands.add("createUser", (username, password, accessLevel) => {
@ -114,7 +125,9 @@ Cypress.Commands.add("createUser", (username, password, accessLevel) => {
.select(accessLevel) .select(accessLevel)
// Save // Save
cy.get(".create-button > button").click() cy.get(".inputs")
.contains("Create")
.click()
}) })
Cypress.Commands.add("addHeadlineComponent", text => { Cypress.Commands.add("addHeadlineComponent", text => {
@ -138,12 +151,12 @@ Cypress.Commands.add("navigateToFrontend", () => {
}) })
Cypress.Commands.add("createScreen", (screenName, route) => { Cypress.Commands.add("createScreen", (screenName, route) => {
cy.get(".newscreen").click() cy.contains("Create New Screen").click()
cy.get("[data-cy=new-screen-dialog] input:first").type(screenName) cy.get(".modal").within(() => {
if (route) { cy.get("input:first").type(screenName)
cy.get("[data-cy=new-screen-dialog] input:last").type(route) if (route) {
} cy.get("input:last").type(route)
cy.get("[data-cy=create-screen-footer]").within(() => { }
cy.contains("Create Screen").click() cy.contains("Create Screen").click()
}) })
cy.get(".nav-items-container").within(() => { cy.get(".nav-items-container").within(() => {

View file

@ -63,7 +63,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.34.6", "@budibase/bbui": "^1.41.0",
"@budibase/client": "^0.1.25", "@budibase/client": "^0.1.25",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@fortawesome/fontawesome-free": "^5.14.0", "@fortawesome/fontawesome-free": "^5.14.0",
@ -79,7 +79,6 @@
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-loading-spinners": "^0.1.1", "svelte-loading-spinners": "^0.1.1",
"svelte-portal": "^0.1.0", "svelte-portal": "^0.1.0",
"svelte-simple-modal": "^0.4.2",
"yup": "^0.29.2" "yup": "^0.29.2"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,9 +1,8 @@
<script> <script>
import Modal from "svelte-simple-modal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { Router, basepath } from "@sveltech/routify" import { Router, basepath } from "@sveltech/routify"
import { routes } from "../routify/routes" import { routes } from "../routify/routes"
import { store, initialise } from "builderStore" import { initialise } from "builderStore"
import NotificationDisplay from "components/common/Notification/NotificationDisplay.svelte" import NotificationDisplay from "components/common/Notification/NotificationDisplay.svelte"
onMount(async () => { onMount(async () => {

View file

@ -158,4 +158,8 @@
.bb__alert--danger { .bb__alert--danger {
background: #fef4f6; background: #fef4f6;
color: #f0506e; color: #f0506e;
}
a {
text-decoration: none;
} }

View file

@ -37,9 +37,9 @@ export default function({ componentInstanceId, screen, components, models }) {
.filter(isInstanceInSharedContext(walkResult)) .filter(isInstanceInSharedContext(walkResult))
.map(componentInstanceToBindable(walkResult)), .map(componentInstanceToBindable(walkResult)),
...walkResult.target._contexts ...(walkResult.target?._contexts
.map(contextToBindables(models, walkResult)) .map(contextToBindables(models, walkResult))
.flat(), .flat() ?? []),
] ]
} }
@ -73,6 +73,15 @@ const componentInstanceToBindable = walkResult => i => {
const contextToBindables = (models, walkResult) => context => { const contextToBindables = (models, walkResult) => context => {
const contextParentPath = getParentPath(walkResult, context) const contextParentPath = getParentPath(walkResult, context)
const isModel = context.model?.isModel || typeof context.model === "string"
const modelId =
typeof context.model === "string" ? context.model : context.model.modelId
const model = models.find(model => model._id === modelId)
// Avoid crashing whenever no data source has been selected
if (model == null) {
return []
}
const newBindable = key => ({ const newBindable = key => ({
type: "context", type: "context",
@ -80,15 +89,12 @@ const contextToBindables = (models, walkResult) => context => {
// how the binding expression persists, and is used in the app at runtime // how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${contextParentPath}data.${key}`, runtimeBinding: `${contextParentPath}data.${key}`,
// how the binding exressions looks to the user of the builder // how the binding exressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${context.model.label}.${key}`, readableBinding: `${context.instance._instanceName}.${model.name}.${key}`,
}) })
// see ModelViewSelect.svelte for the format of context.model // see ModelViewSelect.svelte for the format of context.model
// ... this allows us to bind to Model scheams, or View schemas // ... this allows us to bind to Model schemas, or View schemas
const model = models.find(m => m._id === context.model.modelId) const schema = isModel ? model.schema : model.views[context.model.name].schema
const schema = context.model.isModel
? model.schema
: model.views[context.model.name].schema
return ( return (
Object.keys(schema) Object.keys(schema)

View file

@ -51,9 +51,9 @@
<style> <style>
section { section {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
justify-content: center; justify-content: flex-start;
align-items: flex-start; align-items: center;
overflow: auto; overflow: auto;
height: 100%; height: 100%;
position: relative; position: relative;

View file

@ -3,6 +3,7 @@
import Arrow from "./Arrow.svelte" import Arrow from "./Arrow.svelte"
import { flip } from "svelte/animate" import { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition" import { fade, fly } from "svelte/transition"
import { automationStore } from "builderStore"
export let automation export let automation
export let onSelect export let onSelect
@ -17,8 +18,16 @@
blocks = blocks.concat(automation.definition.steps || []) blocks = blocks.concat(automation.definition.steps || [])
} }
} }
$: automationCount = $automationStore.automations?.length ?? 0
</script> </script>
{#if automationCount === 0}
<i>Create your first automation to get started</i>
{:else if automation == null}
<i>Select an automation to edit</i>
{:else if !blocks.length}
<i>Add some steps to your automation to get started</i>
{/if}
<section class="canvas"> <section class="canvas">
{#each blocks as block, idx (block.id)} {#each blocks as block, idx (block.id)}
<div <div
@ -35,6 +44,13 @@
</section> </section>
<style> <style>
i {
font-size: var(--font-size-xl);
color: var(--grey-4);
padding: var(--spacing-xl) 40px;
align-self: flex-start;
}
section { section {
position: absolute; position: absolute;
padding: 40px; padding: 40px;

View file

@ -1,32 +1,20 @@
<script> <script>
import Modal from "svelte-simple-modal" import { onMount } from "svelte"
import { notifier } from "builderStore/store/notifications" import { automationStore } from "builderStore"
import { onMount, getContext } from "svelte"
import { backendUiStore, automationStore } from "builderStore"
import CreateAutomationModal from "./CreateAutomationModal.svelte" import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Button } from "@budibase/bbui" import { Button, Modal } from "@budibase/bbui"
const { open, close } = getContext("simple-modal") let modal
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id $: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
function newAutomation() {
open(
CreateAutomationModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
onMount(() => { onMount(() => {
automationStore.actions.fetch() automationStore.actions.fetch()
}) })
</script> </script>
<section> <section>
<Button purple wide on:click={newAutomation}>Create New Automation</Button> <Button primary wide on:click={modal.show}>Create New Automation</Button>
<ul> <ul>
{#each $automationStore.automations as automation} {#each $automationStore.automations as automation}
<li <li
@ -39,6 +27,9 @@
{/each} {/each}
</ul> </ul>
</section> </section>
<Modal bind:this={modal}>
<CreateAutomationModal />
</Modal>
<style> <style>
section { section {
@ -57,7 +48,7 @@
color: var(--grey-6); color: var(--grey-6);
} }
i.live { i.live {
color: var(--purple); color: var(--ink);
} }
li { li {

View file

@ -1,84 +1,49 @@
<script> <script>
import { store, backendUiStore, automationStore } from "builderStore" import { store, backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import ActionButton from "components/common/ActionButton.svelte" import { Input, ModalContent } from "@budibase/bbui"
import { Input } from "@budibase/bbui"
import analytics from "analytics" import analytics from "analytics"
export let onClosed
let name let name
$: valid = !!name $: valid = !!name
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId $: appId = $store.appId
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
async function createAutomation() { async function createAutomation() {
await automationStore.actions.create({ await automationStore.actions.create({
name, name,
instanceId, instanceId,
}) })
onClosed()
notifier.success(`Automation ${name} created.`) notifier.success(`Automation ${name} created.`)
analytics.captureEvent("Automation Created", { name }) analytics.captureEvent("Automation Created", { name })
} }
</script> </script>
<div class="container"> <ModalContent
<header> title="Create Automation"
<i class="ri-stackshare-line" /> confirmText="Create"
Create Automation onConfirm={createAutomation}
</header> disabled={!valid}>
<div class="content"> <Input bind:value={name} label="Name" />
<Input bind:value={name} label="Name" /> <div slot="footer">
</div> <a
<footer> target="_blank"
<a href="https://docs.budibase.com"> href="https://docs.budibase.com/automate/introduction-to-automate">
<i class="ri-information-line" /> <i class="ri-information-line" />
<span>Learn about automations</span> <span>Learn about automations</span>
</a> </a>
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton> </div>
<ActionButton disabled={!valid} on:click={createAutomation}> </ModalContent>
Save
</ActionButton>
</footer>
</div>
<style> <style>
.container { a {
padding: var(--spacing-xl);
}
header {
font-size: var(--font-size-xl);
color: var(--ink);
font-weight: bold;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
header i {
margin-right: var(--spacing-m);
font-size: 20px;
background: var(--purple);
color: var(--white);
padding: var(--spacing-s);
border-radius: var(--border-radius-m);
display: inline-block;
}
.content {
padding: var(--spacing-xl) 0;
}
footer {
display: grid;
grid-auto-flow: column;
grid-gap: var(--spacing-m);
grid-auto-columns: 3fr 1fr 1fr;
}
footer a {
color: var(--ink); color: var(--ink);
font-size: 14px; font-size: 14px;
vertical-align: middle; vertical-align: middle;
@ -86,10 +51,10 @@
align-items: center; align-items: center;
text-decoration: none; text-decoration: none;
} }
footer a span { a span {
text-decoration: underline; text-decoration: underline;
} }
footer i { i {
font-size: 20px; font-size: 20px;
margin-right: var(--spacing-m); margin-right: var(--spacing-m);
text-decoration: none; text-decoration: none;

View file

@ -2,11 +2,13 @@
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import AutomationList from "./AutomationList/AutomationList.svelte" import AutomationList from "./AutomationList/AutomationList.svelte"
import BlockList from "./BlockList/BlockList.svelte" import BlockList from "./BlockList/BlockList.svelte"
import { Heading } from "@budibase/bbui"
import { Spacer } from "@budibase/bbui"
let selectedTab = "AUTOMATIONS" let selectedTab = "AUTOMATIONS"
</script> </script>
<header> <Heading black small>
<span <span
data-cy="automation-list" data-cy="automation-list"
class="hoverable automation-header" class="hoverable automation-header"
@ -23,7 +25,8 @@
Steps Steps
</span> </span>
{/if} {/if}
</header> </Heading>
<Spacer medium />
{#if selectedTab === 'AUTOMATIONS'} {#if selectedTab === 'AUTOMATIONS'}
<AutomationList /> <AutomationList />
{:else if selectedTab === 'ADD'} {:else if selectedTab === 'ADD'}

View file

@ -48,7 +48,7 @@
<div class="block-label">{block.name}</div> <div class="block-label">{block.name}</div>
{#each inputs as [key, value]} {#each inputs as [key, value]}
<div class="bb-margin-xl block-field"> <div class="bb-margin-xl block-field">
<div class="field-label">{value.title}</div> <Label extraSmall grey>{value.title}</Label>
{#if value.type === 'string' && value.enum} {#if value.type === 'string' && value.enum}
<Select bind:value={block.inputs[key]} thin secondary> <Select bind:value={block.inputs[key]} thin secondary>
<option value="">Choose an option</option> <option value="">Choose an option</option>
@ -80,15 +80,6 @@
display: grid; display: grid;
} }
.field-label {
color: var(--ink);
margin-bottom: 12px;
display: flex;
font-size: 14px;
font-weight: 500;
font-family: sans-serif;
}
.block-label { .block-label {
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;

View file

@ -1,89 +0,0 @@
<script>
import { store, backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ActionButton from "components/common/ActionButton.svelte"
export let onClosed
let name
$: valid = !!name
$: instanceId = $backendUiStore.selectedDatabase._id
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
automation: $automationStore.selectedAutomation.automation,
})
onClosed()
notifier.danger("Automation deleted.")
}
</script>
<header>
<i class="ri-stackshare-line" />
Delete Automation
</header>
<div>
<p>
Are you sure you want to delete this automation? This action can't be
undone.
</p>
</div>
<footer>
<a href="https://docs.budibase.com">
<i class="ri-information-line" />
Learn about automations
</a>
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton alert on:click={deleteAutomation}>Delete</ActionButton>
</footer>
<style>
header {
font-size: 24px;
color: var(--ink);
font-weight: bold;
padding: 30px;
}
header i {
margin-right: 10px;
font-size: 20px;
background: var(--blue-light);
color: var(--grey-4);
padding: 8px;
}
div {
padding: 0 30px 30px 30px;
}
label {
font-size: 18px;
font-weight: 500;
}
footer {
display: grid;
grid-auto-flow: column;
grid-gap: 5px;
grid-auto-columns: 3fr 1fr 1fr;
padding: 20px;
background: var(--grey-1);
border-radius: 0.5rem;
}
footer a {
color: var(--primary);
font-size: 14px;
vertical-align: middle;
display: flex;
align-items: center;
}
footer i {
font-size: 20px;
margin-right: 10px;
}
</style>

View file

@ -28,42 +28,34 @@
</div> </div>
{#if schemaFields.length} {#if schemaFields.length}
<div class="bb-margin-xl block-field"> <div class="schema-fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
<div class="bb-margin-xl capitalise"> {#if schemaHasOptions(schema)}
{#if schemaHasOptions(schema)} <Select label={field} thin secondary bind:value={value[field]}>
<div class="field-label">{field}</div> <option value="">Choose an option</option>
<Select thin secondary bind:value={value[field]}> {#each schema.constraints.inclusion as option}
<option value="">Choose an option</option> <option value={option}>{option}</option>
{#each schema.constraints.inclusion as option} {/each}
<option value={option}>{option}</option> </Select>
{/each} {:else if schema.type === 'string' || schema.type === 'number'}
</Select> <BindableInput
{:else if schema.type === 'string' || schema.type === 'number'} thin
<BindableInput bind:value={value[field]}
thin label={field}
bind:value={value[field]} type="string"
label={field} {bindings} />
type="string" {/if}
{bindings} />
{/if}
</div>
{/each} {/each}
</div> </div>
{/if} {/if}
<style> <style>
.field-label { .schema-fields {
color: var(--ink); display: grid;
margin-bottom: 12px; grid-gap: var(--spacing-xl);
display: flex; margin-top: var(--spacing-xl);
font-size: 14px;
font-weight: 500;
font-family: sans-serif;
} }
.schema-fields :global(label) {
.capitalise :global(label),
.field-label {
text-transform: capitalize; text-transform: capitalize;
} }
</style> </style>

View file

@ -1,27 +1,19 @@
<script> <script>
import { getContext } from "svelte"
import { backendUiStore, automationStore } from "builderStore" import { backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import AutomationBlockSetup from "./AutomationBlockSetup.svelte" import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
import DeleteAutomationModal from "./DeleteAutomationModal.svelte"
import { Button, Input, Label } from "@budibase/bbui" import { Button, Input, Label } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
const { open, close } = getContext("simple-modal")
let selectedTab = "SETUP" let selectedTab = "SETUP"
let confirmDeleteDialog
$: instanceId = $backendUiStore.selectedDatabase._id
$: automation = $automationStore.selectedAutomation?.automation $: automation = $automationStore.selectedAutomation?.automation
$: allowDeleteBlock = $: allowDeleteBlock =
$automationStore.selectedBlock?.type !== "TRIGGER" || $automationStore.selectedBlock?.type !== "TRIGGER" ||
!automation?.definition?.steps?.length !automation?.definition?.steps?.length
$: name = automation?.name ?? ""
function deleteAutomation() {
open(
DeleteAutomationModal,
{ onClosed: close },
{ styleContent: { padding: "0" } }
)
}
function deleteAutomationBlock() { function deleteAutomationBlock() {
automationStore.actions.deleteAutomationBlock( automationStore.actions.deleteAutomationBlock(
@ -42,11 +34,19 @@
async function saveAutomation() { async function saveAutomation() {
await automationStore.actions.save({ await automationStore.actions.save({
instanceId: $backendUiStore.selectedDatabase._id, instanceId,
automation, automation,
}) })
notifier.success(`Automation ${automation.name} saved.`) notifier.success(`Automation ${automation.name} saved.`)
} }
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
automation,
})
notifier.success("Automation deleted.")
}
</script> </script>
<section> <section>
@ -93,11 +93,18 @@
on:click={saveAutomation}> on:click={saveAutomation}>
Save Automation Save Automation
</Button> </Button>
<Button red wide on:click={deleteAutomation}>Delete Automation</Button> <Button red wide on:click={() => confirmDeleteDialog.show()}>
Delete Automation
</Button>
{/if} {/if}
</div> </div>
</section> </section>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete the automation '${name}'?`}
okText="Delete Automation"
onOk={deleteAutomation} />
<style> <style>
section { section {

View file

@ -0,0 +1,39 @@
<script>
import { backendUiStore } from "builderStore"
import CreateRowButton from "./buttons/CreateRowButton.svelte"
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
import CreateViewButton from "./buttons/CreateViewButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
import * as api from "./api"
import Table from "./Table.svelte"
let data = []
let loading = false
$: title = $backendUiStore.selectedModel.name
$: schema = $backendUiStore.selectedModel.schema
$: modelView = {
schema,
name: $backendUiStore.selectedView.name,
}
// Fetch records for specified model
$: {
if ($backendUiStore.selectedView?.name?.startsWith("all_")) {
loading = true
api.fetchDataForView($backendUiStore.selectedView).then(records => {
data = records || []
loading = false
})
}
}
</script>
<Table {title} {schema} {data} allowEditing={true} {loading}>
<CreateColumnButton />
{#if Object.keys(schema).length > 0}
<CreateRowButton />
<CreateViewButton />
<ExportButton view={modelView} />
{/if}
</Table>

View file

@ -0,0 +1,34 @@
<script>
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
export let meta
export let value = meta.type === "boolean" ? false : ""
$: type = meta.type
$: label = capitalise(meta.name)
</script>
{#if type === 'options'}
<Select thin secondary {label} data-cy="{meta.name}-select" bind:value>
<option value="">Choose an option</option>
{#each meta.constraints.inclusion as opt}
<option value={opt}>{opt}</option>
{/each}
</Select>
{:else if type === 'datetime'}
<DatePicker {label} bind:value />
{:else if type === 'attachment'}
<div>
<Label extraSmall grey forAttr={'dropzone-label'}>{label}</Label>
<Dropzone bind:files={value} />
</div>
{:else if type === 'boolean'}
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
{:else if type === 'link'}
<LinkedRecordSelector bind:linkedRecords={value} schema={meta} />
{:else}
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
{/if}

View file

@ -0,0 +1,40 @@
<script>
import api from "builderStore/api"
import Table from "./Table.svelte"
import { onMount } from "svelte"
import { backendUiStore } from "builderStore"
export let modelId
export let recordId
export let fieldName
let record
let title
$: data = record?.[fieldName] ?? []
$: linkedModelId = data?.length ? data[0].modelId : null
$: linkedModel = $backendUiStore.models.find(
model => model._id === linkedModelId
)
$: schema = linkedModel?.schema
$: model = $backendUiStore.models.find(model => model._id === modelId)
$: fetchData(modelId, recordId)
$: {
let recordLabel = record?.[model?.primaryDisplay]
if (recordLabel) {
title = `${recordLabel} - ${fieldName}`
} else {
title = fieldName
}
}
async function fetchData(modelId, recordId) {
const QUERY_VIEW_URL = `/api/${modelId}/${recordId}/enrich`
const response = await api.get(QUERY_VIEW_URL)
record = await response.json()
}
</script>
{#if record && record._id === recordId}
<Table {title} {schema} {data} />
{/if}

View file

@ -1,71 +1,60 @@
<script> <script>
import { goto, params } from "@sveltech/routify"
import { onMount } from "svelte" import { onMount } from "svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import fsort from "fast-sort" import fsort from "fast-sort"
import getOr from "lodash/fp/getOr" import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Button, Icon } from "@budibase/bbui" import { Button, Icon } from "@budibase/bbui"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import AttachmentList from "./AttachmentList.svelte" import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte" import TablePagination from "./TablePagination.svelte"
import CreateEditRecordModal from "./modals/CreateEditRecordModal.svelte"
import RowPopover from "./buttons/CreateRowButton.svelte"
import ColumnPopover from "./buttons/CreateColumnButton.svelte"
import ViewPopover from "./buttons/CreateViewButton.svelte"
import ColumnHeaderPopover from "./popovers/ColumnPopover.svelte"
import EditRowPopover from "./popovers/RowPopover.svelte"
import CalculationPopover from "./buttons/CalculateButton.svelte"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte"
import ExportPopover from "./popovers/Export.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte"
import * as api from "./api"
const ITEMS_PER_PAGE = 10 const ITEMS_PER_PAGE = 10
// Internal headers we want to hide from the user
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
let modalOpen = false export let schema = []
let data = [] export let data = []
let headers = [] export let title
export let allowEditing = false
export let loading = false
let currentPage = 0 let currentPage = 0
let search
let loading
$: {
if (
$backendUiStore.selectedView &&
$backendUiStore.selectedView.name.startsWith("all_")
) {
loading = true
api.fetchDataForView($backendUiStore.selectedView).then(records => {
data = records || []
loading = false
})
}
}
$: columns = schema ? Object.keys(schema) : []
$: sort = $backendUiStore.sort $: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data $: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
$: paginatedData = sorted $: paginatedData =
? sorted.slice( sorted && sorted.length
currentPage * ITEMS_PER_PAGE, ? sorted.slice(
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE currentPage * ITEMS_PER_PAGE,
) currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
: [] )
: []
$: modelId = data?.length ? data[0].modelId : null
$: headers = Object.keys($backendUiStore.selectedModel.schema) function selectRelationship(record, fieldName) {
.sort() if (!record?.[fieldName]?.length) {
.filter(id => !INTERNAL_HEADERS.includes(id)) return
}
$: schema = $backendUiStore.selectedModel.schema $goto(
$: modelView = { `/${$params.application}/backend/model/${modelId}/relationship/${record._id}/${fieldName}`
schema: $backendUiStore.selectedModel.schema, )
name: $backendUiStore.selectedView.name,
} }
</script> </script>
<section> <section>
<div class="table-controls"> <div class="table-controls">
<h2 class="title"> <h2 class="title">
<span>{$backendUiStore.selectedModel.name}</span> <span>{title}</span>
{#if loading} {#if loading}
<div transition:fade> <div transition:fade>
<Spinner size="10" /> <Spinner size="10" />
@ -73,41 +62,54 @@
{/if} {/if}
</h2> </h2>
<div class="popovers"> <div class="popovers">
<ColumnPopover /> <slot />
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
<RowPopover />
<ViewPopover />
<ExportPopover view={modelView} />
{/if}
</div> </div>
</div> </div>
<table class="bb-table"> <table class="bb-table">
<thead> <thead>
<tr> <tr>
<th class="edit-header"> {#if allowEditing}
<div>Edit</div> <th class="edit-header">
</th> <div>Edit</div>
{#each headers as header} </th>
{/if}
{#each columns as header}
<th> <th>
<ColumnHeaderPopover {#if allowEditing}
field={$backendUiStore.selectedModel.schema[header]} /> <ColumnHeaderPopover field={schema[header]} />
{:else}
<div class="header">{header}</div>
{/if}
</th> </th>
{/each} {/each}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#if paginatedData.length === 0} {#if paginatedData.length === 0}
<div class="no-data">No Data.</div> {#if allowEditing}
<td class="no-border">No data.</td>
{/if}
{#each columns as header, idx}
<td class="no-border">
{#if idx === 0 && !allowEditing}No data.{/if}
</td>
{/each}
{/if} {/if}
{#each paginatedData as row} {#each paginatedData as row}
<tr> <tr>
<td> {#if allowEditing}
<EditRowPopover {row} /> <td>
</td> <EditRowPopover {row} />
{#each headers as header} </td>
{/if}
{#each columns as header}
<td> <td>
{#if schema[header].type === 'link'} {#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} /> <div
class:link={row[header] && row[header].length}
on:click={() => selectRelationship(row, header)}>
{row[header] ? row[header].length : 0} related row(s)
</div>
{:else if schema[header].type === 'attachment'} {:else if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []} /> <AttachmentList files={row[header] || []} />
{:else}{getOr('', header, row)}{/if} {:else}{getOr('', header, row)}{/if}
@ -125,19 +127,17 @@
</section> </section>
<style> <style>
section {
margin-bottom: 20px;
}
.title { .title {
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
text-transform: capitalize; text-transform: capitalize;
margin-top: 0;
display: flex; display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center; align-items: center;
} }
.title > span { .title > span {
margin-right: var(--spacing-xs); margin-right: var(--spacing-xs);
} }
@ -145,54 +145,54 @@
table { table {
border: 1px solid var(--grey-4); border: 1px solid var(--grey-4);
background: #fff; background: #fff;
border-radius: 3px;
border-collapse: collapse; border-collapse: collapse;
margin-top: 0;
} }
thead { thead {
height: 40px;
background: var(--grey-3); background: var(--grey-3);
border: 1px solid var(--grey-4); border: 1px solid var(--grey-4);
} }
thead th { thead th {
color: var(--ink); color: var(--ink);
text-transform: capitalize;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
transition: 0.5s all; transition: 0.5s all;
vertical-align: middle; vertical-align: middle;
height: 48px;
padding-top: 0;
padding-bottom: 0;
} }
.edit-header { thead th:hover {
width: 100px;
cursor: default;
}
.edit-header:hover {
color: var(--ink);
}
th:hover {
color: var(--blue); color: var(--blue);
cursor: pointer; cursor: pointer;
} }
.header {
text-transform: capitalize;
}
td { td {
max-width: 200px; max-width: 200px;
text-overflow: ellipsis; text-overflow: ellipsis;
border: 1px solid var(--grey-4); border: 1px solid var(--grey-4);
overflow: hidden; white-space: nowrap;
white-space: pre;
box-sizing: border-box; box-sizing: border-box;
padding: var(--spacing-l) var(--spacing-m);
font-size: var(--font-size-xs);
}
td.no-border {
border: none;
} }
tbody tr { tbody tr {
border-bottom: 1px solid var(--grey-4); border-bottom: 1px solid var(--grey-4);
transition: 0.3s background-color; transition: 0.3s background-color;
color: var(--ink); color: var(--ink);
font-size: 12px;
} }
tbody tr:hover { tbody tr:hover {
@ -207,7 +207,26 @@
display: flex; display: flex;
} }
.no-data { :global(.popovers > div) {
padding: 14px; margin-right: var(--spacing-m);
margin-bottom: var(--spacing-xl);
}
.edit-header {
width: 60px;
}
.edit-header:hover {
cursor: default;
color: var(--ink);
}
.link {
text-decoration: underline;
}
.link:hover {
color: var(--grey-6);
cursor: pointer;
} }
</style> </style>

View file

@ -1,14 +1,11 @@
<script> <script>
import { backendUiStore } from "builderStore"
export let data export let data
export let currentPage export let currentPage
export let pageItemCount export let pageItemCount
export let ITEMS_PER_PAGE export let ITEMS_PER_PAGE
let numPages = 0 let numPages = 0
$: numPages = Math.ceil((data?.length ?? 0) / ITEMS_PER_PAGE)
$: numPages = Math.ceil(data.length / ITEMS_PER_PAGE)
const next = () => { const next = () => {
if (currentPage + 1 === numPages) return if (currentPage + 1 === numPages) return
@ -27,8 +24,7 @@
<div class="pagination"> <div class="pagination">
<div class="pagination__buttons"> <div class="pagination__buttons">
<button on:click={previous}>Previous</button> <button on:click={previous} disabled={currentPage === 0}>&lt;</button>
<button on:click={next}>Next</button>
{#each Array(numPages) as _, idx} {#each Array(numPages) as _, idx}
<button <button
class:selected={idx === currentPage} class:selected={idx === currentPage}
@ -36,8 +32,19 @@
{idx + 1} {idx + 1}
</button> </button>
{/each} {/each}
<button
on:click={next}
disabled={currentPage === numPages - 1 || numPages === 0}>
&gt;
</button>
</div> </div>
<p>Showing {pageItemCount} of {data.length} entries</p>
<p>
{#if numPages > 1}
Showing {ITEMS_PER_PAGE * currentPage + 1} - {ITEMS_PER_PAGE * currentPage + pageItemCount}
of {data.length} rows
{:else if numPages === 1}Showing all {data.length} row(s){/if}
</p>
</div> </div>
<style> <style>
@ -51,26 +58,36 @@
.pagination__buttons { .pagination__buttons {
display: flex; display: flex;
border: 1px solid var(--grey-4);
border-radius: var(--border-radius-s);
overflow: hidden;
} }
.pagination__buttons button { .pagination__buttons button {
display: inline-block; display: inline-block;
padding: 10px; padding: var(--spacing-s) var(--spacing-m);
margin: 0; margin: 0;
background: #fff; background: #fff;
border: 1px solid var(--grey-4); border: none;
outline: none;
border-right: 1px solid var(--grey-4);
text-transform: capitalize; text-transform: capitalize;
border-radius: 3px;
min-width: 20px; min-width: 20px;
transition: 0.3s background-color; transition: 0.3s background-color;
} }
.pagination__buttons button:last-child {
border-right: none;
}
.pagination__buttons button:hover { .pagination__buttons button:hover {
cursor: pointer; cursor: pointer;
background-color: var(--grey-1); background-color: var(--grey-1);
} }
.pagination__buttons button.selected {
background: var(--grey-2);
}
.selected { p {
color: var(--blue); font-size: var(--font-size-s);
margin: var(--spacing-xl) 0;
} }
</style> </style>

View file

@ -0,0 +1,44 @@
<script>
import api from "builderStore/api"
import Table from "./Table.svelte"
import CalculateButton from "./buttons/CalculateButton.svelte"
import GroupByButton from "./buttons/GroupByButton.svelte"
import FilterButton from "./buttons/FilterButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
export let view = {}
let data = []
$: name = view.name
// Fetch records for specified view
$: {
if (!name.startsWith("all_")) {
fetchViewData(name, view.field, view.groupBy)
}
}
async function fetchViewData(name, field, groupBy) {
const params = new URLSearchParams()
if (field) {
params.set("field", field)
params.set("stats", true)
}
if (groupBy) {
params.set("group", groupBy)
}
const QUERY_VIEW_URL = `/api/views/${name}?${params}`
const response = await api.get(QUERY_VIEW_URL)
data = await response.json()
}
</script>
<Table title={decodeURI(name)} schema={view.schema} {data}>
<FilterButton {view} />
<CalculateButton {view} />
{#if view.calculation}
<GroupByButton {view} />
{/if}
<ExportButton {view} />
</Table>

View file

@ -0,0 +1,19 @@
<script>
import { Popover, TextButton, Icon } from "@budibase/bbui"
import CalculatePopover from "../popovers/CalculatePopover.svelte"
export let view = {}
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show} active={!!view.field}>
<Icon name="calculate" />
Calculate
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<CalculatePopover {view} onClosed={dropdown.hide} />
</Popover>

View file

@ -1,20 +1,21 @@
<script> <script>
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
import CreateEditRecord from "../modals/CreateEditRecord.svelte" import CreateEditColumnPopover from "../popovers/CreateEditColumnPopover.svelte"
let anchor let anchor
let dropdown let dropdown
let fieldName
</script> </script>
<div bind:this={anchor}> <div bind:this={anchor}>
<Button text small on:click={dropdown.show}> <Button text small on:click={dropdown.show}>
<Icon name="addrow" /> <Icon name="addcolumn" />
Create New Row Create New Column
</Button> </Button>
</div> </div>
<DropdownMenu bind:this={dropdown} {anchor} align="left"> <DropdownMenu bind:this={dropdown} {anchor} align="left">
<h5>Add New Row</h5> <h5>Create Column</h5>
<CreateEditRecord onClosed={dropdown.hide} /> <CreateEditColumnPopover onClosed={dropdown.hide} />
</DropdownMenu> </DropdownMenu>
<style> <style>

View file

@ -0,0 +1,16 @@
<script>
import { TextButton as Button, Icon, Modal } from "@budibase/bbui"
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
let modal
</script>
<div>
<Button text small on:click={modal.show}>
<Icon name="addrow" />
Create New Row
</Button>
</div>
<Modal bind:this={modal}>
<CreateEditRecordModal />
</Modal>

View file

@ -0,0 +1,17 @@
<script>
import { Popover, TextButton, Icon } from "@budibase/bbui"
import CreateViewPopover from "../popovers/CreateViewPopover.svelte"
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show}>
<Icon name="view" />
Create New View
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<CreateViewPopover onClosed={dropdown.hide} />
</Popover>

View file

@ -0,0 +1,20 @@
<script>
import { TextButton, Icon, Popover } from "@budibase/bbui"
import api from "builderStore/api"
import ExportPopover from "../popovers/ExportPopover.svelte"
export let view
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show}>
<Icon name="download" />
Export
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<ExportPopover {view} onClosed={dropdown.hide} />
</Popover>

View file

@ -0,0 +1,23 @@
<script>
import { Popover, TextButton, Icon } from "@budibase/bbui"
import FilterPopover from "../popovers/FilterPopover.svelte"
export let view = {}
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton
text
small
on:click={dropdown.show}
active={view.filters && view.filters.length}>
<Icon name="filter" />
Filter
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<FilterPopover {view} onClosed={dropdown.hide} />
</Popover>

View file

@ -0,0 +1,19 @@
<script>
import { Popover, TextButton, Icon } from "@budibase/bbui"
import GroupByPopover from "../popovers/GroupByPopover.svelte"
export let view = {}
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<TextButton text small active={!!view.groupBy} on:click={dropdown.show}>
<Icon name="group" />
Group By
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<GroupByPopover {view} onClosed={dropdown.hide} />
</Popover>

View file

@ -0,0 +1,46 @@
<script>
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import RecordFieldControl from "../RecordFieldControl.svelte"
import * as api from "../api"
import { ModalContent } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let record = {}
let errors = []
$: creating = record?._id == null
$: model = record.modelId
? $backendUiStore.models.find(model => model._id === record?.modelId)
: $backendUiStore.selectedModel
$: modelSchema = Object.entries(model?.schema ?? {})
async function saveRecord() {
const recordResponse = await api.saveRecord(
{ ...record, modelId: model._id },
model._id
)
if (recordResponse.errors) {
errors = Object.keys(recordResponse.errors)
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
.flat()
// Prevent modal closing if there were errors
return false
}
notifier.success("Record saved successfully.")
backendUiStore.actions.records.save(recordResponse)
}
</script>
<ModalContent
title={creating ? 'Create Row' : 'Edit Row'}
confirmText={creating ? 'Create Row' : 'Save Row'}
onConfirm={saveRecord}>
<ErrorsBox {errors} />
{#each modelSchema as [key, meta]}
<div>
<RecordFieldControl {meta} bind:value={record[key]} />
</div>
{/each}
</ModalContent>

View file

@ -1,12 +1,5 @@
<script> <script>
import { import { Button, Input, Select } from "@budibase/bbui"
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import analytics from "analytics" import analytics from "analytics"
@ -19,9 +12,7 @@
] ]
export let view = {} export let view = {}
export let onClosed
let anchor
let dropdown
$: viewModel = $backendUiStore.models.find( $: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId ({ _id }) => _id === $backendUiStore.selectedView.modelId
@ -36,18 +27,12 @@
if (!view.calculation) view.calculation = "stats" if (!view.calculation) view.calculation = "stats"
backendUiStore.actions.views.save(view) backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`) notifier.success(`View ${view.name} saved.`)
onClosed()
analytics.captureEvent("Added View Calculate", { field: view.field }) analytics.captureEvent("Added View Calculate", { field: view.field })
dropdown.hide()
} }
</script> </script>
<div bind:this={anchor}> <div class="actions">
<TextButton text small on:click={dropdown.show} active={!!view.field}>
<Icon name="calculate" />
Calculate
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<h5>Calculate</h5> <h5>Calculate</h5>
<div class="input-group-row"> <div class="input-group-row">
<!-- <p>The</p> <!-- <p>The</p>
@ -66,30 +51,33 @@
{/each} {/each}
</Select> </Select>
</div> </div>
<div class="button-group"> <div class="footer">
<Button secondary on:click={dropdown.hide}>Cancel</Button> <Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button> <Button primary on:click={saveView}>Save</Button>
</div> </div>
</Popover> </div>
<style> <style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 { h5 {
margin-bottom: var(--spacing-l); margin: 0;
font-weight: 500; font-weight: 500;
} }
.button-group { .footer {
margin-top: var(--spacing-l);
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: var(--spacing-s); gap: var(--spacing-m);
} }
.input-group-row { .input-group-row {
display: grid; display: grid;
grid-template-columns: auto 1fr 20px 1fr; grid-template-columns: auto 1fr;
gap: var(--spacing-s); gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
align-items: center; align-items: center;
} }

View file

@ -2,17 +2,20 @@
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import CreateEditColumn from "../modals/CreateEditColumn.svelte" import CreateEditColumnPopover from "./CreateEditColumnPopover.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { notifier } from "../../../../builderStore/store/notifications"
export let field export let field
let anchor let anchor
let dropdown let dropdown
let editing let editing
let confirmDeleteDialog
$: sortColumn = $backendUiStore.sort && $backendUiStore.sort.column $: sortColumn = $backendUiStore.sort && $backendUiStore.sort.column
$: sortDirection = $backendUiStore.sort && $backendUiStore.sort.direction $: sortDirection = $backendUiStore.sort && $backendUiStore.sort.direction
$: type = field?.type
function showEditor() { function showEditor() {
editing = true editing = true
@ -23,8 +26,18 @@
editing = false editing = false
} }
function deleteField() { function showDelete() {
backendUiStore.actions.models.deleteField(field) dropdown.hide()
confirmDeleteDialog.show()
}
function deleteColumn() {
if (field.name === $backendUiStore.selectedModel.primaryDisplay) {
notifier.danger("You cannot delete the primary display column")
} else {
backendUiStore.actions.models.deleteField(field)
notifier.success("Column deleted")
}
hideEditor() hideEditor()
} }
@ -37,21 +50,23 @@
} }
</script> </script>
<div bind:this={anchor} on:click={dropdown.show}> <div class="container" bind:this={anchor} on:click={dropdown.show}>
{field.name} <span>{field.name}</span>
<Icon name="arrowdown" /> <Icon name="arrowdown" />
</div> </div>
<DropdownMenu bind:this={dropdown} {anchor} align="left"> <DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing} {#if editing}
<h5>Edit Column</h5> <h5>Edit Column</h5>
<CreateEditColumn onClosed={hideEditor} {field} /> <CreateEditColumnPopover onClosed={hideEditor} {field} />
{:else} {:else}
<ul> <ul>
<li data-cy="edit-column-header" on:click={showEditor}> {#if type !== 'link'}
<Icon name="edit" /> <li data-cy="edit-column-header" on:click={showEditor}>
Edit <Icon name="edit" />
</li> Edit
<li data-cy="delete-column-header" on:click={deleteField}> </li>
{/if}
<li data-cy="delete-column-header" on:click={showDelete}>
<Icon name="delete" /> <Icon name="delete" />
Delete Delete
</li> </li>
@ -70,8 +85,25 @@
</ul> </ul>
{/if} {/if}
</DropdownMenu> </DropdownMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete this column? Your data will be deleted and this action cannot be undone.`}
okText="Delete Column"
onOk={deleteColumn}
title="Confirm Delete" />
<style> <style>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
}
.container span {
text-transform: capitalize;
}
h5 { h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl); padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0; margin: 0;

View file

@ -0,0 +1,144 @@
<script>
import { onMount } from "svelte"
import {
Input,
TextArea,
Button,
Select,
Toggle,
Label,
} from "@budibase/bbui"
import { cloneDeep, merge } from "lodash/fp"
import { store, backendUiStore } from "builderStore"
import { FIELDS } from "constants/backend"
import { notifier } from "builderStore/store/notifications"
import ButtonGroup from "components/common/ButtonGroup.svelte"
import NumberBox from "components/common/NumberBox.svelte"
import ValuesList from "components/common/ValuesList.svelte"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import Checkbox from "components/common/Checkbox.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import DatePicker from "components/common/DatePicker.svelte"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import * as api from "../api"
let fieldDefinitions = cloneDeep(FIELDS)
export let onClosed
export let field = {
type: "string",
constraints: fieldDefinitions.STRING.constraints,
}
let originalName = field.name
$: modelOptions = $backendUiStore.models.filter(
model => model._id !== $backendUiStore.draftModel._id
)
$: required = !!field?.constraints?.presence
async function saveColumn() {
backendUiStore.update(state => {
backendUiStore.actions.models.saveField({
originalName,
field,
})
return state
})
onClosed()
}
function handleFieldConstraints(event) {
const { type, constraints } = fieldDefinitions[
event.target.value.toUpperCase()
]
field.type = type
field.constraints = constraints
}
function onChangeRequired(e) {
const req = e.target.checked
field.constraints.presence = req ? { allowEmpty: false } : false
required = req
}
</script>
<div class="actions">
<Input label="Name" thin bind:value={field.name} />
<Select
secondary
thin
label="Type"
on:change={handleFieldConstraints}
bind:value={field.type}>
{#each Object.values(fieldDefinitions) as field}
<option value={field.type}>{field.name}</option>
{/each}
</Select>
{#if field.type !== 'link'}
<Toggle
checked={required}
on:change={onChangeRequired}
thin
text="Required" />
{/if}
{#if field.type === 'string'}
<Input
thin
type="number"
label="Max Length"
bind:value={field.constraints.length.maximum} />
{:else if field.type === 'options'}
<ValuesList
label="Options (one per line)"
bind:values={field.constraints.inclusion} />
{:else if field.type === 'datetime'}
<DatePicker
label="Earliest"
bind:value={field.constraints.datetime.earliest} />
<DatePicker label="Latest" bind:value={field.constraints.datetime.latest} />
{:else if field.type === 'number'}
<Input
thin
type="number"
label="Min Value"
bind:value={field.constraints.numericality.greaterThanOrEqualTo} />
<Input
thin
type="number"
label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'}
<Select label="Table" thin secondary bind:value={field.modelId}>
<option value="">Choose an option</option>
{#each modelOptions as model}
<option value={model._id}>{model.name}</option>
{/each}
</Select>
<Input
label={`Column Name in Other Table`}
thin
bind:value={field.fieldName} />
{/if}
<footer>
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveColumn}>Save Column</Button>
</footer>
</div>
<style>
.actions {
padding: var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
min-width: 400px;
}
footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
</style>

View file

@ -1,19 +1,11 @@
<script> <script>
import { import { Button, Input, Select } from "@budibase/bbui"
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import analytics from "analytics" import analytics from "analytics"
let anchor export let onClosed
let dropdown
let name let name
let field let field
@ -36,45 +28,35 @@
field, field,
}) })
notifier.success(`View ${name} created`) notifier.success(`View ${name} created`)
dropdown.hide() onClosed()
analytics.captureEvent("View Created", { name }) analytics.captureEvent("View Created", { name })
$goto(`../../../view/${name}`) $goto(`../../../view/${name}`)
} }
</script> </script>
<div bind:this={anchor}> <div class="actions">
<TextButton text small on:click={dropdown.show}>
<Icon name="view" />
Create New View
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<h5>Create View</h5> <h5>Create View</h5>
<div class="input-group-column"> <Input label="View Name" thin bind:value={name} />
<Input placeholder="View Name" thin bind:value={name} /> <div class="footer">
</div> <Button secondary on:click={onClosed}>Cancel</Button>
<div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={saveView}>Save View</Button> <Button primary on:click={saveView}>Save View</Button>
</div> </div>
</Popover> </div>
<style> <style>
h5 { h5 {
margin-bottom: var(--spacing-l); margin: 0;
font-weight: 500; font-weight: 500;
} }
.button-group { .actions {
margin-top: var(--spacing-l); display: grid;
display: flex; grid-gap: var(--spacing-xl);
justify-content: flex-end;
gap: var(--spacing-s);
} }
.input-group-column { .footer {
display: flex; display: flex;
flex-direction: column; justify-content: flex-end;
gap: var(--spacing-s); gap: var(--spacing-m);
} }
</style> </style>

View file

@ -0,0 +1,61 @@
<script>
import api from "builderStore/api"
import { Button, Select } from "@budibase/bbui"
const FORMATS = [
{
name: "CSV",
key: "csv",
},
{
name: "JSON",
key: "json",
},
]
export let view
export let onClosed
let exportFormat = FORMATS[0].key
async function exportView() {
const response = await api.post(
`/api/views/export?format=${exportFormat}`,
view
)
const downloadInfo = await response.json()
onClosed()
window.location = downloadInfo.url
}
</script>
<div class="popover">
<h5>Export Data</h5>
<Select label="Format" secondary thin bind:value={exportFormat}>
{#each FORMATS as format}
<option value={format.key}>{format.name}</option>
{/each}
</Select>
<div class="footer">
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={exportView}>Export</Button>
</div>
</div>
<style>
.popover {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 {
margin: 0;
font-weight: 500;
}
.footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
</style>

View file

@ -1,13 +1,5 @@
<script> <script>
import { import { Button, Input, Select, DatePicker } from "@budibase/bbui"
Popover,
TextButton,
Button,
Icon,
Input,
Select,
DatePicker,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import analytics from "analytics" import analytics from "analytics"
@ -51,9 +43,7 @@
] ]
export let view = {} export let view = {}
export let onClosed
let anchor
let dropdown
$: viewModel = $backendUiStore.models.find( $: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId ({ _id }) => _id === $backendUiStore.selectedView.modelId
@ -63,7 +53,7 @@
function saveView() { function saveView() {
backendUiStore.actions.views.save(view) backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`) notifier.success(`View ${view.name} saved.`)
dropdown.hide() onClosed()
analytics.captureEvent("Added View Filter", { analytics.captureEvent("Added View Filter", {
filters: JSON.stringify(view.filters), filters: JSON.stringify(view.filters),
}) })
@ -115,96 +105,93 @@
} }
</script> </script>
<div bind:this={anchor}> <div class="actions">
<TextButton
text
small
on:click={dropdown.show}
active={view.filters && view.filters.length}>
<Icon name="filter" />
Filter
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<h5>Filter</h5> <h5>Filter</h5>
<div class="input-group-row"> {#if view.filters.length}
{#each view.filters as filter, idx} <div class="input-group-row">
{#if idx === 0} {#each view.filters as filter, idx}
<p>Where</p> {#if idx === 0}
{:else} <p>Where</p>
<Select secondary thin bind:value={filter.conjunction}> {:else}
<Select secondary thin bind:value={filter.conjunction}>
<option value="">Choose an option</option>
{#each CONJUNCTIONS as conjunction}
<option value={conjunction.key}>{conjunction.name}</option>
{/each}
</Select>
{/if}
<Select
secondary
thin
bind:value={filter.key}
on:change={fieldChanged(filter)}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each CONJUNCTIONS as conjunction} {#each fields as field}
<option value={conjunction.key}>{conjunction.name}</option> <option value={field}>{field}</option>
{/each} {/each}
</Select> </Select>
{/if} <Select secondary thin bind:value={filter.condition}>
<Select
secondary
thin
bind:value={filter.key}
on:change={fieldChanged(filter)}>
<option value="">Choose an option</option>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
<Select secondary thin bind:value={filter.condition}>
<option value="">Choose an option</option>
{#each CONDITIONS as condition}
<option value={condition.key}>{condition.name}</option>
{/each}
</Select>
{#if filter.key && isMultipleChoice(filter.key)}
<Select secondary thin bind:value={filter.value}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each fieldOptions(filter.key) as option} {#each CONDITIONS as condition}
<option value={option}>{option.toString()}</option> <option value={condition.key}>{condition.name}</option>
{/each} {/each}
</Select> </Select>
{:else if filter.key && isDate(filter.key)} {#if filter.key && isMultipleChoice(filter.key)}
<DatePicker <Select secondary thin bind:value={filter.value}>
bind:value={filter.value} <option value="">Choose an option</option>
placeholder={filter.key || fields[0]} /> {#each fieldOptions(filter.key) as option}
{:else if filter.key && isNumber(filter.key)} <option value={option}>{option.toString()}</option>
<Input {/each}
thin </Select>
bind:value={filter.value} {:else if filter.key && isDate(filter.key)}
placeholder={filter.key || fields[0]} <DatePicker
type="number" /> bind:value={filter.value}
{:else} placeholder={filter.key || fields[0]} />
<Input {:else if filter.key && isNumber(filter.key)}
thin <Input
placeholder={filter.key || fields[0]} thin
bind:value={filter.value} /> bind:value={filter.value}
{/if} placeholder={filter.key || fields[0]}
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} /> type="number" />
{/each} {:else}
</div> <Input
<div class="button-group"> thin
placeholder={filter.key || fields[0]}
bind:value={filter.value} />
{/if}
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
{/each}
</div>
{/if}
<div class="footer">
<Button text on:click={addFilter}>Add Filter</Button> <Button text on:click={addFilter}>Add Filter</Button>
<div> <div class="buttons">
<Button secondary on:click={dropdown.hide}>Cancel</Button> <Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button> <Button primary on:click={saveView}>Save</Button>
</div> </div>
</div> </div>
</Popover> </div>
<style> <style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 { h5 {
margin-bottom: var(--spacing-l); margin: 0;
font-weight: 500; font-weight: 500;
} }
.button-group { .footer {
margin-top: var(--spacing-l);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.buttons {
:global(.button-group > div > button) { display: flex;
margin-left: var(--spacing-m); justify-content: flex-end;
gap: var(--spacing-m);
} }
.ri-close-circle-fill { .ri-close-circle-fill {
@ -215,7 +202,6 @@
display: grid; display: grid;
grid-template-columns: minmax(50px, auto) 1fr 1fr 1fr 15px; grid-template-columns: minmax(50px, auto) 1fr 1fr 1fr 15px;
gap: var(--spacing-s); gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
align-items: center; align-items: center;
} }

View file

@ -1,26 +1,10 @@
<script> <script>
import { import { Button, Input, Select } from "@budibase/bbui"
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
const CALCULATIONS = [
{
name: "Statistics",
key: "stats",
},
]
export let view = {} export let view = {}
export let onClosed
let anchor
let dropdown
$: viewModel = $backendUiStore.models.find( $: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId ({ _id }) => _id === $backendUiStore.selectedView.modelId
@ -30,20 +14,14 @@
function saveView() { function saveView() {
backendUiStore.actions.views.save(view) backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`) notifier.success(`View ${view.name} saved.`)
dropdown.hide() onClosed()
} }
</script> </script>
<div bind:this={anchor}> <div class="actions">
<TextButton text small active={!!view.groupBy} on:click={dropdown.show}> <h5>Group</h5>
<Icon name="group" />
Group By
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<h5>Group By</h5>
<div class="input-group-row"> <div class="input-group-row">
<p>Group By</p> <p>By</p>
<Select secondary thin bind:value={view.groupBy}> <Select secondary thin bind:value={view.groupBy}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each fields as field} {#each fields as field}
@ -51,30 +29,33 @@
{/each} {/each}
</Select> </Select>
</div> </div>
<div class="button-group"> <div class="footer">
<Button secondary on:click={dropdown.hide}>Cancel</Button> <Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button> <Button primary on:click={saveView}>Save</Button>
</div> </div>
</Popover> </div>
<style> <style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 { h5 {
margin-bottom: var(--spacing-l); margin: 0;
font-weight: 500; font-weight: 500;
} }
.button-group { .footer {
margin-top: var(--spacing-l);
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: var(--spacing-s); gap: var(--spacing-m);
} }
.input-group-row { .input-group-row {
display: grid; display: grid;
grid-template-columns: 75px 1fr 20px 1fr; grid-template-columns: 20px 1fr;
gap: var(--spacing-s); gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
align-items: center; align-items: center;
} }

View file

@ -0,0 +1,94 @@
<script>
import { backendUiStore } from "builderStore"
import { DropdownMenu, Icon, Modal } from "@budibase/bbui"
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
import * as api from "../api"
import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let row
let anchor
let dropdown
let confirmDeleteDialog
let modal
function showModal() {
dropdown.hide()
modal.show()
}
function showDelete() {
dropdown.hide()
confirmDeleteDialog.show()
}
async function deleteRow() {
await api.deleteRecord(row)
notifier.success("Record deleted")
backendUiStore.actions.records.delete(row)
}
</script>
<div bind:this={anchor} on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<ul>
<li data-cy="edit-row" on:click={showModal}>
<Icon name="edit" />
<span>Edit</span>
</li>
<li data-cy="delete-row" on:click={showDelete}>
<Icon name="delete" />
<span>Delete</span>
</li>
</ul>
</DropdownMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete this row? Your data will be deleted and this action cannot be undone.`}
okText="Delete Row"
onOk={deleteRow}
title="Confirm Delete" />
<Modal bind:this={modal}>
<CreateEditRecordModal record={row} />
</Modal>
<style>
.ri-more-line:hover {
cursor: pointer;
}
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
ul {
list-style: none;
padding-left: 0;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
font-size: var(--font-size-xs);
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
</style>

View file

@ -19,26 +19,24 @@
<style> <style>
.indented { .indented {
grid-template-columns: 50px 1fr 20px; grid-template-columns: 46px 1fr 20px;
} }
.indented i { .indented i {
justify-self: end; justify-self: end;
} }
div { div {
padding: 0 10px 0 10px; padding: var(--spacing-s) var(--spacing-m);
height: 36px; border-radius: var(--border-radius-m);
border-radius: 5px;
display: grid; display: grid;
grid-template-columns: 30px 1fr 20px; grid-template-columns: 20px 1fr 20px;
align-items: center; align-items: center;
transition: 0.3s background-color; transition: 0.3s background-color;
color: var(--ink); color: var(--ink);
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
margin-top: 4px; margin-bottom: var(--spacing-xs);
margin-bottom: 4px; grid-gap: var(--spacing-s);
} }
.selected { .selected {
@ -53,6 +51,5 @@
i { i {
color: var(--grey-7); color: var(--grey-7);
font-size: 20px; font-size: 20px;
margin-right: 8px;
} }
</style> </style>

View file

@ -1,16 +1,12 @@
<script> <script>
import { getContext } from "svelte"
import { slide } from "svelte/transition"
import { Switcher } from "@budibase/bbui"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { store, backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import ListItem from "./ListItem.svelte" import ListItem from "./ListItem.svelte"
import { Button } from "@budibase/bbui" import CreateTableModal from "./modals/CreateTableModal.svelte"
import CreateTablePopover from "./CreateTable.svelte" import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditTablePopover from "./EditTable.svelte" import EditViewPopover from "./popovers/EditViewPopover.svelte"
import EditViewPopover from "./EditView.svelte" import { Heading } from "@budibase/bbui"
import { Spacer } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
$: selectedView = $: selectedView =
$backendUiStore.selectedView && $backendUiStore.selectedView.name $backendUiStore.selectedView && $backendUiStore.selectedView.name
@ -30,8 +26,9 @@
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id} {#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
<div class="hierarchy"> <div class="hierarchy">
<div class="components-list-container"> <div class="components-list-container">
<h4>Tables</h4> <Heading small>Tables</Heading>
<CreateTablePopover /> <Spacer medium />
<CreateTableModal />
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
{#each $backendUiStore.models as model} {#each $backendUiStore.models as model}
<ListItem <ListItem
@ -63,17 +60,18 @@
</div> </div>
<style> <style>
h4 { h5 {
font-weight: 500; font-size: 18px;
font-weight: 600;
margin-top: 0;
margin-bottom: var(--spacing-xl);
} }
.items-root { .items-root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 100%; justify-content: flex-start;
height: 100%; align-items: stretch;
background: var(--white);
padding: 20px;
} }
.hierarchy { .hierarchy {
@ -82,7 +80,7 @@
} }
.hierarchy-items-container { .hierarchy-items-container {
margin-top: 20px; margin-top: var(--spacing-xl);
flex: 1 1 auto; flex: 1 1 auto;
} }
</style> </style>

View file

@ -154,7 +154,7 @@
overflow: hidden; overflow: hidden;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
color: var(--ink); color: var(--ink);
padding: var(--spacing-s) var(--spacing-l); padding: var(--spacing-m) var(--spacing-l);
transition: all 0.2s ease 0s; transition: all 0.2s ease 0s;
display: inline-flex; display: inline-flex;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
@ -169,6 +169,8 @@
width: 100%; width: 100%;
background-color: var(--grey-2); background-color: var(--grey-2);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
line-height: normal;
border: var(--border-transparent);
} }
.omit-button { .omit-button {

View file

@ -0,0 +1,48 @@
<script>
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Button, Input, Label, ModalContent, Modal } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics"
let modal
let name
let dataImport
function resetState() {
name = ""
dataImport = undefined
}
async function saveTable() {
const model = await backendUiStore.actions.models.save({
name,
schema: dataImport.schema || {},
dataImport,
})
notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`)
analytics.captureEvent("Table Created", { name })
}
</script>
<Button primary wide on:click={modal.show}>Create New Table</Button>
<Modal bind:this={modal} on:hide={resetState}>
<ModalContent
title="Create Table"
confirmText="Create"
onConfirm={saveTable}
disabled={!name || (dataImport && !dataImport.valid)}>
<Input
data-cy="table-name-input"
thin
label="Table Name"
bind:value={name} />
<div>
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
<TableDataImport bind:dataImport />
</div>
</ModalContent>
</Modal>

View file

@ -0,0 +1,141 @@
<script>
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let table
let anchor
let dropdown
let editing
let confirmDeleteDialog
$: fields = Object.keys(table.schema)
function showEditor() {
editing = true
}
function hideEditor() {
dropdown?.hide()
editing = false
}
function showModal() {
hideEditor()
confirmDeleteDialog.show()
}
async function deleteTable() {
await backendUiStore.actions.models.delete(table)
notifier.success("Table deleted")
hideEditor()
}
async function save() {
await backendUiStore.actions.models.save(table)
notifier.success("Table renamed successfully")
hideEditor()
}
</script>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu align="left" {anchor} bind:this={dropdown}>
{#if editing}
<div class="actions">
<h5>Edit Table</h5>
<Input label="Table Name" thin bind:value={table.name} />
<Select
label="Primary Display Column"
thin
secondary
bind:value={table.primaryDisplay}>
<option value="">Choose an option</option>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
<footer>
<Button secondary on:click={hideEditor}>Cancel</Button>
<Button primary on:click={save}>Save</Button>
</footer>
</div>
{:else}
<ul>
<li on:click={showEditor}>
<Icon name="edit" />
Edit
</li>
<li data-cy="delete-table" on:click={showModal}>
<Icon name="delete" />
Delete
</li>
</ul>
{/if}
</DropdownMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete the table '${table.name}'? Your data will be deleted and this action cannot be undone.`}
okText="Delete Table"
onOk={deleteTable}
title="Confirm Delete" />
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.icon i {
font-size: 16px;
}
.actions {
padding: var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
min-width: 400px;
}
h5 {
margin: 0;
font-weight: 500;
}
footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
ul {
list-style: none;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
</style>

View file

@ -1,20 +1,18 @@
<script> <script>
import { getContext } from "svelte" import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import DeleteViewModal from "components/database/DataTable/modals/DeleteView.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
const { open, close } = getContext("simple-modal")
export let view export let view
let anchor let anchor
let dropdown let dropdown
let editing let editing
let originalName = view.name let originalName = view.name
let confirmDeleteDialog
function showEditor() { function showEditor() {
editing = true editing = true
@ -23,76 +21,96 @@
function hideEditor() { function hideEditor() {
dropdown.hide() dropdown.hide()
editing = false editing = false
close()
} }
const deleteView = () => { function showDelete() {
open( dropdown.hide()
DeleteViewModal, confirmDeleteDialog.show()
{
onClosed: close,
viewName: view.name,
},
{ styleContent: { padding: "0" } }
)
} }
function save() { async function save() {
backendUiStore.actions.views.save({ await backendUiStore.actions.views.save({
originalName, originalName,
...view, ...view,
}) })
notifier.success("Renamed View Successfully.") notifier.success("View renamed successfully")
hideEditor() hideEditor()
} }
async function deleteView() {
const name = view.name
const id = view.modelId
await backendUiStore.actions.views.delete(name)
notifier.success("View deleted")
$goto(`./model/${id}`)
}
</script> </script>
<div bind:this={anchor} on:click={dropdown.show}> <div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" /> <i class="ri-more-line" />
</div> </div>
<DropdownMenu bind:this={dropdown} {anchor} align="left"> <DropdownMenu align="left" {anchor} bind:this={dropdown}>
{#if editing} {#if editing}
<h5>Edit View</h5> <div class="actions">
<div class="container"> <h5>Edit View</h5>
<Input placeholder="View Name" thin bind:value={view.name} /> <Input label="View Name" thin bind:value={view.name} />
</div> <footer>
<footer>
<div class="button-margin-3">
<Button secondary on:click={hideEditor}>Cancel</Button> <Button secondary on:click={hideEditor}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={save}>Save</Button> <Button primary on:click={save}>Save</Button>
</div> </footer>
</footer> </div>
{:else} {:else}
<ul> <ul>
<li on:click={showEditor}> <li on:click={showEditor}>
<Icon name="edit" /> <Icon name="edit" />
Edit Edit
</li> </li>
<li data-cy="delete-view" on:click={deleteView}> <li data-cy="delete-view" on:click={showDelete}>
<Icon name="delete" /> <Icon name="delete" />
Delete Delete
</li> </li>
</ul> </ul>
{/if} {/if}
</DropdownMenu> </DropdownMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete the view '${view.name}'? Your data will be deleted and this action cannot be undone.`}
okText="Delete View"
onOk={deleteView}
title="Confirm Delete" />
<style> <style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.icon i {
font-size: 16px;
}
.actions {
padding: var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
min-width: 400px;
}
h5 { h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0; margin: 0;
font-weight: 500; font-weight: 500;
} }
.container { footer {
padding: var(--spacing-xl); display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
} }
ul { ul {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
list-style: none; list-style: none;
padding-left: 0;
margin: 0; margin: 0;
padding: var(--spacing-s) 0; padding: var(--spacing-s) 0;
} }
@ -115,29 +133,4 @@
li:active { li:active {
color: var(--blue); color: var(--blue);
} }
footer {
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-1 {
grid-column-start: 1;
display: grid;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style> </style>

View file

@ -1,57 +1,31 @@
<script> <script>
import { Modal, Button, Heading, Spacer } from "@budibase/bbui" import { Modal, ModalContent } from "@budibase/bbui"
export let title = "" export let title = ""
export let body = "" export let body = ""
export let okText = "OK" export let okText = "Confirm"
export let cancelText = "Cancel" export let cancelText = "Cancel"
export let onOk = () => {} export let onOk = undefined
export let onCancel = () => {} export let onCancel = undefined
let modal
export const show = () => { export const show = () => {
theModal.show() modal.show()
} }
export const hide = () => { export const hide = () => {
theModal.hide() modal.hide()
}
let theModal
const cancel = () => {
hide()
onCancel()
}
const ok = () => {
const result = onOk()
// allow caller to return false, to cancel the "ok"
if (result === false) return
hide()
} }
</script> </script>
<Modal id={title} bind:this={theModal}> <Modal bind:this={modal} on:hide={onCancel}>
<h2>{title}</h2> <ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red>
<Spacer extraLarge /> <div class="body">{body}</div>
<slot class="rows">{body}</slot> </ModalContent>
<Spacer extraLarge />
<div class="modal-footer">
<Button red wide on:click={ok}>{okText}</Button>
<Button secondary wide on:click={cancel}>{cancelText}</Button>
</div>
</Modal> </Modal>
<style> <style>
h2 { .body {
font-size: var(--font-size-xl); font-size: var(--font-size-s);
margin: 0;
font-family: var(--font-sans);
font-weight: 600;
}
.modal-footer {
display: grid;
grid-gap: var(--spacing-s);
} }
</style> </style>

View file

@ -11,6 +11,4 @@
} }
</script> </script>
<div class="bb-margin-m"> <DatePicker {label} on:change={onChange} {value} />
<DatePicker placeholder={label} on:change={onChange} {value} />
</div>

View file

@ -5,9 +5,25 @@
</script> </script>
{#if hasErrors} {#if hasErrors}
<div class="bb__alert bb__alert--danger"> <div class="container bb__alert bb__alert--danger">
{#each errors as error} {#each errors as error}
<div>{error.dataPath} {error.message}</div> <div class="error">{error.dataPath} {error.message}</div>
{/each} {/each}
</div> </div>
{/if} {/if}
<style>
.container {
border-radius: var(--border-radius-m);
margin: 0;
padding: var(--spacing-m);
}
.error {
font-size: var(--font-size-xs);
font-weight: 500;
}
.error:first-letter {
text-transform: uppercase;
}
</style>

View file

@ -2,101 +2,52 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import api from "builderStore/api" import api from "builderStore/api"
import { Select, Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "../../helpers"
export let modelId export let schema
export let linkName export let linkedRecords = []
export let linked = []
let records = [] let records = []
let model = {}
let linkedRecords = new Set(linked) $: label = capitalise(schema.name)
$: linkedModelId = schema.modelId
$: linkedModel = $backendUiStore.models.find(
model => model._id === linkedModelId
)
$: fetchRecords(linkedModelId)
$: linked = [...linkedRecords] async function fetchRecords(linkedModelId) {
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name] const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
$: schema = $backendUiStore.selectedModel.schema try {
const response = await api.get(FETCH_RECORDS_URL)
async function fetchRecords() { records = await response.json()
const FETCH_RECORDS_URL = `/api/${modelId}/records` } catch (error) {
const response = await api.get(FETCH_RECORDS_URL) console.log(error)
const modelResponse = await api.get(`/api/models/${modelId}`) records = []
model = await modelResponse.json()
records = await response.json()
}
function linkRecord(id) {
if (linkedRecords.has(id)) {
linkedRecords.delete(id)
} else {
linkedRecords.add(id)
} }
linkedRecords = linkedRecords
} }
onMount(() => { function getPrettyName(record) {
fetchRecords() return record[linkedModel.primaryDisplay || "_id"]
}) }
</script> </script>
<section> {#if linkedModel.primaryDisplay == null}
<header> <Label extraSmall grey>{label}</Label>
<h3>{linkName}</h3> <Label small black>
</header> Please choose a primary display column for the
{#each records as record} <b>{linkedModel.name}</b>
<div class="linked-record" on:click={() => linkRecord(record._id)}> table.
<div class="fields" class:selected={linkedRecords.has(record._id)}> </Label>
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key} {:else}
<div class="field"> <Multiselect
<span>{model.schema[key].name}</span> secondary
<p>{record[key]}</p> bind:value={linkedRecords}
</div> {label}
{/each} placeholder="Choose some options">
</div> {#each records as record}
</div> <option value={record._id}>{getPrettyName(record)}</option>
{/each} {/each}
</section> </Multiselect>
{/if}
<style>
.fields.selected {
background: var(--grey-2);
border: var(--purple) 1px solid;
}
h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
color: var(--ink);
}
.fields {
padding: 15px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
background: var(--white);
border: 1px solid var(--grey);
border-radius: 5px;
transition: 0.5s all;
margin-bottom: 8px;
}
.fields:hover {
cursor: pointer;
}
.field span {
color: var(--ink-lighter);
font-size: 12px;
}
.field p {
color: var(--ink);
font-size: 14px;
word-break: break-word;
font-weight: 500;
margin-top: 4px;
}
</style>

View file

@ -1,7 +1,7 @@
<script> <script>
import { notificationStore } from "builderStore/store/notifications" import { notificationStore } from "builderStore/store/notifications"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { fade } from "svelte/transition" import { fly } from "svelte/transition"
export let themes = { export let themes = {
danger: "#E26D69", danger: "#E26D69",
@ -24,36 +24,42 @@
} }
</script> </script>
<ul class="notifications"> <div class="notifications">
{#each $notificationStore.notifications as notification (notification.id)} {#each $notificationStore.notifications as notification (notification.id)}
<li <div
class="toast" class="toast"
style="background: {themes[notification.type]};" style="background: {themes[notification.type]};"
transition:fade> transition:fly={{ y: -30 }}>
<div class="content">{notification.message}</div> <div class="content">{notification.message}</div>
{#if notification.icon} {#if notification.icon}
<i class={notification.icon} /> <i class={notification.icon} />
{/if} {/if}
</li> </div>
{/each} {/each}
</ul> </div>
<style> <style>
.notifications { .notifications {
width: 40vw;
list-style: none;
position: fixed; position: fixed;
top: 0; top: 10px;
left: 0; left: 0;
right: 0; right: 0;
margin-left: auto; margin: 0 auto;
margin-right: auto;
padding: 0; padding: 0;
z-index: 9999; z-index: 9999;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
pointer-events: none;
} }
.toast { .toast {
flex: 0 0 auto;
margin-bottom: 10px; margin-bottom: 10px;
border-radius: var(--border-radius-s);
/* The toasts now support being auto sized, so this static width could be removed */
width: 40vw;
} }
.content { .content {

View file

@ -26,7 +26,6 @@
.numberbox { .numberbox {
display: grid; display: grid;
align-items: center; align-items: center;
margin-bottom: 16px;
} }
label { label {

View file

@ -1,5 +1,6 @@
<script> <script>
import { join } from "lodash/fp" import { join } from "lodash/fp"
import { TextArea, Label } from "@budibase/bbui"
export let values export let values
export let label export let label
@ -15,35 +16,12 @@
$: valuesText = join("\n")(values) $: valuesText = join("\n")(values)
</script> </script>
<div class="margin"> <div class="container">
<label class="label">{label}</label> <TextArea {label} value={valuesText} thin on:change={inputChanged} />
<textarea value={valuesText} on:change={inputChanged} />
</div> </div>
<style> <style>
.margin { .container :global(textarea) {
margin-bottom: 16px; min-height: 100px;
display: grid;
}
.label {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
textarea {
font-size: 14px;
height: 200px;
width: 100%;
border-radius: 5px;
border: none;
cursor: text;
background: var(--grey-2);
padding: 12px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-family: Inter;
} }
</style> </style>

View file

@ -1,124 +0,0 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
export let ids = []
export let field
let records = []
let open = false
let model
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name]
async function fetchRecords() {
const response = await api.post("/api/records/search", {
keys: ids,
})
const modelResponse = await api.get(`/api/models/${field.modelId}`)
records = await response.json()
model = await modelResponse.json()
}
$: ids && fetchRecords()
function toggleOpen() {
open = !open
}
onMount(() => {
fetchRecords()
})
</script>
<section>
<a on:click={toggleOpen}>{records.length}</a>
{#if open}
<div class="popover" transition:fade>
<header>
<h3>{field.name}</h3>
<i class="ri-close-circle-fill" on:click={toggleOpen} />
</header>
{#each records as record}
<div class="linked-record">
<div class="fields">
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
<div class="field">
<span>{model.schema[key].name}</span>
<p>{record[key]}</p>
</div>
{/each}
</div>
</div>
{/each}
</div>
{/if}
</section>
<style>
section {
display: relative;
}
header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
i {
font-size: 24px;
color: var(--ink-lighter);
}
i:hover {
cursor: pointer;
}
a {
font-size: 14px;
}
.popover {
width: 500px;
position: absolute;
right: 15%;
padding: 20px;
background: var(--grey-1);
border: 1px solid var(--grey);
}
h3 {
font-size: 20px;
font-weight: bold;
margin: 0;
}
.fields {
padding: 15px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
background: var(--white);
border: 1px solid var(--grey);
border-radius: 5px;
margin-bottom: 8px;
}
.field span {
color: var(--ink-lighter);
font-size: 12px;
}
.field p {
color: var(--ink);
font-size: 14px;
word-break: break-word;
font-weight: 500;
margin-top: 4px;
}
</style>

View file

@ -1,149 +0,0 @@
<script>
import { onMount } from "svelte"
import fsort from "fast-sort"
import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Button, Icon } from "@budibase/bbui"
import ActionButton from "components/common/ActionButton.svelte"
import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte"
import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte"
import CalculationPopover from "./popovers/Calculate.svelte"
export let schema = []
export let data = []
export let title
const ITEMS_PER_PAGE = 10
let currentPage = 0
$: columns = schema ? Object.keys(schema) : []
$: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
$: paginatedData =
sorted && sorted.length
? sorted.slice(
currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
)
: []
</script>
<section>
<div class="table-controls">
<h2 class="title">{title}</h2>
<div class="popovers">
<slot />
</div>
</div>
<table class="bb-table">
<thead>
<tr>
{#each columns as header}
<th>{header}</th>
{/each}
</tr>
</thead>
<tbody>
{#if paginatedData.length === 0}
<div class="no-data">No Data.</div>
{/if}
{#each paginatedData as row}
<tr>
{#each columns as header}
<td>
{#if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []} />
{:else}{getOr('', header, row)}{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<TablePagination
{data}
bind:currentPage
pageItemCount={paginatedData.length}
{ITEMS_PER_PAGE} />
</section>
<style>
section {
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: 600;
text-rendering: optimizeLegibility;
text-transform: capitalize;
}
table {
border: 1px solid var(--grey-4);
background: #fff;
border-radius: 3px;
border-collapse: collapse;
}
thead {
height: 40px;
background: var(--grey-3);
border: 1px solid var(--grey-4);
}
thead th {
color: var(--ink);
text-transform: capitalize;
font-weight: 500;
font-size: 14px;
text-rendering: optimizeLegibility;
transition: 0.5s all;
vertical-align: middle;
}
th:hover {
color: var(--blue);
cursor: pointer;
}
td {
max-width: 200px;
text-overflow: ellipsis;
border: 1px solid var(--grey-4);
}
tbody tr {
border-bottom: 1px solid var(--grey-4);
transition: 0.3s background-color;
color: var(--ink);
font-size: 12px;
}
tbody tr:hover {
background: var(--grey-1);
}
.table-controls {
width: 100%;
}
.popovers {
display: flex;
}
:global(.popovers > div) {
margin-right: var(--spacing-m);
}
.no-data {
padding: 14px;
}
</style>

View file

@ -1,55 +0,0 @@
<script>
import { onMount } from "svelte"
import fsort from "fast-sort"
import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Button, Icon } from "@budibase/bbui"
import Table from "./Table.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import TablePagination from "./TablePagination.svelte"
import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte"
import CalculationPopover from "./popovers/Calculate.svelte"
import GroupByPopover from "./popovers/GroupBy.svelte"
import FilterPopover from "./popovers/Filter.svelte"
import ExportPopover from "./popovers/Export.svelte"
export let view = {}
let data = []
$: name = view.name
$: filters = view.filters
$: field = view.field
$: groupBy = view.groupBy
$: !name.startsWith("all_") && filters && fetchViewData(name, field, groupBy)
async function fetchViewData(name, field, groupBy) {
const params = new URLSearchParams()
if (field) {
params.set("field", field)
params.set("stats", true)
}
if (groupBy) params.set("group", groupBy)
let QUERY_VIEW_URL = `/api/views/${name}?${params}`
const response = await api.get(QUERY_VIEW_URL)
data = await response.json()
}
</script>
<Table title={decodeURI(name)} schema={view.schema} {data}>
<FilterPopover {view} />
<CalculationPopover {view} />
{#if view.calculation}
<GroupByPopover {view} />
{/if}
<ExportPopover {view} />
</Table>

View file

@ -1 +0,0 @@
export { default } from "./ModelDataTable.svelte"

View file

@ -1,161 +0,0 @@
<script>
import { onMount } from "svelte"
import { Input, TextArea, Button, Select } from "@budibase/bbui"
import { cloneDeep, merge } from "lodash/fp"
import { store, backendUiStore } from "builderStore"
import { FIELDS } from "constants/backend"
import { notifier } from "builderStore/store/notifications"
import ButtonGroup from "components/common/ButtonGroup.svelte"
import NumberBox from "components/common/NumberBox.svelte"
import ValuesList from "components/common/ValuesList.svelte"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import Checkbox from "components/common/Checkbox.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import DatePicker from "components/common/DatePicker.svelte"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import * as api from "../api"
let fieldDefinitions = cloneDeep(FIELDS)
export let onClosed
export let field = {
type: "string",
constraints: fieldDefinitions.STRING.constraints,
}
let originalName = field.name
$: required = field && field.constraints && field.constraints.presence
async function saveColumn() {
backendUiStore.update(state => {
backendUiStore.actions.models.saveField({
originalName,
field,
})
return state
})
onClosed()
}
function handleFieldConstraints(event) {
const { type, constraints } = fieldDefinitions[
event.target.value.toUpperCase()
]
field.type = type
field.constraints = constraints
}
const getPresence = required => (required ? { allowEmpty: false } : false)
const requiredChanged = ev => {
const req = ev.target.checked
field.constraints.presence = req ? { allowEmpty: false } : false
required = req
}
</script>
<div class="actions">
<Input placeholder="Name" thin bind:value={field.name} />
<Select
secondary
thin
on:change={handleFieldConstraints}
bind:value={field.type}>
{#each Object.values(fieldDefinitions) as field}
<option value={field.type}>{field.name}</option>
{/each}
</Select>
<div class="info">
<div class="field">
<label>Required</label>
<input type="checkbox" checked={required} on:change={requiredChanged} />
</div>
{#if field.type === 'string' && field.constraints}
<NumberBox
label="Max Length"
bind:value={field.constraints.length.maximum} />
<ValuesList
label="Categories"
bind:values={field.constraints.inclusion} />
{:else if field.type === 'datetime' && field.constraints}
<DatePicker
label="Earliest"
bind:value={field.constraints.datetime.earliest} />
<DatePicker
label="Latest"
bind:value={field.constraints.datetime.latest} />
{:else if field.type === 'number' && field.constraints}
<NumberBox
label="Min Value"
bind:value={field.constraints.numericality.greaterThanOrEqualTo} />
<NumberBox
label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'}
<div class="field">
<label>Link</label>
<select class="budibase__input" bind:value={field.modelId}>
<option value={''} />
{#each $backendUiStore.models as model}
{#if model._id !== $backendUiStore.draftModel._id}
<option value={model._id}>{model.name}</option>
{/if}
{/each}
</select>
</div>
{/if}
</div>
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={saveColumn}>Save Column</Button>
</div>
</footer>
<style>
.actions {
padding: var(--spacing-l) var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
}
footer {
padding: 20px 30px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.field {
display: grid;
grid-template-columns: auto 20px 1fr;
align-items: center;
grid-gap: 5px;
font-size: 14px;
font-weight: 500;
margin-bottom: var(--spacing-l);
font-family: var(--font-normal);
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View file

@ -1,93 +0,0 @@
<script>
import { onMount, tick } from "svelte"
import { store, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { compose, map, get, flatten } from "lodash/fp"
import { Input, TextArea, Button } from "@budibase/bbui"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import RecordFieldControl from "./RecordFieldControl.svelte"
import * as api from "../api"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let record = {}
export let onClosed
let errors = []
let selectedModel
$: modelSchema = $backendUiStore.selectedModel
? Object.entries($backendUiStore.selectedModel.schema)
: []
async function saveRecord() {
const recordResponse = await api.saveRecord(
{
...record,
modelId: $backendUiStore.selectedModel._id,
},
$backendUiStore.selectedModel._id
)
if (recordResponse.errors) {
errors = Object.keys(recordResponse.errors)
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
.flat()
return
}
onClosed()
notifier.success("Record saved successfully.")
backendUiStore.actions.records.save(recordResponse)
}
</script>
<div class="actions">
<ErrorsBox {errors} />
<form on:submit|preventDefault>
{#each modelSchema as [key, meta]}
<div class="bb-margin-xl">
{#if meta.type === 'link'}
<LinkedRecordSelector
bind:linked={record[key]}
linkName={meta.name}
modelId={meta.modelId} />
{:else}
<RecordFieldControl {meta} bind:value={record[key]} />
{/if}
</div>
{/each}
</form>
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={saveRecord}>Save</Button>
</div>
</footer>
<style>
.actions {
padding: var(--spacing-l) var(--spacing-xl);
}
footer {
padding: 20px 30px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View file

@ -1,61 +0,0 @@
<script>
import ActionButton from "components/common/ActionButton.svelte"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore } from "builderStore"
import * as api from "../api"
export let record
export let onClosed
</script>
<section>
<div class="content">
<header>
<i class="ri-information-line alert" />
<h4 class="budibase__title--4">Delete Record</h4>
</header>
<p>
Are you sure you want to delete this record? All of your data will be
permanently removed. This action cannot be undone.
</p>
</div>
<div class="modal-actions">
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton
alert
on:click={async () => {
await api.deleteRecord(record)
notifier.danger('Record deleted')
backendUiStore.actions.records.delete(record)
onClosed()
}}>
Delete
</ActionButton>
</div>
</section>
<style>
.alert {
color: rgba(255, 0, 31, 1);
background: var(--grey-1);
padding: 5px;
}
.modal-actions {
padding: 10px;
background: var(--grey-1);
border-top: 1px solid #ccc;
}
header {
display: flex;
align-items: center;
}
.content {
padding: 30px;
}
h4 {
margin: 0 0 0 10px;
}
</style>

View file

@ -1,64 +0,0 @@
<script>
import ActionButton from "components/common/ActionButton.svelte"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore } from "builderStore"
import * as api from "../api"
export let table
export let onClosed
function deleteTable() {
backendUiStore.actions.models.delete(table)
}
</script>
<section>
<div class="content">
<header>
<i class="ri-information-line alert" />
<h4 class="budibase__title--4">Delete Table</h4>
</header>
<p>
Are you sure you want to delete this table? All of your data will be
permanently removed. This action cannot be undone.
</p>
</div>
<div class="modal-actions">
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton
alert
on:click={async () => {
await backendUiStore.actions.models.delete(table)
notifier.danger('Table deleted')
onClosed()
}}>
Delete
</ActionButton>
</div>
</section>
<style>
.alert {
color: rgba(255, 0, 31, 1);
background: var(--grey-1);
padding: 5px;
}
.modal-actions {
padding: 10px;
background: var(--grey-1);
border-top: 1px solid #ccc;
}
header {
display: flex;
align-items: center;
}
.content {
padding: 30px;
}
h4 {
margin: 0 0 0 10px;
}
</style>

View file

@ -1,62 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
import ActionButton from "components/common/ActionButton.svelte"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore } from "builderStore"
import * as api from "../api"
export let viewName
export let onClosed
</script>
<section>
<div class="content">
<header>
<i class="ri-information-line alert" />
<h4 class="budibase__title--4">Delete View</h4>
</header>
<p>
Are you sure you want to delete this view? All of your data will be
permanently removed. This action cannot be undone.
</p>
</div>
<div class="modal-actions">
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton
alert
on:click={async () => {
await backendUiStore.actions.views.delete(viewName)
notifier.danger(`View ${viewName} deleted.`)
$goto(`./backend`)
onClosed()
}}>
Delete
</ActionButton>
</div>
</section>
<style>
.alert {
color: rgba(255, 0, 31, 1);
background: var(--grey-1);
padding: 5px;
}
.modal-actions {
padding: 10px;
background: var(--grey-1);
border-top: 1px solid #ccc;
}
header {
display: flex;
align-items: center;
}
.content {
padding: 30px;
}
h4 {
margin: 0 0 0 10px;
}
</style>

View file

@ -1,75 +0,0 @@
<script>
import { Input, Select, Label, DatePicker } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte"
export let meta
export let value = meta.type === "boolean" ? false : ""
export let originalValue
let isSelect =
meta.type === "string" &&
meta.constraints &&
meta.constraints.inclusion &&
meta.constraints.inclusion.length > 0
let type = determineInputType(meta)
function determineInputType(meta) {
if (meta.type === "datetime") return "date"
if (meta.type === "number") return "number"
if (meta.type === "boolean") return "checkbox"
if (meta.type === "attachment") return "file"
if (isSelect) return "select"
return "text"
}
const handleInput = event => {
if (event.target.type === "checkbox") {
value = event.target.checked
return
}
if (event.target.type === "number") {
value = parseInt(event.target.value)
return
}
value = event.target.value
}
</script>
{#if type === 'select'}
<Select thin secondary data-cy="{meta.name}-select" bind:value>
<option />
{#each meta.constraints.inclusion as opt}
<option value={opt}>{opt}</option>
{/each}
</Select>
{:else if type === 'date'}
<Label small forAttr={'datepicker-label'}>{meta.name}</Label>
<DatePicker bind:value />
{:else if type === 'file'}
<Label small forAttr={'dropzone-label'}>{meta.name}</Label>
<Dropzone bind:files={value} />
{:else}
{#if type === 'checkbox'}
<label>{meta.name}</label>
{/if}
<Input
thin
placeholder={meta.name}
data-cy="{meta.name}-input"
checked={value}
{type}
{value}
on:change={handleInput} />
{/if}
<style>
label {
font-weight: 500;
font-size: var(--font-size-s);
margin-bottom: 12px;
}
</style>

View file

@ -1,2 +0,0 @@
export { default as DeleteRecordModal } from "./DeleteRecord.svelte"
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte"

View file

@ -1,35 +0,0 @@
<script>
import { backendUiStore } from "builderStore"
import {
DropdownMenu,
TextButton as Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
let anchor
let dropdown
let fieldName
</script>
<div bind:this={anchor}>
<Button text small on:click={dropdown.show}>
<Icon name="addcolumn" />
Create New Column
</Button>
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<h5>Create Column</h5>
<CreateEditColumn onClosed={dropdown.hide} />
</DropdownMenu>
<style>
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
</style>

View file

@ -1,103 +0,0 @@
<script>
import { getContext } from "svelte"
import { backendUiStore } from "builderStore"
import {
DropdownMenu,
Button,
Icon,
Input,
Select,
Heading,
} from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
import DeleteRecordModal from "../modals/DeleteRecord.svelte"
const { open, close } = getContext("simple-modal")
export let row
let anchor
let dropdown
let editing
function showEditor() {
editing = true
}
function hideEditor() {
dropdown.hide()
editing = false
close()
}
const deleteRow = () => {
open(
DeleteRecordModal,
{
onClosed: hideEditor,
record: row,
},
{ styleContent: { padding: "0" } }
)
}
</script>
<div bind:this={anchor} on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing}
<h5>Edit Row</h5>
<CreateEditRecord onClosed={hideEditor} record={row} />
{:else}
<ul>
<li data-cy="edit-row" on:click={showEditor}>
<Icon name="edit" />
<span>Edit</span>
</li>
<li data-cy="delete-row" on:click={deleteRow}>
<Icon name="delete" />
<span>Delete</span>
</li>
</ul>
{/if}
</DropdownMenu>
<style>
.ri-more-line:hover {
cursor: pointer;
}
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
ul {
list-style: none;
padding-left: 0;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
</style>

View file

@ -1,71 +0,0 @@
<script>
import {
TextButton,
Button,
Icon,
Input,
Select,
Popover,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import api from "builderStore/api"
const FORMATS = [
{
name: "CSV",
key: "csv",
},
{
name: "JSON",
key: "json",
},
]
export let view
let anchor
let dropdown
let exportFormat
async function exportView() {
const response = await api.post(
`/api/views/export?format=${exportFormat}`,
view
)
const downloadInfo = await response.json()
window.location = downloadInfo.url
}
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show}>
<Icon name="download" />
Export
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<h5>Export Format</h5>
<Select secondary thin bind:value={exportFormat}>
<option value={''}>Select an option</option>
{#each FORMATS as format}
<option value={format.key}>{format.name}</option>
{/each}
</Select>
<div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={exportView}>Export</Button>
</div>
</Popover>
<style>
h5 {
margin-top: 0;
}
.button-group {
margin-top: var(--spacing-l);
display: flex;
justify-content: flex-end;
}
</style>

View file

@ -1,113 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import Spinner from "components/common/Spinner.svelte"
import {
DropdownMenu,
Button,
Label,
Heading,
Icon,
Input,
Select,
Dropzone,
Spacer,
} from "@budibase/bbui"
import TableDataImport from "./TableDataImport.svelte"
import api from "builderStore/api"
import analytics from "analytics"
let anchor
let dropdown
let name
let dataImport
let loading
async function saveTable() {
loading = true
const model = await backendUiStore.actions.models.save({
name,
schema: dataImport.schema || {},
dataImport,
})
notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`)
analytics.captureEvent("Table Created", { name })
name = ""
dropdown.hide()
loading = false
}
const onClosed = () => {
name = ""
dropdown.hide()
}
</script>
<div bind:this={anchor}>
<Button primary wide on:click={dropdown.show}>Create New Table</Button>
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<div class="container">
<h5>Create Table</h5>
<Label grey extraSmall>Name</Label>
<Input
data-cy="table-name-input"
placeholder="Table Name"
thin
bind:value={name} />
<Spacer medium />
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
<TableDataImport bind:dataImport />
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button
disabled={!name || (dataImport && !dataImport.valid)}
primary
on:click={saveTable}>
<span style={`margin-right: ${loading ? '10px' : 0};`}>Save</span>
{#if loading}
<Spinner size="10" />
{/if}
</Button>
</div>
</footer>
</DropdownMenu>
<style>
h5 {
margin-bottom: var(--spacing-m);
margin-top: 0;
font-weight: 500;
}
.container {
padding: var(--spacing-l);
margin: 0;
}
footer {
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View file

@ -1,137 +0,0 @@
<script>
import { getContext } from "svelte"
import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import DeleteTableModal from "components/database/DataTable/modals/DeleteTable.svelte"
const { open, close } = getContext("simple-modal")
export let table
let anchor
let dropdown
let editing
function showEditor() {
editing = true
}
function hideEditor() {
dropdown.hide()
editing = false
close()
}
const deleteTable = () => {
open(
DeleteTableModal,
{
onClosed: close,
table,
},
{ styleContent: { padding: "0" } }
)
}
function save() {
backendUiStore.actions.models.save(table)
hideEditor()
}
</script>
<div bind:this={anchor} on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing}
<h5>Edit Table</h5>
<div class="container">
<Input placeholder="Table Name" thin bind:value={table.name} />
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={hideEditor}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={save}>Save</Button>
</div>
</footer>
{:else}
<ul>
<li on:click={showEditor}>
<Icon name="edit" />
Edit
</li>
<li data-cy="delete-table" on:click={deleteTable}>
<Icon name="delete" />
Delete
</li>
</ul>
{/if}
</DropdownMenu>
<style>
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
.container {
padding: var(--spacing-xl);
}
ul {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
list-style: none;
padding-left: 0;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
footer {
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-1 {
grid-column-start: 1;
display: grid;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View file

@ -1,20 +0,0 @@
<script>
import { isActive, url, goto } from "@sveltech/routify"
export let label = ""
export let href
</script>
<div
on:click={() => $goto(href)}
class="budibase__nav-item backend-nav-item"
class:selected={$isActive(href)}>
{label}
</div>
<style>
.backend-nav-item {
padding-left: 25px;
cursor: pointer;
}
</style>

View file

@ -1,32 +1,17 @@
<script> <script>
import Modal from "./Modal.svelte" import SettingsModal from "./SettingsModal.svelte"
import { SettingsIcon } from "components/common/Icons/" import { SettingsIcon } from "components/common/Icons/"
import { getContext } from "svelte" import { Modal } from "@budibase/bbui"
import { isActive, goto, layout } from "@sveltech/routify"
// Handle create app modal let modal
const { open } = getContext("simple-modal")
const showSettingsModal = () => {
open(
Modal,
{
name: "Placeholder App Name",
description: "This is a hardcoded description that needs to change",
},
{
closeButton: false,
closeOnEsc: true,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
}
</script> </script>
<span class="topnavitemright settings" on:click={showSettingsModal}> <span class="topnavitemright settings" on:click={modal.show}>
<SettingsIcon /> <SettingsIcon />
</span> </span>
<Modal bind:this={modal} width="600px">
<SettingsModal />
</Modal>
<style> <style>
span:first-letter { span:first-letter {
@ -35,8 +20,7 @@
.topnavitemright { .topnavitemright {
cursor: pointer; cursor: pointer;
color: var(--grey-7); color: var(--grey-7);
margin: 0px 20px 0px 0px; margin: 0 20px 0 0;
padding-top: 4px;
font-weight: 500; font-weight: 500;
font-size: 1rem; font-size: 1rem;
height: 100%; height: 100%;

View file

@ -1,99 +0,0 @@
<script>
import { General, Users, DangerZone, APIKeys } from "./tabs"
import { Input, TextArea, Button, Switcher } from "@budibase/bbui"
import { SettingsIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { post } from "builderStore/api"
const { open, close } = getContext("simple-modal")
export let name = ""
export let description = ""
const tabs = [
{
title: "General",
key: "GENERAL",
component: General,
},
{
title: "Users",
key: "USERS",
component: Users,
},
{
title: "API Keys",
key: "API_KEYS",
component: APIKeys,
},
{
title: "Danger Zone",
key: "DANGERZONE",
component: DangerZone,
},
]
let value = "GENERAL"
$: selectedTab = tabs.find(tab => tab.key === value).component
</script>
<div class="container">
<div class="body">
<div class="heading">
<span class="icon">
<SettingsIcon />
</span>
<h3>Settings</h3>
</div>
<Switcher headings={tabs} bind:value>
<svelte:component this={selectedTab} />
</Switcher>
</div>
<div class="close-button" on:click={close}>
<CloseIcon />
</div>
</div>
<style>
.container {
position: relative;
height: 36rem;
}
.close-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
}
.heading {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
}
h3 {
margin: 0;
font-size: 24px;
font-weight: bold;
}
.icon {
display: grid;
border-radius: 3px;
align-content: center;
justify-content: center;
margin-right: 12px;
height: 20px;
width: 20px;
padding: 10px;
background-color: var(--blue-light);
color: var(--grey-7);
}
.body {
padding: 40px 40px 40px 40px;
display: grid;
grid-gap: 20px;
}
</style>

View file

@ -0,0 +1,52 @@
<script>
import { General, Users, DangerZone, APIKeys } from "./tabs"
import { Switcher, ModalContent } from "@budibase/bbui"
const tabs = [
{
title: "General",
key: "GENERAL",
component: General,
},
{
title: "Users",
key: "USERS",
component: Users,
},
{
title: "API Keys",
key: "API_KEYS",
component: APIKeys,
},
{
title: "Danger Zone",
key: "DANGERZONE",
component: DangerZone,
},
]
let value = "GENERAL"
$: selectedTab = tabs.find(tab => tab.key === value).component
</script>
<ModalContent
title="Settings"
showConfirmButton={false}
showCancelButton={false}>
<div class="container">
<Switcher headings={tabs} bind:value>
<svelte:component this={selectedTab} />
</Switcher>
</div>
</ModalContent>
<style>
.container :global(section > header) {
/* Fix margin defined in BBUI as L rather than XL */
margin-bottom: var(--spacing-xl);
}
.container :global(textarea) {
min-height: 60px;
}
</style>

View file

@ -14,7 +14,7 @@
bind:value={user.username} bind:value={user.username}
name="Name" name="Name"
placeholder="Username" /> placeholder="Username" />
<Select disabled={!editMode} bind:value={user.accessLevelId} thin> <Select disabled={!editMode} bind:value={user.accessLevelId} thin secondary>
<option value="">Choose an option</option> <option value="">Choose an option</option>
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option> <option value="POWER_USER">Power User</option>
@ -37,19 +37,7 @@
.inputs { .inputs {
display: grid; display: grid;
justify-items: stretch; justify-items: stretch;
grid-gap: 18px; grid-gap: var(--spacing-m);
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 140px;
}
.inputs :global(input) {
padding: 10px 12px;
border-radius: var(--rounded-small);
}
.inputs :global(select) {
padding: 9px 12px;
border-radius: var(--rounded-small);
}
.inputs :global(button) {
border-radius: var(--rounded-small);
height: initial;
} }
</style> </style>

View file

@ -1,8 +1,6 @@
<script> <script>
import { Input, Button } from "@budibase/bbui" import { Input } from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api" import api from "builderStore/api"
import posthog from "posthog-js"
import analytics from "analytics" import analytics from "analytics"
let keys = { budibase: "" } let keys = { budibase: "" }
@ -34,23 +32,17 @@
</script> </script>
<div class="container"> <div class="container">
<div class="background"> <Input
<Input on:save={e => updateKey(['budibase', e.detail])}
on:save={e => updateKey(['budibase', e.detail])} thin
thin edit
edit value={keys.budibase}
value={keys.budibase} label="Budibase API Key" />
label="Budibase" />
</div>
</div> </div>
<style> <style>
.container { .container {
display: grid; display: grid;
grid-gap: var(--space); grid-gap: var(--spacing-xl);
}
.background {
border-radius: 5px;
padding: 12px 0px;
} }
</style> </style>

View file

@ -1,6 +1,6 @@
<script> <script>
import { params, goto } from "@sveltech/routify" import { params, goto } from "@sveltech/routify"
import { Input, TextArea, Button } from "@budibase/bbui" import { Input, TextArea, Button, Body } from "@budibase/bbui"
import { del } from "builderStore/api" import { del } from "builderStore/api"
let value = "" let value = ""
@ -9,51 +9,50 @@
async function deleteApp() { async function deleteApp() {
loading = true loading = true
const id = $params.application const id = $params.application
const res = await del(`/api/${id}`) await del(`/api/${id}`)
const json = await res.json()
loading = false loading = false
if (res.ok) { $goto("/")
$goto("/")
return json
} else {
throw new Error(json)
}
} }
</script> </script>
<div class="background"> <div class="background">
<p> <Body>
Type DELETE into the textbox, then click the following button to delete your Type
web app: <b>DELETE</b>
</p> into the textbox, then click the following button to delete your entire web
app.
</Body>
<Input <Input
on:change={e => (value = e.target.value)} on:change={e => (value = e.target.value)}
on:input={e => (value = e.target.value)} on:input={e => (value = e.target.value)}
thin thin
disabled={loading} disabled={loading}
placeholder="" /> placeholder="" />
<div class="buttons">
<Button <Button
disabled={value !== 'DELETE' || loading} primary
red disabled={value !== 'DELETE' || loading}
wide red
on:click={deleteApp}> on:click={deleteApp}>
Delete Entire Web App Delete Entire App
</Button> </Button>
</div>
</div> </div>
<style> <style>
.background { .background {
display: grid; display: grid;
grid-gap: 16px; grid-gap: var(--spacing-xl);
border-radius: 5px;
padding: 12px 0px;
} }
p { .background :global(p) {
line-height: 1.2;
margin: 0; margin: 0;
} }
.background :global(button) {
max-width: 100%; .buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
} }
</style> </style>

View file

@ -22,19 +22,18 @@
thin thin
edit edit
value={$store.name} value={$store.name}
label="Name" /> label="App Name" />
<TextArea <TextArea
on:save={e => updateApplication({ description: e.detail })} on:save={e => updateApplication({ description: e.detail })}
thin thin
edit edit
value={$store.description} value={$store.description}
label="Description" /> label="App Description" />
</div> </div>
<style> <style>
.container { .container {
display: grid; display: grid;
grid-gap: 32px; grid-gap: var(--spacing-xl);
margin-top: 32px;
} }
</style> </style>

View file

@ -1,5 +1,5 @@
<script> <script>
import { Input, Select, Button } from "@budibase/bbui" import { Input, Select, Button, Label } from "@budibase/bbui"
import UserRow from "../UserRow.svelte" import UserRow from "../UserRow.svelte"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
@ -8,7 +8,7 @@
let username = "" let username = ""
let password = "" let password = ""
let accessLevelId let accessLevelId = "ADMIN"
$: valid = username && password && accessLevelId $: valid = username && password && accessLevelId
$: appId = $store.appId $: appId = $store.appId
@ -52,8 +52,8 @@
</script> </script>
<div class="container"> <div class="container">
<div class="background"> <div>
<div class="title">Create new user</div> <Label extraSmall grey>Create New User</Label>
<div class="inputs"> <div class="inputs">
<Input thin bind:value={username} name="Name" placeholder="Username" /> <Input thin bind:value={username} name="Name" placeholder="Username" />
<Input <Input
@ -61,20 +61,18 @@
bind:value={password} bind:value={password}
name="Password" name="Password"
placeholder="Password" /> placeholder="Password" />
<Select bind:value={accessLevelId} thin> <Select secondary bind:value={accessLevelId} thin>
<option value="">Choose an option</option> <option value="">Choose an option</option>
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option> <option value="POWER_USER">Power User</option>
</Select> </Select>
</div> <Button on:click={createUser} primary>Create</Button>
<div class="create-button">
<Button on:click={createUser} small primary>Create</Button>
</div> </div>
</div> </div>
<div class="background-users"> <div>
<div class="title">Current Users</div> <Label extraSmall grey>Current Users</Label>
{#await fetchUsersPromise} {#await fetchUsersPromise}
Loading state! Loading...
{:then users} {:then users}
<ul> <ul>
{#each users as user} {#each users as user}
@ -95,57 +93,21 @@
<style> <style>
.container { .container {
display: grid; display: grid;
grid-gap: 32px; grid-gap: var(--spacing-xl);
margin-top: 32px;
}
.background {
position: relative;
display: grid;
grid-gap: 12px;
border-radius: 5px;
} }
.background-users {
position: relative;
display: grid;
grid-gap: 12px;
border-radius: 5px;
}
.create-button {
position: absolute;
top: 0px;
right: 0px;
}
.create-button :global(button) {
font-size: var(--font-size-sm);
min-width: 100px;
border-radius: var(--rounded-small);
}
.title {
font-size: 14px;
font-weight: 500;
}
.inputs { .inputs {
display: grid; display: grid;
margin-top: 12px; justify-items: stretch;
grid-gap: 18px; grid-gap: var(--spacing-m);
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 140px;
}
.inputs :global(input) {
padding: 10px 12px;
border-radius: var(--rounded-small);
}
.inputs :global(select) {
padding: 10px 12px;
border-radius: var(--rounded-small);
background-color: var(--grey-2);
} }
ul { ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
display: grid; display: grid;
grid-gap: 8px; grid-gap: var(--spacing-m);
margin-top: 0; margin: 0;
} }
</style> </style>

View file

@ -1,60 +1,35 @@
<script> <script>
import Button from "components/common/Button.svelte" import { TextButton } from "@budibase/bbui"
import { Heading } from "@budibase/bbui"
import { Spacer } from "@budibase/bbui"
export let name, _id export let name, _id
</script> </script>
<div class="apps-card"> <div class="apps-card">
<h3 class="app-title">{name}</h3> <Heading small black>{name}</Heading>
<Spacer medium />
<div class="card-footer"> <div class="card-footer">
<a href={`/_builder/${_id}`} class="app-button">Open {name}</a> <TextButton text medium blue href="/_builder/{_id}">
Open {name}
</TextButton>
</div> </div>
</div> </div>
<style> <style>
.apps-card { .apps-card {
background-color: var(--white); background-color: var(--white);
padding: var(--spacing-xl); padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-xl)
var(--spacing-xl);
max-width: 300px; max-width: 300px;
max-height: 150px; max-height: 150px;
border-radius: var(--border-radius-m); border-radius: var(--border-radius-m);
border: var(--border-dark); border: var(--border-dark);
} }
.app-button:hover {
background-color: var(--white);
color: var(--black);
text-decoration: none;
}
.app-title {
font-size: var(--font-size-l);
font-weight: 600;
color: var(--ink);
text-transform: capitalize;
}
.card-footer { .card-footer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: baseline; align-items: baseline;
justify-content: space-between; justify-content: space-between;
} }
.app-button {
align-items: center;
display: flex;
background-color: var(--ink);
color: var(--white);
border: 1.5px var(--ink) solid;
width: 100%;
justify-content: center;
padding: 8px 16px;
border-radius: var(--border-radius-s);
font-size: var(--font-size-xs);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
font-family: var(--font-sans);
}
</style> </style>

View file

@ -19,7 +19,7 @@
</script> </script>
<div class="root"> <div class="root">
<Heading medium black>Your Apps</Heading> <Heading lh medium black>Your Apps</Heading>
{#await promise} {#await promise}
<div class="spinner-container"> <div class="spinner-container">
<Spinner size="30" /> <Spinner size="30" />
@ -48,8 +48,4 @@
grid-gap: var(--layout-m); grid-gap: var(--layout-m);
justify-content: start; justify-content: start;
} }
.root {
margin: 20px 80px;
}
</style> </style>

View file

@ -1,6 +1,5 @@
<script> <script>
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { store, automationStore, backendUiStore } from "builderStore" import { store, automationStore, backendUiStore } from "builderStore"
import { string, object } from "yup" import { string, object } from "yup"
import api, { get } from "builderStore/api" import api, { get } from "builderStore/api"
@ -11,12 +10,10 @@
import { Input, TextArea, Button } from "@budibase/bbui" import { Input, TextArea, Button } from "@budibase/bbui"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/" import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { post } from "builderStore/api" import { post } from "builderStore/api"
import analytics from "analytics" 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 //Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
const createAppStore = writable({ currentStep: 0, values: {} }) const createAppStore = writable({ currentStep: 0, values: {} })
@ -172,7 +169,7 @@
} }
const userResp = await api.post(`/api/users`, user) const userResp = await api.post(`/api/users`, user)
const json = await userResp.json() const json = await userResp.json()
$goto(`./${appJson._id}`) $goto(`/${appJson._id}`)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
@ -197,10 +194,6 @@
let onChange = () => {} let onChange = () => {}
function _onCancel() {
close()
}
async function _onOkay() { async function _onOkay() {
await createNewApp() await createNewApp()
} }
@ -253,9 +246,6 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="close-button" on:click={_onCancel}>
<CloseIcon />
</div>
<img src="/_builder/assets/bb-logo.svg" alt="budibase icon" /> <img src="/_builder/assets/bb-logo.svg" alt="budibase icon" />
{#if submitting} {#if submitting}
<div in:fade class="spinner-container"> <div in:fade class="spinner-container">
@ -280,16 +270,6 @@
align-content: center; align-content: center;
background: #f5f5f5; background: #f5f5f5;
} }
.close-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
}
.heading { .heading {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -1,20 +1,22 @@
<script> <script>
import { Input, Heading, Body } from "@budibase/bbui" import { Label, Heading, Input } from "@budibase/bbui"
export let validationErrors export let validationErrors
export let template export let template
let blurred = { appName: false } let blurred = { appName: false }
</script> </script>
{#if template}
<Heading small black>Selected Template</Heading>
<Body>{template.name}</Body>
{/if}
<h2>Create your web app</h2> <h2>Create your web app</h2>
<div class="container"> <div class="container">
{#if template}
<div class="template">
<Label extraSmall grey>Selected Template</Label>
<Heading small>{template.name}</Heading>
</div>
{/if}
<Input <Input
on:input={() => (blurred.appName = true)} on:input={() => (blurred.appName = true)}
label="Web app name" label="Web App Name"
name="applicationName" name="applicationName"
placeholder="Enter name of your web application" placeholder="Enter name of your web application"
type="name" type="name"
@ -24,6 +26,11 @@
<style> <style>
.container { .container {
display: grid; display: grid;
grid-gap: 40px; grid-gap: var(--spacing-xl);
}
.template :global(label) {
/* Fix layout due to LH 0 on heading */
margin-bottom: 16px;
} }
</style> </style>

View file

@ -21,7 +21,7 @@
placeholder="Password" placeholder="Password"
type="password" type="password"
error={blurred.password && validationErrors.password} /> error={blurred.password && validationErrors.password} />
<Select secondary name="accessLevelId"> <Select label="Access Level" secondary name="accessLevelId">
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option> <option value="POWER_USER">Power User</option>
</Select> </Select>
@ -30,6 +30,6 @@
<style> <style>
.container { .container {
display: grid; display: grid;
grid-gap: 40px; grid-gap: var(--spacing-xl);
} }
</style> </style>

View file

@ -1,5 +1,5 @@
<script> <script>
import { Button, Heading, Body } from "@budibase/bbui" import { Button, Heading, Body, Spacer } from "@budibase/bbui"
import AppCard from "./AppCard.svelte" import AppCard from "./AppCard.svelte"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import api from "builderStore/api" import api from "builderStore/api"
@ -15,7 +15,7 @@
</script> </script>
<div class="root"> <div class="root">
<Heading medium black>Start With a Template</Heading> <Heading lh medium black>Start With a Template</Heading>
{#await templatesPromise} {#await templatesPromise}
<div class="spinner-container"> <div class="spinner-container">
<Spinner size="30" /> <Spinner size="30" />
@ -24,11 +24,12 @@
<div class="templates"> <div class="templates">
{#each templates as template} {#each templates as template}
<div class="templates-card"> <div class="templates-card">
<Heading black medium>{template.name}</Heading> <Heading black small>{template.name}</Heading>
<Spacer small />
<Body medium grey>{template.category}</Body> <Body medium grey>{template.category}</Body>
<Body small black>{template.description}</Body> <Body lh small black>{template.description}</Body>
<div> <div>
<img src={template.image} width="300" /> <img src={template.image} width="100%" />
</div> </div>
<div class="card-footer"> <div class="card-footer">
<Button secondary on:click={() => onSelect(template)}> <Button secondary on:click={() => onSelect(template)}>
@ -68,8 +69,4 @@
color: var(--ink); color: var(--ink);
text-transform: capitalize; text-transform: capitalize;
} }
.root {
margin: 20px 80px;
}
</style> </style>

View file

@ -42,8 +42,8 @@
height: 24px; height: 24px;
width: 24px; width: 24px;
background: var(--grey-4); background: var(--grey-4);
right: 7px; right: var(--spacing-s);
bottom: 7px; bottom: 9px;
} }
button:hover { button:hover {
background: var(--grey-5); background: var(--grey-5);

View file

@ -1,6 +1,13 @@
<script> <script>
import groupBy from "lodash/fp/groupBy" import groupBy from "lodash/fp/groupBy"
import { Button, TextArea, Label, Body } from "@budibase/bbui" import {
Button,
TextArea,
Label,
Body,
Heading,
Spacer,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -26,9 +33,10 @@
<div class="container" data-cy="binding-dropdown-modal"> <div class="container" data-cy="binding-dropdown-modal">
<div class="list"> <div class="list">
<Label size="l" color="dark">Objects</Label> <Heading extraSmall>Objects</Heading>
<Spacer medium />
{#if context} {#if context}
<Label size="s" color="dark">Table</Label> <Heading extraSmall>Tables</Heading>
<ul> <ul>
{#each context as { readableBinding }} {#each context as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>{readableBinding}</li> <li on:click={() => addToText(readableBinding)}>{readableBinding}</li>
@ -36,7 +44,7 @@
</ul> </ul>
{/if} {/if}
{#if instance} {#if instance}
<Label size="s" color="dark">Components</Label> <Heading extraSmall>Components</Heading>
<ul> <ul>
{#each instance as { readableBinding }} {#each instance as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>{readableBinding}</li> <li on:click={() => addToText(readableBinding)}>{readableBinding}</li>
@ -45,15 +53,21 @@
{/if} {/if}
</div> </div>
<div class="text"> <div class="text">
<Label size="l" color="dark">Data binding</Label> <Heading extraSmall>Data binding</Heading>
<Body size="s" color="dark"> <Spacer small />
<Body extraSmall lh>
Binding connects one piece of data to another and makes it dynamic. Click Binding connects one piece of data to another and makes it dynamic. Click
the objects on the left, to add them to the textbox. the objects on the left, to add them to the textbox.
</Body> </Body>
<TextArea bind:value placeholder="" /> <Spacer large />
<TextArea
thin
bind:value
placeholder="Add text, or lick the objects on the left to add them to the
textbox." />
<div class="controls"> <div class="controls">
<a href="#"> <a href="https://docs.budibase.com/design/binding">
<Body size="s" color="light">Learn more about binding</Body> <Body small grey>Learn more about binding</Body>
</a> </a>
<Button on:click={cancel} secondary>Cancel</Button> <Button on:click={cancel} secondary>Cancel</Button>
<Button on:click={close} primary>Done</Button> <Button on:click={close} primary>Done</Button>
@ -73,17 +87,19 @@
.controls { .controls {
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
display: grid; display: grid;
align-items: center; align-items: baseline;
grid-gap: var(--spacing-l); grid-gap: var(--spacing-l);
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto auto;
} }
.list { .list {
width: 150px; width: 150px;
border-right: 1.5px solid var(--grey-4); border-right: 1.5px solid var(--grey-4);
padding: var(--spacing-xl);
} }
.text { .text {
width: 600px; width: 600px;
display: grid; padding: var(--spacing-xl);
font-family: var(--font-sans);
} }
.text :global(p) { .text :global(p) {
margin: 0; margin: 0;
@ -99,15 +115,16 @@
display: flex; display: flex;
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--ink); color: var(--grey-7);
padding: var(--spacing-s) var(--spacing-m); padding: var(--spacing-s) 0;
margin: auto 0px; margin: auto 0px;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
} }
li:hover { li:hover {
background-color: var(--grey-2); color: var(--ink);
font-weight: 500;
} }
li:active { li:active {

View file

@ -1,5 +1,4 @@
<script> <script>
import { MoreIcon } from "components/common/Icons"
import { store } from "builderStore" import { store } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils" import { getComponentDefinition } from "builderStore/storeUtils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -109,9 +108,9 @@
</script> </script>
<div bind:this={anchor} on:click|stopPropagation={() => {}}> <div bind:this={anchor} on:click|stopPropagation={() => {}}>
<button on:click={dropdown.show}> <div class="icon" on:click={dropdown.show}>
<MoreIcon /> <i class="ri-more-line" />
</button> </div>
</div> </div>
<DropdownMenu <DropdownMenu
class="menu" class="menu"
@ -122,27 +121,27 @@
align="left"> align="left">
<ul> <ul>
<li class="item" on:click={() => confirmDeleteDialog.show()}> <li class="item" on:click={() => confirmDeleteDialog.show()}>
<i class="icon ri-delete-bin-2-line" /> <i class="ri-delete-bin-2-line" />
Delete Delete
</li> </li>
<li class="item" on:click={moveUpComponent}> <li class="item" on:click={moveUpComponent}>
<i class="icon ri-arrow-up-line" /> <i class="ri-arrow-up-line" />
Move up Move up
</li> </li>
<li class="item" on:click={moveDownComponent}> <li class="item" on:click={moveDownComponent}>
<i class="icon ri-arrow-down-line" /> <i class="ri-arrow-down-line" />
Move down Move down
</li> </li>
<li class="item" on:click={copyComponent}> <li class="item" on:click={copyComponent}>
<i class="icon ri-repeat-one-line" /> <i class="ri-repeat-one-line" />
Duplicate Duplicate
</li> </li>
<li class="item" on:click={() => storeComponentForCopy(true)}> <li class="item" on:click={() => storeComponentForCopy(true)}>
<i class="icon ri-scissors-cut-line" /> <i class="ri-scissors-cut-line" />
Cut Cut
</li> </li>
<li class="item" on:click={() => storeComponentForCopy(false)}> <li class="item" on:click={() => storeComponentForCopy(false)}>
<i class="icon ri-file-copy-line" /> <i class="ri-file-copy-line" />
Copy Copy
</li> </li>
<hr class="hr-style" /> <hr class="hr-style" />
@ -150,21 +149,21 @@
class="item" class="item"
class:disabled={noPaste} class:disabled={noPaste}
on:click={() => pasteComponent('above')}> on:click={() => pasteComponent('above')}>
<i class="icon ri-insert-row-top" /> <i class="ri-insert-row-top" />
Paste above Paste above
</li> </li>
<li <li
class="item" class="item"
class:disabled={noPaste} class:disabled={noPaste}
on:click={() => pasteComponent('below')}> on:click={() => pasteComponent('below')}>
<i class="icon ri-insert-row-bottom" /> <i class="ri-insert-row-bottom" />
Paste below Paste below
</li> </li>
<li <li
class="item" class="item"
class:disabled={noPaste || noChildrenAllowed} class:disabled={noPaste || noChildrenAllowed}
on:click={() => pasteComponent('inside')}> on:click={() => pasteComponent('inside')}>
<i class="icon ri-insert-column-right" /> <i class="ri-insert-column-right" />
Paste inside Paste inside
</li> </li>
</ul> </ul>
@ -181,7 +180,7 @@
ul { ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: var(--spacing-s) 0;
} }
li { li {
@ -190,44 +189,29 @@
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--ink); color: var(--ink);
padding: var(--spacing-s) var(--spacing-m); padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px; margin: auto 0;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
} }
li:not(.disabled):hover {
button {
border-style: none;
border-radius: 2px;
padding: 0;
background: transparent;
cursor: pointer;
color: var(--ink);
outline: none;
}
li:hover {
background-color: var(--grey-2); background-color: var(--grey-2);
} }
li:active { li:active {
color: var(--blue); color: var(--blue);
} }
li i {
.item {
display: flex;
align-items: center;
font-size: 14px;
}
.icon {
margin-right: 8px; margin-right: 8px;
font-size: var(--font-size-s);
} }
li.disabled {
.disabled {
color: var(--grey-4); color: var(--grey-4);
cursor: default; cursor: default;
} }
.icon i {
font-size: 16px;
}
.hr-style { .hr-style {
margin: 8px 0; margin: 8px 0;
color: var(--grey-4); color: var(--grey-4);

View file

@ -190,9 +190,9 @@
.item { .item {
display: grid; display: grid;
grid-template-columns: 1fr auto auto auto; grid-template-columns: 1fr auto auto auto;
padding: 0px 5px 0px 15px; padding: 0 var(--spacing-m);
margin: auto 0px; margin: 0;
border-radius: 5px; border-radius: var(--border-radius-m);
height: 36px; height: 36px;
align-items: center; align-items: center;
} }
@ -205,9 +205,6 @@
.actions { .actions {
display: none; display: none;
color: var(--ink); color: var(--ink);
padding: 0 5px;
width: 24px;
height: 24px;
border-style: none; border-style: none;
background: rgba(0, 0, 0, 0); background: rgba(0, 0, 0, 0);
cursor: pointer; cursor: pointer;

View file

@ -1,6 +1,12 @@
<script> <script>
import { store } from "builderStore" import { store } from "builderStore"
import { TextButton, Button, Heading, DropdownMenu } from "@budibase/bbui" import {
TextButton,
Button,
Body,
DropdownMenu,
ModalContent,
} from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/" import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers" import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
import actionTypes from "./actions" import actionTypes from "./actions"
@ -22,12 +28,6 @@
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME]) actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
.component .component
const closeModal = () => {
dispatch("close")
draftEventHandler = { parameters: [] }
actions = []
}
const updateEventHandler = (updatedHandler, index) => { const updateEventHandler = (updatedHandler, index) => {
actions[index] = updatedHandler actions[index] = updatedHandler
} }
@ -54,20 +54,17 @@
const saveEventData = () => { const saveEventData = () => {
dispatch("change", actions) dispatch("change", actions)
closeModal()
} }
</script> </script>
<div class="root"> <ModalContent title="Actions" confirmText="Save" onConfirm={saveEventData}>
<div slot="header">
<div class="header">
<Heading small dark>Actions</Heading>
<div bind:this={addActionButton}> <div bind:this={addActionButton}>
<TextButton text small blue on:click={addActionDropdown.show}> <TextButton text small blue on:click={addActionDropdown.show}>
Add Action
<div style="height: 20px; width: 20px;"> <div style="height: 20px; width: 20px;">
<AddIcon /> <AddIcon />
</div> </div>
Add Action
</TextButton> </TextButton>
</div> </div>
<DropdownMenu <DropdownMenu
@ -89,11 +86,7 @@
{#each actions as action, index} {#each actions as action, index}
<div class="action-container"> <div class="action-container">
<div class="action-header" on:click={selectAction(action)}> <div class="action-header" on:click={selectAction(action)}>
<p <Body small lh>{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}</Body>
class="bb-body bb-body--small bb-body--color-dark"
style="margin: var(--spacing-s) 0;">
{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}
</p>
<div class="row-expander" class:rotate={action !== selectedAction}> <div class="row-expander" class:rotate={action !== selectedAction}>
<ArrowDownIcon /> <ArrowDownIcon />
</div> </div>
@ -115,30 +108,12 @@
{/if} {/if}
</div> </div>
<div class="footer"> <div slot="footer">
<a href="https://docs.budibase.com">Learn more about Actions</a> <a href="https://docs.budibase.com">Learn more about Actions</a>
<Button secondary on:click={closeModal}>Cancel</Button>
<Button primary on:click={saveEventData}>Save</Button>
</div> </div>
</div> </ModalContent>
<style> <style>
.root {
max-height: 50vh;
width: 700px;
display: flex;
flex-direction: column;
}
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: var(--spacing-xl);
padding-bottom: 0;
}
.action-header { .action-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -166,8 +141,7 @@
.actions-container { .actions-container {
flex: 1; flex: 1;
min-height: 0px; min-height: 0;
padding-bottom: var(--spacing-s);
padding-top: 0; padding-top: 0;
border: var(--border-light); border: var(--border-light);
border-width: 0 0 1px 0; border-width: 0 0 1px 0;
@ -177,10 +151,6 @@
.action-container { .action-container {
border: var(--border-light); border: var(--border-light);
border-width: 1px 0 0 0; border-width: 1px 0 0 0;
padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl);
padding-top: 0;
padding-bottom: 0;
} }
.selected-action-container { .selected-action-container {
@ -195,22 +165,13 @@
flex-direction: row; flex-direction: row;
} }
.footer { a {
display: flex;
flex-direction: row;
gap: var(--spacing-s);
padding: var(--spacing-xl);
padding-top: var(--spacing-m);
}
.footer > a {
flex: 1; flex: 1;
color: var(--grey-5); color: var(--grey-5);
font-size: var(--font-size-s); font-size: var(--font-size-s);
text-decoration: none; text-decoration: none;
} }
a:hover {
.footer > a:hover {
color: var(--blue); color: var(--blue);
} }

View file

@ -1,25 +1,17 @@
<script> <script>
import { Button, Modal } from "@budibase/bbui" import { Button, Modal } from "@budibase/bbui"
import EventEditorModal from "./EventEditorModal.svelte" import EventEditorModal from "./EventEditorModal.svelte"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value
export let name export let name
let eventsModal let modal
</script> </script>
<Button secondary small on:click={eventsModal.show}>Define Actions</Button> <Button secondary small on:click={modal.show}>Define Actions</Button>
<Modal bind:this={eventsModal} maxWidth="100vw" hideCloseButton padding="0"> <Modal bind:this={modal} width="600px">
<EventEditorModal <EventEditorModal event={value} eventType={name} on:change />
event={value}
eventType={name}
on:change
on:close={eventsModal.hide} />
</Modal> </Modal>
<style>
</style>

View file

@ -1,29 +1,14 @@
<script> <script>
import { getContext } from "svelte" import { keys, map, includes, filter } from "lodash/fp"
import {
keys,
map,
some,
includes,
cloneDeep,
isEqual,
sortBy,
filter,
difference,
} from "lodash/fp"
import { pipe } from "components/common/core"
import Checkbox from "components/common/Checkbox.svelte"
import EventEditorModal from "./EventEditorModal.svelte" import EventEditorModal from "./EventEditorModal.svelte"
import { Modal } from "@budibase/bbui"
import { PencilIcon } from "components/common/Icons"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
export const EVENT_TYPE = "event" export const EVENT_TYPE = "event"
export let component export let component
let events = [] let events = []
let selectedEvent = null let selectedEvent = null
let modal
$: { $: {
events = Object.keys(component) events = Object.keys(component)
@ -35,28 +20,9 @@
})) }))
} }
// Handle create app modal
const { open, close } = getContext("simple-modal")
const openModal = event => { const openModal = event => {
selectedEvent = event selectedEvent = event
open( modal.show()
EventEditorModal,
{
eventOptions: events,
event: selectedEvent,
onClose: () => {
close()
selectedEvent = null
},
},
{
closeButton: false,
closeOnEsc: false,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
} }
</script> </script>
@ -81,6 +47,10 @@
</form> </form>
</div> </div>
<Modal bind:this={modal} width="600px">
<EventEditorModal eventOptions={events} event={selectedEvent} />
</Modal>
<style> <style>
.root { .root {
font-size: 10pt; font-size: 10pt;

View file

@ -60,7 +60,7 @@
align-items: baseline; align-items: baseline;
} }
.root :global(.relative:nth-child(2)) { .root :global(> div:nth-child(2)) {
grid-column-start: 2; grid-column-start: 2;
grid-column-end: 6; grid-column-end: 6;
} }

View file

@ -22,7 +22,7 @@
align-items: baseline; align-items: baseline;
} }
.root :global(.relative) { .root :global(> div) {
flex: 1; flex: 1;
margin-left: var(--spacing-l); margin-left: var(--spacing-l);
} }

View file

@ -119,7 +119,7 @@
align-items: baseline; align-items: baseline;
} }
.root :global(.relative:nth-child(2)) { .root :global(> div:nth-child(2)) {
grid-column-start: 2; grid-column-start: 2;
grid-column-end: 6; grid-column-end: 6;
} }

View file

@ -1,57 +1,25 @@
<script> <script>
import { store, backendUiStore } from "builderStore" import { store } from "builderStore"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte" import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte" import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte" import PagesList from "components/userInterface/PagesList.svelte"
import NewScreen from "components/userInterface/NewScreen.svelte" import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import { Button, Spacer, Modal } from "@budibase/bbui"
const newScreen = () => { let modal
newScreenPicker.show()
}
let newScreenPicker
</script> </script>
<PagesList /> <PagesList />
<button class="newscreen" on:click={newScreen}>Create New Screen</button> <Spacer medium />
<Button primary wide on:click={modal.show}>Create New Screen</Button>
<Spacer medium />
<PageLayout layout={$store.pages[$store.currentPageName]} /> <PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="nav-items-container"> <div class="nav-items-container">
<ComponentsHierarchy screens={$store.screens} /> <ComponentsHierarchy screens={$store.screens} />
</div> </div>
<NewScreen bind:this={newScreenPicker} /> <Modal bind:this={modal}>
<NewScreenModal />
<style> </Modal>
.newscreen {
cursor: pointer;
border: 1px solid var(--purple);
border-radius: 5px;
width: 100%;
height: 36px;
padding: 8px 16px;
margin: 20px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: var(--purple);
color: var(--white);
font-size: 14px;
font-weight: 500;
transition: all 3ms;
outline: none;
}
.newscreen:hover {
background: var(--purple-light);
color: var(--purple);
}
.icon {
color: var(--ink);
font-size: 16px;
margin-right: 4px;
}
</style>

View file

@ -1,6 +1,14 @@
<script> <script>
import groupBy from "lodash/fp/groupBy" import groupBy from "lodash/fp/groupBy"
import { TextArea, Label, Body, Button, Popover } from "@budibase/bbui" import {
TextArea,
Label,
Heading,
Body,
Spacer,
Button,
Popover,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -20,11 +28,12 @@
<Popover {anchor} {align} bind:this={popover}> <Popover {anchor} {align} bind:this={popover}>
<div class="container"> <div class="container">
<div class="bindings"> <div class="bindings">
<Label large>Available bindings</Label> <Heading small>Available bindings</Heading>
<div class="bindings__wrapper"> <div class="bindings__wrapper">
<div class="bindings__list"> <div class="bindings__list">
{#each categories as [categoryName, bindings]} {#each categories as [categoryName, bindings]}
<Label small>{categoryName}</Label> <Heading extraSmall>{categoryName}</Heading>
<Spacer extraSmall />
{#each bindings as binding} {#each bindings as binding}
<div class="binding" on:click={() => onClickBinding(binding)}> <div class="binding" on:click={() => onClickBinding(binding)}>
<span class="binding__label">{binding.label}</span> <span class="binding__label">{binding.label}</span>
@ -38,14 +47,17 @@
</div> </div>
</div> </div>
<div class="editor"> <div class="editor">
<Label large>Data binding</Label> <Heading small>Data binding</Heading>
<Body small> <Body small lh black>
Binding connects one piece of data to another and makes it dynamic. Binding connects one piece of data to another and makes it dynamic.
Click the objects on the left to add them to the textbox. Click the objects on the left to add them to the textbox.
</Body> </Body>
<TextArea thin bind:value placeholder="..." /> <TextArea
thin
bind:value
placeholder="Add options from the left, type text, or do both" />
<div class="controls"> <div class="controls">
<a href="#"> <a href="https://docs.budibase.com/design/binding">
<Body small>Learn more about binding</Body> <Body small>Learn more about binding</Body>
</a> </a>
<Button on:click={popover.hide} primary>Done</Button> <Button on:click={popover.hide} primary>Done</Button>

View file

@ -1,16 +1,16 @@
<script> <script>
import { Button, Icon, DropdownMenu } from "@budibase/bbui" import { Button, Icon, DropdownMenu, Spacer, Heading } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let anchor, dropdown let anchorRight, dropdownRight
export let value = {} export let value = {}
function handleSelected(selected) { function handleSelected(selected) {
dispatch("change", selected) dispatch("change", selected)
dropdown.hide() dropdownRight.hide()
} }
const models = $backendUiStore.models.map(m => ({ const models = $backendUiStore.models.map(m => ({
@ -30,21 +30,17 @@
}, []) }, [])
</script> </script>
<div bind:this={anchor}> <div class="dropdownbutton" bind:this={anchorRight}>
<Button secondary small on:click={dropdown.show}> <Button secondary wide on:click={dropdownRight.show}>
<span>{value.label ? value.label : 'Model / View'}</span> <span>{value.label ? value.label : 'Model / View'}</span>
<Icon name="arrowdown" /> <Icon name="arrowdown" />
</Button> </Button>
</div> </div>
<DropdownMenu <DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
bind:this={dropdown} <div class="dropdown">
width="175px" <div class="title">
borderColor="#d1d1d1ff" <Heading extraSmall>Tables</Heading>
{anchor} </div>
align="right">
<div class="model-view-container">
<p>Tables</p>
<ul> <ul>
{#each models as model} {#each models as model}
<li <li
@ -55,7 +51,9 @@
{/each} {/each}
</ul> </ul>
<hr /> <hr />
<p>Views</p> <div class="title">
<Heading extraSmall>Views</Heading>
</div>
<ul> <ul>
{#each views as view} {#each views as view}
<li <li
@ -69,23 +67,19 @@
</DropdownMenu> </DropdownMenu>
<style> <style>
.model-view-container { .dropdownbutton {
padding-bottom: 8px; width: 100%;
font: var(--smallheavybodytext);
} }
.dropdown {
p { padding: var(--spacing-m) 0;
color: var(--grey-7); z-index: 99999999;
margin: 0px;
padding: 8px;
} }
.title {
span { padding: 0 var(--spacing-m) var(--spacing-xs) var(--spacing-m);
text-transform: capitalize;
} }
hr { hr {
margin: 10px 0px 5px 0px; margin: var(--spacing-m) 0 var(--spacing-xl) 0;
} }
ul { ul {
@ -97,7 +91,8 @@
li { li {
cursor: pointer; cursor: pointer;
margin: 0px; margin: 0px;
padding: 5px 8px; padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-xs);
} }
.selected { .selected {

View file

@ -1,117 +0,0 @@
<script>
import { store } from "builderStore"
import { pipe } from "components/common/core"
import { isRootComponent } from "./pagesParsing/searchComponents"
import { splitName } from "./pagesParsing/splitRootComponentName.js"
import { Input, Select, Modal, Button, Spacer } from "@budibase/bbui"
import { find, filter, some, map, includes } from "lodash/fp"
import { assign } from "lodash"
export const show = () => {
dialog.show()
}
let dialog
let layoutComponents
let layoutComponent
let screens
let name = ""
let routeError
$: layoutComponents = Object.values($store.components).filter(
componentDefinition => componentDefinition.container
)
$: layoutComponent = layoutComponent
? layoutComponents.find(
component => component._component === layoutComponent._component
)
: layoutComponents[0]
$: route = !route && $store.screens.length === 0 ? "*" : route
const save = () => {
if (!route) {
routeError = "Url is required"
} else {
if (routeNameExists(route)) {
routeError = "This url is already taken"
} else {
routeError = ""
}
}
if (routeError) return false
store.createScreen(name, route, layoutComponent._component)
name = ""
route = ""
dialog.hide()
}
const cancel = () => {
dialog.hide()
}
const routeNameExists = route => {
return $store.screens.some(
screen => screen.route.toLowerCase() === route.toLowerCase()
)
}
const routeChanged = event => {
if (!event.target.value.startsWith("/")) {
route = "/" + event.target.value
}
}
</script>
<Modal bind:this={dialog} minWidth="500px">
<h2>New Screen</h2>
<Spacer extraLarge />
<div data-cy="new-screen-dialog">
<div class="bb-margin-xl">
<Input label="Name" bind:value={name} />
</div>
<div class="bb-margin-xl">
<Input
label="Url"
error={routeError}
bind:value={route}
on:change={routeChanged} />
</div>
<div class="bb-margin-xl">
<label>Layout Component</label>
<Select bind:value={layoutComponent} secondary>
{#each layoutComponents as { _component, name }}
<option value={_component}>{name}</option>
{/each}
</Select>
</div>
</div>
<Spacer extraLarge />
<div data-cy="create-screen-footer" class="modal-footer">
<Button secondary medium on:click={cancel}>Cancel</Button>
<Button blue medium on:click={save}>Create Screen</Button>
</div>
</Modal>
<style>
h2 {
font-size: var(--font-size-xl);
margin: 0;
font-family: var(--font-sans);
font-weight: 600;
}
.modal-footer {
display: flex;
justify-content: space-between;
}
</style>

View file

@ -0,0 +1,68 @@
<script>
import { store } from "builderStore"
import { Input, Select, ModalContent } from "@budibase/bbui"
import { find, filter, some } from "lodash/fp"
let dialog
let layoutComponents
let layoutComponent
let screens
let name = ""
let routeError
$: layoutComponents = Object.values($store.components).filter(
componentDefinition => componentDefinition.container
)
$: layoutComponent = layoutComponent
? layoutComponents.find(
component => component._component === layoutComponent._component
)
: layoutComponents[0]
$: route = !route && $store.screens.length === 0 ? "*" : route
const save = () => {
if (!route) {
routeError = "Url is required"
} else {
if (routeNameExists(route)) {
routeError = "This url is already taken"
} else {
routeError = ""
}
}
if (routeError) {
return false
}
store.createScreen(name, route, layoutComponent._component)
name = ""
route = ""
}
const routeNameExists = route => {
return $store.screens.some(
screen => screen.route.toLowerCase() === route.toLowerCase()
)
}
const routeChanged = event => {
if (!event.target.value.startsWith("/")) {
route = "/" + event.target.value
}
}
</script>
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}>
<Input label="Name" bind:value={name} />
<Input
label="Url"
error={routeError}
bind:value={route}
on:change={routeChanged} />
<Select label="Layout Component" bind:value={layoutComponent} secondary>
{#each layoutComponents as { _component, name }}
<option value={_component}>{name}</option>
{/each}
</Select>
</ModalContent>

Some files were not shown because too many files have changed in this diff Show more