commit
71778c0dc1
2
LICENSE
2
LICENSE
|
@ -1,3 +1,5 @@
|
|||
Copyright 2019-2021, Budibase Ltd
|
||||
|
||||
Each Budibase package has its own license:
|
||||
|
||||
builder: AGPLv3
|
||||
|
|
|
@ -84,7 +84,7 @@ services:
|
|||
#- "4369:4369"
|
||||
#- "9100:9100"
|
||||
volumes:
|
||||
- couchdb_data:/couchdb
|
||||
- couchdb_data:/opt/couchdb/data
|
||||
|
||||
couch-init:
|
||||
image: curlimages/curl
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Copyright 2019-2021, Budibase Ltd
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
|
|
@ -17,23 +17,33 @@ context("Create a automation", () => {
|
|||
cy.get("[data-cy=new-automation]").click()
|
||||
cy.get(".modal").within(() => {
|
||||
cy.get("input").type("Add Row")
|
||||
cy.get(".buttons").contains("Create").click()
|
||||
cy.get(".buttons")
|
||||
.contains("Create")
|
||||
.click()
|
||||
})
|
||||
|
||||
// Add trigger
|
||||
cy.contains("Trigger").click()
|
||||
cy.contains("Row Saved").click()
|
||||
cy.contains("Row Created").click()
|
||||
cy.get(".setup").within(() => {
|
||||
cy.get("select").first().select("dog")
|
||||
cy.get("select")
|
||||
.first()
|
||||
.select("dog")
|
||||
})
|
||||
|
||||
// Create action
|
||||
cy.contains("Action").click()
|
||||
cy.contains("Create Row").click()
|
||||
cy.get(".setup").within(() => {
|
||||
cy.get("select").first().select("dog")
|
||||
cy.get("input").first().type("goodboy")
|
||||
cy.get("input").eq(1).type("11")
|
||||
cy.get("select")
|
||||
.first()
|
||||
.select("dog")
|
||||
cy.get("input")
|
||||
.first()
|
||||
.type("goodboy")
|
||||
cy.get("input")
|
||||
.eq(1)
|
||||
.type("11")
|
||||
})
|
||||
|
||||
// Save
|
||||
|
|
|
@ -30,7 +30,7 @@ context("Create a Table", () => {
|
|||
// Unset table display column
|
||||
cy.contains("display column").click()
|
||||
cy.contains("Save Column").click()
|
||||
cy.contains("nameupdated").should("have.text", "nameupdated")
|
||||
cy.contains("nameupdated ").should("have.text", "nameupdated ")
|
||||
})
|
||||
|
||||
it("edits a row", () => {
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
function removeSpacing(headers) {
|
||||
let newHeaders = []
|
||||
for (let header of headers) {
|
||||
newHeaders.push(header.replace(/\s\s+/g, " "))
|
||||
}
|
||||
return newHeaders
|
||||
}
|
||||
|
||||
context("Create a View", () => {
|
||||
before(() => {
|
||||
cy.visit(`localhost:${Cypress.env("PORT")}/_builder`)
|
||||
|
@ -28,7 +36,7 @@ context("Create a View", () => {
|
|||
const headers = Array.from($headers).map(header =>
|
||||
header.textContent.trim()
|
||||
)
|
||||
expect(headers).to.deep.eq(["group", "age", "rating"])
|
||||
expect(removeSpacing(headers)).to.deep.eq([ "rating Number", "age Number", "group Text" ])
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -53,27 +61,26 @@ context("Create a View", () => {
|
|||
cy.wait(50)
|
||||
cy.get(".menu-container").find("select").eq(1).select("age")
|
||||
cy.contains("Save").click()
|
||||
cy.wait(100)
|
||||
cy.get(".ag-center-cols-viewport").scrollTo("100%")
|
||||
cy.get("[data-cy=table-header]").then($headers => {
|
||||
expect($headers).to.have.length(7)
|
||||
const headers = Array.from($headers).map(header =>
|
||||
header.textContent.trim()
|
||||
)
|
||||
expect(headers).to.deep.eq([
|
||||
"field",
|
||||
"sum",
|
||||
"min",
|
||||
"max",
|
||||
"count",
|
||||
"sumsqr",
|
||||
"avg",
|
||||
])
|
||||
expect(removeSpacing(headers)).to.deep.eq([ "avg Number",
|
||||
"sumsqr Number",
|
||||
"count Number",
|
||||
"max Number",
|
||||
"min Number",
|
||||
"sum Number",
|
||||
"field Text" ])
|
||||
})
|
||||
cy.get(".ag-cell").then($values => {
|
||||
const values = Array.from($values).map(header =>
|
||||
let values = Array.from($values).map(header =>
|
||||
header.textContent.trim()
|
||||
)
|
||||
expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"])
|
||||
expect(values).to.deep.eq([ "31", "5347", "5", "49", "20", "155", "age" ])
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -92,15 +99,7 @@ context("Create a View", () => {
|
|||
.find(".ag-cell")
|
||||
.then($values => {
|
||||
const values = Array.from($values).map(value => value.textContent)
|
||||
expect(values).to.deep.eq([
|
||||
"Students",
|
||||
"70",
|
||||
"20",
|
||||
"25",
|
||||
"3",
|
||||
"1650",
|
||||
"23.333333333333332",
|
||||
])
|
||||
expect(values).to.deep.eq([ "Students", "23.333333333333332", "1650", "3", "25", "20", "70" ])
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
const rimraf = require("rimraf")
|
||||
const { join, resolve } = require("path")
|
||||
// const run = require("../../cli/src/commands/run/runHandler")
|
||||
const initialiseBudibase = require("../../server/src/utilities/initialiseBudibase")
|
||||
const cypressConfig = require("../cypress.json")
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "1.58.3",
|
||||
"@budibase/bbui": "^1.58.13",
|
||||
"@budibase/client": "^0.7.8",
|
||||
"@budibase/colorpicker": "1.0.1",
|
||||
"@budibase/string-templates": "^0.7.8",
|
||||
|
|
|
@ -1,33 +1,35 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { get } from "svelte/store"
|
||||
import { backendUiStore, store } from "builderStore"
|
||||
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
|
||||
import { findComponentPath } from "./storeUtils"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
import { TableNames } from "../constants"
|
||||
|
||||
// Regex to match all instances of template strings
|
||||
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
|
||||
|
||||
/**
|
||||
* Gets all bindable data context fields and instance fields.
|
||||
*/
|
||||
export const getBindableProperties = (rootComponent, componentId) => {
|
||||
const contextBindings = getContextBindings(rootComponent, componentId)
|
||||
const componentBindings = getComponentBindings(rootComponent)
|
||||
return [...contextBindings, ...componentBindings]
|
||||
export const getBindableProperties = (asset, componentId) => {
|
||||
const contextBindings = getContextBindings(asset, componentId)
|
||||
const userBindings = getUserBindings()
|
||||
const urlBindings = getUrlBindings(asset, componentId)
|
||||
return [...contextBindings, ...userBindings, ...urlBindings]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all data provider components above a component.
|
||||
*/
|
||||
export const getDataProviderComponents = (rootComponent, componentId) => {
|
||||
if (!rootComponent || !componentId) {
|
||||
export const getDataProviderComponents = (asset, componentId) => {
|
||||
if (!asset || !componentId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get the component tree leading up to this component, ignoring the component
|
||||
// itself
|
||||
const path = findComponentPath(rootComponent, componentId)
|
||||
const path = findComponentPath(asset.props, componentId)
|
||||
path.pop()
|
||||
|
||||
// Filter by only data provider components
|
||||
|
@ -37,6 +39,26 @@ export const getDataProviderComponents = (rootComponent, componentId) => {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all data provider components above a component.
|
||||
*/
|
||||
export const getActionProviderComponents = (asset, componentId, actionType) => {
|
||||
if (!asset || !componentId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get the component tree leading up to this component, ignoring the component
|
||||
// itself
|
||||
const path = findComponentPath(asset.props, componentId)
|
||||
path.pop()
|
||||
|
||||
// Filter by only data provider components
|
||||
return path.filter(component => {
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
return def?.actions?.includes(actionType)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a datasource object for a certain data provider component
|
||||
*/
|
||||
|
@ -47,8 +69,9 @@ export const getDatasourceForProvider = component => {
|
|||
}
|
||||
|
||||
// Extract datasource from component instance
|
||||
const validSettingTypes = ["datasource", "table", "schema"]
|
||||
const datasourceSetting = def.settings.find(setting => {
|
||||
return setting.type === "datasource" || setting.type === "table"
|
||||
return validSettingTypes.includes(setting.type)
|
||||
})
|
||||
if (!datasourceSetting) {
|
||||
return null
|
||||
|
@ -58,40 +81,54 @@ export const getDatasourceForProvider = component => {
|
|||
// example an actual datasource object, or a table ID string.
|
||||
// Convert the datasource setting into a proper datasource object so that
|
||||
// we can use it properly
|
||||
if (datasourceSetting.type === "datasource") {
|
||||
return component[datasourceSetting?.key]
|
||||
} else if (datasourceSetting.type === "table") {
|
||||
if (datasourceSetting.type === "table") {
|
||||
return {
|
||||
tableId: component[datasourceSetting?.key],
|
||||
type: "table",
|
||||
}
|
||||
} else {
|
||||
return component[datasourceSetting?.key]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable data contexts. These are fields of schemas of data contexts
|
||||
* provided by data provider components, such as lists or row detail components.
|
||||
* Gets all bindable data properties from component data contexts.
|
||||
*/
|
||||
export const getContextBindings = (rootComponent, componentId) => {
|
||||
const getContextBindings = (asset, componentId) => {
|
||||
// Extract any components which provide data contexts
|
||||
const dataProviders = getDataProviderComponents(rootComponent, componentId)
|
||||
let contextBindings = []
|
||||
const dataProviders = getDataProviderComponents(asset, componentId)
|
||||
let bindings = []
|
||||
|
||||
// Create bindings for each data provider
|
||||
dataProviders.forEach(component => {
|
||||
const isForm = component._component.endsWith("/form")
|
||||
const datasource = getDatasourceForProvider(component)
|
||||
if (!datasource) {
|
||||
let tableName, schema
|
||||
|
||||
// Forms are an edge case which do not need table schemas
|
||||
if (isForm) {
|
||||
schema = buildFormSchema(component)
|
||||
tableName = "Fields"
|
||||
} else {
|
||||
if (!datasource) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get schema and table for the datasource
|
||||
const info = getSchemaForDatasource(datasource, isForm)
|
||||
schema = info.schema
|
||||
tableName = info.table?.name
|
||||
|
||||
// Add _id and _rev fields for certain types
|
||||
if (datasource.type === "table" || datasource.type === "link") {
|
||||
schema["_id"] = { type: "string" }
|
||||
schema["_rev"] = { type: "string" }
|
||||
}
|
||||
}
|
||||
if (!schema || !tableName) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get schema and add _id and _rev fields for certain types
|
||||
let { schema, table } = getSchemaForDatasource(datasource)
|
||||
if (!schema || !table) {
|
||||
return
|
||||
}
|
||||
if (datasource.type === "table" || datasource.type === "link") {
|
||||
schema["_id"] = { type: "string" }
|
||||
schema["_rev"] = { type: "string " }
|
||||
}
|
||||
const keys = Object.keys(schema).sort()
|
||||
|
||||
// Create bindable properties for each schema field
|
||||
|
@ -100,26 +137,33 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
// Replace certain bindings with a new property to help display components
|
||||
let runtimeBoundKey = key
|
||||
if (fieldSchema.type === "link") {
|
||||
runtimeBoundKey = `${key}_count`
|
||||
runtimeBoundKey = `${key}_text`
|
||||
} else if (fieldSchema.type === "attachment") {
|
||||
runtimeBoundKey = `${key}_first`
|
||||
}
|
||||
|
||||
contextBindings.push({
|
||||
bindings.push({
|
||||
type: "context",
|
||||
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
|
||||
runtimeBoundKey
|
||||
)}`,
|
||||
readableBinding: `${component._instanceName}.${table.name}.${key}`,
|
||||
readableBinding: `${component._instanceName}.${tableName}.${key}`,
|
||||
// Field schema and provider are required to construct relationship
|
||||
// datasource options, based on bindable properties
|
||||
fieldSchema,
|
||||
providerId: component._id,
|
||||
tableId: datasource.tableId,
|
||||
field: key,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Add logged in user bindings
|
||||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable properties from the logged in user.
|
||||
*/
|
||||
const getUserBindings = () => {
|
||||
let bindings = []
|
||||
const tables = get(backendUiStore).tables
|
||||
const userTable = tables.find(table => table._id === TableNames.USERS)
|
||||
const schema = {
|
||||
|
@ -133,53 +177,48 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
// Replace certain bindings with a new property to help display components
|
||||
let runtimeBoundKey = key
|
||||
if (fieldSchema.type === "link") {
|
||||
runtimeBoundKey = `${key}_count`
|
||||
runtimeBoundKey = `${key}_text`
|
||||
} else if (fieldSchema.type === "attachment") {
|
||||
runtimeBoundKey = `${key}_first`
|
||||
}
|
||||
|
||||
contextBindings.push({
|
||||
bindings.push({
|
||||
type: "context",
|
||||
runtimeBinding: `user.${runtimeBoundKey}`,
|
||||
readableBinding: `Current User.${key}`,
|
||||
// Field schema and provider are required to construct relationship
|
||||
// datasource options, based on bindable properties
|
||||
fieldSchema,
|
||||
providerId: "user",
|
||||
tableId: TableNames.USERS,
|
||||
field: key,
|
||||
})
|
||||
})
|
||||
|
||||
return contextBindings
|
||||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable components. These are form components which allow their
|
||||
* values to be bound to.
|
||||
* Gets all bindable properties from URL parameters.
|
||||
*/
|
||||
export const getComponentBindings = rootComponent => {
|
||||
if (!rootComponent) {
|
||||
return []
|
||||
}
|
||||
const componentSelector = component => {
|
||||
const type = component._component
|
||||
const definition = store.actions.components.getDefinition(type)
|
||||
return definition?.bindable
|
||||
}
|
||||
const components = findAllMatchingComponents(rootComponent, componentSelector)
|
||||
return components.map(component => {
|
||||
return {
|
||||
type: "instance",
|
||||
providerId: component._id,
|
||||
runtimeBinding: `${makePropSafe(component._id)}`,
|
||||
readableBinding: `${component._instanceName}`,
|
||||
const getUrlBindings = asset => {
|
||||
const url = asset?.routing?.route ?? ""
|
||||
const split = url.split("/")
|
||||
let params = []
|
||||
split.forEach(part => {
|
||||
if (part.startsWith(":") && part.length > 1) {
|
||||
params.push(part.replace(/:/g, "").replace(/\?/g, ""))
|
||||
}
|
||||
})
|
||||
return params.map(param => ({
|
||||
type: "context",
|
||||
runtimeBinding: `url.${param}`,
|
||||
readableBinding: `URL.${param}`,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a schema for a datasource object.
|
||||
*/
|
||||
export const getSchemaForDatasource = datasource => {
|
||||
export const getSchemaForDatasource = (datasource, isForm = false) => {
|
||||
let schema, table
|
||||
if (datasource) {
|
||||
const { type } = datasource
|
||||
|
@ -193,6 +232,23 @@ export const getSchemaForDatasource = datasource => {
|
|||
if (table) {
|
||||
if (type === "view") {
|
||||
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
||||
|
||||
// Some calc views don't include a "name" property inside the schema
|
||||
if (schema) {
|
||||
Object.keys(schema).forEach(field => {
|
||||
if (!schema[field].name) {
|
||||
schema[field].name = field
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (type === "query" && isForm) {
|
||||
schema = {}
|
||||
const params = table.parameters || []
|
||||
params.forEach(param => {
|
||||
if (param?.name) {
|
||||
schema[param.name] = { ...param, type: "string" }
|
||||
}
|
||||
})
|
||||
} else {
|
||||
schema = cloneDeep(table.schema)
|
||||
}
|
||||
|
@ -201,6 +257,46 @@ export const getSchemaForDatasource = datasource => {
|
|||
return { schema, table }
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a form schema given a form component.
|
||||
* A form schema is a schema of all the fields nested anywhere within a form.
|
||||
*/
|
||||
const buildFormSchema = component => {
|
||||
let schema = {}
|
||||
if (!component) {
|
||||
return schema
|
||||
}
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
const fieldSetting = def?.settings?.find(
|
||||
setting => setting.key === "field" && setting.type.startsWith("field/")
|
||||
)
|
||||
if (fieldSetting && component.field) {
|
||||
const type = fieldSetting.type.split("field/")[1]
|
||||
if (type) {
|
||||
schema[component.field] = { name: component.field, type }
|
||||
}
|
||||
}
|
||||
component._children?.forEach(child => {
|
||||
const childSchema = buildFormSchema(child)
|
||||
schema = { ...schema, ...childSchema }
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses the input object to remove any instances of bindings.
|
||||
*/
|
||||
export function removeBindings(obj) {
|
||||
for (let [key, value] of Object.entries(obj)) {
|
||||
if (typeof value === "object") {
|
||||
obj[key] = removeBindings(value)
|
||||
} else if (typeof value === "string") {
|
||||
obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding")
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
||||
*/
|
||||
|
|
|
@ -30,6 +30,7 @@ export const getBackendUiStore = () => {
|
|||
const queries = await queriesResponse.json()
|
||||
const integrationsResponse = await api.get("/api/integrations")
|
||||
const integrations = await integrationsResponse.json()
|
||||
const permissionLevels = await store.actions.permissions.fetchLevels()
|
||||
|
||||
store.update(state => {
|
||||
state.selectedDatabase = db
|
||||
|
@ -37,6 +38,7 @@ export const getBackendUiStore = () => {
|
|||
state.datasources = datasources
|
||||
state.queries = queries
|
||||
state.integrations = integrations
|
||||
state.permissionLevels = permissionLevels
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -133,6 +135,9 @@ export const getBackendUiStore = () => {
|
|||
}
|
||||
query.datasourceId = datasourceId
|
||||
const response = await api.post(`/api/queries`, query)
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed saving query.")
|
||||
}
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
const currentIdx = state.queries.findIndex(
|
||||
|
@ -232,7 +237,7 @@ export const getBackendUiStore = () => {
|
|||
return state
|
||||
})
|
||||
},
|
||||
saveField: ({ originalName, field, primaryDisplay = false }) => {
|
||||
saveField: ({ originalName, field, primaryDisplay = false, indexes }) => {
|
||||
store.update(state => {
|
||||
// delete the original if renaming
|
||||
// need to handle if the column had no name, empty string
|
||||
|
@ -249,6 +254,10 @@ export const getBackendUiStore = () => {
|
|||
state.draftTable.primaryDisplay = field.name
|
||||
}
|
||||
|
||||
if (indexes) {
|
||||
state.draftTable.indexes = indexes
|
||||
}
|
||||
|
||||
state.draftTable.schema[field.name] = cloneDeep(field)
|
||||
store.actions.tables.save(state.draftTable)
|
||||
return state
|
||||
|
@ -324,6 +333,25 @@ export const getBackendUiStore = () => {
|
|||
return response
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
fetchLevels: async () => {
|
||||
const response = await api.get("/api/permission/levels")
|
||||
const json = await response.json()
|
||||
return json
|
||||
},
|
||||
forResource: async resourceId => {
|
||||
const response = await api.get(`/api/permission/${resourceId}`)
|
||||
const json = await response.json()
|
||||
return json
|
||||
},
|
||||
save: async ({ role, resource, level }) => {
|
||||
const response = await api.post(
|
||||
`/api/permission/${role}/${resource}/${level}`
|
||||
)
|
||||
const json = await response.json()
|
||||
return json
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return store
|
||||
|
|
|
@ -15,6 +15,7 @@ import { FrontendTypes } from "constants"
|
|||
import analytics from "analytics"
|
||||
import { findComponentType, findComponentParent } from "../storeUtils"
|
||||
import { uuid } from "../uuid"
|
||||
import { removeBindings } from "../dataBinding"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
apps: [],
|
||||
|
@ -408,15 +409,29 @@ export const getFrontendStore = () => {
|
|||
return state
|
||||
}
|
||||
|
||||
// defines if this is a copy or a cut
|
||||
const cut = state.componentToPaste.isCut
|
||||
|
||||
// immediately need to remove bindings, currently these aren't valid when pasted
|
||||
if (!cut) {
|
||||
state.componentToPaste = removeBindings(state.componentToPaste)
|
||||
}
|
||||
|
||||
// Clone the component to paste
|
||||
// Retain the same ID if cutting as things may be referencing this component
|
||||
const cut = state.componentToPaste.isCut
|
||||
delete state.componentToPaste.isCut
|
||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||
if (cut) {
|
||||
state.componentToPaste = null
|
||||
} else {
|
||||
componentToPaste._id = uuid()
|
||||
const randomizeIds = component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
component._id = uuid()
|
||||
component._children?.forEach(randomizeIds)
|
||||
}
|
||||
randomizeIds(componentToPaste)
|
||||
}
|
||||
|
||||
if (mode === "inside") {
|
||||
|
|
|
@ -9,5 +9,6 @@ const createScreen = () => {
|
|||
return new Screen()
|
||||
.mainType("div")
|
||||
.component("@budibase/standard-components/container")
|
||||
.instanceName("New Screen")
|
||||
.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 rowDetailScreen from "./rowDetailScreen"
|
||||
import rowListScreen from "./rowListScreen"
|
||||
import emptyNewRowScreen from "./emptyNewRowScreen"
|
||||
import createFromScratchScreen from "./createFromScratchScreen"
|
||||
import emptyRowDetailScreen from "./emptyRowDetailScreen"
|
||||
|
||||
const allTemplates = tables => [
|
||||
createFromScratchScreen,
|
||||
...newRowScreen(tables),
|
||||
...rowDetailScreen(tables),
|
||||
...rowListScreen(tables),
|
||||
emptyNewRowScreen,
|
||||
emptyRowDetailScreen,
|
||||
]
|
||||
|
||||
// Allows us to apply common behaviour to all create() functions
|
||||
|
@ -22,8 +17,18 @@ const createTemplateOverride = (frontendState, create) => () => {
|
|||
return screen
|
||||
}
|
||||
|
||||
export default (frontendState, tables) =>
|
||||
allTemplates(tables).map(template => ({
|
||||
export default (frontendState, tables) => {
|
||||
const enrichTemplate = template => ({
|
||||
...template,
|
||||
create: createTemplateOverride(frontendState, template.create),
|
||||
}))
|
||||
})
|
||||
|
||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||
const tableTemplates = allTemplates(tables).map(enrichTemplate)
|
||||
return [
|
||||
fromScratch,
|
||||
...tableTemplates.sort((templateA, templateB) => {
|
||||
return templateA.name > templateB.name ? 1 : -1
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import sanitizeUrl from "./utils/sanitizeUrl"
|
||||
import { Component } from "./utils/Component"
|
||||
import { Screen } from "./utils/Screen"
|
||||
import { Component } from "./utils/Component"
|
||||
import {
|
||||
makeBreadcrumbContainer,
|
||||
makeMainContainer,
|
||||
makeMainForm,
|
||||
makeTitleContainer,
|
||||
makeSaveButton,
|
||||
makeDatasourceFormComponents,
|
||||
} from "./utils/commonComponents"
|
||||
|
||||
export default function(tables) {
|
||||
|
@ -21,29 +22,46 @@ export default function(tables) {
|
|||
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
|
||||
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
|
||||
|
||||
function generateTitleContainer(table, providerId) {
|
||||
return makeTitleContainer("New Row").addChild(
|
||||
makeSaveButton(table, providerId)
|
||||
)
|
||||
function generateTitleContainer(table, formId) {
|
||||
return makeTitleContainer("New Row").addChild(makeSaveButton(table, formId))
|
||||
}
|
||||
|
||||
const createScreen = table => {
|
||||
const screen = new Screen()
|
||||
.component("@budibase/standard-components/newrow")
|
||||
.table(table._id)
|
||||
.route(newRowUrl(table))
|
||||
.component("@budibase/standard-components/container")
|
||||
.instanceName(`${table.name} - New`)
|
||||
.name("")
|
||||
.route(newRowUrl(table))
|
||||
|
||||
const dataform = new Component(
|
||||
"@budibase/standard-components/dataformwide"
|
||||
).instanceName("Form")
|
||||
const form = makeMainForm()
|
||||
.instanceName("Form")
|
||||
.customProps({
|
||||
theme: "spectrum--lightest",
|
||||
size: "spectrum--medium",
|
||||
datasource: {
|
||||
label: table.name,
|
||||
tableId: table._id,
|
||||
type: "table",
|
||||
},
|
||||
})
|
||||
|
||||
const providerId = screen._json.props._id
|
||||
const container = makeMainContainer()
|
||||
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
|
||||
.instanceName("Field Group")
|
||||
.customProps({
|
||||
labelPosition: "left",
|
||||
})
|
||||
|
||||
// Add all form fields from this schema to the field group
|
||||
const datasource = { type: "table", tableId: table._id }
|
||||
makeDatasourceFormComponents(datasource).forEach(component => {
|
||||
fieldGroup.addChild(component)
|
||||
})
|
||||
|
||||
// Add all children to the form
|
||||
const formId = form._json._id
|
||||
form
|
||||
.addChild(makeBreadcrumbContainer(table.name, "New"))
|
||||
.addChild(generateTitleContainer(table, providerId))
|
||||
.addChild(dataform)
|
||||
.addChild(generateTitleContainer(table, formId))
|
||||
.addChild(fieldGroup)
|
||||
|
||||
return screen.addChild(container).json()
|
||||
return screen.addChild(form).json()
|
||||
}
|
||||
|
|
|
@ -4,20 +4,19 @@ import { Screen } from "./utils/Screen"
|
|||
import { Component } from "./utils/Component"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
import {
|
||||
makeMainContainer,
|
||||
makeBreadcrumbContainer,
|
||||
makeTitleContainer,
|
||||
makeSaveButton,
|
||||
makeMainForm,
|
||||
spectrumColor,
|
||||
makeDatasourceFormComponents,
|
||||
} from "./utils/commonComponents"
|
||||
|
||||
export default function(tables) {
|
||||
return tables.map(table => {
|
||||
const heading = table.primaryDisplay
|
||||
? `{{ data.${makePropSafe(table.primaryDisplay)} }}`
|
||||
: null
|
||||
return {
|
||||
name: `${table.name} - Detail`,
|
||||
create: () => createScreen(table, heading),
|
||||
create: () => createScreen(table),
|
||||
id: ROW_DETAIL_TEMPLATE,
|
||||
}
|
||||
})
|
||||
|
@ -26,9 +25,9 @@ export default function(tables) {
|
|||
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
|
||||
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
|
||||
|
||||
function generateTitleContainer(table, title, providerId) {
|
||||
function generateTitleContainer(table, title, formId) {
|
||||
// have to override style for this, its missing margin
|
||||
const saveButton = makeSaveButton(table, providerId).normalStyle({
|
||||
const saveButton = makeSaveButton(table, formId).normalStyle({
|
||||
background: "#000000",
|
||||
"border-width": "0",
|
||||
"border-style": "None",
|
||||
|
@ -54,6 +53,7 @@ function generateTitleContainer(table, title, providerId) {
|
|||
background: "transparent",
|
||||
color: "#4285f4",
|
||||
})
|
||||
.customStyle(spectrumColor(700))
|
||||
.text("Delete")
|
||||
.customProps({
|
||||
className: "",
|
||||
|
@ -61,8 +61,9 @@ function generateTitleContainer(table, title, providerId) {
|
|||
onClick: [
|
||||
{
|
||||
parameters: {
|
||||
rowId: `{{ ${makePropSafe(providerId)}._id }}`,
|
||||
revId: `{{ ${makePropSafe(providerId)}._rev }}`,
|
||||
providerId: formId,
|
||||
rowId: `{{ ${makePropSafe(formId)}._id }}`,
|
||||
revId: `{{ ${makePropSafe(formId)}._rev }}`,
|
||||
tableId: table._id,
|
||||
},
|
||||
"##eventHandlerType": "Delete Row",
|
||||
|
@ -82,23 +83,47 @@ function generateTitleContainer(table, title, providerId) {
|
|||
.addChild(saveButton)
|
||||
}
|
||||
|
||||
const createScreen = (table, heading) => {
|
||||
const createScreen = table => {
|
||||
const screen = new Screen()
|
||||
.component("@budibase/standard-components/rowdetail")
|
||||
.table(table._id)
|
||||
.instanceName(`${table.name} - Detail`)
|
||||
.route(rowDetailUrl(table))
|
||||
.name("")
|
||||
|
||||
const dataform = new Component(
|
||||
"@budibase/standard-components/dataformwide"
|
||||
).instanceName("Form")
|
||||
const form = makeMainForm()
|
||||
.instanceName("Form")
|
||||
.customProps({
|
||||
theme: "spectrum--lightest",
|
||||
size: "spectrum--medium",
|
||||
datasource: {
|
||||
label: table.name,
|
||||
tableId: table._id,
|
||||
type: "table",
|
||||
},
|
||||
})
|
||||
|
||||
const providerId = screen._json.props._id
|
||||
const container = makeMainContainer()
|
||||
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
|
||||
.instanceName("Field Group")
|
||||
.customProps({
|
||||
labelPosition: "left",
|
||||
})
|
||||
|
||||
// Add all form fields from this schema to the field group
|
||||
const datasource = { type: "table", tableId: table._id }
|
||||
makeDatasourceFormComponents(datasource).forEach(component => {
|
||||
fieldGroup.addChild(component)
|
||||
})
|
||||
|
||||
// Add all children to the form
|
||||
const formId = form._json._id
|
||||
const rowDetailId = screen._json.props._id
|
||||
const heading = table.primaryDisplay
|
||||
? `{{ ${makePropSafe(rowDetailId)}.${makePropSafe(table.primaryDisplay)} }}`
|
||||
: null
|
||||
form
|
||||
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
|
||||
.addChild(generateTitleContainer(table, heading || "Edit Row", providerId))
|
||||
.addChild(dataform)
|
||||
.addChild(generateTitleContainer(table, heading || "Edit Row", formId))
|
||||
.addChild(fieldGroup)
|
||||
|
||||
return screen.addChild(container).json()
|
||||
return screen.addChild(form).json()
|
||||
}
|
||||
|
|
|
@ -14,17 +14,11 @@ export class Component extends BaseStructure {
|
|||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
type: "",
|
||||
_instanceName: "",
|
||||
_children: [],
|
||||
}
|
||||
}
|
||||
|
||||
type(type) {
|
||||
this._json.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
normalStyle(styling) {
|
||||
this._json._styles.normal = styling
|
||||
return this
|
||||
|
@ -35,14 +29,25 @@ export class Component extends BaseStructure {
|
|||
return this
|
||||
}
|
||||
|
||||
text(text) {
|
||||
this._json.text = text
|
||||
customStyle(styling) {
|
||||
this._json._styles.custom = styling
|
||||
return this
|
||||
}
|
||||
|
||||
// TODO: do we need this
|
||||
instanceName(name) {
|
||||
this._json._instanceName = name
|
||||
return this
|
||||
}
|
||||
|
||||
// Shorthand for custom props "type"
|
||||
type(type) {
|
||||
this._json.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
// Shorthand for custom props "text"
|
||||
text(text) {
|
||||
this._json.text = text
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
import { Component } from "./Component"
|
||||
import { rowListUrl } from "../rowListScreen"
|
||||
import { getSchemaForDatasource } from "../../../dataBinding"
|
||||
|
||||
export function spectrumColor(number) {
|
||||
// Acorn throws a parsing error in this file if the word g-l-o-b-a-l is found
|
||||
// (without dashes - I can't even type it in a comment).
|
||||
// God knows why. It seems to think optional chaining further down the
|
||||
// file is invalid if the word g-l-o-b-a-l is found - hence the reason this
|
||||
// statement is split into parts.
|
||||
return "color: var(--spectrum-glo" + `bal-color-gray-${number});`
|
||||
}
|
||||
|
||||
export function makeLinkComponent(tableName) {
|
||||
return new Component("@budibase/standard-components/link")
|
||||
|
@ -10,6 +20,7 @@ export function makeLinkComponent(tableName) {
|
|||
.hoverStyle({
|
||||
color: "#4285f4",
|
||||
})
|
||||
.customStyle(spectrumColor(700))
|
||||
.text(tableName)
|
||||
.customProps({
|
||||
url: `/${tableName.toLowerCase()}`,
|
||||
|
@ -22,13 +33,12 @@ export function makeLinkComponent(tableName) {
|
|||
})
|
||||
}
|
||||
|
||||
export function makeMainContainer() {
|
||||
return new Component("@budibase/standard-components/container")
|
||||
export function makeMainForm() {
|
||||
return new Component("@budibase/standard-components/form")
|
||||
.type("div")
|
||||
.normalStyle({
|
||||
width: "700px",
|
||||
padding: "0px",
|
||||
background: "white",
|
||||
"border-radius": "0.5rem",
|
||||
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
||||
margin: "auto",
|
||||
|
@ -39,7 +49,7 @@ export function makeMainContainer() {
|
|||
"padding-left": "48px",
|
||||
"margin-bottom": "20px",
|
||||
})
|
||||
.instanceName("Container")
|
||||
.instanceName("Form")
|
||||
}
|
||||
|
||||
export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
||||
|
@ -51,6 +61,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
"margin-right": "4px",
|
||||
"margin-left": "4px",
|
||||
})
|
||||
.customStyle(spectrumColor(700))
|
||||
.text(">")
|
||||
.instanceName("Arrow")
|
||||
|
||||
|
@ -63,6 +74,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
const identifierText = new Component("@budibase/standard-components/text")
|
||||
.type("none")
|
||||
.normalStyle(textStyling)
|
||||
.customStyle(spectrumColor(700))
|
||||
.text(text)
|
||||
.instanceName("Identifier")
|
||||
|
||||
|
@ -78,7 +90,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
.addChild(identifierText)
|
||||
}
|
||||
|
||||
export function makeSaveButton(table, providerId) {
|
||||
export function makeSaveButton(table, formId) {
|
||||
return new Component("@budibase/standard-components/button")
|
||||
.normalStyle({
|
||||
background: "#000000",
|
||||
|
@ -99,8 +111,14 @@ export function makeSaveButton(table, providerId) {
|
|||
disabled: false,
|
||||
onClick: [
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
parameters: {
|
||||
providerId,
|
||||
componentId: formId,
|
||||
},
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
providerId: formId,
|
||||
},
|
||||
"##eventHandlerType": "Save Row",
|
||||
},
|
||||
|
@ -125,6 +143,7 @@ export function makeTitleContainer(title) {
|
|||
"margin-left": "0px",
|
||||
flex: "1 1 auto",
|
||||
})
|
||||
.customStyle(spectrumColor(900))
|
||||
.type("h3")
|
||||
.instanceName("Title")
|
||||
.text(title)
|
||||
|
@ -142,3 +161,55 @@ export function makeTitleContainer(title) {
|
|||
.instanceName("Title Container")
|
||||
.addChild(heading)
|
||||
}
|
||||
|
||||
const fieldTypeToComponentMap = {
|
||||
string: "stringfield",
|
||||
number: "numberfield",
|
||||
options: "optionsfield",
|
||||
boolean: "booleanfield",
|
||||
longform: "longformfield",
|
||||
datetime: "datetimefield",
|
||||
attachment: "attachmentfield",
|
||||
link: "relationshipfield",
|
||||
}
|
||||
|
||||
export function makeDatasourceFormComponents(datasource) {
|
||||
const { schema } = getSchemaForDatasource(datasource, true)
|
||||
let components = []
|
||||
let fields = Object.keys(schema || {})
|
||||
fields.forEach(field => {
|
||||
const fieldSchema = schema[field]
|
||||
// skip autocolumns
|
||||
if (fieldSchema.autocolumn) {
|
||||
return
|
||||
}
|
||||
const fieldType =
|
||||
typeof fieldSchema === "object" ? fieldSchema.type : fieldSchema
|
||||
const componentType = fieldTypeToComponentMap[fieldType]
|
||||
const fullComponentType = `@budibase/standard-components/${componentType}`
|
||||
if (componentType) {
|
||||
const component = new Component(fullComponentType)
|
||||
.instanceName(field)
|
||||
.customProps({
|
||||
field,
|
||||
label: field,
|
||||
placeholder: field,
|
||||
})
|
||||
if (fieldType === "options") {
|
||||
component.customProps({ placeholder: "Choose an option " })
|
||||
}
|
||||
if (fieldType === "link") {
|
||||
let placeholder =
|
||||
fieldSchema.relationshipType === "one-to-many"
|
||||
? "Choose an option"
|
||||
: "Choose some options"
|
||||
component.customProps({ placeholder })
|
||||
}
|
||||
if (fieldType === "boolean") {
|
||||
component.customProps({ text: field, label: "" })
|
||||
}
|
||||
components.push(component)
|
||||
}
|
||||
})
|
||||
return components
|
||||
}
|
||||
|
|
|
@ -59,8 +59,8 @@ export const findComponentPath = (rootComponent, id, path = []) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Recurses through the component tree and finds all components of a certain
|
||||
* type.
|
||||
* Recurses through the component tree and finds all components which match
|
||||
* a certain selector
|
||||
*/
|
||||
export const findAllMatchingComponents = (rootComponent, selector) => {
|
||||
if (!rootComponent || !selector) {
|
||||
|
@ -81,6 +81,26 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
|
|||
return components.reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the closes parent component which matches certain criteria
|
||||
*/
|
||||
export const findClosestMatchingComponent = (
|
||||
rootComponent,
|
||||
componentId,
|
||||
selector
|
||||
) => {
|
||||
if (!selector) {
|
||||
return null
|
||||
}
|
||||
const componentPath = findComponentPath(rootComponent, componentId).reverse()
|
||||
for (let component of componentPath) {
|
||||
if (selector(component)) {
|
||||
return component
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses through a component tree evaluating a matching function against
|
||||
* components until a match is found
|
||||
|
|
|
@ -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 {
|
||||
TextArea,
|
||||
Label,
|
||||
Input,
|
||||
Heading,
|
||||
Body,
|
||||
|
|
|
@ -30,20 +30,22 @@
|
|||
{#if schemaFields.length}
|
||||
<div class="schema-fields">
|
||||
{#each schemaFields as [field, schema]}
|
||||
{#if schemaHasOptions(schema)}
|
||||
<Select label={field} extraThin secondary bind:value={value[field]}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each schema.constraints.inclusion as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if schema.type === 'string' || schema.type === 'number'}
|
||||
<BindableInput
|
||||
extraThin
|
||||
bind:value={value[field]}
|
||||
label={field}
|
||||
type="string"
|
||||
{bindings} />
|
||||
{#if !schema.autocolumn}
|
||||
{#if schemaHasOptions(schema)}
|
||||
<Select label={field} extraThin secondary bind:value={value[field]}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each schema.constraints.inclusion as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if schema.type === 'string' || schema.type === 'number'}
|
||||
<BindableInput
|
||||
extraThin
|
||||
bind:value={value[field]}
|
||||
label={field}
|
||||
type="string"
|
||||
{bindings} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -5,15 +5,17 @@
|
|||
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||
import ExportButton from "./buttons/ExportButton.svelte"
|
||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||
import * as api from "./api"
|
||||
import Table from "./Table.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
|
||||
let hideAutocolumns = true
|
||||
let data = []
|
||||
let loading = false
|
||||
|
||||
$: isUsersTable = $backendUiStore.selectedTable?._id === TableNames.USERS
|
||||
$: title = $backendUiStore.selectedTable.name
|
||||
$: schema = $backendUiStore.selectedTable.schema
|
||||
|
@ -40,6 +42,7 @@
|
|||
tableId={$backendUiStore.selectedTable?._id}
|
||||
{data}
|
||||
allowEditing={true}
|
||||
bind:hideAutocolumns
|
||||
{loading}>
|
||||
<CreateColumnButton />
|
||||
{#if schema && Object.keys(schema).length > 0}
|
||||
|
@ -47,9 +50,12 @@
|
|||
title={isUsersTable ? 'Create New User' : 'Create New Row'}
|
||||
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
|
||||
<CreateViewButton />
|
||||
<ManageAccessButton resourceId={$backendUiStore.selectedTable?._id} />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<!-- always have the export last -->
|
||||
<ExportButton view={tableView} />
|
||||
{/if}
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
</Table>
|
||||
|
|
|
@ -11,8 +11,9 @@
|
|||
import { capitalise } from "../../../helpers"
|
||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||
|
||||
export let defaultValue
|
||||
export let meta
|
||||
export let value = meta.type === "boolean" ? false : ""
|
||||
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
||||
export let readonly
|
||||
|
||||
$: type = meta.type
|
||||
|
@ -36,7 +37,9 @@
|
|||
{:else if type === 'boolean'}
|
||||
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
||||
{:else if type === 'link'}
|
||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||
<div>
|
||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||
</div>
|
||||
{:else if type === 'longform'}
|
||||
<div>
|
||||
<Label extraSmall grey>{label}</Label>
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
export let allowEditing = false
|
||||
export let loading = false
|
||||
export let theme = "alpine"
|
||||
export let hideAutocolumns
|
||||
|
||||
let columnDefs = []
|
||||
let selectedRows = []
|
||||
|
@ -85,8 +86,12 @@
|
|||
return !(isUsersTable && ["email", "roleId"].includes(key))
|
||||
}
|
||||
|
||||
Object.entries(schema || {}).forEach(([key, value]) => {
|
||||
result.push({
|
||||
for (let [key, value] of Object.entries(schema || {})) {
|
||||
// skip autocolumns if hiding
|
||||
if (hideAutocolumns && value.autocolumn) {
|
||||
continue
|
||||
}
|
||||
let config = {
|
||||
headerCheckboxSelection: false,
|
||||
headerComponent: TableHeader,
|
||||
headerComponentParams: {
|
||||
|
@ -107,9 +112,14 @@
|
|||
autoHeight: true,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
// sort auto-columns to the end if they are present
|
||||
if (value.autocolumn) {
|
||||
result.push(config)
|
||||
} else {
|
||||
result.unshift(config)
|
||||
}
|
||||
}
|
||||
columnDefs = result
|
||||
}
|
||||
|
||||
|
@ -150,13 +160,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="grid-wrapper">
|
||||
<AgGrid
|
||||
{theme}
|
||||
{options}
|
||||
{data}
|
||||
{columnDefs}
|
||||
{loading}
|
||||
on:select={({ detail }) => (selectedRows = detail)} />
|
||||
{#key columnDefs.length}
|
||||
<AgGrid
|
||||
{theme}
|
||||
{options}
|
||||
{data}
|
||||
{columnDefs}
|
||||
{loading}
|
||||
on:select={({ detail }) => (selectedRows = detail)} />
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -289,4 +301,13 @@
|
|||
padding-top: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
:global(.ag-header) {
|
||||
height: 61px !important;
|
||||
min-height: 61px !important;
|
||||
}
|
||||
|
||||
:global(.ag-header-row) {
|
||||
height: 60px !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount, onDestroy } from "svelte"
|
||||
import { Modal, ModalContent } from "@budibase/bbui"
|
||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
const SORT_ICON_MAP = {
|
||||
asc: "ri-arrow-down-fill",
|
||||
|
@ -51,6 +52,8 @@
|
|||
column.removeEventListener("sortChanged", setSort)
|
||||
column.removeEventListener("filterActiveChanged", setFilterActive)
|
||||
})
|
||||
|
||||
$: type = FIELDS[field?.type?.toUpperCase()]?.name
|
||||
</script>
|
||||
|
||||
<header
|
||||
|
@ -58,9 +61,17 @@
|
|||
data-cy="table-header"
|
||||
on:mouseover={() => (hovered = true)}
|
||||
on:mouseleave={() => (hovered = false)}>
|
||||
<div>
|
||||
<span class="column-header-name">{displayName}</span>
|
||||
<i class={`${SORT_ICON_MAP[sortDirection]} sort-icon`} />
|
||||
<div class="column-header">
|
||||
<div class="column-header-text">
|
||||
<div class="column-header-name">
|
||||
{displayName}
|
||||
{#if field.autocolumn}<i class="auto ri-magic-fill" />{/if}
|
||||
</div>
|
||||
{#if type}
|
||||
<div class="column-header-type">{type}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<i class={`${SORT_ICON_MAP[sortDirection]} icon`} />
|
||||
</div>
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
|
@ -73,11 +84,11 @@
|
|||
<section class:show={hovered || filterActive}>
|
||||
{#if editable && hovered}
|
||||
<span on:click|stopPropagation={showModal}>
|
||||
<i class="ri-pencil-line" />
|
||||
<i class="ri-pencil-line icon" />
|
||||
</span>
|
||||
{/if}
|
||||
<span on:click|stopPropagation={toggleMenu} bind:this={menuButton}>
|
||||
<i class="ri-filter-line" class:active={filterActive} />
|
||||
<i class="ri-filter-line icon" class:active={filterActive} />
|
||||
</span>
|
||||
</section>
|
||||
</header>
|
||||
|
@ -103,6 +114,23 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.column-header-text {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.column-header-name {
|
||||
white-space: normal !important;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -112,23 +140,31 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
.column-header-type {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--grey-6);
|
||||
}
|
||||
|
||||
i {
|
||||
.icon {
|
||||
transition: 0.2s all;
|
||||
font-size: var(--font-size-m);
|
||||
font-weight: 500;
|
||||
}
|
||||
.auto {
|
||||
font-size: 9px;
|
||||
transition: none;
|
||||
position: relative;
|
||||
margin-left: 2px;
|
||||
top: -3px;
|
||||
color: var(--grey-6);
|
||||
}
|
||||
|
||||
i:hover {
|
||||
.icon:hover {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
i.active,
|
||||
i:hover {
|
||||
.icon.active,
|
||||
.icon:hover {
|
||||
color: var(--blue);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
import GroupByButton from "./buttons/GroupByButton.svelte"
|
||||
import FilterButton from "./buttons/FilterButton.svelte"
|
||||
import ExportButton from "./buttons/ExportButton.svelte"
|
||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||
|
||||
export let view = {}
|
||||
let hideAutocolumns = true
|
||||
|
||||
let data = []
|
||||
let loading = false
|
||||
|
@ -47,11 +50,18 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Table title={decodeURI(name)} schema={view.schema} {data} {loading}>
|
||||
<Table
|
||||
title={decodeURI(name)}
|
||||
schema={view.schema}
|
||||
{data}
|
||||
{loading}
|
||||
bind:hideAutocolumns>
|
||||
<FilterButton {view} />
|
||||
<CalculateButton {view} />
|
||||
{#if view.calculation}
|
||||
<GroupByButton {view} />
|
||||
{/if}
|
||||
<ManageAccessButton resourceId={decodeURI(name)} />
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<ExportButton {view} />
|
||||
</Table>
|
||||
|
|
|
@ -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 selectRelationship
|
||||
|
||||
$: count =
|
||||
row && columnName && Array.isArray(row[columnName])
|
||||
? row[columnName].length
|
||||
: 0
|
||||
$: items = row?.[columnName] || []
|
||||
</script>
|
||||
|
||||
<div class:link={count} on:click={() => selectRelationship(row, columnName)}>
|
||||
{count}
|
||||
related row(s)
|
||||
<div
|
||||
class="container"
|
||||
class:link={!!items.length}
|
||||
on:click={() => selectRelationship(row, columnName)}>
|
||||
{#each items as item}
|
||||
<div class="item">{item}</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--grey-6);
|
||||
cursor: pointer;
|
||||
}
|
||||
.link:hover .item {
|
||||
color: var(--ink);
|
||||
border-color: var(--ink);
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: var(--font-size-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-s);
|
||||
border: 1px solid var(--grey-5);
|
||||
color: var(--grey-7);
|
||||
line-height: normal;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
<script>
|
||||
import { Input, Button, TextButton, Select, Toggle } from "@budibase/bbui"
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
Label,
|
||||
TextButton,
|
||||
Select,
|
||||
Toggle,
|
||||
Radio,
|
||||
} from "@budibase/bbui"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { FIELDS, AUTO_COLUMN_SUB_TYPES } from "constants/backend"
|
||||
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import ValuesList from "components/common/ValuesList.svelte"
|
||||
import DatePicker from "components/common/DatePicker.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
||||
const AUTO_COL = "auto"
|
||||
const LINK_TYPE = FIELDS.LINK.type
|
||||
let fieldDefinitions = cloneDeep(FIELDS)
|
||||
|
||||
export let onClosed
|
||||
|
@ -24,6 +35,18 @@
|
|||
let primaryDisplay =
|
||||
$backendUiStore.selectedTable.primaryDisplay == null ||
|
||||
$backendUiStore.selectedTable.primaryDisplay === field.name
|
||||
|
||||
let relationshipTypes = [
|
||||
{ text: "Many to many (N:N)", value: "many-to-many" },
|
||||
{ text: "One to many (1:N)", value: "one-to-many" },
|
||||
]
|
||||
let types = ["Many to many (N:N)", "One to many (1:N)"]
|
||||
|
||||
let selectedRelationshipType =
|
||||
relationshipTypes.find(type => type.value === field.relationshipType)
|
||||
?.text || "Many to many (N:N)"
|
||||
|
||||
let indexes = [...($backendUiStore.selectedTable.indexes || [])]
|
||||
let confirmDeleteDialog
|
||||
let deletion
|
||||
|
||||
|
@ -34,13 +57,38 @@
|
|||
$: uneditable =
|
||||
$backendUiStore.selectedTable?._id === TableNames.USERS &&
|
||||
UNEDITABLE_USER_FIELDS.includes(field.name)
|
||||
$: invalid = field.type === FIELDS.LINK.type && !field.tableId
|
||||
|
||||
// used to select what different options can be displayed for column type
|
||||
$: canBeSearched =
|
||||
field.type !== LINK_TYPE &&
|
||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
|
||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
||||
$: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_COL
|
||||
$: canBeRequired =
|
||||
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL
|
||||
|
||||
async function saveColumn() {
|
||||
// Set relationship type if it's
|
||||
if (field.type === "link") {
|
||||
field.relationshipType = relationshipTypes.find(
|
||||
type => type.text === selectedRelationshipType
|
||||
).value
|
||||
}
|
||||
|
||||
if (field.type === AUTO_COL) {
|
||||
field = buildAutoColumn(
|
||||
$backendUiStore.draftTable.name,
|
||||
field.name,
|
||||
field.subtype
|
||||
)
|
||||
}
|
||||
backendUiStore.update(state => {
|
||||
backendUiStore.actions.tables.saveField({
|
||||
originalName,
|
||||
field,
|
||||
primaryDisplay,
|
||||
indexes,
|
||||
})
|
||||
return state
|
||||
})
|
||||
|
@ -57,12 +105,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleFieldConstraints(event) {
|
||||
const { type, constraints } = fieldDefinitions[
|
||||
event.target.value.toUpperCase()
|
||||
]
|
||||
field.type = type
|
||||
field.constraints = constraints
|
||||
function handleTypeChange(event) {
|
||||
const definition = fieldDefinitions[event.target.value.toUpperCase()]
|
||||
if (!definition) {
|
||||
return
|
||||
}
|
||||
field.type = definition.type
|
||||
field.constraints = definition.constraints
|
||||
// remove any extra fields that may not be related to this type
|
||||
delete field.autocolumn
|
||||
delete field.subtype
|
||||
delete field.tableId
|
||||
}
|
||||
|
||||
function onChangeRequired(e) {
|
||||
|
@ -79,6 +132,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
function onChangePrimaryIndex(e) {
|
||||
indexes = e.target.checked ? [field.name] : []
|
||||
}
|
||||
|
||||
function onChangeSecondaryIndex(e) {
|
||||
if (e.target.checked) {
|
||||
indexes[1] = field.name
|
||||
} else {
|
||||
indexes = indexes.slice(0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
confirmDeleteDialog.show()
|
||||
deletion = true
|
||||
|
@ -98,14 +163,15 @@
|
|||
secondary
|
||||
thin
|
||||
label="Type"
|
||||
on:change={handleFieldConstraints}
|
||||
on:change={handleTypeChange}
|
||||
bind:value={field.type}>
|
||||
{#each Object.values(fieldDefinitions) as field}
|
||||
<option value={field.type}>{field.name}</option>
|
||||
{/each}
|
||||
<option value={AUTO_COL}>Auto Column</option>
|
||||
</Select>
|
||||
|
||||
{#if field.type !== 'link' && !uneditable}
|
||||
{#if canBeRequired}
|
||||
<Toggle
|
||||
checked={required}
|
||||
on:change={onChangeRequired}
|
||||
|
@ -114,12 +180,28 @@
|
|||
text="Required" />
|
||||
{/if}
|
||||
|
||||
{#if field.type !== 'link'}
|
||||
{#if canBeDisplay}
|
||||
<Toggle
|
||||
bind:checked={primaryDisplay}
|
||||
on:change={onChangePrimaryDisplay}
|
||||
thin
|
||||
text="Use as table display column" />
|
||||
|
||||
<Label grey small>Search Indexes</Label>
|
||||
{/if}
|
||||
{#if canBeSearched}
|
||||
<Toggle
|
||||
checked={indexes[0] === field.name}
|
||||
disabled={indexes[1] === field.name}
|
||||
on:change={onChangePrimaryIndex}
|
||||
thin
|
||||
text="Primary" />
|
||||
<Toggle
|
||||
checked={indexes[1] === field.name}
|
||||
disabled={!indexes[0] || indexes[0] === field.name}
|
||||
on:change={onChangeSecondaryIndex}
|
||||
thin
|
||||
text="Secondary" />
|
||||
{/if}
|
||||
|
||||
{#if field.type === 'string'}
|
||||
|
@ -149,6 +231,20 @@
|
|||
label="Max Value"
|
||||
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
||||
{:else if field.type === 'link'}
|
||||
<div>
|
||||
<Label grey extraSmall>Select relationship type</Label>
|
||||
<div class="radio-buttons">
|
||||
{#each types as type}
|
||||
<Radio
|
||||
disabled={originalName}
|
||||
name="Relationship type"
|
||||
value={type}
|
||||
bind:group={selectedRelationshipType}>
|
||||
<label for={type}>{type}</label>
|
||||
</Radio>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<Select label="Table" thin secondary bind:value={field.tableId}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each tableOptions as table}
|
||||
|
@ -159,13 +255,22 @@
|
|||
label={`Column Name in Other Table`}
|
||||
thin
|
||||
bind:value={field.fieldName} />
|
||||
{:else if field.type === AUTO_COL}
|
||||
<Select label="Auto Column Type" thin secondary bind:value={field.subtype}>
|
||||
<option value="">Choose a subtype</option>
|
||||
{#each Object.entries(getAutoColumnInformation()) as [subtype, info]}
|
||||
<option value={subtype}>{info.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
<footer class="create-column-options">
|
||||
{#if !uneditable && originalName}
|
||||
{#if !uneditable && originalName != null}
|
||||
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
|
||||
{/if}
|
||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||
<Button primary on:click={saveColumn}>Save Column</Button>
|
||||
<Button primary on:click={saveColumn} bind:disabled={invalid}>
|
||||
Save Column
|
||||
</Button>
|
||||
</footer>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
|
@ -177,6 +282,15 @@
|
|||
title="Confirm Deletion" />
|
||||
|
||||
<style>
|
||||
label {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.radio-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-xl);
|
||||
|
|
|
@ -40,9 +40,11 @@
|
|||
onConfirm={saveRow}>
|
||||
<ErrorsBox {errors} />
|
||||
{#each tableSchema as [key, meta]}
|
||||
<div>
|
||||
<RowFieldControl {meta} bind:value={row[key]} />
|
||||
</div>
|
||||
{#if !meta.autocolumn}
|
||||
<div>
|
||||
<RowFieldControl {meta} bind:value={row[key]} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</ModalContent>
|
||||
|
||||
|
|
|
@ -29,15 +29,31 @@
|
|||
let customSchema = { ...schema }
|
||||
delete customSchema["email"]
|
||||
delete customSchema["roleId"]
|
||||
delete customSchema["status"]
|
||||
return Object.entries(customSchema)
|
||||
}
|
||||
|
||||
const saveRow = async () => {
|
||||
errors = []
|
||||
|
||||
// Do some basic front end validation first
|
||||
if (!row.email) {
|
||||
errors = [...errors, { message: "Email is required" }]
|
||||
}
|
||||
if (!row.password) {
|
||||
errors = [...errors, { message: "Password is required" }]
|
||||
}
|
||||
if (!row.roleId) {
|
||||
errors = [...errors, { message: "Role is required" }]
|
||||
}
|
||||
if (errors.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const rowResponse = await backendApi.saveRow(
|
||||
{ ...row, tableId: table._id },
|
||||
table._id
|
||||
)
|
||||
|
||||
if (rowResponse.errors) {
|
||||
if (Array.isArray(rowResponse.errors)) {
|
||||
errors = rowResponse.errors.map(error => ({ message: error }))
|
||||
|
@ -47,6 +63,9 @@
|
|||
.flat()
|
||||
}
|
||||
return false
|
||||
} else if (rowResponse.status === 400 && rowResponse.message) {
|
||||
errors = [{ message: rowResponse.message }]
|
||||
return false
|
||||
}
|
||||
|
||||
notifier.success("User saved successfully.")
|
||||
|
@ -79,7 +98,13 @@
|
|||
<option value={role._id}>{role.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
<RowFieldControl
|
||||
meta={{ name: 'status', type: 'options', constraints: { inclusion: ['active', 'inactive'] } }}
|
||||
bind:value={row.status}
|
||||
defaultValue={'active'} />
|
||||
{#each customSchemaKeys as [key, meta]}
|
||||
<RowFieldControl {meta} bind:value={row[key]} {creating} />
|
||||
{#if !meta.autocolumn}
|
||||
<RowFieldControl {meta} bind:value={row[key]} {creating} />
|
||||
{/if}
|
||||
{/each}
|
||||
</ModalContent>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
let permissions = []
|
||||
let basePermissions = []
|
||||
let selectedRole = {}
|
||||
let errors = []
|
||||
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
||||
|
@ -16,9 +16,9 @@
|
|||
)
|
||||
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
||||
|
||||
const fetchPermissions = async () => {
|
||||
const permissionsResponse = await api.get("/api/permissions")
|
||||
permissions = await permissionsResponse.json()
|
||||
const fetchBasePermissions = async () => {
|
||||
const permissionsResponse = await api.get("/api/permission/builtin")
|
||||
basePermissions = await permissionsResponse.json()
|
||||
}
|
||||
|
||||
// Changes the selected role
|
||||
|
@ -81,7 +81,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
onMount(fetchPermissions)
|
||||
onMount(fetchBasePermissions)
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
@ -121,11 +121,11 @@
|
|||
<Select
|
||||
thin
|
||||
secondary
|
||||
label="Permissions"
|
||||
label="Base Permissions"
|
||||
bind:value={selectedRole.permissionId}>
|
||||
<option value="">Choose permissions</option>
|
||||
{#each permissions as permission}
|
||||
<option value={permission._id}>{permission.name}</option>
|
||||
{#each basePermissions as basePerm}
|
||||
<option value={basePerm._id}>{basePerm.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
|
|
|
@ -30,7 +30,9 @@
|
|||
Object.keys(viewTable.schema).filter(
|
||||
field =>
|
||||
view.calculation === "count" ||
|
||||
viewTable.schema[field].type === "number"
|
||||
// don't want to perform calculations based on auto ID
|
||||
(viewTable.schema[field].type === "number" &&
|
||||
!viewTable.schema[field].autocolumn)
|
||||
)
|
||||
|
||||
function saveView() {
|
||||
|
|
|
@ -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>
|
||||
import { Input, TextArea, Spacer } from "@budibase/bbui"
|
||||
import { Label, Input, TextArea, Spacer } from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
|
||||
export let integration
|
||||
export let schema
|
||||
|
||||
let unsaved = false
|
||||
</script>
|
||||
|
||||
<form>
|
||||
{#each Object.keys(integration) as configKey}
|
||||
<Input
|
||||
type={integration[configKey].type}
|
||||
label={configKey}
|
||||
bind:value={integration[configKey]} />
|
||||
<Spacer large />
|
||||
{#each Object.keys(schema) as configKey}
|
||||
{#if typeof schema[configKey].type === 'object'}
|
||||
<Label small>{configKey}</Label>
|
||||
<Spacer small />
|
||||
<KeyValueBuilder bind:object={integration[configKey]} on:change />
|
||||
{:else}
|
||||
<div class="form-row">
|
||||
<Label small>{configKey}</Label>
|
||||
<Input
|
||||
outline
|
||||
type={schema[configKey].type}
|
||||
on:change
|
||||
bind:value={integration[configKey]} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
import { onMount } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import { Input, TextArea, Spacer } from "@budibase/bbui"
|
||||
import { Input, Label, TextArea, Spacer } from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import ICONS from "../icons"
|
||||
|
||||
export let integration = {}
|
||||
|
@ -49,17 +50,6 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if schema}
|
||||
{#each Object.keys(schema) as configKey}
|
||||
<Input
|
||||
thin
|
||||
type={schema[configKey].type}
|
||||
label={configKey}
|
||||
bind:value={integration[configKey]} />
|
||||
<Spacer medium />
|
||||
{/each}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -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 MySQL from "./MySQL.svelte"
|
||||
import ArangoDB from "./ArangoDB.svelte"
|
||||
import Rest from "./Rest.svelte"
|
||||
|
||||
export default {
|
||||
POSTGRES: Postgres,
|
||||
|
@ -20,4 +21,5 @@ export default {
|
|||
AIRTABLE: Airtable,
|
||||
MYSQL: MySQL,
|
||||
ARANGODB: ArangoDB,
|
||||
REST: Rest,
|
||||
}
|
||||
|
|
|
@ -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 { DropdownMenu, Button, Input } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
|
||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||
|
||||
export let datasource
|
||||
|
|
|
@ -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 { DropdownMenu, Button, Input } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
|
||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||
|
||||
export let query
|
||||
|
|
|
@ -2,17 +2,11 @@
|
|||
import { goto } from "@sveltech/routify"
|
||||
import { backendUiStore, store } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
ModalContent,
|
||||
Button,
|
||||
Spacer,
|
||||
Toggle,
|
||||
} from "@budibase/bbui"
|
||||
import { Input, Label, ModalContent, Toggle } from "@budibase/bbui"
|
||||
import TableDataImport from "../TableDataImport.svelte"
|
||||
import analytics from "analytics"
|
||||
import screenTemplates from "builderStore/store/screenTemplates"
|
||||
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
|
||||
import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
|
||||
import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen"
|
||||
import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen"
|
||||
|
@ -23,15 +17,28 @@
|
|||
ROW_LIST_TEMPLATE,
|
||||
]
|
||||
|
||||
$: tableNames = $backendUiStore.tables.map(table => table.name)
|
||||
|
||||
let modal
|
||||
let name
|
||||
let dataImport
|
||||
let error = ""
|
||||
let createAutoscreens = true
|
||||
let autoColumns = getAutoColumnInformation()
|
||||
|
||||
function addAutoColumns(tableName, schema) {
|
||||
for (let [subtype, col] of Object.entries(autoColumns)) {
|
||||
if (!col.enabled) {
|
||||
continue
|
||||
}
|
||||
schema[col.name] = buildAutoColumn(tableName, col.name, subtype)
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
function checkValid(evt) {
|
||||
const tableName = evt.target.value
|
||||
if ($backendUiStore.models?.some(model => model.name === tableName)) {
|
||||
if (tableNames.includes(tableName)) {
|
||||
error = `Table with name ${tableName} already exists. Please choose another name.`
|
||||
return
|
||||
}
|
||||
|
@ -41,7 +48,7 @@
|
|||
async function saveTable() {
|
||||
let newTable = {
|
||||
name,
|
||||
schema: dataImport.schema || {},
|
||||
schema: addAutoColumns(name, dataImport.schema || {}),
|
||||
dataImport,
|
||||
}
|
||||
|
||||
|
@ -93,6 +100,28 @@
|
|||
on:input={checkValid}
|
||||
bind:value={name}
|
||||
{error} />
|
||||
<div class="autocolumns">
|
||||
<Label extraSmall grey>Auto Columns</Label>
|
||||
<div class="toggles">
|
||||
<div class="toggle-1">
|
||||
<Toggle
|
||||
text="Created by"
|
||||
bind:checked={autoColumns.createdBy.enabled} />
|
||||
<Toggle
|
||||
text="Created at"
|
||||
bind:checked={autoColumns.createdAt.enabled} />
|
||||
<Toggle text="Auto ID" bind:checked={autoColumns.autoID.enabled} />
|
||||
</div>
|
||||
<div class="toggle-2">
|
||||
<Toggle
|
||||
text="Updated by"
|
||||
bind:checked={autoColumns.updatedBy.enabled} />
|
||||
<Toggle
|
||||
text="Updated at"
|
||||
bind:checked={autoColumns.updatedAt.enabled} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toggle
|
||||
text="Generate screens in the design section"
|
||||
bind:checked={createAutoscreens} />
|
||||
|
@ -101,3 +130,25 @@
|
|||
<TableDataImport bind:dataImport />
|
||||
</div>
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
.autocolumns {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 3px solid var(--grey-1);
|
||||
}
|
||||
|
||||
.toggles {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.toggle-1 :global(> *) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.toggle-2 :global(> *) {
|
||||
margin-bottom: 10px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
.icon {
|
||||
right: 2px;
|
||||
top: 26px;
|
||||
top: 5px;
|
||||
bottom: 2px;
|
||||
position: absolute;
|
||||
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>
|
||||
export let icon
|
||||
export let title
|
||||
export let subtitle
|
||||
export let disabled
|
||||
export let subtitle = undefined
|
||||
export let disabled = false
|
||||
</script>
|
||||
|
||||
<div class="dropdown-item" class:disabled on:click {...$$restProps}>
|
||||
|
|
|
@ -41,13 +41,29 @@
|
|||
table.
|
||||
</Label>
|
||||
{:else}
|
||||
<Multiselect
|
||||
secondary
|
||||
bind:value={linkedRows}
|
||||
{label}
|
||||
placeholder="Choose some options">
|
||||
{#each rows as row}
|
||||
<option value={row._id}>{getPrettyName(row)}</option>
|
||||
{/each}
|
||||
</Multiselect>
|
||||
{#if schema.relationshipType === 'one-to-many'}
|
||||
<Select
|
||||
thin
|
||||
secondary
|
||||
on:change={e => (linkedRows = [e.target.value])}
|
||||
name={label}
|
||||
{label}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each rows as row}
|
||||
<option selected={row._id === linkedRows[0]} value={row._id}>
|
||||
{getPrettyName(row)}
|
||||
</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else}
|
||||
<Multiselect
|
||||
secondary
|
||||
bind:value={linkedRows}
|
||||
{label}
|
||||
placeholder="Choose some options">
|
||||
{#each rows as row}
|
||||
<option value={row._id}>{getPrettyName(row)}</option>
|
||||
{/each}
|
||||
</Multiselect>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<ModalContent title="Webhook Endpoints" confirmText="Done">
|
||||
<ModalContent title="Webhook Endpoints" confirmText="OK" showCancelButton={false}>
|
||||
<p>See below the list of deployed webhook URLs.</p>
|
||||
{#each webhookUrls as webhookUrl}
|
||||
<div>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
timeOnly: {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
hourCycle: "h12",
|
||||
},
|
||||
}
|
||||
const POLL_INTERVAL = 5000
|
||||
|
|
|
@ -3,15 +3,21 @@
|
|||
"datagrid",
|
||||
"list",
|
||||
"button",
|
||||
"search",
|
||||
{
|
||||
"name": "Form",
|
||||
"icon": "ri-file-edit-line",
|
||||
"children": [
|
||||
"dataform",
|
||||
"dataformwide",
|
||||
"input",
|
||||
"richtext",
|
||||
"datepicker"
|
||||
"form",
|
||||
"fieldgroup",
|
||||
"stringfield",
|
||||
"numberfield",
|
||||
"optionsfield",
|
||||
"booleanfield",
|
||||
"longformfield",
|
||||
"datetimefield",
|
||||
"attachmentfield",
|
||||
"relationshipfield"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -55,8 +61,8 @@
|
|||
"screenslot",
|
||||
"navigation",
|
||||
"login",
|
||||
"rowdetail",
|
||||
"newrow"
|
||||
"rowdetail"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -11,9 +11,6 @@
|
|||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<script src='/assets/budibase-client.js'></script>
|
||||
<script>
|
||||
|
|
|
@ -13,9 +13,8 @@
|
|||
let dropdown
|
||||
let anchor
|
||||
|
||||
$: noChildrenAllowed =
|
||||
!component ||
|
||||
!store.actions.components.getDefinition(component._component)?.hasChildren
|
||||
$: definition = store.actions.components.getDefinition(component?._component)
|
||||
$: noChildrenAllowed = !component || !definition?.hasChildren
|
||||
$: noPaste = !$store.componentToPaste
|
||||
|
||||
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
||||
|
@ -130,7 +129,7 @@
|
|||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`}
|
||||
body={`Are you sure you wish to delete this '${definition?.name}' component?`}
|
||||
okText="Delete Component"
|
||||
onOk={deleteComponent} />
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
$: value && checkValid()
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: dispatch("update", value)
|
||||
|
@ -114,8 +114,7 @@
|
|||
bind:getCaretPosition
|
||||
thin
|
||||
bind:value
|
||||
placeholder="Add text, or click the objects on the left to add them to
|
||||
the textbox." />
|
||||
placeholder="Add text, or click the objects on the left to add them to the textbox." />
|
||||
{#if !valid}
|
||||
<p class="syntax-error">
|
||||
Current Handlebars syntax is invalid, please check the guide
|
||||
|
@ -144,9 +143,12 @@
|
|||
}
|
||||
|
||||
.text {
|
||||
padding: var(--spacing-xl);
|
||||
padding: var(--spacing-l);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
.text :global(textarea) {
|
||||
min-height: 100px;
|
||||
}
|
||||
.text :global(p) {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
export let value = {}
|
||||
export let otherSources
|
||||
|
||||
$: tables = $backendUiStore.tables.map(m => ({
|
||||
label: m.name,
|
||||
|
@ -46,7 +47,7 @@
|
|||
type: "query",
|
||||
}))
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: queryBindableProperties = bindableProperties.map(property => ({
|
||||
|
@ -76,7 +77,7 @@
|
|||
dropdownRight.hide()
|
||||
}
|
||||
|
||||
function fetchDatasourceSchema(query) {
|
||||
function fetchQueryDefinition(query) {
|
||||
const source = $backendUiStore.datasources.find(
|
||||
ds => ds._id === query.datasourceId
|
||||
).source
|
||||
|
@ -84,43 +85,48 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="dropdownbutton"
|
||||
bind:this={anchorRight}
|
||||
on:click={dropdownRight.show}>
|
||||
<span>{value?.label ? value.label : 'Choose option'}</span>
|
||||
<Icon name="arrowdown" />
|
||||
<div class="container">
|
||||
<div
|
||||
class="dropdownbutton"
|
||||
bind:this={anchorRight}
|
||||
on:click={dropdownRight.show}>
|
||||
<span>{value?.label ?? 'Choose option'}</span>
|
||||
<Icon name="arrowdown" />
|
||||
</div>
|
||||
{#if value?.type === 'query'}
|
||||
<i class="ri-settings-5-line" on:click={drawer.show} />
|
||||
<Drawer title={'Query Parameters'} bind:this={drawer}>
|
||||
<div slot="buttons">
|
||||
<Button
|
||||
blue
|
||||
thin
|
||||
on:click={() => {
|
||||
notifier.success('Query parameters saved.')
|
||||
handleSelected(value)
|
||||
drawer.hide()
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div class="drawer-contents" slot="body">
|
||||
{#if value.parameters.length > 0}
|
||||
<ParameterBuilder
|
||||
bind:customParams={value.queryParams}
|
||||
parameters={queries.find(query => query._id === value._id).parameters}
|
||||
bindings={queryBindableProperties} />
|
||||
{/if}
|
||||
<!-- <Spacer large />-->
|
||||
<IntegrationQueryEditor
|
||||
height={200}
|
||||
query={value}
|
||||
schema={fetchQueryDefinition(value)}
|
||||
datasource={$backendUiStore.datasources.find(ds => ds._id === value.datasourceId)}
|
||||
editable={false} />
|
||||
<Spacer large />
|
||||
</div>
|
||||
</Drawer>
|
||||
{/if}
|
||||
</div>
|
||||
{#if value?.type === 'query'}
|
||||
<i class="ri-settings-5-line" on:click={drawer.show} />
|
||||
<Drawer title={'Query'} bind:this={drawer}>
|
||||
<div slot="buttons">
|
||||
<Button
|
||||
blue
|
||||
thin
|
||||
on:click={() => {
|
||||
notifier.success('Query parameters saved.')
|
||||
handleSelected(value)
|
||||
drawer.hide()
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div class="drawer-contents" slot="body">
|
||||
<IntegrationQueryEditor
|
||||
query={value}
|
||||
schema={fetchDatasourceSchema(value)}
|
||||
editable={false} />
|
||||
<Spacer large />
|
||||
{#if value.parameters.length > 0}
|
||||
<ParameterBuilder
|
||||
bind:customParams={value.queryParams}
|
||||
parameters={queries.find(query => query._id === value._id).parameters}
|
||||
bindings={queryBindableProperties} />
|
||||
{/if}
|
||||
</div>
|
||||
</Drawer>
|
||||
{/if}
|
||||
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
|
||||
<div class="dropdown">
|
||||
<div class="title">
|
||||
|
@ -175,10 +181,33 @@
|
|||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if otherSources?.length}
|
||||
<hr />
|
||||
<div class="title">
|
||||
<Heading extraSmall>Other</Heading>
|
||||
</div>
|
||||
<ul>
|
||||
{#each otherSources as source}
|
||||
<li
|
||||
class:selected={value === source}
|
||||
on:click={() => handleSelected(source)}>
|
||||
{source.label}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdownbutton {
|
||||
background-color: var(--grey-2);
|
||||
border: var(--border-transparent);
|
||||
|
@ -241,8 +270,8 @@
|
|||
}
|
||||
|
||||
.drawer-contents {
|
||||
padding: var(--spacing-xl);
|
||||
height: 40vh;
|
||||
padding: var(--spacing-l);
|
||||
height: calc(40vh - 2 * var(--spacing-l));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} type="datetime" />
|
|
@ -1,15 +1,6 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Body,
|
||||
DropdownMenu,
|
||||
ModalContent,
|
||||
Spacer,
|
||||
} from "@budibase/bbui"
|
||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||
import { Button, DropdownMenu, Spacer } from "@budibase/bbui"
|
||||
import actionTypes from "./actions"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
|
||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||
|
||||
|
@ -17,12 +8,19 @@
|
|||
|
||||
let addActionButton
|
||||
let addActionDropdown
|
||||
let selectedAction
|
||||
let selectedAction = actions?.length ? actions[0] : null
|
||||
|
||||
$: selectedActionComponent =
|
||||
selectedAction &&
|
||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component
|
||||
|
||||
// Select the first action if we delete an action
|
||||
$: {
|
||||
if (selectedAction && !actions?.includes(selectedAction)) {
|
||||
selectedAction = actions?.[0]
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAction = index => {
|
||||
actions.splice(index, 1)
|
||||
actions = actions
|
||||
|
@ -51,11 +49,10 @@
|
|||
<div class="actions-list">
|
||||
<div>
|
||||
<div bind:this={addActionButton}>
|
||||
<Spacer small />
|
||||
<Button wide secondary on:click={addActionDropdown.show}>
|
||||
Add Action
|
||||
</Button>
|
||||
<Spacer medium />
|
||||
<Spacer small />
|
||||
</div>
|
||||
<DropdownMenu
|
||||
bind:this={addActionDropdown}
|
||||
|
@ -74,11 +71,12 @@
|
|||
{#if actions && actions.length > 0}
|
||||
{#each actions as action, index}
|
||||
<div class="action-container">
|
||||
<div class="action-header" on:click={selectAction(action)}>
|
||||
<span class:selected={action === selectedAction}>
|
||||
{index + 1}.
|
||||
{action[EVENT_TYPE_KEY]}
|
||||
</span>
|
||||
<div
|
||||
class="action-header"
|
||||
class:selected={action === selectedAction}
|
||||
on:click={selectAction(action)}>
|
||||
{index + 1}.
|
||||
{action[EVENT_TYPE_KEY]}
|
||||
</div>
|
||||
<i
|
||||
class="ri-close-fill"
|
||||
|
@ -107,20 +105,22 @@
|
|||
margin-top: var(--spacing-m);
|
||||
}
|
||||
|
||||
.action-header > span {
|
||||
.action-header {
|
||||
margin-bottom: var(--spacing-m);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--grey-7);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-header > span:hover,
|
||||
.selected {
|
||||
.action-header:hover,
|
||||
.action-header.selected {
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.actions-list {
|
||||
border-right: var(--border-light);
|
||||
padding: var(--spacing-s);
|
||||
padding: var(--spacing-l);
|
||||
}
|
||||
|
||||
.available-action {
|
||||
|
@ -136,7 +136,6 @@
|
|||
.actions-container {
|
||||
height: 40vh;
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-m);
|
||||
grid-template-columns: 260px 1fr;
|
||||
grid-auto-flow: column;
|
||||
min-height: 0;
|
||||
|
@ -145,13 +144,16 @@
|
|||
}
|
||||
|
||||
.action-container {
|
||||
border-top: var(--border-light);
|
||||
border-bottom: 1px solid var(--grey-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.action-container:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.selected-action-container {
|
||||
padding: var(--spacing-xl);
|
||||
padding: var(--spacing-l);
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
@ -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(
|
||||
action => action["##eventHandlerType"] === "Trigger Automation"
|
||||
)
|
||||
automationsToCreate.forEach(action => createAutomation(action.parameters))
|
||||
for (let action of automationsToCreate) {
|
||||
await createAutomation(action.parameters)
|
||||
}
|
||||
|
||||
dispatch("change", value)
|
||||
notifier.success("Component actions saved.")
|
||||
|
@ -27,11 +29,8 @@
|
|||
// called by the parent modal when actions are saved
|
||||
const createAutomation = async parameters => {
|
||||
if (parameters.automationId || !parameters.newAutomationName) return
|
||||
|
||||
await automationStore.actions.create({ name: parameters.newAutomationName })
|
||||
|
||||
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
|
||||
|
||||
const newBlock = $automationStore.selectedAutomation.constructBlock(
|
||||
"TRIGGER",
|
||||
"APP",
|
||||
|
@ -39,19 +38,14 @@
|
|||
)
|
||||
|
||||
newBlock.inputs = {
|
||||
fields: Object.entries(parameters.fields).reduce(
|
||||
(fields, [key, value]) => {
|
||||
fields[key] = value.type
|
||||
return fields
|
||||
},
|
||||
{}
|
||||
),
|
||||
fields: Object.keys(parameters.fields).reduce((fields, key) => {
|
||||
fields[key] = "string"
|
||||
return fields
|
||||
}, {}),
|
||||
}
|
||||
|
||||
automationStore.actions.addBlockToAutomation(newBlock)
|
||||
|
||||
await automationStore.actions.save($automationStore.selectedAutomation)
|
||||
|
||||
parameters.automationId = $automationStore.selectedAutomation.automation._id
|
||||
delete parameters.newAutomationName
|
||||
}
|
||||
|
|
|
@ -10,13 +10,14 @@
|
|||
export let parameters
|
||||
|
||||
$: dataProviderComponents = getDataProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: {
|
||||
// Automatically set rev and table ID based on row ID
|
||||
if (parameters.rowId) {
|
||||
parameters.revId = parameters.rowId.replace("_id", "_rev")
|
||||
if (parameters.providerId) {
|
||||
parameters.rowId = `{{ ${parameters.providerId}._id }}`
|
||||
parameters.revId = `{{ ${parameters.providerId}._rev }}`
|
||||
const providerComponent = dataProviderComponents.find(
|
||||
provider => provider._id === parameters.providerId
|
||||
)
|
||||
|
@ -36,13 +37,11 @@
|
|||
a List
|
||||
</div>
|
||||
{:else}
|
||||
<Label size="m" color="dark">Datasource</Label>
|
||||
<Select secondary bind:value={parameters.rowId}>
|
||||
<Label small>Datasource</Label>
|
||||
<Select thin secondary bind:value={parameters.providerId}>
|
||||
<option value="" />
|
||||
{#each dataProviderComponents as provider}
|
||||
<option value={`{{ ${provider._id}._id }}`}>
|
||||
{provider._instanceName}
|
||||
</option>
|
||||
<option value={provider._id}>{provider._instanceName}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
|
@ -51,22 +50,15 @@
|
|||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-s);
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: auto 1fr auto 1fr auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.root :global(> div:nth-child(2)) {
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 6;
|
||||
}
|
||||
|
||||
.cannot-use {
|
||||
color: var(--red);
|
||||
font-size: var(--font-size-s);
|
||||
text-align: center;
|
||||
width: 70%;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,63 +3,57 @@
|
|||
import { store, backendUiStore, currentAsset } from "builderStore"
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
|
||||
export let parameters
|
||||
|
||||
$: query = $backendUiStore.queries.find(q => q._id === parameters.queryId)
|
||||
$: datasource = $backendUiStore.datasources.find(
|
||||
ds => ds._id === parameters.datasourceId
|
||||
)
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
).map(property => ({
|
||||
...property,
|
||||
category: property.type === "instance" ? "Component" : "Table",
|
||||
label: property.readableBinding,
|
||||
path: property.runtimeBinding,
|
||||
}))
|
||||
)
|
||||
|
||||
$: query =
|
||||
parameters.queryId &&
|
||||
$backendUiStore.queries.find(query => query._id === parameters.queryId)
|
||||
function fetchQueryDefinition(query) {
|
||||
const source = $backendUiStore.datasources.find(
|
||||
ds => ds._id === query.datasourceId
|
||||
).source
|
||||
return $backendUiStore.integrations[source].query[query.queryVerb]
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label size="m" color="dark">Datasource</Label>
|
||||
<Select thin secondary bind:value={parameters.datasourceId}>
|
||||
<Label small>Datasource</Label>
|
||||
<Select thin secondary bind:value={parameters.datasourceId}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.datasources as datasource}
|
||||
<option value={datasource._id}>{datasource.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
|
||||
<Spacer medium />
|
||||
|
||||
{#if parameters.datasourceId}
|
||||
<Label small>Query</Label>
|
||||
<Select thin secondary bind:value={parameters.queryId}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.datasources as datasource}
|
||||
<option value={datasource._id}>{datasource.name}</option>
|
||||
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
|
||||
<option value={query._id}>{query.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
|
||||
<Spacer medium />
|
||||
<Spacer medium />
|
||||
|
||||
{#if parameters.datasourceId}
|
||||
<Label size="m" color="dark">Query</Label>
|
||||
<Select thin secondary bind:value={parameters.queryId}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
|
||||
<option value={query._id}>{query.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
|
||||
<Spacer medium />
|
||||
|
||||
{#if query?.parameters?.length > 0}
|
||||
<ParameterBuilder
|
||||
bind:customParams={parameters.queryParams}
|
||||
parameters={query.parameters}
|
||||
bindings={bindableProperties} />
|
||||
{#if query.fields.sql}
|
||||
<pre>{query.fields.queryString}</pre>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
padding: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
{#if query?.parameters?.length > 0}
|
||||
<ParameterBuilder
|
||||
bind:customParams={parameters.queryParams}
|
||||
parameters={query.parameters}
|
||||
bindings={bindableProperties} />
|
||||
<IntegrationQueryEditor
|
||||
height={200}
|
||||
{query}
|
||||
schema={fetchQueryDefinition(query)}
|
||||
editable={false} />
|
||||
{/if}
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
<script>
|
||||
import { DataList, Label } from "@budibase/bbui"
|
||||
import { allScreens } from "builderStore"
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
|
||||
|
||||
export let parameters
|
||||
|
||||
let bindingDrawer
|
||||
let tempValue = parameters.url
|
||||
|
||||
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label size="m" color="dark">Screen</Label>
|
||||
<DataList secondary bind:value={parameters.url}>
|
||||
<option value="" />
|
||||
{#each $allScreens as screen}
|
||||
<option value={screen.routing.route}>{screen.props._instanceName}</option>
|
||||
{/each}
|
||||
</DataList>
|
||||
<Label small>Screen</Label>
|
||||
<DrawerBindableInput
|
||||
title="Destination URL"
|
||||
placeholder="/screen"
|
||||
value={parameters.url}
|
||||
on:change={value => (parameters.url = value.detail)}
|
||||
{bindings} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,115 +1,89 @@
|
|||
<script>
|
||||
import {
|
||||
DataList,
|
||||
Label,
|
||||
TextButton,
|
||||
Spacer,
|
||||
Select,
|
||||
Input,
|
||||
} from "@budibase/bbui"
|
||||
import { Label, TextButton, Spacer, Select, Input } from "@budibase/bbui"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import {
|
||||
getBindableProperties,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let parameterFields
|
||||
export let schemaFields
|
||||
export let fieldLabel = "Column"
|
||||
export let valueLabel = "Value"
|
||||
|
||||
const emptyField = () => ({ name: "", value: "" })
|
||||
|
||||
let fields = Object.entries(parameterFields || {})
|
||||
$: onChange(fields)
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
|
||||
// this statement initialises fields from parameters.fields
|
||||
$: fields =
|
||||
fields ||
|
||||
Object.keys(parameterFields || { "": "" }).map(name => ({
|
||||
name,
|
||||
value:
|
||||
(parameterFields &&
|
||||
runtimeToReadableBinding(
|
||||
bindableProperties,
|
||||
parameterFields[name].value
|
||||
)) ||
|
||||
"",
|
||||
}))
|
||||
|
||||
const addField = () => {
|
||||
const newFields = fields.filter(f => f.name)
|
||||
newFields.push(emptyField())
|
||||
fields = newFields
|
||||
rebuildParameters()
|
||||
fields = [...fields.filter(field => field[0]), ["", ""]]
|
||||
}
|
||||
|
||||
const removeField = field => () => {
|
||||
fields = fields.filter(f => f !== field)
|
||||
rebuildParameters()
|
||||
const removeField = name => {
|
||||
fields = fields.filter(field => field[0] !== name)
|
||||
}
|
||||
|
||||
const rebuildParameters = () => {
|
||||
// rebuilds paramters.fields every time a field name or value is added
|
||||
// as UI below is bound to "fields" array, but we need to output a { key: value }
|
||||
const newParameterFields = {}
|
||||
for (let field of fields) {
|
||||
if (field.name) {
|
||||
// value and type is needed by the client, so it can parse
|
||||
// a string into a correct type
|
||||
newParameterFields[field.name] = {
|
||||
type: schemaFields
|
||||
? schemaFields.find(f => f.name === field.name).type
|
||||
: "string",
|
||||
value: readableToRuntimeBinding(bindableProperties, field.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch("fieldschanged", newParameterFields)
|
||||
const updateFieldValue = (idx, value) => {
|
||||
fields[idx][1] = value
|
||||
fields = fields
|
||||
}
|
||||
|
||||
// just wraps binding in {{ ... }}
|
||||
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
|
||||
const updateFieldName = (idx, name) => {
|
||||
fields[idx][0] = name
|
||||
fields = fields
|
||||
}
|
||||
|
||||
const onChange = fields => {
|
||||
const newParamFields = {}
|
||||
fields
|
||||
.filter(field => field[0])
|
||||
.forEach(([field, value]) => {
|
||||
newParamFields[field] = value
|
||||
})
|
||||
dispatch("change", newParamFields)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if fields}
|
||||
{#each fields as field}
|
||||
<Label size="m" color="dark">{fieldLabel}</Label>
|
||||
{#each fields as field, idx}
|
||||
<Label small>{fieldLabel}</Label>
|
||||
{#if schemaFields}
|
||||
<Select secondary bind:value={field.name} on:blur={rebuildParameters}>
|
||||
<Select
|
||||
thin
|
||||
secondary
|
||||
value={field[0]}
|
||||
on:change={event => updateFieldName(idx, event.target.value)}>
|
||||
<option value="" />
|
||||
{#each schemaFields as schemaField}
|
||||
<option value={schemaField.name}>{schemaField.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else}
|
||||
<Input secondary bind:value={field.name} on:blur={rebuildParameters} />
|
||||
<Input
|
||||
thin
|
||||
secondary
|
||||
value={field[0]}
|
||||
on:change={event => updateFieldName(idx, event.target.value)} />
|
||||
{/if}
|
||||
<Label size="m" color="dark">Value</Label>
|
||||
<DataList secondary bind:value={field.value} on:blur={rebuildParameters}>
|
||||
<option value="" />
|
||||
{#each bindableProperties as bindableProp}
|
||||
<option value={toBindingExpression(bindableProp.readableBinding)}>
|
||||
{bindableProp.readableBinding}
|
||||
</option>
|
||||
{/each}
|
||||
</DataList>
|
||||
<Label small>{valueLabel}</Label>
|
||||
<DrawerBindableInput
|
||||
title={`Value for "${field[0]}"`}
|
||||
value={field[1]}
|
||||
bindings={bindableProperties}
|
||||
on:change={event => updateFieldValue(idx, event.detail)} />
|
||||
<div class="remove-field-container">
|
||||
<TextButton text small on:click={removeField(field)}>
|
||||
<TextButton text small on:click={() => removeField(field[0])}>
|
||||
<CloseCircleIcon />
|
||||
</TextButton>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div>
|
||||
<Spacer small />
|
||||
|
||||
<TextButton text small blue on:click={addField}>
|
||||
Add
|
||||
{fieldLabel}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
export let parameters
|
||||
|
||||
$: dataProviderComponents = getDataProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: providerComponent = dataProviderComponents.find(
|
||||
|
@ -37,8 +37,8 @@
|
|||
Repeater
|
||||
</div>
|
||||
{:else}
|
||||
<Label size="m" color="dark">Datasource</Label>
|
||||
<Select secondary bind:value={parameters.providerId}>
|
||||
<Label small>Datasource</Label>
|
||||
<Select thin secondary bind:value={parameters.providerId}>
|
||||
<option value="" />
|
||||
{#each dataProviderComponents as provider}
|
||||
<option value={provider._id}>{provider._instanceName}</option>
|
||||
|
@ -49,7 +49,7 @@
|
|||
<SaveFields
|
||||
parameterFields={parameters.fields}
|
||||
{schemaFields}
|
||||
on:fieldschanged={onFieldsChanged} />
|
||||
on:change={onFieldsChanged} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -57,7 +57,7 @@
|
|||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-s);
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: auto 1fr auto 1fr auto;
|
||||
align-items: baseline;
|
||||
|
@ -71,8 +71,6 @@
|
|||
.cannot-use {
|
||||
color: var(--red);
|
||||
font-size: var(--font-size-s);
|
||||
text-align: center;
|
||||
width: 70%;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -27,13 +27,11 @@
|
|||
schema,
|
||||
}
|
||||
})
|
||||
|
||||
$: hasAutomations = automations && automations.length > 0
|
||||
|
||||
$: selectedAutomation =
|
||||
parameters &&
|
||||
parameters.automationId &&
|
||||
automations.find(a => a._id === parameters.automationId)
|
||||
$: selectedAutomation = automations?.find(
|
||||
a => a._id === parameters?.automationId
|
||||
)
|
||||
$: selectedSchema = selectedAutomation?.schema
|
||||
|
||||
const onFieldsChanged = e => {
|
||||
parameters.fields = e.detail
|
||||
|
@ -42,95 +40,98 @@
|
|||
const setNew = () => {
|
||||
automationStatus = AUTOMATION_STATUS.NEW
|
||||
parameters.automationId = undefined
|
||||
parameters.fields = {}
|
||||
}
|
||||
|
||||
const setExisting = () => {
|
||||
automationStatus = AUTOMATION_STATUS.EXISTING
|
||||
parameters.newAutomationName = ""
|
||||
parameters.fields = {}
|
||||
parameters.automationId = automations[0]?._id
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="radio-container" on:click={setNew}>
|
||||
<input
|
||||
type="radio"
|
||||
value={AUTOMATION_STATUS.NEW}
|
||||
bind:group={automationStatus}
|
||||
disabled={!hasAutomations} />
|
||||
|
||||
<Label disabled={!hasAutomations}>Create a new automation</Label>
|
||||
<div class="radios">
|
||||
<div class="radio-container" on:click={setNew}>
|
||||
<input
|
||||
type="radio"
|
||||
value={AUTOMATION_STATUS.NEW}
|
||||
bind:group={automationStatus} />
|
||||
<Label small>Create a new automation</Label>
|
||||
</div>
|
||||
<div class="radio-container" on:click={hasAutomations ? setExisting : null}>
|
||||
<input
|
||||
type="radio"
|
||||
value={AUTOMATION_STATUS.EXISTING}
|
||||
bind:group={automationStatus}
|
||||
disabled={!hasAutomations} />
|
||||
<Label small grey={!hasAutomations}>Use an existing automation</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="radio-container" on:click={setExisting}>
|
||||
<input
|
||||
type="radio"
|
||||
value={AUTOMATION_STATUS.EXISTING}
|
||||
bind:group={automationStatus}
|
||||
disabled={!hasAutomations} />
|
||||
<div class="fields">
|
||||
<Label small>Automation</Label>
|
||||
|
||||
<Label disabled={!hasAutomations}>Use an existing automation</Label>
|
||||
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
||||
<Select
|
||||
thin
|
||||
secondary
|
||||
bind:value={parameters.automationId}
|
||||
placeholder="Choose automation">
|
||||
{#each automations as automation}
|
||||
<option value={automation._id}>{automation.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else}
|
||||
<Input
|
||||
thin
|
||||
bind:value={parameters.newAutomationName}
|
||||
placeholder="Enter automation name" />
|
||||
{/if}
|
||||
|
||||
{#key parameters.automationId}
|
||||
<SaveFields
|
||||
schemaFields={selectedSchema}
|
||||
parameterFields={parameters.fields}
|
||||
fieldLabel="Field"
|
||||
on:change={onFieldsChanged} />
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<Label size="m" color="dark">Automation</Label>
|
||||
|
||||
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
||||
<Select
|
||||
secondary
|
||||
bind:value={parameters.automationId}
|
||||
placeholder="Choose automation">
|
||||
<option value="" />
|
||||
{#each automations as automation}
|
||||
<option value={automation._id}>{automation.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else}
|
||||
<Input
|
||||
secondary
|
||||
bind:value={parameters.newAutomationName}
|
||||
placeholder="Enter automation name" />
|
||||
{/if}
|
||||
|
||||
<SaveFields
|
||||
schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema}
|
||||
fieldLabel="Field"
|
||||
on:fieldschanged={onFieldsChanged} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
.fields {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-s);
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: auto 1fr auto 1fr auto;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.root :global(> div:nth-child(4)) {
|
||||
.fields :global(> div:nth-child(2)) {
|
||||
grid-column: 2 / span 4;
|
||||
}
|
||||
|
||||
.radios,
|
||||
.radio-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
.radios {
|
||||
gap: var(--spacing-m);
|
||||
margin-bottom: var(--spacing-l);
|
||||
}
|
||||
.radio-container {
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.radio-container :global(label) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.radio-container:nth-child(1) {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
.radio-container:nth-child(2) {
|
||||
grid-column: 3 / span 3;
|
||||
}
|
||||
|
||||
.radio-container :global(> label) {
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
|
||||
.radio-container > input {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.radio-container > input:focus {
|
||||
outline: none;
|
||||
input[type="radio"]:checked {
|
||||
background: var(--blue);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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 ExecuteQuery from "./ExecuteQuery.svelte"
|
||||
import TriggerAutomation from "./TriggerAutomation.svelte"
|
||||
import ValidateForm from "./ValidateForm.svelte"
|
||||
|
||||
// defines what actions are available, when adding a new one
|
||||
// the component is the setup panel for the action
|
||||
|
@ -30,4 +31,8 @@ export default [
|
|||
name: "Trigger Automation",
|
||||
component: TriggerAutomation,
|
||||
},
|
||||
{
|
||||
name: "Validate Form",
|
||||
component: ValidateForm,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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 =
|
||||
selectedOption && selectedOption.label ? selectedOption.label : value || ""
|
||||
selectedOption && selectedOption.label
|
||||
? selectedOption.label
|
||||
: value || "Choose option"
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -129,11 +131,16 @@
|
|||
on:keydown={handleEscape}
|
||||
class="bb-select-menu">
|
||||
<ul>
|
||||
<li
|
||||
on:click|self={() => handleClick(null)}
|
||||
class:selected={value == null || value === ''}>
|
||||
Choose option
|
||||
</li>
|
||||
{#if isOptionsObject}
|
||||
{#each options as { value: v, label }}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
on:click|self={() => handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{label}
|
||||
</li>
|
||||
|
@ -142,7 +149,7 @@
|
|||
{#each options as v}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
on:click|self={() => handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{v}
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} type="options" />
|
|
@ -7,6 +7,7 @@
|
|||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
|
||||
import { capitalise } from "../../../../helpers"
|
||||
|
||||
export let label = ""
|
||||
export let bindable = true
|
||||
|
@ -24,7 +25,7 @@
|
|||
let valid
|
||||
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
|
||||
|
@ -46,6 +47,11 @@
|
|||
innerVal = value.target.value
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "number") {
|
||||
innerVal = parseInt(innerVal)
|
||||
}
|
||||
|
||||
if (typeof innerVal === "string") {
|
||||
onChange(replaceBindings(innerVal))
|
||||
} else {
|
||||
|
@ -72,6 +78,7 @@
|
|||
value={safeValue}
|
||||
on:change={handleChange}
|
||||
onChange={handleChange}
|
||||
{type}
|
||||
{...props}
|
||||
name={key} />
|
||||
</div>
|
||||
|
@ -82,26 +89,26 @@
|
|||
on:click={bindingDrawer.show}>
|
||||
<Icon name="lightning" />
|
||||
</div>
|
||||
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
|
||||
<div slot="description">
|
||||
<Body extraSmall grey>
|
||||
Add the objects on the left to enrich your text.
|
||||
</Body>
|
||||
</div>
|
||||
<heading slot="buttons">
|
||||
<Button thin blue disabled={!valid} on:click={handleClose}>Save</Button>
|
||||
</heading>
|
||||
<div slot="body">
|
||||
<BindingPanel
|
||||
bind:valid
|
||||
value={safeValue}
|
||||
close={handleClose}
|
||||
on:update={e => (temporaryBindableValue = e.detail)}
|
||||
{bindableProperties} />
|
||||
</div>
|
||||
</Drawer>
|
||||
{/if}
|
||||
</div>
|
||||
<Drawer bind:this={bindingDrawer} title="Bindings">
|
||||
<div slot="description">
|
||||
<Body extraSmall grey>
|
||||
Add the objects on the left to enrich your text.
|
||||
</Body>
|
||||
</div>
|
||||
<heading slot="buttons">
|
||||
<Button thin blue disabled={!valid} on:click={handleClose}>Save</Button>
|
||||
</heading>
|
||||
<div slot="body">
|
||||
<BindingPanel
|
||||
bind:valid
|
||||
value={safeValue}
|
||||
close={handleClose}
|
||||
on:update={e => (temporaryBindableValue = e.detail)}
|
||||
{bindableProperties} />
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.property-control {
|
||||
|
@ -138,7 +145,7 @@
|
|||
align-items: center;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding-left: var(--spacing-xs);
|
||||
padding-left: 7px;
|
||||
border-left: 1px solid var(--grey-4);
|
||||
background-color: var(--grey-2);
|
||||
border-top-right-radius: var(--border-radius-m);
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
|
||||
{#if open}
|
||||
<div>
|
||||
{#each properties as prop}
|
||||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}
|
||||
|
|
|
@ -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>
|
||||
import { get } from "lodash"
|
||||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
import { Button } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
|
||||
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
||||
import Input from "./PropertyControls/Input.svelte"
|
||||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
||||
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
||||
import OptionSelect from "./PropertyControls/OptionSelect.svelte"
|
||||
import MultiTableViewFieldSelect from "./PropertyControls/MultiTableViewFieldSelect.svelte"
|
||||
import Checkbox from "./PropertyControls/Checkbox.svelte"
|
||||
import TableSelect from "./PropertyControls/TableSelect.svelte"
|
||||
import TableViewSelect from "./PropertyControls/TableViewSelect.svelte"
|
||||
import TableViewFieldSelect from "./PropertyControls/TableViewFieldSelect.svelte"
|
||||
import DatasourceSelect from "./PropertyControls/DatasourceSelect.svelte"
|
||||
import FieldSelect from "./PropertyControls/FieldSelect.svelte"
|
||||
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
|
||||
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
|
||||
import EventsEditor from "./PropertyControls/EventsEditor"
|
||||
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
|
||||
import FilterEditor from "./PropertyControls/FilterEditor.svelte"
|
||||
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
|
||||
import { IconSelect } from "./PropertyControls/IconSelect"
|
||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
||||
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
|
||||
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
|
||||
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
|
||||
import BooleanFieldSelect from "./PropertyControls/BooleanFieldSelect.svelte"
|
||||
import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte"
|
||||
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
|
||||
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
|
||||
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
|
||||
|
||||
export let componentDefinition = {}
|
||||
export let componentInstance = {}
|
||||
|
@ -39,6 +52,7 @@
|
|||
"layoutId",
|
||||
"routing.roleId",
|
||||
]
|
||||
let confirmResetFieldsDialog
|
||||
|
||||
$: settings = componentDefinition?.settings ?? []
|
||||
$: isLayout = assetInstance && assetInstance.favicon
|
||||
|
@ -47,8 +61,7 @@
|
|||
const controlMap = {
|
||||
text: Input,
|
||||
select: OptionSelect,
|
||||
datasource: TableViewSelect,
|
||||
screen: ScreenSelect,
|
||||
datasource: DatasourceSelect,
|
||||
detailScreen: DetailScreenSelect,
|
||||
boolean: Checkbox,
|
||||
number: Input,
|
||||
|
@ -56,8 +69,18 @@
|
|||
table: TableSelect,
|
||||
color: ColorPicker,
|
||||
icon: IconSelect,
|
||||
field: TableViewFieldSelect,
|
||||
multifield: MultiTableViewFieldSelect,
|
||||
field: FieldSelect,
|
||||
multifield: MultiFieldSelect,
|
||||
schema: SchemaSelect,
|
||||
filter: FilterEditor,
|
||||
"field/string": StringFieldSelect,
|
||||
"field/number": NumberFieldSelect,
|
||||
"field/options": OptionsFieldSelect,
|
||||
"field/boolean": BooleanFieldSelect,
|
||||
"field/longform": LongFormFieldSelect,
|
||||
"field/datetime": DateTimeFieldSelect,
|
||||
"field/attachment": AttachmentFieldSelect,
|
||||
"field/link": RelationshipFieldSelect,
|
||||
}
|
||||
|
||||
const getControl = type => {
|
||||
|
@ -78,6 +101,20 @@
|
|||
const onInstanceNameChange = name => {
|
||||
onChange("_instanceName", name)
|
||||
}
|
||||
|
||||
const resetFormFields = () => {
|
||||
const form = findClosestMatchingComponent(
|
||||
$currentAsset.props,
|
||||
componentInstance._id,
|
||||
component => component._component.endsWith("/form")
|
||||
)
|
||||
const datasource = form?.datasource
|
||||
const fields = makeDatasourceFormComponents(datasource)
|
||||
onChange(
|
||||
"_children",
|
||||
fields.map(field => field.json())
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="settings-view-container">
|
||||
|
@ -114,7 +151,7 @@
|
|||
value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue}
|
||||
{componentInstance}
|
||||
onChange={val => onChange(setting.key, val)}
|
||||
props={{ options: setting.options }} />
|
||||
props={{ options: setting.options, placeholder: setting.placeholder }} />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
|
@ -122,7 +159,19 @@
|
|||
This component doesn't have any additional settings.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if componentDefinition?.component?.endsWith('/fieldgroup')}
|
||||
<Button secondary wide on:click={() => confirmResetFieldsDialog?.show()}>
|
||||
Reset Fields
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
bind:this={confirmResetFieldsDialog}
|
||||
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to reset this Field Group?`}
|
||||
okText="Reset"
|
||||
onOk={resetFormFields}
|
||||
title="Confirm Reset Fields" />
|
||||
|
||||
<style>
|
||||
.settings-view-container {
|
||||
|
|
|
@ -9,7 +9,6 @@ export const layout = [
|
|||
key: "display",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Block", value: "block" },
|
||||
{ label: "Inline Block", value: "inline-block" },
|
||||
{ label: "Flex", value: "flex" },
|
||||
|
@ -37,7 +36,6 @@ export const layout = [
|
|||
key: "justify-content",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Flex Start", value: "flex-start" },
|
||||
{ label: "Flex End", value: "flex-end" },
|
||||
{ label: "Center", value: "center" },
|
||||
|
@ -51,7 +49,6 @@ export const layout = [
|
|||
key: "align-items",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Flex Start", value: "flex-start" },
|
||||
{ label: "Flex End", value: "flex-end" },
|
||||
{ label: "Center", value: "center" },
|
||||
|
@ -64,7 +61,6 @@ export const layout = [
|
|||
key: "flex-wrap",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Wrap", value: "wrap" },
|
||||
{ label: "No wrap", value: "nowrap" },
|
||||
],
|
||||
|
@ -74,7 +70,6 @@ export const layout = [
|
|||
key: "gap",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -93,7 +88,6 @@ export const margin = [
|
|||
key: "margin",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -113,7 +107,6 @@ export const margin = [
|
|||
key: "margin-top",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -133,7 +126,6 @@ export const margin = [
|
|||
key: "margin-right",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -153,7 +145,6 @@ export const margin = [
|
|||
key: "margin-bottom",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -173,7 +164,6 @@ export const margin = [
|
|||
key: "margin-left",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -196,7 +186,6 @@ export const padding = [
|
|||
key: "padding",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -214,7 +203,6 @@ export const padding = [
|
|||
key: "padding-top",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -232,7 +220,6 @@ export const padding = [
|
|||
key: "padding-right",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -250,7 +237,6 @@ export const padding = [
|
|||
key: "padding-bottom",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -268,7 +254,6 @@ export const padding = [
|
|||
key: "padding-left",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -289,7 +274,6 @@ export const size = [
|
|||
key: "flex",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Shrink", value: "0 1 auto" },
|
||||
{ label: "Grow", value: "1 1 auto" },
|
||||
],
|
||||
|
@ -338,7 +322,6 @@ export const position = [
|
|||
key: "position",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Static", value: "static" },
|
||||
{ label: "Relative", value: "relative" },
|
||||
{ label: "Fixed", value: "fixed" },
|
||||
|
@ -375,7 +358,6 @@ export const position = [
|
|||
key: "z-index",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "-9999", value: "-9999" },
|
||||
{ label: "-3", value: "-3" },
|
||||
{ label: "-2", value: "-2" },
|
||||
|
@ -395,7 +377,6 @@ export const typography = [
|
|||
key: "font-family",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Arial", value: "Arial" },
|
||||
{ label: "Arial Black", value: "Arial Black" },
|
||||
{ label: "Cursive", value: "Cursive" },
|
||||
|
@ -418,7 +399,6 @@ export const typography = [
|
|||
key: "font-weight",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "200", value: "200" },
|
||||
{ label: "300", value: "300" },
|
||||
{ label: "400", value: "400" },
|
||||
|
@ -434,7 +414,6 @@ export const typography = [
|
|||
key: "font-size",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "10px", value: "10px" },
|
||||
{ label: "12px", value: "12px" },
|
||||
|
@ -454,7 +433,6 @@ export const typography = [
|
|||
key: "line-height",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "1", value: "1" },
|
||||
{ label: "1.25", value: "1.25" },
|
||||
{ label: "1.5", value: "1.5" },
|
||||
|
@ -496,7 +474,6 @@ export const typography = [
|
|||
key: "text-decoration-line",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Underline", value: "underline" },
|
||||
{ label: "Overline", value: "overline" },
|
||||
{ label: "Line-through", value: "line-through" },
|
||||
|
@ -516,7 +493,6 @@ export const background = [
|
|||
key: "background-image",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "none" },
|
||||
{
|
||||
label: "Warm Flame",
|
||||
|
@ -603,7 +579,6 @@ export const border = [
|
|||
key: "border-radius",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "X Small", value: "0.125rem" },
|
||||
{ label: "Small", value: "0.25rem" },
|
||||
|
@ -619,7 +594,6 @@ export const border = [
|
|||
key: "border-width",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "X Small", value: "0.5px" },
|
||||
{ label: "Small", value: "1px" },
|
||||
|
@ -638,7 +612,6 @@ export const border = [
|
|||
key: "border-style",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "Hidden", value: "hidden" },
|
||||
{ label: "Dotted", value: "dotted" },
|
||||
|
@ -659,7 +632,6 @@ export const effects = [
|
|||
key: "opacity",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "0", value: "0" },
|
||||
{ label: "0.2", value: "0.2" },
|
||||
{ label: "0.4", value: "0.4" },
|
||||
|
@ -673,7 +645,6 @@ export const effects = [
|
|||
key: "transform",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "45 deg", value: "rotate(45deg)" },
|
||||
{ label: "90 deg", value: "rotate(90deg)" },
|
||||
|
@ -690,7 +661,6 @@ export const effects = [
|
|||
key: "box-shadow",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
|
||||
{
|
||||
|
@ -723,7 +693,6 @@ export const transitions = [
|
|||
key: "transition-property",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Background Color", value: "background color" },
|
||||
|
@ -745,7 +714,6 @@ export const transitions = [
|
|||
control: OptionSelect,
|
||||
placeholder: "sec",
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "0.4s", value: "0.4s" },
|
||||
{ label: "0.6s", value: "0.6s" },
|
||||
{ label: "0.8s", value: "0.8s" },
|
||||
|
@ -759,7 +727,6 @@ export const transitions = [
|
|||
key: "transition-timing-function",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Linear", value: "linear" },
|
||||
{ label: "Ease", value: "ease" },
|
||||
{ label: "Ease in", value: "ease-in" },
|
||||
|
|
|
@ -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>
|
||||
import CodeMirror from "./codemirror"
|
||||
import { Label, Spacer } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { themeStore } from "builderStore"
|
||||
import { handlebarsCompletions } from "constants/completions"
|
||||
|
@ -11,11 +12,13 @@
|
|||
LIGHT: "default",
|
||||
}
|
||||
|
||||
export let label
|
||||
export let value = ""
|
||||
export let readOnly = false
|
||||
export let lineNumbers = true
|
||||
export let tab = true
|
||||
export let mode
|
||||
export let editorHeight = 500
|
||||
// export let parameters = []
|
||||
|
||||
let completions = handlebarsCompletions()
|
||||
|
@ -169,15 +172,21 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
||||
{#if label}
|
||||
<Label small>{label}</Label>
|
||||
<Spacer medium />
|
||||
{/if}
|
||||
<div style={`--code-mirror-height: ${editorHeight}px`}>
|
||||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
textarea {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
:global(.CodeMirror) {
|
||||
height: 500px !important;
|
||||
div :global(.CodeMirror) {
|
||||
height: var(--code-mirror-height) !important;
|
||||
border-radius: var(--border-radius-s);
|
||||
font-family: monospace !important;
|
||||
line-height: 1.3;
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
TextArea,
|
||||
Label,
|
||||
Input,
|
||||
Heading,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import { Label, Spacer, Input } from "@budibase/bbui"
|
||||
import Editor from "./QueryEditor.svelte"
|
||||
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||
|
||||
export let fields = {}
|
||||
export let schema
|
||||
|
@ -26,13 +20,33 @@
|
|||
<form on:submit|preventDefault>
|
||||
<div class="field">
|
||||
{#each schemaKeys as field}
|
||||
<Input
|
||||
placeholder="Enter {field} name"
|
||||
outline
|
||||
disabled={!editable}
|
||||
type={schema.fields[field]?.type}
|
||||
required={schema.fields[field]?.required}
|
||||
bind:value={fields[field]} />
|
||||
{#if schema.fields[field]?.type === 'object'}
|
||||
<div>
|
||||
<Label small>{field}</Label>
|
||||
<Spacer small />
|
||||
<KeyValueBuilder readOnly={!editable} bind:object={fields[field]} />
|
||||
</div>
|
||||
{:else if schema.fields[field]?.type === 'json'}
|
||||
<div>
|
||||
<Label extraSmall grey>{field}</Label>
|
||||
<Editor
|
||||
mode="json"
|
||||
on:change={({ detail }) => (fields[field] = detail.value)}
|
||||
readOnly={!editable}
|
||||
value={fields[field]} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="horizontal">
|
||||
<Label small>{field}</Label>
|
||||
<Input
|
||||
placeholder="Enter {field}"
|
||||
outline
|
||||
disabled={!editable}
|
||||
type={schema.fields[field]?.type}
|
||||
required={schema.fields[field]?.required}
|
||||
bind:value={fields[field]} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -49,8 +63,15 @@
|
|||
.field {
|
||||
margin-bottom: var(--spacing-m);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { Button, Input, Heading, Spacer } from "@budibase/bbui"
|
||||
import BindableInput from "components/common/BindableInput.svelte"
|
||||
import { Body, Button, Input, Heading, Spacer } from "@budibase/bbui"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
|
||||
|
||||
export let bindable = true
|
||||
export let parameters = []
|
||||
|
@ -30,7 +30,21 @@
|
|||
</script>
|
||||
|
||||
<section>
|
||||
<Heading extraSmall black>Parameters</Heading>
|
||||
<div class="controls">
|
||||
<Heading small lh>Parameters</Heading>
|
||||
{#if !bindable}
|
||||
<Button secondary on:click={newQueryParameter}>Add Param</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<Body small grey>
|
||||
{#if !bindable}
|
||||
Parameters come in two parts: the parameter name, and a default/fallback
|
||||
value.
|
||||
{:else}
|
||||
Enter a value for each parameter. The default values will be used for any
|
||||
values left blank.
|
||||
{/if}
|
||||
</Body>
|
||||
<Spacer large />
|
||||
<div class="parameters" class:bindable>
|
||||
{#each parameters as parameter, idx}
|
||||
|
@ -45,9 +59,9 @@
|
|||
disabled={bindable}
|
||||
bind:value={parameter.default} />
|
||||
{#if bindable}
|
||||
<BindableInput
|
||||
<DrawerBindableInput
|
||||
title={`Query parameter "${parameter.name}"`}
|
||||
placeholder="Value"
|
||||
type="string"
|
||||
thin
|
||||
on:change={evt => onBindingChange(parameter.name, evt.detail)}
|
||||
value={runtimeToReadableBinding(bindings, customParams?.[parameter.name])}
|
||||
|
@ -59,9 +73,6 @@
|
|||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if !bindable}
|
||||
<Button secondary on:click={newQueryParameter}>Add Parameter</Button>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
|
@ -69,6 +80,13 @@
|
|||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.parameters {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 5%;
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { goto } from "@sveltech/routify"
|
||||
import {
|
||||
Select,
|
||||
Button,
|
||||
Body,
|
||||
Label,
|
||||
Input,
|
||||
TextArea,
|
||||
Heading,
|
||||
Spacer,
|
||||
Switcher,
|
||||
} from "@budibase/bbui"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import api from "builderStore/api"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
||||
import EditQueryParamsPopover from "components/backend/DatasourceNavigator/popovers/EditQueryParamsPopover.svelte"
|
||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
const PREVIEW_HEADINGS = [
|
||||
|
@ -60,10 +58,10 @@
|
|||
|
||||
$: datasourceType = datasource?.source
|
||||
|
||||
$: config = $backendUiStore.integrations[datasourceType]?.query
|
||||
$: docsLink = $backendUiStore.integrations[datasourceType]?.docs
|
||||
$: integrationInfo = $backendUiStore.integrations[datasourceType]
|
||||
$: queryConfig = integrationInfo?.query
|
||||
|
||||
$: shouldShowQueryConfig = config && query.queryVerb
|
||||
$: shouldShowQueryConfig = queryConfig && query.queryVerb
|
||||
|
||||
function newField() {
|
||||
fields = [...fields, {}]
|
||||
|
@ -92,7 +90,7 @@
|
|||
|
||||
if (response.status !== 200) throw new Error(json.message)
|
||||
|
||||
data = json || []
|
||||
data = json.rows || []
|
||||
|
||||
if (data.length === 0) {
|
||||
notifier.info(
|
||||
|
@ -103,9 +101,9 @@
|
|||
|
||||
notifier.success("Query executed successfully.")
|
||||
|
||||
// Assume all the fields are strings and create a basic schema
|
||||
// from the first record returned by the query
|
||||
fields = Object.keys(json[0]).map(field => ({
|
||||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
fields = json.schemaFields.map(field => ({
|
||||
name: field,
|
||||
type: "STRING",
|
||||
}))
|
||||
|
@ -130,58 +128,93 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="input">
|
||||
<div class="label">Enter query name:</div>
|
||||
<Input outline border bind:value={query.name} />
|
||||
<section class="config">
|
||||
<Heading medium lh>Query {integrationInfo?.friendlyName}</Heading>
|
||||
<hr />
|
||||
<Spacer extraLarge />
|
||||
<Heading small lh>Config</Heading>
|
||||
<Body small grey>Provide a name for your query and select its function.</Body>
|
||||
<Spacer large />
|
||||
<div class="config-field">
|
||||
<Label small>Query Name</Label>
|
||||
<Input thin outline bind:value={query.name} />
|
||||
</div>
|
||||
{#if config}
|
||||
<div class="props">
|
||||
<div class="query-type">
|
||||
Query type:
|
||||
<span class="query-type-span">{config[query.queryVerb].type}</span>
|
||||
</div>
|
||||
<div class="select">
|
||||
<Select primary thin bind:value={query.queryVerb}>
|
||||
{#each Object.keys(config) as queryVerb}
|
||||
<option value={queryVerb}>{queryVerb}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
<Spacer extraLarge />
|
||||
{#if queryConfig}
|
||||
<div class="config-field">
|
||||
<Label small>Function</Label>
|
||||
<Select primary outline thin bind:value={query.queryVerb}>
|
||||
{#each Object.keys(queryConfig) as queryVerb}
|
||||
<option value={queryVerb}>
|
||||
{queryConfig[queryVerb]?.displayName || queryVerb}
|
||||
</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
<EditQueryParamsPopover
|
||||
bind:parameters={query.parameters}
|
||||
bindable={false} />
|
||||
<Spacer extraLarge />
|
||||
<hr />
|
||||
<Spacer extraLarge />
|
||||
<Spacer small />
|
||||
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
|
||||
<hr />
|
||||
{/if}
|
||||
</header>
|
||||
<Spacer extraLarge />
|
||||
</section>
|
||||
|
||||
{#if shouldShowQueryConfig}
|
||||
<section>
|
||||
<Spacer extraLarge />
|
||||
<Spacer small />
|
||||
<div class="config">
|
||||
<Heading small lh>Fields</Heading>
|
||||
<Body small grey>Fill in the fields specific to this query.</Body>
|
||||
<Spacer medium />
|
||||
<Spacer extraLarge />
|
||||
<IntegrationQueryEditor
|
||||
{datasource}
|
||||
{query}
|
||||
schema={config[query.queryVerb]}
|
||||
schema={queryConfig[query.queryVerb]}
|
||||
bind:parameters />
|
||||
<Spacer extraLarge />
|
||||
<hr />
|
||||
<Spacer extraLarge />
|
||||
<Spacer medium />
|
||||
<div class="viewer-controls">
|
||||
<Heading small lh>Results</Heading>
|
||||
<div class="button-container">
|
||||
<Button
|
||||
secondary
|
||||
thin
|
||||
disabled={data.length === 0 || !query.name}
|
||||
on:click={saveQuery}>
|
||||
Save Query
|
||||
</Button>
|
||||
<Spacer medium />
|
||||
<Button thin primary on:click={previewQuery}>Run Query</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Body small grey>
|
||||
Below, you can preview the results from your query and change the
|
||||
schema.
|
||||
</Body>
|
||||
|
||||
<Spacer extraLarge />
|
||||
<Spacer large />
|
||||
|
||||
<div class="viewer-controls">
|
||||
<Button
|
||||
blue
|
||||
disabled={data.length === 0 || !query.name}
|
||||
on:click={saveQuery}>
|
||||
Save Query
|
||||
</Button>
|
||||
<Button primary on:click={previewQuery}>Run Query</Button>
|
||||
</div>
|
||||
<Spacer medium />
|
||||
|
||||
<section class="viewer">
|
||||
{#if data}
|
||||
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
|
||||
{#if tab === 'JSON'}
|
||||
<pre class="preview">{JSON.stringify(data[0], undefined, 2)}</pre>
|
||||
<pre
|
||||
class="preview">
|
||||
<!-- prettier-ignore -->
|
||||
{#if !data[0]}
|
||||
|
||||
Please run your query to fetch some data.
|
||||
|
||||
{:else}
|
||||
{JSON.stringify(data[0], undefined, 2)}
|
||||
{/if}
|
||||
</pre>
|
||||
{:else if tab === 'PREVIEW'}
|
||||
<ExternalDataSourceTable {query} {data} />
|
||||
{:else if tab === 'SCHEMA'}
|
||||
|
@ -214,35 +247,30 @@
|
|||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
<Spacer extraLarge />
|
||||
<Spacer extraLarge />
|
||||
|
||||
<style>
|
||||
.input {
|
||||
width: 500px;
|
||||
display: flex;
|
||||
.config-field {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 200px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.props {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
gap: var(--layout-l);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 50px;
|
||||
grid-template-columns: 1fr 1fr 5%;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: var(--font-size-s);
|
||||
.button-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: var(--layout-m);
|
||||
border: 1px solid var(--grey-2);
|
||||
}
|
||||
|
||||
.config {
|
||||
|
@ -254,49 +282,28 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.query-type {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--grey-8);
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
|
||||
.query-type-span {
|
||||
text-transform: uppercase;
|
||||
.viewer {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 800px;
|
||||
height: 100%;
|
||||
min-height: 120px;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--grey-1);
|
||||
padding: var(--spacing-m);
|
||||
border-radius: 8px;
|
||||
color: var(--grey-6);
|
||||
}
|
||||
|
||||
.viewer-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
direction: rtl;
|
||||
z-index: 5;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-m);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.viewer {
|
||||
margin-top: -28px;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--grey-8);
|
||||
font-size: var(--font-size-s);
|
||||
margin-right: 8px;
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui"
|
||||
import Editor from "./QueryEditor.svelte"
|
||||
import ParameterBuilder from "./QueryParameterBuilder.svelte"
|
||||
import FieldsBuilder from "./QueryFieldsBuilder.svelte"
|
||||
import { Label, Input } from "@budibase/bbui"
|
||||
|
||||
const QueryTypes = {
|
||||
SQL: "sql",
|
||||
|
@ -12,8 +10,14 @@
|
|||
}
|
||||
|
||||
export let query
|
||||
export let datasource
|
||||
export let schema
|
||||
export let editable = true
|
||||
export let height = 500
|
||||
|
||||
$: urlDisplay =
|
||||
schema.urlDisplay &&
|
||||
`${datasource.config.url}${query.fields.path}${query.fields.queryString}`
|
||||
|
||||
function updateQuery({ detail }) {
|
||||
query.fields[schema.type] = detail.value
|
||||
|
@ -24,6 +28,7 @@
|
|||
{#key query._id}
|
||||
{#if schema.type === QueryTypes.SQL}
|
||||
<Editor
|
||||
editorHeight={height}
|
||||
label="Query"
|
||||
mode="sql"
|
||||
on:change={updateQuery}
|
||||
|
@ -32,6 +37,7 @@
|
|||
parameters={query.parameters} />
|
||||
{:else if schema.type === QueryTypes.JSON}
|
||||
<Editor
|
||||
editorHeight={height}
|
||||
label="Query"
|
||||
mode="json"
|
||||
on:change={updateQuery}
|
||||
|
@ -40,6 +46,21 @@
|
|||
parameters={query.parameters} />
|
||||
{:else if schema.type === QueryTypes.FIELDS}
|
||||
<FieldsBuilder bind:fields={query.fields} {schema} {editable} />
|
||||
{#if schema.urlDisplay}
|
||||
<div class="url-row">
|
||||
<Label small>URL</Label>
|
||||
<Input thin outline disabled value={urlDisplay} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.url-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -40,7 +40,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
BOOLEAN: {
|
||||
name: "True/False",
|
||||
name: "Boolean",
|
||||
icon: "ri-toggle-line",
|
||||
type: "boolean",
|
||||
constraints: {
|
||||
|
@ -82,6 +82,22 @@ export const FIELDS = {
|
|||
},
|
||||
}
|
||||
|
||||
export const AUTO_COLUMN_SUB_TYPES = {
|
||||
AUTO_ID: "autoID",
|
||||
CREATED_BY: "createdBy",
|
||||
CREATED_AT: "createdAt",
|
||||
UPDATED_BY: "updatedBy",
|
||||
UPDATED_AT: "updatedAt",
|
||||
}
|
||||
|
||||
export const AUTO_COLUMN_DISPLAY_NAMES = {
|
||||
AUTO_ID: "Auto ID",
|
||||
CREATED_BY: "Created By",
|
||||
CREATED_AT: "Created At",
|
||||
UPDATED_BY: "Updated By",
|
||||
UPDATED_AT: "Updated At",
|
||||
}
|
||||
|
||||
export const FILE_TYPES = {
|
||||
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
|
||||
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],
|
||||
|
@ -92,3 +108,18 @@ export const HostingTypes = {
|
|||
CLOUD: "cloud",
|
||||
SELF: "self",
|
||||
}
|
||||
|
||||
export const Roles = {
|
||||
ADMIN: "ADMIN",
|
||||
POWER: "POWER",
|
||||
BASIC: "BASIC",
|
||||
PUBLIC: "PUBLIC",
|
||||
BUILDER: "BUILDER",
|
||||
}
|
||||
|
||||
export function isAutoColumnUserRelationship(subtype) {
|
||||
return (
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY ||
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
||||
)
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-l);
|
||||
border-right: 1px solid var(--grey-2);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { params } from "@sveltech/routify"
|
||||
import { Switcher, Modal } from "@budibase/bbui"
|
||||
import { Button, Switcher, Modal } from "@budibase/bbui"
|
||||
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||
|
@ -8,11 +8,11 @@
|
|||
|
||||
const tabs = [
|
||||
{
|
||||
title: "Tables",
|
||||
title: "Internal",
|
||||
key: "table",
|
||||
},
|
||||
{
|
||||
title: "Data Sources",
|
||||
title: "External",
|
||||
key: "datasource",
|
||||
},
|
||||
]
|
||||
|
@ -67,6 +67,7 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-l);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
@ -79,6 +80,7 @@
|
|||
align-items: stretch;
|
||||
gap: var(--spacing-l);
|
||||
position: relative;
|
||||
border-right: 1px solid var(--grey-2);
|
||||
}
|
||||
|
||||
i {
|
||||
|
|
|
@ -28,9 +28,11 @@
|
|||
</script>
|
||||
|
||||
<section>
|
||||
{#if $backendUiStore.selectedDatabase._id && selectedQuery}
|
||||
<QueryInterface query={selectedQuery} />
|
||||
{/if}
|
||||
<div class="inner">
|
||||
{#if $backendUiStore.selectedDatabase._id && selectedQuery}
|
||||
<QueryInterface query={selectedQuery} />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
|
@ -41,4 +43,9 @@
|
|||
width: 0px;
|
||||
background: transparent; /* make scrollbar transparent */
|
||||
}
|
||||
|
||||
.inner {
|
||||
width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
<script>
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { Button, Spacer, Icon } from "@budibase/bbui"
|
||||
import { goto, beforeUrlChange } from "@sveltech/routify"
|
||||
import { Button, Heading, Body, Spacer, Icon } from "@budibase/bbui"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
|
||||
let unsaved = false
|
||||
|
||||
$: datasource = $backendUiStore.datasources.find(
|
||||
ds => ds._id === $backendUiStore.selectedDatasourceId
|
||||
)
|
||||
$: integration = datasource && $backendUiStore.integrations[datasource.source]
|
||||
|
||||
async function saveDatasource() {
|
||||
// Create datasource
|
||||
await backendUiStore.actions.datasources.save(datasource)
|
||||
notifier.success(`Datasource ${name} saved successfully.`)
|
||||
unsaved = false
|
||||
}
|
||||
|
||||
function onClickQuery(query) {
|
||||
|
@ -22,28 +27,63 @@
|
|||
backendUiStore.actions.queries.select(query)
|
||||
$goto(`../${query._id}`)
|
||||
}
|
||||
|
||||
function setUnsaved() {
|
||||
unsaved = true
|
||||
}
|
||||
|
||||
$beforeUrlChange((event, store) => {
|
||||
if (unsaved) {
|
||||
notifier.danger(
|
||||
"Unsaved changes. Please save your datasource configuration before leaving."
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if datasource}
|
||||
<section>
|
||||
<Spacer medium />
|
||||
<Spacer extraLarge />
|
||||
<header>
|
||||
<div class="datasource-icon">
|
||||
<svelte:component
|
||||
this={ICONS[datasource.source]}
|
||||
height="26"
|
||||
width="26" />
|
||||
</div>
|
||||
<h3 class="section-title">{datasource.name}</h3>
|
||||
</header>
|
||||
|
||||
<Body small grey lh>{integration.description}</Body>
|
||||
<Spacer extraLarge />
|
||||
<hr />
|
||||
<Spacer large />
|
||||
<Spacer extraLarge />
|
||||
|
||||
<div class="container">
|
||||
<div class="config-header">
|
||||
<h5>Configuration</h5>
|
||||
<Heading small>Configuration</Heading>
|
||||
<Button secondary on:click={saveDatasource}>Save</Button>
|
||||
</div>
|
||||
<Spacer medium />
|
||||
<IntegrationConfigForm integration={datasource.config} />
|
||||
</div>
|
||||
<Spacer extraLarge />
|
||||
<div class="container">
|
||||
|
||||
<Body small grey>
|
||||
Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
|
||||
<Spacer extraLarge />
|
||||
<IntegrationConfigForm
|
||||
schema={integration.datasource}
|
||||
integration={datasource.config}
|
||||
on:change={setUnsaved} />
|
||||
<Spacer extraLarge />
|
||||
<hr />
|
||||
<Spacer large />
|
||||
<Spacer extraLarge />
|
||||
<div class="query-header">
|
||||
<h5>Queries</h5>
|
||||
<Button blue on:click={() => $goto('../new')}>Create Query</Button>
|
||||
<Heading small>Queries</Heading>
|
||||
<Button secondary on:click={() => $goto('../new')}>Add Query</Button>
|
||||
</div>
|
||||
<Spacer extraLarge />
|
||||
<div class="query-list">
|
||||
|
@ -54,7 +94,6 @@
|
|||
<p>→</p>
|
||||
</div>
|
||||
{/each}
|
||||
<Spacer medium />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -63,14 +102,22 @@
|
|||
<style>
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 0 auto;
|
||||
width: 800px;
|
||||
width: 640px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid var(--grey-2);
|
||||
}
|
||||
|
||||
header {
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
@ -85,13 +132,12 @@
|
|||
|
||||
.container {
|
||||
border-radius: var(--border-radius-m);
|
||||
background: var(--background);
|
||||
padding: var(--layout-s);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0 !important;
|
||||
font-size: var(--font-size-l);
|
||||
}
|
||||
|
||||
.query-header {
|
||||
|
@ -115,7 +161,8 @@
|
|||
display: grid;
|
||||
grid-template-columns: 2fr 0.75fr 20px;
|
||||
align-items: center;
|
||||
padding: var(--spacing-m) var(--layout-xs);
|
||||
padding-left: var(--spacing-m);
|
||||
padding-right: var(--spacing-m);
|
||||
gap: var(--layout-xs);
|
||||
transition: 200ms background ease;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue