1
0
Fork 0
mirror of synced 2024-06-27 02:20:35 +12:00

merge from master - including fixes

This commit is contained in:
Michael Shanks 2020-10-08 16:15:12 +01:00
parent df4243abfa
commit eaa8d91392
148 changed files with 3645 additions and 3984 deletions

View file

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

View file

@ -1,70 +1,65 @@
context('Create a Table', () => {
before(() => {
cy.visit('localhost:4001/_builder')
cy.createApp('Table App', 'Table App Description')
})
context("Create a Table", () => {
before(() => {
cy.visit("localhost:4001/_builder")
cy.createApp("Table App", "Table App Description")
})
it('should create a new Table', () => {
cy.createTable('dog')
it("should create a new Table", () => {
cy.createTable("dog")
// Check if Table exists
cy.get('.title').should('contain.text', 'dog')
})
// Check if Table exists
cy.get(".title span").should("have.text", "dog")
})
it('adds a new column to the table', () => {
cy.addColumn('dog', 'name', 'Plain Text')
it("adds a new column to the table", () => {
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', () => {
cy.addRecord(["Rover"])
it("updates a column on the table", () => {
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', () => {
cy.contains("name").click()
cy.get("[data-cy='edit-column-header']").click()
it("deletes a record", () => {
cy.get("tbody .ri-more-line").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")
cy.get("select").select("Plain Text")
cy.contains("Save Column").click()
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 column", () => {
cy.contains("name").click()
cy.get("[data-cy='delete-column-header']").click()
cy.contains("Delete Column").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.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', () => {
before(() => {
cy.visit('localhost:4001/_builder')
cy.createApp('View App', 'View App Description')
cy.createTable('data')
cy.addColumn('data', 'group', 'Plain Text')
cy.addColumn('data', 'age', 'Number')
cy.addColumn('data', 'rating', 'Number')
// 6 Records
cy.addRecord(["Students", 25, 1])
cy.addRecord(["Students", 20, 3])
cy.addRecord(["Students", 18, 6])
cy.addRecord(["Students", 25, 2])
cy.addRecord(["Teachers", 49, 5])
cy.addRecord(["Teachers", 36, 3])
})
// 6 Records
cy.addRecord(["Students", 25, 1])
cy.addRecord(["Students", 20, 3])
cy.addRecord(["Students", 18, 6])
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")
it("creates a view", () => {
cy.contains("Create New View").click()
cy.get(".menu-container").within(() => {
cy.get("input").type("Test View")
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', () => {
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")
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").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())
expect(values.get()).to.deep.eq([
"Students",
@ -94,24 +103,30 @@ context('Create a View', () => {
"25",
"3",
"1650",
"23.333333333333332"
"23.333333333333332",
])
})
})
})
it('renames a view', () => {
cy.contains("[data-cy=model-nav-item]", "Test View").find(".ri-more-line").click()
cy.contains("Edit").click()
cy.get("[placeholder='View Name']").type(" Updated")
it("renames a view", () => {
cy.contains("[data-cy=model-nav-item]", "Test View")
.find(".ri-more-line")
.click()
cy.contains("Edit").click()
cy.get(".menu-container").within(() => {
cy.get("input").type(" Updated")
cy.contains("Save").click()
cy.contains("Test View Updated").should("be.visible")
})
cy.contains("Test View Updated").should("be.visible")
})
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").find(".ri-more-line").click()
cy.contains("Delete").click()
cy.get(".content").contains("button", "Delete").click()
cy.contains("TestView Updated").should("not.be.visible")
})
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")
.find(".ri-more-line")
.click()
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", () => {
cy.createTable("dog")
cy.addColumn("dog", "name", "Plain Text")
cy.addColumn("dog", "name", "Text")
cy.addColumn("dog", "age", "Number")
})
Cypress.Commands.add("createTable", tableName => {
// Enter model name
cy.contains("Create New Table").click()
cy.get("[placeholder='Table Name']").type(tableName)
cy.contains("Save").click()
cy.get(".modal").within(() => {
cy.get("input")
.first()
.type(tableName)
cy.get(".buttons")
.contains("Create")
.click()
})
cy.contains(tableName).should("be.visible")
})
@ -77,25 +82,31 @@ Cypress.Commands.add("addColumn", (tableName, columnName, type) => {
cy.contains(tableName).click()
cy.contains("Create New Column").click()
cy.get("[placeholder=Name]").type(columnName)
cy.get("select").select(type)
cy.contains("Save Column")
cy.contains("Save").click()
// Configure column
cy.get(".menu-container").within(() => {
cy.get("input")
.first()
.type(columnName)
cy.get("select").select(type)
cy.contains("Save").click()
})
})
Cypress.Commands.add("addRecord", values => {
cy.contains("Create New Row").click()
for (let i = 0; i < values.length; i++) {
cy.get(".actions input")
.eq(i)
.type(values[i])
}
cy.get(".modal").within(() => {
for (let i = 0; i < values.length; i++) {
cy.get("input")
.eq(i)
.type(values[i])
}
// Save
cy.contains("Save").click()
// Save
cy.get(".buttons")
.contains("Create")
.click()
})
})
Cypress.Commands.add("createUser", (username, password, accessLevel) => {
@ -114,7 +125,9 @@ Cypress.Commands.add("createUser", (username, password, accessLevel) => {
.select(accessLevel)
// Save
cy.get(".create-button > button").click()
cy.get(".inputs")
.contains("Create")
.click()
})
Cypress.Commands.add("addHeadlineComponent", text => {
@ -138,12 +151,12 @@ Cypress.Commands.add("navigateToFrontend", () => {
})
Cypress.Commands.add("createScreen", (screenName, route) => {
cy.get(".newscreen").click()
cy.get("[data-cy=new-screen-dialog] input:first").type(screenName)
if (route) {
cy.get("[data-cy=new-screen-dialog] input:last").type(route)
}
cy.get("[data-cy=create-screen-footer]").within(() => {
cy.contains("Create New Screen").click()
cy.get(".modal").within(() => {
cy.get("input:first").type(screenName)
if (route) {
cy.get("input:last").type(route)
}
cy.contains("Create Screen").click()
})
cy.get(".nav-items-container").within(() => {

View file

@ -63,7 +63,7 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.34.6",
"@budibase/bbui": "^1.41.0",
"@budibase/client": "^0.1.25",
"@budibase/colorpicker": "^1.0.1",
"@fortawesome/fontawesome-free": "^5.14.0",
@ -79,7 +79,6 @@
"shortid": "^2.2.15",
"svelte-loading-spinners": "^0.1.1",
"svelte-portal": "^0.1.0",
"svelte-simple-modal": "^0.4.2",
"yup": "^0.29.2"
},
"devDependencies": {
@ -113,7 +112,7 @@
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-url": "^2.2.2",
"start-server-and-test": "^1.11.0",
"svelte": "^3.24.1",
"svelte": "^3.29.0",
"svelte-jester": "^1.0.6"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"

View file

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

View file

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

View file

@ -37,9 +37,9 @@ export default function({ componentInstanceId, screen, components, models }) {
.filter(isInstanceInSharedContext(walkResult))
.map(componentInstanceToBindable(walkResult)),
...walkResult.target._contexts
...(walkResult.target?._contexts
.map(contextToBindables(models, walkResult))
.flat(),
.flat() ?? []),
]
}
@ -73,6 +73,15 @@ const componentInstanceToBindable = walkResult => i => {
const contextToBindables = (models, 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 => ({
type: "context",
@ -80,15 +89,12 @@ const contextToBindables = (models, walkResult) => context => {
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${contextParentPath}data.${key}`,
// 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
// ... this allows us to bind to Model scheams, or View schemas
const model = models.find(m => m._id === context.model.modelId)
const schema = context.model.isModel
? model.schema
: model.views[context.model.name].schema
// ... this allows us to bind to Model schemas, or View schemas
const schema = isModel ? model.schema : model.views[context.model.name].schema
return (
Object.keys(schema)

View file

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

View file

@ -3,6 +3,7 @@
import Arrow from "./Arrow.svelte"
import { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition"
import { automationStore } from "builderStore"
export let automation
export let onSelect
@ -17,8 +18,16 @@
blocks = blocks.concat(automation.definition.steps || [])
}
}
$: automationCount = $automationStore.automations?.length ?? 0
</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">
{#each blocks as block, idx (block.id)}
<div
@ -35,6 +44,13 @@
</section>
<style>
i {
font-size: var(--font-size-xl);
color: var(--grey-4);
padding: var(--spacing-xl) 40px;
align-self: flex-start;
}
section {
position: absolute;
padding: 40px;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,27 +1,19 @@
<script>
import { getContext } from "svelte"
import { backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
import DeleteAutomationModal from "./DeleteAutomationModal.svelte"
import { Button, Input, Label } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
let selectedTab = "SETUP"
let confirmDeleteDialog
$: instanceId = $backendUiStore.selectedDatabase._id
$: automation = $automationStore.selectedAutomation?.automation
$: allowDeleteBlock =
$automationStore.selectedBlock?.type !== "TRIGGER" ||
!automation?.definition?.steps?.length
function deleteAutomation() {
open(
DeleteAutomationModal,
{ onClosed: close },
{ styleContent: { padding: "0" } }
)
}
$: name = automation?.name ?? ""
function deleteAutomationBlock() {
automationStore.actions.deleteAutomationBlock(
@ -42,11 +34,19 @@
async function saveAutomation() {
await automationStore.actions.save({
instanceId: $backendUiStore.selectedDatabase._id,
instanceId,
automation,
})
notifier.success(`Automation ${automation.name} saved.`)
}
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
automation,
})
notifier.success("Automation deleted.")
}
</script>
<section>
@ -93,11 +93,18 @@
on:click={saveAutomation}>
Save Automation
</Button>
<Button red wide on:click={deleteAutomation}>Delete Automation</Button>
<Button red wide on:click={() => confirmDeleteDialog.show()}>
Delete Automation
</Button>
{/if}
</div>
</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>
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>
import { goto, params } from "@sveltech/routify"
import { onMount } from "svelte"
import { fade } from "svelte/transition"
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 LinkedRecord from "./LinkedRecord.svelte"
import AttachmentList from "./AttachmentList.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 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
// Internal headers we want to hide from the user
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
let modalOpen = false
let data = []
let headers = []
export let schema = []
export let data = []
export let title
export let allowEditing = false
export let loading = false
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
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
$: paginatedData = sorted
? sorted.slice(
currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
)
: []
$: paginatedData =
sorted && sorted.length
? sorted.slice(
currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
)
: []
$: modelId = data?.length ? data[0].modelId : null
$: headers = Object.keys($backendUiStore.selectedModel.schema)
.sort()
.filter(id => !INTERNAL_HEADERS.includes(id))
$: schema = $backendUiStore.selectedModel.schema
$: modelView = {
schema: $backendUiStore.selectedModel.schema,
name: $backendUiStore.selectedView.name,
function selectRelationship(record, fieldName) {
if (!record?.[fieldName]?.length) {
return
}
$goto(
`/${$params.application}/backend/model/${modelId}/relationship/${record._id}/${fieldName}`
)
}
</script>
<section>
<div class="table-controls">
<h2 class="title">
<span>{$backendUiStore.selectedModel.name}</span>
<span>{title}</span>
{#if loading}
<div transition:fade>
<Spinner size="10" />
@ -73,41 +62,54 @@
{/if}
</h2>
<div class="popovers">
<ColumnPopover />
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
<RowPopover />
<ViewPopover />
<ExportPopover view={modelView} />
{/if}
<slot />
</div>
</div>
<table class="bb-table">
<thead>
<tr>
<th class="edit-header">
<div>Edit</div>
</th>
{#each headers as header}
{#if allowEditing}
<th class="edit-header">
<div>Edit</div>
</th>
{/if}
{#each columns as header}
<th>
<ColumnHeaderPopover
field={$backendUiStore.selectedModel.schema[header]} />
{#if allowEditing}
<ColumnHeaderPopover field={schema[header]} />
{:else}
<div class="header">{header}</div>
{/if}
</th>
{/each}
</tr>
</thead>
<tbody>
{#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}
{#each paginatedData as row}
<tr>
<td>
<EditRowPopover {row} />
</td>
{#each headers as header}
{#if allowEditing}
<td>
<EditRowPopover {row} />
</td>
{/if}
{#each columns as header}
<td>
{#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'}
<AttachmentList files={row[header] || []} />
{:else}{getOr('', header, row)}{/if}
@ -125,19 +127,17 @@
</section>
<style>
section {
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: 600;
text-rendering: optimizeLegibility;
text-transform: capitalize;
margin-top: 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.title > span {
margin-right: var(--spacing-xs);
}
@ -145,54 +145,54 @@
table {
border: 1px solid var(--grey-4);
background: #fff;
border-radius: 3px;
border-collapse: collapse;
margin-top: 0;
}
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;
height: 48px;
padding-top: 0;
padding-bottom: 0;
}
.edit-header {
width: 100px;
cursor: default;
}
.edit-header:hover {
color: var(--ink);
}
th:hover {
thead th:hover {
color: var(--blue);
cursor: pointer;
}
.header {
text-transform: capitalize;
}
td {
max-width: 200px;
text-overflow: ellipsis;
border: 1px solid var(--grey-4);
overflow: hidden;
white-space: pre;
white-space: nowrap;
box-sizing: border-box;
padding: var(--spacing-l) var(--spacing-m);
font-size: var(--font-size-xs);
}
td.no-border {
border: none;
}
tbody tr {
border-bottom: 1px solid var(--grey-4);
transition: 0.3s background-color;
color: var(--ink);
font-size: 12px;
}
tbody tr:hover {
@ -207,7 +207,26 @@
display: flex;
}
.no-data {
padding: 14px;
:global(.popovers > div) {
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>

View file

@ -1,14 +1,11 @@
<script>
import { backendUiStore } from "builderStore"
export let data
export let currentPage
export let pageItemCount
export let ITEMS_PER_PAGE
let numPages = 0
$: numPages = Math.ceil(data.length / ITEMS_PER_PAGE)
$: numPages = Math.ceil((data?.length ?? 0) / ITEMS_PER_PAGE)
const next = () => {
if (currentPage + 1 === numPages) return
@ -27,8 +24,7 @@
<div class="pagination">
<div class="pagination__buttons">
<button on:click={previous}>Previous</button>
<button on:click={next}>Next</button>
<button on:click={previous} disabled={currentPage === 0}>&lt;</button>
{#each Array(numPages) as _, idx}
<button
class:selected={idx === currentPage}
@ -36,8 +32,19 @@
{idx + 1}
</button>
{/each}
<button
on:click={next}
disabled={currentPage === numPages - 1 || numPages === 0}>
&gt;
</button>
</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>
<style>
@ -51,26 +58,36 @@
.pagination__buttons {
display: flex;
border: 1px solid var(--grey-4);
border-radius: var(--border-radius-s);
overflow: hidden;
}
.pagination__buttons button {
display: inline-block;
padding: 10px;
padding: var(--spacing-s) var(--spacing-m);
margin: 0;
background: #fff;
border: 1px solid var(--grey-4);
border: none;
outline: none;
border-right: 1px solid var(--grey-4);
text-transform: capitalize;
border-radius: 3px;
min-width: 20px;
transition: 0.3s background-color;
}
.pagination__buttons button:last-child {
border-right: none;
}
.pagination__buttons button:hover {
cursor: pointer;
background-color: var(--grey-1);
}
.pagination__buttons button.selected {
background: var(--grey-2);
}
.selected {
color: var(--blue);
p {
font-size: var(--font-size-s);
margin: var(--spacing-xl) 0;
}
</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>
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
import CreateEditColumnPopover from "../popovers/CreateEditColumnPopover.svelte"
let anchor
let dropdown
let fieldName
</script>
<div bind:this={anchor}>
<Button text small on:click={dropdown.show}>
<Icon name="addrow" />
Create New Row
<Icon name="addcolumn" />
Create New Column
</Button>
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<h5>Add New Row</h5>
<CreateEditRecord onClosed={dropdown.hide} />
<h5>Create Column</h5>
<CreateEditColumnPopover onClosed={dropdown.hide} />
</DropdownMenu>
<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>
import {
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { Button, Input, Select } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import analytics from "analytics"
@ -19,9 +12,7 @@
]
export let view = {}
let anchor
let dropdown
export let onClosed
$: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId
@ -36,18 +27,12 @@
if (!view.calculation) view.calculation = "stats"
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
onClosed()
analytics.captureEvent("Added View Calculate", { field: view.field })
dropdown.hide()
}
</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">
<div class="actions">
<h5>Calculate</h5>
<div class="input-group-row">
<!-- <p>The</p>
@ -66,30 +51,33 @@
{/each}
</Select>
</div>
<div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<div class="footer">
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</Popover>
</div>
<style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 {
margin-bottom: var(--spacing-l);
margin: 0;
font-weight: 500;
}
.button-group {
margin-top: var(--spacing-l);
.footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-s);
gap: var(--spacing-m);
}
.input-group-row {
display: grid;
grid-template-columns: auto 1fr 20px 1fr;
grid-template-columns: auto 1fr;
gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
align-items: center;
}

View file

@ -2,17 +2,20 @@
import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
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
let anchor
let dropdown
let editing
let confirmDeleteDialog
$: sortColumn = $backendUiStore.sort && $backendUiStore.sort.column
$: sortDirection = $backendUiStore.sort && $backendUiStore.sort.direction
$: type = field?.type
function showEditor() {
editing = true
@ -23,8 +26,18 @@
editing = false
}
function deleteField() {
backendUiStore.actions.models.deleteField(field)
function showDelete() {
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()
}
@ -37,21 +50,23 @@
}
</script>
<div bind:this={anchor} on:click={dropdown.show}>
{field.name}
<div class="container" bind:this={anchor} on:click={dropdown.show}>
<span>{field.name}</span>
<Icon name="arrowdown" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing}
<h5>Edit Column</h5>
<CreateEditColumn onClosed={hideEditor} {field} />
<CreateEditColumnPopover onClosed={hideEditor} {field} />
{:else}
<ul>
<li data-cy="edit-column-header" on:click={showEditor}>
<Icon name="edit" />
Edit
</li>
<li data-cy="delete-column-header" on:click={deleteField}>
{#if type !== 'link'}
<li data-cy="edit-column-header" on:click={showEditor}>
<Icon name="edit" />
Edit
</li>
{/if}
<li data-cy="delete-column-header" on:click={showDelete}>
<Icon name="delete" />
Delete
</li>
@ -70,8 +85,25 @@
</ul>
{/if}
</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>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
}
.container span {
text-transform: capitalize;
}
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
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>
import {
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { Button, Input, Select } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import analytics from "analytics"
let anchor
let dropdown
export let onClosed
let name
let field
@ -36,45 +28,35 @@
field,
})
notifier.success(`View ${name} created`)
dropdown.hide()
onClosed()
analytics.captureEvent("View Created", { name })
$goto(`../../../view/${name}`)
}
</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">
<div class="actions">
<h5>Create View</h5>
<div class="input-group-column">
<Input placeholder="View Name" thin bind:value={name} />
</div>
<div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Input label="View Name" thin bind:value={name} />
<div class="footer">
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveView}>Save View</Button>
</div>
</Popover>
</div>
<style>
h5 {
margin-bottom: var(--spacing-l);
margin: 0;
font-weight: 500;
}
.button-group {
margin-top: var(--spacing-l);
display: flex;
justify-content: flex-end;
gap: var(--spacing-s);
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
.input-group-column {
.footer {
display: flex;
flex-direction: column;
gap: var(--spacing-s);
justify-content: flex-end;
gap: var(--spacing-m);
}
</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>
import {
Popover,
TextButton,
Button,
Icon,
Input,
Select,
DatePicker,
} from "@budibase/bbui"
import { Button, Input, Select, DatePicker } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import analytics from "analytics"
@ -51,9 +43,7 @@
]
export let view = {}
let anchor
let dropdown
export let onClosed
$: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId
@ -63,7 +53,7 @@
function saveView() {
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
dropdown.hide()
onClosed()
analytics.captureEvent("Added View Filter", {
filters: JSON.stringify(view.filters),
})
@ -115,96 +105,93 @@
}
</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">
<div class="actions">
<h5>Filter</h5>
<div class="input-group-row">
{#each view.filters as filter, idx}
{#if idx === 0}
<p>Where</p>
{:else}
<Select secondary thin bind:value={filter.conjunction}>
{#if view.filters.length}
<div class="input-group-row">
{#each view.filters as filter, idx}
{#if idx === 0}
<p>Where</p>
{: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>
{#each CONJUNCTIONS as conjunction}
<option value={conjunction.key}>{conjunction.name}</option>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
{/if}
<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}>
<Select secondary thin bind:value={filter.condition}>
<option value="">Choose an option</option>
{#each fieldOptions(filter.key) as option}
<option value={option}>{option.toString()}</option>
{#each CONDITIONS as condition}
<option value={condition.key}>{condition.name}</option>
{/each}
</Select>
{:else if filter.key && isDate(filter.key)}
<DatePicker
bind:value={filter.value}
placeholder={filter.key || fields[0]} />
{:else if filter.key && isNumber(filter.key)}
<Input
thin
bind:value={filter.value}
placeholder={filter.key || fields[0]}
type="number" />
{:else}
<Input
thin
placeholder={filter.key || fields[0]}
bind:value={filter.value} />
{/if}
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
{/each}
</div>
<div class="button-group">
{#if filter.key && isMultipleChoice(filter.key)}
<Select secondary thin bind:value={filter.value}>
<option value="">Choose an option</option>
{#each fieldOptions(filter.key) as option}
<option value={option}>{option.toString()}</option>
{/each}
</Select>
{:else if filter.key && isDate(filter.key)}
<DatePicker
bind:value={filter.value}
placeholder={filter.key || fields[0]} />
{:else if filter.key && isNumber(filter.key)}
<Input
thin
bind:value={filter.value}
placeholder={filter.key || fields[0]}
type="number" />
{:else}
<Input
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>
<div>
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<div class="buttons">
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</div>
</Popover>
</div>
<style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 {
margin-bottom: var(--spacing-l);
margin: 0;
font-weight: 500;
}
.button-group {
margin-top: var(--spacing-l);
.footer {
display: flex;
justify-content: space-between;
align-items: center;
}
:global(.button-group > div > button) {
margin-left: var(--spacing-m);
.buttons {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
.ri-close-circle-fill {
@ -215,7 +202,6 @@
display: grid;
grid-template-columns: minmax(50px, auto) 1fr 1fr 1fr 15px;
gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
align-items: center;
}

View file

@ -1,26 +1,10 @@
<script>
import {
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { Button, Input, Select } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
const CALCULATIONS = [
{
name: "Statistics",
key: "stats",
},
]
export let view = {}
let anchor
let dropdown
export let onClosed
$: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId
@ -30,20 +14,14 @@
function saveView() {
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
dropdown.hide()
onClosed()
}
</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">
<h5>Group By</h5>
<div class="actions">
<h5>Group</h5>
<div class="input-group-row">
<p>Group By</p>
<p>By</p>
<Select secondary thin bind:value={view.groupBy}>
<option value="">Choose an option</option>
{#each fields as field}
@ -51,30 +29,33 @@
{/each}
</Select>
</div>
<div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<div class="footer">
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</Popover>
</div>
<style>
.actions {
display: grid;
grid-gap: var(--spacing-xl);
}
h5 {
margin-bottom: var(--spacing-l);
margin: 0;
font-weight: 500;
}
.button-group {
margin-top: var(--spacing-l);
.footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-s);
gap: var(--spacing-m);
}
.input-group-row {
display: grid;
grid-template-columns: 75px 1fr 20px 1fr;
grid-template-columns: 20px 1fr;
gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
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>
.indented {
grid-template-columns: 50px 1fr 20px;
grid-template-columns: 46px 1fr 20px;
}
.indented i {
justify-self: end;
}
div {
padding: 0 10px 0 10px;
height: 36px;
border-radius: 5px;
padding: var(--spacing-s) var(--spacing-m);
border-radius: var(--border-radius-m);
display: grid;
grid-template-columns: 30px 1fr 20px;
grid-template-columns: 20px 1fr 20px;
align-items: center;
transition: 0.3s background-color;
color: var(--ink);
font-weight: 400;
font-size: 14px;
margin-top: 4px;
margin-bottom: 4px;
margin-bottom: var(--spacing-xs);
grid-gap: var(--spacing-s);
}
.selected {
@ -53,6 +51,5 @@
i {
color: var(--grey-7);
font-size: 20px;
margin-right: 8px;
}
</style>

View file

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

View file

@ -154,7 +154,7 @@
overflow: hidden;
border-radius: var(--border-radius-s);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-l);
padding: var(--spacing-m) var(--spacing-l);
transition: all 0.2s ease 0s;
display: inline-flex;
text-rendering: optimizeLegibility;
@ -169,6 +169,8 @@
width: 100%;
background-color: var(--grey-2);
font-size: var(--font-size-xs);
line-height: normal;
border: var(--border-transparent);
}
.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>
import { getContext } from "svelte"
import { goto } from "@sveltech/routify"
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 DeleteViewModal from "components/database/DataTable/modals/DeleteView.svelte"
const { open, close } = getContext("simple-modal")
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let view
let anchor
let dropdown
let editing
let originalName = view.name
let confirmDeleteDialog
function showEditor() {
editing = true
@ -23,76 +21,96 @@
function hideEditor() {
dropdown.hide()
editing = false
close()
}
const deleteView = () => {
open(
DeleteViewModal,
{
onClosed: close,
viewName: view.name,
},
{ styleContent: { padding: "0" } }
)
function showDelete() {
dropdown.hide()
confirmDeleteDialog.show()
}
function save() {
backendUiStore.actions.views.save({
async function save() {
await backendUiStore.actions.views.save({
originalName,
...view,
})
notifier.success("Renamed View Successfully.")
notifier.success("View renamed successfully")
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>
<div bind:this={anchor} on:click={dropdown.show}>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<DropdownMenu align="left" {anchor} bind:this={dropdown}>
{#if editing}
<h5>Edit View</h5>
<div class="container">
<Input placeholder="View Name" thin bind:value={view.name} />
</div>
<footer>
<div class="button-margin-3">
<div class="actions">
<h5>Edit View</h5>
<Input label="View Name" thin bind:value={view.name} />
<footer>
<Button secondary on:click={hideEditor}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={save}>Save</Button>
</div>
</footer>
</footer>
</div>
{:else}
<ul>
<li on:click={showEditor}>
<Icon name="edit" />
Edit
</li>
<li data-cy="delete-view" on:click={deleteView}>
<li data-cy="delete-view" on:click={showDelete}>
<Icon name="delete" />
Delete
</li>
</ul>
{/if}
</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>
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 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
.container {
padding: var(--spacing-xl);
footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
ul {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
list-style: none;
padding-left: 0;
margin: 0;
padding: var(--spacing-s) 0;
}
@ -115,29 +133,4 @@
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,57 +1,31 @@
<script>
import { Modal, Button, Heading, Spacer } from "@budibase/bbui"
import { Modal, ModalContent } from "@budibase/bbui"
export let title = ""
export let body = ""
export let okText = "OK"
export let okText = "Confirm"
export let cancelText = "Cancel"
export let onOk = () => {}
export let onCancel = () => {}
export let onOk = undefined
export let onCancel = undefined
let modal
export const show = () => {
theModal.show()
modal.show()
}
export const hide = () => {
theModal.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()
modal.hide()
}
</script>
<Modal id={title} bind:this={theModal}>
<h2>{title}</h2>
<Spacer extraLarge />
<slot class="rows">{body}</slot>
<Spacer extraLarge />
<div class="modal-footer">
<Button red wide on:click={ok}>{okText}</Button>
<Button secondary wide on:click={cancel}>{cancelText}</Button>
</div>
<Modal bind:this={modal} on:hide={onCancel}>
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red>
<div class="body">{body}</div>
</ModalContent>
</Modal>
<style>
h2 {
font-size: var(--font-size-xl);
margin: 0;
font-family: var(--font-sans);
font-weight: 600;
}
.modal-footer {
display: grid;
grid-gap: var(--spacing-s);
.body {
font-size: var(--font-size-s);
}
</style>

View file

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

View file

@ -5,9 +5,25 @@
</script>
{#if hasErrors}
<div class="bb__alert bb__alert--danger">
<div class="container bb__alert bb__alert--danger">
{#each errors as error}
<div>{error.dataPath} {error.message}</div>
<div class="error">{error.dataPath} {error.message}</div>
{/each}
</div>
{/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 { backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Select, Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "../../helpers"
export let modelId
export let linkName
export let linked = []
export let schema
export let linkedRecords = []
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]
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name]
$: schema = $backendUiStore.selectedModel.schema
async function fetchRecords() {
const FETCH_RECORDS_URL = `/api/${modelId}/records`
const response = await api.get(FETCH_RECORDS_URL)
const modelResponse = await api.get(`/api/models/${modelId}`)
model = await modelResponse.json()
records = await response.json()
}
function linkRecord(id) {
if (linkedRecords.has(id)) {
linkedRecords.delete(id)
} else {
linkedRecords.add(id)
async function fetchRecords(linkedModelId) {
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
try {
const response = await api.get(FETCH_RECORDS_URL)
records = await response.json()
} catch (error) {
console.log(error)
records = []
}
linkedRecords = linkedRecords
}
onMount(() => {
fetchRecords()
})
function getPrettyName(record) {
return record[linkedModel.primaryDisplay || "_id"]
}
</script>
<section>
<header>
<h3>{linkName}</h3>
</header>
{#each records as record}
<div class="linked-record" on:click={() => linkRecord(record._id)}>
<div class="fields" class:selected={linkedRecords.has(record._id)}>
{#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}
</section>
<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>
{#if linkedModel.primaryDisplay == null}
<Label extraSmall grey>{label}</Label>
<Label small black>
Please choose a primary display column for the
<b>{linkedModel.name}</b>
table.
</Label>
{:else}
<Multiselect
secondary
bind:value={linkedRecords}
{label}
placeholder="Choose some options">
{#each records as record}
<option value={record._id}>{getPrettyName(record)}</option>
{/each}
</Multiselect>
{/if}

View file

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

View file

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

View file

@ -1,5 +1,6 @@
<script>
import { join } from "lodash/fp"
import { TextArea, Label } from "@budibase/bbui"
export let values
export let label
@ -15,35 +16,12 @@
$: valuesText = join("\n")(values)
</script>
<div class="margin">
<label class="label">{label}</label>
<textarea value={valuesText} on:change={inputChanged} />
<div class="container">
<TextArea {label} value={valuesText} thin on:change={inputChanged} />
</div>
<style>
.margin {
margin-bottom: 16px;
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;
.container :global(textarea) {
min-height: 100px;
}
</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>
import Modal from "./Modal.svelte"
import SettingsModal from "./SettingsModal.svelte"
import { SettingsIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { isActive, goto, layout } from "@sveltech/routify"
import { Modal } from "@budibase/bbui"
// Handle create app modal
const { open } = getContext("simple-modal")
const showSettingsModal = () => {
open(
Modal,
{
name: "Placeholder App Name",
description: "This is a hardcoded description that needs to change",
},
{
closeButton: false,
closeOnEsc: true,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
}
let modal
</script>
<span class="topnavitemright settings" on:click={showSettingsModal}>
<span class="topnavitemright settings" on:click={modal.show}>
<SettingsIcon />
</span>
<Modal bind:this={modal} width="600px">
<SettingsModal />
</Modal>
<style>
span:first-letter {
@ -35,8 +20,7 @@
.topnavitemright {
cursor: pointer;
color: var(--grey-7);
margin: 0px 20px 0px 0px;
padding-top: 4px;
margin: 0 20px 0 0;
font-weight: 500;
font-size: 1rem;
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}
name="Name"
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="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
@ -37,19 +37,7 @@
.inputs {
display: grid;
justify-items: stretch;
grid-gap: 18px;
grid-template-columns: 1fr 1fr 1fr;
}
.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;
grid-gap: var(--spacing-m);
grid-template-columns: 1fr 1fr 140px;
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,60 +1,35 @@
<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
</script>
<div class="apps-card">
<h3 class="app-title">{name}</h3>
<Heading small black>{name}</Heading>
<Spacer medium />
<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>
<style>
.apps-card {
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-height: 150px;
border-radius: var(--border-radius-m);
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 {
display: flex;
flex-direction: row;
align-items: baseline;
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>

View file

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

View file

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

View file

@ -1,20 +1,22 @@
<script>
import { Input, Heading, Body } from "@budibase/bbui"
import { Label, Heading, Input } from "@budibase/bbui"
export let validationErrors
export let template
let blurred = { appName: false }
</script>
{#if template}
<Heading small black>Selected Template</Heading>
<Body>{template.name}</Body>
{/if}
<h2>Create your web app</h2>
<div class="container">
{#if template}
<div class="template">
<Label extraSmall grey>Selected Template</Label>
<Heading small>{template.name}</Heading>
</div>
{/if}
<Input
on:input={() => (blurred.appName = true)}
label="Web app name"
label="Web App Name"
name="applicationName"
placeholder="Enter name of your web application"
type="name"
@ -24,6 +26,11 @@
<style>
.container {
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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,12 @@
<script>
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 { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
import actionTypes from "./actions"
@ -22,12 +28,6 @@
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
.component
const closeModal = () => {
dispatch("close")
draftEventHandler = { parameters: [] }
actions = []
}
const updateEventHandler = (updatedHandler, index) => {
actions[index] = updatedHandler
}
@ -54,20 +54,17 @@
const saveEventData = () => {
dispatch("change", actions)
closeModal()
}
</script>
<div class="root">
<div class="header">
<Heading small dark>Actions</Heading>
<ModalContent title="Actions" confirmText="Save" onConfirm={saveEventData}>
<div slot="header">
<div bind:this={addActionButton}>
<TextButton text small blue on:click={addActionDropdown.show}>
Add Action
<div style="height: 20px; width: 20px;">
<AddIcon />
</div>
Add Action
</TextButton>
</div>
<DropdownMenu
@ -89,11 +86,7 @@
{#each actions as action, index}
<div class="action-container">
<div class="action-header" on:click={selectAction(action)}>
<p
class="bb-body bb-body--small bb-body--color-dark"
style="margin: var(--spacing-s) 0;">
{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}
</p>
<Body small lh>{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}</Body>
<div class="row-expander" class:rotate={action !== selectedAction}>
<ArrowDownIcon />
</div>
@ -115,30 +108,12 @@
{/if}
</div>
<div class="footer">
<div slot="footer">
<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>
</ModalContent>
<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 {
display: flex;
flex-direction: row;
@ -166,8 +141,7 @@
.actions-container {
flex: 1;
min-height: 0px;
padding-bottom: var(--spacing-s);
min-height: 0;
padding-top: 0;
border: var(--border-light);
border-width: 0 0 1px 0;
@ -177,10 +151,6 @@
.action-container {
border: var(--border-light);
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 {
@ -195,22 +165,13 @@
flex-direction: row;
}
.footer {
display: flex;
flex-direction: row;
gap: var(--spacing-s);
padding: var(--spacing-xl);
padding-top: var(--spacing-m);
}
.footer > a {
a {
flex: 1;
color: var(--grey-5);
font-size: var(--font-size-s);
text-decoration: none;
}
.footer > a:hover {
a:hover {
color: var(--blue);
}

View file

@ -1,25 +1,17 @@
<script>
import { Button, Modal } from "@budibase/bbui"
import EventEditorModal from "./EventEditorModal.svelte"
import { createEventDispatcher, onMount } from "svelte"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value
export let name
let eventsModal
let modal
</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">
<EventEditorModal
event={value}
eventType={name}
on:change
on:close={eventsModal.hide} />
<Modal bind:this={modal} width="600px">
<EventEditorModal event={value} eventType={name} on:change />
</Modal>
<style>
</style>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,57 +1,25 @@
<script>
import { store, backendUiStore } from "builderStore"
import { store } from "builderStore"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import NewScreen from "components/userInterface/CreateScreen/NewScreen.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import { Button, Spacer, Modal } from "@budibase/bbui"
const newScreen = () => {
newScreenPicker.show()
}
let newScreenPicker
let modal
</script>
<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]} />
<div class="nav-items-container">
<ComponentsHierarchy screens={$store.screens} />
</div>
<NewScreen bind:this={newScreenPicker} />
<style>
.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>
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>

View file

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

View file

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

View file

@ -1,22 +1,14 @@
<script>
import { store, backendUiStore } from "builderStore"
import { Input, Button, Spacer, Select, Modal } from "@budibase/bbui"
import { Input, Button, Spacer, Select, ModalContent } from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates"
import { createEventDispatcher } from "svelte"
import { some } from "lodash/fp"
const dispatch = createEventDispatcher()
const CONTAINER = "@budibase/standard-components/container"
export const show = () => {
dialog.show()
}
let screens
let name = ""
let routeError
let baseComponent = CONTAINER
let dialog
let templateIndex = 0
let draftScreen
@ -73,7 +65,6 @@
name = ""
route = ""
baseComponent = CONTAINER
dialog.hide()
}
const routeNameExists = route => {
@ -99,64 +90,32 @@
}
</script>
<Modal bind:this={dialog} minWidth="500px">
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}>
<h2>New Screen</h2>
<Spacer extraLarge />
<Select
label="Choose a Template"
bind:value={templateIndex}
secondary
on:change={templateChanged}>
{#if templates}
{#each templates as template, index}
<option value={index}>{template.name}</option>
{/each}
{/if}
</Select>
<div class="bb-margin-xl">
<label>Choose a Template</label>
<Select bind:value={templateIndex} secondary on:change={templateChanged}>
{#if templates}
{#each templates as template, index}
<option value={index}>{template.name}</option>
{/each}
{/if}
</Select>
</div>
<div data-cy="new-screen-dialog">
<div class="bb-margin-xl">
<Input label="Name" bind:value={name} />
</div>
<Input label="Name" bind:value={name} />
<div class="bb-margin-xl">
<Input
label="Url"
error={routeError}
bind:value={route}
on:change={routeChanged} />
</div>
<Input
label="Url"
error={routeError}
bind:value={route}
on:change={routeChanged} />
<div class="bb-margin-xl">
<label>Screen Type</label>
<Select bind:value={baseComponent} secondary>
{#each baseComponents as component}
<option value={component}>{componentShortName(component)}</option>
{/each}
</Select>
</div>
<Select label="Screen Type" bind:value={baseComponent} secondary>
{#each baseComponents as component}
<option value={component}>{componentShortName(component)}</option>
{/each}
</Select>
</div>
<Spacer extraLarge />
<div data-cy="create-screen-footer" class="modal-footer">
<Button secondary medium on:click={finished}>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>
</ModalContent>

View file

@ -10,7 +10,7 @@
} from "builderStore/replaceBindings"
import { DropdownMenu } from "@budibase/bbui"
import BindingDropdown from "components/userInterface/BindingDropdown.svelte"
import { onMount, getContext } from "svelte"
import { onMount } from "svelte"
export let label = ""
export let componentInstance = {}
@ -118,7 +118,7 @@
position: relative;
display: flex;
flex-flow: row;
width: 260px;
min-width: 260px;
margin: 8px 0px;
align-items: center;
}
@ -143,13 +143,15 @@
}
button {
position: absolute;
cursor: pointer;
background: none;
border: none;
border-radius: 50%;
height: 24px;
width: 24px;
background: rgb(224, 224, 224);
right: 5px;
height: 90%;
width: 2rem;
background: var(--grey-2);
right: 10px;
--spacing-s: 0;
border-left: 0.5px solid var(--grey-3);
outline-color: var(--blue);
}
</style>

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