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
|
||||
|
|
|
@ -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)
|
||||
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 add _id and _rev fields for certain types
|
||||
let { schema, table } = getSchemaForDatasource(datasource)
|
||||
if (!schema || !table) {
|
||||
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
|
||||
}
|
||||
|
||||
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,6 +30,7 @@
|
|||
{#if schemaFields.length}
|
||||
<div class="schema-fields">
|
||||
{#each schemaFields as [field, schema]}
|
||||
{#if !schema.autocolumn}
|
||||
{#if schemaHasOptions(schema)}
|
||||
<Select label={field} extraThin secondary bind:value={value[field]}>
|
||||
<option value="">Choose an option</option>
|
||||
|
@ -45,6 +46,7 @@
|
|||
type="string"
|
||||
{bindings} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -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 />
|
||||
<ExportButton view={tableView} />
|
||||
{/if}
|
||||
<ManageAccessButton resourceId={$backendUiStore.selectedTable?._id} />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<!-- always have the export last -->
|
||||
<ExportButton view={tableView} />
|
||||
{/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'}
|
||||
<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,6 +160,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="grid-wrapper">
|
||||
{#key columnDefs.length}
|
||||
<AgGrid
|
||||
{theme}
|
||||
{options}
|
||||
|
@ -157,6 +168,7 @@
|
|||
{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]}
|
||||
{#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]}
|
||||
{#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}
|
||||
{#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
|
||||
type={integration[configKey].type}
|
||||
label={configKey}
|
||||
outline
|
||||
type={schema[configKey].type}
|
||||
on:change
|
||||
bind:value={integration[configKey]} />
|
||||
<Spacer large />
|
||||
</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}>
|
||||
|
|
|
@ -40,6 +40,21 @@
|
|||
<b>{linkedTable.name}</b>
|
||||
table.
|
||||
</Label>
|
||||
{:else}
|
||||
{#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
|
||||
|
@ -51,3 +66,4 @@
|
|||
{/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,16 +85,17 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div
|
||||
class="dropdownbutton"
|
||||
bind:this={anchorRight}
|
||||
on:click={dropdownRight.show}>
|
||||
<span>{value?.label ? value.label : 'Choose option'}</span>
|
||||
<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'} bind:this={drawer}>
|
||||
<Drawer title={'Query Parameters'} bind:this={drawer}>
|
||||
<div slot="buttons">
|
||||
<Button
|
||||
blue
|
||||
|
@ -107,20 +109,24 @@
|
|||
</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}
|
||||
<!-- <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>
|
||||
<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}>
|
||||
<div
|
||||
class="action-header"
|
||||
class:selected={action === selectedAction}
|
||||
on:click={selectAction(action)}>
|
||||
{index + 1}.
|
||||
{action[EVENT_TYPE_KEY]}
|
||||
</span>
|
||||
</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
|
||||
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,29 +3,28 @@
|
|||
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>
|
||||
<Label small>Datasource</Label>
|
||||
<Select thin secondary bind:value={parameters.datasourceId}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.datasources as datasource}
|
||||
|
@ -36,7 +35,7 @@
|
|||
<Spacer medium />
|
||||
|
||||
{#if parameters.datasourceId}
|
||||
<Label size="m" color="dark">Query</Label>
|
||||
<Label small>Query</Label>
|
||||
<Select thin secondary bind:value={parameters.queryId}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
|
||||
|
@ -52,14 +51,9 @@
|
|||
bind:customParams={parameters.queryParams}
|
||||
parameters={query.parameters}
|
||||
bindings={bindableProperties} />
|
||||
{#if query.fields.sql}
|
||||
<pre>{query.fields.queryString}</pre>
|
||||
<IntegrationQueryEditor
|
||||
height={200}
|
||||
{query}
|
||||
schema={fetchQueryDefinition(query)}
|
||||
editable={false} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
padding: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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="radios">
|
||||
<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>
|
||||
bind:group={automationStatus} />
|
||||
<Label small>Create a new automation</Label>
|
||||
</div>
|
||||
|
||||
<div class="radio-container" on:click={setExisting}>
|
||||
<div class="radio-container" on:click={hasAutomations ? setExisting : null}>
|
||||
<input
|
||||
type="radio"
|
||||
value={AUTOMATION_STATUS.EXISTING}
|
||||
bind:group={automationStatus}
|
||||
disabled={!hasAutomations} />
|
||||
|
||||
<Label disabled={!hasAutomations}>Use an existing automation</Label>
|
||||
<Label small grey={!hasAutomations}>Use an existing automation</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Label size="m" color="dark">Automation</Label>
|
||||
<div class="fields">
|
||||
<Label small>Automation</Label>
|
||||
|
||||
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
||||
<Select
|
||||
thin
|
||||
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
|
||||
thin
|
||||
bind:value={parameters.newAutomationName}
|
||||
placeholder="Enter automation name" />
|
||||
{/if}
|
||||
|
||||
{#key parameters.automationId}
|
||||
<SaveFields
|
||||
schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema}
|
||||
schemaFields={selectedSchema}
|
||||
parameterFields={parameters.fields}
|
||||
fieldLabel="Field"
|
||||
on:fieldschanged={onFieldsChanged} />
|
||||
on:change={onFieldsChanged} />
|
||||
{/key}
|
||||
</div>
|
||||
</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,9 +89,7 @@
|
|||
on:click={bindingDrawer.show}>
|
||||
<Icon name="lightning" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Drawer bind:this={bindingDrawer} title="Bindings">
|
||||
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
|
||||
<div slot="description">
|
||||
<Body extraSmall grey>
|
||||
Add the objects on the left to enrich your text.
|
||||
|
@ -102,6 +107,8 @@
|
|||
{bindableProperties} />
|
||||
</div>
|
||||
</Drawer>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
{#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}
|
||||
{#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} name"
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
<EditQueryParamsPopover
|
||||
bind:parameters={query.parameters}
|
||||
bindable={false} />
|
||||
{/if}
|
||||
</header>
|
||||
<Spacer extraLarge />
|
||||
<hr />
|
||||
<Spacer extraLarge />
|
||||
<Spacer small />
|
||||
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
|
||||
<hr />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if shouldShowQueryConfig}
|
||||
<section>
|
||||
<div class="config">
|
||||
<IntegrationQueryEditor
|
||||
{query}
|
||||
schema={config[query.queryVerb]}
|
||||
bind:parameters />
|
||||
|
||||
<Spacer extraLarge />
|
||||
<Spacer large />
|
||||
|
||||
<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={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
|
||||
blue
|
||||
secondary
|
||||
thin
|
||||
disabled={data.length === 0 || !query.name}
|
||||
on:click={saveQuery}>
|
||||
Save Query
|
||||
</Button>
|
||||
<Button primary on:click={previewQuery}>Run 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 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>
|
||||
<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>
|
||||
|
||||
<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="container">
|
||||
<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;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue