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 71778c0dc1
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: Each Budibase package has its own license:
builder: AGPLv3 builder: AGPLv3

View File

@ -84,7 +84,7 @@ services:
#- "4369:4369" #- "4369:4369"
#- "9100:9100" #- "9100:9100"
volumes: volumes:
- couchdb_data:/couchdb - couchdb_data:/opt/couchdb/data
couch-init: couch-init:
image: curlimages/curl 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 GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 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 Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. 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("[data-cy=new-automation]").click()
cy.get(".modal").within(() => { cy.get(".modal").within(() => {
cy.get("input").type("Add Row") cy.get("input").type("Add Row")
cy.get(".buttons").contains("Create").click() cy.get(".buttons")
.contains("Create")
.click()
}) })
// Add trigger // Add trigger
cy.contains("Trigger").click() cy.contains("Trigger").click()
cy.contains("Row Saved").click() cy.contains("Row Created").click()
cy.get(".setup").within(() => { cy.get(".setup").within(() => {
cy.get("select").first().select("dog") cy.get("select")
.first()
.select("dog")
}) })
// Create action // Create action
cy.contains("Action").click() cy.contains("Action").click()
cy.contains("Create Row").click() cy.contains("Create Row").click()
cy.get(".setup").within(() => { cy.get(".setup").within(() => {
cy.get("select").first().select("dog") cy.get("select")
cy.get("input").first().type("goodboy") .first()
cy.get("input").eq(1).type("11") .select("dog")
cy.get("input")
.first()
.type("goodboy")
cy.get("input")
.eq(1)
.type("11")
}) })
// Save // Save

View File

@ -30,7 +30,7 @@ context("Create a Table", () => {
// Unset table display column // Unset table display column
cy.contains("display column").click() cy.contains("display column").click()
cy.contains("Save Column").click() cy.contains("Save Column").click()
cy.contains("nameupdated").should("have.text", "nameupdated") cy.contains("nameupdated ").should("have.text", "nameupdated ")
}) })
it("edits a row", () => { 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", () => { context("Create a View", () => {
before(() => { before(() => {
cy.visit(`localhost:${Cypress.env("PORT")}/_builder`) cy.visit(`localhost:${Cypress.env("PORT")}/_builder`)
@ -28,7 +36,7 @@ context("Create a View", () => {
const headers = Array.from($headers).map(header => const headers = Array.from($headers).map(header =>
header.textContent.trim() 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.wait(50)
cy.get(".menu-container").find("select").eq(1).select("age") cy.get(".menu-container").find("select").eq(1).select("age")
cy.contains("Save").click() cy.contains("Save").click()
cy.wait(100)
cy.get(".ag-center-cols-viewport").scrollTo("100%") cy.get(".ag-center-cols-viewport").scrollTo("100%")
cy.get("[data-cy=table-header]").then($headers => { cy.get("[data-cy=table-header]").then($headers => {
expect($headers).to.have.length(7) expect($headers).to.have.length(7)
const headers = Array.from($headers).map(header => const headers = Array.from($headers).map(header =>
header.textContent.trim() header.textContent.trim()
) )
expect(headers).to.deep.eq([ expect(removeSpacing(headers)).to.deep.eq([ "avg Number",
"field", "sumsqr Number",
"sum", "count Number",
"min", "max Number",
"max", "min Number",
"count", "sum Number",
"sumsqr", "field Text" ])
"avg",
])
}) })
cy.get(".ag-cell").then($values => { cy.get(".ag-cell").then($values => {
const values = Array.from($values).map(header => let values = Array.from($values).map(header =>
header.textContent.trim() 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") .find(".ag-cell")
.then($values => { .then($values => {
const values = Array.from($values).map(value => value.textContent) const values = Array.from($values).map(value => value.textContent)
expect(values).to.deep.eq([ expect(values).to.deep.eq([ "Students", "23.333333333333332", "1650", "3", "25", "20", "70" ])
"Students",
"70",
"20",
"25",
"3",
"1650",
"23.333333333333332",
])
}) })
}) })

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import { FrontendTypes } from "constants"
import analytics from "analytics" import analytics from "analytics"
import { findComponentType, findComponentParent } from "../storeUtils" import { findComponentType, findComponentParent } from "../storeUtils"
import { uuid } from "../uuid" import { uuid } from "../uuid"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = { const INITIAL_FRONTEND_STATE = {
apps: [], apps: [],
@ -408,15 +409,29 @@ export const getFrontendStore = () => {
return state 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 // Clone the component to paste
// Retain the same ID if cutting as things may be referencing this component // Retain the same ID if cutting as things may be referencing this component
const cut = state.componentToPaste.isCut
delete state.componentToPaste.isCut delete state.componentToPaste.isCut
let componentToPaste = cloneDeep(state.componentToPaste) let componentToPaste = cloneDeep(state.componentToPaste)
if (cut) { if (cut) {
state.componentToPaste = null state.componentToPaste = null
} else { } else {
componentToPaste._id = uuid() const randomizeIds = component => {
if (!component) {
return
}
component._id = uuid()
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
} }
if (mode === "inside") { if (mode === "inside") {

View File

@ -9,5 +9,6 @@ const createScreen = () => {
return new Screen() return new Screen()
.mainType("div") .mainType("div")
.component("@budibase/standard-components/container") .component("@budibase/standard-components/container")
.instanceName("New Screen")
.json() .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 newRowScreen from "./newRowScreen"
import rowDetailScreen from "./rowDetailScreen" import rowDetailScreen from "./rowDetailScreen"
import rowListScreen from "./rowListScreen" import rowListScreen from "./rowListScreen"
import emptyNewRowScreen from "./emptyNewRowScreen"
import createFromScratchScreen from "./createFromScratchScreen" import createFromScratchScreen from "./createFromScratchScreen"
import emptyRowDetailScreen from "./emptyRowDetailScreen"
const allTemplates = tables => [ const allTemplates = tables => [
createFromScratchScreen,
...newRowScreen(tables), ...newRowScreen(tables),
...rowDetailScreen(tables), ...rowDetailScreen(tables),
...rowListScreen(tables), ...rowListScreen(tables),
emptyNewRowScreen,
emptyRowDetailScreen,
] ]
// Allows us to apply common behaviour to all create() functions // Allows us to apply common behaviour to all create() functions
@ -22,8 +17,18 @@ const createTemplateOverride = (frontendState, create) => () => {
return screen return screen
} }
export default (frontendState, tables) => export default (frontendState, tables) => {
allTemplates(tables).map(template => ({ const enrichTemplate = template => ({
...template, ...template,
create: createTemplateOverride(frontendState, template.create), 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 sanitizeUrl from "./utils/sanitizeUrl"
import { Component } from "./utils/Component"
import { Screen } from "./utils/Screen" import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component"
import { import {
makeBreadcrumbContainer, makeBreadcrumbContainer,
makeMainContainer, makeMainForm,
makeTitleContainer, makeTitleContainer,
makeSaveButton, makeSaveButton,
makeDatasourceFormComponents,
} from "./utils/commonComponents" } from "./utils/commonComponents"
export default function(tables) { export default function(tables) {
@ -21,29 +22,46 @@ export default function(tables) {
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`) export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE" export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
function generateTitleContainer(table, providerId) { function generateTitleContainer(table, formId) {
return makeTitleContainer("New Row").addChild( return makeTitleContainer("New Row").addChild(makeSaveButton(table, formId))
makeSaveButton(table, providerId)
)
} }
const createScreen = table => { const createScreen = table => {
const screen = new Screen() const screen = new Screen()
.component("@budibase/standard-components/newrow") .component("@budibase/standard-components/container")
.table(table._id)
.route(newRowUrl(table))
.instanceName(`${table.name} - New`) .instanceName(`${table.name} - New`)
.name("") .route(newRowUrl(table))
const dataform = new Component( const form = makeMainForm()
"@budibase/standard-components/dataformwide" .instanceName("Form")
).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 fieldGroup = new Component("@budibase/standard-components/fieldgroup")
const container = makeMainContainer() .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(makeBreadcrumbContainer(table.name, "New"))
.addChild(generateTitleContainer(table, providerId)) .addChild(generateTitleContainer(table, formId))
.addChild(dataform) .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 { Component } from "./utils/Component"
import { makePropSafe } from "@budibase/string-templates" import { makePropSafe } from "@budibase/string-templates"
import { import {
makeMainContainer,
makeBreadcrumbContainer, makeBreadcrumbContainer,
makeTitleContainer, makeTitleContainer,
makeSaveButton, makeSaveButton,
makeMainForm,
spectrumColor,
makeDatasourceFormComponents,
} from "./utils/commonComponents" } from "./utils/commonComponents"
export default function(tables) { export default function(tables) {
return tables.map(table => { return tables.map(table => {
const heading = table.primaryDisplay
? `{{ data.${makePropSafe(table.primaryDisplay)} }}`
: null
return { return {
name: `${table.name} - Detail`, name: `${table.name} - Detail`,
create: () => createScreen(table, heading), create: () => createScreen(table),
id: ROW_DETAIL_TEMPLATE, id: ROW_DETAIL_TEMPLATE,
} }
}) })
@ -26,9 +25,9 @@ export default function(tables) {
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE" export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`) 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 // have to override style for this, its missing margin
const saveButton = makeSaveButton(table, providerId).normalStyle({ const saveButton = makeSaveButton(table, formId).normalStyle({
background: "#000000", background: "#000000",
"border-width": "0", "border-width": "0",
"border-style": "None", "border-style": "None",
@ -54,6 +53,7 @@ function generateTitleContainer(table, title, providerId) {
background: "transparent", background: "transparent",
color: "#4285f4", color: "#4285f4",
}) })
.customStyle(spectrumColor(700))
.text("Delete") .text("Delete")
.customProps({ .customProps({
className: "", className: "",
@ -61,8 +61,9 @@ function generateTitleContainer(table, title, providerId) {
onClick: [ onClick: [
{ {
parameters: { parameters: {
rowId: `{{ ${makePropSafe(providerId)}._id }}`, providerId: formId,
revId: `{{ ${makePropSafe(providerId)}._rev }}`, rowId: `{{ ${makePropSafe(formId)}._id }}`,
revId: `{{ ${makePropSafe(formId)}._rev }}`,
tableId: table._id, tableId: table._id,
}, },
"##eventHandlerType": "Delete Row", "##eventHandlerType": "Delete Row",
@ -82,23 +83,47 @@ function generateTitleContainer(table, title, providerId) {
.addChild(saveButton) .addChild(saveButton)
} }
const createScreen = (table, heading) => { const createScreen = table => {
const screen = new Screen() const screen = new Screen()
.component("@budibase/standard-components/rowdetail") .component("@budibase/standard-components/rowdetail")
.table(table._id) .table(table._id)
.instanceName(`${table.name} - Detail`) .instanceName(`${table.name} - Detail`)
.route(rowDetailUrl(table)) .route(rowDetailUrl(table))
.name("")
const dataform = new Component( const form = makeMainForm()
"@budibase/standard-components/dataformwide" .instanceName("Form")
).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 fieldGroup = new Component("@budibase/standard-components/fieldgroup")
const container = makeMainContainer() .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(makeBreadcrumbContainer(table.name, heading || "Edit"))
.addChild(generateTitleContainer(table, heading || "Edit Row", providerId)) .addChild(generateTitleContainer(table, heading || "Edit Row", formId))
.addChild(dataform) .addChild(fieldGroup)
return screen.addChild(container).json() return screen.addChild(form).json()
} }

View File

@ -14,17 +14,11 @@ export class Component extends BaseStructure {
active: {}, active: {},
selected: {}, selected: {},
}, },
type: "",
_instanceName: "", _instanceName: "",
_children: [], _children: [],
} }
} }
type(type) {
this._json.type = type
return this
}
normalStyle(styling) { normalStyle(styling) {
this._json._styles.normal = styling this._json._styles.normal = styling
return this return this
@ -35,14 +29,25 @@ export class Component extends BaseStructure {
return this return this
} }
text(text) { customStyle(styling) {
this._json.text = text this._json._styles.custom = styling
return this return this
} }
// TODO: do we need this
instanceName(name) { instanceName(name) {
this._json._instanceName = name this._json._instanceName = name
return this 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 { Component } from "./Component"
import { rowListUrl } from "../rowListScreen" 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) { export function makeLinkComponent(tableName) {
return new Component("@budibase/standard-components/link") return new Component("@budibase/standard-components/link")
@ -10,6 +20,7 @@ export function makeLinkComponent(tableName) {
.hoverStyle({ .hoverStyle({
color: "#4285f4", color: "#4285f4",
}) })
.customStyle(spectrumColor(700))
.text(tableName) .text(tableName)
.customProps({ .customProps({
url: `/${tableName.toLowerCase()}`, url: `/${tableName.toLowerCase()}`,
@ -22,13 +33,12 @@ export function makeLinkComponent(tableName) {
}) })
} }
export function makeMainContainer() { export function makeMainForm() {
return new Component("@budibase/standard-components/container") return new Component("@budibase/standard-components/form")
.type("div") .type("div")
.normalStyle({ .normalStyle({
width: "700px", width: "700px",
padding: "0px", padding: "0px",
background: "white",
"border-radius": "0.5rem", "border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto", margin: "auto",
@ -39,7 +49,7 @@ export function makeMainContainer() {
"padding-left": "48px", "padding-left": "48px",
"margin-bottom": "20px", "margin-bottom": "20px",
}) })
.instanceName("Container") .instanceName("Form")
} }
export function makeBreadcrumbContainer(tableName, text, capitalise = false) { export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
@ -51,6 +61,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
"margin-right": "4px", "margin-right": "4px",
"margin-left": "4px", "margin-left": "4px",
}) })
.customStyle(spectrumColor(700))
.text(">") .text(">")
.instanceName("Arrow") .instanceName("Arrow")
@ -63,6 +74,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
const identifierText = new Component("@budibase/standard-components/text") const identifierText = new Component("@budibase/standard-components/text")
.type("none") .type("none")
.normalStyle(textStyling) .normalStyle(textStyling)
.customStyle(spectrumColor(700))
.text(text) .text(text)
.instanceName("Identifier") .instanceName("Identifier")
@ -78,7 +90,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
.addChild(identifierText) .addChild(identifierText)
} }
export function makeSaveButton(table, providerId) { export function makeSaveButton(table, formId) {
return new Component("@budibase/standard-components/button") return new Component("@budibase/standard-components/button")
.normalStyle({ .normalStyle({
background: "#000000", background: "#000000",
@ -99,8 +111,14 @@ export function makeSaveButton(table, providerId) {
disabled: false, disabled: false,
onClick: [ onClick: [
{ {
"##eventHandlerType": "Validate Form",
parameters: { parameters: {
providerId, componentId: formId,
},
},
{
parameters: {
providerId: formId,
}, },
"##eventHandlerType": "Save Row", "##eventHandlerType": "Save Row",
}, },
@ -125,6 +143,7 @@ export function makeTitleContainer(title) {
"margin-left": "0px", "margin-left": "0px",
flex: "1 1 auto", flex: "1 1 auto",
}) })
.customStyle(spectrumColor(900))
.type("h3") .type("h3")
.instanceName("Title") .instanceName("Title")
.text(title) .text(title)
@ -142,3 +161,55 @@ export function makeTitleContainer(title) {
.instanceName("Title Container") .instanceName("Title Container")
.addChild(heading) .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 * Recurses through the component tree and finds all components which match
* type. * a certain selector
*/ */
export const findAllMatchingComponents = (rootComponent, selector) => { export const findAllMatchingComponents = (rootComponent, selector) => {
if (!rootComponent || !selector) { if (!rootComponent || !selector) {
@ -81,6 +81,26 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
return components.reverse() 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 * Recurses through a component tree evaluating a matching function against
* components until a match is found * 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 groupBy from "lodash/fp/groupBy"
import { import {
TextArea, TextArea,
Label,
Input, Input,
Heading, Heading,
Body, Body,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,8 +6,11 @@
import GroupByButton from "./buttons/GroupByButton.svelte" import GroupByButton from "./buttons/GroupByButton.svelte"
import FilterButton from "./buttons/FilterButton.svelte" import FilterButton from "./buttons/FilterButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte" import ExportButton from "./buttons/ExportButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
export let view = {} export let view = {}
let hideAutocolumns = true
let data = [] let data = []
let loading = false let loading = false
@ -47,11 +50,18 @@
} }
</script> </script>
<Table title={decodeURI(name)} schema={view.schema} {data} {loading}> <Table
title={decodeURI(name)}
schema={view.schema}
{data}
{loading}
bind:hideAutocolumns>
<FilterButton {view} /> <FilterButton {view} />
<CalculateButton {view} /> <CalculateButton {view} />
{#if view.calculation} {#if view.calculation}
<GroupByButton {view} /> <GroupByButton {view} />
{/if} {/if}
<ManageAccessButton resourceId={decodeURI(name)} />
<HideAutocolumnButton bind:hideAutocolumns />
<ExportButton {view} /> <ExportButton {view} />
</Table> </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 row
export let selectRelationship export let selectRelationship
$: count = $: items = row?.[columnName] || []
row && columnName && Array.isArray(row[columnName])
? row[columnName].length
: 0
</script> </script>
<div class:link={count} on:click={() => selectRelationship(row, columnName)}> <div
{count} class="container"
related row(s) class:link={!!items.length}
on:click={() => selectRelationship(row, columnName)}>
{#each items as item}
<div class="item">{item}</div>
{/each}
</div> </div>
<style> <style>
.link { .container {
text-decoration: underline; display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
} }
.link:hover { .link:hover {
color: var(--grey-6); color: var(--grey-6);
cursor: pointer; 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> </style>

View File

@ -1,14 +1,25 @@
<script> <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 { cloneDeep } from "lodash/fp"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" 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 { notifier } from "builderStore/store/notifications"
import ValuesList from "components/common/ValuesList.svelte" import ValuesList from "components/common/ValuesList.svelte"
import DatePicker from "components/common/DatePicker.svelte" import DatePicker from "components/common/DatePicker.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
const AUTO_COL = "auto"
const LINK_TYPE = FIELDS.LINK.type
let fieldDefinitions = cloneDeep(FIELDS) let fieldDefinitions = cloneDeep(FIELDS)
export let onClosed export let onClosed
@ -24,6 +35,18 @@
let primaryDisplay = let primaryDisplay =
$backendUiStore.selectedTable.primaryDisplay == null || $backendUiStore.selectedTable.primaryDisplay == null ||
$backendUiStore.selectedTable.primaryDisplay === field.name $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 confirmDeleteDialog
let deletion let deletion
@ -34,13 +57,38 @@
$: uneditable = $: uneditable =
$backendUiStore.selectedTable?._id === TableNames.USERS && $backendUiStore.selectedTable?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(field.name) 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() { 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.update(state => {
backendUiStore.actions.tables.saveField({ backendUiStore.actions.tables.saveField({
originalName, originalName,
field, field,
primaryDisplay, primaryDisplay,
indexes,
}) })
return state return state
}) })
@ -57,12 +105,17 @@
} }
} }
function handleFieldConstraints(event) { function handleTypeChange(event) {
const { type, constraints } = fieldDefinitions[ const definition = fieldDefinitions[event.target.value.toUpperCase()]
event.target.value.toUpperCase() if (!definition) {
] return
field.type = type }
field.constraints = constraints 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) { 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() { function confirmDelete() {
confirmDeleteDialog.show() confirmDeleteDialog.show()
deletion = true deletion = true
@ -98,14 +163,15 @@
secondary secondary
thin thin
label="Type" label="Type"
on:change={handleFieldConstraints} on:change={handleTypeChange}
bind:value={field.type}> bind:value={field.type}>
{#each Object.values(fieldDefinitions) as field} {#each Object.values(fieldDefinitions) as field}
<option value={field.type}>{field.name}</option> <option value={field.type}>{field.name}</option>
{/each} {/each}
<option value={AUTO_COL}>Auto Column</option>
</Select> </Select>
{#if field.type !== 'link' && !uneditable} {#if canBeRequired}
<Toggle <Toggle
checked={required} checked={required}
on:change={onChangeRequired} on:change={onChangeRequired}
@ -114,12 +180,28 @@
text="Required" /> text="Required" />
{/if} {/if}
{#if field.type !== 'link'} {#if canBeDisplay}
<Toggle <Toggle
bind:checked={primaryDisplay} bind:checked={primaryDisplay}
on:change={onChangePrimaryDisplay} on:change={onChangePrimaryDisplay}
thin thin
text="Use as table display column" /> 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}
{#if field.type === 'string'} {#if field.type === 'string'}
@ -149,6 +231,20 @@
label="Max Value" label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} /> bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'} {: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}> <Select label="Table" thin secondary bind:value={field.tableId}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each tableOptions as table} {#each tableOptions as table}
@ -159,13 +255,22 @@
label={`Column Name in Other Table`} label={`Column Name in Other Table`}
thin thin
bind:value={field.fieldName} /> 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} {/if}
<footer class="create-column-options"> <footer class="create-column-options">
{#if !uneditable && originalName} {#if !uneditable && originalName != null}
<TextButton text on:click={confirmDelete}>Delete Column</TextButton> <TextButton text on:click={confirmDelete}>Delete Column</TextButton>
{/if} {/if}
<Button secondary on:click={onClosed}>Cancel</Button> <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> </footer>
</div> </div>
<ConfirmDialog <ConfirmDialog
@ -177,6 +282,15 @@
title="Confirm Deletion" /> title="Confirm Deletion" />
<style> <style>
label {
display: grid;
place-items: center;
}
.radio-buttons {
display: flex;
gap: var(--spacing-m);
font-size: var(--font-size-xs);
}
.actions { .actions {
display: grid; display: grid;
grid-gap: var(--spacing-xl); grid-gap: var(--spacing-xl);

View File

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

View File

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

View File

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

View File

@ -30,7 +30,9 @@
Object.keys(viewTable.schema).filter( Object.keys(viewTable.schema).filter(
field => field =>
view.calculation === "count" || 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() { 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> <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 integration
export let schema
let unsaved = false
</script> </script>
<form> <form>
{#each Object.keys(integration) as configKey} {#each Object.keys(schema) as configKey}
<Input {#if typeof schema[configKey].type === 'object'}
type={integration[configKey].type} <Label small>{configKey}</Label>
label={configKey} <Spacer small />
bind:value={integration[configKey]} /> <KeyValueBuilder bind:object={integration[configKey]} on:change />
<Spacer large /> {:else}
<div class="form-row">
<Label small>{configKey}</Label>
<Input
outline
type={schema[configKey].type}
on:change
bind:value={integration[configKey]} />
</div>
{/if}
{/each} {/each}
</form> </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 { onMount } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import api from "builderStore/api" 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" import ICONS from "../icons"
export let integration = {} export let integration = {}
@ -49,17 +50,6 @@
</div> </div>
{/each} {/each}
</div> </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> </section>
<style> <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 SqlServer from "./SQLServer.svelte"
import MySQL from "./MySQL.svelte" import MySQL from "./MySQL.svelte"
import ArangoDB from "./ArangoDB.svelte" import ArangoDB from "./ArangoDB.svelte"
import Rest from "./Rest.svelte"
export default { export default {
POSTGRES: Postgres, POSTGRES: Postgres,
@ -20,4 +21,5 @@ export default {
AIRTABLE: Airtable, AIRTABLE: Airtable,
MYSQL: MySQL, MYSQL: MySQL,
ARANGODB: ArangoDB, 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 { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui" import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let datasource 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 { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui" import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let query export let query

View File

@ -2,17 +2,11 @@
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore" import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { import { Input, Label, ModalContent, Toggle } from "@budibase/bbui"
Input,
Label,
ModalContent,
Button,
Spacer,
Toggle,
} from "@budibase/bbui"
import TableDataImport from "../TableDataImport.svelte" import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics" import analytics from "analytics"
import screenTemplates from "builderStore/store/screenTemplates" import screenTemplates from "builderStore/store/screenTemplates"
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen" import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen" import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen"
import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen" import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen"
@ -23,15 +17,28 @@
ROW_LIST_TEMPLATE, ROW_LIST_TEMPLATE,
] ]
$: tableNames = $backendUiStore.tables.map(table => table.name)
let modal let modal
let name let name
let dataImport let dataImport
let error = "" let error = ""
let createAutoscreens = true 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) { function checkValid(evt) {
const tableName = evt.target.value 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.` error = `Table with name ${tableName} already exists. Please choose another name.`
return return
} }
@ -41,7 +48,7 @@
async function saveTable() { async function saveTable() {
let newTable = { let newTable = {
name, name,
schema: dataImport.schema || {}, schema: addAutoColumns(name, dataImport.schema || {}),
dataImport, dataImport,
} }
@ -93,6 +100,28 @@
on:input={checkValid} on:input={checkValid}
bind:value={name} bind:value={name}
{error} /> {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 <Toggle
text="Generate screens in the design section" text="Generate screens in the design section"
bind:checked={createAutoscreens} /> bind:checked={createAutoscreens} />
@ -101,3 +130,25 @@
<TableDataImport bind:dataImport /> <TableDataImport bind:dataImport />
</div> </div>
</ModalContent> </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 { .icon {
right: 2px; right: 2px;
top: 26px; top: 5px;
bottom: 2px; bottom: 2px;
position: absolute; position: absolute;
align-items: center; 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> <script>
export let icon export let icon
export let title export let title
export let subtitle export let subtitle = undefined
export let disabled export let disabled = false
</script> </script>
<div class="dropdown-item" class:disabled on:click {...$$restProps}> <div class="dropdown-item" class:disabled on:click {...$$restProps}>

View File

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

View File

@ -23,7 +23,7 @@
}) })
</script> </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> <p>See below the list of deployed webhook URLs.</p>
{#each webhookUrls as webhookUrl} {#each webhookUrls as webhookUrl}
<div> <div>

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@
$: value && checkValid() $: value && checkValid()
$: bindableProperties = getBindableProperties( $: bindableProperties = getBindableProperties(
$currentAsset.props, $currentAsset,
$store.selectedComponentId $store.selectedComponentId
) )
$: dispatch("update", value) $: dispatch("update", value)
@ -114,8 +114,7 @@
bind:getCaretPosition bind:getCaretPosition
thin thin
bind:value bind:value
placeholder="Add text, or click the objects on the left to add them to placeholder="Add text, or click the objects on the left to add them to the textbox." />
the textbox." />
{#if !valid} {#if !valid}
<p class="syntax-error"> <p class="syntax-error">
Current Handlebars syntax is invalid, please check the guide Current Handlebars syntax is invalid, please check the guide
@ -144,9 +143,12 @@
} }
.text { .text {
padding: var(--spacing-xl); padding: var(--spacing-l);
font-family: var(--font-sans); font-family: var(--font-sans);
} }
.text :global(textarea) {
min-height: 100px;
}
.text :global(p) { .text :global(p) {
margin: 0; 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 let drawer
export let value = {} export let value = {}
export let otherSources
$: tables = $backendUiStore.tables.map(m => ({ $: tables = $backendUiStore.tables.map(m => ({
label: m.name, label: m.name,
@ -46,7 +47,7 @@
type: "query", type: "query",
})) }))
$: bindableProperties = getBindableProperties( $: bindableProperties = getBindableProperties(
$currentAsset.props, $currentAsset,
$store.selectedComponentId $store.selectedComponentId
) )
$: queryBindableProperties = bindableProperties.map(property => ({ $: queryBindableProperties = bindableProperties.map(property => ({
@ -76,7 +77,7 @@
dropdownRight.hide() dropdownRight.hide()
} }
function fetchDatasourceSchema(query) { function fetchQueryDefinition(query) {
const source = $backendUiStore.datasources.find( const source = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId ds => ds._id === query.datasourceId
).source ).source
@ -84,43 +85,48 @@
} }
</script> </script>
<div <div class="container">
class="dropdownbutton" <div
bind:this={anchorRight} class="dropdownbutton"
on:click={dropdownRight.show}> bind:this={anchorRight}
<span>{value?.label ? value.label : 'Choose option'}</span> on:click={dropdownRight.show}>
<Icon name="arrowdown" /> <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> </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}> <DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown"> <div class="dropdown">
<div class="title"> <div class="title">
@ -175,10 +181,33 @@
</li> </li>
{/each} {/each}
</ul> </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> </div>
</DropdownMenu> </DropdownMenu>
<style> <style>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.dropdownbutton { .dropdownbutton {
background-color: var(--grey-2); background-color: var(--grey-2);
border: var(--border-transparent); border: var(--border-transparent);
@ -241,8 +270,8 @@
} }
.drawer-contents { .drawer-contents {
padding: var(--spacing-xl); padding: var(--spacing-l);
height: 40vh; height: calc(40vh - 2 * var(--spacing-l));
overflow-y: auto; 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> <script>
import { import { Button, DropdownMenu, Spacer } from "@budibase/bbui"
Button,
Body,
DropdownMenu,
ModalContent,
Spacer,
} from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import actionTypes from "./actions" import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
import { automationStore } from "builderStore"
const EVENT_TYPE_KEY = "##eventHandlerType" const EVENT_TYPE_KEY = "##eventHandlerType"
@ -17,12 +8,19 @@
let addActionButton let addActionButton
let addActionDropdown let addActionDropdown
let selectedAction let selectedAction = actions?.length ? actions[0] : null
$: selectedActionComponent = $: selectedActionComponent =
selectedAction && selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component 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 => { const deleteAction = index => {
actions.splice(index, 1) actions.splice(index, 1)
actions = actions actions = actions
@ -51,11 +49,10 @@
<div class="actions-list"> <div class="actions-list">
<div> <div>
<div bind:this={addActionButton}> <div bind:this={addActionButton}>
<Spacer small />
<Button wide secondary on:click={addActionDropdown.show}> <Button wide secondary on:click={addActionDropdown.show}>
Add Action Add Action
</Button> </Button>
<Spacer medium /> <Spacer small />
</div> </div>
<DropdownMenu <DropdownMenu
bind:this={addActionDropdown} bind:this={addActionDropdown}
@ -74,11 +71,12 @@
{#if actions && actions.length > 0} {#if actions && actions.length > 0}
{#each actions as action, index} {#each actions as action, index}
<div class="action-container"> <div class="action-container">
<div class="action-header" on:click={selectAction(action)}> <div
<span class:selected={action === selectedAction}> class="action-header"
{index + 1}. class:selected={action === selectedAction}
{action[EVENT_TYPE_KEY]} on:click={selectAction(action)}>
</span> {index + 1}.
{action[EVENT_TYPE_KEY]}
</div> </div>
<i <i
class="ri-close-fill" class="ri-close-fill"
@ -107,20 +105,22 @@
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
} }
.action-header > span { .action-header {
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--grey-7);
font-weight: 500;
} }
.action-header > span:hover, .action-header:hover,
.selected { .action-header.selected {
cursor: pointer; cursor: pointer;
font-weight: 500; color: var(--ink);
} }
.actions-list { .actions-list {
border-right: var(--border-light); border-right: var(--border-light);
padding: var(--spacing-s); padding: var(--spacing-l);
} }
.available-action { .available-action {
@ -136,7 +136,6 @@
.actions-container { .actions-container {
height: 40vh; height: 40vh;
display: grid; display: grid;
grid-gap: var(--spacing-m);
grid-template-columns: 260px 1fr; grid-template-columns: 260px 1fr;
grid-auto-flow: column; grid-auto-flow: column;
min-height: 0; min-height: 0;
@ -145,13 +144,16 @@
} }
.action-container { .action-container {
border-top: var(--border-light); border-bottom: 1px solid var(--grey-1);
display: flex; display: flex;
align-items: center; align-items: center;
} }
.action-container:last-child {
border-bottom: none;
}
.selected-action-container { .selected-action-container {
padding: var(--spacing-xl); padding: var(--spacing-l);
} }
a { 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( const automationsToCreate = value.filter(
action => action["##eventHandlerType"] === "Trigger Automation" action => action["##eventHandlerType"] === "Trigger Automation"
) )
automationsToCreate.forEach(action => createAutomation(action.parameters)) for (let action of automationsToCreate) {
await createAutomation(action.parameters)
}
dispatch("change", value) dispatch("change", value)
notifier.success("Component actions saved.") notifier.success("Component actions saved.")
@ -27,11 +29,8 @@
// called by the parent modal when actions are saved // called by the parent modal when actions are saved
const createAutomation = async parameters => { const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) return if (parameters.automationId || !parameters.newAutomationName) return
await automationStore.actions.create({ name: parameters.newAutomationName }) await automationStore.actions.create({ name: parameters.newAutomationName })
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock( const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER", "TRIGGER",
"APP", "APP",
@ -39,19 +38,14 @@
) )
newBlock.inputs = { newBlock.inputs = {
fields: Object.entries(parameters.fields).reduce( fields: Object.keys(parameters.fields).reduce((fields, key) => {
(fields, [key, value]) => { fields[key] = "string"
fields[key] = value.type return fields
return fields }, {}),
},
{}
),
} }
automationStore.actions.addBlockToAutomation(newBlock) automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save($automationStore.selectedAutomation) await automationStore.actions.save($automationStore.selectedAutomation)
parameters.automationId = $automationStore.selectedAutomation.automation._id parameters.automationId = $automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName delete parameters.newAutomationName
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,13 +27,11 @@
schema, schema,
} }
}) })
$: hasAutomations = automations && automations.length > 0 $: hasAutomations = automations && automations.length > 0
$: selectedAutomation = automations?.find(
$: selectedAutomation = a => a._id === parameters?.automationId
parameters && )
parameters.automationId && $: selectedSchema = selectedAutomation?.schema
automations.find(a => a._id === parameters.automationId)
const onFieldsChanged = e => { const onFieldsChanged = e => {
parameters.fields = e.detail parameters.fields = e.detail
@ -42,95 +40,98 @@
const setNew = () => { const setNew = () => {
automationStatus = AUTOMATION_STATUS.NEW automationStatus = AUTOMATION_STATUS.NEW
parameters.automationId = undefined parameters.automationId = undefined
parameters.fields = {}
} }
const setExisting = () => { const setExisting = () => {
automationStatus = AUTOMATION_STATUS.EXISTING automationStatus = AUTOMATION_STATUS.EXISTING
parameters.newAutomationName = "" parameters.newAutomationName = ""
parameters.fields = {}
parameters.automationId = automations[0]?._id
} }
</script> </script>
<div class="root"> <div class="root">
<div class="radio-container" on:click={setNew}> <div class="radios">
<input <div class="radio-container" on:click={setNew}>
type="radio" <input
value={AUTOMATION_STATUS.NEW} type="radio"
bind:group={automationStatus} value={AUTOMATION_STATUS.NEW}
disabled={!hasAutomations} /> bind:group={automationStatus} />
<Label small>Create a new automation</Label>
<Label disabled={!hasAutomations}>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>
<div class="radio-container" on:click={setExisting}> <div class="fields">
<input <Label small>Automation</Label>
type="radio"
value={AUTOMATION_STATUS.EXISTING}
bind:group={automationStatus}
disabled={!hasAutomations} />
<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> </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> </div>
<style> <style>
.root { .fields {
display: grid; display: grid;
column-gap: var(--spacing-s); column-gap: var(--spacing-l);
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto; grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline; align-items: baseline;
} }
.root :global(> div:nth-child(4)) { .fields :global(> div:nth-child(2)) {
grid-column: 2 / span 4; grid-column: 2 / span 4;
} }
.radios,
.radio-container { .radio-container {
display: grid; display: flex;
grid-template-columns: auto 1fr; 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) { input[type="radio"]:checked {
grid-column: 1 / span 2; background: var(--blue);
}
.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;
} }
</style> </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 DeleteRow from "./DeleteRow.svelte"
import ExecuteQuery from "./ExecuteQuery.svelte" import ExecuteQuery from "./ExecuteQuery.svelte"
import TriggerAutomation from "./TriggerAutomation.svelte" import TriggerAutomation from "./TriggerAutomation.svelte"
import ValidateForm from "./ValidateForm.svelte"
// defines what actions are available, when adding a new one // defines what actions are available, when adding a new one
// the component is the setup panel for the action // the component is the setup panel for the action
@ -30,4 +31,8 @@ export default [
name: "Trigger Automation", name: "Trigger Automation",
component: TriggerAutomation, 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 = $: displayLabel =
selectedOption && selectedOption.label ? selectedOption.label : value || "" selectedOption && selectedOption.label
? selectedOption.label
: value || "Choose option"
</script> </script>
<div <div
@ -129,11 +131,16 @@
on:keydown={handleEscape} on:keydown={handleEscape}
class="bb-select-menu"> class="bb-select-menu">
<ul> <ul>
<li
on:click|self={() => handleClick(null)}
class:selected={value == null || value === ''}>
Choose option
</li>
{#if isOptionsObject} {#if isOptionsObject}
{#each options as { value: v, label }} {#each options as { value: v, label }}
<li <li
{...handleStyleBind(v)} {...handleStyleBind(v)}
on:click|self={handleClick(v)} on:click|self={() => handleClick(v)}
class:selected={value === v}> class:selected={value === v}>
{label} {label}
</li> </li>
@ -142,7 +149,7 @@
{#each options as v} {#each options as v}
<li <li
{...handleStyleBind(v)} {...handleStyleBind(v)}
on:click|self={handleClick(v)} on:click|self={() => handleClick(v)}
class:selected={value === v}> class:selected={value === v}>
{v} {v}
</li> </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, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte" import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
import { capitalise } from "../../../../helpers"
export let label = "" export let label = ""
export let bindable = true export let bindable = true
@ -24,7 +25,7 @@
let valid let valid
$: bindableProperties = getBindableProperties( $: bindableProperties = getBindableProperties(
$currentAsset.props, $currentAsset,
$store.selectedComponentId $store.selectedComponentId
) )
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties) $: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
@ -46,6 +47,11 @@
innerVal = value.target.value innerVal = value.target.value
} }
} }
if (type === "number") {
innerVal = parseInt(innerVal)
}
if (typeof innerVal === "string") { if (typeof innerVal === "string") {
onChange(replaceBindings(innerVal)) onChange(replaceBindings(innerVal))
} else { } else {
@ -72,6 +78,7 @@
value={safeValue} value={safeValue}
on:change={handleChange} on:change={handleChange}
onChange={handleChange} onChange={handleChange}
{type}
{...props} {...props}
name={key} /> name={key} />
</div> </div>
@ -82,26 +89,26 @@
on:click={bindingDrawer.show}> on:click={bindingDrawer.show}>
<Icon name="lightning" /> <Icon name="lightning" />
</div> </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} {/if}
</div> </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> <style>
.property-control { .property-control {
@ -138,7 +145,7 @@
align-items: center; align-items: center;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
padding-left: var(--spacing-xs); padding-left: 7px;
border-left: 1px solid var(--grey-4); border-left: 1px solid var(--grey-4);
background-color: var(--grey-2); background-color: var(--grey-2);
border-top-right-radius: var(--border-radius-m); border-top-right-radius: var(--border-radius-m);

View File

@ -25,7 +25,7 @@
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin> <DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
{#if open} {#if open}
<div> <div>
{#each properties as prop} {#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
<PropertyControl <PropertyControl
bindable={false} bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`} 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> <script>
import { get } from "lodash" import { get } from "lodash"
import { isEmpty } from "lodash/fp" 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 PropertyControl from "./PropertyControls/PropertyControl.svelte"
import Input from "./PropertyControls/Input.svelte" import Input from "./PropertyControls/Input.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte" import RoleSelect from "./PropertyControls/RoleSelect.svelte"
import OptionSelect from "./PropertyControls/OptionSelect.svelte" import OptionSelect from "./PropertyControls/OptionSelect.svelte"
import MultiTableViewFieldSelect from "./PropertyControls/MultiTableViewFieldSelect.svelte"
import Checkbox from "./PropertyControls/Checkbox.svelte" import Checkbox from "./PropertyControls/Checkbox.svelte"
import TableSelect from "./PropertyControls/TableSelect.svelte" import TableSelect from "./PropertyControls/TableSelect.svelte"
import TableViewSelect from "./PropertyControls/TableViewSelect.svelte" import DatasourceSelect from "./PropertyControls/DatasourceSelect.svelte"
import TableViewFieldSelect from "./PropertyControls/TableViewFieldSelect.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 EventsEditor from "./PropertyControls/EventsEditor"
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte" import FilterEditor from "./PropertyControls/FilterEditor.svelte"
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte" import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
import { IconSelect } from "./PropertyControls/IconSelect" import { IconSelect } from "./PropertyControls/IconSelect"
import ColorPicker from "./PropertyControls/ColorPicker.svelte" 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 componentDefinition = {}
export let componentInstance = {} export let componentInstance = {}
@ -39,6 +52,7 @@
"layoutId", "layoutId",
"routing.roleId", "routing.roleId",
] ]
let confirmResetFieldsDialog
$: settings = componentDefinition?.settings ?? [] $: settings = componentDefinition?.settings ?? []
$: isLayout = assetInstance && assetInstance.favicon $: isLayout = assetInstance && assetInstance.favicon
@ -47,8 +61,7 @@
const controlMap = { const controlMap = {
text: Input, text: Input,
select: OptionSelect, select: OptionSelect,
datasource: TableViewSelect, datasource: DatasourceSelect,
screen: ScreenSelect,
detailScreen: DetailScreenSelect, detailScreen: DetailScreenSelect,
boolean: Checkbox, boolean: Checkbox,
number: Input, number: Input,
@ -56,8 +69,18 @@
table: TableSelect, table: TableSelect,
color: ColorPicker, color: ColorPicker,
icon: IconSelect, icon: IconSelect,
field: TableViewFieldSelect, field: FieldSelect,
multifield: MultiTableViewFieldSelect, 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 => { const getControl = type => {
@ -78,6 +101,20 @@
const onInstanceNameChange = name => { const onInstanceNameChange = name => {
onChange("_instanceName", 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> </script>
<div class="settings-view-container"> <div class="settings-view-container">
@ -114,7 +151,7 @@
value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue} value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue}
{componentInstance} {componentInstance}
onChange={val => onChange(setting.key, val)} onChange={val => onChange(setting.key, val)}
props={{ options: setting.options }} /> props={{ options: setting.options, placeholder: setting.placeholder }} />
{/if} {/if}
{/each} {/each}
{:else} {:else}
@ -122,7 +159,19 @@
This component doesn't have any additional settings. This component doesn't have any additional settings.
</div> </div>
{/if} {/if}
{#if componentDefinition?.component?.endsWith('/fieldgroup')}
<Button secondary wide on:click={() => confirmResetFieldsDialog?.show()}>
Reset Fields
</Button>
{/if}
</div> </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> <style>
.settings-view-container { .settings-view-container {

View File

@ -9,7 +9,6 @@ export const layout = [
key: "display", key: "display",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Block", value: "block" }, { label: "Block", value: "block" },
{ label: "Inline Block", value: "inline-block" }, { label: "Inline Block", value: "inline-block" },
{ label: "Flex", value: "flex" }, { label: "Flex", value: "flex" },
@ -37,7 +36,6 @@ export const layout = [
key: "justify-content", key: "justify-content",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Flex Start", value: "flex-start" }, { label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" }, { label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" }, { label: "Center", value: "center" },
@ -51,7 +49,6 @@ export const layout = [
key: "align-items", key: "align-items",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Flex Start", value: "flex-start" }, { label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" }, { label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" }, { label: "Center", value: "center" },
@ -64,7 +61,6 @@ export const layout = [
key: "flex-wrap", key: "flex-wrap",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Wrap", value: "wrap" }, { label: "Wrap", value: "wrap" },
{ label: "No wrap", value: "nowrap" }, { label: "No wrap", value: "nowrap" },
], ],
@ -74,7 +70,6 @@ export const layout = [
key: "gap", key: "gap",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -93,7 +88,6 @@ export const margin = [
key: "margin", key: "margin",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -113,7 +107,6 @@ export const margin = [
key: "margin-top", key: "margin-top",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -133,7 +126,6 @@ export const margin = [
key: "margin-right", key: "margin-right",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -153,7 +145,6 @@ export const margin = [
key: "margin-bottom", key: "margin-bottom",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -173,7 +164,6 @@ export const margin = [
key: "margin-left", key: "margin-left",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -196,7 +186,6 @@ export const padding = [
key: "padding", key: "padding",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -214,7 +203,6 @@ export const padding = [
key: "padding-top", key: "padding-top",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -232,7 +220,6 @@ export const padding = [
key: "padding-right", key: "padding-right",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -250,7 +237,6 @@ export const padding = [
key: "padding-bottom", key: "padding-bottom",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -268,7 +254,6 @@ export const padding = [
key: "padding-left", key: "padding-left",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -289,7 +274,6 @@ export const size = [
key: "flex", key: "flex",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Shrink", value: "0 1 auto" }, { label: "Shrink", value: "0 1 auto" },
{ label: "Grow", value: "1 1 auto" }, { label: "Grow", value: "1 1 auto" },
], ],
@ -338,7 +322,6 @@ export const position = [
key: "position", key: "position",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Static", value: "static" }, { label: "Static", value: "static" },
{ label: "Relative", value: "relative" }, { label: "Relative", value: "relative" },
{ label: "Fixed", value: "fixed" }, { label: "Fixed", value: "fixed" },
@ -375,7 +358,6 @@ export const position = [
key: "z-index", key: "z-index",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "-9999", value: "-9999" }, { label: "-9999", value: "-9999" },
{ label: "-3", value: "-3" }, { label: "-3", value: "-3" },
{ label: "-2", value: "-2" }, { label: "-2", value: "-2" },
@ -395,7 +377,6 @@ export const typography = [
key: "font-family", key: "font-family",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Arial", value: "Arial" }, { label: "Arial", value: "Arial" },
{ label: "Arial Black", value: "Arial Black" }, { label: "Arial Black", value: "Arial Black" },
{ label: "Cursive", value: "Cursive" }, { label: "Cursive", value: "Cursive" },
@ -418,7 +399,6 @@ export const typography = [
key: "font-weight", key: "font-weight",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "200", value: "200" }, { label: "200", value: "200" },
{ label: "300", value: "300" }, { label: "300", value: "300" },
{ label: "400", value: "400" }, { label: "400", value: "400" },
@ -434,7 +414,6 @@ export const typography = [
key: "font-size", key: "font-size",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
{ label: "10px", value: "10px" }, { label: "10px", value: "10px" },
{ label: "12px", value: "12px" }, { label: "12px", value: "12px" },
@ -454,7 +433,6 @@ export const typography = [
key: "line-height", key: "line-height",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "1", value: "1" }, { label: "1", value: "1" },
{ label: "1.25", value: "1.25" }, { label: "1.25", value: "1.25" },
{ label: "1.5", value: "1.5" }, { label: "1.5", value: "1.5" },
@ -496,7 +474,6 @@ export const typography = [
key: "text-decoration-line", key: "text-decoration-line",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Underline", value: "underline" }, { label: "Underline", value: "underline" },
{ label: "Overline", value: "overline" }, { label: "Overline", value: "overline" },
{ label: "Line-through", value: "line-through" }, { label: "Line-through", value: "line-through" },
@ -516,7 +493,6 @@ export const background = [
key: "background-image", key: "background-image",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" }, { label: "None", value: "none" },
{ {
label: "Warm Flame", label: "Warm Flame",
@ -603,7 +579,6 @@ export const border = [
key: "border-radius", key: "border-radius",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" }, { label: "None", value: "0" },
{ label: "X Small", value: "0.125rem" }, { label: "X Small", value: "0.125rem" },
{ label: "Small", value: "0.25rem" }, { label: "Small", value: "0.25rem" },
@ -619,7 +594,6 @@ export const border = [
key: "border-width", key: "border-width",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" }, { label: "None", value: "0" },
{ label: "X Small", value: "0.5px" }, { label: "X Small", value: "0.5px" },
{ label: "Small", value: "1px" }, { label: "Small", value: "1px" },
@ -638,7 +612,6 @@ export const border = [
key: "border-style", key: "border-style",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" }, { label: "None", value: "none" },
{ label: "Hidden", value: "hidden" }, { label: "Hidden", value: "hidden" },
{ label: "Dotted", value: "dotted" }, { label: "Dotted", value: "dotted" },
@ -659,7 +632,6 @@ export const effects = [
key: "opacity", key: "opacity",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "0", value: "0" }, { label: "0", value: "0" },
{ label: "0.2", value: "0.2" }, { label: "0.2", value: "0.2" },
{ label: "0.4", value: "0.4" }, { label: "0.4", value: "0.4" },
@ -673,7 +645,6 @@ export const effects = [
key: "transform", key: "transform",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" }, { label: "None", value: "0" },
{ label: "45 deg", value: "rotate(45deg)" }, { label: "45 deg", value: "rotate(45deg)" },
{ label: "90 deg", value: "rotate(90deg)" }, { label: "90 deg", value: "rotate(90deg)" },
@ -690,7 +661,6 @@ export const effects = [
key: "box-shadow", key: "box-shadow",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" }, { label: "None", value: "none" },
{ label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }, { label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
{ {
@ -723,7 +693,6 @@ export const transitions = [
key: "transition-property", key: "transition-property",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" }, { label: "None", value: "none" },
{ label: "All", value: "all" }, { label: "All", value: "all" },
{ label: "Background Color", value: "background color" }, { label: "Background Color", value: "background color" },
@ -745,7 +714,6 @@ export const transitions = [
control: OptionSelect, control: OptionSelect,
placeholder: "sec", placeholder: "sec",
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "0.4s", value: "0.4s" }, { label: "0.4s", value: "0.4s" },
{ label: "0.6s", value: "0.6s" }, { label: "0.6s", value: "0.6s" },
{ label: "0.8s", value: "0.8s" }, { label: "0.8s", value: "0.8s" },
@ -759,7 +727,6 @@ export const transitions = [
key: "transition-timing-function", key: "transition-timing-function",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Linear", value: "linear" }, { label: "Linear", value: "linear" },
{ label: "Ease", value: "ease" }, { label: "Ease", value: "ease" },
{ label: "Ease in", value: "ease-in" }, { 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> <script>
import CodeMirror from "./codemirror" import CodeMirror from "./codemirror"
import { Label, Spacer } from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte" import { onMount, createEventDispatcher } from "svelte"
import { themeStore } from "builderStore" import { themeStore } from "builderStore"
import { handlebarsCompletions } from "constants/completions" import { handlebarsCompletions } from "constants/completions"
@ -11,11 +12,13 @@
LIGHT: "default", LIGHT: "default",
} }
export let label
export let value = "" export let value = ""
export let readOnly = false export let readOnly = false
export let lineNumbers = true export let lineNumbers = true
export let tab = true export let tab = true
export let mode export let mode
export let editorHeight = 500
// export let parameters = [] // export let parameters = []
let completions = handlebarsCompletions() let completions = handlebarsCompletions()
@ -169,15 +172,21 @@
} }
</script> </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> <style>
textarea { textarea {
visibility: hidden; visibility: hidden;
} }
:global(.CodeMirror) { div :global(.CodeMirror) {
height: 500px !important; height: var(--code-mirror-height) !important;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
font-family: monospace !important; font-family: monospace !important;
line-height: 1.3; line-height: 1.3;

View File

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

View File

@ -1,10 +1,10 @@
<script> <script>
import { Button, Input, Heading, Spacer } from "@budibase/bbui" import { Body, Button, Input, Heading, Spacer } from "@budibase/bbui"
import BindableInput from "components/common/BindableInput.svelte"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
export let bindable = true export let bindable = true
export let parameters = [] export let parameters = []
@ -30,7 +30,21 @@
</script> </script>
<section> <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 /> <Spacer large />
<div class="parameters" class:bindable> <div class="parameters" class:bindable>
{#each parameters as parameter, idx} {#each parameters as parameter, idx}
@ -45,9 +59,9 @@
disabled={bindable} disabled={bindable}
bind:value={parameter.default} /> bind:value={parameter.default} />
{#if bindable} {#if bindable}
<BindableInput <DrawerBindableInput
title={`Query parameter "${parameter.name}"`}
placeholder="Value" placeholder="Value"
type="string"
thin thin
on:change={evt => onBindingChange(parameter.name, evt.detail)} on:change={evt => onBindingChange(parameter.name, evt.detail)}
value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])} value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}
@ -59,9 +73,6 @@
{/if} {/if}
{/each} {/each}
</div> </div>
{#if !bindable}
<Button secondary on:click={newQueryParameter}>Add Parameter</Button>
{/if}
</section> </section>
<style> <style>
@ -69,6 +80,13 @@
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} }
.controls {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
}
.parameters { .parameters {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 5%; grid-template-columns: 1fr 1fr 5%;

View File

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

View File

@ -1,9 +1,7 @@
<script> <script>
import { onMount } from "svelte"
import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte" import Editor from "./QueryEditor.svelte"
import ParameterBuilder from "./QueryParameterBuilder.svelte"
import FieldsBuilder from "./QueryFieldsBuilder.svelte" import FieldsBuilder from "./QueryFieldsBuilder.svelte"
import { Label, Input } from "@budibase/bbui"
const QueryTypes = { const QueryTypes = {
SQL: "sql", SQL: "sql",
@ -12,8 +10,14 @@
} }
export let query export let query
export let datasource
export let schema export let schema
export let editable = true export let editable = true
export let height = 500
$: urlDisplay =
schema.urlDisplay &&
`${datasource.config.url}${query.fields.path}${query.fields.queryString}`
function updateQuery({ detail }) { function updateQuery({ detail }) {
query.fields[schema.type] = detail.value query.fields[schema.type] = detail.value
@ -24,6 +28,7 @@
{#key query._id} {#key query._id}
{#if schema.type === QueryTypes.SQL} {#if schema.type === QueryTypes.SQL}
<Editor <Editor
editorHeight={height}
label="Query" label="Query"
mode="sql" mode="sql"
on:change={updateQuery} on:change={updateQuery}
@ -32,6 +37,7 @@
parameters={query.parameters} /> parameters={query.parameters} />
{:else if schema.type === QueryTypes.JSON} {:else if schema.type === QueryTypes.JSON}
<Editor <Editor
editorHeight={height}
label="Query" label="Query"
mode="json" mode="json"
on:change={updateQuery} on:change={updateQuery}
@ -40,6 +46,21 @@
parameters={query.parameters} /> parameters={query.parameters} />
{:else if schema.type === QueryTypes.FIELDS} {:else if schema.type === QueryTypes.FIELDS}
<FieldsBuilder bind:fields={query.fields} {schema} {editable} /> <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} {/if}
{/key} {/key}
{/if} {/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: { BOOLEAN: {
name: "True/False", name: "Boolean",
icon: "ri-toggle-line", icon: "ri-toggle-line",
type: "boolean", type: "boolean",
constraints: { 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 = { export const FILE_TYPES = {
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"], IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"], CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],
@ -92,3 +108,18 @@ export const HostingTypes = {
CLOUD: "cloud", CLOUD: "cloud",
SELF: "self", 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; justify-content: flex-start;
align-items: stretch; align-items: stretch;
gap: var(--spacing-l); gap: var(--spacing-l);
border-right: 1px solid var(--grey-2);
} }
.content { .content {

View File

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

View File

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

View File

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

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