merge from master - including fixes
This commit is contained in:
parent
a3bd28d99f
commit
027819bae0
|
@ -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")
|
||||
})
|
||||
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -158,4 +158,8 @@
|
|||
.bb__alert--danger {
|
||||
background: #fef4f6;
|
||||
color: #f0506e;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
|
@ -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}
|
|
@ -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}
|
|
@ -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>
|
|
@ -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}><</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}>
|
||||
>
|
||||
</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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 {
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -11,6 +11,4 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="bb-margin-m">
|
||||
<DatePicker placeholder={label} on:change={onChange} {value} />
|
||||
</div>
|
||||
<DatePicker {label} on:change={onChange} {value} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
.numberbox {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./ModelDataTable.svelte"
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,2 +0,0 @@
|
|||
export { default as DeleteRecordModal } from "./DeleteRecord.svelte"
|
||||
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte"
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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%;
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
align-items: baseline;
|
||||
}
|
||||
|
||||
.root :global(.relative) {
|
||||
.root :global(> div) {
|
||||
flex: 1;
|
||||
margin-left: var(--spacing-l);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue