1
0
Fork 0
mirror of synced 2024-06-27 18:40:42 +12:00

Merge branch 'master' of github.com:Budibase/budibase into tidy-up-store

This commit is contained in:
Martin McKeaveney 2020-10-30 16:25:00 +00:00
commit f29d34ae0b
213 changed files with 3583 additions and 6570 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/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.
@ -95,14 +93,9 @@ then `cd ` into your local copy.
### 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.
```
cd packages/server
yarn run budi new your-app-name
```
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.
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.
### 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
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.1",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -14,35 +14,26 @@ context("Create a automation", () => {
cy.createTestTableWithData()
cy.contains("automate").click()
cy.contains("Create New Automation").click()
cy.get("[data-cy=new-automation]").click()
cy.get(".modal").within(() => {
cy.get("input").type("Add Row")
cy.get(".buttons")
.contains("Create")
.click()
cy.get(".buttons").contains("Create").click()
})
// Add trigger
cy.get("[data-cy=add-automation-component]").click()
cy.get("[data-cy=ROW_SAVED]").click()
cy.get("[data-cy=automation-block-setup]").within(() => {
cy.get("select")
.first()
.select("dog")
cy.contains("Trigger").click()
cy.contains("Row Saved").click()
cy.get(".setup").within(() => {
cy.get("select").first().select("dog")
})
// Create action
cy.get("[data-cy=CREATE_ROW]").click()
cy.get("[data-cy=automation-block-setup]").within(() => {
cy.get("select")
.first()
.select("dog")
cy.get("input")
.first()
.type("goodboy")
cy.get("input")
.eq(1)
.type("11")
cy.contains("Action").click()
cy.contains("Create Row").click()
cy.get(".setup").within(() => {
cy.get("select").first().select("dog")
cy.get("input").first().type("goodboy")
cy.get("input").eq(1).type("11")
})
// Save
@ -50,7 +41,7 @@ context("Create a automation", () => {
// Activate Automation
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", () => {

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(() => {
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')
})
// https://on.cypress.io/interacting-with-elements
it("should add a container", () => {
cy.navigateToFrontend()
cy.get(".switcher > :nth-child(2)").click()
cy.contains("Container").click()
})
it("should add a headline", () => {
cy.addHeadlineComponent("An Amazing headline!")
// https://on.cypress.io/interacting-with-elements
it('should add a container', () => {
cy.contains('frontend').click()
cy.get('.switcher > :nth-child(2)').click()
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()
cy.contains('Container').click()
})
it('should add a headline', () => {
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')
})
getIframeBody()
.contains("An Amazing headline!")
.should("have.css", "font-size", "60px")
})
})
const getIframeDocument = () => {
return cy
.get('iframe')
// Cypress yields jQuery element, which has the real
// DOM element under property "0".
// From the real DOM iframe element we can get
// the "document" element, it is stored in "contentDocument" property
// Cypress "its" command can access deep properties using dot notation
// https://on.cypress.io/its
.its('0.contentDocument').should('exist')
return (
cy
.get("iframe")
// Cypress yields jQuery element, which has the real
// DOM element under property "0".
// From the real DOM iframe element we can get
// the "document" element, it is stored in "contentDocument" property
// Cypress "its" command can access deep properties using dot notation
// https://on.cypress.io/its
.its("0.contentDocument")
.should("exist")
)
}
const getIframeBody = () => {
// get the document
return getIframeDocument()
// automatically retries until body is loaded
.its('body').should('not.be.undefined')
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
.then(cy.wrap)
}
// get the document
return (
getIframeDocument()
// automatically retries until body is loaded
.its("body")
.should("not.be.undefined")
// 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")
// 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", () => {
@ -26,9 +26,7 @@ context("Create a Table", () => {
.trigger("mouseover")
.find(".ri-pencil-line")
.click({ force: true })
cy.get(".actions input")
.first()
.type("updated")
cy.get(".actions input").first().type("updated")
// Unset table display column
cy.contains("display column").click()
cy.contains("Save Column").click()
@ -36,7 +34,8 @@ context("Create a Table", () => {
})
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.contains("Save").click()
cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
@ -50,10 +49,10 @@ context("Create a Table", () => {
})
it("deletes a column", () => {
cy
.contains("header", "name")
.trigger("mouseover")
.find(".ri-pencil-line").click({ force: true })
cy.contains("header", "name")
.trigger("mouseover")
.find(".ri-pencil-line")
.click({ force: true })
cy.contains("Delete").click()
cy.wait(50)
cy.get(".buttons").contains("Delete").click()
@ -61,9 +60,7 @@ context("Create a Table", () => {
})
it("deletes a table", () => {
cy.contains("div", "dog")
.get(".ri-more-line")
.click()
cy.contains(".nav-item", "dog").get(".actions").invoke("show").click()
cy.get("[data-cy=delete-table]").click()
cy.contains("Delete Table").click()
cy.contains("dog").should("not.exist")

View file

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

View file

@ -4,8 +4,8 @@
// 3. Runs the server using said folder
const rimraf = require("rimraf")
const { join } = require("path")
const run = require("../../cli/src/commands/run/runHandler")
const { join, resolve } = require("path")
// const run = require("../../cli/src/commands/run/runHandler")
const initialiseBudibase = require("../../server/src/utilities/initialiseBudibase")
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.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" })
.then(() => {
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))

View file

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

View file

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

View file

@ -85,14 +85,6 @@
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 {
border: 1px solid var(--grey-4);
background: #fff;

View file

@ -106,11 +106,15 @@ const contextToBindables = (tables, walkResult) => context => {
}
}
const stringType = { type: "string" }
return (
Object.entries(schema)
.map(newBindable)
// 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"
export default function(component, state) {
const capitalised = get_capitalised_name(component)
const capitalised = get_capitalised_name(
component.name || component._component
)
const matchingComponents = []

View file

@ -132,10 +132,10 @@ const setPackage = (store, initial) => async pkg => {
...Object.values(unauth_screens),
]
initial.builtins = [getBuiltin("##builtin/screenslot")]
initial.appInstances = pkg.application.instances
initial.appInstance = pkg.application.instance
initial.appId = pkg.application._id
store.set(initial)
await backendUiStore.actions.database.select(initial.appInstances[0])
await backendUiStore.actions.database.select(initial.appInstance)
return initial
}
@ -346,7 +346,7 @@ const addChildComponent = store => (componentToAdd, presetProps = {}) => {
const component = getComponentDefinition(state, componentToAdd)
const instanceId = get(backendUiStore).selectedDatabase._id
const instanceName = getNewComponentName(componentToAdd, state)
const instanceName = getNewComponentName(component, state)
const newComponent = createProps(
component,

View file

@ -1,12 +1,12 @@
<script>
import { afterUpdate } from "svelte"
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
$: automationLive = automation?.live
$: instanceId = $backendUiStore.selectedDatabase._id
$: automationCount = $automationStore.automations?.length ?? 0
function onSelect(block) {
automationStore.update(state => {
@ -14,80 +14,19 @@
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>
<section>
{#if automation}
<BlockList />
<Flowchart {automation} {onSelect} />
</section>
<footer>
{#if automation}
<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>
{:else if automationCount === 0}
<i>Create your first automation to get started</i>
{:else}<i>Select an automation to edit</i>{/if}
<style>
section {
display: flex;
flex-direction: column;
justify-content: flex-start;
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);
i {
font-size: var(--font-size-m);
color: var(--grey-5);
margin-top: 2px;
}
</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 { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition"
import { automationStore } from "builderStore"
export let automation
export let onSelect
@ -18,16 +17,9 @@
blocks = blocks.concat(automation.definition.steps || [])
}
}
$: automationCount = $automationStore.automations?.length ?? 0
</script>
{#if automationCount === 0}
<i>Create your first automation to get started</i>
{:else if automation == null}
<i>Select an automation to edit</i>
{:else if !blocks.length}
<i>Add some steps to your automation to get started</i>
{/if}
{#if !blocks.length}<i>Add a trigger to your automation to get started</i>{/if}
<section class="canvas">
{#each blocks as block, idx (block.id)}
<div
@ -44,19 +36,18 @@
</section>
<style>
i {
font-size: var(--font-size-xl);
color: var(--grey-4);
padding: var(--spacing-xl) 40px;
align-self: flex-start;
}
section {
position: absolute;
padding: 40px;
margin: 0 -40px calc(-1 * var(--spacing-l)) -40px;
padding: var(--spacing-l) 40px 0 40px;
display: flex;
align-items: center;
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 {
@ -65,4 +56,9 @@
justify-content: flex-start;
align-items: center;
}
i {
font-size: var(--font-size-m);
color: var(--grey-5);
}
</style>

View file

@ -10,6 +10,11 @@
$: steps =
$automationStore.selectedAutomation?.automation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id)
$: allowDeleteTrigger = !steps.length
function deleteStep() {
automationStore.actions.deleteAutomationBlock(block)
}
</script>
<div
@ -30,6 +35,9 @@
<div class="label">
{#if block.type === 'TRIGGER'}Trigger{:else}Step {blockIdx + 1}{/if}
</div>
{#if block.type !== 'TRIGGER' || allowDeleteTrigger}
<i on:click|stopPropagation={deleteStep} class="delete ri-close-line" />
{/if}
</header>
<hr />
<p>
@ -61,6 +69,7 @@
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
}
header span {
flex: 1 1 auto;
@ -74,7 +83,13 @@
}
header i {
font-size: 20px;
margin-right: 5px;
}
header i.delete {
opacity: 0.5;
}
header i.delete:hover {
cursor: pointer;
opacity: 1;
}
.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>
import { automationStore } from "builderStore"
import AutomationList from "./AutomationList/AutomationList.svelte"
import BlockList from "./BlockList/BlockList.svelte"
import { Heading } from "@budibase/bbui"
import { Spacer } from "@budibase/bbui"
import AutomationList from "./AutomationList.svelte"
import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Modal } from "@budibase/bbui"
import { automationStore, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
let selectedTab = "AUTOMATIONS"
let modal
</script>
<Heading black small>
<span
data-cy="automation-list"
class="hoverable automation-header"
class:selected={selectedTab === 'AUTOMATIONS'}
on:click={() => (selectedTab = 'AUTOMATIONS')}>
Automations
</span>
{#if $automationStore.selectedAutomation}
<span
data-cy="add-automation-component"
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}
<div class="title">
<h1>Automations</h1>
<i
on:click={modal.show}
data-cy="new-automation"
class="ri-add-circle-fill" />
</div>
<AutomationList />
<Modal bind:this={modal}>
<CreateAutomationModal />
</Modal>
<style>
header {
font-size: 18px;
font-weight: 600;
background: none;
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
}
.automation-header {
margin-right: var(--spacing-xl);
.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);
}
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>
import TableSelector from "./ParamInputs/TableSelector.svelte"
import RowSelector from "./ParamInputs/RowSelector.svelte"
import { Button, Input, TextArea, Select, Label } from "@budibase/bbui"
import TableSelector from "./TableSelector.svelte"
import RowSelector from "./RowSelector.svelte"
import { Button, Input, Select, Label } from "@budibase/bbui"
import { automationStore } from "builderStore"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import BindableInput from "../../userInterface/BindableInput.svelte"
import BindableInput from "components/userInterface/BindableInput.svelte"
export let block
export let webhookModal
@ -47,43 +47,41 @@
}
</script>
<div class="container" data-cy="automation-block-setup">
<div class="block-label">{block.name}</div>
{#each inputs as [key, value]}
<div class="bb-margin-xl block-field">
<Label extraSmall grey>{value.title}</Label>
{#if value.type === 'string' && value.enum}
<Select bind:value={block.inputs[key]} thin secondary>
<option value="">Choose an option</option>
{#each value.enum as option, idx}
<option value={option}>
{value.pretty ? value.pretty[idx] : option}
</option>
{/each}
</Select>
{:else if value.customType === 'password'}
<Input type="password" thin bind:value={block.inputs[key]} />
{:else if value.customType === 'table'}
<TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'row'}
<RowSelector bind:value={block.inputs[key]} {bindings} />
{:else if value.customType === 'webhookUrl'}
<WebhookDisplay value={block.inputs[key]} />
{:else if value.type === 'string' || value.type === 'number'}
<BindableInput
type="string"
thin
bind:value={block.inputs[key]}
{bindings} />
{/if}
</div>
{/each}
{#if stepId === 'WEBHOOK'}
<Button wide secondary on:click={() => webhookModal.show()}>
Setup webhook
</Button>
{/if}
</div>
<div class="block-label">{block.name}</div>
{#each inputs as [key, value]}
<div class="block-field">
<Label extraSmall grey>{value.title}</Label>
{#if value.type === 'string' && value.enum}
<Select bind:value={block.inputs[key]} extraThin secondary>
<option value="">Choose an option</option>
{#each value.enum as option, idx}
<option value={option}>
{value.pretty ? value.pretty[idx] : option}
</option>
{/each}
</Select>
{:else if value.customType === 'password'}
<Input type="password" extraThin bind:value={block.inputs[key]} />
{:else if value.customType === 'table'}
<TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'row'}
<RowSelector bind:value={block.inputs[key]} {bindings} />
{:else if value.customType === 'webhookUrl'}
<WebhookDisplay value={block.inputs[key]} />
{:else if value.type === 'string' || value.type === 'number'}
<BindableInput
type="string"
extraThin
bind:value={block.inputs[key]}
{bindings} />
{/if}
</div>
{/each}
{#if stepId === 'WEBHOOK'}
<Button wide secondary on:click={() => webhookModal.show()}>
Set Up Webhook
</Button>
{/if}
<style>
.block-field {
@ -92,7 +90,7 @@
.block-label {
font-weight: 500;
font-size: 14px;
font-size: var(--font-size-xs);
color: var(--grey-7);
}

View file

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

View file

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

View file

@ -6,7 +6,7 @@
</script>
<div class="block-field">
<Select bind:value secondary thin>
<Select bind:value secondary extraThin>
<option value="">Choose an option</option>
{#each $backendUiStore.tables as table}
<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 { ModalContent } from "@budibase/bbui"
import { onMount, onDestroy } from "svelte"
import { cloneDeep } from "lodash/fp"
import analytics from "analytics"
const POLL_RATE_MS = 2500
const DEFAULT_SCHEMA_OUTPUT = "Any input allowed"
let name
let interval
let finished = false
let schemaURL
@ -88,8 +84,7 @@
text-decoration: none;
}
p {
margin-top: 0;
padding-top: 0;
margin: 0;
text-align: justify;
}
.finished-text {

View file

@ -1,7 +1,7 @@
<script>
import { notifier } from "builderStore/store/notifications"
import { Input } from "@budibase/bbui"
import { store } from "../../../builderStore"
import { store } from "builderStore"
export let value
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
$: schema = $backendUiStore.selectedTable.schema
$: tableId = $backendUiStore.selectedTable._id
$: tableView = {
schema,
name: $backendUiStore.selectedView.name,

View file

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

View file

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

View file

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

View file

@ -12,8 +12,8 @@
{#if FILE_TYPES.IMAGE.includes(file.extension.toLowerCase())}
<img {width} {height} src={file.url} />
{:else}
<i class="ri-file-line" />
<span class="extension">.{file.extension}</span>
<i class="ri-file-line" />
<span>{file.name}</span>
{/if}
</div>
@ -33,12 +33,12 @@
}
i {
font-size: 36px;
margin-bottom: var(--spacing-m);
font-size: 24px;
position: relative;
top: 5px;
}
.file {
position: relative;
height: 75px;
width: 75px;
border: 2px dashed var(--grey-7);
@ -46,18 +46,23 @@
display: flex;
flex-direction: column;
align-items: center;
position: relative;
justify-content: center;
overflow: hidden;
text-overflow: ellipsis;
}
.extension {
position: absolute;
top: var(--spacing-s);
left: var(--spacing-s);
top: -5px;
font-size: var(--font-size-xs);
font-weight: 500;
}
span {
font-size: var(--font-size-xs);
width: 75px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View file

@ -31,6 +31,9 @@ export function deleteRowRenderer(params) {
export function editRowRenderer(params) {
const container = document.createElement("div")
container.style.height = "100%"
container.style.display = "flex"
container.style.alignItems = "center"
new EditRow({
target: container,
@ -46,6 +49,9 @@ export function editRowRenderer(params) {
function attachmentRenderer(options, constraints, editable) {
return params => {
const container = document.createElement("div")
container.style.height = "100%"
container.style.display = "flex"
container.style.alignItems = "center"
const attachmentInstance = new AttachmentList({
target: container,
@ -64,6 +70,7 @@ function linkedRowRenderer() {
let container = document.createElement("div")
container.style.display = "grid"
container.style.height = "100%"
container.style.alignItems = "center"
new RelationshipDisplay({
target: container,

View file

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

View file

@ -1,14 +1,11 @@
<script>
import { backendUiStore } from "builderStore"
import { DropdownMenu, Icon, Modal } from "@budibase/bbui"
import * as api from "../api"
import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let row
let anchor
let dropdown
let confirmDeleteDialog
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.`}
okText="Delete Row"
onOk={deleteRow}
title="Confirm Delete" />
title="Confirm Deletion" />
<style>
.ri-delete-bin-line:hover {

View file

@ -1,13 +1,9 @@
<script>
import { backendUiStore } from "builderStore"
import { DropdownMenu, Icon, Modal, Button } from "@budibase/bbui"
import { Modal, Button } from "@budibase/bbui"
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
export let row
let anchor
let dropdown
let confirmDeleteDialog
let modal
function showModal(e) {
@ -16,7 +12,7 @@
}
</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}>
<CreateEditRowModal {row} />
</Modal>

View file

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

View file

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

View file

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

View file

@ -1,11 +1,9 @@
<script>
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import screenTemplates from "builderStore/store/screenTemplates"
import api from "builderStore/api"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let table
@ -66,42 +64,46 @@
}
</script>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
<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}>
{#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>
<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
bind:this={confirmDeleteDialog}
okText="Delete Table"
onOk={deleteTable}
title="Confirm Delete">
title="Confirm Deletion">
Are you sure you wish to delete the table
<i>{table.name}?</i>
The following will also be deleted:
@ -155,29 +157,4 @@
justify-content: flex-end;
gap: var(--spacing-m);
}
ul {
list-style: none;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
</style>

View file

@ -2,9 +2,9 @@
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let view
@ -46,38 +46,42 @@
}
</script>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
<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}>
{#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>
<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
bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete the view '${view.name}'? Your data will be deleted and this action cannot be undone.`}
okText="Delete View"
onOk={deleteView}
title="Confirm Delete" />
title="Confirm Deletion" />
<style>
div.icon {
@ -108,29 +112,4 @@
justify-content: flex-end;
gap: var(--spacing-m);
}
ul {
list-style: none;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
</style>

View file

@ -1,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>
import { notifier } from "builderStore/store/notifications"
import { Heading, Body, Button, Dropzone } from "@budibase/bbui"
import { Dropzone } from "@budibase/bbui"
import api from "builderStore/api"
export let files = []
function handleFileTooLarge() {
const BYTES_IN_MB = 1000000
function handleFileTooLarge(fileSizeLimit) {
notifier.danger(
`Files cannot exceed ${fileSizeLimit /
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>
import SettingsModal from "./SettingsModal.svelte"
import { SettingsIcon } from "components/common/Icons/"
import { Modal } from "@budibase/bbui"
let modal
</script>
<span class="topnavitemright settings" on:click={modal.show}>
<SettingsIcon />
</span>
<div class="topnavitemright settings" on:click={modal.show}>
<i class="ri-settings-3-line" />
</div>
<Modal bind:this={modal} width="600px">
<SettingsModal />
</Modal>
<style>
span:first-letter {
text-transform: capitalize;
i {
font-size: 18px;
color: var(--grey-7);
}
.topnavitemright {
cursor: pointer;
color: var(--grey-7);
margin: 0 20px 0 0;
margin: 0 12px 0 0;
font-weight: 500;
font-size: 1rem;
height: 100%;
display: flex;
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
box-sizing: border-box;
height: 24px;
width: 24px;
}
.topnavitemright:hover i {
color: var(--ink);
}
</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 { API, Info, User } from "./Steps"
import Indicator from "./Indicator.svelte"
import { Input, TextArea, Button } from "@budibase/bbui"
import { Button } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { fade } from "svelte/transition"
import { post } from "builderStore/api"
import analytics from "analytics"

View file

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

View file

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

View file

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

View file

@ -3,10 +3,10 @@
import { store } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils"
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 { uuid } from "builderStore/uuid"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let component
@ -98,110 +98,65 @@
}
</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>
<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>
<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
bind:this={confirmDeleteDialog}
title="Confirm Delete"
title="Confirm Deletion"
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`}
okText="Delete Component"
onOk={deleteComponent} />
<style>
ul {
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 {
hr {
margin: 8px 0;
color: var(--grey-4);
background-color: var(--grey-4);
height: 1px;
border: none;
}
</style>

View file

@ -63,41 +63,31 @@
}
</script>
<div class="root">
<CategoryTab
onClick={category => (selectedCategory = category)}
{categories}
{selectedCategory} />
<CategoryTab
onClick={category => (selectedCategory = category)}
{categories}
{selectedCategory} />
{#if displayName}
<div class="instance-name">{componentInstance._instanceName}</div>
{#if displayName}
<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}
<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>
<style>
.root {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
box-sizing: border-box;
}
.title > div:nth-child(1) {
grid-column-start: name;
color: var(--ink);
@ -108,7 +98,6 @@
}
.component-props-container {
margin-top: 16px;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
@ -118,8 +107,7 @@
}
.instance-name {
margin-top: 20px;
font-size: 14px;
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--grey-7);
}

View file

@ -2,47 +2,97 @@
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import components from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte"
import Tab from "./ItemTab/Tab.svelte"
export let toggleTab
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
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 => {
store.addChildComponent(component._component, component.presetProps)
toggleTab("Navigate")
// Get ID path
const path = store.getPathToComponent($store.currentComponentInfo)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
close()
}
</script>
<div class="root">
<CategoryTab
onClick={category => (selectedCategory = category)}
{selectedCategory}
{categories} />
<div class="panel">
<Tab
list={selectedCategory}
on:selectItem={e => onComponentChosen(e.detail)}
{toggleTab} />
</div>
<div class="container">
{#each categories as category, idx}
<div
bind:this={anchors[idx]}
class="category"
on:click={() => onCategoryChosen(category, idx)}
class:active={idx === selectedIndex}>
{#if category.icon}<i class={category.icon} />{/if}
<span>{category.name}</span>
{#if category.isCategory}<i class="ri-arrow-down-s-line arrow" />{/if}
</div>
{/each}
</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>
.panel {
margin-top: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
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>

View file

@ -1,13 +1,12 @@
<script>
import { params, goto } from "@sveltech/routify"
import { goto } from "@sveltech/routify"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import { last, sortBy, map, trimCharsStart, trimChars, join } from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { pipe } from "components/common/core"
import { trimCharsStart, trimChars } from "lodash/fp"
import { pipe } from "../../helpers"
import { store } from "builderStore"
import { ArrowDownIcon, ShapeIcon } from "components/common/Icons/"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
import { writable } from "svelte/store"
import NavItem from "components/common/NavItem.svelte"
export let screens = []
@ -24,8 +23,6 @@
let confirmDeleteDialog
let componentToDelete = ""
const joinPath = join("/")
const normalizedName = name =>
pipe(name, [
trimCharsStart("./"),
@ -42,26 +39,15 @@
<div class="root">
{#each sortedScreens as screen}
<div
class="budibase__nav-item screen-header-row"
class:selected={$store.currentComponentInfo._id === screen.props._id}
on:click|stopPropagation={() => changeScreen(screen)}>
<span
class="icon"
class:rotate={$store.currentPreviewItem.name !== screen.props._instanceName}>
{#if screen.props._children.length}
<ArrowDownIcon />
{/if}
</span>
<i class="ri-artboard-2-fill icon" />
<span class="title">{screen.props._instanceName}</span>
<div class="dropdown-menu">
<ScreenDropdownMenu {screen} />
</div>
</div>
<NavItem
icon="ri-artboard-2-line"
text={screen.props._instanceName}
withArrow={screen.props._children.length}
selected={$store.currentComponentInfo._id === screen.props._id}
opened={$store.currentPreviewItem.name === screen.props._id}
on:click={() => changeScreen(screen)}>
<ScreenDropdownMenu {screen} />
</NavItem>
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children}
<ComponentsHierarchyChildren
@ -71,55 +57,3 @@
{/if}
{/each}
</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 { store } from "builderStore"
import { last } from "lodash/fp"
import { pipe } from "components/common/core"
import { pipe } from "../../helpers"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import {
XCircleIcon,
ChevronUpIcon,
ChevronDownIcon,
CopyIcon,
} from "../common/Icons"
import NavItem from "components/common/NavItem.svelte"
import { getComponentDefinition } from "builderStore/storeUtils"
export let components = []
@ -40,7 +35,6 @@
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const isScreenslot = name => name === "##builtin/screenslot"
@ -134,29 +128,22 @@
on:drop={drop}
ondragover="return false"
ondragenter="return false"
class="budibase__nav-item item drop-item"
style="margin-left: {level * 20 + 40}px" />
class="drop-item"
style="margin-left: {(level + 1) * 18}px" />
{/if}
<div
class="budibase__nav-item item"
class:selected={currentComponent === component}
style="padding-left: {level * 20 + 40}px"
draggable={true}
<NavItem
draggable
on:dragend={dragend}
on:dragstart={dragstart(component)}
on:dragover={dragover(component, index)}
on:drop={drop}
ondragover="return false"
ondragenter="return false">
<div class="nav-item">
<i class="icon ri-arrow-right-circle-line" />
{isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
</div>
<div class="actions">
<ComponentDropdownMenu {component} />
</div>
</div>
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
withArrow
indentLevel={level + 1}
selected={currentComponent === component}>
<ComponentDropdownMenu {component} />
</NavItem>
{#if component._children}
<svelte:self
@ -172,8 +159,8 @@
on:drop={drop}
ondragover="return false"
ondragenter="return false"
class="budibase__nav-item item drop-item"
style="margin-left: {(level + ($dragDropStore.dropPosition === 'inside' ? 2 : 0)) * 20 + 40}px" />
class="drop-item"
style="margin-left: {(level + ($dragDropStore.dropPosition === 'inside' ? 3 : 1)) * 18}px" />
{/if}
</li>
{/each}
@ -186,47 +173,9 @@
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 {
border-radius: var(--border-radius-m);
height: 32px;
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>

View file

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

View file

@ -29,7 +29,13 @@
</script>
<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="" />
{#each urls as url}
<option value={url.url}>{url.name}</option>

View file

@ -1,14 +1,7 @@
<script>
import { store } from "builderStore"
import {
TextButton,
Button,
Body,
DropdownMenu,
ModalContent,
} from "@budibase/bbui"
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
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 { createEventDispatcher } from "svelte"

View file

@ -1,11 +1,6 @@
<script>
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 { ArrowDownIcon } from "components/common/Icons/"
import { createEventDispatcher } from "svelte"
export let parameter

View file

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

View file

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

View file

@ -4,22 +4,41 @@
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import { Button, Spacer, Modal } from "@budibase/bbui"
import { Modal } from "@budibase/bbui"
let modal
</script>
<div class="title">
<h1>Screens</h1>
<i on:click={modal.show} data-cy="new-screen" class="ri-add-circle-fill" />
</div>
<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">
<PageLayout layout={$store.pages[$store.currentPageName]} />
<ComponentsHierarchy screens={$store.screens} />
</div>
<Modal bind:this={modal}>
<NewScreenModal />
</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>
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import { createEventDispatcher, tick } from "svelte"
import icons from "./icons.js"
const dispatch = createEventDispatcher()
export let value = ""
export let maxIconsPerPage = 30
let searchTerm = ""
let selectedLetter = "A"
let currentPage = 1
let filteredIcons = findIconByTerm(selectedLetter)
$: dispatch("change", value)
const alphabet = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
]
let buttonAnchor, dropdown
let loading = false
function findIconByTerm(term) {
const r = new RegExp(`\^${term}`, "i")
return icons.filter(i => r.test(i.label))
}
async function switchLetter(letter) {
currentPage = 1
searchTerm = ""
loading = true
selectedLetter = letter
filteredIcons = findIconByTerm(letter)
await tick() //svg icons do not update without tick
loading = false
}
async function findIconOnPage() {
loading = true
const iconIdx = filteredIcons.findIndex(i => i.value === value)
if (iconIdx !== -1) {
currentPage = Math.ceil(iconIdx / maxIconsPerPage)
}
await tick() //svg icons do not update without tick
loading = false
}
async function setSelectedUI() {
if (value) {
const letter = displayValue.substring(0, 1)
await switchLetter(letter)
await findIconOnPage()
}
}
async function pageClick(next) {
loading = true
if (next && currentPage < totalPages) {
currentPage++
} else if (!next && currentPage > 1) {
currentPage--
}
await tick() //svg icons do not update without tick
loading = false
}
async function searchForIcon(e) {
currentPage = 1
loading = true
filteredIcons = findIconByTerm(searchTerm)
await tick() //svg icons do not update without tick
loading = false
}
$: displayValue = value ? value.substring(7) : "Pick Icon"
$: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage)
$: pageEndIdx = maxIconsPerPage * currentPage
$: pagedIcons = filteredIcons.slice(pageEndIdx - maxIconsPerPage, pageEndIdx)
$: pagerText = `Page ${currentPage} of ${totalPages}`
</script>
<div bind:this={buttonAnchor}>
<Button secondary on:click={dropdown.show}>{displayValue}</Button>
</div>
<DropdownMenu
bind:this={dropdown}
on:open={setSelectedUI}
anchor={buttonAnchor}>
<div class="container">
<div class="search-area">
<div class="alphabet-area">
{#each alphabet as letter, idx}
<span
class="letter"
class:letter-selected={letter === selectedLetter}
on:click={() => switchLetter(letter)}>
{letter}
</span>
{#if idx !== alphabet.length - 1}<span>-</span>{/if}
{/each}
</div>
<div class="search-input">
<div class="input-wrapper">
<Input bind:value={searchTerm} thin placeholder="Search Icon" />
</div>
<Button secondary on:click={searchForIcon}>Search</Button>
</div>
<div class="page-area">
<div class="pager">
<span on:click={() => pageClick(false)}>
<i class="page-btn fas fa-chevron-left" />
</span>
<span>{pagerText}</span>
<span on:click={() => pageClick(true)}>
<i class="page-btn fas fa-chevron-right" />
</span>
</div>
</div>
</div>
{#if pagedIcons.length > 0}
<div class="icon-area">
{#if !loading}
{#each pagedIcons as icon}
<div
class="icon-container"
class:selected={value === icon.value}
on:click={() => (value = icon.value)}>
<div class="icon-preview">
<i class={`${icon.value} fa-3x`} />
</div>
<div class="icon-label">{icon.label}</div>
</div>
{/each}
{/if}
</div>
{:else}
<div class="no-icons">
<h5>
{`There is no icons for this ${searchTerm ? 'search' : 'page'}`}
</h5>
</div>
{/if}
</div>
</DropdownMenu>
<style>
.container {
width: 610px;
height: 350px;
display: flex;
flex-direction: column;
padding: 10px 0px 10px 15px;
overflow-x: hidden;
}
.search-area {
flex: 0 0 80px;
display: flex;
flex-direction: column;
}
.icon-area {
flex: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 5px;
justify-content: flex-start;
overflow-y: auto;
overflow-x: hidden;
padding-right: 10px;
}
.no-icons {
display: flex;
justify-content: center;
align-items: center;
}
.alphabet-area {
display: flex;
flex-flow: row wrap;
padding-bottom: 10px;
padding-right: 15px;
justify-content: space-around;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
}
.search-input {
display: flex;
flex-flow: row nowrap;
width: 100%;
padding-right: 15px;
}
.input-wrapper {
width: 510px;
margin-right: 5px;
}
.page-area {
padding: 10px;
display: flex;
justify-content: center;
}
.letter {
color: var(--blue);
}
.letter:hover {
cursor: pointer;
text-decoration: underline;
}
.letter-selected {
text-decoration: underline;
}
.icon-container {
height: 100px;
display: flex;
justify-content: center;
flex-direction: column;
border: var(--border-dark);
}
.icon-container:hover {
cursor: pointer;
background: var(--grey-2);
}
.selected {
background: var(--grey-3);
}
.icon-preview {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.icon-label {
flex: 0 0 20px;
text-align: center;
font-size: 12px;
}
.page-btn {
color: var(--blue);
}
.page-btn:hover {
cursor: pointer;
}
</style>
<script>
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import { createEventDispatcher, tick } from "svelte"
import icons from "./icons.js"
const dispatch = createEventDispatcher()
export let value = ""
export let maxIconsPerPage = 30
let searchTerm = ""
let selectedLetter = "A"
let currentPage = 1
let filteredIcons = findIconByTerm(selectedLetter)
$: dispatch("change", value)
const alphabet = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
]
let buttonAnchor, dropdown
let loading = false
function findIconByTerm(term) {
const r = new RegExp(`\^${term}`, "i")
return icons.filter(i => r.test(i.label))
}
async function switchLetter(letter) {
currentPage = 1
searchTerm = ""
loading = true
selectedLetter = letter
filteredIcons = findIconByTerm(letter)
await tick() //svg icons do not update without tick
loading = false
}
async function findIconOnPage() {
loading = true
const iconIdx = filteredIcons.findIndex(i => i.value === value)
if (iconIdx !== -1) {
currentPage = Math.ceil(iconIdx / maxIconsPerPage)
}
await tick() //svg icons do not update without tick
loading = false
}
async function setSelectedUI() {
if (value) {
const letter = displayValue.substring(0, 1)
await switchLetter(letter)
await findIconOnPage()
}
}
async function pageClick(next) {
loading = true
if (next && currentPage < totalPages) {
currentPage++
} else if (!next && currentPage > 1) {
currentPage--
}
await tick() //svg icons do not update without tick
loading = false
}
async function searchForIcon(e) {
currentPage = 1
loading = true
filteredIcons = findIconByTerm(searchTerm)
await tick() //svg icons do not update without tick
loading = false
}
$: displayValue = value ? value.substring(7) : "Pick Icon"
$: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage)
$: pageEndIdx = maxIconsPerPage * currentPage
$: pagedIcons = filteredIcons.slice(pageEndIdx - maxIconsPerPage, pageEndIdx)
$: pagerText = `Page ${currentPage} of ${totalPages}`
</script>
<div bind:this={buttonAnchor}>
<Button secondary small on:click={dropdown.show}>{displayValue}</Button>
</div>
<DropdownMenu
bind:this={dropdown}
on:open={setSelectedUI}
anchor={buttonAnchor}>
<div class="container">
<div class="search-area">
<div class="alphabet-area">
{#each alphabet as letter, idx}
<span
class="letter"
class:letter-selected={letter === selectedLetter}
on:click={() => switchLetter(letter)}>
{letter}
</span>
{#if idx !== alphabet.length - 1}<span>-</span>{/if}
{/each}
</div>
<div class="search-input">
<div class="input-wrapper">
<Input bind:value={searchTerm} thin placeholder="Search Icon" />
</div>
<Button secondary on:click={searchForIcon}>Search</Button>
</div>
<div class="page-area">
<div class="pager">
<span on:click={() => pageClick(false)}>
<i class="page-btn fas fa-chevron-left" />
</span>
<span>{pagerText}</span>
<span on:click={() => pageClick(true)}>
<i class="page-btn fas fa-chevron-right" />
</span>
</div>
</div>
</div>
{#if pagedIcons.length > 0}
<div class="icon-area">
{#if !loading}
{#each pagedIcons as icon}
<div
class="icon-container"
class:selected={value === icon.value}
on:click={() => (value = icon.value)}>
<div class="icon-preview">
<i class={`${icon.value} fa-3x`} />
</div>
<div class="icon-label">{icon.label}</div>
</div>
{/each}
{/if}
</div>
{:else}
<div class="no-icons">
<h5>
{`There is no icons for this ${searchTerm ? 'search' : 'page'}`}
</h5>
</div>
{/if}
</div>
</DropdownMenu>
<style>
.container {
width: 610px;
height: 350px;
display: flex;
flex-direction: column;
padding: 10px 0px 10px 15px;
overflow-x: hidden;
}
.search-area {
flex: 0 0 80px;
display: flex;
flex-direction: column;
}
.icon-area {
flex: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 5px;
justify-content: flex-start;
overflow-y: auto;
overflow-x: hidden;
padding-right: 10px;
}
.no-icons {
display: flex;
justify-content: center;
align-items: center;
}
.alphabet-area {
display: flex;
flex-flow: row wrap;
padding-bottom: 10px;
padding-right: 15px;
justify-content: space-around;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
}
.search-input {
display: flex;
flex-flow: row nowrap;
width: 100%;
padding-right: 15px;
}
.input-wrapper {
width: 510px;
margin-right: 5px;
}
.page-area {
padding: 10px;
display: flex;
justify-content: center;
}
.letter {
color: var(--blue);
}
.letter:hover {
cursor: pointer;
text-decoration: underline;
}
.letter-selected {
text-decoration: underline;
}
.icon-container {
height: 100px;
display: flex;
justify-content: center;
flex-direction: column;
border: var(--border-dark);
}
.icon-container:hover {
cursor: pointer;
background: var(--grey-2);
}
.selected {
background: var(--grey-3);
}
.icon-preview {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.icon-label {
flex: 0 0 20px;
text-align: center;
font-size: 12px;
}
.page-btn {
color: var(--blue);
}
.page-btn:hover {
cursor: pointer;
}
</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 selectMenu
let icon
let width = 0
let selectAnchor = null
let dimensions = { top: 0, bottom: 0, left: 0 }
@ -91,6 +92,7 @@
"transform-origin": `center ${positionSide}`,
[positionSide]: `${dimensions[positionSide]}px`,
left: `${dimensions.left.toFixed(0)}px`,
width: `${width}px`,
})
$: isOptionsObject = options.every(o => typeof o === "object")
@ -108,6 +110,7 @@
</script>
<div
bind:clientWidth={width}
tabindex="0"
bind:this={select}
class="bb-select-container"
@ -164,19 +167,17 @@
.bb-select-container {
outline: none;
height: 36px;
cursor: pointer;
font-size: 14px;
overflow: hidden;
flex: 1 1 auto;
}
.bb-select-anchor {
cursor: pointer;
display: flex;
padding: 0px 12px;
height: 36px;
padding: var(--spacing-s) var(--spacing-m);
background-color: var(--grey-2);
border-radius: 5px;
border-radius: var(--border-radius-m);
align-items: center;
white-space: nowrap;
}
@ -184,8 +185,11 @@
.bb-select-anchor > span {
color: var(--ink);
font-weight: 400;
width: 140px;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--font-size-xs);
flex: 1 1 auto;
}
.bb-select-anchor > i {
@ -208,7 +212,6 @@
box-sizing: border-box;
flex-direction: column;
opacity: 0;
width: 160px;
z-index: 2;
color: var(--ink);
font-weight: 400;
@ -237,7 +240,7 @@
padding: 5px 0px;
cursor: pointer;
padding-left: 10px;
font-size: var(--font-size-s);
font-size: var(--font-size-xs);
}
li:hover {

View file

@ -1,21 +1,9 @@
<script>
import { goto } from "@sveltech/routify"
// import { tick } from "svelte"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import {
last,
sortBy,
map,
trimCharsStart,
trimChars,
join,
compose,
} from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { pipe } from "components/common/core"
import NavItem from "components/common/NavItem.svelte"
import { last } from "lodash/fp"
import { store } from "builderStore"
import { ArrowDownIcon, GridIcon } from "components/common/Icons/"
import { writable } from "svelte/store"
export let layout
@ -24,13 +12,10 @@
let componentToDelete = ""
const dragDropStore = writable({})
const joinPath = join("/")
const lastPartOfName = c =>
c && last(c.name ? c.name.split("/") : c._component.split("/"))
const isComponentSelected = (current, comp) => current === comp
$: _layout = {
component: layout,
title: lastPartOfName(layout),
@ -42,18 +27,14 @@
}
</script>
<div
class="budibase__nav-item root"
class:selected={$store.currentComponentInfo._id === _layout.component.props._id}
on:click|stopPropagation={setCurrentScreenToLayout}>
<span
class="icon"
class:rotate={$store.currentPreviewItem.name !== _layout.title}>
<ArrowDownIcon />
</span>
<i class="ri-layout-3-fill icon-big" />
<span class="title">Master Screen</span>
</div>
<NavItem
border={false}
icon="ri-layout-3-line"
text="Master Screen"
withArrow
selected={$store.currentComponentInfo._id === _layout.component.props._id}
opened={$store.currentPreviewItem.name === _layout.title}
on:click={setCurrentScreenToLayout} />
{#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
<ComponentsHierarchyChildren
@ -62,29 +43,3 @@
currentComponent={$store.currentComponentInfo}
{dragDropStore} />
{/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 {
cursor: pointer;
padding: 0px 16px;
height: 36px;
padding: 0 var(--spacing-m);
height: 32px;
text-align: center;
background: #ffffff;
color: var(--grey-7);
border-radius: 5px;
font-family: inter;
font-size: 14px;
font-weight: 400;
font-size: var(--font-size-xs);
font-weight: 500;
transition: all 0.3s;
text-rendering: optimizeLegibility;
border: none !important;
transition: 0.2s;
outline: none;
font-family: var(--font-sans);
}
.active {
background: var(--grey-3);
background: var(--grey-2);
color: var(--ink);
}
</style>

View file

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

View file

@ -13,14 +13,26 @@
$: style = componentInstance["_styles"][styleCategory] || {}
</script>
<DetailSummary {name} on:open show={open}>
{#each properties as props}
<PropertyControl
label={props.label}
control={props.control}
key={props.key}
value={style[props.key]}
onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
props={{ ...excludeProps(props, ['control', 'label']) }} />
{/each}
<DetailSummary {name} on:open show={open} thin>
<div>
{#each properties as props}
<PropertyControl
label={props.label}
control={props.control}
key={props.key}
value={style[props.key]}
onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
props={{ ...excludeProps(props, ['control', 'label']) }} />
{/each}
</div>
</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
</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 { store } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import api from "builderStore/api"
import Portal from "svelte-portal"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let screen
@ -12,10 +11,6 @@
let dropdown
let anchor
const hideDropdown = () => {
dropdown.hide()
}
const deleteScreen = () => {
store.deleteScreens(screen, $store.currentPageName)
// update the page if required
@ -29,79 +24,27 @@
}
</script>
<div
bind:this={anchor}
class="root boundary"
on:click|stopPropagation={() => {}}>
<div bind:this={anchor} on:click|stopPropagation>
<div class="icon" on:click={() => dropdown.show()}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<ul on:click={hideDropdown}>
<li on:click={() => confirmDeleteDialog.show()}>
<i class="ri-delete-bin-2-line" />
Delete
</li>
</ul>
<DropdownContainer>
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={() => confirmDeleteDialog.show()} />
</DropdownContainer>
</DropdownMenu>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
title="Confirm Deletion"
body={`Are you sure you wish to delete the screen '${screen.props._instanceName}' ?`}
okText="Delete Screen"
onOk={deleteScreen} />
<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 {
font-size: 16px;
}

View file

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

View file

@ -85,53 +85,65 @@
}
</script>
{#if screenOrPageInstance}
{#each screenOrPageDefinition as def}
<PropertyControl
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)}
<div class="settings-view-container">
{#if screenOrPageInstance}
{#each screenOrPageDefinition as def}
<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']) }} />
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}
{/each}
{:else}
<div>This component does not have any settings.</div>
{/if}
{/if}
{#if panelDefinition && panelDefinition.length > 0}
{#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>
div {
font-size: var(--font-size-s);
.settings-view-container {
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);
color: var(--grey-6);
color: var(--grey-5);
}
.duplicate-name {

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { last } from "lodash/fp"
import { pipe } from "components/common/core"
import { last, flow } from "lodash/fp"
export const buildStyle = styles => {
let str = ""
for (let s in styles) {
@ -15,6 +15,8 @@ export const convertCamel = str => {
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 get_name = s => (!s ? "" : last(s.split("/")))

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