1
0
Fork 0
mirror of synced 2024-06-02 02:25:17 +12:00

Merge pull request #1163 from Budibase/develop

Develop
This commit is contained in:
Martin McKeaveney 2021-02-23 15:35:39 +00:00 committed by GitHub
commit 8db4b28a1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
234 changed files with 7712 additions and 12108 deletions

View file

@ -1,3 +1,5 @@
Copyright 2019-2021, Budibase Ltd
Each Budibase package has its own license:
builder: AGPLv3

View file

@ -84,7 +84,7 @@ services:
#- "4369:4369"
#- "9100:9100"
volumes:
- couchdb_data:/couchdb
- couchdb_data:/opt/couchdb/data
couch-init:
image: curlimages/curl

8068
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Copyright 2019-2021, Budibase Ltd
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.

View file

@ -17,23 +17,33 @@ context("Create a automation", () => {
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.contains("Trigger").click()
cy.contains("Row Saved").click()
cy.contains("Row Created").click()
cy.get(".setup").within(() => {
cy.get("select").first().select("dog")
cy.get("select")
.first()
.select("dog")
})
// Create action
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")
cy.get("select")
.first()
.select("dog")
cy.get("input")
.first()
.type("goodboy")
cy.get("input")
.eq(1)
.type("11")
})
// Save

View file

@ -30,7 +30,7 @@ context("Create a Table", () => {
// Unset table display column
cy.contains("display column").click()
cy.contains("Save Column").click()
cy.contains("nameupdated").should("have.text", "nameupdated")
cy.contains("nameupdated ").should("have.text", "nameupdated ")
})
it("edits a row", () => {

View file

@ -1,3 +1,11 @@
function removeSpacing(headers) {
let newHeaders = []
for (let header of headers) {
newHeaders.push(header.replace(/\s\s+/g, " "))
}
return newHeaders
}
context("Create a View", () => {
before(() => {
cy.visit(`localhost:${Cypress.env("PORT")}/_builder`)
@ -28,7 +36,7 @@ context("Create a View", () => {
const headers = Array.from($headers).map(header =>
header.textContent.trim()
)
expect(headers).to.deep.eq(["group", "age", "rating"])
expect(removeSpacing(headers)).to.deep.eq([ "rating Number", "age Number", "group Text" ])
})
})
@ -53,27 +61,26 @@ context("Create a View", () => {
cy.wait(50)
cy.get(".menu-container").find("select").eq(1).select("age")
cy.contains("Save").click()
cy.wait(100)
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()
)
expect(headers).to.deep.eq([
"field",
"sum",
"min",
"max",
"count",
"sumsqr",
"avg",
])
expect(removeSpacing(headers)).to.deep.eq([ "avg Number",
"sumsqr Number",
"count Number",
"max Number",
"min Number",
"sum Number",
"field Text" ])
})
cy.get(".ag-cell").then($values => {
const values = Array.from($values).map(header =>
let values = Array.from($values).map(header =>
header.textContent.trim()
)
expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"])
expect(values).to.deep.eq([ "31", "5347", "5", "49", "20", "155", "age" ])
})
})
@ -92,15 +99,7 @@ context("Create a View", () => {
.find(".ag-cell")
.then($values => {
const values = Array.from($values).map(value => value.textContent)
expect(values).to.deep.eq([
"Students",
"70",
"20",
"25",
"3",
"1650",
"23.333333333333332",
])
expect(values).to.deep.eq([ "Students", "23.333333333333332", "1650", "3", "25", "20", "70" ])
})
})

View file

@ -5,7 +5,6 @@
const rimraf = require("rimraf")
const { join, resolve } = require("path")
// const run = require("../../cli/src/commands/run/runHandler")
const initialiseBudibase = require("../../server/src/utilities/initialiseBudibase")
const cypressConfig = require("../cypress.json")

View file

@ -63,7 +63,7 @@
}
},
"dependencies": {
"@budibase/bbui": "1.58.3",
"@budibase/bbui": "^1.58.13",
"@budibase/client": "^0.7.8",
"@budibase/colorpicker": "1.0.1",
"@budibase/string-templates": "^0.7.8",

View file

@ -1,33 +1,35 @@
import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store"
import { backendUiStore, store } from "builderStore"
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
import { findComponentPath } from "./storeUtils"
import { makePropSafe } from "@budibase/string-templates"
import { TableNames } from "../constants"
// Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
/**
* Gets all bindable data context fields and instance fields.
*/
export const getBindableProperties = (rootComponent, componentId) => {
const contextBindings = getContextBindings(rootComponent, componentId)
const componentBindings = getComponentBindings(rootComponent)
return [...contextBindings, ...componentBindings]
export const getBindableProperties = (asset, componentId) => {
const contextBindings = getContextBindings(asset, componentId)
const userBindings = getUserBindings()
const urlBindings = getUrlBindings(asset, componentId)
return [...contextBindings, ...userBindings, ...urlBindings]
}
/**
* Gets all data provider components above a component.
*/
export const getDataProviderComponents = (rootComponent, componentId) => {
if (!rootComponent || !componentId) {
export const getDataProviderComponents = (asset, componentId) => {
if (!asset || !componentId) {
return []
}
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(rootComponent, componentId)
const path = findComponentPath(asset.props, componentId)
path.pop()
// Filter by only data provider components
@ -37,6 +39,26 @@ export const getDataProviderComponents = (rootComponent, componentId) => {
})
}
/**
* Gets all data provider components above a component.
*/
export const getActionProviderComponents = (asset, componentId, actionType) => {
if (!asset || !componentId) {
return []
}
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(asset.props, componentId)
path.pop()
// Filter by only data provider components
return path.filter(component => {
const def = store.actions.components.getDefinition(component._component)
return def?.actions?.includes(actionType)
})
}
/**
* Gets a datasource object for a certain data provider component
*/
@ -47,8 +69,9 @@ export const getDatasourceForProvider = component => {
}
// Extract datasource from component instance
const validSettingTypes = ["datasource", "table", "schema"]
const datasourceSetting = def.settings.find(setting => {
return setting.type === "datasource" || setting.type === "table"
return validSettingTypes.includes(setting.type)
})
if (!datasourceSetting) {
return null
@ -58,40 +81,54 @@ export const getDatasourceForProvider = component => {
// example an actual datasource object, or a table ID string.
// Convert the datasource setting into a proper datasource object so that
// we can use it properly
if (datasourceSetting.type === "datasource") {
return component[datasourceSetting?.key]
} else if (datasourceSetting.type === "table") {
if (datasourceSetting.type === "table") {
return {
tableId: component[datasourceSetting?.key],
type: "table",
}
} else {
return component[datasourceSetting?.key]
}
return null
}
/**
* Gets all bindable data contexts. These are fields of schemas of data contexts
* provided by data provider components, such as lists or row detail components.
* Gets all bindable data properties from component data contexts.
*/
export const getContextBindings = (rootComponent, componentId) => {
const getContextBindings = (asset, componentId) => {
// Extract any components which provide data contexts
const dataProviders = getDataProviderComponents(rootComponent, componentId)
let contextBindings = []
const dataProviders = getDataProviderComponents(asset, componentId)
let bindings = []
// Create bindings for each data provider
dataProviders.forEach(component => {
const isForm = component._component.endsWith("/form")
const datasource = getDatasourceForProvider(component)
if (!datasource) {
let tableName, schema
// Forms are an edge case which do not need table schemas
if (isForm) {
schema = buildFormSchema(component)
tableName = "Fields"
} else {
if (!datasource) {
return
}
// Get schema and table for the datasource
const info = getSchemaForDatasource(datasource, isForm)
schema = info.schema
tableName = info.table?.name
// Add _id and _rev fields for certain types
if (datasource.type === "table" || datasource.type === "link") {
schema["_id"] = { type: "string" }
schema["_rev"] = { type: "string" }
}
}
if (!schema || !tableName) {
return
}
// Get schema and add _id and _rev fields for certain types
let { schema, table } = getSchemaForDatasource(datasource)
if (!schema || !table) {
return
}
if (datasource.type === "table" || datasource.type === "link") {
schema["_id"] = { type: "string" }
schema["_rev"] = { type: "string " }
}
const keys = Object.keys(schema).sort()
// Create bindable properties for each schema field
@ -100,26 +137,33 @@ export const getContextBindings = (rootComponent, componentId) => {
// Replace certain bindings with a new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_count`
runtimeBoundKey = `${key}_text`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
contextBindings.push({
bindings.push({
type: "context",
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
runtimeBoundKey
)}`,
readableBinding: `${component._instanceName}.${table.name}.${key}`,
readableBinding: `${component._instanceName}.${tableName}.${key}`,
// Field schema and provider are required to construct relationship
// datasource options, based on bindable properties
fieldSchema,
providerId: component._id,
tableId: datasource.tableId,
field: key,
})
})
})
// Add logged in user bindings
return bindings
}
/**
* Gets all bindable properties from the logged in user.
*/
const getUserBindings = () => {
let bindings = []
const tables = get(backendUiStore).tables
const userTable = tables.find(table => table._id === TableNames.USERS)
const schema = {
@ -133,53 +177,48 @@ export const getContextBindings = (rootComponent, componentId) => {
// Replace certain bindings with a new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_count`
runtimeBoundKey = `${key}_text`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
contextBindings.push({
bindings.push({
type: "context",
runtimeBinding: `user.${runtimeBoundKey}`,
readableBinding: `Current User.${key}`,
// Field schema and provider are required to construct relationship
// datasource options, based on bindable properties
fieldSchema,
providerId: "user",
tableId: TableNames.USERS,
field: key,
})
})
return contextBindings
return bindings
}
/**
* Gets all bindable components. These are form components which allow their
* values to be bound to.
* Gets all bindable properties from URL parameters.
*/
export const getComponentBindings = rootComponent => {
if (!rootComponent) {
return []
}
const componentSelector = component => {
const type = component._component
const definition = store.actions.components.getDefinition(type)
return definition?.bindable
}
const components = findAllMatchingComponents(rootComponent, componentSelector)
return components.map(component => {
return {
type: "instance",
providerId: component._id,
runtimeBinding: `${makePropSafe(component._id)}`,
readableBinding: `${component._instanceName}`,
const getUrlBindings = asset => {
const url = asset?.routing?.route ?? ""
const split = url.split("/")
let params = []
split.forEach(part => {
if (part.startsWith(":") && part.length > 1) {
params.push(part.replace(/:/g, "").replace(/\?/g, ""))
}
})
return params.map(param => ({
type: "context",
runtimeBinding: `url.${param}`,
readableBinding: `URL.${param}`,
}))
}
/**
* Gets a schema for a datasource object.
*/
export const getSchemaForDatasource = datasource => {
export const getSchemaForDatasource = (datasource, isForm = false) => {
let schema, table
if (datasource) {
const { type } = datasource
@ -193,6 +232,23 @@ export const getSchemaForDatasource = datasource => {
if (table) {
if (type === "view") {
schema = cloneDeep(table.views?.[datasource.name]?.schema)
// Some calc views don't include a "name" property inside the schema
if (schema) {
Object.keys(schema).forEach(field => {
if (!schema[field].name) {
schema[field].name = field
}
})
}
} else if (type === "query" && isForm) {
schema = {}
const params = table.parameters || []
params.forEach(param => {
if (param?.name) {
schema[param.name] = { ...param, type: "string" }
}
})
} else {
schema = cloneDeep(table.schema)
}
@ -201,6 +257,46 @@ export const getSchemaForDatasource = datasource => {
return { schema, table }
}
/**
* Builds a form schema given a form component.
* A form schema is a schema of all the fields nested anywhere within a form.
*/
const buildFormSchema = component => {
let schema = {}
if (!component) {
return schema
}
const def = store.actions.components.getDefinition(component._component)
const fieldSetting = def?.settings?.find(
setting => setting.key === "field" && setting.type.startsWith("field/")
)
if (fieldSetting && component.field) {
const type = fieldSetting.type.split("field/")[1]
if (type) {
schema[component.field] = { name: component.field, type }
}
}
component._children?.forEach(child => {
const childSchema = buildFormSchema(child)
schema = { ...schema, ...childSchema }
})
return schema
}
/**
* Recurses the input object to remove any instances of bindings.
*/
export function removeBindings(obj) {
for (let [key, value] of Object.entries(obj)) {
if (typeof value === "object") {
obj[key] = removeBindings(value)
} else if (typeof value === "string") {
obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding")
}
}
return obj
}
/**
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
*/

View file

@ -30,6 +30,7 @@ export const getBackendUiStore = () => {
const queries = await queriesResponse.json()
const integrationsResponse = await api.get("/api/integrations")
const integrations = await integrationsResponse.json()
const permissionLevels = await store.actions.permissions.fetchLevels()
store.update(state => {
state.selectedDatabase = db
@ -37,6 +38,7 @@ export const getBackendUiStore = () => {
state.datasources = datasources
state.queries = queries
state.integrations = integrations
state.permissionLevels = permissionLevels
return state
})
},
@ -133,6 +135,9 @@ export const getBackendUiStore = () => {
}
query.datasourceId = datasourceId
const response = await api.post(`/api/queries`, query)
if (response.status !== 200) {
throw new Error("Failed saving query.")
}
const json = await response.json()
store.update(state => {
const currentIdx = state.queries.findIndex(
@ -232,7 +237,7 @@ export const getBackendUiStore = () => {
return state
})
},
saveField: ({ originalName, field, primaryDisplay = false }) => {
saveField: ({ originalName, field, primaryDisplay = false, indexes }) => {
store.update(state => {
// delete the original if renaming
// need to handle if the column had no name, empty string
@ -249,6 +254,10 @@ export const getBackendUiStore = () => {
state.draftTable.primaryDisplay = field.name
}
if (indexes) {
state.draftTable.indexes = indexes
}
state.draftTable.schema[field.name] = cloneDeep(field)
store.actions.tables.save(state.draftTable)
return state
@ -324,6 +333,25 @@ export const getBackendUiStore = () => {
return response
},
},
permissions: {
fetchLevels: async () => {
const response = await api.get("/api/permission/levels")
const json = await response.json()
return json
},
forResource: async resourceId => {
const response = await api.get(`/api/permission/${resourceId}`)
const json = await response.json()
return json
},
save: async ({ role, resource, level }) => {
const response = await api.post(
`/api/permission/${role}/${resource}/${level}`
)
const json = await response.json()
return json
},
},
}
return store

View file

@ -15,6 +15,7 @@ import { FrontendTypes } from "constants"
import analytics from "analytics"
import { findComponentType, findComponentParent } from "../storeUtils"
import { uuid } from "../uuid"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = {
apps: [],
@ -408,15 +409,29 @@ export const getFrontendStore = () => {
return state
}
// defines if this is a copy or a cut
const cut = state.componentToPaste.isCut
// immediately need to remove bindings, currently these aren't valid when pasted
if (!cut) {
state.componentToPaste = removeBindings(state.componentToPaste)
}
// Clone the component to paste
// Retain the same ID if cutting as things may be referencing this component
const cut = state.componentToPaste.isCut
delete state.componentToPaste.isCut
let componentToPaste = cloneDeep(state.componentToPaste)
if (cut) {
state.componentToPaste = null
} else {
componentToPaste._id = uuid()
const randomizeIds = component => {
if (!component) {
return
}
component._id = uuid()
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
}
if (mode === "inside") {

View file

@ -9,5 +9,6 @@ const createScreen = () => {
return new Screen()
.mainType("div")
.component("@budibase/standard-components/container")
.instanceName("New Screen")
.json()
}

View file

@ -1,13 +0,0 @@
import { Screen } from "./utils/Screen"
export default {
name: `New Row (Empty)`,
create: () => createScreen(),
}
const createScreen = () => {
return new Screen()
.component("@budibase/standard-components/newrow")
.table("")
.json()
}

View file

@ -1,13 +0,0 @@
import { Screen } from "./utils/Screen"
export default {
name: `Row Detail (Empty)`,
create: () => createScreen(),
}
const createScreen = () => {
return new Screen()
.component("@budibase/standard-components/rowdetail")
.table("")
.json()
}

View file

@ -1,17 +1,12 @@
import newRowScreen from "./newRowScreen"
import rowDetailScreen from "./rowDetailScreen"
import rowListScreen from "./rowListScreen"
import emptyNewRowScreen from "./emptyNewRowScreen"
import createFromScratchScreen from "./createFromScratchScreen"
import emptyRowDetailScreen from "./emptyRowDetailScreen"
const allTemplates = tables => [
createFromScratchScreen,
...newRowScreen(tables),
...rowDetailScreen(tables),
...rowListScreen(tables),
emptyNewRowScreen,
emptyRowDetailScreen,
]
// Allows us to apply common behaviour to all create() functions
@ -22,8 +17,18 @@ const createTemplateOverride = (frontendState, create) => () => {
return screen
}
export default (frontendState, tables) =>
allTemplates(tables).map(template => ({
export default (frontendState, tables) => {
const enrichTemplate = template => ({
...template,
create: createTemplateOverride(frontendState, template.create),
}))
})
const fromScratch = enrichTemplate(createFromScratchScreen)
const tableTemplates = allTemplates(tables).map(enrichTemplate)
return [
fromScratch,
...tableTemplates.sort((templateA, templateB) => {
return templateA.name > templateB.name ? 1 : -1
}),
]
}

View file

@ -1,11 +1,12 @@
import sanitizeUrl from "./utils/sanitizeUrl"
import { Component } from "./utils/Component"
import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component"
import {
makeBreadcrumbContainer,
makeMainContainer,
makeMainForm,
makeTitleContainer,
makeSaveButton,
makeDatasourceFormComponents,
} from "./utils/commonComponents"
export default function(tables) {
@ -21,29 +22,46 @@ export default function(tables) {
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
function generateTitleContainer(table, providerId) {
return makeTitleContainer("New Row").addChild(
makeSaveButton(table, providerId)
)
function generateTitleContainer(table, formId) {
return makeTitleContainer("New Row").addChild(makeSaveButton(table, formId))
}
const createScreen = table => {
const screen = new Screen()
.component("@budibase/standard-components/newrow")
.table(table._id)
.route(newRowUrl(table))
.component("@budibase/standard-components/container")
.instanceName(`${table.name} - New`)
.name("")
.route(newRowUrl(table))
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const form = makeMainForm()
.instanceName("Form")
.customProps({
theme: "spectrum--lightest",
size: "spectrum--medium",
datasource: {
label: table.name,
tableId: table._id,
type: "table",
},
})
const providerId = screen._json.props._id
const container = makeMainContainer()
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
.instanceName("Field Group")
.customProps({
labelPosition: "left",
})
// Add all form fields from this schema to the field group
const datasource = { type: "table", tableId: table._id }
makeDatasourceFormComponents(datasource).forEach(component => {
fieldGroup.addChild(component)
})
// Add all children to the form
const formId = form._json._id
form
.addChild(makeBreadcrumbContainer(table.name, "New"))
.addChild(generateTitleContainer(table, providerId))
.addChild(dataform)
.addChild(generateTitleContainer(table, formId))
.addChild(fieldGroup)
return screen.addChild(container).json()
return screen.addChild(form).json()
}

View file

@ -4,20 +4,19 @@ import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component"
import { makePropSafe } from "@budibase/string-templates"
import {
makeMainContainer,
makeBreadcrumbContainer,
makeTitleContainer,
makeSaveButton,
makeMainForm,
spectrumColor,
makeDatasourceFormComponents,
} from "./utils/commonComponents"
export default function(tables) {
return tables.map(table => {
const heading = table.primaryDisplay
? `{{ data.${makePropSafe(table.primaryDisplay)} }}`
: null
return {
name: `${table.name} - Detail`,
create: () => createScreen(table, heading),
create: () => createScreen(table),
id: ROW_DETAIL_TEMPLATE,
}
})
@ -26,9 +25,9 @@ export default function(tables) {
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
function generateTitleContainer(table, title, providerId) {
function generateTitleContainer(table, title, formId) {
// have to override style for this, its missing margin
const saveButton = makeSaveButton(table, providerId).normalStyle({
const saveButton = makeSaveButton(table, formId).normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
@ -54,6 +53,7 @@ function generateTitleContainer(table, title, providerId) {
background: "transparent",
color: "#4285f4",
})
.customStyle(spectrumColor(700))
.text("Delete")
.customProps({
className: "",
@ -61,8 +61,9 @@ function generateTitleContainer(table, title, providerId) {
onClick: [
{
parameters: {
rowId: `{{ ${makePropSafe(providerId)}._id }}`,
revId: `{{ ${makePropSafe(providerId)}._rev }}`,
providerId: formId,
rowId: `{{ ${makePropSafe(formId)}._id }}`,
revId: `{{ ${makePropSafe(formId)}._rev }}`,
tableId: table._id,
},
"##eventHandlerType": "Delete Row",
@ -82,23 +83,47 @@ function generateTitleContainer(table, title, providerId) {
.addChild(saveButton)
}
const createScreen = (table, heading) => {
const createScreen = table => {
const screen = new Screen()
.component("@budibase/standard-components/rowdetail")
.table(table._id)
.instanceName(`${table.name} - Detail`)
.route(rowDetailUrl(table))
.name("")
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const form = makeMainForm()
.instanceName("Form")
.customProps({
theme: "spectrum--lightest",
size: "spectrum--medium",
datasource: {
label: table.name,
tableId: table._id,
type: "table",
},
})
const providerId = screen._json.props._id
const container = makeMainContainer()
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
.instanceName("Field Group")
.customProps({
labelPosition: "left",
})
// Add all form fields from this schema to the field group
const datasource = { type: "table", tableId: table._id }
makeDatasourceFormComponents(datasource).forEach(component => {
fieldGroup.addChild(component)
})
// Add all children to the form
const formId = form._json._id
const rowDetailId = screen._json.props._id
const heading = table.primaryDisplay
? `{{ ${makePropSafe(rowDetailId)}.${makePropSafe(table.primaryDisplay)} }}`
: null
form
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
.addChild(generateTitleContainer(table, heading || "Edit Row", providerId))
.addChild(dataform)
.addChild(generateTitleContainer(table, heading || "Edit Row", formId))
.addChild(fieldGroup)
return screen.addChild(container).json()
return screen.addChild(form).json()
}

View file

@ -14,17 +14,11 @@ export class Component extends BaseStructure {
active: {},
selected: {},
},
type: "",
_instanceName: "",
_children: [],
}
}
type(type) {
this._json.type = type
return this
}
normalStyle(styling) {
this._json._styles.normal = styling
return this
@ -35,14 +29,25 @@ export class Component extends BaseStructure {
return this
}
text(text) {
this._json.text = text
customStyle(styling) {
this._json._styles.custom = styling
return this
}
// TODO: do we need this
instanceName(name) {
this._json._instanceName = name
return this
}
// Shorthand for custom props "type"
type(type) {
this._json.type = type
return this
}
// Shorthand for custom props "text"
text(text) {
this._json.text = text
return this
}
}

View file

@ -1,5 +1,15 @@
import { Component } from "./Component"
import { rowListUrl } from "../rowListScreen"
import { getSchemaForDatasource } from "../../../dataBinding"
export function spectrumColor(number) {
// Acorn throws a parsing error in this file if the word g-l-o-b-a-l is found
// (without dashes - I can't even type it in a comment).
// God knows why. It seems to think optional chaining further down the
// file is invalid if the word g-l-o-b-a-l is found - hence the reason this
// statement is split into parts.
return "color: var(--spectrum-glo" + `bal-color-gray-${number});`
}
export function makeLinkComponent(tableName) {
return new Component("@budibase/standard-components/link")
@ -10,6 +20,7 @@ export function makeLinkComponent(tableName) {
.hoverStyle({
color: "#4285f4",
})
.customStyle(spectrumColor(700))
.text(tableName)
.customProps({
url: `/${tableName.toLowerCase()}`,
@ -22,13 +33,12 @@ export function makeLinkComponent(tableName) {
})
}
export function makeMainContainer() {
return new Component("@budibase/standard-components/container")
export function makeMainForm() {
return new Component("@budibase/standard-components/form")
.type("div")
.normalStyle({
width: "700px",
padding: "0px",
background: "white",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto",
@ -39,7 +49,7 @@ export function makeMainContainer() {
"padding-left": "48px",
"margin-bottom": "20px",
})
.instanceName("Container")
.instanceName("Form")
}
export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
@ -51,6 +61,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
"margin-right": "4px",
"margin-left": "4px",
})
.customStyle(spectrumColor(700))
.text(">")
.instanceName("Arrow")
@ -63,6 +74,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
const identifierText = new Component("@budibase/standard-components/text")
.type("none")
.normalStyle(textStyling)
.customStyle(spectrumColor(700))
.text(text)
.instanceName("Identifier")
@ -78,7 +90,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
.addChild(identifierText)
}
export function makeSaveButton(table, providerId) {
export function makeSaveButton(table, formId) {
return new Component("@budibase/standard-components/button")
.normalStyle({
background: "#000000",
@ -99,8 +111,14 @@ export function makeSaveButton(table, providerId) {
disabled: false,
onClick: [
{
"##eventHandlerType": "Validate Form",
parameters: {
providerId,
componentId: formId,
},
},
{
parameters: {
providerId: formId,
},
"##eventHandlerType": "Save Row",
},
@ -125,6 +143,7 @@ export function makeTitleContainer(title) {
"margin-left": "0px",
flex: "1 1 auto",
})
.customStyle(spectrumColor(900))
.type("h3")
.instanceName("Title")
.text(title)
@ -142,3 +161,55 @@ export function makeTitleContainer(title) {
.instanceName("Title Container")
.addChild(heading)
}
const fieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
options: "optionsfield",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
}
export function makeDatasourceFormComponents(datasource) {
const { schema } = getSchemaForDatasource(datasource, true)
let components = []
let fields = Object.keys(schema || {})
fields.forEach(field => {
const fieldSchema = schema[field]
// skip autocolumns
if (fieldSchema.autocolumn) {
return
}
const fieldType =
typeof fieldSchema === "object" ? fieldSchema.type : fieldSchema
const componentType = fieldTypeToComponentMap[fieldType]
const fullComponentType = `@budibase/standard-components/${componentType}`
if (componentType) {
const component = new Component(fullComponentType)
.instanceName(field)
.customProps({
field,
label: field,
placeholder: field,
})
if (fieldType === "options") {
component.customProps({ placeholder: "Choose an option " })
}
if (fieldType === "link") {
let placeholder =
fieldSchema.relationshipType === "one-to-many"
? "Choose an option"
: "Choose some options"
component.customProps({ placeholder })
}
if (fieldType === "boolean") {
component.customProps({ text: field, label: "" })
}
components.push(component)
}
})
return components
}

View file

@ -59,8 +59,8 @@ export const findComponentPath = (rootComponent, id, path = []) => {
}
/**
* Recurses through the component tree and finds all components of a certain
* type.
* Recurses through the component tree and finds all components which match
* a certain selector
*/
export const findAllMatchingComponents = (rootComponent, selector) => {
if (!rootComponent || !selector) {
@ -81,6 +81,26 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
return components.reverse()
}
/**
* Finds the closes parent component which matches certain criteria
*/
export const findClosestMatchingComponent = (
rootComponent,
componentId,
selector
) => {
if (!selector) {
return null
}
const componentPath = findComponentPath(rootComponent, componentId).reverse()
for (let component of componentPath) {
if (selector(component)) {
return component
}
}
return null
}
/**
* Recurses through a component tree evaluating a matching function against
* components until a match is found

View file

@ -0,0 +1,55 @@
import { TableNames } from "../constants"
import {
AUTO_COLUMN_DISPLAY_NAMES,
AUTO_COLUMN_SUB_TYPES,
FIELDS,
isAutoColumnUserRelationship,
} from "../constants/backend"
export function getAutoColumnInformation(enabled = true) {
let info = {}
for (let [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
info[subtype] = { enabled, name: AUTO_COLUMN_DISPLAY_NAMES[key] }
}
return info
}
export function buildAutoColumn(tableName, name, subtype) {
let type, constraints
switch (subtype) {
case AUTO_COLUMN_SUB_TYPES.UPDATED_BY:
case AUTO_COLUMN_SUB_TYPES.CREATED_BY:
type = FIELDS.LINK.type
constraints = FIELDS.LINK.constraints
break
case AUTO_COLUMN_SUB_TYPES.AUTO_ID:
type = FIELDS.NUMBER.type
constraints = FIELDS.NUMBER.constraints
break
case AUTO_COLUMN_SUB_TYPES.UPDATED_AT:
case AUTO_COLUMN_SUB_TYPES.CREATED_AT:
type = FIELDS.DATETIME.type
constraints = FIELDS.DATETIME.constraints
break
default:
type = FIELDS.STRING.type
constraints = FIELDS.STRING.constraints
break
}
if (Object.values(AUTO_COLUMN_SUB_TYPES).indexOf(subtype) === -1) {
throw "Cannot build auto column with supplied subtype"
}
const base = {
name,
type,
subtype,
icon: "ri-magic-line",
autocolumn: true,
constraints,
}
if (isAutoColumnUserRelationship(subtype)) {
base.tableId = TableNames.USERS
base.fieldName = `${tableName}-${name}`
}
return base
}

View file

@ -2,7 +2,6 @@
import groupBy from "lodash/fp/groupBy"
import {
TextArea,
Label,
Input,
Heading,
Body,

View file

@ -30,20 +30,22 @@
{#if schemaFields.length}
<div class="schema-fields">
{#each schemaFields as [field, schema]}
{#if schemaHasOptions(schema)}
<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>
{/each}
</Select>
{:else if schema.type === 'string' || schema.type === 'number'}
<BindableInput
extraThin
bind:value={value[field]}
label={field}
type="string"
{bindings} />
{#if !schema.autocolumn}
{#if schemaHasOptions(schema)}
<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>
{/each}
</Select>
{:else if schema.type === 'string' || schema.type === 'number'}
<BindableInput
extraThin
bind:value={value[field]}
label={field}
type="string"
{bindings} />
{/if}
{/if}
{/each}
</div>

View file

@ -5,15 +5,17 @@
import CreateViewButton from "./buttons/CreateViewButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
import EditRolesButton from "./buttons/EditRolesButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
import * as api from "./api"
import Table from "./Table.svelte"
import { TableNames } from "constants"
import CreateEditUser from "./modals/CreateEditUser.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte"
let hideAutocolumns = true
let data = []
let loading = false
$: isUsersTable = $backendUiStore.selectedTable?._id === TableNames.USERS
$: title = $backendUiStore.selectedTable.name
$: schema = $backendUiStore.selectedTable.schema
@ -40,6 +42,7 @@
tableId={$backendUiStore.selectedTable?._id}
{data}
allowEditing={true}
bind:hideAutocolumns
{loading}>
<CreateColumnButton />
{#if schema && Object.keys(schema).length > 0}
@ -47,9 +50,12 @@
title={isUsersTable ? 'Create New User' : 'Create New Row'}
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
<CreateViewButton />
<ManageAccessButton resourceId={$backendUiStore.selectedTable?._id} />
{#if isUsersTable}
<EditRolesButton />
{/if}
<HideAutocolumnButton bind:hideAutocolumns />
<!-- always have the export last -->
<ExportButton view={tableView} />
{/if}
{#if isUsersTable}
<EditRolesButton />
{/if}
</Table>

View file

@ -11,8 +11,9 @@
import { capitalise } from "../../../helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let defaultValue
export let meta
export let value = meta.type === "boolean" ? false : ""
export let value = defaultValue || (meta.type === "boolean" ? false : "")
export let readonly
$: type = meta.type
@ -36,7 +37,9 @@
{:else if type === 'boolean'}
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
{:else if type === 'link'}
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
<div>
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
</div>
{:else if type === 'longform'}
<div>
<Label extraSmall grey>{label}</Label>

View file

@ -24,6 +24,7 @@
export let allowEditing = false
export let loading = false
export let theme = "alpine"
export let hideAutocolumns
let columnDefs = []
let selectedRows = []
@ -85,8 +86,12 @@
return !(isUsersTable && ["email", "roleId"].includes(key))
}
Object.entries(schema || {}).forEach(([key, value]) => {
result.push({
for (let [key, value] of Object.entries(schema || {})) {
// skip autocolumns if hiding
if (hideAutocolumns && value.autocolumn) {
continue
}
let config = {
headerCheckboxSelection: false,
headerComponent: TableHeader,
headerComponentParams: {
@ -107,9 +112,14 @@
autoHeight: true,
resizable: true,
minWidth: 200,
})
})
}
// sort auto-columns to the end if they are present
if (value.autocolumn) {
result.push(config)
} else {
result.unshift(config)
}
}
columnDefs = result
}
@ -150,13 +160,15 @@
</div>
</div>
<div class="grid-wrapper">
<AgGrid
{theme}
{options}
{data}
{columnDefs}
{loading}
on:select={({ detail }) => (selectedRows = detail)} />
{#key columnDefs.length}
<AgGrid
{theme}
{options}
{data}
{columnDefs}
{loading}
on:select={({ detail }) => (selectedRows = detail)} />
{/key}
</div>
<style>
@ -289,4 +301,13 @@
padding-top: var(--spacing-xs);
padding-bottom: var(--spacing-xs);
}
:global(.ag-header) {
height: 61px !important;
min-height: 61px !important;
}
:global(.ag-header-row) {
height: 60px !important;
}
</style>

View file

@ -2,6 +2,7 @@
import { onMount, onDestroy } from "svelte"
import { Modal, ModalContent } from "@budibase/bbui"
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
import { FIELDS } from "constants/backend"
const SORT_ICON_MAP = {
asc: "ri-arrow-down-fill",
@ -51,6 +52,8 @@
column.removeEventListener("sortChanged", setSort)
column.removeEventListener("filterActiveChanged", setFilterActive)
})
$: type = FIELDS[field?.type?.toUpperCase()]?.name
</script>
<header
@ -58,9 +61,17 @@
data-cy="table-header"
on:mouseover={() => (hovered = true)}
on:mouseleave={() => (hovered = false)}>
<div>
<span class="column-header-name">{displayName}</span>
<i class={`${SORT_ICON_MAP[sortDirection]} sort-icon`} />
<div class="column-header">
<div class="column-header-text">
<div class="column-header-name">
{displayName}
{#if field.autocolumn}<i class="auto ri-magic-fill" />{/if}
</div>
{#if type}
<div class="column-header-type">{type}</div>
{/if}
</div>
<i class={`${SORT_ICON_MAP[sortDirection]} icon`} />
</div>
<Modal bind:this={modal}>
<ModalContent
@ -73,11 +84,11 @@
<section class:show={hovered || filterActive}>
{#if editable && hovered}
<span on:click|stopPropagation={showModal}>
<i class="ri-pencil-line" />
<i class="ri-pencil-line icon" />
</span>
{/if}
<span on:click|stopPropagation={toggleMenu} bind:this={menuButton}>
<i class="ri-filter-line" class:active={filterActive} />
<i class="ri-filter-line icon" class:active={filterActive} />
</span>
</section>
</header>
@ -103,6 +114,23 @@
opacity: 1;
}
.column-header {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-s);
}
.column-header-text {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xs);
}
.column-header-name {
white-space: normal !important;
text-overflow: ellipsis;
@ -112,23 +140,31 @@
overflow: hidden;
}
.sort-icon {
position: relative;
top: 2px;
.column-header-type {
font-size: var(--font-size-xs);
color: var(--grey-6);
}
i {
.icon {
transition: 0.2s all;
font-size: var(--font-size-m);
font-weight: 500;
}
.auto {
font-size: 9px;
transition: none;
position: relative;
margin-left: 2px;
top: -3px;
color: var(--grey-6);
}
i:hover {
.icon:hover {
color: var(--blue);
}
i.active,
i:hover {
.icon.active,
.icon:hover {
color: var(--blue);
}
</style>

View file

@ -6,8 +6,11 @@
import GroupByButton from "./buttons/GroupByButton.svelte"
import FilterButton from "./buttons/FilterButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
export let view = {}
let hideAutocolumns = true
let data = []
let loading = false
@ -47,11 +50,18 @@
}
</script>
<Table title={decodeURI(name)} schema={view.schema} {data} {loading}>
<Table
title={decodeURI(name)}
schema={view.schema}
{data}
{loading}
bind:hideAutocolumns>
<FilterButton {view} />
<CalculateButton {view} />
{#if view.calculation}
<GroupByButton {view} />
{/if}
<ManageAccessButton resourceId={decodeURI(name)} />
<HideAutocolumnButton bind:hideAutocolumns />
<ExportButton {view} />
</Table>

View file

@ -0,0 +1,27 @@
<script>
import { TextButton } from "@budibase/bbui"
export let hideAutocolumns
let anchor
let dropdown
function hideOrUnhide() {
hideAutocolumns = !hideAutocolumns
}
</script>
<div bind:this={anchor}>
<TextButton text small on:click={hideOrUnhide}>
{#if hideAutocolumns}
<i class="ri-magic-line" />
Show Auto Columns
{:else}<i class="ri-magic-fill" /> Hide Auto Columns{/if}
</TextButton>
</div>
<style>
i {
margin-right: 4px;
}
</style>

View file

@ -0,0 +1,43 @@
<script>
import { TextButton, Icon, Popover } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { Roles } from "constants/backend"
import api from "builderStore/api"
import ManageAccessPopover from "../popovers/ManageAccessPopover.svelte"
export let resourceId
let anchor
let dropdown
let levels
let permissions
async function openDropdown() {
permissions = await backendUiStore.actions.permissions.forResource(
resourceId
)
levels = await backendUiStore.actions.permissions.fetchLevels()
dropdown.show()
}
</script>
<div bind:this={anchor}>
<TextButton text small on:click={openDropdown}>
<i class="ri-lock-line" />
Manage Access
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<ManageAccessPopover
{resourceId}
{levels}
{permissions}
onClosed={dropdown.hide} />
</Popover>
<style>
i {
margin-right: var(--spacing-xs);
font-size: var(--font-size-s);
}
</style>

View file

@ -3,24 +3,43 @@
export let row
export let selectRelationship
$: count =
row && columnName && Array.isArray(row[columnName])
? row[columnName].length
: 0
$: items = row?.[columnName] || []
</script>
<div class:link={count} on:click={() => selectRelationship(row, columnName)}>
{count}
related row(s)
<div
class="container"
class:link={!!items.length}
on:click={() => selectRelationship(row, columnName)}>
{#each items as item}
<div class="item">{item}</div>
{/each}
</div>
<style>
.link {
text-decoration: underline;
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
}
.link:hover {
color: var(--grey-6);
cursor: pointer;
}
.link:hover .item {
color: var(--ink);
border-color: var(--ink);
}
.item {
font-size: var(--font-size-xs);
padding: var(--spacing-xs) var(--spacing-s);
border: 1px solid var(--grey-5);
color: var(--grey-7);
line-height: normal;
border-radius: 4px;
text-decoration: none;
}
</style>

View file

@ -1,14 +1,25 @@
<script>
import { Input, Button, TextButton, Select, Toggle } from "@budibase/bbui"
import {
Input,
Button,
Label,
TextButton,
Select,
Toggle,
Radio,
} from "@budibase/bbui"
import { cloneDeep } from "lodash/fp"
import { backendUiStore } from "builderStore"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import { FIELDS } from "constants/backend"
import { FIELDS, AUTO_COLUMN_SUB_TYPES } from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import { notifier } from "builderStore/store/notifications"
import ValuesList from "components/common/ValuesList.svelte"
import DatePicker from "components/common/DatePicker.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
const AUTO_COL = "auto"
const LINK_TYPE = FIELDS.LINK.type
let fieldDefinitions = cloneDeep(FIELDS)
export let onClosed
@ -24,6 +35,18 @@
let primaryDisplay =
$backendUiStore.selectedTable.primaryDisplay == null ||
$backendUiStore.selectedTable.primaryDisplay === field.name
let relationshipTypes = [
{ text: "Many to many (N:N)", value: "many-to-many" },
{ text: "One to many (1:N)", value: "one-to-many" },
]
let types = ["Many to many (N:N)", "One to many (1:N)"]
let selectedRelationshipType =
relationshipTypes.find(type => type.value === field.relationshipType)
?.text || "Many to many (N:N)"
let indexes = [...($backendUiStore.selectedTable.indexes || [])]
let confirmDeleteDialog
let deletion
@ -34,13 +57,38 @@
$: uneditable =
$backendUiStore.selectedTable?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(field.name)
$: invalid = field.type === FIELDS.LINK.type && !field.tableId
// used to select what different options can be displayed for column type
$: canBeSearched =
field.type !== LINK_TYPE &&
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY
$: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_COL
$: canBeRequired =
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL
async function saveColumn() {
// Set relationship type if it's
if (field.type === "link") {
field.relationshipType = relationshipTypes.find(
type => type.text === selectedRelationshipType
).value
}
if (field.type === AUTO_COL) {
field = buildAutoColumn(
$backendUiStore.draftTable.name,
field.name,
field.subtype
)
}
backendUiStore.update(state => {
backendUiStore.actions.tables.saveField({
originalName,
field,
primaryDisplay,
indexes,
})
return state
})
@ -57,12 +105,17 @@
}
}
function handleFieldConstraints(event) {
const { type, constraints } = fieldDefinitions[
event.target.value.toUpperCase()
]
field.type = type
field.constraints = constraints
function handleTypeChange(event) {
const definition = fieldDefinitions[event.target.value.toUpperCase()]
if (!definition) {
return
}
field.type = definition.type
field.constraints = definition.constraints
// remove any extra fields that may not be related to this type
delete field.autocolumn
delete field.subtype
delete field.tableId
}
function onChangeRequired(e) {
@ -79,6 +132,18 @@
}
}
function onChangePrimaryIndex(e) {
indexes = e.target.checked ? [field.name] : []
}
function onChangeSecondaryIndex(e) {
if (e.target.checked) {
indexes[1] = field.name
} else {
indexes = indexes.slice(0, 1)
}
}
function confirmDelete() {
confirmDeleteDialog.show()
deletion = true
@ -98,14 +163,15 @@
secondary
thin
label="Type"
on:change={handleFieldConstraints}
on:change={handleTypeChange}
bind:value={field.type}>
{#each Object.values(fieldDefinitions) as field}
<option value={field.type}>{field.name}</option>
{/each}
<option value={AUTO_COL}>Auto Column</option>
</Select>
{#if field.type !== 'link' && !uneditable}
{#if canBeRequired}
<Toggle
checked={required}
on:change={onChangeRequired}
@ -114,12 +180,28 @@
text="Required" />
{/if}
{#if field.type !== 'link'}
{#if canBeDisplay}
<Toggle
bind:checked={primaryDisplay}
on:change={onChangePrimaryDisplay}
thin
text="Use as table display column" />
<Label grey small>Search Indexes</Label>
{/if}
{#if canBeSearched}
<Toggle
checked={indexes[0] === field.name}
disabled={indexes[1] === field.name}
on:change={onChangePrimaryIndex}
thin
text="Primary" />
<Toggle
checked={indexes[1] === field.name}
disabled={!indexes[0] || indexes[0] === field.name}
on:change={onChangeSecondaryIndex}
thin
text="Secondary" />
{/if}
{#if field.type === 'string'}
@ -149,6 +231,20 @@
label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'}
<div>
<Label grey extraSmall>Select relationship type</Label>
<div class="radio-buttons">
{#each types as type}
<Radio
disabled={originalName}
name="Relationship type"
value={type}
bind:group={selectedRelationshipType}>
<label for={type}>{type}</label>
</Radio>
{/each}
</div>
</div>
<Select label="Table" thin secondary bind:value={field.tableId}>
<option value="">Choose an option</option>
{#each tableOptions as table}
@ -159,13 +255,22 @@
label={`Column Name in Other Table`}
thin
bind:value={field.fieldName} />
{:else if field.type === AUTO_COL}
<Select label="Auto Column Type" thin secondary bind:value={field.subtype}>
<option value="">Choose a subtype</option>
{#each Object.entries(getAutoColumnInformation()) as [subtype, info]}
<option value={subtype}>{info.name}</option>
{/each}
</Select>
{/if}
<footer class="create-column-options">
{#if !uneditable && originalName}
{#if !uneditable && originalName != null}
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
{/if}
<Button secondary on:click={onClosed}>Cancel</Button>
<Button primary on:click={saveColumn}>Save Column</Button>
<Button primary on:click={saveColumn} bind:disabled={invalid}>
Save Column
</Button>
</footer>
</div>
<ConfirmDialog
@ -177,6 +282,15 @@
title="Confirm Deletion" />
<style>
label {
display: grid;
place-items: center;
}
.radio-buttons {
display: flex;
gap: var(--spacing-m);
font-size: var(--font-size-xs);
}
.actions {
display: grid;
grid-gap: var(--spacing-xl);

View file

@ -40,9 +40,11 @@
onConfirm={saveRow}>
<ErrorsBox {errors} />
{#each tableSchema as [key, meta]}
<div>
<RowFieldControl {meta} bind:value={row[key]} />
</div>
{#if !meta.autocolumn}
<div>
<RowFieldControl {meta} bind:value={row[key]} />
</div>
{/if}
{/each}
</ModalContent>

View file

@ -29,15 +29,31 @@
let customSchema = { ...schema }
delete customSchema["email"]
delete customSchema["roleId"]
delete customSchema["status"]
return Object.entries(customSchema)
}
const saveRow = async () => {
errors = []
// Do some basic front end validation first
if (!row.email) {
errors = [...errors, { message: "Email is required" }]
}
if (!row.password) {
errors = [...errors, { message: "Password is required" }]
}
if (!row.roleId) {
errors = [...errors, { message: "Role is required" }]
}
if (errors.length) {
return false
}
const rowResponse = await backendApi.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
if (Array.isArray(rowResponse.errors)) {
errors = rowResponse.errors.map(error => ({ message: error }))
@ -47,6 +63,9 @@
.flat()
}
return false
} else if (rowResponse.status === 400 && rowResponse.message) {
errors = [{ message: rowResponse.message }]
return false
}
notifier.success("User saved successfully.")
@ -79,7 +98,13 @@
<option value={role._id}>{role.name}</option>
{/each}
</Select>
<RowFieldControl
meta={{ name: 'status', type: 'options', constraints: { inclusion: ['active', 'inactive'] } }}
bind:value={row.status}
defaultValue={'active'} />
{#each customSchemaKeys as [key, meta]}
<RowFieldControl {meta} bind:value={row[key]} {creating} />
{#if !meta.autocolumn}
<RowFieldControl {meta} bind:value={row[key]} {creating} />
{/if}
{/each}
</ModalContent>

View file

@ -6,7 +6,7 @@
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { backendUiStore } from "builderStore"
let permissions = []
let basePermissions = []
let selectedRole = {}
let errors = []
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
@ -16,9 +16,9 @@
)
$: isCreating = selectedRoleId == null || selectedRoleId === ""
const fetchPermissions = async () => {
const permissionsResponse = await api.get("/api/permissions")
permissions = await permissionsResponse.json()
const fetchBasePermissions = async () => {
const permissionsResponse = await api.get("/api/permission/builtin")
basePermissions = await permissionsResponse.json()
}
// Changes the selected role
@ -81,7 +81,7 @@
}
}
onMount(fetchPermissions)
onMount(fetchBasePermissions)
</script>
<ModalContent
@ -121,11 +121,11 @@
<Select
thin
secondary
label="Permissions"
label="Base Permissions"
bind:value={selectedRole.permissionId}>
<option value="">Choose permissions</option>
{#each permissions as permission}
<option value={permission._id}>{permission.name}</option>
{#each basePermissions as basePerm}
<option value={basePerm._id}>{basePerm.name}</option>
{/each}
</Select>
{/if}

View file

@ -30,7 +30,9 @@
Object.keys(viewTable.schema).filter(
field =>
view.calculation === "count" ||
viewTable.schema[field].type === "number"
// don't want to perform calculations based on auto ID
(viewTable.schema[field].type === "number" &&
!viewTable.schema[field].autocolumn)
)
function saveView() {

View file

@ -0,0 +1,94 @@
<script>
import { onMount } from "svelte"
import { backendUiStore } from "builderStore"
import { Roles } from "constants/backend"
import api from "builderStore/api"
import { notifier } from "builderStore/store/notifications"
import { Button, Label, Input, Select, Spacer } from "@budibase/bbui"
export let resourceId
export let permissions
export let onClosed
async function changePermission(level, role) {
await backendUiStore.actions.permissions.save({
level,
role,
resource: resourceId,
})
// Show updated permissions in UI: REMOVE
permissions = await backendUiStore.actions.permissions.forResource(
resourceId
)
notifier.success("Updated permissions.")
// TODO: update permissions
// permissions[]
}
</script>
<div class="popover">
<h5>Who Can Access This Data?</h5>
<div class="note">
<Label extraSmall grey>
Specify the minimum access level role for this data.
</Label>
</div>
<Spacer large />
<div class="row">
<Label extraSmall grey>Level</Label>
<Label extraSmall grey>Role</Label>
{#each Object.keys(permissions) as level}
<Input secondary thin value={level} disabled={true} />
<Select
secondary
thin
value={permissions[level]}
on:change={e => changePermission(level, e.target.value)}>
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
{/each}
</div>
<Spacer large />
<div class="footer">
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
</div>
<style>
.popover {
display: grid;
width: 400px;
}
h5 {
margin: 0;
font-weight: 500;
}
hr {
margin: var(--spacing-s) 0 var(--spacing-m) 0;
}
.footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
margin-top: var(--spacing-l);
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing-m);
}
.note {
margin-top: 10px;
margin-bottom: 0;
}
</style>

View file

@ -1,15 +1,38 @@
<script>
import { Input, TextArea, Spacer } from "@budibase/bbui"
import { Label, Input, TextArea, Spacer } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
export let integration
export let schema
let unsaved = false
</script>
<form>
{#each Object.keys(integration) as configKey}
<Input
type={integration[configKey].type}
label={configKey}
bind:value={integration[configKey]} />
<Spacer large />
{#each Object.keys(schema) as configKey}
{#if typeof schema[configKey].type === 'object'}
<Label small>{configKey}</Label>
<Spacer small />
<KeyValueBuilder bind:object={integration[configKey]} on:change />
{:else}
<div class="form-row">
<Label small>{configKey}</Label>
<Input
outline
type={schema[configKey].type}
on:change
bind:value={integration[configKey]} />
</div>
{/if}
{/each}
</form>
<style>
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
margin-bottom: var(--spacing-m);
}
</style>

View file

@ -2,7 +2,8 @@
import { onMount } from "svelte"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Input, TextArea, Spacer } from "@budibase/bbui"
import { Input, Label, TextArea, Spacer } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import ICONS from "../icons"
export let integration = {}
@ -49,17 +50,6 @@
</div>
{/each}
</div>
{#if schema}
{#each Object.keys(schema) as configKey}
<Input
thin
type={schema[configKey].type}
label={configKey}
bind:value={integration[configKey]} />
<Spacer medium />
{/each}
{/if}
</section>
<style>

View file

@ -0,0 +1,36 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
viewBox="0 0 120 120"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M103.125 20.625H68.25L60.375
36.375H16.5V99.375H111V20.625H103.125ZM103.125 36.375H72.375L76.5
28.5H103.125V36.375Z"
fill="#FFBA58" />
<path
d="M75 46.875V52.5H60C58.0127 52.5059 56.1085 53.298 54.7033 54.7033C53.298
56.1085 52.5059 58.0127 52.5 60V75H46.875C44.3886 75 42.004 75.9877 40.2459
77.7459C38.4877 79.504 37.5 81.8886 37.5 84.375C37.5 86.8614 38.4877 89.246
40.2459 91.0041C42.004 92.7623 44.3886 93.75 46.875
93.75H52.5V108.75C52.5059 110.737 53.298 112.642 54.7033 114.047C56.1085
115.452 58.0127 116.244 60 116.25H74.25V110.625C74.25 107.94 75.3167 105.364
77.2155 103.466C79.1143 101.567 81.6897 100.5 84.375 100.5C87.0603 100.5
89.6357 101.567 91.5345 103.466C93.4333 105.364 94.5 107.94 94.5
110.625V116.25H108.75C110.737 116.244 112.642 115.452 114.047
114.047C115.452 112.642 116.244 110.737 116.25 108.75V94.5H110.625C107.94
94.5 105.364 93.4333 103.466 91.5345C101.567 89.6357 100.5 87.0603 100.5
84.375C100.5 81.6897 101.567 79.1143 103.466 77.2155C105.364 75.3167 107.94
74.25 110.625 74.25H116.25V60C116.244 58.0127 115.452 56.1085 114.047
54.7033C112.642 53.298 110.737 52.5059 108.75 52.5H93.75V46.875C93.75
44.3886 92.7623 42.004 91.0041 40.2459C89.246 38.4877 86.8614 37.5 84.375
37.5C81.8886 37.5 79.504 38.4877 77.7459 40.2459C75.9877 42.004 75 44.3886
75 46.875Z"
fill="#E76A00" />
</svg>

View file

@ -8,6 +8,7 @@ import Airtable from "./Airtable.svelte"
import SqlServer from "./SQLServer.svelte"
import MySQL from "./MySQL.svelte"
import ArangoDB from "./ArangoDB.svelte"
import Rest from "./Rest.svelte"
export default {
POSTGRES: Postgres,
@ -20,4 +21,5 @@ export default {
AIRTABLE: Airtable,
MYSQL: MySQL,
ARANGODB: ArangoDB,
REST: Rest,
}

View file

@ -1,61 +0,0 @@
<script>
import { goto, params } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui"
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
import analytics from "analytics"
let modal
let error = ""
let name
let source
let integration
let datasource
function checkValid(evt) {
const datasourceName = evt.target.value
if (
$backendUiStore.datasources?.some(
datasource => datasource.name === datasourceName
)
) {
error = `Datasource with name ${tableName} already exists. Please choose another name.`
return
}
error = ""
}
async function saveDatasource() {
const { type, ...config } = integration
// Create datasource
await backendUiStore.actions.datasources.save({
name,
source: type,
config,
})
notifier.success(`Datasource ${name} created successfully.`)
analytics.captureEvent("Datasource Created", { name })
// Navigate to new datasource
$goto(`./datasource/${datasource._id}`)
}
</script>
<ModalContent
title="Create Datasource"
confirmText="Create"
onConfirm={saveDatasource}
disabled={error || !name}>
<Input
data-cy="datasource-name-input"
thin
label="Datasource Name"
on:input={checkValid}
bind:value={name}
{error} />
<Label grey extraSmall>Create Integrated Table from External Source</Label>
<TableIntegrationMenu bind:integration />
</ModalContent>

View file

@ -3,7 +3,6 @@
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let datasource

View file

@ -1,39 +0,0 @@
<script>
import { backendUiStore, store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input, TextButton, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
export let bindable
export let parameters
let anchor
let dropdown
let confirmDeleteDialog
function hideEditor() {
dropdown?.hide()
}
</script>
<div on:click|stopPropagation bind:this={anchor}>
<TextButton text on:click={dropdown.show} active={false}>
<Icon name="add" />
Add Parameters
</TextButton>
<DropdownMenu align="right" {anchor} bind:this={dropdown}>
<div class="wrapper">
<ParameterBuilder bind:parameters {bindable} />
</div>
</DropdownMenu>
</div>
<style>
.wrapper {
padding: var(--spacing-xl);
min-width: 600px;
}
</style>

View file

@ -3,7 +3,6 @@
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let query

View file

@ -2,17 +2,11 @@
import { goto } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import {
Input,
Label,
ModalContent,
Button,
Spacer,
Toggle,
} from "@budibase/bbui"
import { Input, Label, ModalContent, Toggle } from "@budibase/bbui"
import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics"
import screenTemplates from "builderStore/store/screenTemplates"
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen"
import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen"
@ -23,15 +17,28 @@
ROW_LIST_TEMPLATE,
]
$: tableNames = $backendUiStore.tables.map(table => table.name)
let modal
let name
let dataImport
let error = ""
let createAutoscreens = true
let autoColumns = getAutoColumnInformation()
function addAutoColumns(tableName, schema) {
for (let [subtype, col] of Object.entries(autoColumns)) {
if (!col.enabled) {
continue
}
schema[col.name] = buildAutoColumn(tableName, col.name, subtype)
}
return schema
}
function checkValid(evt) {
const tableName = evt.target.value
if ($backendUiStore.models?.some(model => model.name === tableName)) {
if (tableNames.includes(tableName)) {
error = `Table with name ${tableName} already exists. Please choose another name.`
return
}
@ -41,7 +48,7 @@
async function saveTable() {
let newTable = {
name,
schema: dataImport.schema || {},
schema: addAutoColumns(name, dataImport.schema || {}),
dataImport,
}
@ -93,6 +100,28 @@
on:input={checkValid}
bind:value={name}
{error} />
<div class="autocolumns">
<Label extraSmall grey>Auto Columns</Label>
<div class="toggles">
<div class="toggle-1">
<Toggle
text="Created by"
bind:checked={autoColumns.createdBy.enabled} />
<Toggle
text="Created at"
bind:checked={autoColumns.createdAt.enabled} />
<Toggle text="Auto ID" bind:checked={autoColumns.autoID.enabled} />
</div>
<div class="toggle-2">
<Toggle
text="Updated by"
bind:checked={autoColumns.updatedBy.enabled} />
<Toggle
text="Updated at"
bind:checked={autoColumns.updatedAt.enabled} />
</div>
</div>
</div>
<Toggle
text="Generate screens in the design section"
bind:checked={createAutoscreens} />
@ -101,3 +130,25 @@
<TableDataImport bind:dataImport />
</div>
</ModalContent>
<style>
.autocolumns {
padding-bottom: 10px;
border-bottom: 3px solid var(--grey-1);
}
.toggles {
display: flex;
width: 100%;
margin-top: 6px;
}
.toggle-1 :global(> *) {
margin-bottom: 10px;
}
.toggle-2 :global(> *) {
margin-bottom: 10px;
margin-left: 20px;
}
</style>

View file

@ -41,7 +41,7 @@
.icon {
right: 2px;
top: 26px;
top: 5px;
bottom: 2px;
position: absolute;
align-items: center;

View file

@ -0,0 +1,86 @@
<script>
import { Icon, Input, Drawer, Body, Button } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value = ""
export let bindings = []
export let thin = true
export let title = "Bindings"
export let placeholder
let bindingDrawer
$: tempValue = value
$: readableValue = runtimeToReadableBinding(bindings, value)
const handleClose = () => {
onChange(tempValue)
bindingDrawer.hide()
}
const onChange = value => {
dispatch("change", readableToRuntimeBinding(bindings, value))
}
</script>
<div class="control">
<Input
{thin}
value={readableValue}
on:change={event => onChange(event.target.value)}
{placeholder} />
<div class="icon" on:click={bindingDrawer.show}>
<Icon name="lightning" />
</div>
</div>
<Drawer bind:this={bindingDrawer} {title}>
<div slot="description">
<Body extraSmall grey>
Add the objects on the left to enrich your text.
</Body>
</div>
<heading slot="buttons">
<Button thin blue on:click={handleClose}>Save</Button>
</heading>
<div slot="body">
<BindingPanel
value={readableValue}
close={handleClose}
on:update={event => (tempValue = event.detail)}
bindableProperties={bindings} />
</div>
</Drawer>
<style>
.control {
flex: 1;
position: relative;
}
.icon {
right: 2px;
top: 2px;
bottom: 2px;
position: absolute;
align-items: center;
display: flex;
box-sizing: border-box;
padding-left: 7px;
border-left: 1px solid var(--grey-4);
background-color: var(--grey-2);
border-top-right-radius: var(--border-radius-m);
border-bottom-right-radius: var(--border-radius-m);
color: var(--grey-7);
font-size: 14px;
}
.icon:hover {
color: var(--ink);
cursor: pointer;
}
</style>

View file

@ -1,8 +1,8 @@
<script>
export let icon
export let title
export let subtitle
export let disabled
export let subtitle = undefined
export let disabled = false
</script>
<div class="dropdown-item" class:disabled on:click {...$$restProps}>

View file

@ -41,13 +41,29 @@
table.
</Label>
{:else}
<Multiselect
secondary
bind:value={linkedRows}
{label}
placeholder="Choose some options">
{#each rows as row}
<option value={row._id}>{getPrettyName(row)}</option>
{/each}
</Multiselect>
{#if schema.relationshipType === 'one-to-many'}
<Select
thin
secondary
on:change={e => (linkedRows = [e.target.value])}
name={label}
{label}>
<option value="">Choose an option</option>
{#each rows as row}
<option selected={row._id === linkedRows[0]} value={row._id}>
{getPrettyName(row)}
</option>
{/each}
</Select>
{:else}
<Multiselect
secondary
bind:value={linkedRows}
{label}
placeholder="Choose some options">
{#each rows as row}
<option value={row._id}>{getPrettyName(row)}</option>
{/each}
</Multiselect>
{/if}
{/if}

View file

@ -23,7 +23,7 @@
})
</script>
<ModalContent title="Webhook Endpoints" confirmText="Done">
<ModalContent title="Webhook Endpoints" confirmText="OK" showCancelButton={false}>
<p>See below the list of deployed webhook URLs.</p>
{#each webhookUrls as webhookUrl}
<div>

View file

@ -24,7 +24,7 @@
timeOnly: {
hour: "numeric",
minute: "numeric",
hour12: true,
hourCycle: "h12",
},
}
const POLL_INTERVAL = 5000

View file

@ -3,15 +3,21 @@
"datagrid",
"list",
"button",
"search",
{
"name": "Form",
"icon": "ri-file-edit-line",
"children": [
"dataform",
"dataformwide",
"input",
"richtext",
"datepicker"
"form",
"fieldgroup",
"stringfield",
"numberfield",
"optionsfield",
"booleanfield",
"longformfield",
"datetimefield",
"attachmentfield",
"relationshipfield"
]
},
{
@ -55,8 +61,8 @@
"screenslot",
"navigation",
"login",
"rowdetail",
"newrow"
"rowdetail"
]
}
]
]

View file

@ -11,9 +11,6 @@
*, *:before, *:after {
box-sizing: border-box;
}
* {
pointer-events: none;
}
</style>
<script src='/assets/budibase-client.js'></script>
<script>

View file

@ -13,9 +13,8 @@
let dropdown
let anchor
$: noChildrenAllowed =
!component ||
!store.actions.components.getDefinition(component._component)?.hasChildren
$: definition = store.actions.components.getDefinition(component?._component)
$: noChildrenAllowed = !component || !definition?.hasChildren
$: noPaste = !$store.componentToPaste
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
@ -130,7 +129,7 @@
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`}
body={`Are you sure you wish to delete this '${definition?.name}' component?`}
okText="Delete Component"
onOk={deleteComponent} />

View file

@ -24,7 +24,7 @@
$: value && checkValid()
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: dispatch("update", value)
@ -114,8 +114,7 @@
bind:getCaretPosition
thin
bind:value
placeholder="Add text, or click the objects on the left to add them to
the textbox." />
placeholder="Add text, or click the objects on the left to add them to the textbox." />
{#if !valid}
<p class="syntax-error">
Current Handlebars syntax is invalid, please check the guide
@ -144,9 +143,12 @@
}
.text {
padding: var(--spacing-xl);
padding: var(--spacing-l);
font-family: var(--font-sans);
}
.text :global(textarea) {
min-height: 100px;
}
.text :global(p) {
margin: 0;
}

View file

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="attachment" />

View file

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="boolean" />

View file

@ -19,6 +19,7 @@
let drawer
export let value = {}
export let otherSources
$: tables = $backendUiStore.tables.map(m => ({
label: m.name,
@ -46,7 +47,7 @@
type: "query",
}))
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: queryBindableProperties = bindableProperties.map(property => ({
@ -76,7 +77,7 @@
dropdownRight.hide()
}
function fetchDatasourceSchema(query) {
function fetchQueryDefinition(query) {
const source = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId
).source
@ -84,43 +85,48 @@
}
</script>
<div
class="dropdownbutton"
bind:this={anchorRight}
on:click={dropdownRight.show}>
<span>{value?.label ? value.label : 'Choose option'}</span>
<Icon name="arrowdown" />
<div class="container">
<div
class="dropdownbutton"
bind:this={anchorRight}
on:click={dropdownRight.show}>
<span>{value?.label ?? 'Choose option'}</span>
<Icon name="arrowdown" />
</div>
{#if value?.type === 'query'}
<i class="ri-settings-5-line" on:click={drawer.show} />
<Drawer title={'Query Parameters'} bind:this={drawer}>
<div slot="buttons">
<Button
blue
thin
on:click={() => {
notifier.success('Query parameters saved.')
handleSelected(value)
drawer.hide()
}}>
Save
</Button>
</div>
<div class="drawer-contents" slot="body">
{#if value.parameters.length > 0}
<ParameterBuilder
bind:customParams={value.queryParams}
parameters={queries.find(query => query._id === value._id).parameters}
bindings={queryBindableProperties} />
{/if}
<!-- <Spacer large />-->
<IntegrationQueryEditor
height={200}
query={value}
schema={fetchQueryDefinition(value)}
datasource={$backendUiStore.datasources.find(ds => ds._id === value.datasourceId)}
editable={false} />
<Spacer large />
</div>
</Drawer>
{/if}
</div>
{#if value?.type === 'query'}
<i class="ri-settings-5-line" on:click={drawer.show} />
<Drawer title={'Query'} bind:this={drawer}>
<div slot="buttons">
<Button
blue
thin
on:click={() => {
notifier.success('Query parameters saved.')
handleSelected(value)
drawer.hide()
}}>
Save
</Button>
</div>
<div class="drawer-contents" slot="body">
<IntegrationQueryEditor
query={value}
schema={fetchDatasourceSchema(value)}
editable={false} />
<Spacer large />
{#if value.parameters.length > 0}
<ParameterBuilder
bind:customParams={value.queryParams}
parameters={queries.find(query => query._id === value._id).parameters}
bindings={queryBindableProperties} />
{/if}
</div>
</Drawer>
{/if}
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown">
<div class="title">
@ -175,10 +181,33 @@
</li>
{/each}
</ul>
{#if otherSources?.length}
<hr />
<div class="title">
<Heading extraSmall>Other</Heading>
</div>
<ul>
{#each otherSources as source}
<li
class:selected={value === source}
on:click={() => handleSelected(source)}>
{source.label}
</li>
{/each}
</ul>
{/if}
</div>
</DropdownMenu>
<style>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.dropdownbutton {
background-color: var(--grey-2);
border: var(--border-transparent);
@ -241,8 +270,8 @@
}
.drawer-contents {
padding: var(--spacing-xl);
height: 40vh;
padding: var(--spacing-l);
height: calc(40vh - 2 * var(--spacing-l));
overflow-y: auto;
}

View file

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="datetime" />

View file

@ -1,15 +1,6 @@
<script>
import {
Button,
Body,
DropdownMenu,
ModalContent,
Spacer,
} from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import { Button, DropdownMenu, Spacer } from "@budibase/bbui"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
import { automationStore } from "builderStore"
const EVENT_TYPE_KEY = "##eventHandlerType"
@ -17,12 +8,19 @@
let addActionButton
let addActionDropdown
let selectedAction
let selectedAction = actions?.length ? actions[0] : null
$: selectedActionComponent =
selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component
// Select the first action if we delete an action
$: {
if (selectedAction && !actions?.includes(selectedAction)) {
selectedAction = actions?.[0]
}
}
const deleteAction = index => {
actions.splice(index, 1)
actions = actions
@ -51,11 +49,10 @@
<div class="actions-list">
<div>
<div bind:this={addActionButton}>
<Spacer small />
<Button wide secondary on:click={addActionDropdown.show}>
Add Action
</Button>
<Spacer medium />
<Spacer small />
</div>
<DropdownMenu
bind:this={addActionDropdown}
@ -74,11 +71,12 @@
{#if actions && actions.length > 0}
{#each actions as action, index}
<div class="action-container">
<div class="action-header" on:click={selectAction(action)}>
<span class:selected={action === selectedAction}>
{index + 1}.
{action[EVENT_TYPE_KEY]}
</span>
<div
class="action-header"
class:selected={action === selectedAction}
on:click={selectAction(action)}>
{index + 1}.
{action[EVENT_TYPE_KEY]}
</div>
<i
class="ri-close-fill"
@ -107,20 +105,22 @@
margin-top: var(--spacing-m);
}
.action-header > span {
.action-header {
margin-bottom: var(--spacing-m);
font-size: var(--font-size-xs);
color: var(--grey-7);
font-weight: 500;
}
.action-header > span:hover,
.selected {
.action-header:hover,
.action-header.selected {
cursor: pointer;
font-weight: 500;
color: var(--ink);
}
.actions-list {
border-right: var(--border-light);
padding: var(--spacing-s);
padding: var(--spacing-l);
}
.available-action {
@ -136,7 +136,6 @@
.actions-container {
height: 40vh;
display: grid;
grid-gap: var(--spacing-m);
grid-template-columns: 260px 1fr;
grid-auto-flow: column;
min-height: 0;
@ -145,13 +144,16 @@
}
.action-container {
border-top: var(--border-light);
border-bottom: 1px solid var(--grey-1);
display: flex;
align-items: center;
}
.action-container:last-child {
border-bottom: none;
}
.selected-action-container {
padding: var(--spacing-xl);
padding: var(--spacing-l);
}
a {

View file

@ -1,207 +0,0 @@
<script>
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
import { automationStore } from "builderStore"
const dispatch = createEventDispatcher()
const eventTypeKey = "##eventHandlerType"
export let event
let addActionButton
let addActionDropdown
let selectedAction
$: actions = event || []
$: selectedActionComponent =
selectedAction &&
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
const deleteAction = index => {
actions.splice(index, 1)
actions = actions
}
const addAction = actionType => () => {
const newAction = {
parameters: {},
[eventTypeKey]: actionType.name,
}
actions.push(newAction)
selectedAction = newAction
actions = actions
addActionDropdown.hide()
}
const selectAction = action => () => {
selectedAction = action
}
const saveEventData = async () => {
// e.g. The Trigger Automation action exposes beforeSave, so it can
// create any automations it needs to
for (let action of actions) {
if (action[eventTypeKey] === "Trigger Automation") {
await createAutomation(action.parameters)
}
}
dispatch("change", actions)
}
// called by the parent modal when actions are saved
const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) return
await automationStore.actions.create({ name: parameters.newAutomationName })
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
"APP",
appActionDefinition
)
newBlock.inputs = {
fields: Object.entries(parameters.fields).reduce(
(fields, [key, value]) => {
fields[key] = value.type
return fields
},
{}
),
}
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save($automationStore.selectedAutomation)
parameters.automationId = $automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName
}
</script>
<ModalContent title="Actions" confirmText="Save" onConfirm={saveEventData}>
<div slot="header">
<div bind:this={addActionButton}>
<TextButton text small blue on:click={addActionDropdown.show}>
<div style="height: 20px; width: 20px;">
<AddIcon />
</div>
Add Action
</TextButton>
</div>
<DropdownMenu
bind:this={addActionDropdown}
anchor={addActionButton}
align="right">
<div class="available-actions-container">
{#each actionTypes as actionType}
<div class="available-action" on:click={addAction(actionType)}>
<span>{actionType.name}</span>
</div>
{/each}
</div>
</DropdownMenu>
</div>
<div class="actions-container">
{#if actions && actions.length > 0}
{#each actions as action, index}
<div class="action-container">
<div class="action-header" on:click={selectAction(action)}>
<Body small lh>{index + 1}. {action[eventTypeKey]}</Body>
<div class="row-expander" class:rotate={action !== selectedAction}>
<ArrowDownIcon />
</div>
</div>
{#if action === selectedAction}
<div class="selected-action-container">
<svelte:component
this={selectedActionComponent}
parameters={selectedAction.parameters} />
<div class="delete-action-button">
<TextButton text medium on:click={() => deleteAction(index)}>
Delete
</TextButton>
</div>
</div>
{/if}
</div>
{/each}
{/if}
</div>
<div slot="footer">
<a href="https://docs.budibase.com">Learn more about Actions</a>
</div>
</ModalContent>
<style>
.action-header {
display: flex;
flex-direction: row;
align-items: center;
}
.action-header > p {
flex: 1;
}
.row-expander {
height: 30px;
width: 30px;
}
.available-action {
padding: var(--spacing-m);
font-size: var(--font-size-m);
cursor: pointer;
}
.available-action:hover {
background: var(--grey-2);
}
.actions-container {
flex: 1;
min-height: 0;
padding-top: 0;
border: var(--border-light);
border-width: 0 0 1px 0;
overflow-y: auto;
}
.action-container {
border: var(--border-light);
border-width: 1px 0 0 0;
}
.selected-action-container {
padding-bottom: var(--spacing-s);
padding-top: var(--spacing-s);
}
.delete-action-button {
padding-top: var(--spacing-l);
display: flex;
justify-content: flex-end;
flex-direction: row;
}
a {
flex: 1;
color: var(--grey-5);
font-size: var(--font-size-s);
text-decoration: none;
}
a:hover {
color: var(--blue);
}
.rotate :global(svg) {
transform: rotate(90deg);
}
</style>

View file

@ -17,7 +17,9 @@
const automationsToCreate = value.filter(
action => action["##eventHandlerType"] === "Trigger Automation"
)
automationsToCreate.forEach(action => createAutomation(action.parameters))
for (let action of automationsToCreate) {
await createAutomation(action.parameters)
}
dispatch("change", value)
notifier.success("Component actions saved.")
@ -27,11 +29,8 @@
// called by the parent modal when actions are saved
const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) return
await automationStore.actions.create({ name: parameters.newAutomationName })
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
"APP",
@ -39,19 +38,14 @@
)
newBlock.inputs = {
fields: Object.entries(parameters.fields).reduce(
(fields, [key, value]) => {
fields[key] = value.type
return fields
},
{}
),
fields: Object.keys(parameters.fields).reduce((fields, key) => {
fields[key] = "string"
return fields
}, {}),
}
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save($automationStore.selectedAutomation)
parameters.automationId = $automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName
}

View file

@ -10,13 +10,14 @@
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: {
// Automatically set rev and table ID based on row ID
if (parameters.rowId) {
parameters.revId = parameters.rowId.replace("_id", "_rev")
if (parameters.providerId) {
parameters.rowId = `{{ ${parameters.providerId}._id }}`
parameters.revId = `{{ ${parameters.providerId}._rev }}`
const providerComponent = dataProviderComponents.find(
provider => provider._id === parameters.providerId
)
@ -36,13 +37,11 @@
a List
</div>
{:else}
<Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.rowId}>
<Label small>Datasource</Label>
<Select thin secondary bind:value={parameters.providerId}>
<option value="" />
{#each dataProviderComponents as provider}
<option value={`{{ ${provider._id}._id }}`}>
{provider._instanceName}
</option>
<option value={provider._id}>{provider._instanceName}</option>
{/each}
</Select>
{/if}
@ -51,22 +50,15 @@
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
grid-template-columns: auto 1fr;
align-items: baseline;
}
.root :global(> div:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View file

@ -3,63 +3,57 @@
import { store, backendUiStore, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
export let parameters
$: query = $backendUiStore.queries.find(q => q._id === parameters.queryId)
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === parameters.datasourceId
)
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
).map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.runtimeBinding,
}))
)
$: query =
parameters.queryId &&
$backendUiStore.queries.find(query => query._id === parameters.queryId)
function fetchQueryDefinition(query) {
const source = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId
).source
return $backendUiStore.integrations[source].query[query.queryVerb]
}
</script>
<div class="root">
<Label size="m" color="dark">Datasource</Label>
<Select thin secondary bind:value={parameters.datasourceId}>
<Label small>Datasource</Label>
<Select thin secondary bind:value={parameters.datasourceId}>
<option value="" />
{#each $backendUiStore.datasources as datasource}
<option value={datasource._id}>{datasource.name}</option>
{/each}
</Select>
<Spacer medium />
{#if parameters.datasourceId}
<Label small>Query</Label>
<Select thin secondary bind:value={parameters.queryId}>
<option value="" />
{#each $backendUiStore.datasources as datasource}
<option value={datasource._id}>{datasource.name}</option>
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
<option value={query._id}>{query.name}</option>
{/each}
</Select>
{/if}
<Spacer medium />
<Spacer medium />
{#if parameters.datasourceId}
<Label size="m" color="dark">Query</Label>
<Select thin secondary bind:value={parameters.queryId}>
<option value="" />
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
<option value={query._id}>{query.name}</option>
{/each}
</Select>
{/if}
<Spacer medium />
{#if query?.parameters?.length > 0}
<ParameterBuilder
bind:customParams={parameters.queryParams}
parameters={query.parameters}
bindings={bindableProperties} />
{#if query.fields.sql}
<pre>{query.fields.queryString}</pre>
{/if}
{/if}
</div>
<style>
.root {
padding: var(--spacing-m);
}
</style>
{#if query?.parameters?.length > 0}
<ParameterBuilder
bind:customParams={parameters.queryParams}
parameters={query.parameters}
bindings={bindableProperties} />
<IntegrationQueryEditor
height={200}
{query}
schema={fetchQueryDefinition(query)}
editable={false} />
{/if}

View file

@ -1,18 +1,25 @@
<script>
import { DataList, Label } from "@budibase/bbui"
import { allScreens } from "builderStore"
import { Label } from "@budibase/bbui"
import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
export let parameters
let bindingDrawer
let tempValue = parameters.url
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
</script>
<div class="root">
<Label size="m" color="dark">Screen</Label>
<DataList secondary bind:value={parameters.url}>
<option value="" />
{#each $allScreens as screen}
<option value={screen.routing.route}>{screen.props._instanceName}</option>
{/each}
</DataList>
<Label small>Screen</Label>
<DrawerBindableInput
title="Destination URL"
placeholder="/screen"
value={parameters.url}
on:change={value => (parameters.url = value.detail)}
{bindings} />
</div>
<style>

View file

@ -1,115 +1,89 @@
<script>
import {
DataList,
Label,
TextButton,
Spacer,
Select,
Input,
} from "@budibase/bbui"
import { Label, TextButton, Spacer, Select, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import {
getBindableProperties,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import { getBindableProperties } from "builderStore/dataBinding"
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import { createEventDispatcher } from "svelte"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
const dispatch = createEventDispatcher()
export let parameterFields
export let schemaFields
export let fieldLabel = "Column"
export let valueLabel = "Value"
const emptyField = () => ({ name: "", value: "" })
let fields = Object.entries(parameterFields || {})
$: onChange(fields)
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
// this statement initialises fields from parameters.fields
$: fields =
fields ||
Object.keys(parameterFields || { "": "" }).map(name => ({
name,
value:
(parameterFields &&
runtimeToReadableBinding(
bindableProperties,
parameterFields[name].value
)) ||
"",
}))
const addField = () => {
const newFields = fields.filter(f => f.name)
newFields.push(emptyField())
fields = newFields
rebuildParameters()
fields = [...fields.filter(field => field[0]), ["", ""]]
}
const removeField = field => () => {
fields = fields.filter(f => f !== field)
rebuildParameters()
const removeField = name => {
fields = fields.filter(field => field[0] !== name)
}
const rebuildParameters = () => {
// rebuilds paramters.fields every time a field name or value is added
// as UI below is bound to "fields" array, but we need to output a { key: value }
const newParameterFields = {}
for (let field of fields) {
if (field.name) {
// value and type is needed by the client, so it can parse
// a string into a correct type
newParameterFields[field.name] = {
type: schemaFields
? schemaFields.find(f => f.name === field.name).type
: "string",
value: readableToRuntimeBinding(bindableProperties, field.value),
}
}
}
dispatch("fieldschanged", newParameterFields)
const updateFieldValue = (idx, value) => {
fields[idx][1] = value
fields = fields
}
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
const updateFieldName = (idx, name) => {
fields[idx][0] = name
fields = fields
}
const onChange = fields => {
const newParamFields = {}
fields
.filter(field => field[0])
.forEach(([field, value]) => {
newParamFields[field] = value
})
dispatch("change", newParamFields)
}
</script>
{#if fields}
{#each fields as field}
<Label size="m" color="dark">{fieldLabel}</Label>
{#each fields as field, idx}
<Label small>{fieldLabel}</Label>
{#if schemaFields}
<Select secondary bind:value={field.name} on:blur={rebuildParameters}>
<Select
thin
secondary
value={field[0]}
on:change={event => updateFieldName(idx, event.target.value)}>
<option value="" />
{#each schemaFields as schemaField}
<option value={schemaField.name}>{schemaField.name}</option>
{/each}
</Select>
{:else}
<Input secondary bind:value={field.name} on:blur={rebuildParameters} />
<Input
thin
secondary
value={field[0]}
on:change={event => updateFieldName(idx, event.target.value)} />
{/if}
<Label size="m" color="dark">Value</Label>
<DataList secondary bind:value={field.value} on:blur={rebuildParameters}>
<option value="" />
{#each bindableProperties as bindableProp}
<option value={toBindingExpression(bindableProp.readableBinding)}>
{bindableProp.readableBinding}
</option>
{/each}
</DataList>
<Label small>{valueLabel}</Label>
<DrawerBindableInput
title={`Value for "${field[0]}"`}
value={field[1]}
bindings={bindableProperties}
on:change={event => updateFieldValue(idx, event.detail)} />
<div class="remove-field-container">
<TextButton text small on:click={removeField(field)}>
<TextButton text small on:click={() => removeField(field[0])}>
<CloseCircleIcon />
</TextButton>
</div>
{/each}
<div>
<Spacer small />
<TextButton text small blue on:click={addField}>
Add
{fieldLabel}

View file

@ -11,7 +11,7 @@
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: providerComponent = dataProviderComponents.find(
@ -37,8 +37,8 @@
Repeater
</div>
{:else}
<Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.providerId}>
<Label small>Datasource</Label>
<Select thin secondary bind:value={parameters.providerId}>
<option value="" />
{#each dataProviderComponents as provider}
<option value={provider._id}>{provider._instanceName}</option>
@ -49,7 +49,7 @@
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:fieldschanged={onFieldsChanged} />
on:change={onFieldsChanged} />
{/if}
{/if}
</div>
@ -57,7 +57,7 @@
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
@ -71,8 +71,6 @@
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View file

@ -27,13 +27,11 @@
schema,
}
})
$: hasAutomations = automations && automations.length > 0
$: selectedAutomation =
parameters &&
parameters.automationId &&
automations.find(a => a._id === parameters.automationId)
$: selectedAutomation = automations?.find(
a => a._id === parameters?.automationId
)
$: selectedSchema = selectedAutomation?.schema
const onFieldsChanged = e => {
parameters.fields = e.detail
@ -42,95 +40,98 @@
const setNew = () => {
automationStatus = AUTOMATION_STATUS.NEW
parameters.automationId = undefined
parameters.fields = {}
}
const setExisting = () => {
automationStatus = AUTOMATION_STATUS.EXISTING
parameters.newAutomationName = ""
parameters.fields = {}
parameters.automationId = automations[0]?._id
}
</script>
<div class="root">
<div class="radio-container" on:click={setNew}>
<input
type="radio"
value={AUTOMATION_STATUS.NEW}
bind:group={automationStatus}
disabled={!hasAutomations} />
<Label disabled={!hasAutomations}>Create a new automation</Label>
<div class="radios">
<div class="radio-container" on:click={setNew}>
<input
type="radio"
value={AUTOMATION_STATUS.NEW}
bind:group={automationStatus} />
<Label small>Create a new automation</Label>
</div>
<div class="radio-container" on:click={hasAutomations ? setExisting : null}>
<input
type="radio"
value={AUTOMATION_STATUS.EXISTING}
bind:group={automationStatus}
disabled={!hasAutomations} />
<Label small grey={!hasAutomations}>Use an existing automation</Label>
</div>
</div>
<div class="radio-container" on:click={setExisting}>
<input
type="radio"
value={AUTOMATION_STATUS.EXISTING}
bind:group={automationStatus}
disabled={!hasAutomations} />
<div class="fields">
<Label small>Automation</Label>
<Label disabled={!hasAutomations}>Use an existing automation</Label>
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
<Select
thin
secondary
bind:value={parameters.automationId}
placeholder="Choose automation">
{#each automations as automation}
<option value={automation._id}>{automation.name}</option>
{/each}
</Select>
{:else}
<Input
thin
bind:value={parameters.newAutomationName}
placeholder="Enter automation name" />
{/if}
{#key parameters.automationId}
<SaveFields
schemaFields={selectedSchema}
parameterFields={parameters.fields}
fieldLabel="Field"
on:change={onFieldsChanged} />
{/key}
</div>
<Label size="m" color="dark">Automation</Label>
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
<Select
secondary
bind:value={parameters.automationId}
placeholder="Choose automation">
<option value="" />
{#each automations as automation}
<option value={automation._id}>{automation.name}</option>
{/each}
</Select>
{:else}
<Input
secondary
bind:value={parameters.newAutomationName}
placeholder="Enter automation name" />
{/if}
<SaveFields
schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema}
fieldLabel="Field"
on:fieldschanged={onFieldsChanged} />
</div>
<style>
.root {
.fields {
display: grid;
column-gap: var(--spacing-s);
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(> div:nth-child(4)) {
.fields :global(> div:nth-child(2)) {
grid-column: 2 / span 4;
}
.radios,
.radio-container {
display: grid;
grid-template-columns: auto 1fr;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.radios {
gap: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
.radio-container {
gap: var(--spacing-m);
}
.radio-container :global(label) {
margin: 0;
}
.radio-container:nth-child(1) {
grid-column: 1 / span 2;
}
.radio-container:nth-child(2) {
grid-column: 3 / span 3;
}
.radio-container :global(> label) {
margin-left: var(--spacing-m);
}
.radio-container > input {
margin-bottom: var(--spacing-s);
}
.radio-container > input:focus {
outline: none;
input[type="radio"]:checked {
background: var(--blue);
}
</style>

View file

@ -0,0 +1,38 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding"
export let parameters
$: actionProviders = getActionProviderComponents(
$currentAsset,
$store.selectedComponentId,
"ValidateForm"
)
</script>
<div class="root">
<Label small>Form</Label>
<Select thin secondary bind:value={parameters.componentId}>
<option value="" />
{#if actionProviders}
{#each actionProviders as component}
<option value={component._id}>{component._instanceName}</option>
{/each}
{/if}
</Select>
</div>
<style>
.root {
display: flex;
flex-direction: row;
align-items: baseline;
}
.root :global(> div) {
flex: 1;
margin-left: var(--spacing-l);
}
</style>

View file

@ -3,6 +3,7 @@ import SaveRow from "./SaveRow.svelte"
import DeleteRow from "./DeleteRow.svelte"
import ExecuteQuery from "./ExecuteQuery.svelte"
import TriggerAutomation from "./TriggerAutomation.svelte"
import ValidateForm from "./ValidateForm.svelte"
// defines what actions are available, when adding a new one
// the component is the setup panel for the action
@ -30,4 +31,8 @@ export default [
name: "Trigger Automation",
component: TriggerAutomation,
},
{
name: "Validate Form",
component: ValidateForm,
},
]

View file

@ -0,0 +1,77 @@
<script>
import { Button, Drawer, Spacer, Body } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { notifier } from "builderStore/store/notifications"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import SaveFields from "./EventsEditor/actions/SaveFields.svelte"
const dispatch = createEventDispatcher()
export let value = {}
export let componentInstance
let drawer
let tempValue = value
$: schemaFields = getSchemaFields(componentInstance)
const getSchemaFields = component => {
const datasource = getDatasourceForProvider(component)
const { schema } = getSchemaForDatasource(datasource)
return Object.values(schema || {})
}
const saveFilter = async () => {
dispatch("change", tempValue)
notifier.success("Filters saved.")
drawer.hide()
}
const onFieldsChanged = event => {
tempValue = event.detail
}
</script>
<Button secondary wide on:click={drawer.show}>Define Filters</Button>
<Drawer bind:this={drawer} title={'Filtering'}>
<heading slot="buttons">
<Button thin blue on:click={saveFilter}>Save</Button>
</heading>
<div slot="body">
<div class="root">
<Body small grey>
{#if !Object.keys(tempValue || {}).length}
Add your first filter column.
{:else}
Results are filtered to only those which match all of the following
constaints.
{/if}
</Body>
<Spacer medium />
<div class="fields">
<SaveFields
parameterFields={value}
{schemaFields}
valueLabel="Equals"
on:change={onFieldsChanged} />
</div>
</div>
</div>
</Drawer>
<style>
.root {
padding: var(--spacing-l);
min-height: calc(40vh - 2 * var(--spacing-l));
}
.fields {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
</style>

View file

@ -0,0 +1,59 @@
<script>
import { DataList } from "@budibase/bbui"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/storeUtils"
export let componentInstance
export let value
export let onChange
export let type
$: form = findClosestMatchingComponent(
$currentAsset.props,
componentInstance._id,
component => component._component === "@budibase/standard-components/form"
)
$: datasource = getDatasourceForProvider(form)
$: schema = getSchemaForDatasource(datasource, true).schema
$: options = getOptions(schema, type)
const getOptions = (schema, fieldType) => {
let entries = Object.entries(schema ?? {})
if (fieldType) {
entries = entries.filter(entry => entry[1].type === fieldType)
}
return entries.map(entry => entry[0])
}
const handleBlur = () => onChange(value)
</script>
<div>
<DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" />
{#each options as option}
<option value={option}>{option}</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

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="longform" />

View file

@ -0,0 +1,5 @@
<script>
import FieldSelect from "./FieldSelect.svelte"
</script>
<FieldSelect {...$$props} multiselect />

View file

@ -1,5 +0,0 @@
<script>
import TableViewFieldSelect from "./TableViewFieldSelect.svelte"
</script>
<TableViewFieldSelect {...$$props} multiselect />

View file

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="number" />

View file

@ -106,7 +106,9 @@
}
$: displayLabel =
selectedOption && selectedOption.label ? selectedOption.label : value || ""
selectedOption && selectedOption.label
? selectedOption.label
: value || "Choose option"
</script>
<div
@ -129,11 +131,16 @@
on:keydown={handleEscape}
class="bb-select-menu">
<ul>
<li
on:click|self={() => handleClick(null)}
class:selected={value == null || value === ''}>
Choose option
</li>
{#if isOptionsObject}
{#each options as { value: v, label }}
<li
{...handleStyleBind(v)}
on:click|self={handleClick(v)}
on:click|self={() => handleClick(v)}
class:selected={value === v}>
{label}
</li>
@ -142,7 +149,7 @@
{#each options as v}
<li
{...handleStyleBind(v)}
on:click|self={handleClick(v)}
on:click|self={() => handleClick(v)}
class:selected={value === v}>
{v}
</li>

View file

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="options" />

View file

@ -7,6 +7,7 @@
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
import { capitalise } from "../../../../helpers"
export let label = ""
export let bindable = true
@ -24,7 +25,7 @@
let valid
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
@ -46,6 +47,11 @@
innerVal = value.target.value
}
}
if (type === "number") {
innerVal = parseInt(innerVal)
}
if (typeof innerVal === "string") {
onChange(replaceBindings(innerVal))
} else {
@ -72,6 +78,7 @@
value={safeValue}
on:change={handleChange}
onChange={handleChange}
{type}
{...props}
name={key} />
</div>
@ -82,26 +89,26 @@
on:click={bindingDrawer.show}>
<Icon name="lightning" />
</div>
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
<div slot="description">
<Body extraSmall grey>
Add the objects on the left to enrich your text.
</Body>
</div>
<heading slot="buttons">
<Button thin blue disabled={!valid} on:click={handleClose}>Save</Button>
</heading>
<div slot="body">
<BindingPanel
bind:valid
value={safeValue}
close={handleClose}
on:update={e => (temporaryBindableValue = e.detail)}
{bindableProperties} />
</div>
</Drawer>
{/if}
</div>
<Drawer bind:this={bindingDrawer} title="Bindings">
<div slot="description">
<Body extraSmall grey>
Add the objects on the left to enrich your text.
</Body>
</div>
<heading slot="buttons">
<Button thin blue disabled={!valid} on:click={handleClose}>Save</Button>
</heading>
<div slot="body">
<BindingPanel
bind:valid
value={safeValue}
close={handleClose}
on:update={e => (temporaryBindableValue = e.detail)}
{bindableProperties} />
</div>
</Drawer>
<style>
.property-control {
@ -138,7 +145,7 @@
align-items: center;
display: flex;
box-sizing: border-box;
padding-left: var(--spacing-xs);
padding-left: 7px;
border-left: 1px solid var(--grey-4);
background-color: var(--grey-2);
border-top-right-radius: var(--border-radius-m);

View file

@ -25,7 +25,7 @@
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
{#if open}
<div>
{#each properties as prop}
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
<PropertyControl
bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}

View file

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="link" />

View file

@ -0,0 +1,7 @@
<script>
import DatasourceSelect from "./DatasourceSelect.svelte"
const otherSources = [{ name: "Custom", label: "Custom" }]
</script>
<DatasourceSelect on:change {...$$props} {otherSources} />

View file

@ -1,80 +0,0 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, allScreens, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
export let value = ""
$: urls = getUrls($allScreens, $currentAsset, $store.selectedComponentId)
// Update value on blur
const dispatch = createEventDispatcher()
const handleBlur = () => dispatch("change", value)
// Get all valid screen URL, as well as detail screens which can be used in
// the current data context
const getUrls = (screens, asset, componentId) => {
// Get all screens which aren't detail screens
let urls = screens
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({
name: screen.props._instanceName,
url: screen.routing.route,
sort: screen.props._component,
}))
// Add detail screens enriched with the current data context
const bindableProperties = getBindableProperties(asset.props, componentId)
screens
.filter(screen => screen.props._component.endsWith("/rowdetail"))
.forEach(detailScreen => {
// Find any _id bindings that match the detail screen's table
const binding = bindableProperties.find(p => {
return (
p.type === "context" &&
p.runtimeBinding.endsWith("._id") &&
p.tableId === detailScreen.props.table
)
})
if (binding) {
urls.push({
name: detailScreen.props._instanceName,
url: detailScreen.routing.route.replace(
":id",
`{{ ${binding.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

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="string" />

View file

@ -1,22 +1,35 @@
<script>
import { get } from "lodash"
import { isEmpty } from "lodash/fp"
import { Button } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { currentAsset } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/storeUtils"
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import Input from "./PropertyControls/Input.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
import OptionSelect from "./PropertyControls/OptionSelect.svelte"
import MultiTableViewFieldSelect from "./PropertyControls/MultiTableViewFieldSelect.svelte"
import Checkbox from "./PropertyControls/Checkbox.svelte"
import TableSelect from "./PropertyControls/TableSelect.svelte"
import TableViewSelect from "./PropertyControls/TableViewSelect.svelte"
import TableViewFieldSelect from "./PropertyControls/TableViewFieldSelect.svelte"
import DatasourceSelect from "./PropertyControls/DatasourceSelect.svelte"
import FieldSelect from "./PropertyControls/FieldSelect.svelte"
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
import EventsEditor from "./PropertyControls/EventsEditor"
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
import FilterEditor from "./PropertyControls/FilterEditor.svelte"
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
import { IconSelect } from "./PropertyControls/IconSelect"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
import BooleanFieldSelect from "./PropertyControls/BooleanFieldSelect.svelte"
import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte"
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
export let componentDefinition = {}
export let componentInstance = {}
@ -39,6 +52,7 @@
"layoutId",
"routing.roleId",
]
let confirmResetFieldsDialog
$: settings = componentDefinition?.settings ?? []
$: isLayout = assetInstance && assetInstance.favicon
@ -47,8 +61,7 @@
const controlMap = {
text: Input,
select: OptionSelect,
datasource: TableViewSelect,
screen: ScreenSelect,
datasource: DatasourceSelect,
detailScreen: DetailScreenSelect,
boolean: Checkbox,
number: Input,
@ -56,8 +69,18 @@
table: TableSelect,
color: ColorPicker,
icon: IconSelect,
field: TableViewFieldSelect,
multifield: MultiTableViewFieldSelect,
field: FieldSelect,
multifield: MultiFieldSelect,
schema: SchemaSelect,
filter: FilterEditor,
"field/string": StringFieldSelect,
"field/number": NumberFieldSelect,
"field/options": OptionsFieldSelect,
"field/boolean": BooleanFieldSelect,
"field/longform": LongFormFieldSelect,
"field/datetime": DateTimeFieldSelect,
"field/attachment": AttachmentFieldSelect,
"field/link": RelationshipFieldSelect,
}
const getControl = type => {
@ -78,6 +101,20 @@
const onInstanceNameChange = name => {
onChange("_instanceName", name)
}
const resetFormFields = () => {
const form = findClosestMatchingComponent(
$currentAsset.props,
componentInstance._id,
component => component._component.endsWith("/form")
)
const datasource = form?.datasource
const fields = makeDatasourceFormComponents(datasource)
onChange(
"_children",
fields.map(field => field.json())
)
}
</script>
<div class="settings-view-container">
@ -114,7 +151,7 @@
value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue}
{componentInstance}
onChange={val => onChange(setting.key, val)}
props={{ options: setting.options }} />
props={{ options: setting.options, placeholder: setting.placeholder }} />
{/if}
{/each}
{:else}
@ -122,7 +159,19 @@
This component doesn't have any additional settings.
</div>
{/if}
{#if componentDefinition?.component?.endsWith('/fieldgroup')}
<Button secondary wide on:click={() => confirmResetFieldsDialog?.show()}>
Reset Fields
</Button>
{/if}
</div>
<ConfirmDialog
bind:this={confirmResetFieldsDialog}
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to reset this Field Group?`}
okText="Reset"
onOk={resetFormFields}
title="Confirm Reset Fields" />
<style>
.settings-view-container {

View file

@ -9,7 +9,6 @@ export const layout = [
key: "display",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Block", value: "block" },
{ label: "Inline Block", value: "inline-block" },
{ label: "Flex", value: "flex" },
@ -37,7 +36,6 @@ export const layout = [
key: "justify-content",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
@ -51,7 +49,6 @@ export const layout = [
key: "align-items",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
@ -64,7 +61,6 @@ export const layout = [
key: "flex-wrap",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Wrap", value: "wrap" },
{ label: "No wrap", value: "nowrap" },
],
@ -74,7 +70,6 @@ export const layout = [
key: "gap",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -93,7 +88,6 @@ export const margin = [
key: "margin",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -113,7 +107,6 @@ export const margin = [
key: "margin-top",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -133,7 +126,6 @@ export const margin = [
key: "margin-right",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -153,7 +145,6 @@ export const margin = [
key: "margin-bottom",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -173,7 +164,6 @@ export const margin = [
key: "margin-left",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -196,7 +186,6 @@ export const padding = [
key: "padding",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -214,7 +203,6 @@ export const padding = [
key: "padding-top",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -232,7 +220,6 @@ export const padding = [
key: "padding-right",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -250,7 +237,6 @@ export const padding = [
key: "padding-bottom",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -268,7 +254,6 @@ export const padding = [
key: "padding-left",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -289,7 +274,6 @@ export const size = [
key: "flex",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Shrink", value: "0 1 auto" },
{ label: "Grow", value: "1 1 auto" },
],
@ -338,7 +322,6 @@ export const position = [
key: "position",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Static", value: "static" },
{ label: "Relative", value: "relative" },
{ label: "Fixed", value: "fixed" },
@ -375,7 +358,6 @@ export const position = [
key: "z-index",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "-9999", value: "-9999" },
{ label: "-3", value: "-3" },
{ label: "-2", value: "-2" },
@ -395,7 +377,6 @@ export const typography = [
key: "font-family",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Arial", value: "Arial" },
{ label: "Arial Black", value: "Arial Black" },
{ label: "Cursive", value: "Cursive" },
@ -418,7 +399,6 @@ export const typography = [
key: "font-weight",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "200", value: "200" },
{ label: "300", value: "300" },
{ label: "400", value: "400" },
@ -434,7 +414,6 @@ export const typography = [
key: "font-size",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "8px", value: "8px" },
{ label: "10px", value: "10px" },
{ label: "12px", value: "12px" },
@ -454,7 +433,6 @@ export const typography = [
key: "line-height",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "1", value: "1" },
{ label: "1.25", value: "1.25" },
{ label: "1.5", value: "1.5" },
@ -496,7 +474,6 @@ export const typography = [
key: "text-decoration-line",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Underline", value: "underline" },
{ label: "Overline", value: "overline" },
{ label: "Line-through", value: "line-through" },
@ -516,7 +493,6 @@ export const background = [
key: "background-image",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" },
{
label: "Warm Flame",
@ -603,7 +579,6 @@ export const border = [
key: "border-radius",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" },
{ label: "X Small", value: "0.125rem" },
{ label: "Small", value: "0.25rem" },
@ -619,7 +594,6 @@ export const border = [
key: "border-width",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" },
{ label: "X Small", value: "0.5px" },
{ label: "Small", value: "1px" },
@ -638,7 +612,6 @@ export const border = [
key: "border-style",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" },
{ label: "Hidden", value: "hidden" },
{ label: "Dotted", value: "dotted" },
@ -659,7 +632,6 @@ export const effects = [
key: "opacity",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "0", value: "0" },
{ label: "0.2", value: "0.2" },
{ label: "0.4", value: "0.4" },
@ -673,7 +645,6 @@ export const effects = [
key: "transform",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" },
{ label: "45 deg", value: "rotate(45deg)" },
{ label: "90 deg", value: "rotate(90deg)" },
@ -690,7 +661,6 @@ export const effects = [
key: "box-shadow",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" },
{ label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
{
@ -723,7 +693,6 @@ export const transitions = [
key: "transition-property",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" },
{ label: "All", value: "all" },
{ label: "Background Color", value: "background color" },
@ -745,7 +714,6 @@ export const transitions = [
control: OptionSelect,
placeholder: "sec",
options: [
{ label: "Choose option", value: "" },
{ label: "0.4s", value: "0.4s" },
{ label: "0.6s", value: "0.6s" },
{ label: "0.8s", value: "0.8s" },
@ -759,7 +727,6 @@ export const transitions = [
key: "transition-timing-function",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Linear", value: "linear" },
{ label: "Ease", value: "ease" },
{ label: "Ease in", value: "ease-in" },

View file

@ -0,0 +1,50 @@
<script>
import { Button, Input } from "@budibase/bbui"
export let object = {}
export let readOnly
let fields = Object.entries(object).map(([name, value]) => ({ name, value }))
$: object = fields.reduce(
(acc, next) => ({ ...acc, [next.name]: next.value }),
{}
)
function addEntry() {
fields = [...fields, {}]
}
function deleteEntry(idx) {
fields.splice(idx, 1)
fields = fields
}
</script>
<!-- Builds Objects with Key Value Pairs. Useful for building things like Request Headers. -->
<div class="container" class:readOnly>
{#each fields as field, idx}
<Input placeholder="Key" thin outline bind:value={field.name} />
<Input placeholder="Value" thin outline bind:value={field.value} />
{#if !readOnly}
<i class="ri-close-circle-fill" on:click={() => deleteEntry(idx)} />
{/if}
{/each}
</div>
{#if !readOnly}
<Button secondary thin outline on:click={addEntry}>Add</Button>
{/if}
<style>
.container {
display: grid;
grid-template-columns: 1fr 1fr 20px;
grid-gap: var(--spacing-m);
align-items: center;
margin-bottom: var(--spacing-m);
}
.ri-close-circle-fill {
cursor: pointer;
}
</style>

View file

@ -1,5 +1,6 @@
<script>
import CodeMirror from "./codemirror"
import { Label, Spacer } from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte"
import { themeStore } from "builderStore"
import { handlebarsCompletions } from "constants/completions"
@ -11,11 +12,13 @@
LIGHT: "default",
}
export let label
export let value = ""
export let readOnly = false
export let lineNumbers = true
export let tab = true
export let mode
export let editorHeight = 500
// export let parameters = []
let completions = handlebarsCompletions()
@ -169,15 +172,21 @@
}
</script>
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
{#if label}
<Label small>{label}</Label>
<Spacer medium />
{/if}
<div style={`--code-mirror-height: ${editorHeight}px`}>
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
</div>
<style>
textarea {
visibility: hidden;
}
:global(.CodeMirror) {
height: 500px !important;
div :global(.CodeMirror) {
height: var(--code-mirror-height) !important;
border-radius: var(--border-radius-s);
font-family: monospace !important;
line-height: 1.3;

View file

@ -1,13 +1,7 @@
<script>
import {
Button,
TextArea,
Label,
Input,
Heading,
Select,
} from "@budibase/bbui"
import { Label, Spacer, Input } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte"
import KeyValueBuilder from "./KeyValueBuilder.svelte"
export let fields = {}
export let schema
@ -26,13 +20,33 @@
<form on:submit|preventDefault>
<div class="field">
{#each schemaKeys as field}
<Input
placeholder="Enter {field} name"
outline
disabled={!editable}
type={schema.fields[field]?.type}
required={schema.fields[field]?.required}
bind:value={fields[field]} />
{#if schema.fields[field]?.type === 'object'}
<div>
<Label small>{field}</Label>
<Spacer small />
<KeyValueBuilder readOnly={!editable} bind:object={fields[field]} />
</div>
{:else if schema.fields[field]?.type === 'json'}
<div>
<Label extraSmall grey>{field}</Label>
<Editor
mode="json"
on:change={({ detail }) => (fields[field] = detail.value)}
readOnly={!editable}
value={fields[field]} />
</div>
{:else}
<div class="horizontal">
<Label small>{field}</Label>
<Input
placeholder="Enter {field}"
outline
disabled={!editable}
type={schema.fields[field]?.type}
required={schema.fields[field]?.required}
bind:value={fields[field]} />
</div>
{/if}
{/each}
</div>
</form>
@ -49,8 +63,15 @@
.field {
margin-bottom: var(--spacing-m);
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr;
grid-gap: var(--spacing-m);
align-items: center;
}
.horizontal {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View file

@ -1,10 +1,10 @@
<script>
import { Button, Input, Heading, Spacer } from "@budibase/bbui"
import BindableInput from "components/common/BindableInput.svelte"
import { Body, Button, Input, Heading, Spacer } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
export let bindable = true
export let parameters = []
@ -30,7 +30,21 @@
</script>
<section>
<Heading extraSmall black>Parameters</Heading>
<div class="controls">
<Heading small lh>Parameters</Heading>
{#if !bindable}
<Button secondary on:click={newQueryParameter}>Add Param</Button>
{/if}
</div>
<Body small grey>
{#if !bindable}
Parameters come in two parts: the parameter name, and a default/fallback
value.
{:else}
Enter a value for each parameter. The default values will be used for any
values left blank.
{/if}
</Body>
<Spacer large />
<div class="parameters" class:bindable>
{#each parameters as parameter, idx}
@ -45,9 +59,9 @@
disabled={bindable}
bind:value={parameter.default} />
{#if bindable}
<BindableInput
<DrawerBindableInput
title={`Query parameter "${parameter.name}"`}
placeholder="Value"
type="string"
thin
on:change={evt => onBindingChange(parameter.name, evt.detail)}
value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}
@ -59,9 +73,6 @@
{/if}
{/each}
</div>
{#if !bindable}
<Button secondary on:click={newQueryParameter}>Add Parameter</Button>
{/if}
</section>
<style>
@ -69,6 +80,13 @@
grid-template-columns: 1fr 1fr 1fr;
}
.controls {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
}
.parameters {
display: grid;
grid-template-columns: 1fr 1fr 5%;

View file

@ -1,22 +1,20 @@
<script>
import { onMount } from "svelte"
import { goto } from "@sveltech/routify"
import {
Select,
Button,
Body,
Label,
Input,
TextArea,
Heading,
Spacer,
Switcher,
} from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import api from "builderStore/api"
import { FIELDS } from "constants/backend"
import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
import EditQueryParamsPopover from "components/backend/DatasourceNavigator/popovers/EditQueryParamsPopover.svelte"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import { backendUiStore } from "builderStore"
const PREVIEW_HEADINGS = [
@ -60,10 +58,10 @@
$: datasourceType = datasource?.source
$: config = $backendUiStore.integrations[datasourceType]?.query
$: docsLink = $backendUiStore.integrations[datasourceType]?.docs
$: integrationInfo = $backendUiStore.integrations[datasourceType]
$: queryConfig = integrationInfo?.query
$: shouldShowQueryConfig = config && query.queryVerb
$: shouldShowQueryConfig = queryConfig && query.queryVerb
function newField() {
fields = [...fields, {}]
@ -92,7 +90,7 @@
if (response.status !== 200) throw new Error(json.message)
data = json || []
data = json.rows || []
if (data.length === 0) {
notifier.info(
@ -103,9 +101,9 @@
notifier.success("Query executed successfully.")
// Assume all the fields are strings and create a basic schema
// from the first record returned by the query
fields = Object.keys(json[0]).map(field => ({
// Assume all the fields are strings and create a basic schema from the
// unique fields returned by the server
fields = json.schemaFields.map(field => ({
name: field,
type: "STRING",
}))
@ -130,58 +128,93 @@
}
</script>
<header>
<div class="input">
<div class="label">Enter query name:</div>
<Input outline border bind:value={query.name} />
<section class="config">
<Heading medium lh>Query {integrationInfo?.friendlyName}</Heading>
<hr />
<Spacer extraLarge />
<Heading small lh>Config</Heading>
<Body small grey>Provide a name for your query and select its function.</Body>
<Spacer large />
<div class="config-field">
<Label small>Query Name</Label>
<Input thin outline bind:value={query.name} />
</div>
{#if config}
<div class="props">
<div class="query-type">
Query type:
<span class="query-type-span">{config[query.queryVerb].type}</span>
</div>
<div class="select">
<Select primary thin bind:value={query.queryVerb}>
{#each Object.keys(config) as queryVerb}
<option value={queryVerb}>{queryVerb}</option>
{/each}
</Select>
</div>
<Spacer extraLarge />
{#if queryConfig}
<div class="config-field">
<Label small>Function</Label>
<Select primary outline thin bind:value={query.queryVerb}>
{#each Object.keys(queryConfig) as queryVerb}
<option value={queryVerb}>
{queryConfig[queryVerb]?.displayName || queryVerb}
</option>
{/each}
</Select>
</div>
<EditQueryParamsPopover
bind:parameters={query.parameters}
bindable={false} />
<Spacer extraLarge />
<hr />
<Spacer extraLarge />
<Spacer small />
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
<hr />
{/if}
</header>
<Spacer extraLarge />
</section>
{#if shouldShowQueryConfig}
<section>
<Spacer extraLarge />
<Spacer small />
<div class="config">
<Heading small lh>Fields</Heading>
<Body small grey>Fill in the fields specific to this query.</Body>
<Spacer medium />
<Spacer extraLarge />
<IntegrationQueryEditor
{datasource}
{query}
schema={config[query.queryVerb]}
schema={queryConfig[query.queryVerb]}
bind:parameters />
<Spacer extraLarge />
<hr />
<Spacer extraLarge />
<Spacer medium />
<div class="viewer-controls">
<Heading small lh>Results</Heading>
<div class="button-container">
<Button
secondary
thin
disabled={data.length === 0 || !query.name}
on:click={saveQuery}>
Save Query
</Button>
<Spacer medium />
<Button thin primary on:click={previewQuery}>Run Query</Button>
</div>
</div>
<Body small grey>
Below, you can preview the results from your query and change the
schema.
</Body>
<Spacer extraLarge />
<Spacer large />
<div class="viewer-controls">
<Button
blue
disabled={data.length === 0 || !query.name}
on:click={saveQuery}>
Save Query
</Button>
<Button primary on:click={previewQuery}>Run Query</Button>
</div>
<Spacer medium />
<section class="viewer">
{#if data}
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
{#if tab === 'JSON'}
<pre class="preview">{JSON.stringify(data[0], undefined, 2)}</pre>
<pre
class="preview">
<!-- prettier-ignore -->
{#if !data[0]}
Please run your query to fetch some data.
{:else}
{JSON.stringify(data[0], undefined, 2)}
{/if}
</pre>
{:else if tab === 'PREVIEW'}
<ExternalDataSourceTable {query} {data} />
{:else if tab === 'SCHEMA'}
@ -214,35 +247,30 @@
</div>
</section>
{/if}
<Spacer extraLarge />
<Spacer extraLarge />
<style>
.input {
width: 500px;
display: flex;
.config-field {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
.select {
width: 200px;
margin-right: 40px;
}
.props {
display: flex;
flex-direction: row;
margin-left: auto;
align-items: center;
gap: var(--layout-l);
}
.field {
display: grid;
grid-template-columns: 1fr 1fr 50px;
grid-template-columns: 1fr 1fr 5%;
gap: var(--spacing-l);
}
a {
font-size: var(--font-size-s);
.button-container {
display: flex;
}
hr {
margin-top: var(--layout-m);
border: 1px solid var(--grey-2);
}
.config {
@ -254,49 +282,28 @@
cursor: pointer;
}
.query-type {
font-family: var(--font-sans);
color: var(--grey-8);
font-size: var(--font-size-s);
}
.query-type-span {
text-transform: uppercase;
.viewer {
min-height: 200px;
}
.preview {
width: 800px;
height: 100%;
min-height: 120px;
overflow-y: auto;
overflow-wrap: break-word;
white-space: pre-wrap;
}
header {
display: flex;
align-items: center;
background-color: var(--grey-1);
padding: var(--spacing-m);
border-radius: 8px;
color: var(--grey-6);
}
.viewer-controls {
display: flex;
flex-direction: row;
margin-left: auto;
direction: rtl;
z-index: 5;
justify-content: space-between;
gap: var(--spacing-m);
min-width: 150px;
}
.viewer {
margin-top: -28px;
z-index: -2;
}
.label {
font-family: var(--font-sans);
color: var(--grey-8);
font-size: var(--font-size-s);
margin-right: 8px;
font-weight: 600;
align-items: center;
}
</style>

View file

@ -1,9 +1,7 @@
<script>
import { onMount } from "svelte"
import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte"
import ParameterBuilder from "./QueryParameterBuilder.svelte"
import FieldsBuilder from "./QueryFieldsBuilder.svelte"
import { Label, Input } from "@budibase/bbui"
const QueryTypes = {
SQL: "sql",
@ -12,8 +10,14 @@
}
export let query
export let datasource
export let schema
export let editable = true
export let height = 500
$: urlDisplay =
schema.urlDisplay &&
`${datasource.config.url}${query.fields.path}${query.fields.queryString}`
function updateQuery({ detail }) {
query.fields[schema.type] = detail.value
@ -24,6 +28,7 @@
{#key query._id}
{#if schema.type === QueryTypes.SQL}
<Editor
editorHeight={height}
label="Query"
mode="sql"
on:change={updateQuery}
@ -32,6 +37,7 @@
parameters={query.parameters} />
{:else if schema.type === QueryTypes.JSON}
<Editor
editorHeight={height}
label="Query"
mode="json"
on:change={updateQuery}
@ -40,6 +46,21 @@
parameters={query.parameters} />
{:else if schema.type === QueryTypes.FIELDS}
<FieldsBuilder bind:fields={query.fields} {schema} {editable} />
{#if schema.urlDisplay}
<div class="url-row">
<Label small>URL</Label>
<Input thin outline disabled value={urlDisplay} />
</div>
{/if}
{/if}
{/key}
{/if}
<style>
.url-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View file

@ -40,7 +40,7 @@ export const FIELDS = {
},
},
BOOLEAN: {
name: "True/False",
name: "Boolean",
icon: "ri-toggle-line",
type: "boolean",
constraints: {
@ -82,6 +82,22 @@ export const FIELDS = {
},
}
export const AUTO_COLUMN_SUB_TYPES = {
AUTO_ID: "autoID",
CREATED_BY: "createdBy",
CREATED_AT: "createdAt",
UPDATED_BY: "updatedBy",
UPDATED_AT: "updatedAt",
}
export const AUTO_COLUMN_DISPLAY_NAMES = {
AUTO_ID: "Auto ID",
CREATED_BY: "Created By",
CREATED_AT: "Created At",
UPDATED_BY: "Updated By",
UPDATED_AT: "Updated At",
}
export const FILE_TYPES = {
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],
@ -92,3 +108,18 @@ export const HostingTypes = {
CLOUD: "cloud",
SELF: "self",
}
export const Roles = {
ADMIN: "ADMIN",
POWER: "POWER",
BASIC: "BASIC",
PUBLIC: "PUBLIC",
BUILDER: "BUILDER",
}
export function isAutoColumnUserRelationship(subtype) {
return (
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY ||
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
)
}

View file

@ -36,6 +36,7 @@
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l);
border-right: 1px solid var(--grey-2);
}
.content {

View file

@ -1,6 +1,6 @@
<script>
import { params } from "@sveltech/routify"
import { Switcher, Modal } from "@budibase/bbui"
import { Button, Switcher, Modal } from "@budibase/bbui"
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
@ -8,11 +8,11 @@
const tabs = [
{
title: "Tables",
title: "Internal",
key: "table",
},
{
title: "Data Sources",
title: "External",
key: "datasource",
},
]
@ -67,6 +67,7 @@
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l);
background: var(--background);
}
.nav {
@ -79,6 +80,7 @@
align-items: stretch;
gap: var(--spacing-l);
position: relative;
border-right: 1px solid var(--grey-2);
}
i {

View file

@ -28,9 +28,11 @@
</script>
<section>
{#if $backendUiStore.selectedDatabase._id && selectedQuery}
<QueryInterface query={selectedQuery} />
{/if}
<div class="inner">
{#if $backendUiStore.selectedDatabase._id && selectedQuery}
<QueryInterface query={selectedQuery} />
{/if}
</div>
</section>
<style>
@ -41,4 +43,9 @@
width: 0px;
background: transparent; /* make scrollbar transparent */
}
.inner {
width: 640px;
margin: 0 auto;
}
</style>

View file

@ -1,18 +1,23 @@
<script>
import { goto } from "@sveltech/routify"
import { Button, Spacer, Icon } from "@budibase/bbui"
import { goto, beforeUrlChange } from "@sveltech/routify"
import { Button, Heading, Body, Spacer, Icon } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons"
let unsaved = false
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === $backendUiStore.selectedDatasourceId
)
$: integration = datasource && $backendUiStore.integrations[datasource.source]
async function saveDatasource() {
// Create datasource
await backendUiStore.actions.datasources.save(datasource)
notifier.success(`Datasource ${name} saved successfully.`)
unsaved = false
}
function onClickQuery(query) {
@ -22,28 +27,63 @@
backendUiStore.actions.queries.select(query)
$goto(`../${query._id}`)
}
function setUnsaved() {
unsaved = true
}
$beforeUrlChange((event, store) => {
if (unsaved) {
notifier.danger(
"Unsaved changes. Please save your datasource configuration before leaving."
)
return false
}
return true
})
</script>
{#if datasource}
<section>
<Spacer medium />
<Spacer extraLarge />
<header>
<div class="datasource-icon">
<svelte:component
this={ICONS[datasource.source]}
height="26"
width="26" />
</div>
<h3 class="section-title">{datasource.name}</h3>
</header>
<Body small grey lh>{integration.description}</Body>
<Spacer extraLarge />
<hr />
<Spacer large />
<Spacer extraLarge />
<div class="container">
<div class="config-header">
<h5>Configuration</h5>
<Heading small>Configuration</Heading>
<Button secondary on:click={saveDatasource}>Save</Button>
</div>
<Spacer medium />
<IntegrationConfigForm integration={datasource.config} />
</div>
<Spacer extraLarge />
<div class="container">
<Body small grey>
Connect your database to Budibase using the config below.
</Body>
<Spacer extraLarge />
<IntegrationConfigForm
schema={integration.datasource}
integration={datasource.config}
on:change={setUnsaved} />
<Spacer extraLarge />
<hr />
<Spacer large />
<Spacer extraLarge />
<div class="query-header">
<h5>Queries</h5>
<Button blue on:click={() => $goto('../new')}>Create Query</Button>
<Heading small>Queries</Heading>
<Button secondary on:click={() => $goto('../new')}>Add Query</Button>
</div>
<Spacer extraLarge />
<div class="query-list">
@ -54,7 +94,6 @@
<p></p>
</div>
{/each}
<Spacer medium />
</div>
</div>
</section>
@ -63,14 +102,22 @@
<style>
h3 {
margin: 0;
font-size: 24px;
}
section {
margin: 0 auto;
width: 800px;
width: 640px;
}
hr {
border: 1px solid var(--grey-2);
}
header {
margin: 0 0 var(--spacing-xs) 0;
display: flex;
gap: var(--spacing-m);
}
.section-title {
@ -85,13 +132,12 @@
.container {
border-radius: var(--border-radius-m);
background: var(--background);
padding: var(--layout-s);
margin: 0 auto;
}
h5 {
margin: 0 !important;
font-size: var(--font-size-l);
}
.query-header {
@ -115,7 +161,8 @@
display: grid;
grid-template-columns: 2fr 0.75fr 20px;
align-items: center;
padding: var(--spacing-m) var(--layout-xs);
padding-left: var(--spacing-m);
padding-right: var(--spacing-m);
gap: var(--layout-xs);
transition: 200ms background ease;
}

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