commit
71778c0dc1
2
LICENSE
2
LICENSE
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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",
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
@ -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}>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="attachment" />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="boolean" />
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="datetime" />
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="longform" />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FieldSelect from "./FieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FieldSelect {...$$props} multiselect />
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import TableViewFieldSelect from "./TableViewFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TableViewFieldSelect {...$$props} multiselect />
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="number" />
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="options" />
|
|
@ -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);
|
||||||
|
|
|
@ -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) ? ' *' : ''}`}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="link" />
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import DatasourceSelect from "./DatasourceSelect.svelte"
|
||||||
|
|
||||||
|
const otherSources = [{ name: "Custom", label: "Custom" }]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DatasourceSelect on:change {...$$props} {otherSources} />
|
|
@ -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>
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="string" />
|
|
@ -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 {
|
||||||
|
|
|
@ -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" },
|
||||||
|
|
|
@ -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>
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue