Merge branch 'master' of github.com:Budibase/budibase into feature/remove-multitenancy

This commit is contained in:
mike12345567 2020-10-28 20:35:29 +00:00
commit ccc8f3c13e
143 changed files with 3021 additions and 5602 deletions

View File

@ -12,8 +12,6 @@ Budibase is a monorepo managed by [lerna](https://github.com/lerna/lerna). Lerna
- **packages/client** - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it. - **packages/client** - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it.
- **packages/cli** - The budibase CLI. This is the main module that gets downloaded from NPM and is responsible for creating and managing apps, as well as running the budibase server.
- **packages/server** - The budibase server. This [Koa](https://koajs.com/) app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system. - **packages/server** - The budibase server. This [Koa](https://koajs.com/) app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
@ -95,14 +93,9 @@ then `cd ` into your local copy.
### 4. Initialising Budibase and Creating a Budibase App ### 4. Initialising Budibase and Creating a Budibase App
`yarn initialise` will initialise your budibase installation. A Budibase apps folder will have been created in `~/.budibase`. You can also just start up the budibase electron app and it should initialise budibase for you. Starting up the budibase electron app should initialise budibase for you. A Budibase apps folder will have been created in `~/.budibase`.
This is a blank apps folder, so you will need to create yourself an app. This is a blank apps folder, so you will need to create yourself an app, you can do this by clicking "Create New App" from the budibase builder.
```
cd packages/server
yarn run budi new your-app-name
```
This will create a new budibase application in the `~/.budibase/<your-app-uuid>` directory, and NPM install the component libraries for that application. Let's start building your app with the budibase builder! This will create a new budibase application in the `~/.budibase/<your-app-uuid>` directory, and NPM install the component libraries for that application. Let's start building your app with the budibase builder!
@ -116,16 +109,6 @@ To run the budibase server and builder in dev mode (i.e. with live reloading):
This will enable watch mode for both the builder app, server, client library and any component libraries. This will enable watch mode for both the builder app, server, client library and any component libraries.
### Running Commands from /server Directory
Notice that when inside `packages/server`, you can use any Budibase CLI command via yarn:
e.g. `yarn budi new mikes_app` == `budi new mikes_app`
This will use the CLI directly from `packages/cli`, rather than your globally installed `budi`
## Data Storage ## Data Storage
When you are running locally, budibase stores data on disk using [PouchDB](https://pouchdb.com/), as well as some JSON on local files. After setting up budibase, you can find all of this data in the `~/.budibase` directory. When you are running locally, budibase stores data on disk using [PouchDB](https://pouchdb.com/), as well as some JSON on local files. After setting up budibase, you can find all of this data in the `~/.budibase` directory.

View File

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

View File

@ -14,35 +14,26 @@ context("Create a automation", () => {
cy.createTestTableWithData() cy.createTestTableWithData()
cy.contains("automate").click() cy.contains("automate").click()
cy.contains("Create New Automation").click() cy.get("[data-cy=new-automation]").click()
cy.get(".modal").within(() => { cy.get(".modal").within(() => {
cy.get("input").type("Add Row") cy.get("input").type("Add Row")
cy.get(".buttons") cy.get(".buttons").contains("Create").click()
.contains("Create")
.click()
}) })
// Add trigger // Add trigger
cy.get("[data-cy=add-automation-component]").click() cy.contains("Trigger").click()
cy.get("[data-cy=ROW_SAVED]").click() cy.contains("Row Saved").click()
cy.get("[data-cy=automation-block-setup]").within(() => { cy.get(".setup").within(() => {
cy.get("select") cy.get("select").first().select("dog")
.first()
.select("dog")
}) })
// Create action // Create action
cy.get("[data-cy=CREATE_ROW]").click() cy.contains("Action").click()
cy.get("[data-cy=automation-block-setup]").within(() => { cy.contains("Create Row").click()
cy.get("select") cy.get(".setup").within(() => {
.first() cy.get("select").first().select("dog")
.select("dog") cy.get("input").first().type("goodboy")
cy.get("input") cy.get("input").eq(1).type("11")
.first()
.type("goodboy")
cy.get("input")
.eq(1)
.type("11")
}) })
// Save // Save
@ -50,7 +41,7 @@ context("Create a automation", () => {
// Activate Automation // Activate Automation
cy.get("[data-cy=activate-automation]").click() cy.get("[data-cy=activate-automation]").click()
cy.get(".stop-button.highlighted").should("be.visible") cy.get(".ri-stop-circle-fill.highlighted").should("be.visible")
}) })
it("should add row when a new row is added", () => { it("should add row when a new row is added", () => {

View File

@ -1,54 +1,60 @@
xcontext('Create Components', () => { xcontext("Create Components", () => {
before(() => {
cy.server()
cy.visit("localhost:4001/_builder")
// https://on.cypress.io/type
cy.createApp("Table App", "Table App Description")
cy.createTable("dog", "name", "age")
cy.addRow("bob", "15")
})
before(() => { // https://on.cypress.io/interacting-with-elements
cy.server() it("should add a container", () => {
cy.visit('localhost:4001/_builder') cy.navigateToFrontend()
// https://on.cypress.io/type cy.get(".switcher > :nth-child(2)").click()
cy.createApp('Table App', 'Table App Description') cy.contains("Container").click()
cy.createTable('dog', 'name', 'age') })
cy.addRow('bob', '15') it("should add a headline", () => {
}) cy.addHeadlineComponent("An Amazing headline!")
// https://on.cypress.io/interacting-with-elements getIframeBody().contains("An Amazing headline!")
it('should add a container', () => { })
cy.contains('frontend').click() it("change the font size of the headline", () => {
cy.get('.switcher > :nth-child(2)').click() cy.contains("Typography").click()
cy.get("[data-cy=font-size-prop-control]").click()
cy.contains("60px").click()
cy.contains("Design").click()
cy.contains('Container').click() getIframeBody()
}) .contains("An Amazing headline!")
it('should add a headline', () => { .should("have.css", "font-size", "60px")
cy.addHeadlineComponent('An Amazing headline!') })
getIframeBody().contains('An Amazing headline!')
})
it('change the font size of the headline', () => {
cy.contains('Typography').click()
cy.get('[data-cy=font-size-prop-control]').click()
cy.contains("60px").click()
cy.contains('Design').click()
getIframeBody().contains('An Amazing headline!').should('have.css', 'font-size', '60px')
})
}) })
const getIframeDocument = () => { const getIframeDocument = () => {
return cy return (
.get('iframe') cy
// Cypress yields jQuery element, which has the real .get("iframe")
// DOM element under property "0". // Cypress yields jQuery element, which has the real
// From the real DOM iframe element we can get // DOM element under property "0".
// the "document" element, it is stored in "contentDocument" property // From the real DOM iframe element we can get
// Cypress "its" command can access deep properties using dot notation // the "document" element, it is stored in "contentDocument" property
// https://on.cypress.io/its // Cypress "its" command can access deep properties using dot notation
.its('0.contentDocument').should('exist') // https://on.cypress.io/its
.its("0.contentDocument")
.should("exist")
)
} }
const getIframeBody = () => { const getIframeBody = () => {
// get the document // get the document
return getIframeDocument() return (
// automatically retries until body is loaded getIframeDocument()
.its('body').should('not.be.undefined') // automatically retries until body is loaded
// wraps "body" DOM element to allow .its("body")
// chaining more Cypress commands, like ".find(...)" .should("not.be.undefined")
.then(cy.wrap) // wraps "body" DOM element to allow
} // chaining more Cypress commands, like ".find(...)"
.then(cy.wrap)
)
}

View File

@ -8,7 +8,7 @@ context("Create a Table", () => {
cy.createTable("dog") cy.createTable("dog")
// Check if Table exists // Check if Table exists
cy.get(".title span").should("have.text", "dog") cy.get(".table-title h1").should("have.text", "dog")
}) })
it("adds a new column to the table", () => { it("adds a new column to the table", () => {
@ -26,9 +26,7 @@ context("Create a Table", () => {
.trigger("mouseover") .trigger("mouseover")
.find(".ri-pencil-line") .find(".ri-pencil-line")
.click({ force: true }) .click({ force: true })
cy.get(".actions input") cy.get(".actions input").first().type("updated")
.first()
.type("updated")
// Unset table display column // Unset table display column
cy.contains("display column").click() cy.contains("display column").click()
cy.contains("Save Column").click() cy.contains("Save Column").click()
@ -36,7 +34,8 @@ context("Create a Table", () => {
}) })
it("edits a row", () => { it("edits a row", () => {
cy.get("button").contains("Edit").click() cy.contains("button", "Edit").click({ force: true })
cy.wait(1000)
cy.get(".modal input").type("Updated") cy.get(".modal input").type("Updated")
cy.contains("Save").click() cy.contains("Save").click()
cy.contains("RoverUpdated").should("have.text", "RoverUpdated") cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
@ -50,10 +49,10 @@ context("Create a Table", () => {
}) })
it("deletes a column", () => { it("deletes a column", () => {
cy cy.contains("header", "name")
.contains("header", "name") .trigger("mouseover")
.trigger("mouseover") .find(".ri-pencil-line")
.find(".ri-pencil-line").click({ force: true }) .click({ force: true })
cy.contains("Delete").click() cy.contains("Delete").click()
cy.wait(50) cy.wait(50)
cy.get(".buttons").contains("Delete").click() cy.get(".buttons").contains("Delete").click()
@ -61,9 +60,7 @@ context("Create a Table", () => {
}) })
it("deletes a table", () => { it("deletes a table", () => {
cy.contains("div", "dog") cy.contains(".nav-item", "dog").get(".actions").invoke("show").click()
.get(".ri-more-line")
.click()
cy.get("[data-cy=delete-table]").click() cy.get("[data-cy=delete-table]").click()
cy.contains("Delete Table").click() cy.contains("Delete Table").click()
cy.contains("dog").should("not.exist") cy.contains("dog").should("not.exist")

View File

@ -22,10 +22,12 @@ context("Create a View", () => {
cy.get("input").type("Test View") cy.get("input").type("Test View")
cy.contains("Save View").click() cy.contains("Save View").click()
}) })
cy.get(".title").contains("Test View") cy.get(".table-title h1").contains("Test View")
cy.get("[data-cy=table-header]").then($headers => { cy.get("[data-cy=table-header]").then($headers => {
expect($headers).to.have.length(3) expect($headers).to.have.length(3)
const headers = Array.from($headers).map(header => header.textContent.trim()) const headers = Array.from($headers).map(header =>
header.textContent.trim()
)
expect(headers).to.deep.eq(["group", "age", "rating"]) expect(headers).to.deep.eq(["group", "age", "rating"])
}) })
}) })
@ -33,14 +35,8 @@ context("Create a View", () => {
it("filters the view by age over 10", () => { it("filters the view by age over 10", () => {
cy.contains("Filter").click() cy.contains("Filter").click()
cy.contains("Add Filter").click() cy.contains("Add Filter").click()
cy.get(".menu-container") cy.get(".menu-container").find("select").first().select("age")
.find("select") cy.get(".menu-container").find("select").eq(1).select("More Than")
.first()
.select("age")
cy.get(".menu-container")
.find("select")
.eq(1)
.select("More Than")
cy.get(".menu-container").find("input").type(18) cy.get(".menu-container").find("input").type(18)
cy.contains("Save").click() cy.contains("Save").click()
cy.get("[role=rowgroup] .ag-row").get($values => { cy.get("[role=rowgroup] .ag-row").get($values => {
@ -53,20 +49,16 @@ context("Create a View", () => {
cy.viewport("macbook-15") cy.viewport("macbook-15")
cy.contains("Calculate").click() cy.contains("Calculate").click()
cy.get(".menu-container") cy.get(".menu-container").find("select").eq(0).select("Statistics")
.find("select")
.eq(0)
.select("Statistics")
cy.wait(50) cy.wait(50)
cy.get(".menu-container") cy.get(".menu-container").find("select").eq(1).select("age")
.find("select")
.eq(1)
.select("age")
cy.contains("Save").click() cy.contains("Save").click()
cy.get(".ag-center-cols-viewport").scrollTo("100%") cy.get(".ag-center-cols-viewport").scrollTo("100%")
cy.get("[data-cy=table-header]").then($headers => { cy.get("[data-cy=table-header]").then($headers => {
expect($headers).to.have.length(7) expect($headers).to.have.length(7)
const headers = Array.from($headers).map(header => header.textContent.trim()) const headers = Array.from($headers).map(header =>
header.textContent.trim()
)
expect(headers).to.deep.eq([ expect(headers).to.deep.eq([
"field", "field",
"sum", "sum",
@ -78,16 +70,10 @@ context("Create a View", () => {
]) ])
}) })
cy.get(".ag-cell").then($values => { cy.get(".ag-cell").then($values => {
const values = Array.from($values).map(header => header.textContent.trim()) const values = Array.from($values).map(header =>
expect(values).to.deep.eq([ header.textContent.trim()
"age", )
"155", expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"])
"20",
"49",
"5",
"5347",
"31",
])
}) })
}) })
@ -102,8 +88,7 @@ context("Create a View", () => {
cy.contains("Students").should("be.visible") cy.contains("Students").should("be.visible")
cy.contains("Teachers").should("be.visible") cy.contains("Teachers").should("be.visible")
cy cy.get(".ag-row[row-index=0]")
.get(".ag-row[row-index=0]")
.find(".ag-cell") .find(".ag-cell")
.then($values => { .then($values => {
const values = Array.from($values).map(value => value.textContent) const values = Array.from($values).map(value => value.textContent)
@ -120,10 +105,11 @@ context("Create a View", () => {
}) })
it("renames a view", () => { it("renames a view", () => {
cy.contains("[data-cy=table-nav-item]", "Test View") cy.contains(".nav-item", "Test View")
.find(".ri-more-line") .find(".actions")
.invoke("show")
.click() .click()
cy.contains("Edit").click() cy.get("[data-cy=edit-view]").click()
cy.get(".menu-container").within(() => { cy.get(".menu-container").within(() => {
cy.get("input").type(" Updated") cy.get("input").type(" Updated")
cy.contains("Save").click() cy.contains("Save").click()
@ -132,11 +118,11 @@ context("Create a View", () => {
}) })
it("deletes a view", () => { it("deletes a view", () => {
cy.contains("[data-cy=table-nav-item]", "Test View Updated").click() cy.contains(".nav-item", "Test View Updated")
cy.contains("[data-cy=table-nav-item]", "Test View Updated") .find(".actions")
.find(".ri-more-line") .invoke("show")
.click() .click()
cy.contains("Delete").click() cy.get("[data-cy=delete-view]").click()
cy.contains("Delete View").click() cy.contains("Delete View").click()
cy.contains("TestView Updated").should("not.be.visible") cy.contains("TestView Updated").should("not.be.visible")
}) })

View File

@ -4,8 +4,8 @@
// 3. Runs the server using said folder // 3. Runs the server using said folder
const rimraf = require("rimraf") const rimraf = require("rimraf")
const { join } = require("path") const { join, resolve } = require("path")
const run = require("../../cli/src/commands/run/runHandler") // const run = require("../../cli/src/commands/run/runHandler")
const initialiseBudibase = require("../../server/src/utilities/initialiseBudibase") const initialiseBudibase = require("../../server/src/utilities/initialiseBudibase")
const homedir = join(require("os").homedir(), ".budibase") const homedir = join(require("os").homedir(), ".budibase")
@ -16,9 +16,22 @@ process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
process.env.NODE_ENV = "cypress" process.env.NODE_ENV = "cypress"
process.env.ENABLE_ANALYTICS = "false" process.env.ENABLE_ANALYTICS = "false"
async function run(dir) {
process.env.BUDIBASE_DIR = resolve(dir)
require("dotenv").config({ path: resolve(dir, ".env") })
// dont make this a variable or top level require
// it will cause environment module to be loaded prematurely
const server = require("../../server/src/app")
server.on("close", () => console.log("Server Closed"))
}
initialiseBudibase({ dir: homedir, clientId: "cypress-test" }) initialiseBudibase({ dir: homedir, clientId: "cypress-test" })
.then(() => { .then(() => {
delete require.cache[require.resolve("../../server/src/environment")] delete require.cache[require.resolve("../../server/src/environment")]
run({ dir: homedir }) const xPlatHomeDir = homedir.startsWith("~")
? join(homedir(), homedir.substring(1))
: homedir
run(xPlatHomeDir)
}) })
.catch(e => console.error(e)) .catch(e => console.error(e))

View File

@ -51,7 +51,7 @@ Cypress.Commands.add("createApp", name => {
.click() .click()
.type("test") .type("test")
cy.contains("Submit").click() cy.contains("Submit").click()
cy.contains("Create New Table", { cy.get("[data-cy=new-table]", {
timeout: 20000, timeout: 20000,
}).should("be.visible") }).should("be.visible")
}) })
@ -65,7 +65,7 @@ Cypress.Commands.add("createTestTableWithData", () => {
Cypress.Commands.add("createTable", tableName => { Cypress.Commands.add("createTable", tableName => {
// Enter table name // Enter table name
cy.contains("Create New Table").click() cy.get("[data-cy=new-table]").click()
cy.get(".modal").within(() => { cy.get(".modal").within(() => {
cy.get("input") cy.get("input")
.first() .first()
@ -79,9 +79,7 @@ Cypress.Commands.add("createTable", tableName => {
Cypress.Commands.add("addColumn", (tableName, columnName, type) => { Cypress.Commands.add("addColumn", (tableName, columnName, type) => {
// Select Table // Select Table
cy.get(".root") cy.contains(".nav-item", tableName).click()
.contains(tableName)
.click()
cy.contains("Create New Column").click() cy.contains("Create New Column").click()
// Configure column // Configure column
@ -155,7 +153,7 @@ Cypress.Commands.add("navigateToFrontend", () => {
}) })
Cypress.Commands.add("createScreen", (screenName, route) => { Cypress.Commands.add("createScreen", (screenName, route) => {
cy.contains("Create New Screen").click() cy.get("[data-cy=new-screen]").click()
cy.get(".modal").within(() => { cy.get(".modal").within(() => {
cy.get("input") cy.get("input")
.eq(0) .eq(0)

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.2.6", "version": "0.3.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -64,7 +64,7 @@
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.47.0", "@budibase/bbui": "^1.47.0",
"@budibase/client": "^0.2.6", "@budibase/client": "^0.3.0",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@budibase/svelte-ag-grid": "^0.0.16", "@budibase/svelte-ag-grid": "^0.0.16",
"@fortawesome/fontawesome-free": "^5.14.0", "@fortawesome/fontawesome-free": "^5.14.0",

View File

@ -85,14 +85,6 @@
box-sizing: border-box; box-sizing: border-box;
} }
.preview-pane {
grid-column: 2;
margin: 80px 60px;
background: #fff;
border-radius: 5px;
box-shadow: 0 0px 6px rgba(0, 0, 0, 0.05);
}
.budibase__table { .budibase__table {
border: 1px solid var(--grey-4); border: 1px solid var(--grey-4);
background: #fff; background: #fff;

View File

@ -106,11 +106,15 @@ const contextToBindables = (tables, walkResult) => context => {
} }
} }
const stringType = { type: "string" }
return ( return (
Object.entries(schema) Object.entries(schema)
.map(newBindable) .map(newBindable)
// add _id and _rev fields - not part of schema, but always valid // add _id and _rev fields - not part of schema, but always valid
.concat([newBindable(["_id", "string"]), newBindable(["_rev", "string"])]) .concat([
newBindable(["_id", stringType]),
newBindable(["_rev", stringType]),
])
) )
} }

View File

@ -2,7 +2,9 @@ import { walkProps } from "./storeUtils"
import { get_capitalised_name } from "../helpers" import { get_capitalised_name } from "../helpers"
export default function(component, state) { export default function(component, state) {
const capitalised = get_capitalised_name(component) const capitalised = get_capitalised_name(
component.name || component._component
)
const matchingComponents = [] const matchingComponents = []

View File

@ -371,7 +371,7 @@ const addChildComponent = store => (componentToAdd, presetProps = {}) => {
const component = getComponentDefinition(state, componentToAdd) const component = getComponentDefinition(state, componentToAdd)
const instanceId = get(backendUiStore).selectedDatabase._id const instanceId = get(backendUiStore).selectedDatabase._id
const instanceName = getNewComponentName(componentToAdd, state) const instanceName = getNewComponentName(component, state)
const newComponent = createProps( const newComponent = createProps(
component, component,

View File

@ -1,12 +1,12 @@
<script> <script>
import { afterUpdate } from "svelte"
import { automationStore, backendUiStore } from "builderStore" import { automationStore, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import Flowchart from "./FlowChart/FlowChart.svelte"
import Flowchart from "./flowchart/FlowChart.svelte" import BlockList from "./BlockList.svelte"
$: automation = $automationStore.selectedAutomation?.automation $: automation = $automationStore.selectedAutomation?.automation
$: automationLive = automation?.live $: automationLive = automation?.live
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
$: automationCount = $automationStore.automations?.length ?? 0
function onSelect(block) { function onSelect(block) {
automationStore.update(state => { automationStore.update(state => {
@ -14,80 +14,19 @@
return state return state
}) })
} }
function setAutomationLive(live) {
automation.live = live
automationStore.actions.save({ instanceId, automation })
if (live) {
notifier.info(`Automation ${automation.name} enabled.`)
} else {
notifier.danger(`Automation ${automation.name} disabled.`)
}
}
</script> </script>
<section> {#if automation}
<BlockList />
<Flowchart {automation} {onSelect} /> <Flowchart {automation} {onSelect} />
</section> {:else if automationCount === 0}
<footer> <i>Create your first automation to get started</i>
{#if automation} {:else}<i>Select an automation to edit</i>{/if}
<button
class:highlighted={automationLive}
class:hoverable={automationLive}
class="stop-button hoverable">
<i class="ri-stop-fill" on:click={() => setAutomationLive(false)} />
</button>
<button
class:highlighted={!automationLive}
class:hoverable={!automationLive}
class="play-button hoverable"
data-cy="activate-automation"
on:click={() => setAutomationLive(true)}>
<i class="ri-play-fill" />
</button>
{/if}
</footer>
<style> <style>
section { i {
display: flex; font-size: var(--font-size-m);
flex-direction: column; color: var(--grey-5);
justify-content: flex-start; margin-top: 2px;
align-items: center;
overflow: auto;
height: 100%;
position: relative;
}
footer {
position: absolute;
bottom: var(--spacing-xl);
right: 30px;
display: flex;
align-items: flex-end;
}
footer > button {
border-radius: 100%;
color: var(--white);
width: 76px;
height: 76px;
border: none;
background: #adaec4;
font-size: 45px;
display: flex;
align-items: center;
justify-content: center;
}
footer > button:first-child {
margin-right: var(--spacing-m);
}
.play-button.highlighted {
background: var(--purple);
}
.stop-button.highlighted {
background: var(--red);
} }
</style> </style>

View File

@ -0,0 +1,136 @@
<script>
import { sortBy } from "lodash/fp"
import { automationStore } from "builderStore"
import { DropdownMenu, Modal } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
import analytics from "analytics"
import CreateWebhookModal from "../Shared/CreateWebhookModal.svelte"
$: hasTrigger = $automationStore.selectedAutomation.hasTrigger()
$: tabs = [
{
label: "Trigger",
value: "TRIGGER",
icon: "ri-organization-chart",
disabled: hasTrigger,
},
{
label: "Action",
value: "ACTION",
icon: "ri-flow-chart",
disabled: !hasTrigger,
},
{
label: "Logic",
value: "LOGIC",
icon: "ri-filter-line",
disabled: !hasTrigger,
},
]
let selectedIndex
let anchors = []
let popover
let webhookModal
$: selectedTab = selectedIndex == null ? null : tabs[selectedIndex].value
$: anchor = selectedIndex === -1 ? null : anchors[selectedIndex]
$: blocks = sortBy(entry => entry[1].name)(
Object.entries($automationStore.blockDefinitions[selectedTab] ?? {})
)
function onChangeTab(idx) {
selectedIndex = idx
popover.show()
}
function closePopover() {
selectedIndex = null
popover.hide()
}
function addBlockToAutomation(stepId, blockDefinition) {
const newBlock = {
...blockDefinition,
inputs: blockDefinition.inputs || {},
stepId,
type: selectedTab,
}
automationStore.actions.addBlockToAutomation(newBlock)
analytics.captureEvent("Added Automation Block", {
name: blockDefinition.name,
})
closePopover()
if (stepId === "WEBHOOK") {
webhookModal.show()
}
}
</script>
<div class="tab-container">
{#each tabs as tab, idx}
<div
bind:this={anchors[idx]}
class="tab"
class:disabled={tab.disabled}
on:click={tab.disabled ? null : () => onChangeTab(idx)}
class:active={idx === selectedIndex}>
{#if tab.icon}<i class={tab.icon} />{/if}
<span>{tab.label}</span>
<i class="ri-arrow-down-s-line arrow" />
</div>
{/each}
</div>
<DropdownMenu
on:close={() => (selectedIndex = null)}
bind:this={popover}
{anchor}
align="left">
<DropdownContainer>
{#each blocks as [stepId, blockDefinition]}
<DropdownItem
icon={blockDefinition.icon}
title={blockDefinition.name}
subtitle={blockDefinition.description}
on:click={() => addBlockToAutomation(stepId, blockDefinition)} />
{/each}
</DropdownContainer>
</DropdownMenu>
<Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal />
</Modal>
<style>
.tab-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-l);
min-height: 24px;
}
.tab {
color: var(--grey-7);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-xs);
}
.tab span {
font-weight: 500;
user-select: none;
}
.tab.active,
.tab:not(.disabled):hover {
color: var(--ink);
cursor: pointer;
}
.tab.disabled {
color: var(--grey-5);
}
.tab i:not(:last-child) {
font-size: 16px;
}
</style>

View File

@ -3,7 +3,6 @@
import Arrow from "./Arrow.svelte" import Arrow from "./Arrow.svelte"
import { flip } from "svelte/animate" import { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition" import { fade, fly } from "svelte/transition"
import { automationStore } from "builderStore"
export let automation export let automation
export let onSelect export let onSelect
@ -18,16 +17,9 @@
blocks = blocks.concat(automation.definition.steps || []) blocks = blocks.concat(automation.definition.steps || [])
} }
} }
$: automationCount = $automationStore.automations?.length ?? 0
</script> </script>
{#if automationCount === 0} {#if !blocks.length}<i>Add a trigger to your automation to get started</i>{/if}
<i>Create your first automation to get started</i>
{:else if automation == null}
<i>Select an automation to edit</i>
{:else if !blocks.length}
<i>Add some steps to your automation to get started</i>
{/if}
<section class="canvas"> <section class="canvas">
{#each blocks as block, idx (block.id)} {#each blocks as block, idx (block.id)}
<div <div
@ -44,19 +36,18 @@
</section> </section>
<style> <style>
i {
font-size: var(--font-size-xl);
color: var(--grey-4);
padding: var(--spacing-xl) 40px;
align-self: flex-start;
}
section { section {
position: absolute; margin: 0 -40px calc(-1 * var(--spacing-l)) -40px;
padding: 40px; padding: var(--spacing-l) 40px 0 40px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
overflow-y: auto;
flex: 1 1 auto;
}
/* Fix for firefox not respecting bottom padding in scrolling containers */
section > *:last-child {
padding-bottom: 40px;
} }
.block { .block {
@ -65,4 +56,9 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
} }
i {
font-size: var(--font-size-m);
color: var(--grey-5);
}
</style> </style>

View File

@ -10,6 +10,11 @@
$: steps = $: steps =
$automationStore.selectedAutomation?.automation?.definition?.steps ?? [] $automationStore.selectedAutomation?.automation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id) $: blockIdx = steps.findIndex(step => step.id === block.id)
$: allowDeleteTrigger = !steps.length
function deleteStep() {
automationStore.actions.deleteAutomationBlock(block)
}
</script> </script>
<div <div
@ -30,6 +35,9 @@
<div class="label"> <div class="label">
{#if block.type === 'TRIGGER'}Trigger{:else}Step {blockIdx + 1}{/if} {#if block.type === 'TRIGGER'}Trigger{:else}Step {blockIdx + 1}{/if}
</div> </div>
{#if block.type !== 'TRIGGER' || allowDeleteTrigger}
<i on:click|stopPropagation={deleteStep} class="delete ri-close-line" />
{/if}
</header> </header>
<hr /> <hr />
<p> <p>
@ -61,6 +69,7 @@
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
gap: var(--spacing-xs);
} }
header span { header span {
flex: 1 1 auto; flex: 1 1 auto;
@ -74,7 +83,13 @@
} }
header i { header i {
font-size: 20px; font-size: 20px;
margin-right: 5px; }
header i.delete {
opacity: 0.5;
}
header i.delete:hover {
cursor: pointer;
opacity: 1;
} }
.ACTION { .ACTION {

View File

@ -0,0 +1,34 @@
<script>
import { onMount } from "svelte"
import { automationStore } from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
onMount(() => {
automationStore.actions.fetch()
})
</script>
<div class="automations-list">
{#each $automationStore.automations as automation, idx}
<NavItem
border={idx > 0}
icon="ri-stackshare-line"
text={automation.name}
selected={automation._id === selectedAutomationId}
on:click={() => automationStore.actions.select(automation)}>
<EditAutomationPopover {automation} />
</NavItem>
{/each}
</div>
<style>
.automations-list {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
</style>

View File

@ -1,79 +0,0 @@
<script>
import { onMount } from "svelte"
import { automationStore } from "builderStore"
import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Button, Modal } from "@budibase/bbui"
let modal
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
onMount(() => {
automationStore.actions.fetch()
})
</script>
<section>
<Button primary wide on:click={modal.show}>Create New Automation</Button>
<ul>
{#each $automationStore.automations as automation}
<li
class="automation-item"
class:selected={automation._id === selectedAutomationId}
on:click={() => automationStore.actions.select(automation)}>
<i class="ri-stackshare-line" class:live={automation.live} />
{automation.name}
</li>
{/each}
</ul>
</section>
<Modal bind:this={modal}>
<CreateAutomationModal />
</Modal>
<style>
section {
display: flex;
flex-direction: column;
}
ul {
list-style-type: none;
padding: 0;
margin: var(--spacing-xl) 0 0 0;
flex: 1;
}
i {
color: var(--grey-6);
}
i.live {
color: var(--ink);
}
li {
font-size: 14px;
}
.automation-item {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
border-radius: var(--border-radius-m);
padding: var(--spacing-s) var(--spacing-m);
margin-bottom: var(--spacing-xs);
color: var(--ink);
}
.automation-item i {
font-size: 24px;
margin-right: var(--spacing-m);
}
.automation-item:hover {
cursor: pointer;
background: var(--grey-1);
}
.automation-item.selected {
background: var(--grey-2);
}
</style>

View File

@ -1,50 +1,44 @@
<script> <script>
import { automationStore } from "builderStore" import AutomationList from "./AutomationList.svelte"
import AutomationList from "./AutomationList/AutomationList.svelte" import CreateAutomationModal from "./CreateAutomationModal.svelte"
import BlockList from "./BlockList/BlockList.svelte" import { Modal } from "@budibase/bbui"
import { Heading } from "@budibase/bbui" import { automationStore, backendUiStore } from "builderStore"
import { Spacer } from "@budibase/bbui" import { notifier } from "builderStore/store/notifications"
let selectedTab = "AUTOMATIONS" let selectedTab = "AUTOMATIONS"
let modal
</script> </script>
<Heading black small> <div class="title">
<span <h1>Automations</h1>
data-cy="automation-list" <i
class="hoverable automation-header" on:click={modal.show}
class:selected={selectedTab === 'AUTOMATIONS'} data-cy="new-automation"
on:click={() => (selectedTab = 'AUTOMATIONS')}> class="ri-add-circle-fill" />
Automations </div>
</span> <AutomationList />
{#if $automationStore.selectedAutomation} <Modal bind:this={modal}>
<span <CreateAutomationModal />
data-cy="add-automation-component" </Modal>
class="hoverable"
class:selected={selectedTab === 'ADD'}
on:click={() => (selectedTab = 'ADD')}>
Steps
</span>
{/if}
</Heading>
<Spacer medium />
{#if selectedTab === 'AUTOMATIONS'}
<AutomationList />
{:else if selectedTab === 'ADD'}
<BlockList />
{/if}
<style> <style>
header { .title {
font-size: 18px;
font-weight: 600;
background: none;
display: flex; display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: var(--spacing-xl);
} }
.title h1 {
.automation-header { font-size: var(--font-size-m);
margin-right: var(--spacing-xl); font-weight: 500;
margin: 0;
}
.title i {
font-size: 20px;
}
.title i:hover {
cursor: pointer;
color: var(--blue);
} }
span:not(.selected) { span:not(.selected) {

View File

@ -1,82 +0,0 @@
<script>
import { backendUiStore, automationStore } from "builderStore"
import CreateWebookModal from "../../Shared/CreateWebhookModal.svelte"
import analytics from "analytics"
import { Modal } from "@budibase/bbui"
export let blockDefinition
export let stepId
export let blockType
let modal
$: instanceId = $backendUiStore.selectedDatabase._id
$: automation = $automationStore.selectedAutomation?.automation
function addBlockToAutomation() {
automationStore.actions.addBlockToAutomation({
...blockDefinition,
inputs: blockDefinition.inputs || {},
stepId,
type: blockType,
})
if (stepId === "WEBHOOK") {
modal.show()
}
analytics.captureEvent("Added Automation Block", {
name: blockDefinition.name,
})
}
</script>
<div
class="automation-block hoverable"
on:click={addBlockToAutomation}
data-cy={stepId}>
<div><i class={blockDefinition.icon} /></div>
<div class="automation-text">
<h4>{blockDefinition.name}</h4>
<p>{blockDefinition.description}</p>
</div>
</div>
<Modal bind:this={modal} width="30%">
<CreateWebookModal />
</Modal>
<style>
.automation-block {
display: grid;
grid-template-columns: 20px auto;
align-items: center;
margin-top: var(--spacing-s);
padding: var(--spacing-m);
border-radius: var(--border-radius-m);
}
.automation-block:hover {
background-color: var(--grey-1);
}
.automation-block:first-child {
margin-top: 0;
}
i {
color: var(--grey-7);
font-size: 20px;
}
.automation-text {
margin-left: 16px;
}
.automation-text h4 {
font-size: 14px;
font-weight: 500;
margin-bottom: 5px;
margin-top: 0;
}
.automation-text p {
font-size: 12px;
line-height: 1.4;
color: var(--grey-7);
margin: 0;
}
</style>

View File

@ -1,48 +0,0 @@
<script>
import { sortBy } from "lodash/fp"
import { automationStore } from "builderStore"
import AutomationBlock from "./AutomationBlock.svelte"
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
let selectedTab = "TRIGGER"
let buttonProps = []
$: blocks = sortBy(entry => entry[1].name)(
Object.entries($automationStore.blockDefinitions[selectedTab])
)
$: {
if ($automationStore.selectedAutomation.hasTrigger()) {
buttonProps = [
{ value: "ACTION", text: "Action" },
{ value: "LOGIC", text: "Logic" },
]
if (selectedTab === "TRIGGER") {
selectedTab = "ACTION"
}
} else {
buttonProps = [{ value: "TRIGGER", text: "Trigger" }]
if (selectedTab !== "TRIGGER") {
selectedTab = "TRIGGER"
}
}
}
function onChangeTab(tab) {
selectedTab = tab
}
</script>
<section>
<FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
<div id="blocklist">
{#each blocks as [stepId, blockDefinition]}
<AutomationBlock {blockDefinition} {stepId} blockType={selectedTab} />
{/each}
</div>
</section>
<style>
#blocklist {
margin-top: var(--spacing-xl);
}
</style>

View File

@ -0,0 +1,88 @@
<script>
import { automationStore, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let automation
let anchor
let dropdown
let confirmDeleteDialog
$: instanceId = $backendUiStore.selectedDatabase._id
function showModal() {
dropdown.hide()
confirmDeleteDialog.show()
}
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
automation,
})
notifier.success("Automation deleted.")
}
</script>
<div on:click|stopPropagation>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu align="left" {anchor} bind:this={dropdown}>
<DropdownContainer>
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={showModal} />
</DropdownContainer>
</DropdownMenu>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Automation"
onOk={deleteAutomation}
title="Confirm Deletion">
Are you sure you wish to delete the automation
<i>{automation.name}?</i>
This action cannot be undone.
</ConfirmDialog>
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.icon i {
font-size: 16px;
}
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 0;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
</style>

View File

@ -1,3 +0,0 @@
export { default as AutomationPanel } from "./AutomationPanel.svelte"
export { default as BlockList } from "./BlockList/BlockList.svelte"
export { default as AutomationList } from "./AutomationList/AutomationList.svelte"

View File

@ -1,10 +1,10 @@
<script> <script>
import TableSelector from "./ParamInputs/TableSelector.svelte" import TableSelector from "./TableSelector.svelte"
import RowSelector from "./ParamInputs/RowSelector.svelte" import RowSelector from "./RowSelector.svelte"
import { Button, Input, TextArea, Select, Label } from "@budibase/bbui" import { Button, Input, Select, Label } from "@budibase/bbui"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte" import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import BindableInput from "../../userInterface/BindableInput.svelte" import BindableInput from "components/userInterface/BindableInput.svelte"
export let block export let block
export let webhookModal export let webhookModal
@ -47,43 +47,41 @@
} }
</script> </script>
<div class="container" data-cy="automation-block-setup"> <div class="block-label">{block.name}</div>
<div class="block-label">{block.name}</div> {#each inputs as [key, value]}
{#each inputs as [key, value]} <div class="block-field">
<div class="bb-margin-xl block-field"> <Label extraSmall grey>{value.title}</Label>
<Label extraSmall grey>{value.title}</Label> {#if value.type === 'string' && value.enum}
{#if value.type === 'string' && value.enum} <Select bind:value={block.inputs[key]} extraThin secondary>
<Select bind:value={block.inputs[key]} thin secondary> <option value="">Choose an option</option>
<option value="">Choose an option</option> {#each value.enum as option, idx}
{#each value.enum as option, idx} <option value={option}>
<option value={option}> {value.pretty ? value.pretty[idx] : option}
{value.pretty ? value.pretty[idx] : option} </option>
</option> {/each}
{/each} </Select>
</Select> {:else if value.customType === 'password'}
{:else if value.customType === 'password'} <Input type="password" extraThin bind:value={block.inputs[key]} />
<Input type="password" thin bind:value={block.inputs[key]} /> {:else if value.customType === 'table'}
{:else if value.customType === 'table'} <TableSelector bind:value={block.inputs[key]} />
<TableSelector bind:value={block.inputs[key]} /> {:else if value.customType === 'row'}
{:else if value.customType === 'row'} <RowSelector bind:value={block.inputs[key]} {bindings} />
<RowSelector bind:value={block.inputs[key]} {bindings} /> {:else if value.customType === 'webhookUrl'}
{:else if value.customType === 'webhookUrl'} <WebhookDisplay value={block.inputs[key]} />
<WebhookDisplay value={block.inputs[key]} /> {:else if value.type === 'string' || value.type === 'number'}
{:else if value.type === 'string' || value.type === 'number'} <BindableInput
<BindableInput type="string"
type="string" extraThin
thin bind:value={block.inputs[key]}
bind:value={block.inputs[key]} {bindings} />
{bindings} /> {/if}
{/if} </div>
</div> {/each}
{/each} {#if stepId === 'WEBHOOK'}
{#if stepId === 'WEBHOOK'} <Button wide secondary on:click={() => webhookModal.show()}>
<Button wide secondary on:click={() => webhookModal.show()}> Set Up Webhook
Setup webhook </Button>
</Button> {/if}
{/if}
</div>
<style> <style>
.block-field { .block-field {
@ -92,7 +90,7 @@
.block-label { .block-label {
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: var(--font-size-xs);
color: var(--grey-7); color: var(--grey-7);
} }

View File

@ -1,7 +1,7 @@
<script> <script>
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { Input, Select, Label } from "@budibase/bbui" import { Input, Select, Label } from "@budibase/bbui"
import BindableInput from "../../../userInterface/BindableInput.svelte" import BindableInput from "../../userInterface/BindableInput.svelte"
export let value export let value
export let bindings export let bindings
@ -19,7 +19,7 @@
</script> </script>
<div class="block-field"> <div class="block-field">
<Select bind:value={value.tableId} thin secondary> <Select bind:value={value.tableId} extraThin secondary>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each $backendUiStore.tables as table} {#each $backendUiStore.tables as table}
<option value={table._id}>{table.name}</option> <option value={table._id}>{table.name}</option>
@ -31,7 +31,7 @@
<div class="schema-fields"> <div class="schema-fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
{#if schemaHasOptions(schema)} {#if schemaHasOptions(schema)}
<Select label={field} thin secondary bind:value={value[field]}> <Select label={field} extraThin secondary bind:value={value[field]}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each schema.constraints.inclusion as option} {#each schema.constraints.inclusion as option}
<option value={option}>{option}</option> <option value={option}>{option}</option>
@ -39,7 +39,7 @@
</Select> </Select>
{:else if schema.type === 'string' || schema.type === 'number'} {:else if schema.type === 'string' || schema.type === 'number'}
<BindableInput <BindableInput
thin extraThin
bind:value={value[field]} bind:value={value[field]}
label={field} label={field}
type="string" type="string"

View File

@ -2,25 +2,27 @@
import { backendUiStore, automationStore } from "builderStore" import { backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import AutomationBlockSetup from "./AutomationBlockSetup.svelte" import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
import { Button, Input, Label, Modal } from "@budibase/bbui" import { Button, Modal } from "@budibase/bbui"
import CreateWebookModal from "../Shared/CreateWebhookModal.svelte" import CreateWebookModal from "../Shared/CreateWebhookModal.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
let selectedTab = "SETUP"
let confirmDeleteDialog
let webhookModal let webhookModal
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
$: automation = $automationStore.selectedAutomation?.automation $: automation = $automationStore.selectedAutomation?.automation
$: allowDeleteBlock =
$automationStore.selectedBlock?.type !== "TRIGGER" ||
!automation?.definition?.steps?.length
$: name = automation?.name ?? "" $: name = automation?.name ?? ""
$: automationLive = automation?.live
function deleteAutomationBlock() { function setAutomationLive(live) {
automationStore.actions.deleteAutomationBlock( if (automation.live === live) {
$automationStore.selectedBlock return
) }
automation.live = live
automationStore.actions.save({ instanceId, automation })
if (live) {
notifier.info(`Automation ${automation.name} enabled.`)
} else {
notifier.danger(`Automation ${automation.name} disabled.`)
}
} }
async function testAutomation() { async function testAutomation() {
@ -41,115 +43,78 @@
}) })
notifier.success(`Automation ${automation.name} saved.`) notifier.success(`Automation ${automation.name} saved.`)
} }
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
automation,
})
notifier.success("Automation deleted.")
}
</script> </script>
<section> <div class="title">
<header> <h1>Setup</h1>
<span <i
class="hoverable" class:highlighted={automationLive}
class:selected={selectedTab === 'SETUP'} class:hoverable={automationLive}
on:click={() => (selectedTab = 'SETUP')}> on:click={() => setAutomationLive(false)}
Setup class="ri-stop-circle-fill" />
</span> <i
</header> class:highlighted={!automationLive}
<div class="content"> class:hoverable={!automationLive}
{#if $automationStore.selectedBlock} data-cy="activate-automation"
<AutomationBlockSetup on:click={() => setAutomationLive(true)}
bind:block={$automationStore.selectedBlock} class="ri-play-circle-fill" />
{webhookModal} /> </div>
{:else if $automationStore.selectedAutomation} {#if $automationStore.selectedBlock}
<div class="block-label">Automation <b>{automation.name}</b></div> <AutomationBlockSetup
<Button secondary wide on:click={testAutomation}>Test Automation</Button> bind:block={$automationStore.selectedBlock}
{/if} {webhookModal} />
</div> {:else if $automationStore.selectedAutomation}
<div class="buttons"> <div class="block-label">{automation.name}</div>
{#if $automationStore.selectedBlock} <Button secondary wide on:click={testAutomation}>Test Automation</Button>
<Button {/if}
green <Button
wide secondary
data-cy="save-automation-setup" wide
on:click={saveAutomation}> data-cy="save-automation-setup"
Save Automation on:click={saveAutomation}>
</Button> Save Automation
<Button </Button>
disabled={!allowDeleteBlock}
red
wide
on:click={deleteAutomationBlock}>
Delete Step
</Button>
{:else if $automationStore.selectedAutomation}
<Button
green
wide
data-cy="save-automation-setup"
on:click={saveAutomation}>
Save 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} />
<Modal bind:this={webhookModal} width="30%"> <Modal bind:this={webhookModal} width="30%">
<CreateWebookModal /> <CreateWebookModal />
</Modal> </Modal>
<style> <style>
section { .title {
display: flex;
flex-direction: column;
height: 100%;
justify-content: flex-start;
align-items: stretch;
}
header {
font-size: 18px;
font-weight: 600;
font-family: inter, sans-serif;
display: flex; display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: var(--spacing-xl); gap: var(--spacing-xs);
color: var(--ink);
} }
header > span { .title h1 {
font-size: var(--font-size-m);
font-weight: 500;
margin: 0;
flex: 1 1 auto;
}
.title i {
font-size: 20px;
color: var(--grey-5); color: var(--grey-5);
margin-right: var(--spacing-xl);
cursor: pointer;
} }
.selected { .title i.highlighted {
color: var(--ink); color: var(--ink);
} }
.title i.hoverable:hover {
cursor: pointer;
color: var(--blue);
}
.block-label { .block-label {
font-size: var(--font-size-xs);
font-weight: 500; font-weight: 500;
font-size: 14px;
color: var(--grey-7); color: var(--grey-7);
margin-bottom: var(--spacing-xl);
} }
.content { .footer {
flex: 1 0 auto; flex: 1 1 auto;
} display: flex;
flex-direction: column;
.buttons { justify-content: flex-end;
display: grid; align-items: stretch;
gap: var(--spacing-m);
} }
</style> </style>

View File

@ -6,7 +6,7 @@
</script> </script>
<div class="block-field"> <div class="block-field">
<Select bind:value secondary thin> <Select bind:value secondary extraThin>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each $backendUiStore.tables as table} {#each $backendUiStore.tables as table}
<option value={table._id}>{table.name}</option> <option value={table._id}>{table.name}</option>

View File

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

View File

@ -3,12 +3,8 @@
import WebhookDisplay from "./WebhookDisplay.svelte" import WebhookDisplay from "./WebhookDisplay.svelte"
import { ModalContent } from "@budibase/bbui" import { ModalContent } from "@budibase/bbui"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { cloneDeep } from "lodash/fp"
import analytics from "analytics"
const POLL_RATE_MS = 2500 const POLL_RATE_MS = 2500
const DEFAULT_SCHEMA_OUTPUT = "Any input allowed"
let name
let interval let interval
let finished = false let finished = false
let schemaURL let schemaURL
@ -88,8 +84,7 @@
text-decoration: none; text-decoration: none;
} }
p { p {
margin-top: 0; margin: 0;
padding-top: 0;
text-align: justify; text-align: justify;
} }
.finished-text { .finished-text {

View File

@ -1,7 +1,7 @@
<script> <script>
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { Input } from "@budibase/bbui" import { Input } from "@budibase/bbui"
import { store } from "../../../builderStore" import { store } from "builderStore"
export let value export let value
export let production = false export let production = false

View File

@ -1,3 +0,0 @@
export { default as AutomationBuilder } from "./AutomationBuilder/AutomationBuilder.svelte"
export { default as SetupPanel } from "./SetupPanel/SetupPanel.svelte"
export { default as AutomationPanel } from "./AutomationPanel/AutomationPanel.svelte"

View File

@ -12,7 +12,6 @@
$: title = $backendUiStore.selectedTable.name $: title = $backendUiStore.selectedTable.name
$: schema = $backendUiStore.selectedTable.schema $: schema = $backendUiStore.selectedTable.schema
$: tableId = $backendUiStore.selectedTable._id
$: tableView = { $: tableView = {
schema, schema,
name: $backendUiStore.selectedView.name, name: $backendUiStore.selectedView.name,

View File

@ -1,7 +1,6 @@
<script> <script>
import api from "builderStore/api" import api from "builderStore/api"
import Table from "./Table.svelte" import Table from "./Table.svelte"
import { onMount } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
export let tableId export let tableId

View File

@ -1,6 +1,5 @@
<script> <script>
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { Button } from "@budibase/bbui"
import { goto, params } from "@sveltech/routify" import { goto, params } from "@sveltech/routify"
import AgGrid from "@budibase/svelte-ag-grid" import AgGrid from "@budibase/svelte-ag-grid"
@ -8,11 +7,7 @@
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte" import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
import { import { getRenderer, editRowRenderer } from "./cells/cellRenderers"
getRenderer,
editRowRenderer,
deleteRowRenderer,
} from "./cells/cellRenderers"
import TableLoadingOverlay from "./TableLoadingOverlay" import TableLoadingOverlay from "./TableLoadingOverlay"
import TableHeader from "./TableHeader" import TableHeader from "./TableHeader"
import "@budibase/svelte-ag-grid/dist/index.css" import "@budibase/svelte-ag-grid/dist/index.css"
@ -58,8 +53,8 @@
resizable: false, resizable: false,
suppressMovable: true, suppressMovable: true,
suppressMenu: true, suppressMenu: true,
minWidth: 100, minWidth: 84,
width: 100, width: 84,
cellRenderer: editRowRenderer, cellRenderer: editRowRenderer,
}, },
] ]
@ -100,7 +95,7 @@
} }
const deleteRows = async () => { const deleteRows = async () => {
const response = await api.post(`/api/${tableId}/rows`, { await api.post(`/api/${tableId}/rows`, {
rows: selectedRows, rows: selectedRows,
type: "delete", type: "delete",
}) })
@ -110,16 +105,23 @@
} }
</script> </script>
<section> <div>
<div class="table-controls"> <div class="table-title">
<h2 class="title"><span>{title}</span></h2> <h1>{title}</h1>
<div class="popovers"> {#if loading}
<slot /> <div transition:fade>
{#if selectedRows.length > 0} <Spinner size="10" />
<DeleteRowsButton {selectedRows} {deleteRows} /> </div>
{/if} {/if}
</div>
</div> </div>
<div class="popovers">
<slot />
{#if selectedRows.length > 0}
<DeleteRowsButton {selectedRows} {deleteRows} />
{/if}
</div>
</div>
<div class="grid-wrapper">
<AgGrid <AgGrid
{theme} {theme}
{options} {options}
@ -127,35 +129,57 @@
{columnDefs} {columnDefs}
{loading} {loading}
on:select={({ detail }) => (selectedRows = detail)} /> on:select={({ detail }) => (selectedRows = detail)} />
</section> </div>
<style> <style>
.title { .table-title {
font-size: 24px; height: 24px;
font-weight: 600;
text-rendering: optimizeLegibility;
margin-top: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
} }
.table-title h1 {
.title > span { font-size: var(--font-size-m);
margin-right: var(--spacing-xs); font-weight: 500;
margin: 0;
} }
.table-title > div {
.table-controls { margin-left: var(--spacing-xs);
width: 100%;
} }
.popovers { .popovers {
display: flex; display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-l);
}
.popovers :global(button) {
font-weight: 500;
margin-top: var(--spacing-l);
}
.popovers :global(button svg) {
margin-right: var(--spacing-xs);
} }
:global(.popovers > div) { .grid-wrapper {
margin-right: var(--spacing-m); flex: 1 1 auto;
margin-bottom: var(--spacing-xl); display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.grid-wrapper :global(> *) {
height: auto;
flex: 1 1 auto;
}
:global(.ag-theme-alpine) {
--ag-border-color: var(--grey-4);
--ag-header-background-color: var(--grey-1);
--ag-odd-row-background-color: var(--grey-1);
--ag-row-border-color: var(--grey-3);
} }
:global(.ag-menu) { :global(.ag-menu) {
@ -168,11 +192,15 @@
} }
:global(.ag-header-cell-text) { :global(.ag-header-cell-text) {
font-family: Inter; font-family: var(--font-sans);
font-weight: 600; font-weight: 600;
color: var(--ink); color: var(--ink);
} }
tbody tr:hover {
background: var(--grey-1);
}
:global(.ag-filter) { :global(.ag-filter) {
padding: var(--spacing-s); padding: var(--spacing-s);
outline: none; outline: none;

View File

@ -1,6 +1,6 @@
<script> <script>
import { TextButton, Icon, Modal, ModalContent } from "@budibase/bbui" import { TextButton, Icon } from "@budibase/bbui"
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let selectedRows export let selectedRows
export let deleteRows export let deleteRows
@ -21,14 +21,12 @@
row(s) row(s)
</TextButton> </TextButton>
</div> </div>
<Modal bind:this={modal}> <ConfirmDialog
<ModalContent bind:this={modal}
red okText="Delete"
confirmText="Delete" onOk={confirmDeletion}
onConfirm={confirmDeletion} title="Confirm Deletion">
title="Confirm Row Deletion"> Are you sure you want to delete
Are you sure you want to delete {selectedRows.length}
{selectedRows.length} row{selectedRows.length > 1 ? 's' : ''}?
row{selectedRows.length > 1 ? 's' : ''}? </ConfirmDialog>
</ModalContent>
</Modal>

View File

@ -170,7 +170,7 @@
okText="Delete Column" okText="Delete Column"
onOk={deleteColumn} onOk={deleteColumn}
onCancel={hideDeleteDialog} onCancel={hideDeleteDialog}
title="Confirm Delete" /> title="Confirm Deletion" />
<style> <style>
.actions { .actions {

View File

@ -1,14 +1,11 @@
<script> <script>
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { DropdownMenu, Icon, Modal } from "@budibase/bbui"
import * as api from "../api" import * as api from "../api"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let row export let row
let anchor
let dropdown
let confirmDeleteDialog let confirmDeleteDialog
function showDelete() { function showDelete() {
@ -28,7 +25,7 @@
body={`Are you sure you wish to delete this row? Your data will be deleted and this action cannot be undone.`} body={`Are you sure you wish to delete this row? Your data will be deleted and this action cannot be undone.`}
okText="Delete Row" okText="Delete Row"
onOk={deleteRow} onOk={deleteRow}
title="Confirm Delete" /> title="Confirm Deletion" />
<style> <style>
.ri-delete-bin-line:hover { .ri-delete-bin-line:hover {

View File

@ -1,13 +1,9 @@
<script> <script>
import { backendUiStore } from "builderStore" import { Modal, Button } from "@budibase/bbui"
import { DropdownMenu, Icon, Modal, Button } from "@budibase/bbui"
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte" import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
export let row export let row
let anchor
let dropdown
let confirmDeleteDialog
let modal let modal
function showModal(e) { function showModal(e) {
@ -16,7 +12,7 @@
} }
</script> </script>
<Button translucent small on:click={showModal}>Edit</Button> <Button data-cy="edit-row" translucent small on:click={showModal}>Edit</Button>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<CreateEditRowModal {row} /> <CreateEditRowModal {row} />
</Modal> </Modal>

View File

@ -1,10 +1,9 @@
<script> <script>
import { Heading, Body, Button, Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import api from "builderStore/api" import api from "builderStore/api"
const BYTES_IN_KB = 1000
const BYTES_IN_MB = 1000000 const BYTES_IN_MB = 1000000
const FILE_SIZE_LIMIT = BYTES_IN_MB * 1 const FILE_SIZE_LIMIT = BYTES_IN_MB * 1

View File

@ -5,9 +5,10 @@
import CreateTableModal from "./modals/CreateTableModal.svelte" import CreateTableModal from "./modals/CreateTableModal.svelte"
import EditTablePopover from "./popovers/EditTablePopover.svelte" import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte" import EditViewPopover from "./popovers/EditViewPopover.svelte"
import { Heading } from "@budibase/bbui" import { Modal } from "@budibase/bbui"
import { Spacer } from "@budibase/bbui" import NavItem from "components/common/NavItem.svelte"
let modal
$: selectedView = $: selectedView =
$backendUiStore.selectedView && $backendUiStore.selectedView.name $backendUiStore.selectedView && $backendUiStore.selectedView.name
@ -20,67 +21,68 @@
backendUiStore.actions.views.select(view) backendUiStore.actions.views.select(view)
$goto(`./view/${view.name}`) $goto(`./view/${view.name}`)
} }
function onClickView(table, viewName) {
if (selectedView === viewName) {
return
}
selectView({
name: viewName,
...table.views[viewName],
})
}
</script> </script>
<div class="items-root"> {#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id} <div class="title">
<div class="hierarchy"> <h1>Tables</h1>
<div class="components-list-container"> <i data-cy="new-table" on:click={modal.show} class="ri-add-circle-fill" />
<Heading small>Tables</Heading> </div>
<Spacer medium /> <div class="hierarchy-items-container">
<CreateTableModal /> {#each $backendUiStore.tables as table, idx}
<div class="hierarchy-items-container"> <NavItem
{#each $backendUiStore.tables as table} border={idx > 0}
<ListItem icon="ri-table-line"
selected={selectedView === `all_${table._id}`} text={table.name}
title={table.name} selected={selectedView === `all_${table._id}`}
icon="ri-table-fill" on:click={() => selectTable(table)}>
on:click={() => selectTable(table)}> <EditTablePopover {table} />
<EditTablePopover {table} /> </NavItem>
</ListItem> {#each Object.keys(table.views || {}) as viewName}
{#each Object.keys(table.views || {}) as viewName} <NavItem
<ListItem indentLevel={1}
indented icon="ri-eye-line"
selected={selectedView === viewName} text={viewName}
title={viewName} selected={selectedView === viewName}
icon="ri-eye-line" on:click={() => onClickView(table, viewName)}>
on:click={() => (selectedView === viewName ? {} : selectView({ <EditViewPopover
name: viewName, view={{ name: viewName, ...table.views[viewName] }} />
...table.views[viewName], </NavItem>
}))}> {/each}
<EditViewPopover {/each}
view={{ name: viewName, ...table.views[viewName] }} /> </div>
</ListItem> {/if}
{/each} <Modal bind:this={modal}>
{/each} <CreateTableModal />
</div> </Modal>
</div>
</div>
{/if}
</div>
<style> <style>
h5 { .title {
font-size: 18px;
font-weight: 600;
margin-top: 0;
margin-bottom: var(--spacing-xl);
}
.items-root {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
justify-content: flex-start; justify-content: space-between;
align-items: stretch; align-items: center;
} }
.title h1 {
.hierarchy { font-size: var(--font-size-m);
display: flex; font-weight: 500;
flex-direction: column; margin: 0;
} }
.title i {
.hierarchy-items-container { font-size: 20px;
margin-top: var(--spacing-xl); }
flex: 1 1 auto; .title i:hover {
cursor: pointer;
color: var(--blue);
} }
</style> </style>

View File

@ -2,8 +2,7 @@
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore" import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { Button, Input, Label, ModalContent, Modal } from "@budibase/bbui" import { Input, Label, ModalContent } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import TableDataImport from "../TableDataImport.svelte" import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics" import analytics from "analytics"
import screenTemplates from "builderStore/store/screenTemplates" import screenTemplates from "builderStore/store/screenTemplates"
@ -22,12 +21,6 @@
let dataImport let dataImport
let error = "" let error = ""
function resetState() {
name = ""
dataImport = undefined
error = ""
}
function checkValid(evt) { function checkValid(evt) {
const tableName = evt.target.value const tableName = evt.target.value
if ($backendUiStore.models?.some(model => model.name === tableName)) { if ($backendUiStore.models?.some(model => model.name === tableName)) {
@ -84,23 +77,20 @@
} }
</script> </script>
<Button primary wide on:click={modal.show}>Create New Table</Button> <ModalContent
<Modal bind:this={modal} on:hide={resetState}> title="Create Table"
<ModalContent confirmText="Create"
title="Create Table" onConfirm={saveTable}
confirmText="Create" disabled={error || !name || (dataImport && !dataImport.valid)}>
onConfirm={saveTable} <Input
disabled={error || !name || (dataImport && !dataImport.valid)}> data-cy="table-name-input"
<Input thin
data-cy="table-name-input" label="Table Name"
thin on:input={checkValid}
label="Table Name" bind:value={name}
on:input={checkValid} {error} />
bind:value={name} <div>
{error} /> <Label grey extraSmall>Create Table from CSV (Optional)</Label>
<div> <TableDataImport bind:dataImport />
<Label grey extraSmall>Create Table from CSV (Optional)</Label> </div>
<TableDataImport bind:dataImport /> </ModalContent>
</div>
</ModalContent>
</Modal>

View File

@ -1,11 +1,9 @@
<script> <script>
import { backendUiStore, store } from "builderStore" import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Input } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import screenTemplates from "builderStore/store/screenTemplates" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
import api from "builderStore/api"
export let table export let table
@ -66,42 +64,46 @@
} }
</script> </script>
<div bind:this={anchor} class="icon" on:click={dropdown.show}> <div on:click|stopPropagation>
<i class="ri-more-line" /> <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}
on:input={checkValid}
{error} />
<footer>
<Button secondary on:click={hideEditor}>Cancel</Button>
<Button primary disabled={error} on:click={save}>Save</Button>
</footer>
</div>
{:else}
<DropdownContainer>
<DropdownItem
icon="ri-edit-line"
data-cy="edit-table"
title="Edit"
on:click={showEditor} />
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={showModal}
data-cy="delete-table" />
</DropdownContainer>
{/if}
</DropdownMenu>
</div> </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}
on:input={checkValid}
{error} />
<footer>
<Button secondary on:click={hideEditor}>Cancel</Button>
<Button primary disabled={error} 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 <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
okText="Delete Table" okText="Delete Table"
onOk={deleteTable} onOk={deleteTable}
title="Confirm Delete"> title="Confirm Deletion">
Are you sure you wish to delete the table Are you sure you wish to delete the table
<i>{table.name}?</i> <i>{table.name}?</i>
The following will also be deleted: The following will also be deleted:
@ -155,29 +157,4 @@
justify-content: flex-end; justify-content: flex-end;
gap: var(--spacing-m); 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> </style>

View File

@ -2,9 +2,9 @@
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Input } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let view export let view
@ -46,38 +46,42 @@
} }
</script> </script>
<div bind:this={anchor} class="icon" on:click={dropdown.show}> <div on:click|stopPropagation>
<i class="ri-more-line" /> <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 View</h5>
<Input label="View Name" thin bind:value={view.name} />
<footer>
<Button secondary on:click={hideEditor}>Cancel</Button>
<Button primary on:click={save}>Save</Button>
</footer>
</div>
{:else}
<DropdownContainer>
<DropdownItem
icon="ri-edit-line"
data-cy="edit-view"
title="Edit"
on:click={showEditor} />
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
data-cy="delete-view"
on:click={showDelete} />
</DropdownContainer>
{/if}
</DropdownMenu>
</div> </div>
<DropdownMenu align="left" {anchor} bind:this={dropdown}>
{#if editing}
<div class="actions">
<h5>Edit View</h5>
<Input label="View Name" thin bind:value={view.name} />
<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-view" on:click={showDelete}>
<Icon name="delete" />
Delete
</li>
</ul>
{/if}
</DropdownMenu>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} 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.`} 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" okText="Delete View"
onOk={deleteView} onOk={deleteView}
title="Confirm Delete" /> title="Confirm Deletion" />
<style> <style>
div.icon { div.icon {
@ -108,29 +112,4 @@
justify-content: flex-end; justify-content: flex-end;
gap: var(--spacing-m); 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> </style>

View File

@ -1,70 +0,0 @@
<script>
export let disabled = false
export let hidden = false
export let secondary = false
export let primary = true
export let cancel = false
export let alert = false
export let warning = false
</script>
<button
on:click
class="button"
class:hidden
class:secondary
class:primary
class:alert
class:cancel
class:warning
{disabled}>
<slot />
</button>
<style>
.primary {
color: #ffffff;
background: var(--blue);
border: solid 1px var(--blue);
}
.alert {
color: white;
background: #e26d69;
border: solid 1px #e26d69;
}
.cancel {
color: var(--secondary40);
background: none;
}
.secondary {
color: var(--ink);
border: solid 1px var(--grey-4);
background: white;
}
.button {
font-size: 14px;
font-weight: 500;
border-radius: 3px;
padding: 10px 20px;
height: 40px;
}
.button:hover {
cursor: pointer;
filter: saturate(90%);
}
.button:disabled {
color: rgba(22, 48, 87, 0.2);
cursor: default;
background: transparent;
}
.hidden {
visibility: hidden;
}
</style>

View File

@ -1,171 +0,0 @@
<script>
export let color = "primary"
export let className = ""
export let style = ""
export let groupPosition = ""
export let grouped = false
$: borderClass = grouped ? "" : "border-normal"
</script>
<button
class="{color}
{className}
{borderClass}
{grouped ? 'grouped' : ''}"
{style}
on:click>
<slot />
</button>
<style>
.border-normal {
border-radius: var(--borderradiusall);
}
.border-left {
border-radius: var(--borderradius) 0 0 var(--borderradius);
}
.border-right {
border-radius: 0 var(--borderradius) var(--borderradius) 0;
}
.border-middle {
border-radius: 0;
}
button {
border-style: solid;
padding: 7.5px 15px;
cursor: pointer;
margin: 5px;
border-radius: 5px;
}
/* ---- PRIMARY ----*/
.primary {
background-color: var(--blue);
border-color: var(--blue);
color: var(--white);
}
.primary:hover {
background-color: var(--blue);
border-color: var(--blue);
}
.primary:active {
background-color: var(--primarydark);
border-color: var(--primarydark);
}
.primary-outline {
background-color: var(--white);
border-color: var(--blue);
color: var(--blue);
}
.primary-outline:hover {
background-color: var(--primary10);
}
.primary-outline:pressed {
background-color: var(--primary25);
}
/* ---- secondary ----*/
.secondary {
background-color: var(--ink);
border-color: var(--ink);
color: var(--white);
}
.secondary:hover {
background-color: var(--secondary75);
border-color: var(--secondary75);
}
.secondary:pressed {
background-color: var(--secondarydark);
border-color: var(--secondarydark);
}
.secondary-outline {
background-color: var(--white);
border-color: var(--ink);
color: var(--ink);
}
.secondary-outline:hover {
background-color: var(--secondary10);
}
.secondary-outline:pressed {
background-color: var(--secondary25);
}
/* ---- success ----*/
.success {
background-color: var(--success100);
border-color: var(--success100);
color: var(--white);
}
.success:hover {
background-color: var(--success75);
border-color: var(--success75);
}
.success:pressed {
background-color: var(--successdark);
border-color: var(--successdark);
}
.success-outline {
background-color: var(--white);
border-color: var(--success100);
color: var(--success100);
}
.success-outline:hover {
background-color: var(--success10);
}
.success-outline:pressed {
background-color: var(--success25);
}
/* ---- deletion ----*/
.deletion {
background-color: var(--red);
border-color: var(--red);
color: var(--white);
}
.deletion:hover {
background-color: var(--red-light);
border-color: var(--red);
color: var(--red);
}
.deletion:pressed {
background-color: var(--red-dark);
border-color: var(--red-dark);
color: var(--white);
}
.deletion-outline {
background-color: var(--white);
border-color: var(--red);
color: var(--red);
}
.deletion-outline:hover {
background-color: var(--red-light);
color: var(--red);
}
.deletion-outline:pressed {
background-color: var(--red-dark);
color: var(--white);
}
</style>

View File

@ -1,15 +0,0 @@
<script>
export let style = ""
</script>
<div class="root" {style}>
<slot />
</div>
<style>
.root {
display: flex;
flex-direction: row;
justify-content: space-between;
}
</style>

View File

@ -0,0 +1,13 @@
<div class="dropdown-container" on:click>
<slot />
</div>
<style>
.dropdown-container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
padding: var(--spacing-s) 0;
}
</style>

View File

@ -0,0 +1,70 @@
<script>
export let icon
export let title
export let subtitle
export let disabled
</script>
<div
class="dropdown-item"
class:disabled
on:click
class:big={subtitle != null}
{...$$restProps}>
{#if icon}<i class={icon} />{/if}
<div class="content">
<div class="title">{title}</div>
{#if subtitle != null}
<div class="subtitle">{subtitle}</div>
{/if}
</div>
</div>
<style>
.dropdown-item {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
padding: var(--spacing-xs) var(--spacing-l);
color: var(--ink);
}
.dropdown-item.disabled,
.dropdown-item.disabled .subtitle {
pointer-events: none;
color: var(--grey-5);
}
.dropdown-item.big {
padding: var(--spacing-s) var(--spacing-l);
}
.dropdown-item:not(.disabled):hover {
background-color: var(--grey-2);
cursor: pointer;
}
.content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.title,
.subtitle {
font-size: var(--font-size-xs);
}
.title {
font-weight: 400;
}
.subtitle {
color: var(--grey-7);
font-weight: 400;
}
i {
font-size: 16px;
}
</style>

View File

@ -0,0 +1,2 @@
export { default as DropdownContainer } from "./DropdownContainer.svelte"
export { default as DropdownItem } from "./DropdownItem.svelte"

View File

@ -1,11 +1,13 @@
<script> <script>
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { Heading, Body, Button, Dropzone } from "@budibase/bbui" import { Dropzone } from "@budibase/bbui"
import api from "builderStore/api" import api from "builderStore/api"
export let files = [] export let files = []
function handleFileTooLarge() { const BYTES_IN_MB = 1000000
function handleFileTooLarge(fileSizeLimit) {
notifier.danger( notifier.danger(
`Files cannot exceed ${fileSizeLimit / `Files cannot exceed ${fileSizeLimit /
BYTES_IN_MB}MB. Please try again with smaller files.` BYTES_IN_MB}MB. Please try again with smaller files.`

View File

@ -0,0 +1,109 @@
<script>
export let icon
export let withArrow = false
export let withActions = true
export let indentLevel = 0
export let text
export let border = true
export let selected = false
export let opened = false
export let draggable = false
</script>
<div
class="nav-item"
class:border
class:selected
style={`padding-left: ${indentLevel * 18}px`}
{draggable}
on:dragend
on:dragstart
on:dragover
on:drop
on:click
ondragover="return false"
ondragenter="return false">
<div class="content">
{#if withArrow}
<div class:opened class="icon arrow">
<i class="ri-arrow-right-s-line" />
</div>
{/if}
{#if icon}
<div class="icon"><i class={icon} /></div>
{/if}
<div class="text">{text}</div>
{#if withActions}
<div class="actions">
<slot />
</div>
{/if}
</div>
</div>
<style>
.nav-item {
border-radius: var(--border-radius-m);
cursor: pointer;
color: var(--grey-7);
}
.nav-item.border {
border-top: 1px solid var(--grey-1);
}
.nav-item.selected {
background-color: var(--grey-2);
color: var(--ink);
}
.nav-item:hover {
background-color: var(--grey-1);
}
.nav-item:hover .actions {
display: flex;
}
.content {
padding: 0 var(--spacing-m);
height: 32px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
}
.icon {
font-size: 16px;
flex: 0 0 20px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.icon.arrow {
margin: 0 -2px 0 -6px;
font-size: 12px;
}
.icon.arrow.opened {
transform: rotate(90deg);
}
.icon + .icon {
margin-left: -4px;
}
.text {
flex: 1 1 auto;
font-weight: 500;
font-size: var(--font-size-xs);
}
.actions {
display: none;
width: 20px;
height: 20px;
cursor: pointer;
position: relative;
flex-direction: row;
justify-content: center;
align-items: center;
}
</style>

View File

@ -1,36 +0,0 @@
<script>
export let value
export let label
const inputChanged = ev => {
try {
value = Number(ev.target.value)
} catch (_) {
value = null
}
}
let numberText = value === null || value === undefined ? "" : value.toString()
</script>
<div class="numberbox">
<label>{label}</label>
<input
class="budibase__input"
type="number"
{value}
on:change={inputChanged} />
</div>
<style>
.numberbox {
display: grid;
align-items: center;
}
label {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
</style>

View File

@ -1,76 +0,0 @@
<script>
export let tabs = []
export const selectTab = tabName => {
selected = tabName
selectedIndex = tabs.indexOf(selected)
}
let selected = tabs.length > 0 && tabs[0]
let selectedIndex = 0
const isSelected = tab => selected === tab
</script>
<div class="root">
<div class="switcher">
{#each tabs as tab}
<button class:selected={selected === tab} on:click={() => selectTab(tab)}>
{tab}
</button>
{/each}
</div>
<div class="panel">
{#if selectedIndex === 0}
<slot name="0" />
{:else if selectedIndex === 1}
<slot name="1" />
{:else if selectedIndex === 2}
<slot name="2" />
{:else if selectedIndex === 3}
<slot name="3" />
{/if}
</div>
</div>
<style>
.root {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px 20px;
border-left: solid 1px var(--grey-2);
box-sizing: border-box;
}
.switcher {
display: flex;
margin: 0px 20px 20px 0px;
box-sizing: border-box;
}
.switcher > button {
display: inline-block;
border: none;
margin: 0;
padding: 0;
cursor: pointer;
font-size: 18px;
font-weight: 600;
color: var(--grey-5);
margin-right: 20px;
background: none;
outline: none;
font-family: Inter;
}
.switcher > .selected {
color: var(--ink);
}
.panel {
min-height: 0;
height: 100%;
overflow-y: auto;
}
</style>

View File

@ -1,3 +0,0 @@
import { flow } from "lodash/fp"
export const pipe = (arg, funcs) => flow(funcs)(arg)

View File

@ -1,13 +0,0 @@
import { eventHandlers } from "../../../../client/src/state/eventHandlers"
export { EVENT_TYPE_MEMBER_NAME } from "../../../../client/src/state/eventHandlers"
export const allHandlers = () => {
const handlersObj = eventHandlers()
const handlers = Object.keys(handlersObj).map(name => ({
name,
...handlersObj[name],
}))
return handlers
}

View File

@ -1,32 +1,36 @@
<script> <script>
import SettingsModal from "./SettingsModal.svelte" import SettingsModal from "./SettingsModal.svelte"
import { SettingsIcon } from "components/common/Icons/"
import { Modal } from "@budibase/bbui" import { Modal } from "@budibase/bbui"
let modal let modal
</script> </script>
<span class="topnavitemright settings" on:click={modal.show}> <div class="topnavitemright settings" on:click={modal.show}>
<SettingsIcon /> <i class="ri-settings-3-line" />
</span> </div>
<Modal bind:this={modal} width="600px"> <Modal bind:this={modal} width="600px">
<SettingsModal /> <SettingsModal />
</Modal> </Modal>
<style> <style>
span:first-letter { i {
text-transform: capitalize; font-size: 18px;
color: var(--grey-7);
} }
.topnavitemright { .topnavitemright {
cursor: pointer; cursor: pointer;
color: var(--grey-7); color: var(--grey-7);
margin: 0 20px 0 0; margin: 0 12px 0 0;
font-weight: 500; font-weight: 500;
font-size: 1rem; font-size: 1rem;
height: 100%;
display: flex; display: flex;
flex: 1; flex-direction: row;
justify-content: center;
align-items: center; align-items: center;
box-sizing: border-box; height: 24px;
width: 24px;
}
.topnavitemright:hover i {
color: var(--ink);
} }
</style> </style>

View File

@ -1,13 +0,0 @@
<h3>
<slot />
</h3>
<style>
h3 {
font-size: 18px;
font-weight: bold;
color: var(--ink);
margin-top: 40px;
margin-bottom: 20px;
}
</style>

View File

@ -7,9 +7,8 @@
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import { API, Info, User } from "./Steps" import { API, Info, User } from "./Steps"
import Indicator from "./Indicator.svelte" import Indicator from "./Indicator.svelte"
import { Input, TextArea, Button } from "@budibase/bbui" import { Button } from "@budibase/bbui"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { post } from "builderStore/api" import { post } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"

View File

@ -2,7 +2,7 @@
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import { map, join } from "lodash/fp" import { map, join } from "lodash/fp"
import iframeTemplate from "./iframeTemplate" import iframeTemplate from "./iframeTemplate"
import { pipe } from "components/common/core" import { pipe } from "../../../helpers"
let iframe let iframe
let styles = "" let styles = ""

View File

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

View File

@ -18,9 +18,13 @@
<style> <style>
.tabs { .tabs {
display: flex; display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
list-style: none; list-style: none;
font-size: 18px; font-size: var(--font-size-m);
font-weight: 600; font-weight: 500;
height: 24px;
} }
li { li {

View File

@ -3,10 +3,10 @@
import { store } from "builderStore" import { store } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils" import { getComponentDefinition } from "builderStore/storeUtils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last, cloneDeep } from "lodash/fp" import { last } from "lodash/fp"
import { getParent, saveCurrentPreviewItem } from "builderStore/storeUtils" import { getParent, saveCurrentPreviewItem } from "builderStore/storeUtils"
import { uuid } from "builderStore/uuid"
import { DropdownMenu } from "@budibase/bbui" import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let component export let component
@ -98,110 +98,65 @@
} }
</script> </script>
<div bind:this={anchor} on:click|stopPropagation={() => {}}> <div bind:this={anchor} on:click|stopPropagation>
<div class="icon" on:click={dropdown.show}><i class="ri-more-line" /></div> <div class="icon" on:click={dropdown.show}><i class="ri-more-line" /></div>
<DropdownMenu bind:this={dropdown} width="170px" {anchor} align="left">
<DropdownContainer on:click={hideDropdown}>
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={() => confirmDeleteDialog.show()} />
<DropdownItem
icon="ri-arrow-up-line"
title="Move up"
on:click={moveUpComponent} />
<DropdownItem
icon="ri-arrow-down-line"
title="Move down"
on:click={moveDownComponent} />
<DropdownItem
icon="ri-repeat-one-line"
title="Duplicate"
on:click={copyComponent} />
<DropdownItem
icon="ri-scissors-cut-line"
title="Cut"
on:click={() => storeComponentForCopy(true)} />
<DropdownItem
icon="ri-file-copy-line"
title="Copy"
on:click={() => storeComponentForCopy(false)} />
<hr class="hr-style" />
<DropdownItem
icon="ri-insert-row-top"
title="Paste above"
disabled={noPaste}
on:click={() => pasteComponent('above')} />
<DropdownItem
icon="ri-insert-row-bottom"
title="Paste below"
disabled={noPaste}
on:click={() => pasteComponent('below')} />
<DropdownItem
icon="ri-insert-column-right"
title="Paste inside"
disabled={noPaste || noChildrenAllowed}
on:click={() => pasteComponent('inside')} />
</DropdownContainer>
</DropdownMenu>
</div> </div>
<DropdownMenu
bind:this={dropdown}
on:click={hideDropdown}
width="170px"
{anchor}
align="left">
<ul>
<li class="item" on:click={() => confirmDeleteDialog.show()}>
<i class="ri-delete-bin-2-line" />
Delete
</li>
<li class="item" on:click={moveUpComponent}>
<i class="ri-arrow-up-line" />
Move up
</li>
<li class="item" on:click={moveDownComponent}>
<i class="ri-arrow-down-line" />
Move down
</li>
<li class="item" on:click={copyComponent}>
<i class="ri-repeat-one-line" />
Duplicate
</li>
<li class="item" on:click={() => storeComponentForCopy(true)}>
<i class="ri-scissors-cut-line" />
Cut
</li>
<li class="item" on:click={() => storeComponentForCopy(false)}>
<i class="ri-file-copy-line" />
Copy
</li>
<hr class="hr-style" />
<li
class="item"
class:disabled={noPaste}
on:click={() => pasteComponent('above')}>
<i class="ri-insert-row-top" />
Paste above
</li>
<li
class="item"
class:disabled={noPaste}
on:click={() => pasteComponent('below')}>
<i class="ri-insert-row-bottom" />
Paste below
</li>
<li
class="item"
class:disabled={noPaste || noChildrenAllowed}
on:click={() => pasteComponent('inside')}>
<i class="ri-insert-column-right" />
Paste inside
</li>
</ul>
</DropdownMenu>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
title="Confirm Delete" title="Confirm Deletion"
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`} body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`}
okText="Delete Component" okText="Delete Component"
onOk={deleteComponent} /> onOk={deleteComponent} />
<style> <style>
ul { hr {
list-style: none;
padding: 0;
margin: 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 0;
align-items: center;
cursor: pointer;
}
li:not(.disabled):hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
li i {
margin-right: 8px;
font-size: var(--font-size-s);
}
li.disabled {
color: var(--grey-4);
cursor: default;
}
.icon i {
font-size: 16px;
}
.hr-style {
margin: 8px 0; margin: 8px 0;
color: var(--grey-4); background-color: var(--grey-4);
height: 1px;
border: none;
} }
</style> </style>

View File

@ -63,41 +63,31 @@
} }
</script> </script>
<div class="root"> <CategoryTab
<CategoryTab onClick={category => (selectedCategory = category)}
onClick={category => (selectedCategory = category)} {categories}
{categories} {selectedCategory} />
{selectedCategory} />
{#if displayName} {#if displayName}
<div class="instance-name">{componentInstance._instanceName}</div> <div class="instance-name">{componentInstance._instanceName}</div>
{/if}
<div class="component-props-container">
{#if selectedCategory.value === 'design'}
<DesignView {panelDefinition} {componentInstance} {onStyleChanged} />
{:else if selectedCategory.value === 'settings'}
<SettingsView
{componentInstance}
{componentDefinition}
{panelDefinition}
displayNameField={displayName}
onChange={store.setComponentProp}
onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{/if} {/if}
<div class="component-props-container">
{#if selectedCategory.value === 'design'}
<DesignView {panelDefinition} {componentInstance} {onStyleChanged} />
{:else if selectedCategory.value === 'settings'}
<SettingsView
{componentInstance}
{componentDefinition}
{panelDefinition}
displayNameField={displayName}
onChange={store.setComponentProp}
onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{/if}
</div>
</div> </div>
<style> <style>
.root {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
box-sizing: border-box;
}
.title > div:nth-child(1) { .title > div:nth-child(1) {
grid-column-start: name; grid-column-start: name;
color: var(--ink); color: var(--ink);
@ -108,7 +98,6 @@
} }
.component-props-container { .component-props-container {
margin-top: 16px;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
@ -118,8 +107,7 @@
} }
.instance-name { .instance-name {
margin-top: 20px; font-size: var(--font-size-xs);
font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--grey-7); color: var(--grey-7);
} }

View File

@ -2,47 +2,97 @@
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { store } from "builderStore" import { store } from "builderStore"
import components from "./temporaryPanelStructure.js" import components from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte" import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
import Tab from "./ItemTab/Tab.svelte"
export let toggleTab
const categories = components.categories const categories = components.categories
let selectedCategory = categories[0] let selectedIndex
let anchors = []
let popover
$: anchor = selectedIndex === -1 ? null : anchors[selectedIndex]
const close = () => {
popover.hide()
}
const onCategoryChosen = (category, idx) => {
if (category.isCategory) {
selectedIndex = idx
popover.show()
} else {
onComponentChosen(category)
}
}
const onComponentChosen = component => { const onComponentChosen = component => {
store.addChildComponent(component._component, component.presetProps) store.addChildComponent(component._component, component.presetProps)
toggleTab("Navigate")
// Get ID path
const path = store.getPathToComponent($store.currentComponentInfo) const path = store.getPathToComponent($store.currentComponentInfo)
// Go to correct URL
$goto(`./:page/:screen/${path}`) $goto(`./:page/:screen/${path}`)
close()
} }
</script> </script>
<div class="root"> <div class="container">
<CategoryTab {#each categories as category, idx}
onClick={category => (selectedCategory = category)} <div
{selectedCategory} bind:this={anchors[idx]}
{categories} /> class="category"
on:click={() => onCategoryChosen(category, idx)}
<div class="panel"> class:active={idx === selectedIndex}>
<Tab {#if category.icon}<i class={category.icon} />{/if}
list={selectedCategory} <span>{category.name}</span>
on:selectItem={e => onComponentChosen(e.detail)} {#if category.isCategory}<i class="ri-arrow-down-s-line arrow" />{/if}
{toggleTab} /> </div>
</div> {/each}
</div> </div>
<DropdownMenu
on:close={() => (selectedIndex = null)}
bind:this={popover}
{anchor}
align="left">
<DropdownContainer>
{#each categories[selectedIndex].children as item}
{#if !item.showOnPages || item.showOnPages.includes($store.currentPageName)}
<DropdownItem
icon={item.icon}
title={item.name}
on:click={() => onComponentChosen(item)} />
{/if}
{/each}
</DropdownContainer>
</DropdownMenu>
<style> <style>
.panel { .container {
margin-top: 20px; display: flex;
display: grid; flex-direction: row;
grid-template-columns: 1fr 1fr; justify-content: flex-start;
grid-gap: 20px; align-items: center;
z-index: 1;
min-height: 24px;
flex-wrap: wrap;
gap: var(--spacing-l);
}
.category {
color: var(--grey-7);
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-xs);
}
.category span {
font-weight: 500;
user-select: none;
}
.category.active,
.category:hover {
color: var(--ink);
}
.category i:not(:last-child) {
font-size: 16px;
} }
</style> </style>

View File

@ -1,13 +1,12 @@
<script> <script>
import { params, goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte" import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import { last, sortBy, map, trimCharsStart, trimChars, join } from "lodash/fp" import { trimCharsStart, trimChars } from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import { pipe } from "../../helpers"
import { pipe } from "components/common/core"
import { store } from "builderStore" import { store } from "builderStore"
import { ArrowDownIcon, ShapeIcon } from "components/common/Icons/"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte" import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import NavItem from "components/common/NavItem.svelte"
export let screens = [] export let screens = []
@ -24,8 +23,6 @@
let confirmDeleteDialog let confirmDeleteDialog
let componentToDelete = "" let componentToDelete = ""
const joinPath = join("/")
const normalizedName = name => const normalizedName = name =>
pipe(name, [ pipe(name, [
trimCharsStart("./"), trimCharsStart("./"),
@ -42,26 +39,15 @@
<div class="root"> <div class="root">
{#each sortedScreens as screen} {#each sortedScreens as screen}
<div <NavItem
class="budibase__nav-item screen-header-row" icon="ri-artboard-2-line"
class:selected={$store.currentComponentInfo._id === screen.props._id} text={screen.props._instanceName}
on:click|stopPropagation={() => changeScreen(screen)}> withArrow={screen.props._children.length}
<span selected={$store.currentComponentInfo._id === screen.props._id}
class="icon" opened={$store.currentPreviewItem.name === screen.props._id}
class:rotate={$store.currentPreviewItem.name !== screen.props._instanceName}> on:click={() => changeScreen(screen)}>
{#if screen.props._children.length} <ScreenDropdownMenu {screen} />
<ArrowDownIcon /> </NavItem>
{/if}
</span>
<i class="ri-artboard-2-fill icon" />
<span class="title">{screen.props._instanceName}</span>
<div class="dropdown-menu">
<ScreenDropdownMenu {screen} />
</div>
</div>
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children} {#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children}
<ComponentsHierarchyChildren <ComponentsHierarchyChildren
@ -71,55 +57,3 @@
{/if} {/if}
{/each} {/each}
</div> </div>
<style>
.root {
font-weight: 400;
color: var(--ink);
}
.screen-header-row {
display: flex;
flex-direction: row;
}
.title {
margin-left: 14px;
font-size: 14px;
font-weight: 400;
flex: 1;
}
.icon {
display: inline-block;
transition: 0.2s;
font-size: 24px;
width: 18px;
color: var(--grey-7);
}
.icon:nth-of-type(2) {
width: 14px;
margin: 0 0 0 5px;
}
.rotate :global(svg) {
transform: rotate(-90deg);
}
.dropdown-menu {
display: none;
color: var(--ink);
padding: 0 5px;
width: 24px;
height: 24px;
border-style: none;
background: rgba(0, 0, 0, 0);
cursor: pointer;
position: relative;
}
.budibase__nav-item:hover .dropdown-menu {
display: block;
}
</style>

View File

@ -2,14 +2,9 @@
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { store } from "builderStore" import { store } from "builderStore"
import { last } from "lodash/fp" import { last } from "lodash/fp"
import { pipe } from "components/common/core" import { pipe } from "../../helpers"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte" import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import { import NavItem from "components/common/NavItem.svelte"
XCircleIcon,
ChevronUpIcon,
ChevronDownIcon,
CopyIcon,
} from "../common/Icons"
import { getComponentDefinition } from "builderStore/storeUtils" import { getComponentDefinition } from "builderStore/storeUtils"
export let components = [] export let components = []
@ -40,7 +35,6 @@
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/"))) const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise]) const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const isScreenslot = name => name === "##builtin/screenslot" const isScreenslot = name => name === "##builtin/screenslot"
@ -134,29 +128,22 @@
on:drop={drop} on:drop={drop}
ondragover="return false" ondragover="return false"
ondragenter="return false" ondragenter="return false"
class="budibase__nav-item item drop-item" class="drop-item"
style="margin-left: {level * 20 + 40}px" /> style="margin-left: {(level + 1) * 18}px" />
{/if} {/if}
<div <NavItem
class="budibase__nav-item item" draggable
class:selected={currentComponent === component}
style="padding-left: {level * 20 + 40}px"
draggable={true}
on:dragend={dragend} on:dragend={dragend}
on:dragstart={dragstart(component)} on:dragstart={dragstart(component)}
on:dragover={dragover(component, index)} on:dragover={dragover(component, index)}
on:drop={drop} on:drop={drop}
ondragover="return false" text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
ondragenter="return false"> withArrow
<div class="nav-item"> indentLevel={level + 1}
<i class="icon ri-arrow-right-circle-line" /> selected={currentComponent === component}>
{isScreenslot(component._component) ? 'Screenslot' : component._instanceName} <ComponentDropdownMenu {component} />
</div> </NavItem>
<div class="actions">
<ComponentDropdownMenu {component} />
</div>
</div>
{#if component._children} {#if component._children}
<svelte:self <svelte:self
@ -172,8 +159,8 @@
on:drop={drop} on:drop={drop}
ondragover="return false" ondragover="return false"
ondragenter="return false" ondragenter="return false"
class="budibase__nav-item item drop-item" class="drop-item"
style="margin-left: {(level + ($dragDropStore.dropPosition === 'inside' ? 2 : 0)) * 20 + 40}px" /> style="margin-left: {(level + ($dragDropStore.dropPosition === 'inside' ? 3 : 1)) * 18}px" />
{/if} {/if}
</li> </li>
{/each} {/each}
@ -186,47 +173,9 @@
margin: 0; margin: 0;
} }
.item {
display: grid;
grid-template-columns: 1fr auto auto auto;
padding: 0 var(--spacing-m);
margin: 0;
border-radius: var(--border-radius-m);
height: 36px;
align-items: center;
}
.drop-item { .drop-item {
border-radius: var(--border-radius-m);
height: 32px;
background: var(--blue-light); background: var(--blue-light);
height: 36px;
}
.actions {
display: none;
color: var(--ink);
border-style: none;
background: rgba(0, 0, 0, 0);
cursor: pointer;
position: relative;
}
.item:hover {
background: var(--grey-1);
cursor: pointer;
}
.item:hover .actions {
display: block;
}
.nav-item {
display: flex;
align-items: center;
font-size: 14px;
color: var(--ink);
}
.icon {
color: var(--grey-7);
margin-right: 8px;
} }
</style> </style>

View File

@ -48,7 +48,7 @@
{/each} {/each}
{:else} {:else}
<div class="no-design"> <div class="no-design">
This component does not have any design properties. This component doesn't have any design properties.
</div> </div>
{/if} {/if}
</div> </div>
@ -61,10 +61,10 @@
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
gap: var(--spacing-l);
} }
.design-view-state-categories { .design-view-state-categories {
flex: 0 0 50px;
} }
.positioned-wrapper { .positioned-wrapper {
@ -79,10 +79,15 @@
min-height: 0; min-height: 0;
margin: 0 -20px; margin: 0 -20px;
padding: 0 20px; padding: 0 20px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-m);
} }
.no-design { .no-design {
font-size: var(--font-size-s); font-size: var(--font-size-xs);
color: var(--grey-6); color: var(--grey-5);
} }
</style> </style>

View File

@ -29,7 +29,13 @@
</script> </script>
<div> <div>
<DataList editable secondary thin on:blur={handleBlur} on:change bind:value> <DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" /> <option value="" />
{#each urls as url} {#each urls as url}
<option value={url.url}>{url.name}</option> <option value={url.url}>{url.name}</option>

View File

@ -1,14 +1,7 @@
<script> <script>
import { store } from "builderStore" import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
import {
TextButton,
Button,
Body,
DropdownMenu,
ModalContent,
} from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/" import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers" import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/state/eventHandlers"
import actionTypes from "./actions" import actionTypes from "./actions"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"

View File

@ -1,11 +1,6 @@
<script> <script>
import { Input, DataList, Select } from "@budibase/bbui" import { Input, DataList, Select } from "@budibase/bbui"
import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe } from "components/common/core"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import { ArrowDownIcon } from "components/common/Icons/"
import { createEventDispatcher } from "svelte"
export let parameter export let parameter

View File

@ -1,5 +1,4 @@
<script> <script>
import { FeedbackIcon } from "components/common/Icons/"
import { Popover } from "@budibase/bbui" import { Popover } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
@ -16,41 +15,37 @@
}, FIVE_MINUTES) }, FIVE_MINUTES)
</script> </script>
<span <div class="container" bind:this={iconContainer} on:click={popover.show}>
class="container" <i class="ri-feedback-line" class:highlight={$store.highlightFeedbackIcon} />
bind:this={iconContainer} </div>
on:click={popover.show}
class:highlight={$store.highlightFeedbackIcon}>
<FeedbackIcon />
</span>
<Popover bind:this={popover} anchor={iconContainer} align="right"> <Popover bind:this={popover} anchor={iconContainer} align="right">
<FeedbackIframe on:finished={popover.hide} /> <FeedbackIframe on:finished={popover.hide} />
</Popover> </Popover>
<style> <style>
i {
font-size: 18px;
color: var(--grey-7);
}
i.highlight {
color: var(--blue);
filter: drop-shadow(0 0 20px var(--blue));
}
.container { .container {
cursor: pointer; cursor: pointer;
color: var(--grey-7); color: var(--grey-7);
margin: 0 20px 0 0; margin: 0 12px 0 0;
font-weight: 500; font-weight: 500;
font-size: 1rem; font-size: 1rem;
height: 100%;
display: flex; display: flex;
flex: 1; flex-direction: row;
justify-content: center;
align-items: center; align-items: center;
box-sizing: border-box; height: 24px;
width: 24px;
} }
.container:hover i {
.container:hover {
color: var(--ink); color: var(--ink);
font-weight: 500;
}
.highlight {
color: var(--blue);
}
.highlight > :global(svg) {
filter: drop-shadow(0 0 20px var(--blue));
} }
</style> </style>

View File

@ -22,23 +22,22 @@
<style> <style>
.flatbutton { .flatbutton {
cursor: pointer; cursor: pointer;
max-height: 36px; height: 32px;
padding: 8px 2px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
background: #ffffff; background: white;
color: var(--grey-7); color: var(--grey-7);
border-radius: 5px; border-radius: var(--border-radius-m);
font-size: 14px; font-size: var(--font-size-xs);
font-weight: 400; font-weight: 500;
transition: all 0.3s; transition: all 0.3s;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
.selected { .selected {
background: var(--grey-3); background: var(--grey-2);
color: var(--ink); color: var(--ink);
} }

View File

@ -4,22 +4,41 @@
import PageLayout from "components/userInterface/PageLayout.svelte" import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte" import PagesList from "components/userInterface/PagesList.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte" import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import { Button, Spacer, Modal } from "@budibase/bbui" import { Modal } from "@budibase/bbui"
let modal let modal
</script> </script>
<div class="title">
<h1>Screens</h1>
<i on:click={modal.show} data-cy="new-screen" class="ri-add-circle-fill" />
</div>
<PagesList /> <PagesList />
<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"> <div class="nav-items-container">
<PageLayout layout={$store.pages[$store.currentPageName]} />
<ComponentsHierarchy screens={$store.screens} /> <ComponentsHierarchy screens={$store.screens} />
</div> </div>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<NewScreenModal /> <NewScreenModal />
</Modal> </Modal>
<style>
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.title h1 {
font-size: var(--font-size-m);
font-weight: 500;
margin: 0;
}
.title i {
font-size: 20px;
}
.title i:hover {
cursor: pointer;
color: var(--blue);
}
</style>

View File

@ -1,291 +1,291 @@
<script> <script>
import { DropdownMenu, Button, Input } from "@budibase/bbui" import { DropdownMenu, Button, Input } from "@budibase/bbui"
import { createEventDispatcher, tick } from "svelte" import { createEventDispatcher, tick } from "svelte"
import icons from "./icons.js" import icons from "./icons.js"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value = "" export let value = ""
export let maxIconsPerPage = 30 export let maxIconsPerPage = 30
let searchTerm = "" let searchTerm = ""
let selectedLetter = "A" let selectedLetter = "A"
let currentPage = 1 let currentPage = 1
let filteredIcons = findIconByTerm(selectedLetter) let filteredIcons = findIconByTerm(selectedLetter)
$: dispatch("change", value) $: dispatch("change", value)
const alphabet = [ const alphabet = [
"A", "A",
"B", "B",
"C", "C",
"D", "D",
"E", "E",
"F", "F",
"G", "G",
"H", "H",
"I", "I",
"J", "J",
"K", "K",
"L", "L",
"M", "M",
"N", "N",
"O", "O",
"P", "P",
"Q", "Q",
"R", "R",
"S", "S",
"T", "T",
"U", "U",
"V", "V",
"W", "W",
"X", "X",
"Y", "Y",
"Z", "Z",
] ]
let buttonAnchor, dropdown let buttonAnchor, dropdown
let loading = false let loading = false
function findIconByTerm(term) { function findIconByTerm(term) {
const r = new RegExp(`\^${term}`, "i") const r = new RegExp(`\^${term}`, "i")
return icons.filter(i => r.test(i.label)) return icons.filter(i => r.test(i.label))
} }
async function switchLetter(letter) { async function switchLetter(letter) {
currentPage = 1 currentPage = 1
searchTerm = "" searchTerm = ""
loading = true loading = true
selectedLetter = letter selectedLetter = letter
filteredIcons = findIconByTerm(letter) filteredIcons = findIconByTerm(letter)
await tick() //svg icons do not update without tick await tick() //svg icons do not update without tick
loading = false loading = false
} }
async function findIconOnPage() { async function findIconOnPage() {
loading = true loading = true
const iconIdx = filteredIcons.findIndex(i => i.value === value) const iconIdx = filteredIcons.findIndex(i => i.value === value)
if (iconIdx !== -1) { if (iconIdx !== -1) {
currentPage = Math.ceil(iconIdx / maxIconsPerPage) currentPage = Math.ceil(iconIdx / maxIconsPerPage)
} }
await tick() //svg icons do not update without tick await tick() //svg icons do not update without tick
loading = false loading = false
} }
async function setSelectedUI() { async function setSelectedUI() {
if (value) { if (value) {
const letter = displayValue.substring(0, 1) const letter = displayValue.substring(0, 1)
await switchLetter(letter) await switchLetter(letter)
await findIconOnPage() await findIconOnPage()
} }
} }
async function pageClick(next) { async function pageClick(next) {
loading = true loading = true
if (next && currentPage < totalPages) { if (next && currentPage < totalPages) {
currentPage++ currentPage++
} else if (!next && currentPage > 1) { } else if (!next && currentPage > 1) {
currentPage-- currentPage--
} }
await tick() //svg icons do not update without tick await tick() //svg icons do not update without tick
loading = false loading = false
} }
async function searchForIcon(e) { async function searchForIcon(e) {
currentPage = 1 currentPage = 1
loading = true loading = true
filteredIcons = findIconByTerm(searchTerm) filteredIcons = findIconByTerm(searchTerm)
await tick() //svg icons do not update without tick await tick() //svg icons do not update without tick
loading = false loading = false
} }
$: displayValue = value ? value.substring(7) : "Pick Icon" $: displayValue = value ? value.substring(7) : "Pick Icon"
$: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage) $: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage)
$: pageEndIdx = maxIconsPerPage * currentPage $: pageEndIdx = maxIconsPerPage * currentPage
$: pagedIcons = filteredIcons.slice(pageEndIdx - maxIconsPerPage, pageEndIdx) $: pagedIcons = filteredIcons.slice(pageEndIdx - maxIconsPerPage, pageEndIdx)
$: pagerText = `Page ${currentPage} of ${totalPages}` $: pagerText = `Page ${currentPage} of ${totalPages}`
</script> </script>
<div bind:this={buttonAnchor}> <div bind:this={buttonAnchor}>
<Button secondary on:click={dropdown.show}>{displayValue}</Button> <Button secondary small on:click={dropdown.show}>{displayValue}</Button>
</div> </div>
<DropdownMenu <DropdownMenu
bind:this={dropdown} bind:this={dropdown}
on:open={setSelectedUI} on:open={setSelectedUI}
anchor={buttonAnchor}> anchor={buttonAnchor}>
<div class="container"> <div class="container">
<div class="search-area"> <div class="search-area">
<div class="alphabet-area"> <div class="alphabet-area">
{#each alphabet as letter, idx} {#each alphabet as letter, idx}
<span <span
class="letter" class="letter"
class:letter-selected={letter === selectedLetter} class:letter-selected={letter === selectedLetter}
on:click={() => switchLetter(letter)}> on:click={() => switchLetter(letter)}>
{letter} {letter}
</span> </span>
{#if idx !== alphabet.length - 1}<span>-</span>{/if} {#if idx !== alphabet.length - 1}<span>-</span>{/if}
{/each} {/each}
</div> </div>
<div class="search-input"> <div class="search-input">
<div class="input-wrapper"> <div class="input-wrapper">
<Input bind:value={searchTerm} thin placeholder="Search Icon" /> <Input bind:value={searchTerm} thin placeholder="Search Icon" />
</div> </div>
<Button secondary on:click={searchForIcon}>Search</Button> <Button secondary on:click={searchForIcon}>Search</Button>
</div> </div>
<div class="page-area"> <div class="page-area">
<div class="pager"> <div class="pager">
<span on:click={() => pageClick(false)}> <span on:click={() => pageClick(false)}>
<i class="page-btn fas fa-chevron-left" /> <i class="page-btn fas fa-chevron-left" />
</span> </span>
<span>{pagerText}</span> <span>{pagerText}</span>
<span on:click={() => pageClick(true)}> <span on:click={() => pageClick(true)}>
<i class="page-btn fas fa-chevron-right" /> <i class="page-btn fas fa-chevron-right" />
</span> </span>
</div> </div>
</div> </div>
</div> </div>
{#if pagedIcons.length > 0} {#if pagedIcons.length > 0}
<div class="icon-area"> <div class="icon-area">
{#if !loading} {#if !loading}
{#each pagedIcons as icon} {#each pagedIcons as icon}
<div <div
class="icon-container" class="icon-container"
class:selected={value === icon.value} class:selected={value === icon.value}
on:click={() => (value = icon.value)}> on:click={() => (value = icon.value)}>
<div class="icon-preview"> <div class="icon-preview">
<i class={`${icon.value} fa-3x`} /> <i class={`${icon.value} fa-3x`} />
</div> </div>
<div class="icon-label">{icon.label}</div> <div class="icon-label">{icon.label}</div>
</div> </div>
{/each} {/each}
{/if} {/if}
</div> </div>
{:else} {:else}
<div class="no-icons"> <div class="no-icons">
<h5> <h5>
{`There is no icons for this ${searchTerm ? 'search' : 'page'}`} {`There is no icons for this ${searchTerm ? 'search' : 'page'}`}
</h5> </h5>
</div> </div>
{/if} {/if}
</div> </div>
</DropdownMenu> </DropdownMenu>
<style> <style>
.container { .container {
width: 610px; width: 610px;
height: 350px; height: 350px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px 0px 10px 15px; padding: 10px 0px 10px 15px;
overflow-x: hidden; overflow-x: hidden;
} }
.search-area { .search-area {
flex: 0 0 80px; flex: 0 0 80px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.icon-area { .icon-area {
flex: 1; flex: 1;
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
grid-gap: 5px; grid-gap: 5px;
justify-content: flex-start; justify-content: flex-start;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding-right: 10px; padding-right: 10px;
} }
.no-icons { .no-icons {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.alphabet-area { .alphabet-area {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
padding-bottom: 10px; padding-bottom: 10px;
padding-right: 15px; padding-right: 15px;
justify-content: space-around; justify-content: space-around;
} }
.loading-container { .loading-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.search-input { .search-input {
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
width: 100%; width: 100%;
padding-right: 15px; padding-right: 15px;
} }
.input-wrapper { .input-wrapper {
width: 510px; width: 510px;
margin-right: 5px; margin-right: 5px;
} }
.page-area { .page-area {
padding: 10px; padding: 10px;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.letter { .letter {
color: var(--blue); color: var(--blue);
} }
.letter:hover { .letter:hover {
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
} }
.letter-selected { .letter-selected {
text-decoration: underline; text-decoration: underline;
} }
.icon-container { .icon-container {
height: 100px; height: 100px;
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
border: var(--border-dark); border: var(--border-dark);
} }
.icon-container:hover { .icon-container:hover {
cursor: pointer; cursor: pointer;
background: var(--grey-2); background: var(--grey-2);
} }
.selected { .selected {
background: var(--grey-3); background: var(--grey-3);
} }
.icon-preview { .icon-preview {
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.icon-label { .icon-label {
flex: 0 0 20px; flex: 0 0 20px;
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
} }
.page-btn { .page-btn {
color: var(--blue); color: var(--blue);
} }
.page-btn:hover { .page-btn:hover {
cursor: pointer; cursor: pointer;
} }
</style> </style>

View File

@ -1,50 +0,0 @@
<script>
export let item
</script>
<div data-cy={item.name} class="item-item" on:click>
<div class="item-icon"><i class={item.icon} /></div>
<div class="item-text">
<div class="item-name">{item.name}</div>
</div>
</div>
<style>
.item-item {
display: flex;
flex-direction: column;
cursor: pointer;
padding: 12px 16px 16px 16px;
height: 80px;
justify-content: center;
align-items: center;
background-color: var(--grey-1);
border-radius: 5px;
}
.item-item:hover {
background: var(--grey-2);
transition: all 0.3s;
}
.item-icon {
border-radius: 3px;
display: flex;
}
.item-text {
display: flex;
flex-direction: column;
}
.item-name {
font-size: 14px;
font-weight: 400;
text-align: center;
}
i {
font-size: 24px;
color: var(--grey-7);
}
</style>

View File

@ -1,52 +0,0 @@
<script>
import { createEventDispatcher } from "svelte"
import { store } from "builderStore"
import Item from "./Item.svelte"
const dispatch = createEventDispatcher()
export let list
let category = list
const handleClick = item => {
if (item.children && item.children.length > 0) {
list = item
} else {
dispatch("selectItem", item)
}
}
const goBack = () => {
list = category
}
</script>
{#if !list.isCategory}
<button class="back-button" on:click={goBack}>Back</button>
{/if}
{#each list.children as item}
{#if !item.showOnPages || item.showOnPages.includes($store.currentPageName)}
<Item {item} on:click={() => handleClick(item)} />
{/if}
{/each}
<style>
.back-button {
grid-column: 1 / span 2;
font-size: 14px;
text-align: center;
height: 36px;
border-radius: 5px;
border: solid 1px var(--grey-3);
background: white;
cursor: pointer;
font-weight: 500;
font-family: Inter;
transition: all 0.3ms;
}
.back-button:hover {
background: var(--grey-1);
}
</style>

View File

@ -13,6 +13,7 @@
let select let select
let selectMenu let selectMenu
let icon let icon
let width = 0
let selectAnchor = null let selectAnchor = null
let dimensions = { top: 0, bottom: 0, left: 0 } let dimensions = { top: 0, bottom: 0, left: 0 }
@ -91,6 +92,7 @@
"transform-origin": `center ${positionSide}`, "transform-origin": `center ${positionSide}`,
[positionSide]: `${dimensions[positionSide]}px`, [positionSide]: `${dimensions[positionSide]}px`,
left: `${dimensions.left.toFixed(0)}px`, left: `${dimensions.left.toFixed(0)}px`,
width: `${width}px`,
}) })
$: isOptionsObject = options.every(o => typeof o === "object") $: isOptionsObject = options.every(o => typeof o === "object")
@ -108,6 +110,7 @@
</script> </script>
<div <div
bind:clientWidth={width}
tabindex="0" tabindex="0"
bind:this={select} bind:this={select}
class="bb-select-container" class="bb-select-container"
@ -164,19 +167,17 @@
.bb-select-container { .bb-select-container {
outline: none; outline: none;
height: 36px;
cursor: pointer; cursor: pointer;
font-size: 14px;
overflow: hidden; overflow: hidden;
flex: 1 1 auto;
} }
.bb-select-anchor { .bb-select-anchor {
cursor: pointer; cursor: pointer;
display: flex; display: flex;
padding: 0px 12px; padding: var(--spacing-s) var(--spacing-m);
height: 36px;
background-color: var(--grey-2); background-color: var(--grey-2);
border-radius: 5px; border-radius: var(--border-radius-m);
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;
} }
@ -184,8 +185,11 @@
.bb-select-anchor > span { .bb-select-anchor > span {
color: var(--ink); color: var(--ink);
font-weight: 400; font-weight: 400;
width: 140px;
overflow-x: hidden; overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--font-size-xs);
flex: 1 1 auto;
} }
.bb-select-anchor > i { .bb-select-anchor > i {
@ -208,7 +212,6 @@
box-sizing: border-box; box-sizing: border-box;
flex-direction: column; flex-direction: column;
opacity: 0; opacity: 0;
width: 160px;
z-index: 2; z-index: 2;
color: var(--ink); color: var(--ink);
font-weight: 400; font-weight: 400;
@ -237,7 +240,7 @@
padding: 5px 0px; padding: 5px 0px;
cursor: pointer; cursor: pointer;
padding-left: 10px; padding-left: 10px;
font-size: var(--font-size-s); font-size: var(--font-size-xs);
} }
li:hover { li:hover {

View File

@ -1,21 +1,9 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
// import { tick } from "svelte"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte" import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import NavItem from "components/common/NavItem.svelte"
import { import { last } from "lodash/fp"
last,
sortBy,
map,
trimCharsStart,
trimChars,
join,
compose,
} from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { pipe } from "components/common/core"
import { store } from "builderStore" import { store } from "builderStore"
import { ArrowDownIcon, GridIcon } from "components/common/Icons/"
import { writable } from "svelte/store" import { writable } from "svelte/store"
export let layout export let layout
@ -24,13 +12,10 @@
let componentToDelete = "" let componentToDelete = ""
const dragDropStore = writable({}) const dragDropStore = writable({})
const joinPath = join("/")
const lastPartOfName = c => const lastPartOfName = c =>
c && last(c.name ? c.name.split("/") : c._component.split("/")) c && last(c.name ? c.name.split("/") : c._component.split("/"))
const isComponentSelected = (current, comp) => current === comp
$: _layout = { $: _layout = {
component: layout, component: layout,
title: lastPartOfName(layout), title: lastPartOfName(layout),
@ -42,18 +27,14 @@
} }
</script> </script>
<div <NavItem
class="budibase__nav-item root" border={false}
class:selected={$store.currentComponentInfo._id === _layout.component.props._id} icon="ri-layout-3-line"
on:click|stopPropagation={setCurrentScreenToLayout}> text="Master Screen"
<span withArrow
class="icon" selected={$store.currentComponentInfo._id === _layout.component.props._id}
class:rotate={$store.currentPreviewItem.name !== _layout.title}> opened={$store.currentPreviewItem.name === _layout.title}
<ArrowDownIcon /> on:click={setCurrentScreenToLayout} />
</span>
<i class="ri-layout-3-fill icon-big" />
<span class="title">Master Screen</span>
</div>
{#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children} {#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
<ComponentsHierarchyChildren <ComponentsHierarchyChildren
@ -62,29 +43,3 @@
currentComponent={$store.currentComponentInfo} currentComponent={$store.currentComponentInfo}
{dragDropStore} /> {dragDropStore} />
{/if} {/if}
<style>
.title {
margin-left: 10px;
font-size: 14px;
font-weight: 400;
color: var(--ink);
}
.icon {
width: 24px;
display: inline-block;
transition: 0.2s;
width: 20px;
color: var(--grey-7);
}
.icon-big {
font-size: 20px;
color: var(--grey-7);
}
.rotate :global(svg) {
transform: rotate(-90deg);
}
</style>

View File

@ -43,24 +43,23 @@
button { button {
cursor: pointer; cursor: pointer;
padding: 0px 16px; padding: 0 var(--spacing-m);
height: 36px; height: 32px;
text-align: center; text-align: center;
background: #ffffff; background: #ffffff;
color: var(--grey-7); color: var(--grey-7);
border-radius: 5px; border-radius: 5px;
font-family: inter; font-size: var(--font-size-xs);
font-size: 14px; font-weight: 500;
font-weight: 400;
transition: all 0.3s; transition: all 0.3s;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
border: none !important; border: none !important;
transition: 0.2s;
outline: none; outline: none;
font-family: var(--font-sans);
} }
.active { .active {
background: var(--grey-3); background: var(--grey-2);
color: var(--ink); color: var(--ink);
} }
</style> </style>

View File

@ -119,16 +119,14 @@
position: relative; position: relative;
display: flex; display: flex;
flex-flow: row; flex-flow: row;
margin: 8px 0;
align-items: center; align-items: center;
} }
.label { .label {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
flex: 0 0 100px; flex: 0 0 80px;
text-align: left; text-align: left;
color: var(--ink); color: var(--ink);
margin-right: auto; margin-right: auto;
@ -149,7 +147,7 @@
height: 90%; height: 90%;
width: 2rem; width: 2rem;
background: var(--grey-2); background: var(--grey-2);
right: 10px; right: 4px;
--spacing-s: 0; --spacing-s: 0;
border-left: 0.5px solid var(--grey-3); border-left: 0.5px solid var(--grey-3);
outline-color: var(--blue); outline-color: var(--blue);

View File

@ -13,14 +13,26 @@
$: style = componentInstance["_styles"][styleCategory] || {} $: style = componentInstance["_styles"][styleCategory] || {}
</script> </script>
<DetailSummary {name} on:open show={open}> <DetailSummary {name} on:open show={open} thin>
{#each properties as props} <div>
<PropertyControl {#each properties as props}
label={props.label} <PropertyControl
control={props.control} label={props.label}
key={props.key} control={props.control}
value={style[props.key]} key={props.key}
onChange={(key, value) => onStyleChanged(styleCategory, key, value)} value={style[props.key]}
props={{ ...excludeProps(props, ['control', 'label']) }} /> onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
{/each} props={{ ...excludeProps(props, ['control', 'label']) }} />
{/each}
</div>
</DetailSummary> </DetailSummary>
<style>
div {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-s);
}
</style>

View File

@ -8,4 +8,4 @@
export let name, value, placeholder, type export let name, value, placeholder, type
</script> </script>
<Input {name} {value} {placeholder} {type} thin on:change /> <Input {name} {value} {placeholder} {type} extraThin on:change />

View File

@ -2,9 +2,8 @@
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { store } from "builderStore" import { store } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import api from "builderStore/api"
import Portal from "svelte-portal"
import { DropdownMenu } from "@budibase/bbui" import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let screen export let screen
@ -12,10 +11,6 @@
let dropdown let dropdown
let anchor let anchor
const hideDropdown = () => {
dropdown.hide()
}
const deleteScreen = () => { const deleteScreen = () => {
store.deleteScreens(screen, $store.currentPageName) store.deleteScreens(screen, $store.currentPageName)
// update the page if required // update the page if required
@ -29,79 +24,27 @@
} }
</script> </script>
<div <div bind:this={anchor} on:click|stopPropagation>
bind:this={anchor}
class="root boundary"
on:click|stopPropagation={() => {}}>
<div class="icon" on:click={() => dropdown.show()}> <div class="icon" on:click={() => dropdown.show()}>
<i class="ri-more-line" /> <i class="ri-more-line" />
</div> </div>
<DropdownMenu bind:this={dropdown} {anchor} align="left"> <DropdownMenu bind:this={dropdown} {anchor} align="left">
<ul on:click={hideDropdown}> <DropdownContainer>
<li on:click={() => confirmDeleteDialog.show()}> <DropdownItem
<i class="ri-delete-bin-2-line" /> icon="ri-delete-bin-line"
Delete title="Delete"
</li> on:click={() => confirmDeleteDialog.show()} />
</ul> </DropdownContainer>
</DropdownMenu> </DropdownMenu>
</div> </div>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
title="Confirm Delete" title="Confirm Deletion"
body={`Are you sure you wish to delete the screen '${screen.props._instanceName}' ?`} body={`Are you sure you wish to delete the screen '${screen.props._instanceName}' ?`}
okText="Delete Screen" okText="Delete Screen"
onOk={deleteScreen} /> onOk={deleteScreen} />
<style> <style>
.root {
overflow: hidden;
z-index: 9;
}
.root button {
border-style: none;
border-radius: 2px;
padding: 0;
background: transparent;
cursor: pointer;
color: var(--ink);
outline: none;
}
ul {
z-index: 100000;
overflow: visible;
margin: var(--spacing-s) 0;
border-radius: var(--border-radius-s);
padding: 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 0;
align-items: center;
cursor: pointer;
}
li:not(.disabled):hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
li i {
margin-right: 8px;
font-size: var(--font-size-s);
}
li.disabled {
color: var(--grey-4);
cursor: default;
}
.icon i { .icon i {
font-size: 16px; font-size: 16px;
} }

View File

@ -1,88 +1,94 @@
<script> <script>
import { DataList } from "@budibase/bbui" import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value = "" export let value = ""
$: urls = getUrls() $: urls = getUrls()
const handleBlur = () => dispatch("change", value) const handleBlur = () => dispatch("change", value)
// this will get urls of all screens, but only // this will get urls of all screens, but only
// choose detail screens that are usable in the current context // choose detail screens that are usable in the current context
// and substitute the :id param for the actual {{ ._id }} binding // and substitute the :id param for the actual {{ ._id }} binding
const getUrls = () => { const getUrls = () => {
const urls = [ const urls = [
...$store.screens ...$store.screens
.filter(screen => !screen.props._component.endsWith("/rowdetail")) .filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({ .map(screen => ({
name: screen.props._instanceName, name: screen.props._instanceName,
url: screen.route, url: screen.route,
sort: screen.props._component, sort: screen.props._component,
})), })),
] ]
const bindableProperties = fetchBindableProperties({ const bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.currentComponentInfo._id,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $store.currentPreviewItem,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })
const detailScreens = $store.screens.filter(screen => const detailScreens = $store.screens.filter(screen =>
screen.props._component.endsWith("/rowdetail") screen.props._component.endsWith("/rowdetail")
) )
for (let detailScreen of detailScreens) { for (let detailScreen of detailScreens) {
const idBinding = bindableProperties.find(p => { const idBinding = bindableProperties.find(p => {
if ( if (
p.type === "context" && p.type === "context" &&
p.runtimeBinding.endsWith("._id") && p.runtimeBinding.endsWith("._id") &&
p.table p.table
) { ) {
const tableId = const tableId =
typeof p.table === "string" ? p.table : p.table.tableId typeof p.table === "string" ? p.table : p.table.tableId
return tableId === detailScreen.props.table return tableId === detailScreen.props.table
} }
return false return false
}) })
if (idBinding) { if (idBinding) {
urls.push({ urls.push({
name: detailScreen.props._instanceName, name: detailScreen.props._instanceName,
url: detailScreen.route.replace( url: detailScreen.route.replace(
":id", ":id",
`{{ ${idBinding.runtimeBinding} }}` `{{ ${idBinding.runtimeBinding} }}`
), ),
sort: detailScreen.props._component, sort: detailScreen.props._component,
}) })
} }
} }
return urls return urls
} }
</script> </script>
<div> <div>
<DataList editable secondary thin on:blur={handleBlur} on:change bind:value> <DataList
<option value="" /> editable
{#each urls as url} secondary
<option value={url.url}>{url.name}</option> extraThin
{/each} on:blur={handleBlur}
</DataList> on:change
</div> bind:value>
<option value="" />
<style> {#each urls as url}
div { <option value={url.url}>{url.name}</option>
flex: 1 1 auto; {/each}
display: flex; </DataList>
flex-direction: row; </div>
}
div :global(> div) { <style>
flex: 1 1 auto; div {
} flex: 1 1 auto;
</style> display: flex;
flex-direction: row;
}
div :global(> div) {
flex: 1 1 auto;
}
</style>

View File

@ -85,53 +85,65 @@
} }
</script> </script>
{#if screenOrPageInstance} <div class="settings-view-container">
{#each screenOrPageDefinition as def} {#if screenOrPageInstance}
<PropertyControl {#each screenOrPageDefinition as def}
bindable={false}
control={def.control}
label={def.label}
key={def.key}
value={screenOrPageInstance[def.key]}
onChange={onScreenPropChange}
props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each}
{/if}
{#if displayNameField}
<PropertyControl
control={Input}
label="Name"
key="_instanceName"
value={componentInstance._instanceName}
onChange={onInstanceNameChange} />
{#if duplicateName}
<span class="duplicate-name">Name must be unique</span>
{/if}
{/if}
{#if panelDefinition && panelDefinition.length > 0}
{#each panelDefinition as definition}
{#if canRenderControl(definition.key, definition.dependsOn)}
<PropertyControl <PropertyControl
control={definition.control} bindable={false}
label={definition.label} control={def.control}
key={definition.key} label={def.label}
value={componentInstance[definition.key] || componentInstance[definition.key]?.defaultValue} key={def.key}
{componentInstance} value={screenOrPageInstance[def.key]}
{onChange} onChange={onScreenPropChange}
props={{ ...excludeProps(definition, ['control', 'label']) }} /> props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each}
{/if}
{#if displayNameField}
<PropertyControl
control={Input}
label="Name"
key="_instanceName"
value={componentInstance._instanceName}
onChange={onInstanceNameChange} />
{#if duplicateName}
<span class="duplicate-name">Name must be unique</span>
{/if} {/if}
{/each} {/if}
{:else}
<div>This component does not have any settings.</div> {#if panelDefinition && panelDefinition.length > 0}
{/if} {#each panelDefinition as definition}
{#if canRenderControl(definition.key, definition.dependsOn)}
<PropertyControl
control={definition.control}
label={definition.label}
key={definition.key}
value={componentInstance[definition.key] || componentInstance[definition.key]?.defaultValue}
{componentInstance}
{onChange}
props={{ ...excludeProps(definition, ['control', 'label']) }} />
{/if}
{/each}
{:else}
<div class="empty">
This component doesn't have any additional settings.
</div>
{/if}
</div>
<style> <style>
div { .settings-view-container {
font-size: var(--font-size-s); display: flex;
flex-direction: column;
width: 100%;
height: 100%;
gap: var(--spacing-s);
}
.empty {
font-size: var(--font-size-xs);
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
color: var(--grey-6); color: var(--grey-5);
} }
.duplicate-name { .duplicate-name {

View File

@ -6,7 +6,7 @@
</script> </script>
<div> <div>
<Select thin secondary wide on:change {value}> <Select extraThin secondary wide on:change {value}>
<option value="">Choose a table</option> <option value="">Choose a table</option>
{#each $backendUiStore.tables as table} {#each $backendUiStore.tables as table}
<option value={table._id}>{table.name}</option> <option value={table._id}>{table.name}</option>

View File

@ -1,163 +1,163 @@
<script> <script>
import { Button, Icon, DropdownMenu, Spacer, Heading } from "@budibase/bbui" import { Button, Icon, DropdownMenu, Spacer, Heading } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "../../builderStore/fetchBindableProperties" import fetchBindableProperties from "../../builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let anchorRight, dropdownRight let anchorRight, dropdownRight
export let value = {} export let value = {}
function handleSelected(selected) { function handleSelected(selected) {
dispatch("change", selected) dispatch("change", selected)
dropdownRight.hide() dropdownRight.hide()
} }
$: tables = $backendUiStore.tables.map(m => ({ $: tables = $backendUiStore.tables.map(m => ({
label: m.name, label: m.name,
name: `all_${m._id}`, name: `all_${m._id}`,
tableId: m._id, tableId: m._id,
type: "table", type: "table",
})) }))
$: views = $backendUiStore.tables.reduce((acc, cur) => { $: views = $backendUiStore.tables.reduce((acc, cur) => {
let viewsArr = Object.entries(cur.views).map(([key, value]) => ({ let viewsArr = Object.entries(cur.views).map(([key, value]) => ({
label: key, label: key,
name: key, name: key,
...value, ...value,
type: "view", type: "view",
})) }))
return [...acc, ...viewsArr] return [...acc, ...viewsArr]
}, []) }, [])
$: bindableProperties = fetchBindableProperties({ $: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.currentComponentInfo._id,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $store.currentPreviewItem,
tables: $backendUiStore.tables, tables: $backendUiStore.tables,
}) })
$: links = bindableProperties $: links = bindableProperties
.filter(x => x.fieldSchema.type === "link") .filter(x => x.fieldSchema?.type === "link")
.map(property => ({ .map(property => ({
label: property.readableBinding, label: property.readableBinding,
fieldName: property.fieldSchema.name, fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`, name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId, tableId: property.fieldSchema.tableId,
type: "link", type: "link",
})) }))
</script> </script>
<div <div
class="dropdownbutton" class="dropdownbutton"
bind:this={anchorRight} bind:this={anchorRight}
on:click={dropdownRight.show}> on:click={dropdownRight.show}>
<span>{value.label ? value.label : 'Table / View'}</span> <span>{value.label ? value.label : 'Table / View'}</span>
<Icon name="arrowdown" /> <Icon name="arrowdown" />
</div> </div>
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}> <DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown"> <div class="dropdown">
<div class="title"> <div class="title">
<Heading extraSmall>Tables</Heading> <Heading extraSmall>Tables</Heading>
</div> </div>
<ul> <ul>
{#each tables as table} {#each tables as table}
<li <li
class:selected={value === table} class:selected={value === table}
on:click={() => handleSelected(table)}> on:click={() => handleSelected(table)}>
{table.label} {table.label}
</li> </li>
{/each} {/each}
</ul> </ul>
<hr /> <hr />
<div class="title"> <div class="title">
<Heading extraSmall>Views</Heading> <Heading extraSmall>Views</Heading>
</div> </div>
<ul> <ul>
{#each views as view} {#each views as view}
<li <li
class:selected={value === view} class:selected={value === view}
on:click={() => handleSelected(view)}> on:click={() => handleSelected(view)}>
{view.label} {view.label}
</li> </li>
{/each} {/each}
</ul> </ul>
<hr /> <hr />
<div class="title"> <div class="title">
<Heading extraSmall>Relationships</Heading> <Heading extraSmall>Relationships</Heading>
</div> </div>
<ul> <ul>
{#each links as link} {#each links as link}
<li <li
class:selected={value === link} class:selected={value === link}
on:click={() => handleSelected(link)}> on:click={() => handleSelected(link)}>
{link.label} {link.label}
</li> </li>
{/each} {/each}
</ul> </ul>
</div> </div>
</DropdownMenu> </DropdownMenu>
<style> <style>
.dropdownbutton { .dropdownbutton {
background-color: var(--grey-2); background-color: var(--grey-2);
border: var(--border-transparent); border: var(--border-transparent);
padding: var(--spacing-m); padding: var(--spacing-s) var(--spacing-m);
border-radius: var(--border-radius-m); border-radius: var(--border-radius-m);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
flex: 1 1 auto; flex: 1 1 auto;
} }
.dropdownbutton:hover { .dropdownbutton:hover {
cursor: pointer; cursor: pointer;
background-color: var(--grey-3); background-color: var(--grey-3);
} }
.dropdownbutton span { .dropdownbutton span {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
flex: 1 1 auto; flex: 1 1 auto;
text-align: left; text-align: left;
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
} }
.dropdownbutton :global(svg) { .dropdownbutton :global(svg) {
margin: -4px 0; margin: -4px 0;
} }
.dropdown { .dropdown {
padding: var(--spacing-m) 0; padding: var(--spacing-m) 0;
z-index: 99999999; z-index: 99999999;
} }
.title { .title {
padding: 0 var(--spacing-m) var(--spacing-xs) var(--spacing-m); padding: 0 var(--spacing-m) var(--spacing-xs) var(--spacing-m);
} }
hr { hr {
margin: var(--spacing-m) 0 var(--spacing-xl) 0; margin: var(--spacing-m) 0 var(--spacing-xl) 0;
} }
ul { ul {
list-style: none; list-style: none;
padding-left: 0px; padding-left: 0px;
margin: 0px; margin: 0px;
} }
li { li {
cursor: pointer; cursor: pointer;
margin: 0px; margin: 0px;
padding: var(--spacing-s) var(--spacing-m); padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
} }
.selected { .selected {
background-color: var(--grey-4); background-color: var(--grey-4);
} }
li:hover { li:hover {
background-color: var(--grey-4); background-color: var(--grey-4);
} }
</style> </style>

View File

@ -1,193 +0,0 @@
<script>
import ComponentsHierarchy from "./ComponentsHierarchy.svelte"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import PageLayout from "./PageLayout.svelte"
import PagesList from "./PagesList.svelte"
import { store } from "builderStore"
import NewScreenModal from "./NewScreenModal.svelte"
import CurrentItemPreview from "./AppPreview/CurrentItemPreview.svelte"
import SettingsView from "./SettingsView.svelte"
import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp"
import { AddIcon } from "components/common/Icons"
import { Modal } from "@budibase/bbui"
let newScreenPicker
let confirmDeleteDialog
let componentToDelete = ""
let settingsView
let modal
const settings = () => {
settingsView.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script>
<div class="root">
<div class="ui-nav">
<div class="pages-list-container">
<div class="nav-header">
<span class="navigator-title">Navigator</span>
<span class="components-nav-page">Pages</span>
</div>
<div class="nav-items-container">
<PagesList />
</div>
</div>
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="components-list-container">
<div class="nav-group-header">
<span class="components-nav-header" style="margin-top: 0;">
Screens
</span>
<div>
<button on:click={modal.show}>
<AddIcon />
</button>
</div>
</div>
<div class="nav-items-container">
<ComponentsHierarchy screens={$store.screens} />
</div>
</div>
</div>
<div class="preview-pane">
<CurrentItemPreview />
</div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
<div class="components-pane">
<ComponentsPaneSwitcher />
</div>
{/if}
</div>
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
<SettingsView bind:this={settingsView} />
<style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
width: 20px;
padding-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
.root {
display: grid;
grid-template-columns: 300px 1fr 300px;
height: 100%;
width: 100%;
background: var(--grey-1);
}
.ui-nav {
grid-column: 1;
background-color: var(--white);
height: calc(100vh - 49px);
padding: 0;
display: flex;
flex-direction: column;
}
.preview-pane {
grid-column: 2;
margin: 40px;
background: #fff;
border-radius: 5px;
box-shadow: 0 0px 6px rgba(0, 0, 0, 0.05);
}
.components-pane {
grid-column: 3;
background-color: var(--white);
height: calc(100vh - 49px);
}
.components-nav-page {
font-size: 13px;
color: var(--ink);
text-transform: uppercase;
padding-left: 20px;
margin-top: 20px;
font-weight: 600;
opacity: 0.4;
}
.components-nav-header {
font-size: 13px;
color: var(--ink);
text-transform: uppercase;
margin-top: 20px;
font-weight: 600;
opacity: 0.4;
}
.nav-header {
display: flex;
flex-direction: column;
margin-top: 20px;
}
.nav-items-container {
padding: 1rem 0rem 0rem 0rem;
}
.nav-group-header {
display: flex;
padding: 0px 20px 0px 20px;
font-size: 0.9rem;
font-weight: bold;
justify-content: space-between;
align-items: center;
}
.nav-group-header > div:nth-child(1) {
padding: 0rem 0.5rem 0rem 0rem;
vertical-align: bottom;
grid-column-start: icon;
margin-right: 5px;
}
.nav-group-header > span:nth-child(3) {
margin-left: 5px;
vertical-align: bottom;
grid-column-start: title;
margin-top: auto;
}
.nav-group-header > div:nth-child(3) {
vertical-align: bottom;
grid-column-start: button;
cursor: pointer;
color: var(--blue);
}
.nav-group-header > div:nth-child(3):hover {
color: var(--blue);
}
.navigator-title {
font-size: 14px;
color: var(--ink);
font-weight: 600;
text-transform: uppercase;
padding: 0 20px 20px 20px;
line-height: 1rem !important;
}
.components-list-container {
padding: 10px 0px 0 0;
}
</style>

View File

@ -1,5 +1,5 @@
import { isUndefined, filter, some, includes } from "lodash/fp" import { isUndefined, filter, some, includes } from "lodash/fp"
import { pipe } from "components/common/core" import { pipe } from "../../../helpers"
const normalString = s => (s || "").trim().toLowerCase() const normalString = s => (s || "").trim().toLowerCase()

View File

@ -1,5 +1,5 @@
import { split, last } from "lodash/fp" import { split, last } from "lodash/fp"
import { pipe } from "components/common/core" import { pipe } from "../../../helpers"
export const splitName = fullname => { export const splitName = fullname => {
const componentName = pipe(fullname, [split("/"), last]) const componentName = pipe(fullname, [split("/"), last])

View File

@ -1,5 +1,5 @@
import { last } from "lodash/fp" import { last, flow } from "lodash/fp"
import { pipe } from "components/common/core"
export const buildStyle = styles => { export const buildStyle = styles => {
let str = "" let str = ""
for (let s in styles) { for (let s in styles) {
@ -15,6 +15,8 @@ export const convertCamel = str => {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`) return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
} }
export const pipe = (arg, funcs) => flow(funcs)(arg)
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
export const get_name = s => (!s ? "" : last(s.split("/"))) export const get_name = s => (!s ? "" : last(s.split("/")))

View File

@ -15,7 +15,7 @@
</head> </head>
<body id="app"> <body id="app">
<script src='/_builder/bundle.js'></script> <script src='/_builder/bundle.js'></script>
</body> </body>

View File

@ -67,23 +67,27 @@
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<FeedbackNavLink /> <FeedbackNavLink />
<div class="topnavitemright">
<a target="_blank" href="https://docs.budibase.com">
<i class="ri-question-line" />
</a>
</div>
<div class="topnavitemright">
<a
target="_blank"
href="https://github.com/Budibase/budibase/discussions">
<i class="ri-discuss-line" />
</a>
</div>
<SettingsLink /> <SettingsLink />
<span <Button
class:active={false} secondary
class="topnavitemright"
on:click={() => { on:click={() => {
document.cookie = 'budibase:token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' document.cookie = 'budibase:token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
window.open(`/${application}`) window.open(`/${application}`)
}}> }}>
<PreviewIcon /> Preview
</span> </Button>
<span class="topnavitemright">
<a
target="_blank"
href="https://github.com/Budibase/budibase/discussions">
<i class="ri-question-fill help" />
</a>
</span>
</div> </div>
</div> </div>
<div class="beta"> <div class="beta">
@ -122,7 +126,7 @@
flex: 0 0 auto; flex: 0 0 auto;
height: 60px; height: 60px;
background: #fff; background: #fff;
padding: 0px 20px 0 20px; padding: 0 20px;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
justify-content: space-between; justify-content: space-between;
@ -168,20 +172,19 @@
font-weight: 500; font-weight: 500;
} }
.topnavitemright { .topnavitemright a {
cursor: pointer; cursor: pointer;
color: var(--grey-7); color: var(--grey-7);
margin: 0 20px 0 0; margin: 0 12px 0 0;
font-weight: 500;
font-size: 1rem;
height: 100%;
display: flex; display: flex;
flex: 1; flex-direction: row;
justify-content: center;
align-items: center; align-items: center;
box-sizing: border-box; height: 24px;
width: 24px;
} }
.topnavitemright:hover { .topnavitemright a:hover {
color: var(--ink); color: var(--ink);
font-weight: 500; font-weight: 500;
} }
@ -207,10 +210,13 @@
text-transform: capitalize; text-transform: capitalize;
} }
.help { i {
font-size: 24px; font-size: 18px;
color: var(--grey-7); color: var(--grey-7);
} }
i:hover {
color: var(--ink);
}
.beta { .beta {
position: absolute; position: absolute;

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