commit
9dc94fbed6
|
@ -63,7 +63,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.58.3",
|
"@budibase/bbui": "^1.58.5",
|
||||||
"@budibase/client": "^0.7.6",
|
"@budibase/client": "^0.7.6",
|
||||||
"@budibase/colorpicker": "1.0.1",
|
"@budibase/colorpicker": "1.0.1",
|
||||||
"@budibase/string-templates": "^0.7.6",
|
"@budibase/string-templates": "^0.7.6",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { backendUiStore, store } from "builderStore"
|
import { backendUiStore, store } from "builderStore"
|
||||||
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
|
import { findComponentPath } from "./storeUtils"
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
|
|
||||||
|
@ -12,9 +12,7 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||||
* Gets all bindable data context fields and instance fields.
|
* Gets all bindable data context fields and instance fields.
|
||||||
*/
|
*/
|
||||||
export const getBindableProperties = (rootComponent, componentId) => {
|
export const getBindableProperties = (rootComponent, componentId) => {
|
||||||
const contextBindings = getContextBindings(rootComponent, componentId)
|
return getContextBindings(rootComponent, componentId)
|
||||||
const componentBindings = getComponentBindings(rootComponent)
|
|
||||||
return [...contextBindings, ...componentBindings]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,6 +35,30 @@ export const getDataProviderComponents = (rootComponent, componentId) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all data provider components above a component.
|
||||||
|
*/
|
||||||
|
export const getActionProviderComponents = (
|
||||||
|
rootComponent,
|
||||||
|
componentId,
|
||||||
|
actionType
|
||||||
|
) => {
|
||||||
|
if (!rootComponent || !componentId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the component tree leading up to this component, ignoring the component
|
||||||
|
// itself
|
||||||
|
const path = findComponentPath(rootComponent, componentId)
|
||||||
|
path.pop()
|
||||||
|
|
||||||
|
// Filter by only data provider components
|
||||||
|
return path.filter(component => {
|
||||||
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
|
return def?.actions?.includes(actionType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a datasource object for a certain data provider component
|
* Gets a datasource object for a certain data provider component
|
||||||
*/
|
*/
|
||||||
|
@ -47,8 +69,9 @@ export const getDatasourceForProvider = component => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract datasource from component instance
|
// Extract datasource from component instance
|
||||||
|
const validSettingTypes = ["datasource", "table", "schema"]
|
||||||
const datasourceSetting = def.settings.find(setting => {
|
const datasourceSetting = def.settings.find(setting => {
|
||||||
return setting.type === "datasource" || setting.type === "table"
|
return validSettingTypes.includes(setting.type)
|
||||||
})
|
})
|
||||||
if (!datasourceSetting) {
|
if (!datasourceSetting) {
|
||||||
return null
|
return null
|
||||||
|
@ -58,15 +81,14 @@ export const getDatasourceForProvider = component => {
|
||||||
// example an actual datasource object, or a table ID string.
|
// example an actual datasource object, or a table ID string.
|
||||||
// Convert the datasource setting into a proper datasource object so that
|
// Convert the datasource setting into a proper datasource object so that
|
||||||
// we can use it properly
|
// we can use it properly
|
||||||
if (datasourceSetting.type === "datasource") {
|
if (datasourceSetting.type === "table") {
|
||||||
return component[datasourceSetting?.key]
|
|
||||||
} else if (datasourceSetting.type === "table") {
|
|
||||||
return {
|
return {
|
||||||
tableId: component[datasourceSetting?.key],
|
tableId: component[datasourceSetting?.key],
|
||||||
type: "table",
|
type: "table",
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return component[datasourceSetting?.key]
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,21 +99,37 @@ export const getContextBindings = (rootComponent, componentId) => {
|
||||||
// Extract any components which provide data contexts
|
// Extract any components which provide data contexts
|
||||||
const dataProviders = getDataProviderComponents(rootComponent, componentId)
|
const dataProviders = getDataProviderComponents(rootComponent, componentId)
|
||||||
let contextBindings = []
|
let contextBindings = []
|
||||||
|
|
||||||
|
// Create bindings for each data provider
|
||||||
dataProviders.forEach(component => {
|
dataProviders.forEach(component => {
|
||||||
|
const isForm = component._component.endsWith("/form")
|
||||||
const datasource = getDatasourceForProvider(component)
|
const datasource = getDatasourceForProvider(component)
|
||||||
if (!datasource) {
|
let tableName, schema
|
||||||
|
|
||||||
|
// Forms are an edge case which do not need table schemas
|
||||||
|
if (isForm) {
|
||||||
|
schema = buildFormSchema(component)
|
||||||
|
tableName = "Schema"
|
||||||
|
} else {
|
||||||
|
if (!datasource) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get schema and table for the datasource
|
||||||
|
const info = getSchemaForDatasource(datasource, isForm)
|
||||||
|
schema = info.schema
|
||||||
|
tableName = info.table?.name
|
||||||
|
|
||||||
|
// Add _id and _rev fields for certain types
|
||||||
|
if (datasource.type === "table" || datasource.type === "link") {
|
||||||
|
schema["_id"] = { type: "string" }
|
||||||
|
schema["_rev"] = { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!schema || !tableName) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get schema and add _id and _rev fields for certain types
|
|
||||||
let { schema, table } = getSchemaForDatasource(datasource)
|
|
||||||
if (!schema || !table) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (datasource.type === "table" || datasource.type === "link") {
|
|
||||||
schema["_id"] = { type: "string" }
|
|
||||||
schema["_rev"] = { type: "string " }
|
|
||||||
}
|
|
||||||
const keys = Object.keys(schema).sort()
|
const keys = Object.keys(schema).sort()
|
||||||
|
|
||||||
// Create bindable properties for each schema field
|
// Create bindable properties for each schema field
|
||||||
|
@ -110,11 +148,11 @@ export const getContextBindings = (rootComponent, componentId) => {
|
||||||
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
|
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
|
||||||
runtimeBoundKey
|
runtimeBoundKey
|
||||||
)}`,
|
)}`,
|
||||||
readableBinding: `${component._instanceName}.${table.name}.${key}`,
|
readableBinding: `${component._instanceName}.${tableName}.${key}`,
|
||||||
|
// Field schema and provider are required to construct relationship
|
||||||
|
// datasource options, based on bindable properties
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
providerId: component._id,
|
providerId: component._id,
|
||||||
tableId: datasource.tableId,
|
|
||||||
field: key,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -142,44 +180,20 @@ export const getContextBindings = (rootComponent, componentId) => {
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding: `user.${runtimeBoundKey}`,
|
runtimeBinding: `user.${runtimeBoundKey}`,
|
||||||
readableBinding: `Current User.${key}`,
|
readableBinding: `Current User.${key}`,
|
||||||
|
// Field schema and provider are required to construct relationship
|
||||||
|
// datasource options, based on bindable properties
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
providerId: "user",
|
providerId: "user",
|
||||||
tableId: TableNames.USERS,
|
|
||||||
field: key,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return contextBindings
|
return contextBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all bindable components. These are form components which allow their
|
|
||||||
* values to be bound to.
|
|
||||||
*/
|
|
||||||
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}`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a schema for a datasource object.
|
* Gets a schema for a datasource object.
|
||||||
*/
|
*/
|
||||||
export const getSchemaForDatasource = datasource => {
|
export const getSchemaForDatasource = (datasource, isForm = false) => {
|
||||||
let schema, table
|
let schema, table
|
||||||
if (datasource) {
|
if (datasource) {
|
||||||
const { type } = datasource
|
const { type } = datasource
|
||||||
|
@ -193,6 +207,14 @@ export const getSchemaForDatasource = datasource => {
|
||||||
if (table) {
|
if (table) {
|
||||||
if (type === "view") {
|
if (type === "view") {
|
||||||
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
||||||
|
} else if (type === "query" && isForm) {
|
||||||
|
schema = {}
|
||||||
|
const params = table.parameters || []
|
||||||
|
params.forEach(param => {
|
||||||
|
if (param?.name) {
|
||||||
|
schema[param.name] = { ...param, type: "string" }
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
schema = cloneDeep(table.schema)
|
schema = cloneDeep(table.schema)
|
||||||
}
|
}
|
||||||
|
@ -201,6 +223,32 @@ export const getSchemaForDatasource = datasource => {
|
||||||
return { schema, table }
|
return { schema, table }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a form schema given a form component.
|
||||||
|
* A form schema is a schema of all the fields nested anywhere within a form.
|
||||||
|
*/
|
||||||
|
const buildFormSchema = component => {
|
||||||
|
let schema = {}
|
||||||
|
if (!component) {
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
|
const fieldSetting = def?.settings?.find(
|
||||||
|
setting => setting.key === "field" && setting.type.startsWith("field/")
|
||||||
|
)
|
||||||
|
if (fieldSetting && component.field) {
|
||||||
|
const type = fieldSetting.type.split("field/")[1]
|
||||||
|
if (type) {
|
||||||
|
schema[component.field] = { name: component.field, type }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
component._children?.forEach(child => {
|
||||||
|
const childSchema = buildFormSchema(child)
|
||||||
|
schema = { ...schema, ...childSchema }
|
||||||
|
})
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -416,7 +416,14 @@ export const getFrontendStore = () => {
|
||||||
if (cut) {
|
if (cut) {
|
||||||
state.componentToPaste = null
|
state.componentToPaste = null
|
||||||
} else {
|
} else {
|
||||||
componentToPaste._id = uuid()
|
const randomizeIds = component => {
|
||||||
|
if (!component) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component._id = uuid()
|
||||||
|
component._children?.forEach(randomizeIds)
|
||||||
|
}
|
||||||
|
randomizeIds(componentToPaste)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "inside") {
|
if (mode === "inside") {
|
||||||
|
|
|
@ -9,5 +9,6 @@ const createScreen = () => {
|
||||||
return new Screen()
|
return new Screen()
|
||||||
.mainType("div")
|
.mainType("div")
|
||||||
.component("@budibase/standard-components/container")
|
.component("@budibase/standard-components/container")
|
||||||
|
.instanceName("New Screen")
|
||||||
.json()
|
.json()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { Screen } from "./utils/Screen"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: `New Row (Empty)`,
|
|
||||||
create: () => createScreen(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const createScreen = () => {
|
|
||||||
return new Screen()
|
|
||||||
.component("@budibase/standard-components/newrow")
|
|
||||||
.table("")
|
|
||||||
.json()
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { Screen } from "./utils/Screen"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: `Row Detail (Empty)`,
|
|
||||||
create: () => createScreen(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const createScreen = () => {
|
|
||||||
return new Screen()
|
|
||||||
.component("@budibase/standard-components/rowdetail")
|
|
||||||
.table("")
|
|
||||||
.json()
|
|
||||||
}
|
|
|
@ -1,17 +1,12 @@
|
||||||
import newRowScreen from "./newRowScreen"
|
import newRowScreen from "./newRowScreen"
|
||||||
import rowDetailScreen from "./rowDetailScreen"
|
import rowDetailScreen from "./rowDetailScreen"
|
||||||
import rowListScreen from "./rowListScreen"
|
import rowListScreen from "./rowListScreen"
|
||||||
import emptyNewRowScreen from "./emptyNewRowScreen"
|
|
||||||
import createFromScratchScreen from "./createFromScratchScreen"
|
import createFromScratchScreen from "./createFromScratchScreen"
|
||||||
import emptyRowDetailScreen from "./emptyRowDetailScreen"
|
|
||||||
|
|
||||||
const allTemplates = tables => [
|
const allTemplates = tables => [
|
||||||
createFromScratchScreen,
|
|
||||||
...newRowScreen(tables),
|
...newRowScreen(tables),
|
||||||
...rowDetailScreen(tables),
|
...rowDetailScreen(tables),
|
||||||
...rowListScreen(tables),
|
...rowListScreen(tables),
|
||||||
emptyNewRowScreen,
|
|
||||||
emptyRowDetailScreen,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// Allows us to apply common behaviour to all create() functions
|
// Allows us to apply common behaviour to all create() functions
|
||||||
|
@ -22,8 +17,18 @@ const createTemplateOverride = (frontendState, create) => () => {
|
||||||
return screen
|
return screen
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (frontendState, tables) =>
|
export default (frontendState, tables) => {
|
||||||
allTemplates(tables).map(template => ({
|
const enrichTemplate = template => ({
|
||||||
...template,
|
...template,
|
||||||
create: createTemplateOverride(frontendState, template.create),
|
create: createTemplateOverride(frontendState, template.create),
|
||||||
}))
|
})
|
||||||
|
|
||||||
|
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||||
|
const tableTemplates = allTemplates(tables).map(enrichTemplate)
|
||||||
|
return [
|
||||||
|
fromScratch,
|
||||||
|
...tableTemplates.sort((templateA, templateB) => {
|
||||||
|
return templateA.name > templateB.name ? 1 : -1
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import sanitizeUrl from "./utils/sanitizeUrl"
|
import sanitizeUrl from "./utils/sanitizeUrl"
|
||||||
import { Component } from "./utils/Component"
|
|
||||||
import { Screen } from "./utils/Screen"
|
import { Screen } from "./utils/Screen"
|
||||||
|
import { Component } from "./utils/Component"
|
||||||
import {
|
import {
|
||||||
makeBreadcrumbContainer,
|
makeBreadcrumbContainer,
|
||||||
makeMainContainer,
|
makeMainForm,
|
||||||
makeTitleContainer,
|
makeTitleContainer,
|
||||||
makeSaveButton,
|
makeSaveButton,
|
||||||
|
makeDatasourceFormComponents,
|
||||||
} from "./utils/commonComponents"
|
} from "./utils/commonComponents"
|
||||||
|
|
||||||
export default function(tables) {
|
export default function(tables) {
|
||||||
|
@ -21,29 +22,46 @@ export default function(tables) {
|
||||||
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
|
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
|
||||||
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
|
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
|
||||||
|
|
||||||
function generateTitleContainer(table, providerId) {
|
function generateTitleContainer(table, formId) {
|
||||||
return makeTitleContainer("New Row").addChild(
|
return makeTitleContainer("New Row").addChild(makeSaveButton(table, formId))
|
||||||
makeSaveButton(table, providerId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScreen = table => {
|
const createScreen = table => {
|
||||||
const screen = new Screen()
|
const screen = new Screen()
|
||||||
.component("@budibase/standard-components/newrow")
|
.component("@budibase/standard-components/container")
|
||||||
.table(table._id)
|
|
||||||
.route(newRowUrl(table))
|
|
||||||
.instanceName(`${table.name} - New`)
|
.instanceName(`${table.name} - New`)
|
||||||
.name("")
|
.route(newRowUrl(table))
|
||||||
|
|
||||||
const dataform = new Component(
|
const form = makeMainForm()
|
||||||
"@budibase/standard-components/dataformwide"
|
.instanceName("Form")
|
||||||
).instanceName("Form")
|
.customProps({
|
||||||
|
theme: "spectrum--lightest",
|
||||||
|
size: "spectrum--medium",
|
||||||
|
datasource: {
|
||||||
|
label: table.name,
|
||||||
|
tableId: table._id,
|
||||||
|
type: "table",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const providerId = screen._json.props._id
|
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
|
||||||
const container = makeMainContainer()
|
.instanceName("Field Group")
|
||||||
|
.customProps({
|
||||||
|
labelPosition: "left",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add all form fields from this schema to the field group
|
||||||
|
const datasource = { type: "table", tableId: table._id }
|
||||||
|
makeDatasourceFormComponents(datasource).forEach(component => {
|
||||||
|
fieldGroup.addChild(component)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add all children to the form
|
||||||
|
const formId = form._json._id
|
||||||
|
form
|
||||||
.addChild(makeBreadcrumbContainer(table.name, "New"))
|
.addChild(makeBreadcrumbContainer(table.name, "New"))
|
||||||
.addChild(generateTitleContainer(table, providerId))
|
.addChild(generateTitleContainer(table, formId))
|
||||||
.addChild(dataform)
|
.addChild(fieldGroup)
|
||||||
|
|
||||||
return screen.addChild(container).json()
|
return screen.addChild(form).json()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,20 +4,19 @@ import { Screen } from "./utils/Screen"
|
||||||
import { Component } from "./utils/Component"
|
import { Component } from "./utils/Component"
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import {
|
import {
|
||||||
makeMainContainer,
|
|
||||||
makeBreadcrumbContainer,
|
makeBreadcrumbContainer,
|
||||||
makeTitleContainer,
|
makeTitleContainer,
|
||||||
makeSaveButton,
|
makeSaveButton,
|
||||||
|
makeMainForm,
|
||||||
|
spectrumColor,
|
||||||
|
makeDatasourceFormComponents,
|
||||||
} from "./utils/commonComponents"
|
} from "./utils/commonComponents"
|
||||||
|
|
||||||
export default function(tables) {
|
export default function(tables) {
|
||||||
return tables.map(table => {
|
return tables.map(table => {
|
||||||
const heading = table.primaryDisplay
|
|
||||||
? `{{ data.${makePropSafe(table.primaryDisplay)} }}`
|
|
||||||
: null
|
|
||||||
return {
|
return {
|
||||||
name: `${table.name} - Detail`,
|
name: `${table.name} - Detail`,
|
||||||
create: () => createScreen(table, heading),
|
create: () => createScreen(table),
|
||||||
id: ROW_DETAIL_TEMPLATE,
|
id: ROW_DETAIL_TEMPLATE,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -26,9 +25,9 @@ export default function(tables) {
|
||||||
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
|
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
|
||||||
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
|
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
|
||||||
|
|
||||||
function generateTitleContainer(table, title, providerId) {
|
function generateTitleContainer(table, title, formId) {
|
||||||
// have to override style for this, its missing margin
|
// have to override style for this, its missing margin
|
||||||
const saveButton = makeSaveButton(table, providerId).normalStyle({
|
const saveButton = makeSaveButton(table, formId).normalStyle({
|
||||||
background: "#000000",
|
background: "#000000",
|
||||||
"border-width": "0",
|
"border-width": "0",
|
||||||
"border-style": "None",
|
"border-style": "None",
|
||||||
|
@ -54,6 +53,7 @@ function generateTitleContainer(table, title, providerId) {
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
color: "#4285f4",
|
color: "#4285f4",
|
||||||
})
|
})
|
||||||
|
.customStyle(spectrumColor(700))
|
||||||
.text("Delete")
|
.text("Delete")
|
||||||
.customProps({
|
.customProps({
|
||||||
className: "",
|
className: "",
|
||||||
|
@ -61,8 +61,9 @@ function generateTitleContainer(table, title, providerId) {
|
||||||
onClick: [
|
onClick: [
|
||||||
{
|
{
|
||||||
parameters: {
|
parameters: {
|
||||||
rowId: `{{ ${makePropSafe(providerId)}._id }}`,
|
providerId: formId,
|
||||||
revId: `{{ ${makePropSafe(providerId)}._rev }}`,
|
rowId: `{{ ${makePropSafe(formId)}._id }}`,
|
||||||
|
revId: `{{ ${makePropSafe(formId)}._rev }}`,
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
},
|
},
|
||||||
"##eventHandlerType": "Delete Row",
|
"##eventHandlerType": "Delete Row",
|
||||||
|
@ -82,23 +83,47 @@ function generateTitleContainer(table, title, providerId) {
|
||||||
.addChild(saveButton)
|
.addChild(saveButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScreen = (table, heading) => {
|
const createScreen = table => {
|
||||||
const screen = new Screen()
|
const screen = new Screen()
|
||||||
.component("@budibase/standard-components/rowdetail")
|
.component("@budibase/standard-components/rowdetail")
|
||||||
.table(table._id)
|
.table(table._id)
|
||||||
.instanceName(`${table.name} - Detail`)
|
.instanceName(`${table.name} - Detail`)
|
||||||
.route(rowDetailUrl(table))
|
.route(rowDetailUrl(table))
|
||||||
.name("")
|
|
||||||
|
|
||||||
const dataform = new Component(
|
const form = makeMainForm()
|
||||||
"@budibase/standard-components/dataformwide"
|
.instanceName("Form")
|
||||||
).instanceName("Form")
|
.customProps({
|
||||||
|
theme: "spectrum--lightest",
|
||||||
|
size: "spectrum--medium",
|
||||||
|
datasource: {
|
||||||
|
label: table.name,
|
||||||
|
tableId: table._id,
|
||||||
|
type: "table",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const providerId = screen._json.props._id
|
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
|
||||||
const container = makeMainContainer()
|
.instanceName("Field Group")
|
||||||
|
.customProps({
|
||||||
|
labelPosition: "left",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add all form fields from this schema to the field group
|
||||||
|
const datasource = { type: "table", tableId: table._id }
|
||||||
|
makeDatasourceFormComponents(datasource).forEach(component => {
|
||||||
|
fieldGroup.addChild(component)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add all children to the form
|
||||||
|
const formId = form._json._id
|
||||||
|
const rowDetailId = screen._json.props._id
|
||||||
|
const heading = table.primaryDisplay
|
||||||
|
? `{{ ${makePropSafe(rowDetailId)}.${makePropSafe(table.primaryDisplay)} }}`
|
||||||
|
: null
|
||||||
|
form
|
||||||
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
|
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
|
||||||
.addChild(generateTitleContainer(table, heading || "Edit Row", providerId))
|
.addChild(generateTitleContainer(table, heading || "Edit Row", formId))
|
||||||
.addChild(dataform)
|
.addChild(fieldGroup)
|
||||||
|
|
||||||
return screen.addChild(container).json()
|
return screen.addChild(form).json()
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,17 +14,11 @@ export class Component extends BaseStructure {
|
||||||
active: {},
|
active: {},
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
type: "",
|
|
||||||
_instanceName: "",
|
_instanceName: "",
|
||||||
_children: [],
|
_children: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type(type) {
|
|
||||||
this._json.type = type
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
normalStyle(styling) {
|
normalStyle(styling) {
|
||||||
this._json._styles.normal = styling
|
this._json._styles.normal = styling
|
||||||
return this
|
return this
|
||||||
|
@ -35,14 +29,25 @@ export class Component extends BaseStructure {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
text(text) {
|
customStyle(styling) {
|
||||||
this._json.text = text
|
this._json._styles.custom = styling
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: do we need this
|
|
||||||
instanceName(name) {
|
instanceName(name) {
|
||||||
this._json._instanceName = name
|
this._json._instanceName = name
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shorthand for custom props "type"
|
||||||
|
type(type) {
|
||||||
|
this._json.type = type
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shorthand for custom props "text"
|
||||||
|
text(text) {
|
||||||
|
this._json.text = text
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
import { Component } from "./Component"
|
import { Component } from "./Component"
|
||||||
import { rowListUrl } from "../rowListScreen"
|
import { rowListUrl } from "../rowListScreen"
|
||||||
|
import { getSchemaForDatasource } from "../../../dataBinding"
|
||||||
|
|
||||||
|
export function spectrumColor(number) {
|
||||||
|
// Acorn throws a parsing error in this file if the word g-l-o-b-a-l is found
|
||||||
|
// (without dashes - I can't even type it in a comment).
|
||||||
|
// God knows why. It seems to think optional chaining further down the
|
||||||
|
// file is invalid if the word g-l-o-b-a-l is found - hence the reason this
|
||||||
|
// statement is split into parts.
|
||||||
|
return "color: var(--spectrum-glo" + `bal-color-gray-${number});`
|
||||||
|
}
|
||||||
|
|
||||||
export function makeLinkComponent(tableName) {
|
export function makeLinkComponent(tableName) {
|
||||||
return new Component("@budibase/standard-components/link")
|
return new Component("@budibase/standard-components/link")
|
||||||
|
@ -10,6 +20,7 @@ export function makeLinkComponent(tableName) {
|
||||||
.hoverStyle({
|
.hoverStyle({
|
||||||
color: "#4285f4",
|
color: "#4285f4",
|
||||||
})
|
})
|
||||||
|
.customStyle(spectrumColor(700))
|
||||||
.text(tableName)
|
.text(tableName)
|
||||||
.customProps({
|
.customProps({
|
||||||
url: `/${tableName.toLowerCase()}`,
|
url: `/${tableName.toLowerCase()}`,
|
||||||
|
@ -22,13 +33,12 @@ export function makeLinkComponent(tableName) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeMainContainer() {
|
export function makeMainForm() {
|
||||||
return new Component("@budibase/standard-components/container")
|
return new Component("@budibase/standard-components/form")
|
||||||
.type("div")
|
.type("div")
|
||||||
.normalStyle({
|
.normalStyle({
|
||||||
width: "700px",
|
width: "700px",
|
||||||
padding: "0px",
|
padding: "0px",
|
||||||
background: "white",
|
|
||||||
"border-radius": "0.5rem",
|
"border-radius": "0.5rem",
|
||||||
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
||||||
margin: "auto",
|
margin: "auto",
|
||||||
|
@ -39,7 +49,7 @@ export function makeMainContainer() {
|
||||||
"padding-left": "48px",
|
"padding-left": "48px",
|
||||||
"margin-bottom": "20px",
|
"margin-bottom": "20px",
|
||||||
})
|
})
|
||||||
.instanceName("Container")
|
.instanceName("Form")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
||||||
|
@ -51,6 +61,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
||||||
"margin-right": "4px",
|
"margin-right": "4px",
|
||||||
"margin-left": "4px",
|
"margin-left": "4px",
|
||||||
})
|
})
|
||||||
|
.customStyle(spectrumColor(700))
|
||||||
.text(">")
|
.text(">")
|
||||||
.instanceName("Arrow")
|
.instanceName("Arrow")
|
||||||
|
|
||||||
|
@ -63,6 +74,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
||||||
const identifierText = new Component("@budibase/standard-components/text")
|
const identifierText = new Component("@budibase/standard-components/text")
|
||||||
.type("none")
|
.type("none")
|
||||||
.normalStyle(textStyling)
|
.normalStyle(textStyling)
|
||||||
|
.customStyle(spectrumColor(700))
|
||||||
.text(text)
|
.text(text)
|
||||||
.instanceName("Identifier")
|
.instanceName("Identifier")
|
||||||
|
|
||||||
|
@ -78,7 +90,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
||||||
.addChild(identifierText)
|
.addChild(identifierText)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeSaveButton(table, providerId) {
|
export function makeSaveButton(table, formId) {
|
||||||
return new Component("@budibase/standard-components/button")
|
return new Component("@budibase/standard-components/button")
|
||||||
.normalStyle({
|
.normalStyle({
|
||||||
background: "#000000",
|
background: "#000000",
|
||||||
|
@ -99,8 +111,14 @@ export function makeSaveButton(table, providerId) {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
onClick: [
|
onClick: [
|
||||||
{
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
parameters: {
|
parameters: {
|
||||||
providerId,
|
componentId: formId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
providerId: formId,
|
||||||
},
|
},
|
||||||
"##eventHandlerType": "Save Row",
|
"##eventHandlerType": "Save Row",
|
||||||
},
|
},
|
||||||
|
@ -125,6 +143,7 @@ export function makeTitleContainer(title) {
|
||||||
"margin-left": "0px",
|
"margin-left": "0px",
|
||||||
flex: "1 1 auto",
|
flex: "1 1 auto",
|
||||||
})
|
})
|
||||||
|
.customStyle(spectrumColor(900))
|
||||||
.type("h3")
|
.type("h3")
|
||||||
.instanceName("Title")
|
.instanceName("Title")
|
||||||
.text(title)
|
.text(title)
|
||||||
|
@ -142,3 +161,44 @@ export function makeTitleContainer(title) {
|
||||||
.instanceName("Title Container")
|
.instanceName("Title Container")
|
||||||
.addChild(heading)
|
.addChild(heading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fieldTypeToComponentMap = {
|
||||||
|
string: "stringfield",
|
||||||
|
number: "numberfield",
|
||||||
|
options: "optionsfield",
|
||||||
|
boolean: "booleanfield",
|
||||||
|
longform: "longformfield",
|
||||||
|
datetime: "datetimefield",
|
||||||
|
attachment: "attachmentfield",
|
||||||
|
link: "relationshipfield",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeDatasourceFormComponents(datasource) {
|
||||||
|
const { schema } = getSchemaForDatasource(datasource, true)
|
||||||
|
let components = []
|
||||||
|
let fields = Object.keys(schema || {})
|
||||||
|
fields.forEach(field => {
|
||||||
|
const fieldSchema = schema[field]
|
||||||
|
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 === "boolean") {
|
||||||
|
component.customProps({ text: field, label: "" })
|
||||||
|
}
|
||||||
|
components.push(component)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return components
|
||||||
|
}
|
||||||
|
|
|
@ -59,8 +59,8 @@ export const findComponentPath = (rootComponent, id, path = []) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recurses through the component tree and finds all components of a certain
|
* Recurses through the component tree and finds all components which match
|
||||||
* type.
|
* a certain selector
|
||||||
*/
|
*/
|
||||||
export const findAllMatchingComponents = (rootComponent, selector) => {
|
export const findAllMatchingComponents = (rootComponent, selector) => {
|
||||||
if (!rootComponent || !selector) {
|
if (!rootComponent || !selector) {
|
||||||
|
@ -81,6 +81,26 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
|
||||||
return components.reverse()
|
return components.reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the closes parent component which matches certain criteria
|
||||||
|
*/
|
||||||
|
export const findClosestMatchingComponent = (
|
||||||
|
rootComponent,
|
||||||
|
componentId,
|
||||||
|
selector
|
||||||
|
) => {
|
||||||
|
if (!selector) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const componentPath = findComponentPath(rootComponent, componentId).reverse()
|
||||||
|
for (let component of componentPath) {
|
||||||
|
if (selector(component)) {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recurses through a component tree evaluating a matching function against
|
* Recurses through a component tree evaluating a matching function against
|
||||||
* components until a match is found
|
* components until a match is found
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import groupBy from "lodash/fp/groupBy"
|
import groupBy from "lodash/fp/groupBy"
|
||||||
import {
|
import {
|
||||||
TextArea,
|
TextArea,
|
||||||
Label,
|
|
||||||
Input,
|
Input,
|
||||||
Heading,
|
Heading,
|
||||||
Body,
|
Body,
|
||||||
|
|
|
@ -36,7 +36,9 @@
|
||||||
{:else if type === 'boolean'}
|
{:else if type === 'boolean'}
|
||||||
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
||||||
{:else if type === 'link'}
|
{:else if type === 'link'}
|
||||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
<div>
|
||||||
|
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||||
|
</div>
|
||||||
{:else if type === 'longform'}
|
{:else if type === 'longform'}
|
||||||
<div>
|
<div>
|
||||||
<Label extraSmall grey>{label}</Label>
|
<Label extraSmall grey>{label}</Label>
|
||||||
|
|
|
@ -8,11 +8,16 @@
|
||||||
"name": "Form",
|
"name": "Form",
|
||||||
"icon": "ri-file-edit-line",
|
"icon": "ri-file-edit-line",
|
||||||
"children": [
|
"children": [
|
||||||
"dataform",
|
"form",
|
||||||
"dataformwide",
|
"fieldgroup",
|
||||||
"input",
|
"stringfield",
|
||||||
"richtext",
|
"numberfield",
|
||||||
"datepicker"
|
"optionsfield",
|
||||||
|
"booleanfield",
|
||||||
|
"longformfield",
|
||||||
|
"datetimefield",
|
||||||
|
"attachmentfield",
|
||||||
|
"relationshipfield"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -56,8 +61,8 @@
|
||||||
"screenslot",
|
"screenslot",
|
||||||
"navigation",
|
"navigation",
|
||||||
"login",
|
"login",
|
||||||
"rowdetail",
|
"rowdetail"
|
||||||
"newrow"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,6 @@
|
||||||
*, *:before, *:after {
|
*, *:before, *:after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
* {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<script src='/assets/budibase-client.js'></script>
|
<script src='/assets/budibase-client.js'></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -13,9 +13,8 @@
|
||||||
let dropdown
|
let dropdown
|
||||||
let anchor
|
let anchor
|
||||||
|
|
||||||
$: noChildrenAllowed =
|
$: definition = store.actions.components.getDefinition(component?._component)
|
||||||
!component ||
|
$: noChildrenAllowed = !component || !definition?.hasChildren
|
||||||
!store.actions.components.getDefinition(component._component)?.hasChildren
|
|
||||||
$: noPaste = !$store.componentToPaste
|
$: noPaste = !$store.componentToPaste
|
||||||
|
|
||||||
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
||||||
|
@ -130,7 +129,7 @@
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`}
|
body={`Are you sure you wish to delete this '${definition?.name}' component?`}
|
||||||
okText="Delete Component"
|
okText="Delete Component"
|
||||||
onOk={deleteComponent} />
|
onOk={deleteComponent} />
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="attachment" />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="boolean" />
|
|
@ -19,6 +19,7 @@
|
||||||
let drawer
|
let drawer
|
||||||
|
|
||||||
export let value = {}
|
export let value = {}
|
||||||
|
export let otherSources
|
||||||
|
|
||||||
$: tables = $backendUiStore.tables.map(m => ({
|
$: tables = $backendUiStore.tables.map(m => ({
|
||||||
label: m.name,
|
label: m.name,
|
||||||
|
@ -88,7 +89,7 @@
|
||||||
class="dropdownbutton"
|
class="dropdownbutton"
|
||||||
bind:this={anchorRight}
|
bind:this={anchorRight}
|
||||||
on:click={dropdownRight.show}>
|
on:click={dropdownRight.show}>
|
||||||
<span>{value?.label ? value.label : 'Choose option'}</span>
|
<span>{value?.label ?? 'Choose option'}</span>
|
||||||
<Icon name="arrowdown" />
|
<Icon name="arrowdown" />
|
||||||
</div>
|
</div>
|
||||||
{#if value?.type === 'query'}
|
{#if value?.type === 'query'}
|
||||||
|
@ -175,6 +176,22 @@
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{#if otherSources?.length}
|
||||||
|
<hr />
|
||||||
|
<div class="title">
|
||||||
|
<Heading extraSmall>Other</Heading>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{#each otherSources as source}
|
||||||
|
<li
|
||||||
|
class:selected={value === source}
|
||||||
|
on:click={() => handleSelected(source)}>
|
||||||
|
{source.label}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="datetime" />
|
|
@ -1,15 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Button, DropdownMenu, Spacer } from "@budibase/bbui"
|
||||||
Button,
|
|
||||||
Body,
|
|
||||||
DropdownMenu,
|
|
||||||
ModalContent,
|
|
||||||
Spacer,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
|
||||||
import actionTypes from "./actions"
|
import actionTypes from "./actions"
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import { automationStore } from "builderStore"
|
|
||||||
|
|
||||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,9 @@
|
||||||
)
|
)
|
||||||
$: {
|
$: {
|
||||||
// Automatically set rev and table ID based on row ID
|
// Automatically set rev and table ID based on row ID
|
||||||
if (parameters.rowId) {
|
if (parameters.providerId) {
|
||||||
parameters.revId = parameters.rowId.replace("_id", "_rev")
|
parameters.rowId = `{{ ${parameters.providerId}._id }}`
|
||||||
|
parameters.revId = `{{ ${parameters.providerId}._rev }}`
|
||||||
const providerComponent = dataProviderComponents.find(
|
const providerComponent = dataProviderComponents.find(
|
||||||
provider => provider._id === parameters.providerId
|
provider => provider._id === parameters.providerId
|
||||||
)
|
)
|
||||||
|
@ -37,12 +38,10 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Label size="m" color="dark">Datasource</Label>
|
<Label size="m" color="dark">Datasource</Label>
|
||||||
<Select secondary bind:value={parameters.rowId}>
|
<Select secondary bind:value={parameters.providerId}>
|
||||||
<option value="" />
|
<option value="" />
|
||||||
{#each dataProviderComponents as provider}
|
{#each dataProviderComponents as provider}
|
||||||
<option value={`{{ ${provider._id}._id }}`}>
|
<option value={provider._id}>{provider._instanceName}</option>
|
||||||
{provider._instanceName}
|
|
||||||
</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Label } from "@budibase/bbui"
|
||||||
|
import { currentAsset, store } from "builderStore"
|
||||||
|
import { getDataProviderComponents } from "builderStore/dataBinding"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
|
||||||
|
$: dataProviders = getDataProviderComponents(
|
||||||
|
$currentAsset.props,
|
||||||
|
$store.selectedComponentId
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label size="m" color="dark">Form</Label>
|
||||||
|
<Select secondary bind:value={parameters.componentId}>
|
||||||
|
<option value="" />
|
||||||
|
{#if dataProviders}
|
||||||
|
{#each dataProviders 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>
|
|
@ -32,7 +32,7 @@
|
||||||
// this statement initialises fields from parameters.fields
|
// this statement initialises fields from parameters.fields
|
||||||
$: fields =
|
$: fields =
|
||||||
fields ||
|
fields ||
|
||||||
Object.keys(parameterFields || { "": "" }).map(name => ({
|
Object.keys(parameterFields || {}).map(name => ({
|
||||||
name,
|
name,
|
||||||
value:
|
value:
|
||||||
(parameterFields &&
|
(parameterFields &&
|
||||||
|
|
|
@ -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.props,
|
||||||
|
$store.selectedComponentId,
|
||||||
|
"ValidateForm"
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label size="m" color="dark">Form</Label>
|
||||||
|
<Select 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,8 @@ import SaveRow from "./SaveRow.svelte"
|
||||||
import DeleteRow from "./DeleteRow.svelte"
|
import DeleteRow from "./DeleteRow.svelte"
|
||||||
import ExecuteQuery from "./ExecuteQuery.svelte"
|
import ExecuteQuery from "./ExecuteQuery.svelte"
|
||||||
import TriggerAutomation from "./TriggerAutomation.svelte"
|
import TriggerAutomation from "./TriggerAutomation.svelte"
|
||||||
|
import ValidateForm from "./ValidateForm.svelte"
|
||||||
|
import RefreshDatasource from "./RefreshDatasource.svelte"
|
||||||
|
|
||||||
// defines what actions are available, when adding a new one
|
// defines what actions are available, when adding a new one
|
||||||
// the component is the setup panel for the action
|
// the component is the setup panel for the action
|
||||||
|
@ -30,4 +32,12 @@ export default [
|
||||||
name: "Trigger Automation",
|
name: "Trigger Automation",
|
||||||
component: TriggerAutomation,
|
component: TriggerAutomation,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Validate Form",
|
||||||
|
component: ValidateForm,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Refresh Datasource",
|
||||||
|
component: RefreshDatasource,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script>
|
||||||
|
import { DataList } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
getDatasourceForProvider,
|
||||||
|
getSchemaForDatasource,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { currentAsset } from "builderStore"
|
||||||
|
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||||
|
|
||||||
|
export let componentInstance
|
||||||
|
export let value
|
||||||
|
export let onChange
|
||||||
|
export let type
|
||||||
|
|
||||||
|
$: form = findClosestMatchingComponent(
|
||||||
|
$currentAsset.props,
|
||||||
|
componentInstance._id,
|
||||||
|
component => component._component === "@budibase/standard-components/form"
|
||||||
|
)
|
||||||
|
$: datasource = getDatasourceForProvider(form)
|
||||||
|
$: schema = getSchemaForDatasource(datasource, true).schema
|
||||||
|
$: options = getOptions(schema, type)
|
||||||
|
|
||||||
|
const getOptions = (schema, fieldType) => {
|
||||||
|
let entries = Object.entries(schema ?? {})
|
||||||
|
if (fieldType) {
|
||||||
|
entries = entries.filter(entry => entry[1].type === fieldType)
|
||||||
|
}
|
||||||
|
return entries.map(entry => entry[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = () => onChange(value)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<DataList
|
||||||
|
editable
|
||||||
|
secondary
|
||||||
|
extraThin
|
||||||
|
on:blur={handleBlur}
|
||||||
|
on:change
|
||||||
|
bind:value>
|
||||||
|
<option value="" />
|
||||||
|
{#each options as option}
|
||||||
|
<option value={option}>{option}</option>
|
||||||
|
{/each}
|
||||||
|
</DataList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
div :global(> div) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="longform" />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FieldSelect from "./FieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FieldSelect {...$$props} multiselect />
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import TableViewFieldSelect from "./TableViewFieldSelect.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TableViewFieldSelect {...$$props} multiselect />
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="number" />
|
|
@ -106,7 +106,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: displayLabel =
|
$: displayLabel =
|
||||||
selectedOption && selectedOption.label ? selectedOption.label : value || ""
|
selectedOption && selectedOption.label
|
||||||
|
? selectedOption.label
|
||||||
|
: value || "Choose option"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -129,11 +131,16 @@
|
||||||
on:keydown={handleEscape}
|
on:keydown={handleEscape}
|
||||||
class="bb-select-menu">
|
class="bb-select-menu">
|
||||||
<ul>
|
<ul>
|
||||||
|
<li
|
||||||
|
on:click|self={() => handleClick(null)}
|
||||||
|
class:selected={value == null || value === ''}>
|
||||||
|
Choose option
|
||||||
|
</li>
|
||||||
{#if isOptionsObject}
|
{#if isOptionsObject}
|
||||||
{#each options as { value: v, label }}
|
{#each options as { value: v, label }}
|
||||||
<li
|
<li
|
||||||
{...handleStyleBind(v)}
|
{...handleStyleBind(v)}
|
||||||
on:click|self={handleClick(v)}
|
on:click|self={() => handleClick(v)}
|
||||||
class:selected={value === v}>
|
class:selected={value === v}>
|
||||||
{label}
|
{label}
|
||||||
</li>
|
</li>
|
||||||
|
@ -142,7 +149,7 @@
|
||||||
{#each options as v}
|
{#each options as v}
|
||||||
<li
|
<li
|
||||||
{...handleStyleBind(v)}
|
{...handleStyleBind(v)}
|
||||||
on:click|self={handleClick(v)}
|
on:click|self={() => handleClick(v)}
|
||||||
class:selected={value === v}>
|
class:selected={value === v}>
|
||||||
{v}
|
{v}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="options" />
|
|
@ -144,7 +144,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-left: var(--spacing-xs);
|
padding-left: 7px;
|
||||||
border-left: 1px solid var(--grey-4);
|
border-left: 1px solid var(--grey-4);
|
||||||
background-color: var(--grey-2);
|
background-color: var(--grey-2);
|
||||||
border-top-right-radius: var(--border-radius-m);
|
border-top-right-radius: var(--border-radius-m);
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
|
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
|
||||||
{#if open}
|
{#if open}
|
||||||
<div>
|
<div>
|
||||||
{#each properties as prop}
|
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
bindable={false}
|
bindable={false}
|
||||||
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}
|
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="link" />
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import DatasourceSelect from "./DatasourceSelect.svelte"
|
||||||
|
|
||||||
|
const otherSources = [{ name: "Custom", label: "Custom" }]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DatasourceSelect on:change {...$$props} {otherSources} />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormFieldSelect {...$$props} type="string" />
|
|
@ -1,22 +1,35 @@
|
||||||
<script>
|
<script>
|
||||||
import { get } from "lodash"
|
import { get } from "lodash"
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
import { Button } from "@budibase/bbui"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { currentAsset } from "builderStore"
|
||||||
|
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||||
|
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
|
||||||
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
||||||
import Input from "./PropertyControls/Input.svelte"
|
import Input from "./PropertyControls/Input.svelte"
|
||||||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
||||||
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
||||||
import OptionSelect from "./PropertyControls/OptionSelect.svelte"
|
import OptionSelect from "./PropertyControls/OptionSelect.svelte"
|
||||||
import MultiTableViewFieldSelect from "./PropertyControls/MultiTableViewFieldSelect.svelte"
|
|
||||||
import Checkbox from "./PropertyControls/Checkbox.svelte"
|
import Checkbox from "./PropertyControls/Checkbox.svelte"
|
||||||
import TableSelect from "./PropertyControls/TableSelect.svelte"
|
import TableSelect from "./PropertyControls/TableSelect.svelte"
|
||||||
import TableViewSelect from "./PropertyControls/TableViewSelect.svelte"
|
import DatasourceSelect from "./PropertyControls/DatasourceSelect.svelte"
|
||||||
import TableViewFieldSelect from "./PropertyControls/TableViewFieldSelect.svelte"
|
import FieldSelect from "./PropertyControls/FieldSelect.svelte"
|
||||||
|
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
|
||||||
|
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
|
||||||
import EventsEditor from "./PropertyControls/EventsEditor"
|
import EventsEditor from "./PropertyControls/EventsEditor"
|
||||||
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
|
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
|
||||||
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
|
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
|
||||||
import { IconSelect } from "./PropertyControls/IconSelect"
|
import { IconSelect } from "./PropertyControls/IconSelect"
|
||||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
||||||
|
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
|
||||||
|
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
|
||||||
|
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
|
||||||
|
import BooleanFieldSelect from "./PropertyControls/BooleanFieldSelect.svelte"
|
||||||
|
import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte"
|
||||||
|
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
|
||||||
|
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
|
||||||
|
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
|
||||||
|
|
||||||
export let componentDefinition = {}
|
export let componentDefinition = {}
|
||||||
export let componentInstance = {}
|
export let componentInstance = {}
|
||||||
|
@ -39,6 +52,7 @@
|
||||||
"layoutId",
|
"layoutId",
|
||||||
"routing.roleId",
|
"routing.roleId",
|
||||||
]
|
]
|
||||||
|
let confirmResetFieldsDialog
|
||||||
|
|
||||||
$: settings = componentDefinition?.settings ?? []
|
$: settings = componentDefinition?.settings ?? []
|
||||||
$: isLayout = assetInstance && assetInstance.favicon
|
$: isLayout = assetInstance && assetInstance.favicon
|
||||||
|
@ -47,7 +61,7 @@
|
||||||
const controlMap = {
|
const controlMap = {
|
||||||
text: Input,
|
text: Input,
|
||||||
select: OptionSelect,
|
select: OptionSelect,
|
||||||
datasource: TableViewSelect,
|
datasource: DatasourceSelect,
|
||||||
screen: ScreenSelect,
|
screen: ScreenSelect,
|
||||||
detailScreen: DetailScreenSelect,
|
detailScreen: DetailScreenSelect,
|
||||||
boolean: Checkbox,
|
boolean: Checkbox,
|
||||||
|
@ -56,8 +70,17 @@
|
||||||
table: TableSelect,
|
table: TableSelect,
|
||||||
color: ColorPicker,
|
color: ColorPicker,
|
||||||
icon: IconSelect,
|
icon: IconSelect,
|
||||||
field: TableViewFieldSelect,
|
field: FieldSelect,
|
||||||
multifield: MultiTableViewFieldSelect,
|
multifield: MultiFieldSelect,
|
||||||
|
schema: SchemaSelect,
|
||||||
|
"field/string": StringFieldSelect,
|
||||||
|
"field/number": NumberFieldSelect,
|
||||||
|
"field/options": OptionsFieldSelect,
|
||||||
|
"field/boolean": BooleanFieldSelect,
|
||||||
|
"field/longform": LongFormFieldSelect,
|
||||||
|
"field/datetime": DateTimeFieldSelect,
|
||||||
|
"field/attachment": AttachmentFieldSelect,
|
||||||
|
"field/link": RelationshipFieldSelect,
|
||||||
}
|
}
|
||||||
|
|
||||||
const getControl = type => {
|
const getControl = type => {
|
||||||
|
@ -78,6 +101,20 @@
|
||||||
const onInstanceNameChange = name => {
|
const onInstanceNameChange = name => {
|
||||||
onChange("_instanceName", name)
|
onChange("_instanceName", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetFormFields = () => {
|
||||||
|
const form = findClosestMatchingComponent(
|
||||||
|
$currentAsset.props,
|
||||||
|
componentInstance._id,
|
||||||
|
component => component._component.endsWith("/form")
|
||||||
|
)
|
||||||
|
const datasource = form?.datasource
|
||||||
|
const fields = makeDatasourceFormComponents(datasource)
|
||||||
|
onChange(
|
||||||
|
"_children",
|
||||||
|
fields.map(field => field.json())
|
||||||
|
)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="settings-view-container">
|
<div class="settings-view-container">
|
||||||
|
@ -114,7 +151,7 @@
|
||||||
value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue}
|
value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue}
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
onChange={val => onChange(setting.key, val)}
|
onChange={val => onChange(setting.key, val)}
|
||||||
props={{ options: setting.options }} />
|
props={{ options: setting.options, placeholder: setting.placeholder }} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -122,7 +159,19 @@
|
||||||
This component doesn't have any additional settings.
|
This component doesn't have any additional settings.
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if componentDefinition?.component?.endsWith('/fieldgroup')}
|
||||||
|
<Button secondary wide on:click={() => confirmResetFieldsDialog?.show()}>
|
||||||
|
Reset Fields
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmResetFieldsDialog}
|
||||||
|
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to reset this Field Group?`}
|
||||||
|
okText="Reset"
|
||||||
|
onOk={resetFormFields}
|
||||||
|
title="Confirm Reset Fields" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.settings-view-container {
|
.settings-view-container {
|
||||||
|
|
|
@ -9,7 +9,6 @@ export const layout = [
|
||||||
key: "display",
|
key: "display",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "Block", value: "block" },
|
{ label: "Block", value: "block" },
|
||||||
{ label: "Inline Block", value: "inline-block" },
|
{ label: "Inline Block", value: "inline-block" },
|
||||||
{ label: "Flex", value: "flex" },
|
{ label: "Flex", value: "flex" },
|
||||||
|
@ -37,7 +36,6 @@ export const layout = [
|
||||||
key: "justify-content",
|
key: "justify-content",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "Flex Start", value: "flex-start" },
|
{ label: "Flex Start", value: "flex-start" },
|
||||||
{ label: "Flex End", value: "flex-end" },
|
{ label: "Flex End", value: "flex-end" },
|
||||||
{ label: "Center", value: "center" },
|
{ label: "Center", value: "center" },
|
||||||
|
@ -51,7 +49,6 @@ export const layout = [
|
||||||
key: "align-items",
|
key: "align-items",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "Flex Start", value: "flex-start" },
|
{ label: "Flex Start", value: "flex-start" },
|
||||||
{ label: "Flex End", value: "flex-end" },
|
{ label: "Flex End", value: "flex-end" },
|
||||||
{ label: "Center", value: "center" },
|
{ label: "Center", value: "center" },
|
||||||
|
@ -64,7 +61,6 @@ export const layout = [
|
||||||
key: "flex-wrap",
|
key: "flex-wrap",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "Wrap", value: "wrap" },
|
{ label: "Wrap", value: "wrap" },
|
||||||
{ label: "No wrap", value: "nowrap" },
|
{ label: "No wrap", value: "nowrap" },
|
||||||
],
|
],
|
||||||
|
@ -74,7 +70,6 @@ export const layout = [
|
||||||
key: "gap",
|
key: "gap",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -93,7 +88,6 @@ export const margin = [
|
||||||
key: "margin",
|
key: "margin",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -113,7 +107,6 @@ export const margin = [
|
||||||
key: "margin-top",
|
key: "margin-top",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -133,7 +126,6 @@ export const margin = [
|
||||||
key: "margin-right",
|
key: "margin-right",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -153,7 +145,6 @@ export const margin = [
|
||||||
key: "margin-bottom",
|
key: "margin-bottom",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -173,7 +164,6 @@ export const margin = [
|
||||||
key: "margin-left",
|
key: "margin-left",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -196,7 +186,6 @@ export const padding = [
|
||||||
key: "padding",
|
key: "padding",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -214,7 +203,6 @@ export const padding = [
|
||||||
key: "padding-top",
|
key: "padding-top",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -232,7 +220,6 @@ export const padding = [
|
||||||
key: "padding-right",
|
key: "padding-right",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -250,7 +237,6 @@ export const padding = [
|
||||||
key: "padding-bottom",
|
key: "padding-bottom",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -268,7 +254,6 @@ export const padding = [
|
||||||
key: "padding-left",
|
key: "padding-left",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0px" },
|
{ label: "None", value: "0px" },
|
||||||
{ label: "4px", value: "4px" },
|
{ label: "4px", value: "4px" },
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
|
@ -289,7 +274,6 @@ export const size = [
|
||||||
key: "flex",
|
key: "flex",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "Shrink", value: "0 1 auto" },
|
{ label: "Shrink", value: "0 1 auto" },
|
||||||
{ label: "Grow", value: "1 1 auto" },
|
{ label: "Grow", value: "1 1 auto" },
|
||||||
],
|
],
|
||||||
|
@ -338,7 +322,6 @@ export const position = [
|
||||||
key: "position",
|
key: "position",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "Static", value: "static" },
|
{ label: "Static", value: "static" },
|
||||||
{ label: "Relative", value: "relative" },
|
{ label: "Relative", value: "relative" },
|
||||||
{ label: "Fixed", value: "fixed" },
|
{ label: "Fixed", value: "fixed" },
|
||||||
|
@ -375,7 +358,6 @@ export const position = [
|
||||||
key: "z-index",
|
key: "z-index",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "-9999", value: "-9999" },
|
{ label: "-9999", value: "-9999" },
|
||||||
{ label: "-3", value: "-3" },
|
{ label: "-3", value: "-3" },
|
||||||
{ label: "-2", value: "-2" },
|
{ label: "-2", value: "-2" },
|
||||||
|
@ -395,7 +377,6 @@ export const typography = [
|
||||||
key: "font-family",
|
key: "font-family",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "Arial", value: "Arial" },
|
{ label: "Arial", value: "Arial" },
|
||||||
{ label: "Arial Black", value: "Arial Black" },
|
{ label: "Arial Black", value: "Arial Black" },
|
||||||
{ label: "Cursive", value: "Cursive" },
|
{ label: "Cursive", value: "Cursive" },
|
||||||
|
@ -418,7 +399,6 @@ export const typography = [
|
||||||
key: "font-weight",
|
key: "font-weight",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "200", value: "200" },
|
{ label: "200", value: "200" },
|
||||||
{ label: "300", value: "300" },
|
{ label: "300", value: "300" },
|
||||||
{ label: "400", value: "400" },
|
{ label: "400", value: "400" },
|
||||||
|
@ -434,7 +414,6 @@ export const typography = [
|
||||||
key: "font-size",
|
key: "font-size",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "8px", value: "8px" },
|
{ label: "8px", value: "8px" },
|
||||||
{ label: "10px", value: "10px" },
|
{ label: "10px", value: "10px" },
|
||||||
{ label: "12px", value: "12px" },
|
{ label: "12px", value: "12px" },
|
||||||
|
@ -454,7 +433,6 @@ export const typography = [
|
||||||
key: "line-height",
|
key: "line-height",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "1", value: "1" },
|
{ label: "1", value: "1" },
|
||||||
{ label: "1.25", value: "1.25" },
|
{ label: "1.25", value: "1.25" },
|
||||||
{ label: "1.5", value: "1.5" },
|
{ label: "1.5", value: "1.5" },
|
||||||
|
@ -496,7 +474,6 @@ export const typography = [
|
||||||
key: "text-decoration-line",
|
key: "text-decoration-line",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "Underline", value: "underline" },
|
{ label: "Underline", value: "underline" },
|
||||||
{ label: "Overline", value: "overline" },
|
{ label: "Overline", value: "overline" },
|
||||||
{ label: "Line-through", value: "line-through" },
|
{ label: "Line-through", value: "line-through" },
|
||||||
|
@ -516,7 +493,6 @@ export const background = [
|
||||||
key: "background-image",
|
key: "background-image",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "none" },
|
{ label: "None", value: "none" },
|
||||||
{
|
{
|
||||||
label: "Warm Flame",
|
label: "Warm Flame",
|
||||||
|
@ -603,7 +579,6 @@ export const border = [
|
||||||
key: "border-radius",
|
key: "border-radius",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0" },
|
{ label: "None", value: "0" },
|
||||||
{ label: "X Small", value: "0.125rem" },
|
{ label: "X Small", value: "0.125rem" },
|
||||||
{ label: "Small", value: "0.25rem" },
|
{ label: "Small", value: "0.25rem" },
|
||||||
|
@ -619,7 +594,6 @@ export const border = [
|
||||||
key: "border-width",
|
key: "border-width",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0" },
|
{ label: "None", value: "0" },
|
||||||
{ label: "X Small", value: "0.5px" },
|
{ label: "X Small", value: "0.5px" },
|
||||||
{ label: "Small", value: "1px" },
|
{ label: "Small", value: "1px" },
|
||||||
|
@ -638,7 +612,6 @@ export const border = [
|
||||||
key: "border-style",
|
key: "border-style",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "none" },
|
{ label: "None", value: "none" },
|
||||||
{ label: "Hidden", value: "hidden" },
|
{ label: "Hidden", value: "hidden" },
|
||||||
{ label: "Dotted", value: "dotted" },
|
{ label: "Dotted", value: "dotted" },
|
||||||
|
@ -659,7 +632,6 @@ export const effects = [
|
||||||
key: "opacity",
|
key: "opacity",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "0", value: "0" },
|
{ label: "0", value: "0" },
|
||||||
{ label: "0.2", value: "0.2" },
|
{ label: "0.2", value: "0.2" },
|
||||||
{ label: "0.4", value: "0.4" },
|
{ label: "0.4", value: "0.4" },
|
||||||
|
@ -673,7 +645,6 @@ export const effects = [
|
||||||
key: "transform",
|
key: "transform",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "0" },
|
{ label: "None", value: "0" },
|
||||||
{ label: "45 deg", value: "rotate(45deg)" },
|
{ label: "45 deg", value: "rotate(45deg)" },
|
||||||
{ label: "90 deg", value: "rotate(90deg)" },
|
{ label: "90 deg", value: "rotate(90deg)" },
|
||||||
|
@ -690,7 +661,6 @@ export const effects = [
|
||||||
key: "box-shadow",
|
key: "box-shadow",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "none" },
|
{ label: "None", value: "none" },
|
||||||
{ label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
|
{ label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
|
||||||
{
|
{
|
||||||
|
@ -723,7 +693,6 @@ export const transitions = [
|
||||||
key: "transition-property",
|
key: "transition-property",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "None", value: "none" },
|
{ label: "None", value: "none" },
|
||||||
{ label: "All", value: "all" },
|
{ label: "All", value: "all" },
|
||||||
{ label: "Background Color", value: "background color" },
|
{ label: "Background Color", value: "background color" },
|
||||||
|
@ -745,7 +714,6 @@ export const transitions = [
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
placeholder: "sec",
|
placeholder: "sec",
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "0.4s", value: "0.4s" },
|
{ label: "0.4s", value: "0.4s" },
|
||||||
{ label: "0.6s", value: "0.6s" },
|
{ label: "0.6s", value: "0.6s" },
|
||||||
{ label: "0.8s", value: "0.8s" },
|
{ label: "0.8s", value: "0.8s" },
|
||||||
|
@ -759,7 +727,6 @@ export const transitions = [
|
||||||
key: "transition-timing-function",
|
key: "transition-timing-function",
|
||||||
control: OptionSelect,
|
control: OptionSelect,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Choose option", value: "" },
|
|
||||||
{ label: "Linear", value: "linear" },
|
{ label: "Linear", value: "linear" },
|
||||||
{ label: "Ease", value: "ease" },
|
{ label: "Ease", value: "ease" },
|
||||||
{ label: "Ease in", value: "ease-in" },
|
{ label: "Ease in", value: "ease-in" },
|
||||||
|
|
|
@ -842,10 +842,10 @@
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@budibase/bbui@^1.58.3":
|
"@budibase/bbui@^1.58.5":
|
||||||
version "1.58.3"
|
version "1.58.5"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.3.tgz#86ad6aa68eec7426e1ccdf1f7e7fc957cb57d3a3"
|
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.5.tgz#c9ce712941760825c7774a1de77594e989db4561"
|
||||||
integrity sha512-PpbxfBhVpmP0EO1nPBhrz486EHCIgtJlXELC/ElzjG+FCQZSCvDSM7mq/97FOW35iYdTiQTlwFgOtvOgT1P8IQ==
|
integrity sha512-0j1I7BetJ2GzB1BXKyvvlkuFphLmADJh2U/Ihubwxx5qUDY8REoVzLgAB4c24zt0CGVTF9VMmOoMLd0zD0QwdQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
markdown-it "^12.0.2"
|
markdown-it "^12.0.2"
|
||||||
quill "^1.3.7"
|
quill "^1.3.7"
|
||||||
|
|
|
@ -18,14 +18,12 @@ const handleError = error => {
|
||||||
const makeApiCall = async ({ method, url, body, json = true }) => {
|
const makeApiCall = async ({ method, url, body, json = true }) => {
|
||||||
try {
|
try {
|
||||||
const requestBody = json ? JSON.stringify(body) : body
|
const requestBody = json ? JSON.stringify(body) : body
|
||||||
let headers = {
|
const inBuilder = window["##BUDIBASE_IN_BUILDER##"]
|
||||||
|
const headers = {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
...(json && { "Content-Type": "application/json" }),
|
|
||||||
"x-budibase-app-id": window["##BUDIBASE_APP_ID##"],
|
"x-budibase-app-id": window["##BUDIBASE_APP_ID##"],
|
||||||
}
|
...(json && { "Content-Type": "application/json" }),
|
||||||
|
...(!inBuilder && { "x-budibase-type": "client" }),
|
||||||
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
|
||||||
headers["x-budibase-type"] = "client"
|
|
||||||
}
|
}
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { fetchTableData, searchTableData } from "./tables"
|
import { fetchTableData } from "./tables"
|
||||||
import { fetchViewData } from "./views"
|
import { fetchViewData } from "./views"
|
||||||
import { fetchRelationshipData } from "./relationships"
|
import { fetchRelationshipData } from "./relationships"
|
||||||
import { executeQuery } from "./queries"
|
import { executeQuery } from "./queries"
|
||||||
import { enrichRows } from "./rows"
|
|
||||||
|
|
||||||
export const searchTable = async ({ tableId, search, pagination }) => {
|
|
||||||
const rows = await searchTableData({ tableId, search, pagination })
|
|
||||||
return await enrichRows(rows, tableId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all rows for a particular Budibase data source.
|
* Fetches all rows for a particular Budibase data source.
|
||||||
|
@ -33,7 +27,7 @@ export const fetchDatasource = async datasource => {
|
||||||
parameters[param.name] = param.default
|
parameters[param.name] = param.default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await executeQuery({ queryId: datasource._id, parameters })
|
rows = await executeQuery({ queryId: datasource._id, parameters })
|
||||||
} else if (type === "link") {
|
} else if (type === "link") {
|
||||||
rows = await fetchRelationshipData({
|
rows = await fetchRelationshipData({
|
||||||
rowId: datasource.rowId,
|
rowId: datasource.rowId,
|
||||||
|
@ -42,6 +36,6 @@ export const fetchDatasource = async datasource => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich rows so they can displayed properly
|
// Enrich the result is always an array
|
||||||
return await enrichRows(rows, tableId)
|
return Array.isArray(rows) ? rows : []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { notificationStore } from "../store/notification"
|
import { notificationStore, datasourceStore } from "../store"
|
||||||
import API from "./api"
|
import API from "./api"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a query against an external data connector.
|
* Executes a query against an external data connector.
|
||||||
*/
|
*/
|
||||||
export const executeQuery = async ({ queryId, parameters }) => {
|
export const executeQuery = async ({ queryId, parameters }) => {
|
||||||
|
const query = await API.get({ url: `/api/queries/${queryId}` })
|
||||||
|
if (query?.datasourceId == null) {
|
||||||
|
notificationStore.danger("That query couldn't be found")
|
||||||
|
return
|
||||||
|
}
|
||||||
const res = await API.post({
|
const res = await API.post({
|
||||||
url: `/api/queries/${queryId}`,
|
url: `/api/queries/${queryId}`,
|
||||||
body: {
|
body: {
|
||||||
|
@ -13,6 +18,9 @@ export const executeQuery = async ({ queryId, parameters }) => {
|
||||||
})
|
})
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
notificationStore.danger("An error has occurred")
|
notificationStore.danger("An error has occurred")
|
||||||
|
} else if (!query.readable) {
|
||||||
|
notificationStore.success("Query executed successfully")
|
||||||
|
datasourceStore.actions.invalidateDatasource(query.datasourceId)
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { notificationStore } from "../store/notification"
|
import { notificationStore, datasourceStore } from "../store"
|
||||||
import API from "./api"
|
import API from "./api"
|
||||||
import { fetchTableDefinition } from "./tables"
|
import { fetchTableDefinition } from "./tables"
|
||||||
|
|
||||||
|
@ -6,6 +6,9 @@ import { fetchTableDefinition } from "./tables"
|
||||||
* Fetches data about a certain row in a table.
|
* Fetches data about a certain row in a table.
|
||||||
*/
|
*/
|
||||||
export const fetchRow = async ({ tableId, rowId }) => {
|
export const fetchRow = async ({ tableId, rowId }) => {
|
||||||
|
if (!tableId || !rowId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const row = await API.get({
|
const row = await API.get({
|
||||||
url: `/api/${tableId}/rows/${rowId}`,
|
url: `/api/${tableId}/rows/${rowId}`,
|
||||||
})
|
})
|
||||||
|
@ -16,6 +19,9 @@ export const fetchRow = async ({ tableId, rowId }) => {
|
||||||
* Creates a row in a table.
|
* Creates a row in a table.
|
||||||
*/
|
*/
|
||||||
export const saveRow = async row => {
|
export const saveRow = async row => {
|
||||||
|
if (!row?.tableId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const res = await API.post({
|
const res = await API.post({
|
||||||
url: `/api/${row.tableId}/rows`,
|
url: `/api/${row.tableId}/rows`,
|
||||||
body: row,
|
body: row,
|
||||||
|
@ -23,6 +29,10 @@ export const saveRow = async row => {
|
||||||
res.error
|
res.error
|
||||||
? notificationStore.danger("An error has occurred")
|
? notificationStore.danger("An error has occurred")
|
||||||
: notificationStore.success("Row saved")
|
: notificationStore.success("Row saved")
|
||||||
|
|
||||||
|
// Refresh related datasources
|
||||||
|
datasourceStore.actions.invalidateDatasource(row.tableId)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +40,9 @@ export const saveRow = async row => {
|
||||||
* Updates a row in a table.
|
* Updates a row in a table.
|
||||||
*/
|
*/
|
||||||
export const updateRow = async row => {
|
export const updateRow = async row => {
|
||||||
|
if (!row?.tableId || !row?._id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const res = await API.patch({
|
const res = await API.patch({
|
||||||
url: `/api/${row.tableId}/rows/${row._id}`,
|
url: `/api/${row.tableId}/rows/${row._id}`,
|
||||||
body: row,
|
body: row,
|
||||||
|
@ -37,6 +50,10 @@ export const updateRow = async row => {
|
||||||
res.error
|
res.error
|
||||||
? notificationStore.danger("An error has occurred")
|
? notificationStore.danger("An error has occurred")
|
||||||
: notificationStore.success("Row updated")
|
: notificationStore.success("Row updated")
|
||||||
|
|
||||||
|
// Refresh related datasources
|
||||||
|
datasourceStore.actions.invalidateDatasource(row.tableId)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,12 +61,19 @@ export const updateRow = async row => {
|
||||||
* Deletes a row from a table.
|
* Deletes a row from a table.
|
||||||
*/
|
*/
|
||||||
export const deleteRow = async ({ tableId, rowId, revId }) => {
|
export const deleteRow = async ({ tableId, rowId, revId }) => {
|
||||||
|
if (!tableId || !rowId || !revId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const res = await API.del({
|
const res = await API.del({
|
||||||
url: `/api/${tableId}/rows/${rowId}/${revId}`,
|
url: `/api/${tableId}/rows/${rowId}/${revId}`,
|
||||||
})
|
})
|
||||||
res.error
|
res.error
|
||||||
? notificationStore.danger("An error has occurred")
|
? notificationStore.danger("An error has occurred")
|
||||||
: notificationStore.success("Row deleted")
|
: notificationStore.success("Row deleted")
|
||||||
|
|
||||||
|
// Refresh related datasources
|
||||||
|
datasourceStore.actions.invalidateDatasource(tableId)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +81,9 @@ export const deleteRow = async ({ tableId, rowId, revId }) => {
|
||||||
* Deletes many rows from a table.
|
* Deletes many rows from a table.
|
||||||
*/
|
*/
|
||||||
export const deleteRows = async ({ tableId, rows }) => {
|
export const deleteRows = async ({ tableId, rows }) => {
|
||||||
|
if (!tableId || !rows) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const res = await API.post({
|
const res = await API.post({
|
||||||
url: `/api/${tableId}/rows`,
|
url: `/api/${tableId}/rows`,
|
||||||
body: {
|
body: {
|
||||||
|
@ -67,6 +94,10 @@ export const deleteRows = async ({ tableId, rows }) => {
|
||||||
res.error
|
res.error
|
||||||
? notificationStore.danger("An error has occurred")
|
? notificationStore.danger("An error has occurred")
|
||||||
: notificationStore.success(`${rows.length} row(s) deleted`)
|
: notificationStore.success(`${rows.length} row(s) deleted`)
|
||||||
|
|
||||||
|
// Refresh related datasources
|
||||||
|
datasourceStore.actions.invalidateDatasource(tableId)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +106,10 @@ export const deleteRows = async ({ tableId, rows }) => {
|
||||||
* be properly displayed.
|
* be properly displayed.
|
||||||
*/
|
*/
|
||||||
export const enrichRows = async (rows, tableId) => {
|
export const enrichRows = async (rows, tableId) => {
|
||||||
if (rows && rows.length && tableId) {
|
if (!Array.isArray(rows)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (rows.length && tableId) {
|
||||||
// Fetch table schema so we can check column types
|
// Fetch table schema so we can check column types
|
||||||
const tableDefinition = await fetchTableDefinition(tableId)
|
const tableDefinition = await fetchTableDefinition(tableId)
|
||||||
const schema = tableDefinition && tableDefinition.schema
|
const schema = tableDefinition && tableDefinition.schema
|
||||||
|
|
|
@ -3,14 +3,19 @@
|
||||||
import { setContext, onMount } from "svelte"
|
import { setContext, onMount } from "svelte"
|
||||||
import Component from "./Component.svelte"
|
import Component from "./Component.svelte"
|
||||||
import NotificationDisplay from "./NotificationDisplay.svelte"
|
import NotificationDisplay from "./NotificationDisplay.svelte"
|
||||||
|
import Provider from "./Provider.svelte"
|
||||||
import SDK from "../sdk"
|
import SDK from "../sdk"
|
||||||
import { createDataStore, initialise, screenStore, authStore } from "../store"
|
import {
|
||||||
|
createContextStore,
|
||||||
|
initialise,
|
||||||
|
screenStore,
|
||||||
|
authStore,
|
||||||
|
} from "../store"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
setContext("sdk", SDK)
|
setContext("sdk", SDK)
|
||||||
setContext("component", writable({}))
|
setContext("component", writable({}))
|
||||||
setContext("data", createDataStore())
|
setContext("context", createContextStore())
|
||||||
setContext("screenslot", false)
|
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
|
@ -23,6 +28,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded && $screenStore.activeLayout}
|
{#if loaded && $screenStore.activeLayout}
|
||||||
<Component definition={$screenStore.activeLayout.props} />
|
<Provider key="user" data={$authStore}>
|
||||||
|
<Component definition={$screenStore.activeLayout.props} />
|
||||||
|
<NotificationDisplay />
|
||||||
|
</Provider>
|
||||||
{/if}
|
{/if}
|
||||||
<NotificationDisplay />
|
|
||||||
|
|
|
@ -1,19 +1,28 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
import { writable } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import * as ComponentLibrary from "@budibase/standard-components"
|
import * as ComponentLibrary from "@budibase/standard-components"
|
||||||
import Router from "./Router.svelte"
|
import Router from "./Router.svelte"
|
||||||
import { enrichProps, propsAreSame } from "../utils/componentProps"
|
import { enrichProps, propsAreSame } from "../utils/componentProps"
|
||||||
import { authStore, bindingStore, builderStore } from "../store"
|
import { builderStore } from "../store"
|
||||||
|
import { hashString } from "../utils/hash"
|
||||||
|
|
||||||
export let definition = {}
|
export let definition = {}
|
||||||
|
|
||||||
let enrichedProps
|
// Props that will be passed to the component instance
|
||||||
let componentProps
|
let componentProps
|
||||||
|
|
||||||
|
// Props are hashed when inside the builder preview and used as a key, so that
|
||||||
|
// components fully remount whenever any props change
|
||||||
|
let propsHash = 0
|
||||||
|
|
||||||
|
// Latest timestamp that we started a props update.
|
||||||
|
// Due to enrichment now being async, we need to avoid overwriting newer
|
||||||
|
// props with old ones, depending on how long enrichment takes.
|
||||||
|
let latestUpdateTime
|
||||||
|
|
||||||
// Get contexts
|
// Get contexts
|
||||||
const dataContext = getContext("data")
|
const context = getContext("context")
|
||||||
const screenslotContext = getContext("screenslot")
|
|
||||||
|
|
||||||
// Create component context
|
// Create component context
|
||||||
const componentStore = writable({})
|
const componentStore = writable({})
|
||||||
|
@ -23,39 +32,16 @@
|
||||||
$: constructor = getComponentConstructor(definition._component)
|
$: constructor = getComponentConstructor(definition._component)
|
||||||
$: children = definition._children || []
|
$: children = definition._children || []
|
||||||
$: id = definition._id
|
$: id = definition._id
|
||||||
$: enrichComponentProps(definition, $dataContext, $bindingStore, $authStore)
|
$: updateComponentProps(definition, $context)
|
||||||
$: updateProps(enrichedProps)
|
|
||||||
$: styles = definition._styles
|
$: styles = definition._styles
|
||||||
|
|
||||||
// Allow component selection in the builder preview if we're previewing a
|
|
||||||
// layout, or we're preview a screen and we're inside the screenslot
|
|
||||||
$: allowSelection =
|
|
||||||
$builderStore.previewType === "layout" || screenslotContext
|
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: componentStore.set({
|
$: componentStore.set({
|
||||||
id,
|
id,
|
||||||
children: children.length,
|
children: children.length,
|
||||||
styles: { ...styles, id, allowSelection },
|
styles: { ...styles, id },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Updates the component props.
|
|
||||||
// Most props are deeply compared so that svelte will only trigger reactive
|
|
||||||
// statements on props that have actually changed.
|
|
||||||
const updateProps = props => {
|
|
||||||
if (!props) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!componentProps) {
|
|
||||||
componentProps = {}
|
|
||||||
}
|
|
||||||
Object.keys(props).forEach(key => {
|
|
||||||
if (!propsAreSame(props[key], componentProps[key])) {
|
|
||||||
componentProps[key] = props[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the component constructor for the specified component
|
// Gets the component constructor for the specified component
|
||||||
const getComponentConstructor = component => {
|
const getComponentConstructor = component => {
|
||||||
const split = component?.split("/")
|
const split = component?.split("/")
|
||||||
|
@ -67,25 +53,53 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enriches any string component props using handlebars
|
// Enriches any string component props using handlebars
|
||||||
const enrichComponentProps = async (definition, context, bindings, user) => {
|
const updateComponentProps = async (definition, context) => {
|
||||||
enrichedProps = await enrichProps(definition, context, bindings, user)
|
// Record the timestamp so we can reference it after enrichment
|
||||||
}
|
latestUpdateTime = Date.now()
|
||||||
|
const enrichmentTime = latestUpdateTime
|
||||||
|
|
||||||
// Returns a unique key to let svelte know when to remount components.
|
// Enrich props with context
|
||||||
// If a component is selected we want to remount it every time any props
|
const enrichedProps = await enrichProps(definition, context)
|
||||||
// change.
|
|
||||||
const getChildKey = childId => {
|
// Abandon this update if a newer update has started
|
||||||
const selected = childId === $builderStore.selectedComponentId
|
if (enrichmentTime !== latestUpdateTime) {
|
||||||
return selected ? `${childId}-${$builderStore.previewId}` : childId
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the component props.
|
||||||
|
// Most props are deeply compared so that svelte will only trigger reactive
|
||||||
|
// statements on props that have actually changed.
|
||||||
|
if (!enrichedProps) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let propsChanged = false
|
||||||
|
if (!componentProps) {
|
||||||
|
componentProps = {}
|
||||||
|
propsChanged = true
|
||||||
|
}
|
||||||
|
Object.keys(enrichedProps).forEach(key => {
|
||||||
|
if (!propsAreSame(enrichedProps[key], componentProps[key])) {
|
||||||
|
propsChanged = true
|
||||||
|
componentProps[key] = enrichedProps[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the hash if we're in the builder so we can fully remount this
|
||||||
|
// component
|
||||||
|
if (get(builderStore).inBuilder && propsChanged) {
|
||||||
|
propsHash = hashString(JSON.stringify(componentProps))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if constructor && componentProps}
|
{#if constructor && componentProps}
|
||||||
<svelte:component this={constructor} {...componentProps}>
|
{#key propsHash}
|
||||||
{#if children.length}
|
<svelte:component this={constructor} {...componentProps}>
|
||||||
{#each children as child (getChildKey(child._id))}
|
{#if children.length}
|
||||||
<svelte:self definition={child} />
|
{#each children as child (child._id)}
|
||||||
{/each}
|
<svelte:self definition={child} />
|
||||||
{/if}
|
{/each}
|
||||||
</svelte:component>
|
{/if}
|
||||||
|
</svelte:component>
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext, setContext } from "svelte"
|
|
||||||
import { createDataStore } from "../store"
|
|
||||||
|
|
||||||
export let row
|
|
||||||
|
|
||||||
// Clone and create new data context for this component tree
|
|
||||||
const dataContext = getContext("data")
|
|
||||||
const component = getContext("component")
|
|
||||||
const newData = createDataStore($dataContext)
|
|
||||||
setContext("data", newData)
|
|
||||||
$: newData.actions.addContext(row, $component.id)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import { getContext, setContext, onMount } from "svelte"
|
||||||
|
import { datasourceStore, createContextStore } from "../store"
|
||||||
|
import { ActionTypes } from "../constants"
|
||||||
|
import { generate } from "shortid"
|
||||||
|
|
||||||
|
export let data
|
||||||
|
export let actions
|
||||||
|
export let key
|
||||||
|
|
||||||
|
// Clone and create new data context for this component tree
|
||||||
|
const context = getContext("context")
|
||||||
|
const component = getContext("component")
|
||||||
|
const newContext = createContextStore(context)
|
||||||
|
setContext("context", newContext)
|
||||||
|
|
||||||
|
$: providerKey = key || $component.id
|
||||||
|
|
||||||
|
// Add data context
|
||||||
|
$: newContext.actions.provideData(providerKey, data)
|
||||||
|
|
||||||
|
// Instance ID is unique to each instance of a provider
|
||||||
|
let instanceId
|
||||||
|
|
||||||
|
// Add actions context
|
||||||
|
$: {
|
||||||
|
if (instanceId) {
|
||||||
|
actions?.forEach(({ type, callback, metadata }) => {
|
||||||
|
newContext.actions.provideAction(providerKey, type, callback)
|
||||||
|
|
||||||
|
// Register any "refresh datasource" actions with a singleton store
|
||||||
|
// so we can easily refresh data at all levels for any datasource
|
||||||
|
if (type === ActionTypes.RefreshDatasource) {
|
||||||
|
const { datasource } = metadata || {}
|
||||||
|
datasourceStore.actions.registerDatasource(
|
||||||
|
datasource,
|
||||||
|
instanceId,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Generate a permanent unique ID for this component and use it to register
|
||||||
|
// any datasource actions
|
||||||
|
instanceId = generate()
|
||||||
|
|
||||||
|
// Unregister all datasource instances when unmounting this provider
|
||||||
|
return () => datasourceStore.actions.unregisterInstance(instanceId)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
|
@ -7,9 +7,6 @@
|
||||||
const { styleable } = getContext("sdk")
|
const { styleable } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
// Set context flag so components know that we're now inside the screenslot
|
|
||||||
setContext("screenslot", true)
|
|
||||||
|
|
||||||
// Only wrap this as an array to take advantage of svelte keying,
|
// Only wrap this as an array to take advantage of svelte keying,
|
||||||
// to ensure the svelte-spa-router is fully remounted when route config
|
// to ensure the svelte-spa-router is fully remounted when route config
|
||||||
// changes
|
// changes
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
export const TableNames = {
|
export const TableNames = {
|
||||||
USERS: "ta_users",
|
USERS: "ta_users",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ActionTypes = {
|
||||||
|
ValidateForm: "ValidateForm",
|
||||||
|
RefreshDatasource: "RefreshDatasource",
|
||||||
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@ import {
|
||||||
notificationStore,
|
notificationStore,
|
||||||
routeStore,
|
routeStore,
|
||||||
screenStore,
|
screenStore,
|
||||||
bindingStore,
|
|
||||||
builderStore,
|
builderStore,
|
||||||
} from "./store"
|
} from "./store"
|
||||||
import { styleable } from "./utils/styleable"
|
import { styleable } from "./utils/styleable"
|
||||||
import { linkable } from "./utils/linkable"
|
import { linkable } from "./utils/linkable"
|
||||||
import DataProvider from "./components/DataProvider.svelte"
|
import Provider from "./components/Provider.svelte"
|
||||||
|
import { ActionTypes } from "./constants"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
API,
|
API,
|
||||||
|
@ -20,6 +20,6 @@ export default {
|
||||||
builderStore,
|
builderStore,
|
||||||
styleable,
|
styleable,
|
||||||
linkable,
|
linkable,
|
||||||
DataProvider,
|
Provider,
|
||||||
setBindableValue: bindingStore.actions.setBindableValue,
|
ActionTypes,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
|
|
||||||
const createBindingStore = () => {
|
|
||||||
const store = writable({})
|
|
||||||
|
|
||||||
const setBindableValue = (value, componentId) => {
|
|
||||||
store.update(state => {
|
|
||||||
if (componentId) {
|
|
||||||
state[componentId] = value
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: store.subscribe,
|
|
||||||
actions: { setBindableValue },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const bindingStore = createBindingStore()
|
|
|
@ -13,9 +13,11 @@ const createBuilderStore = () => {
|
||||||
const store = writable(initialState)
|
const store = writable(initialState)
|
||||||
const actions = {
|
const actions = {
|
||||||
selectComponent: id => {
|
selectComponent: id => {
|
||||||
window.dispatchEvent(
|
if (id) {
|
||||||
new CustomEvent("bb-select-component", { detail: id })
|
window.dispatchEvent(
|
||||||
)
|
new CustomEvent("bb-select-component", { detail: id })
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { writable, derived } from "svelte/store"
|
||||||
|
|
||||||
|
export const createContextStore = oldContext => {
|
||||||
|
const newContext = writable({})
|
||||||
|
const contexts = oldContext ? [oldContext, newContext] : [newContext]
|
||||||
|
const totalContext = derived(contexts, $contexts => {
|
||||||
|
return $contexts.reduce((total, context) => ({ ...total, ...context }), {})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Adds a data context layer to the tree
|
||||||
|
const provideData = (providerId, data) => {
|
||||||
|
if (!providerId || data === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newContext.update(state => {
|
||||||
|
state[providerId] = data
|
||||||
|
|
||||||
|
// Keep track of the closest component ID so we can later hydrate a "data" prop.
|
||||||
|
// This is only required for legacy bindings that used "data" rather than a
|
||||||
|
// component ID.
|
||||||
|
state.closestComponentId = providerId
|
||||||
|
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds an action context layer to the tree
|
||||||
|
const provideAction = (providerId, actionType, callback) => {
|
||||||
|
if (!providerId || !actionType) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newContext.update(state => {
|
||||||
|
state[`${providerId}_${actionType}`] = callback
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: totalContext.subscribe,
|
||||||
|
actions: { provideData, provideAction },
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
|
|
||||||
export const createDataStore = existingContext => {
|
|
||||||
const store = writable({ ...existingContext })
|
|
||||||
|
|
||||||
// Adds a context layer to the data context tree
|
|
||||||
const addContext = (row, componentId) => {
|
|
||||||
store.update(state => {
|
|
||||||
if (componentId) {
|
|
||||||
state[componentId] = row
|
|
||||||
state[`${componentId}_draft`] = cloneDeep(row)
|
|
||||||
state.closestComponentId = componentId
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: store.subscribe,
|
|
||||||
update: store.update,
|
|
||||||
actions: { addContext },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dataStore = createDataStore()
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
import { notificationStore } from "./notification"
|
||||||
|
|
||||||
|
export const createDatasourceStore = () => {
|
||||||
|
const store = writable([])
|
||||||
|
|
||||||
|
// Registers a new datasource instance
|
||||||
|
const registerDatasource = (datasource, instanceId, refresh) => {
|
||||||
|
if (!datasource || !instanceId || !refresh) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a list of all relevant datasource IDs which would require that
|
||||||
|
// this datasource is refreshed
|
||||||
|
let datasourceIds = []
|
||||||
|
|
||||||
|
// Extract table ID
|
||||||
|
if (datasource.type === "table") {
|
||||||
|
if (datasource.tableId) {
|
||||||
|
datasourceIds.push(datasource.tableId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract both table IDs from both sides of the relationship
|
||||||
|
else if (datasource.type === "link") {
|
||||||
|
if (datasource.rowTableId) {
|
||||||
|
datasourceIds.push(datasource.rowTableId)
|
||||||
|
}
|
||||||
|
if (datasource.tableId) {
|
||||||
|
datasourceIds.push(datasource.tableId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the datasource ID (not the query ID) for queries
|
||||||
|
else if (datasource.type === "query") {
|
||||||
|
if (datasource.datasourceId) {
|
||||||
|
datasourceIds.push(datasource.datasourceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store configs for each relevant datasource ID
|
||||||
|
if (datasourceIds.length) {
|
||||||
|
store.update(state => {
|
||||||
|
datasourceIds.forEach(id => {
|
||||||
|
state.push({
|
||||||
|
datasourceId: id,
|
||||||
|
instanceId,
|
||||||
|
refresh,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes all registered datasource instances belonging to a particular
|
||||||
|
// instance ID
|
||||||
|
const unregisterInstance = instanceId => {
|
||||||
|
store.update(state => {
|
||||||
|
return state.filter(instance => instance.instanceId !== instanceId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidates a specific datasource ID by refreshing all instances
|
||||||
|
// which depend on data from that datasource
|
||||||
|
const invalidateDatasource = datasourceId => {
|
||||||
|
const relatedInstances = get(store).filter(instance => {
|
||||||
|
return instance.datasourceId === datasourceId
|
||||||
|
})
|
||||||
|
if (relatedInstances?.length) {
|
||||||
|
notificationStore.blockNotifications(1000)
|
||||||
|
}
|
||||||
|
relatedInstances?.forEach(instance => {
|
||||||
|
instance.refresh()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { registerDatasource, unregisterInstance, invalidateDatasource },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const datasourceStore = createDatasourceStore()
|
|
@ -3,10 +3,10 @@ export { notificationStore } from "./notification"
|
||||||
export { routeStore } from "./routes"
|
export { routeStore } from "./routes"
|
||||||
export { screenStore } from "./screens"
|
export { screenStore } from "./screens"
|
||||||
export { builderStore } from "./builder"
|
export { builderStore } from "./builder"
|
||||||
export { bindingStore } from "./binding"
|
export { datasourceStore } from "./datasource"
|
||||||
|
|
||||||
// Data stores are layered and duplicated, so it is not a singleton
|
// Context stores are layered and duplicated, so it is not a singleton
|
||||||
export { createDataStore, dataStore } from "./data"
|
export { createContextStore } from "./context"
|
||||||
|
|
||||||
// Initialises an app by loading screens and routes
|
// Initialises an app by loading screens and routes
|
||||||
export { initialise } from "./initialise"
|
export { initialise } from "./initialise"
|
||||||
|
|
|
@ -5,13 +5,22 @@ const NOTIFICATION_TIMEOUT = 3000
|
||||||
|
|
||||||
const createNotificationStore = () => {
|
const createNotificationStore = () => {
|
||||||
const _notifications = writable([])
|
const _notifications = writable([])
|
||||||
|
let block = false
|
||||||
|
|
||||||
const send = (message, type = "default") => {
|
const send = (message, type = "default") => {
|
||||||
|
if (block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
_notifications.update(state => {
|
_notifications.update(state => {
|
||||||
return [...state, { id: generate(), type, message }]
|
return [...state, { id: generate(), type, message }]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blockNotifications = (timeout = 1000) => {
|
||||||
|
block = true
|
||||||
|
setTimeout(() => (block = false), timeout)
|
||||||
|
}
|
||||||
|
|
||||||
const notifications = derived(_notifications, ($_notifications, set) => {
|
const notifications = derived(_notifications, ($_notifications, set) => {
|
||||||
set($_notifications)
|
set($_notifications)
|
||||||
if ($_notifications.length > 0) {
|
if ($_notifications.length > 0) {
|
||||||
|
@ -36,6 +45,7 @@ const createNotificationStore = () => {
|
||||||
warning: msg => send(msg, "warning"),
|
warning: msg => send(msg, "warning"),
|
||||||
info: msg => send(msg, "info"),
|
info: msg => send(msg, "info"),
|
||||||
success: msg => send(msg, "success"),
|
success: msg => send(msg, "success"),
|
||||||
|
blockNotifications,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,43 +1,34 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { enrichDataBinding, enrichDataBindings } from "./enrichDataBinding"
|
|
||||||
import { routeStore, builderStore } from "../store"
|
import { routeStore, builderStore } from "../store"
|
||||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
|
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
|
||||||
|
import { ActionTypes } from "../constants"
|
||||||
|
|
||||||
const saveRowHandler = async (action, context) => {
|
const saveRowHandler = async (action, context) => {
|
||||||
const { fields, providerId } = action.parameters
|
const { fields, providerId } = action.parameters
|
||||||
if (providerId) {
|
if (providerId) {
|
||||||
let draft = context[`${providerId}_draft`]
|
let draft = context[providerId]
|
||||||
if (fields) {
|
if (fields) {
|
||||||
for (let [key, entry] of Object.entries(fields)) {
|
for (let [key, entry] of Object.entries(fields)) {
|
||||||
draft[key] = await enrichDataBinding(entry.value, context)
|
draft[key] = entry.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await saveRow(draft)
|
await saveRow(draft)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRowHandler = async (action, context) => {
|
const deleteRowHandler = async action => {
|
||||||
const { tableId, revId, rowId } = action.parameters
|
const { tableId, revId, rowId } = action.parameters
|
||||||
if (tableId && revId && rowId) {
|
if (tableId && revId && rowId) {
|
||||||
const [enrichTable, enrichRow, enrichRev] = await Promise.all([
|
await deleteRow({ tableId, rowId, revId })
|
||||||
enrichDataBinding(tableId, context),
|
|
||||||
enrichDataBinding(rowId, context),
|
|
||||||
enrichDataBinding(revId, context),
|
|
||||||
])
|
|
||||||
await deleteRow({
|
|
||||||
tableId: enrichTable,
|
|
||||||
rowId: enrichRow,
|
|
||||||
revId: enrichRev,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerAutomationHandler = async (action, context) => {
|
const triggerAutomationHandler = async action => {
|
||||||
const { fields } = action.parameters()
|
const { fields } = action.parameters
|
||||||
if (fields) {
|
if (fields) {
|
||||||
const params = {}
|
const params = {}
|
||||||
for (let field in fields) {
|
for (let field in fields) {
|
||||||
params[field] = await enrichDataBinding(fields[field].value, context)
|
params[field] = fields[field].value
|
||||||
}
|
}
|
||||||
await triggerAutomation(action.parameters.automationId, params)
|
await triggerAutomation(action.parameters.automationId, params)
|
||||||
}
|
}
|
||||||
|
@ -49,25 +40,46 @@ const navigationHandler = action => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryExecutionHandler = async (action, context) => {
|
const queryExecutionHandler = async action => {
|
||||||
const { datasourceId, queryId, queryParams } = action.parameters
|
const { datasourceId, queryId, queryParams } = action.parameters
|
||||||
const enrichedQueryParameters = await enrichDataBindings(
|
|
||||||
queryParams || {},
|
|
||||||
context
|
|
||||||
)
|
|
||||||
await executeQuery({
|
await executeQuery({
|
||||||
datasourceId,
|
datasourceId,
|
||||||
queryId,
|
queryId,
|
||||||
parameters: enrichedQueryParameters,
|
parameters: queryParams,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const executeActionHandler = async (context, componentId, actionType) => {
|
||||||
|
const fn = context[`${componentId}_${actionType}`]
|
||||||
|
if (fn) {
|
||||||
|
return await fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateFormHandler = async (action, context) => {
|
||||||
|
return await executeActionHandler(
|
||||||
|
context,
|
||||||
|
action.parameters.componentId,
|
||||||
|
ActionTypes.ValidateForm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshDatasourceHandler = async (action, context) => {
|
||||||
|
return await executeActionHandler(
|
||||||
|
context,
|
||||||
|
action.parameters.componentId,
|
||||||
|
ActionTypes.RefreshDatasource
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const handlerMap = {
|
const handlerMap = {
|
||||||
["Save Row"]: saveRowHandler,
|
["Save Row"]: saveRowHandler,
|
||||||
["Delete Row"]: deleteRowHandler,
|
["Delete Row"]: deleteRowHandler,
|
||||||
["Navigate To"]: navigationHandler,
|
["Navigate To"]: navigationHandler,
|
||||||
["Execute Query"]: queryExecutionHandler,
|
["Execute Query"]: queryExecutionHandler,
|
||||||
["Trigger Automation"]: triggerAutomationHandler,
|
["Trigger Automation"]: triggerAutomationHandler,
|
||||||
|
["Validate Form"]: validateFormHandler,
|
||||||
|
["Refresh Datasource"]: refreshDatasourceHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,7 +94,18 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||||
return async () => {
|
return async () => {
|
||||||
for (let i = 0; i < handlers.length; i++) {
|
for (let i = 0; i < handlers.length; i++) {
|
||||||
await handlers[i](actions[i], context)
|
try {
|
||||||
|
const result = await handlers[i](actions[i], context)
|
||||||
|
// A handler returning `false` is a flag to stop execution of handlers
|
||||||
|
if (result === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error while executing button handler")
|
||||||
|
console.error(error)
|
||||||
|
// Stop executing on an error
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const propsAreSame = (a, b) => {
|
||||||
* Enriches component props.
|
* Enriches component props.
|
||||||
* Data bindings are enriched, and button actions are enriched.
|
* Data bindings are enriched, and button actions are enriched.
|
||||||
*/
|
*/
|
||||||
export const enrichProps = async (props, dataContexts, dataBindings, user) => {
|
export const enrichProps = async (props, context) => {
|
||||||
// Exclude all private props that start with an underscore
|
// Exclude all private props that start with an underscore
|
||||||
let validProps = {}
|
let validProps = {}
|
||||||
Object.entries(props)
|
Object.entries(props)
|
||||||
|
@ -32,20 +32,23 @@ export const enrichProps = async (props, dataContexts, dataBindings, user) => {
|
||||||
|
|
||||||
// Create context of all bindings and data contexts
|
// Create context of all bindings and data contexts
|
||||||
// Duplicate the closest context as "data" which the builder requires
|
// Duplicate the closest context as "data" which the builder requires
|
||||||
const context = {
|
const totalContext = {
|
||||||
...dataContexts,
|
...context,
|
||||||
...dataBindings,
|
|
||||||
user,
|
// This is only required for legacy bindings that used "data" rather than a
|
||||||
data: dataContexts[dataContexts.closestComponentId],
|
// component ID.
|
||||||
data_draft: dataContexts[`${dataContexts.closestComponentId}_draft`],
|
data: context[context.closestComponentId],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich all data bindings in top level props
|
// Enrich all data bindings in top level props
|
||||||
let enrichedProps = await enrichDataBindings(validProps, context)
|
let enrichedProps = await enrichDataBindings(validProps, totalContext)
|
||||||
|
|
||||||
// Enrich button actions if they exist
|
// Enrich button actions if they exist
|
||||||
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
||||||
enrichedProps.onClick = enrichButtonActions(enrichedProps.onClick, context)
|
enrichedProps.onClick = enrichButtonActions(
|
||||||
|
enrichedProps.onClick,
|
||||||
|
totalContext
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return enrichedProps
|
return enrichedProps
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const hashString = str => {
|
||||||
|
if (!str) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
let hash = 0
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
let char = str.charCodeAt(i)
|
||||||
|
hash = (hash << 5) - hash + char
|
||||||
|
hash = hash & hash // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}
|
|
@ -1,15 +1,12 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { builderStore } from "../store"
|
import { builderStore } from "../store"
|
||||||
|
|
||||||
const selectedComponentWidth = 2
|
|
||||||
const selectedComponentColor = "#4285f4"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to build a CSS string from a style object.
|
* Helper to build a CSS string from a style object.
|
||||||
*/
|
*/
|
||||||
const buildStyleString = (styleObject, customStyles) => {
|
const buildStyleString = (styleObject, customStyles) => {
|
||||||
let str = ""
|
let str = ""
|
||||||
Object.entries(styleObject).forEach(([style, value]) => {
|
Object.entries(styleObject || {}).forEach(([style, value]) => {
|
||||||
if (style && value != null) {
|
if (style && value != null) {
|
||||||
str += `${style}: ${value}; `
|
str += `${style}: ${value}; `
|
||||||
}
|
}
|
||||||
|
@ -23,24 +20,14 @@ const buildStyleString = (styleObject, customStyles) => {
|
||||||
* events for any selectable components (overriding the blanket ban on pointer
|
* events for any selectable components (overriding the blanket ban on pointer
|
||||||
* events in the iframe HTML).
|
* events in the iframe HTML).
|
||||||
*/
|
*/
|
||||||
const addBuilderPreviewStyles = (styleString, componentId, selectable) => {
|
const addBuilderPreviewStyles = (node, styleString, componentId) => {
|
||||||
let str = styleString
|
if (componentId === get(builderStore).selectedComponentId) {
|
||||||
|
const style = window.getComputedStyle(node)
|
||||||
// Apply extra styles if we're in the builder preview
|
const property = style?.display === "table-row" ? "outline" : "border"
|
||||||
const state = get(builderStore)
|
return styleString + `;${property}: 2px solid #4285f4 !important;`
|
||||||
if (state.inBuilder) {
|
} else {
|
||||||
// Allow pointer events and always enable cursor
|
return styleString
|
||||||
if (selectable) {
|
|
||||||
str += ";pointer-events: all !important; cursor: pointer !important;"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlighted selected element
|
|
||||||
if (componentId === state.selectedComponentId) {
|
|
||||||
str += `;border: ${selectedComponentWidth}px solid ${selectedComponentColor} !important;`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return str
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,28 +39,20 @@ export const styleable = (node, styles = {}) => {
|
||||||
let applyHoverStyles
|
let applyHoverStyles
|
||||||
let selectComponent
|
let selectComponent
|
||||||
|
|
||||||
// Kill JS even bubbling
|
|
||||||
const blockEvent = event => {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates event listeners and applies initial styles
|
// Creates event listeners and applies initial styles
|
||||||
const setupStyles = newStyles => {
|
const setupStyles = (newStyles = {}) => {
|
||||||
const componentId = newStyles.id
|
const componentId = newStyles.id
|
||||||
const selectable = newStyles.allowSelection
|
const customStyles = newStyles.custom || ""
|
||||||
const customStyles = newStyles.custom
|
const normalStyles = newStyles.normal || {}
|
||||||
const normalStyles = newStyles.normal
|
|
||||||
const hoverStyles = {
|
const hoverStyles = {
|
||||||
...normalStyles,
|
...normalStyles,
|
||||||
...newStyles.hover,
|
...(newStyles.hover || {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies a style string to a DOM node, enriching it for the builder
|
// Applies a style string to a DOM node
|
||||||
// preview
|
|
||||||
const applyStyles = styleString => {
|
const applyStyles = styleString => {
|
||||||
node.style = addBuilderPreviewStyles(styleString, componentId, selectable)
|
node.style = addBuilderPreviewStyles(node, styleString, componentId)
|
||||||
|
node.dataset.componentId = componentId
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies the "normal" style definition
|
// Applies the "normal" style definition
|
||||||
|
@ -89,8 +68,10 @@ export const styleable = (node, styles = {}) => {
|
||||||
// Handler to select a component in the builder when clicking it in the
|
// Handler to select a component in the builder when clicking it in the
|
||||||
// builder preview
|
// builder preview
|
||||||
selectComponent = event => {
|
selectComponent = event => {
|
||||||
builderStore.actions.selectComponent(newStyles.id)
|
builderStore.actions.selectComponent(componentId)
|
||||||
return blockEvent(event)
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add listeners to toggle hover styles
|
// Add listeners to toggle hover styles
|
||||||
|
@ -100,10 +81,6 @@ export const styleable = (node, styles = {}) => {
|
||||||
// Add builder preview click listener
|
// Add builder preview click listener
|
||||||
if (get(builderStore).inBuilder) {
|
if (get(builderStore).inBuilder) {
|
||||||
node.addEventListener("click", selectComponent, false)
|
node.addEventListener("click", selectComponent, false)
|
||||||
|
|
||||||
// Kill other interaction events
|
|
||||||
node.addEventListener("mousedown", blockEvent)
|
|
||||||
node.addEventListener("mouseup", blockEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply initial normal styles
|
// Apply initial normal styles
|
||||||
|
@ -118,8 +95,6 @@ export const styleable = (node, styles = {}) => {
|
||||||
// Remove builder preview click listener
|
// Remove builder preview click listener
|
||||||
if (get(builderStore).inBuilder) {
|
if (get(builderStore).inBuilder) {
|
||||||
node.removeEventListener("click", selectComponent)
|
node.removeEventListener("click", selectComponent)
|
||||||
node.removeEventListener("mousedown", blockEvent)
|
|
||||||
node.removeEventListener("mouseup", blockEvent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,20 @@ const { processString } = require("@budibase/string-templates")
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const { generateQueryID, getQueryParams } = require("../../db/utils")
|
const { generateQueryID, getQueryParams } = require("../../db/utils")
|
||||||
const { integrations } = require("../../integrations")
|
const { integrations } = require("../../integrations")
|
||||||
|
const { BaseQueryVerbs } = require("../../constants")
|
||||||
|
const env = require("../../environment")
|
||||||
|
|
||||||
|
// simple function to append "readable" to all read queries
|
||||||
|
function enrichQueries(input) {
|
||||||
|
const wasArray = Array.isArray(input)
|
||||||
|
const queries = wasArray ? input : [input]
|
||||||
|
for (let query of queries) {
|
||||||
|
if (query.queryVerb === BaseQueryVerbs.READ) {
|
||||||
|
query.readable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wasArray ? queries : queries[0]
|
||||||
|
}
|
||||||
|
|
||||||
function formatResponse(resp) {
|
function formatResponse(resp) {
|
||||||
if (typeof resp === "string") {
|
if (typeof resp === "string") {
|
||||||
|
@ -21,7 +35,7 @@ exports.fetch = async function(ctx) {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
ctx.body = body.rows.map(row => row.doc)
|
ctx.body = enrichQueries(body.rows.map(row => row.doc))
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.save = async function(ctx) {
|
exports.save = async function(ctx) {
|
||||||
|
@ -61,6 +75,18 @@ async function enrichQueryFields(fields, parameters) {
|
||||||
return enrichedQuery
|
return enrichedQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.find = async function(ctx) {
|
||||||
|
const db = new CouchDB(ctx.user.appId)
|
||||||
|
const query = enrichQueries(await db.get(ctx.params.queryId))
|
||||||
|
// remove properties that could be dangerous in real app
|
||||||
|
if (env.CLOUD) {
|
||||||
|
delete query.fields
|
||||||
|
delete query.parameters
|
||||||
|
delete query.schema
|
||||||
|
}
|
||||||
|
ctx.body = query
|
||||||
|
}
|
||||||
|
|
||||||
exports.preview = async function(ctx) {
|
exports.preview = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.appId)
|
const db = new CouchDB(ctx.user.appId)
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,6 @@ const {
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
const QueryVerb = {
|
|
||||||
Create: "create",
|
|
||||||
Read: "read",
|
|
||||||
Update: "update",
|
|
||||||
Delete: "delete",
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateQueryValidation() {
|
function generateQueryValidation() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.object({
|
||||||
|
@ -36,7 +29,7 @@ function generateQueryValidation() {
|
||||||
name: Joi.string(),
|
name: Joi.string(),
|
||||||
default: Joi.string()
|
default: Joi.string()
|
||||||
})),
|
})),
|
||||||
queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(),
|
queryVerb: Joi.string().allow().required(),
|
||||||
schema: Joi.object({}).required().unknown(true)
|
schema: Joi.object({}).required().unknown(true)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -45,7 +38,7 @@ function generateQueryPreviewValidation() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return joiValidator.body(Joi.object({
|
return joiValidator.body(Joi.object({
|
||||||
fields: Joi.object().required(),
|
fields: Joi.object().required(),
|
||||||
queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(),
|
queryVerb: Joi.string().allow().required(),
|
||||||
datasourceId: Joi.string().required(),
|
datasourceId: Joi.string().required(),
|
||||||
parameters: Joi.object({}).required().unknown(true)
|
parameters: Joi.object({}).required().unknown(true)
|
||||||
}))
|
}))
|
||||||
|
@ -67,6 +60,11 @@ router
|
||||||
generateQueryPreviewValidation(),
|
generateQueryPreviewValidation(),
|
||||||
queryController.preview
|
queryController.preview
|
||||||
)
|
)
|
||||||
|
.get(
|
||||||
|
"/api/queries/:queryId",
|
||||||
|
authorized(PermissionTypes.QUERY, PermissionLevels.READ),
|
||||||
|
queryController.find
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/queries/:queryId",
|
"/api/queries/:queryId",
|
||||||
paramResource("queryId"),
|
paramResource("queryId"),
|
||||||
|
|
|
@ -4,7 +4,7 @@ const {
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
builderEndpointShouldBlockNormalUsers,
|
builderEndpointShouldBlockNormalUsers,
|
||||||
getDocument,
|
getDocument,
|
||||||
insertDocument
|
insertDocument,
|
||||||
} = require("./couchTestUtils")
|
} = require("./couchTestUtils")
|
||||||
let { generateDatasourceID, generateQueryID } = require("../../../db/utils")
|
let { generateDatasourceID, generateQueryID } = require("../../../db/utils")
|
||||||
|
|
||||||
|
@ -21,11 +21,11 @@ const TEST_DATASOURCE = {
|
||||||
const TEST_QUERY = {
|
const TEST_QUERY = {
|
||||||
_id: generateQueryID(DATASOURCE_ID),
|
_id: generateQueryID(DATASOURCE_ID),
|
||||||
datasourceId: DATASOURCE_ID,
|
datasourceId: DATASOURCE_ID,
|
||||||
name:"New Query",
|
name: "New Query",
|
||||||
parameters:[],
|
parameters: [],
|
||||||
fields:{},
|
fields: {},
|
||||||
schema:{},
|
schema: {},
|
||||||
queryVerb:"read",
|
queryVerb: "read",
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("/queries", () => {
|
describe("/queries", () => {
|
||||||
|
@ -37,8 +37,8 @@ describe("/queries", () => {
|
||||||
let query
|
let query
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
({ request, server } = await supertest())
|
;({ request, server } = await supertest())
|
||||||
});
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
server.close()
|
server.close()
|
||||||
|
@ -47,7 +47,7 @@ describe("/queries", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await createApplication(request)
|
app = await createApplication(request)
|
||||||
appId = app.instance._id
|
appId = app.instance._id
|
||||||
});
|
})
|
||||||
|
|
||||||
async function createDatasource() {
|
async function createDatasource() {
|
||||||
return await insertDocument(appId, TEST_DATASOURCE)
|
return await insertDocument(appId, TEST_DATASOURCE)
|
||||||
|
@ -63,65 +63,68 @@ describe("/queries", () => {
|
||||||
.post(`/api/queries`)
|
.post(`/api/queries`)
|
||||||
.send(TEST_QUERY)
|
.send(TEST_QUERY)
|
||||||
.set(defaultHeaders(appId))
|
.set(defaultHeaders(appId))
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.res.statusMessage).toEqual(`Query ${TEST_QUERY.name} saved successfully.`);
|
expect(res.res.statusMessage).toEqual(
|
||||||
expect(res.body).toEqual({
|
`Query ${TEST_QUERY.name} saved successfully.`
|
||||||
_rev: res.body._rev,
|
)
|
||||||
...TEST_QUERY,
|
expect(res.body).toEqual({
|
||||||
});
|
_rev: res.body._rev,
|
||||||
|
...TEST_QUERY,
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
let datasource
|
let datasource
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
datasource = await createDatasource()
|
datasource = await createDatasource()
|
||||||
});
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
delete datasource._rev
|
delete datasource._rev
|
||||||
});
|
})
|
||||||
|
|
||||||
it("returns all the queries from the server", async () => {
|
it("returns all the queries from the server", async () => {
|
||||||
const query = await createQuery()
|
const query = await createQuery()
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/queries`)
|
.get(`/api/queries`)
|
||||||
.set(defaultHeaders(appId))
|
.set(defaultHeaders(appId))
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
const queries = res.body;
|
const queries = res.body
|
||||||
expect(queries).toEqual([
|
expect(queries).toEqual([
|
||||||
{
|
{
|
||||||
"_rev": query.rev,
|
_rev: query.rev,
|
||||||
...TEST_QUERY
|
...TEST_QUERY,
|
||||||
}
|
readable: true,
|
||||||
]);
|
},
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
await builderEndpointShouldBlockNormalUsers({
|
await builderEndpointShouldBlockNormalUsers({
|
||||||
request,
|
request,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/api/datasources`,
|
url: `/api/datasources`,
|
||||||
appId: appId,
|
appId: appId,
|
||||||
})
|
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("destroy", () => {
|
describe("destroy", () => {
|
||||||
let datasource;
|
let datasource
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
datasource = await createDatasource()
|
datasource = await createDatasource()
|
||||||
});
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
delete datasource._rev
|
delete datasource._rev
|
||||||
});
|
})
|
||||||
|
|
||||||
it("deletes a query and returns a success message", async () => {
|
it("deletes a query and returns a success message", async () => {
|
||||||
const query = await createQuery()
|
const query = await createQuery()
|
||||||
|
@ -134,10 +137,10 @@ describe("/queries", () => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/queries`)
|
.get(`/api/queries`)
|
||||||
.set(defaultHeaders(appId))
|
.set(defaultHeaders(appId))
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body).toEqual([])
|
expect(res.body).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
|
@ -148,5 +151,5 @@ describe("/queries", () => {
|
||||||
appId: appId,
|
appId: appId,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
|
@ -44,3 +44,9 @@ exports.USERS_TABLE_SCHEMA = USERS_TABLE_SCHEMA
|
||||||
exports.BUILDER_CONFIG_DB = "builder-config-db"
|
exports.BUILDER_CONFIG_DB = "builder-config-db"
|
||||||
exports.HOSTING_DOC = "hosting-doc"
|
exports.HOSTING_DOC = "hosting-doc"
|
||||||
exports.OBJ_STORE_DIRECTORY = "/app-assets/assets"
|
exports.OBJ_STORE_DIRECTORY = "/app-assets/assets"
|
||||||
|
exports.BaseQueryVerbs = {
|
||||||
|
CREATE: "create",
|
||||||
|
READ: "read",
|
||||||
|
UPDATE: "update",
|
||||||
|
DELETE: "delete",
|
||||||
|
}
|
||||||
|
|
|
@ -138,6 +138,13 @@ class LinkController {
|
||||||
// iterate through the link IDs in the row field, see if any don't exist already
|
// iterate through the link IDs in the row field, see if any don't exist already
|
||||||
for (let linkId of rowField) {
|
for (let linkId of rowField) {
|
||||||
if (linkId && linkId !== "" && linkDocIds.indexOf(linkId) === -1) {
|
if (linkId && linkId !== "" && linkDocIds.indexOf(linkId) === -1) {
|
||||||
|
// first check the doc we're linking to exists
|
||||||
|
try {
|
||||||
|
await this._db.get(linkId)
|
||||||
|
} catch (err) {
|
||||||
|
// skip links that don't exist
|
||||||
|
continue
|
||||||
|
}
|
||||||
operations.push(
|
operations.push(
|
||||||
new LinkDocument(
|
new LinkDocument(
|
||||||
table._id,
|
table._id,
|
||||||
|
|
|
@ -16,6 +16,12 @@ const TYPE_TRANSFORM_MAP = {
|
||||||
"": [],
|
"": [],
|
||||||
[null]: [],
|
[null]: [],
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
|
parse: link => {
|
||||||
|
if (typeof link === "string") {
|
||||||
|
return [link]
|
||||||
|
}
|
||||||
|
return link
|
||||||
|
},
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
"": "",
|
"": "",
|
||||||
|
@ -165,15 +171,15 @@ exports.walkDir = (dirPath, callback) => {
|
||||||
* @param {object} type The type fo coerce to
|
* @param {object} type The type fo coerce to
|
||||||
* @returns {object} The coerced value
|
* @returns {object} The coerced value
|
||||||
*/
|
*/
|
||||||
exports.coerceValue = (value, type) => {
|
exports.coerceValue = (row, type) => {
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(value)) {
|
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) {
|
||||||
return TYPE_TRANSFORM_MAP[type][value]
|
return TYPE_TRANSFORM_MAP[type][row]
|
||||||
} else if (TYPE_TRANSFORM_MAP[type].parse) {
|
} else if (TYPE_TRANSFORM_MAP[type].parse) {
|
||||||
return TYPE_TRANSFORM_MAP[type].parse(value)
|
return TYPE_TRANSFORM_MAP[type].parse(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -106,6 +106,7 @@
|
||||||
"styleable": true,
|
"styleable": true,
|
||||||
"hasChildren": true,
|
"hasChildren": true,
|
||||||
"dataProvider": true,
|
"dataProvider": true,
|
||||||
|
"actions": ["RefreshDatasource"],
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "datasource",
|
"type": "datasource",
|
||||||
|
@ -114,8 +115,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "No Rows Message",
|
"label": "Empty Text",
|
||||||
"key": "noRowsMessage"
|
"key": "noRowsMessage",
|
||||||
|
"defaultValue": "No rows found."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -140,66 +142,15 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"label": "Rows Per Page",
|
"label": "Rows/Page",
|
||||||
"defaultValue": 25,
|
"defaultValue": 25,
|
||||||
"key": "pageSize"
|
"key": "pageSize"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "No Rows Message",
|
"label": "Empty Text",
|
||||||
"key": "noRowsMessage",
|
"key": "noRowsMessage",
|
||||||
"defaultValue": "No Rows"
|
"defaultValue": "No rows found."
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dataform": {
|
|
||||||
"name": "Form",
|
|
||||||
"icon": "ri-file-edit-line",
|
|
||||||
"styleable": true
|
|
||||||
},
|
|
||||||
"dataformwide": {
|
|
||||||
"name": "Wide Form",
|
|
||||||
"icon": "ri-file-edit-line",
|
|
||||||
"styleable": true
|
|
||||||
},
|
|
||||||
"input": {
|
|
||||||
"name": "Text Field",
|
|
||||||
"description": "A textfield component that allows the user to input text.",
|
|
||||||
"icon": "ri-edit-box-line",
|
|
||||||
"styleable": true,
|
|
||||||
"bindable": true,
|
|
||||||
"settings": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Label",
|
|
||||||
"key": "label"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Type",
|
|
||||||
"key": "type",
|
|
||||||
"defaultValue": "text",
|
|
||||||
"options": ["text", "password"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"richtext": {
|
|
||||||
"name": "Rich Text",
|
|
||||||
"description": "A component that allows the user to enter long form text.",
|
|
||||||
"icon": "ri-edit-box-line",
|
|
||||||
"styleable": true,
|
|
||||||
"bindable": true
|
|
||||||
},
|
|
||||||
"datepicker": {
|
|
||||||
"name": "Date Picker",
|
|
||||||
"description": "A basic date picker component",
|
|
||||||
"icon": "ri-calendar-line",
|
|
||||||
"styleable": true,
|
|
||||||
"bindable": true,
|
|
||||||
"settings": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Placeholder",
|
|
||||||
"key": "placeholder"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1127,5 +1078,262 @@
|
||||||
"defaultValue": true
|
"defaultValue": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"name": "Form",
|
||||||
|
"icon": "ri-file-text-line",
|
||||||
|
"styleable": true,
|
||||||
|
"hasChildren": true,
|
||||||
|
"dataProvider": true,
|
||||||
|
"actions": ["ValidateForm"],
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "schema",
|
||||||
|
"label": "Schema",
|
||||||
|
"key": "datasource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Theme",
|
||||||
|
"key": "theme",
|
||||||
|
"defaultValue": "spectrum--light",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Lightest",
|
||||||
|
"value": "spectrum--lightest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Light",
|
||||||
|
"value": "spectrum--light"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dark",
|
||||||
|
"value": "spectrum--dark"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Darkest",
|
||||||
|
"value": "spectrum--darkest"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Size",
|
||||||
|
"key": "size",
|
||||||
|
"defaultValue": "spectrum--medium",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Medium",
|
||||||
|
"value": "spectrum--medium"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Large",
|
||||||
|
"value": "spectrum--large"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fieldgroup": {
|
||||||
|
"name": "Field Group",
|
||||||
|
"icon": "ri-layout-row-line",
|
||||||
|
"styleable": true,
|
||||||
|
"hasChildren": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Labels",
|
||||||
|
"key": "labelPosition",
|
||||||
|
"defaultValue": "above",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Left",
|
||||||
|
"value": "left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Right",
|
||||||
|
"value": "right"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Above",
|
||||||
|
"value": "above"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"stringfield": {
|
||||||
|
"name": "Text Field",
|
||||||
|
"icon": "ri-t-box-line",
|
||||||
|
"styleable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/string",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Placeholder",
|
||||||
|
"key": "placeholder"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"numberfield": {
|
||||||
|
"name": "Number Field",
|
||||||
|
"icon": "ri-edit-box-line",
|
||||||
|
"styleable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/number",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Placeholder",
|
||||||
|
"key": "placeholder"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"optionsfield": {
|
||||||
|
"name": "Options Picker",
|
||||||
|
"icon": "ri-file-list-line",
|
||||||
|
"styleable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/options",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Placeholder",
|
||||||
|
"key": "placeholder",
|
||||||
|
"placeholder": "Choose an option"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"booleanfield": {
|
||||||
|
"name": "Checkbox",
|
||||||
|
"icon": "ri-checkbox-line",
|
||||||
|
"styleable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/boolean",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Text",
|
||||||
|
"key": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"longformfield": {
|
||||||
|
"name": "Rich Text",
|
||||||
|
"icon": "ri-file-edit-line",
|
||||||
|
"styleable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/longform",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Placeholder",
|
||||||
|
"key": "placeholder",
|
||||||
|
"placeholder": "Type something..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"datetimefield": {
|
||||||
|
"name": "Date Picker",
|
||||||
|
"icon": "ri-calendar-line",
|
||||||
|
"styleable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/datetime",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Placeholder",
|
||||||
|
"key": "placeholder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Show Time",
|
||||||
|
"key": "enableTime",
|
||||||
|
"defaultValue": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"attachmentfield": {
|
||||||
|
"name": "Attachment",
|
||||||
|
"icon": "ri-image-edit-line",
|
||||||
|
"styleable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/attachment",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"relationshipfield": {
|
||||||
|
"name": "Relationship Picker",
|
||||||
|
"icon": "ri-links-line",
|
||||||
|
"styleable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/link",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"rollup-plugin-node-builtins": "^2.1.2",
|
"rollup-plugin-node-builtins": "^2.1.2",
|
||||||
"rollup-plugin-postcss": "^3.1.5",
|
"rollup-plugin-postcss": "^3.1.5",
|
||||||
"rollup-plugin-svelte": "^6.1.1",
|
"rollup-plugin-svelte": "^6.1.1",
|
||||||
|
"rollup-plugin-svg": "^2.0.0",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"sirv-cli": "^0.4.4",
|
"sirv-cli": "^0.4.4",
|
||||||
"svelte": "^3.30.0"
|
"svelte": "^3.30.0"
|
||||||
|
@ -38,10 +39,25 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.55.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.1.0",
|
||||||
|
"@budibase/bbui": "^1.58.5",
|
||||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||||
|
"@spectrum-css/actionbutton": "^1.0.0-beta.1",
|
||||||
|
"@spectrum-css/button": "^3.0.0-beta.6",
|
||||||
|
"@spectrum-css/checkbox": "^3.0.0-beta.6",
|
||||||
|
"@spectrum-css/fieldlabel": "^3.0.0-beta.7",
|
||||||
|
"@spectrum-css/icon": "^3.0.0-beta.2",
|
||||||
|
"@spectrum-css/inputgroup": "^3.0.0-beta.7",
|
||||||
|
"@spectrum-css/menu": "^3.0.0-beta.5",
|
||||||
|
"@spectrum-css/page": "^3.0.0-beta.0",
|
||||||
|
"@spectrum-css/picker": "^1.0.0-beta.3",
|
||||||
|
"@spectrum-css/popover": "^3.0.0-beta.6",
|
||||||
|
"@spectrum-css/stepper": "^3.0.0-beta.7",
|
||||||
|
"@spectrum-css/textfield": "^3.0.0-beta.6",
|
||||||
|
"@spectrum-css/vars": "^3.0.0-beta.2",
|
||||||
"apexcharts": "^3.22.1",
|
"apexcharts": "^3.22.1",
|
||||||
"flatpickr": "^4.6.6",
|
"flatpickr": "^4.6.6",
|
||||||
|
"loadicons": "^1.0.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"markdown-it": "^12.0.2",
|
"markdown-it": "^12.0.2",
|
||||||
"quill": "^1.3.7",
|
"quill": "^1.3.7",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import svelte from "rollup-plugin-svelte"
|
||||||
import postcss from "rollup-plugin-postcss"
|
import postcss from "rollup-plugin-postcss"
|
||||||
import json from "@rollup/plugin-json"
|
import json from "@rollup/plugin-json"
|
||||||
import { terser } from "rollup-plugin-terser"
|
import { terser } from "rollup-plugin-terser"
|
||||||
|
import svg from "rollup-plugin-svg"
|
||||||
|
|
||||||
import builtins from "rollup-plugin-node-builtins"
|
import builtins from "rollup-plugin-node-builtins"
|
||||||
|
|
||||||
|
@ -33,5 +34,6 @@ export default {
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
json(),
|
json(),
|
||||||
|
svg(),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import Form from "./Form.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Form wide={false} />
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import Form from "./Form.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Form wide />
|
|
|
@ -1,21 +0,0 @@
|
||||||
<script>
|
|
||||||
import { DatePicker } from "@budibase/bbui"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { styleable, setBindableValue } = getContext("sdk")
|
|
||||||
const component = getContext("component")
|
|
||||||
|
|
||||||
export let placeholder
|
|
||||||
|
|
||||||
let value
|
|
||||||
$: setBindableValue(value, $component.id)
|
|
||||||
|
|
||||||
function handleChange(event) {
|
|
||||||
const [fullDate] = event.detail
|
|
||||||
value = fullDate
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
|
||||||
<DatePicker {placeholder} on:change={handleChange} {value} />
|
|
||||||
</div>
|
|
|
@ -1,103 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import {
|
|
||||||
Label,
|
|
||||||
DatePicker,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Toggle,
|
|
||||||
RichText,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import Dropzone from "./attachments/Dropzone.svelte"
|
|
||||||
import LinkedRowSelector from "./LinkedRowSelector.svelte"
|
|
||||||
import { capitalise } from "./helpers"
|
|
||||||
|
|
||||||
const { styleable, API } = getContext("sdk")
|
|
||||||
const component = getContext("component")
|
|
||||||
const dataContext = getContext("data")
|
|
||||||
|
|
||||||
export let wide = false
|
|
||||||
|
|
||||||
let row
|
|
||||||
let schema
|
|
||||||
let fields = []
|
|
||||||
|
|
||||||
// Fetch info about the closest data context
|
|
||||||
$: getFormData($dataContext[$dataContext.closestComponentId])
|
|
||||||
|
|
||||||
const getFormData = async context => {
|
|
||||||
if (context) {
|
|
||||||
const tableDefinition = await API.fetchTableDefinition(context.tableId)
|
|
||||||
schema = tableDefinition?.schema
|
|
||||||
fields = Object.keys(schema ?? {})
|
|
||||||
|
|
||||||
// Use the draft version for editing
|
|
||||||
row = $dataContext[`${$dataContext.closestComponentId}_draft`]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="form-content" use:styleable={$component.styles}>
|
|
||||||
<!-- <ErrorsBox errors={$store.saveRowErrors || {}} />-->
|
|
||||||
{#each fields as field}
|
|
||||||
<div class="form-field" class:wide>
|
|
||||||
{#if !(schema[field].type === 'boolean' && !wide)}
|
|
||||||
<Label extraSmall={!wide} grey>{capitalise(schema[field].name)}</Label>
|
|
||||||
{/if}
|
|
||||||
{#if schema[field].type === 'options'}
|
|
||||||
<Select secondary bind:value={row[field]}>
|
|
||||||
<option value="">Choose an option</option>
|
|
||||||
{#each schema[field].constraints.inclusion as opt}
|
|
||||||
<option>{opt}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
{:else if schema[field].type === 'datetime'}
|
|
||||||
<DatePicker bind:value={row[field]} />
|
|
||||||
{:else if schema[field].type === 'boolean'}
|
|
||||||
<Toggle
|
|
||||||
text={wide ? null : capitalise(schema[field].name)}
|
|
||||||
bind:checked={row[field]} />
|
|
||||||
{:else if schema[field].type === 'number'}
|
|
||||||
<Input type="number" bind:value={row[field]} />
|
|
||||||
{:else if schema[field].type === 'string'}
|
|
||||||
<Input bind:value={row[field]} />
|
|
||||||
{:else if schema[field].type === 'longform'}
|
|
||||||
<RichText bind:value={row[field]} />
|
|
||||||
{:else if schema[field].type === 'attachment'}
|
|
||||||
<Dropzone bind:files={row[field]} />
|
|
||||||
{:else if schema[field].type === 'link'}
|
|
||||||
<LinkedRowSelector
|
|
||||||
secondary
|
|
||||||
showLabel={false}
|
|
||||||
bind:linkedRows={row[field]}
|
|
||||||
schema={schema[field]} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-content {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
.form-field.wide {
|
|
||||||
align-items: center;
|
|
||||||
grid-template-columns: 20% 1fr;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.form-field.wide :global(label) {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { styleable, setBindableValue } = getContext("sdk")
|
|
||||||
const component = getContext("component")
|
|
||||||
|
|
||||||
let value
|
|
||||||
|
|
||||||
function onBlur() {
|
|
||||||
setBindableValue(value, $component.id)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<input bind:value on:blur={onBlur} use:styleable={$component.styles} />
|
|
|
@ -2,16 +2,24 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API, styleable, DataProvider, builderStore } = getContext("sdk")
|
export let datasource
|
||||||
|
export let noRowsMessage
|
||||||
|
|
||||||
|
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
|
||||||
|
"sdk"
|
||||||
|
)
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
export let datasource = []
|
|
||||||
export let noRowsMessage = "Feed me some data"
|
|
||||||
|
|
||||||
let rows = []
|
let rows = []
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
$: fetchData(datasource)
|
$: fetchData(datasource)
|
||||||
|
$: actions = [
|
||||||
|
{
|
||||||
|
type: ActionTypes.RefreshDatasource,
|
||||||
|
callback: () => fetchData(datasource),
|
||||||
|
metadata: { datasource },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
async function fetchData(datasource) {
|
async function fetchData(datasource) {
|
||||||
if (!isEmpty(datasource)) {
|
if (!isEmpty(datasource)) {
|
||||||
|
@ -21,28 +29,38 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
<Provider {actions}>
|
||||||
{#if rows.length > 0}
|
<div use:styleable={$component.styles}>
|
||||||
{#if $component.children === 0 && $builderStore.inBuilder}
|
{#if rows.length > 0}
|
||||||
<p>Add some components too</p>
|
{#if $component.children === 0 && $builderStore.inBuilder}
|
||||||
{:else}
|
<p><i class="ri-image-line" />Add some components to display.</p>
|
||||||
{#each rows as row}
|
{:else}
|
||||||
<DataProvider {row}>
|
{#each rows as row}
|
||||||
<slot />
|
<Provider data={row}>
|
||||||
</DataProvider>
|
<slot />
|
||||||
{/each}
|
</Provider>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{:else if loaded && noRowsMessage}
|
||||||
|
<p><i class="ri-list-check-2" />{noRowsMessage}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if loaded && $builderStore.inBuilder}
|
</div>
|
||||||
<p>{noRowsMessage}</p>
|
</Provider>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
|
margin: 0 var(--spacing-m);
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
color: var(--grey-6);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: #f5f5f5;
|
}
|
||||||
border: #ccc 1px solid;
|
p i {
|
||||||
padding: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--grey-5);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const ENTER_KEY = 13
|
const { authStore, styleable, builderStore } = getContext("sdk")
|
||||||
|
|
||||||
const { authStore, styleable } = getContext("sdk")
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
export let buttonText = "Log In"
|
export let buttonText = "Log In"
|
||||||
|
@ -25,13 +23,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
|
if ($builderStore.inBuilder) {
|
||||||
|
return
|
||||||
|
}
|
||||||
loading = true
|
loading = true
|
||||||
await authStore.actions.logIn({ email, password })
|
await authStore.actions.logIn({ email, password })
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(evt) {
|
function handleKeydown(evt) {
|
||||||
if (evt.keyCode === ENTER_KEY) {
|
if (evt.key === "Enter") {
|
||||||
login()
|
login()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const { authStore, linkable, styleable } = getContext("sdk")
|
const { authStore, linkable, styleable, builderStore } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
export let logoUrl
|
export let logoUrl
|
||||||
|
|
||||||
const logOut = async () => {
|
const logOut = async () => {
|
||||||
|
if ($builderStore.inBuilder) {
|
||||||
|
return
|
||||||
|
}
|
||||||
await authStore.actions.logOut()
|
await authStore.actions.logOut()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { DataProvider, styleable } = getContext("sdk")
|
|
||||||
const component = getContext("component")
|
|
||||||
|
|
||||||
export let table
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
|
||||||
<DataProvider row={{ tableId: table }}>
|
|
||||||
<slot />
|
|
||||||
</DataProvider>
|
|
||||||
</div>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { RichText } from "@budibase/bbui"
|
|
||||||
|
|
||||||
const { styleable } = getContext("sdk")
|
|
||||||
const component = getContext("component")
|
|
||||||
|
|
||||||
export let value = ""
|
|
||||||
|
|
||||||
// Need to determine what options we want to expose.
|
|
||||||
let options = {
|
|
||||||
modules: {
|
|
||||||
toolbar: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
header: [1, 2, 3, false],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
["bold", "italic", "underline", "strike"],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
placeholder: "Type something...",
|
|
||||||
theme: "snow",
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
|
||||||
<RichText bind:value {options} />
|
|
||||||
</div>
|
|
|
@ -1,46 +1,57 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
|
|
||||||
const { API, screenStore, routeStore, DataProvider, styleable } = getContext(
|
|
||||||
"sdk"
|
|
||||||
)
|
|
||||||
const component = getContext("component")
|
|
||||||
|
|
||||||
export let table
|
export let table
|
||||||
|
|
||||||
|
const {
|
||||||
|
API,
|
||||||
|
screenStore,
|
||||||
|
routeStore,
|
||||||
|
Provider,
|
||||||
|
styleable,
|
||||||
|
ActionTypes,
|
||||||
|
} = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
let headers = []
|
let headers = []
|
||||||
let row
|
let row
|
||||||
|
|
||||||
async function fetchFirstRow() {
|
const fetchFirstRow = async tableId => {
|
||||||
const rows = await API.fetchTableData(table)
|
const rows = await API.fetchTableData(tableId)
|
||||||
return Array.isArray(rows) && rows.length ? rows[0] : { tableId: table }
|
return Array.isArray(rows) && rows.length ? rows[0] : { tableId }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData() {
|
const fetchData = async (rowId, tableId) => {
|
||||||
if (!table) {
|
if (!tableId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathParts = window.location.pathname.split("/")
|
const pathParts = window.location.pathname.split("/")
|
||||||
const routeParamId = $routeStore.routeParams.id
|
|
||||||
|
|
||||||
// if srcdoc, then we assume this is the builder preview
|
// if srcdoc, then we assume this is the builder preview
|
||||||
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && table) {
|
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && tableId) {
|
||||||
row = await fetchFirstRow()
|
row = await fetchFirstRow(tableId)
|
||||||
} else if (routeParamId) {
|
} else if (rowId) {
|
||||||
row = await API.fetchRow({ tableId: table, rowId: routeParamId })
|
row = await API.fetchRow({ tableId, rowId })
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Row ID was not supplied to RowDetail")
|
throw new Error("Row ID was not supplied to RowDetail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(fetchData)
|
$: actions = [
|
||||||
|
{
|
||||||
|
type: ActionTypes.RefreshDatasource,
|
||||||
|
callback: () => fetchData($routeStore.routeParams.id, table),
|
||||||
|
metadata: { datasource: { type: "table", tableId: table } },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
onMount(() => fetchData($routeStore.routeParams.id, table))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if row}
|
{#if row}
|
||||||
<div use:styleable={$component.styles}>
|
<Provider data={row} {actions}>
|
||||||
<DataProvider {row}>
|
<div use:styleable={$component.styles}>
|
||||||
<slot />
|
<slot />
|
||||||
</DataProvider>
|
</div>
|
||||||
</div>
|
</Provider>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { isEmpty } from "lodash/fp"
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
|
@ -10,10 +9,12 @@
|
||||||
Input,
|
Input,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
const { API, styleable, DataProvider, builderStore } = getContext("sdk")
|
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
|
||||||
|
"sdk"
|
||||||
|
)
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
export let table = []
|
export let table
|
||||||
export let columns = []
|
export let columns = []
|
||||||
export let pageSize
|
export let pageSize
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
@ -34,12 +35,19 @@
|
||||||
search[next] === "" ? acc : { ...acc, [next]: search[next] },
|
search[next] === "" ? acc : { ...acc, [next]: search[next] },
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
$: actions = [
|
||||||
|
{
|
||||||
|
type: ActionTypes.RefreshDatasource,
|
||||||
|
callback: () => fetchData(table, page),
|
||||||
|
metadata: { datasource: { type: "table", tableId: table } },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
async function fetchData(table, page) {
|
async function fetchData(table, page) {
|
||||||
if (!isEmpty(table)) {
|
if (table) {
|
||||||
const tableDef = await API.fetchTableDefinition(table)
|
const tableDef = await API.fetchTableDefinition(table)
|
||||||
schema = tableDef.schema
|
schema = tableDef.schema
|
||||||
rows = await API.searchTable({
|
rows = await API.searchTableData({
|
||||||
tableId: table,
|
tableId: table,
|
||||||
search: parsedSearch,
|
search: parsedSearch,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
@ -60,84 +68,92 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles}>
|
<Provider {actions}>
|
||||||
<div class="query-builder">
|
<div use:styleable={$component.styles}>
|
||||||
{#if schema}
|
<div class="query-builder">
|
||||||
{#each columns as field}
|
{#if schema}
|
||||||
<div class="form-field">
|
{#each columns as field}
|
||||||
<Label extraSmall grey>{schema[field].name}</Label>
|
<div class="form-field">
|
||||||
{#if schema[field].type === 'options'}
|
<Label extraSmall grey>{schema[field].name}</Label>
|
||||||
<Select secondary bind:value={search[field]}>
|
{#if schema[field].type === 'options'}
|
||||||
<option value="">Choose an option</option>
|
<Select secondary bind:value={search[field]}>
|
||||||
{#each schema[field].constraints.inclusion as opt}
|
<option value="">Choose an option</option>
|
||||||
<option>{opt}</option>
|
{#each schema[field].constraints.inclusion as opt}
|
||||||
{/each}
|
<option>{opt}</option>
|
||||||
</Select>
|
{/each}
|
||||||
{:else if schema[field].type === 'datetime'}
|
</Select>
|
||||||
<DatePicker bind:value={search[field]} />
|
{:else if schema[field].type === 'datetime'}
|
||||||
{:else if schema[field].type === 'boolean'}
|
<DatePicker bind:value={search[field]} />
|
||||||
<Toggle text={schema[field].name} bind:checked={search[field]} />
|
{:else if schema[field].type === 'boolean'}
|
||||||
{:else if schema[field].type === 'number'}
|
<Toggle text={schema[field].name} bind:checked={search[field]} />
|
||||||
<Input type="number" bind:value={search[field]} />
|
{:else if schema[field].type === 'number'}
|
||||||
{:else if schema[field].type === 'string'}
|
<Input type="number" bind:value={search[field]} />
|
||||||
<Input bind:value={search[field]} />
|
{:else if schema[field].type === 'string'}
|
||||||
{/if}
|
<Input bind:value={search[field]} />
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
</div>
|
||||||
{/if}
|
|
||||||
<div class="actions">
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
on:click={() => {
|
|
||||||
search = {}
|
|
||||||
page = 0
|
|
||||||
}}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
on:click={() => {
|
|
||||||
page = 0
|
|
||||||
fetchData(table, page)
|
|
||||||
}}>
|
|
||||||
Search
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if loaded}
|
|
||||||
{#if rows.length > 0}
|
|
||||||
{#if $component.children === 0 && $builderStore.inBuilder}
|
|
||||||
<p>Add some components too</p>
|
|
||||||
{:else}
|
|
||||||
{#each rows as row}
|
|
||||||
<DataProvider {row}>
|
|
||||||
<slot />
|
|
||||||
</DataProvider>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if $builderStore.inBuilder}
|
<div class="actions">
|
||||||
<p>Feed me some data</p>
|
<Button
|
||||||
{:else}
|
secondary
|
||||||
<p>{noRowsMessage}</p>
|
on:click={() => {
|
||||||
{/if}
|
search = {}
|
||||||
{/if}
|
page = 0
|
||||||
<div class="pagination">
|
}}>
|
||||||
{#if page > 0}
|
Reset
|
||||||
<Button primary on:click={previousPage}>Back</Button>
|
</Button>
|
||||||
{/if}
|
<Button
|
||||||
{#if rows.length === pageSize}
|
primary
|
||||||
<Button primary on:click={nextPage}>Next</Button>
|
on:click={() => {
|
||||||
|
page = 0
|
||||||
|
fetchData(table, page)
|
||||||
|
}}>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if loaded}
|
||||||
|
{#if rows.length > 0}
|
||||||
|
{#if $component.children === 0 && $builderStore.inBuilder}
|
||||||
|
<p><i class="ri-image-line" />Add some components to display.</p>
|
||||||
|
{:else}
|
||||||
|
{#each rows as row}
|
||||||
|
<Provider data={row}>
|
||||||
|
<slot />
|
||||||
|
</Provider>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{:else if noRowsMessage}
|
||||||
|
<p><i class="ri-search-2-line" />{noRowsMessage}</p>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="pagination">
|
||||||
|
{#if page > 0}
|
||||||
|
<Button primary on:click={previousPage}>Back</Button>
|
||||||
|
{/if}
|
||||||
|
{#if rows.length === pageSize}
|
||||||
|
<Button primary on:click={nextPage}>Next</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Provider>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
|
margin: 0 var(--spacing-m);
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
color: var(--grey-6);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: #f5f5f5;
|
}
|
||||||
border: #ccc 1px solid;
|
p i {
|
||||||
padding: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--grey-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder {
|
.query-builder {
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
const dataContext = getContext("data")
|
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
const dataContext = getContext("data")
|
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
const dataContext = getContext("data")
|
|
||||||
|
|
||||||
// Common props
|
// Common props
|
||||||
export let title
|
export let title
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
const dataContext = getContext("data")
|
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import Dropzone from "../attachments/Dropzone.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let field
|
||||||
|
export let label
|
||||||
|
|
||||||
|
let fieldState
|
||||||
|
let fieldApi
|
||||||
|
|
||||||
|
// Update form value from bound value after we've mounted
|
||||||
|
let value
|
||||||
|
let mounted = false
|
||||||
|
$: mounted && fieldApi?.setValue(value)
|
||||||
|
|
||||||
|
// Get the fields initial value after initialising
|
||||||
|
onMount(() => {
|
||||||
|
value = $fieldState?.value
|
||||||
|
mounted = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
{label}
|
||||||
|
{field}
|
||||||
|
type="attachment"
|
||||||
|
bind:fieldState
|
||||||
|
bind:fieldApi
|
||||||
|
defaultValue={[]}>
|
||||||
|
{#if mounted}
|
||||||
|
<Dropzone bind:files={value} />
|
||||||
|
{/if}
|
||||||
|
</Field>
|
|
@ -0,0 +1,57 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/checkbox/dist/index-vars.css"
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
|
||||||
|
export let field
|
||||||
|
export let label
|
||||||
|
export let text
|
||||||
|
|
||||||
|
let fieldState
|
||||||
|
let fieldApi
|
||||||
|
|
||||||
|
const onChange = event => {
|
||||||
|
fieldApi.setValue(event.target.checked)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
{label}
|
||||||
|
{field}
|
||||||
|
type="boolean"
|
||||||
|
bind:fieldState
|
||||||
|
bind:fieldApi
|
||||||
|
defaultValue={false}>
|
||||||
|
{#if fieldState}
|
||||||
|
<div class="spectrum-FieldGroup spectrum-FieldGroup--horizontal">
|
||||||
|
<label class="spectrum-Checkbox" class:is-invalid={!$fieldState.valid}>
|
||||||
|
<input
|
||||||
|
checked={$fieldState.value}
|
||||||
|
on:change={onChange}
|
||||||
|
type="checkbox"
|
||||||
|
class="spectrum-Checkbox-input"
|
||||||
|
id={$fieldState.fieldId} />
|
||||||
|
<span class="spectrum-Checkbox-box">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark75 spectrum-Checkbox-checkmark"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true">
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark75" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Dash75 spectrum-Checkbox-partialCheckmark"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true">
|
||||||
|
<use xlink:href="#spectrum-css-icon-Dash75" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="spectrum-Checkbox-label">{text || 'Checkbox'}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Checkbox {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,141 @@
|
||||||
|
<script>
|
||||||
|
import Flatpickr from "svelte-flatpickr"
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import "flatpickr/dist/flatpickr.css"
|
||||||
|
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
||||||
|
import { generateID } from "../helpers"
|
||||||
|
|
||||||
|
export let field
|
||||||
|
export let label
|
||||||
|
export let placeholder
|
||||||
|
export let enableTime
|
||||||
|
|
||||||
|
let fieldState
|
||||||
|
let fieldApi
|
||||||
|
let open = false
|
||||||
|
let flatpickr
|
||||||
|
|
||||||
|
$: flatpickrId = `${$fieldState?.id}-${generateID()}-wrapper`
|
||||||
|
$: flatpickrOptions = {
|
||||||
|
element: `#${flatpickrId}`,
|
||||||
|
enableTime: enableTime || false,
|
||||||
|
altInput: true,
|
||||||
|
altFormat: enableTime ? "F j Y, H:i" : "F j, Y",
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = event => {
|
||||||
|
const [dates] = event.detail
|
||||||
|
fieldApi.setValue(dates[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearDateOnBackspace = event => {
|
||||||
|
if (["Backspace", "Clear", "Delete"].includes(event.key)) {
|
||||||
|
fieldApi.setValue(null)
|
||||||
|
flatpickr.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOpen = () => {
|
||||||
|
open = true
|
||||||
|
document.addEventListener("keyup", clearDateOnBackspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
open = false
|
||||||
|
document.removeEventListener("keyup", clearDateOnBackspace)
|
||||||
|
|
||||||
|
// Manually blur all input fields since flatpickr creates a second
|
||||||
|
// duplicate input field.
|
||||||
|
// We need to blur both because the focus styling does not get properly
|
||||||
|
// applied.
|
||||||
|
const els = document.querySelectorAll(`#${flatpickrId} input`)
|
||||||
|
els.forEach(el => el.blur())
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field {label} {field} type="datetime" bind:fieldState bind:fieldApi>
|
||||||
|
{#if fieldState}
|
||||||
|
<Flatpickr
|
||||||
|
bind:flatpickr
|
||||||
|
value={$fieldState.value}
|
||||||
|
on:open={onOpen}
|
||||||
|
on:close={onClose}
|
||||||
|
options={flatpickrOptions}
|
||||||
|
on:change={handleChange}
|
||||||
|
element={`#${flatpickrId}`}>
|
||||||
|
<div
|
||||||
|
id={flatpickrId}
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-invalid={!$fieldState.valid}
|
||||||
|
class:is-invalid={!$fieldState.valid}
|
||||||
|
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
|
||||||
|
class:is-focused={open}
|
||||||
|
aria-readonly="false"
|
||||||
|
aria-required="false"
|
||||||
|
aria-haspopup="true">
|
||||||
|
<div
|
||||||
|
on:click={flatpickr?.open}
|
||||||
|
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
||||||
|
class:is-invalid={!$fieldState.valid}>
|
||||||
|
{#if !$fieldState.valid}
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true">
|
||||||
|
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<input
|
||||||
|
data-input
|
||||||
|
type="text"
|
||||||
|
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||||
|
aria-invalid={!$fieldState.valid}
|
||||||
|
{placeholder}
|
||||||
|
id={$fieldState.fieldId}
|
||||||
|
value={$fieldState.value} />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="spectrum-Picker spectrum-InputGroup-button"
|
||||||
|
tabindex="-1"
|
||||||
|
class:is-invalid={!$fieldState.valid}
|
||||||
|
on:click={flatpickr?.open}>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeM"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-label="Calendar">
|
||||||
|
<use xlink:href="#spectrum-icon-18-Calendar" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Flatpickr>
|
||||||
|
{#if open}
|
||||||
|
<div class="overlay" on:mousedown|self={flatpickr?.close} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Textfield-input {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.spectrum-Textfield:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.flatpickr {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.flatpickr .spectrum-Textfield {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,86 @@
|
||||||
|
<script>
|
||||||
|
import Placeholder from "./Placeholder.svelte"
|
||||||
|
import FieldGroupFallback from "./FieldGroupFallback.svelte"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
export let label
|
||||||
|
export let field
|
||||||
|
export let fieldState
|
||||||
|
export let fieldApi
|
||||||
|
export let fieldSchema
|
||||||
|
export let defaultValue
|
||||||
|
export let type
|
||||||
|
|
||||||
|
// Get contexts
|
||||||
|
const formContext = getContext("form")
|
||||||
|
const fieldGroupContext = getContext("fieldGroup")
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
|
// Register field with form
|
||||||
|
const formApi = formContext?.formApi
|
||||||
|
const labelPosition = fieldGroupContext?.labelPosition || "above"
|
||||||
|
const formField = formApi?.registerField(field, defaultValue)
|
||||||
|
|
||||||
|
// Expose field properties to parent component
|
||||||
|
fieldState = formField?.fieldState
|
||||||
|
fieldApi = formField?.fieldApi
|
||||||
|
fieldSchema = formField?.fieldSchema
|
||||||
|
|
||||||
|
// Extract label position from field group context
|
||||||
|
$: labelPositionClass =
|
||||||
|
labelPosition === "above" ? "" : `spectrum-FieldLabel--${labelPosition}`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FieldGroupFallback>
|
||||||
|
<div class="spectrum-Form-item" use:styleable={$component.styles}>
|
||||||
|
<label
|
||||||
|
for={$fieldState?.fieldId}
|
||||||
|
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}>
|
||||||
|
{label || ''}
|
||||||
|
</label>
|
||||||
|
<div class="spectrum-Form-itemField">
|
||||||
|
{#if !formContext}
|
||||||
|
<Placeholder>Form components need to be wrapped in a Form</Placeholder>
|
||||||
|
{:else if !fieldState}
|
||||||
|
<Placeholder>
|
||||||
|
Add the Field setting to start using your component
|
||||||
|
</Placeholder>
|
||||||
|
{:else if fieldSchema?.type && fieldSchema?.type !== type}
|
||||||
|
<Placeholder>
|
||||||
|
This Field setting is the wrong data type for this component
|
||||||
|
</Placeholder>
|
||||||
|
{:else}
|
||||||
|
<slot />
|
||||||
|
{#if $fieldState.error}
|
||||||
|
<div class="error">{$fieldState.error}</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FieldGroupFallback>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-Form-itemField {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(
|
||||||
|
--spectrum-semantic-negative-color-default,
|
||||||
|
var(--spectrum-global-color-red-500)
|
||||||
|
);
|
||||||
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
|
margin-top: var(--spectrum-global-dimension-size-75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-FieldLabel--right,
|
||||||
|
.spectrum-FieldLabel--left {
|
||||||
|
padding-right: var(--spectrum-global-dimension-size-200);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
import { getContext, setContext } from "svelte"
|
||||||
|
|
||||||
|
export let labelPosition = "above"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
setContext("fieldGroup", { labelPosition })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper" use:styleable={$component.styles}>
|
||||||
|
<div
|
||||||
|
class="spectrum-Form"
|
||||||
|
class:spectrum-Form--labelsAbove={labelPosition === 'above'}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.spectrum-Form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const fieldGroupContext = getContext("fieldGroup")
|
||||||
|
const labelPosition = fieldGroupContext?.labelPosition || "above"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if fieldGroupContext}
|
||||||
|
<slot />
|
||||||
|
{:else}
|
||||||
|
<div class="spectrum-Form--labelsAbove">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -0,0 +1,173 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/fieldlabel/dist/index-vars.css"
|
||||||
|
import { setContext, getContext, onMount } from "svelte"
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
import { createValidatorFromConstraints } from "./validation"
|
||||||
|
import { generateID } from "../helpers"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
export let theme
|
||||||
|
export let size
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const context = getContext("context")
|
||||||
|
const { styleable, API, Provider, ActionTypes } = getContext("sdk")
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
let schema
|
||||||
|
let table
|
||||||
|
let fieldMap = {}
|
||||||
|
|
||||||
|
// Checks if the closest data context matches the model for this forms
|
||||||
|
// datasource, and use it as the initial form values if so
|
||||||
|
const getInitialValues = context => {
|
||||||
|
return context && context.tableId === datasource?.tableId ? context : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the closest data context as the initial form values if it matches
|
||||||
|
const initialValues = getInitialValues(
|
||||||
|
$context[`${$context.closestComponentId}`]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Form state contains observable data about the form
|
||||||
|
const formState = writable({ values: initialValues, errors: {}, valid: true })
|
||||||
|
|
||||||
|
// Form API contains functions to control the form
|
||||||
|
const formApi = {
|
||||||
|
registerField: (field, defaultValue = null) => {
|
||||||
|
if (!field) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (fieldMap[field] != null) {
|
||||||
|
return fieldMap[field]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create validation function based on field schema
|
||||||
|
const constraints = schema?.[field]?.constraints
|
||||||
|
const validate = createValidatorFromConstraints(constraints, field, table)
|
||||||
|
|
||||||
|
fieldMap[field] = {
|
||||||
|
fieldState: makeFieldState(field, defaultValue),
|
||||||
|
fieldApi: makeFieldApi(field, defaultValue, validate),
|
||||||
|
fieldSchema: schema?.[field] ?? {},
|
||||||
|
}
|
||||||
|
return fieldMap[field]
|
||||||
|
},
|
||||||
|
validate: () => {
|
||||||
|
const fields = Object.keys(fieldMap)
|
||||||
|
fields.forEach(field => {
|
||||||
|
const { fieldApi } = fieldMap[field]
|
||||||
|
fieldApi.validate()
|
||||||
|
})
|
||||||
|
return get(formState).valid
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide both form API and state to children
|
||||||
|
setContext("form", { formApi, formState })
|
||||||
|
|
||||||
|
// Action context to pass to children
|
||||||
|
$: actions = [{ type: ActionTypes.ValidateForm, callback: formApi.validate }]
|
||||||
|
|
||||||
|
// Creates an API for a specific field
|
||||||
|
const makeFieldApi = (field, defaultValue, validate) => {
|
||||||
|
const setValue = (value, skipCheck = false) => {
|
||||||
|
const { fieldState } = fieldMap[field]
|
||||||
|
|
||||||
|
// Skip if the value is the same
|
||||||
|
if (!skipCheck && get(fieldState).value === value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = value == null ? defaultValue : value
|
||||||
|
const newError = validate ? validate(newValue) : null
|
||||||
|
const newValid = !newError
|
||||||
|
|
||||||
|
// Update field state
|
||||||
|
fieldState.update(state => {
|
||||||
|
state.value = newValue
|
||||||
|
state.error = newError
|
||||||
|
state.valid = newValid
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update form state
|
||||||
|
formState.update(state => {
|
||||||
|
state.values = { ...state.values, [field]: newValue }
|
||||||
|
if (newError) {
|
||||||
|
state.errors = { ...state.errors, [field]: newError }
|
||||||
|
} else {
|
||||||
|
delete state.errors[field]
|
||||||
|
}
|
||||||
|
state.valid = Object.keys(state.errors).length === 0
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
|
||||||
|
return newValid
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
setValue,
|
||||||
|
validate: () => {
|
||||||
|
const { fieldState } = fieldMap[field]
|
||||||
|
setValue(get(fieldState).value, true)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates observable state data about a specific field
|
||||||
|
const makeFieldState = (field, defaultValue) => {
|
||||||
|
return writable({
|
||||||
|
field,
|
||||||
|
fieldId: `id-${generateID()}`,
|
||||||
|
value: initialValues[field] ?? defaultValue,
|
||||||
|
error: null,
|
||||||
|
valid: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches the form schema from this form's datasource, if one exists
|
||||||
|
const fetchSchema = async () => {
|
||||||
|
if (!datasource?.tableId) {
|
||||||
|
schema = {}
|
||||||
|
table = null
|
||||||
|
} else {
|
||||||
|
table = await API.fetchTableDefinition(datasource?.tableId)
|
||||||
|
if (table) {
|
||||||
|
if (datasource?.type === "query") {
|
||||||
|
schema = {}
|
||||||
|
const params = table.parameters || []
|
||||||
|
params.forEach(param => {
|
||||||
|
schema[param.name] = { ...param, type: "string" }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
schema = table.schema || {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the form schema on mount
|
||||||
|
onMount(fetchSchema)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Provider
|
||||||
|
{actions}
|
||||||
|
data={{ ...$formState.values, tableId: datasource?.tableId }}>
|
||||||
|
<div
|
||||||
|
lang="en"
|
||||||
|
dir="ltr"
|
||||||
|
use:styleable={$component.styles}
|
||||||
|
class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}>
|
||||||
|
{#if loaded}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Provider>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { RichText } from "@budibase/bbui"
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
|
||||||
|
export let field
|
||||||
|
export let label
|
||||||
|
export let placeholder
|
||||||
|
|
||||||
|
let fieldState
|
||||||
|
let fieldApi
|
||||||
|
|
||||||
|
// Update form value from bound value after we've mounted
|
||||||
|
let value
|
||||||
|
let mounted = false
|
||||||
|
$: mounted && fieldApi?.setValue(value)
|
||||||
|
|
||||||
|
// Get the fields initial value after initialising
|
||||||
|
onMount(() => {
|
||||||
|
value = $fieldState?.value
|
||||||
|
mounted = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Options for rich text component
|
||||||
|
const options = {
|
||||||
|
modules: {
|
||||||
|
toolbar: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
header: [1, 2, 3, false],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["bold", "italic", "underline", "strike"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
placeholder: placeholder || "Type something...",
|
||||||
|
theme: "snow",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
{label}
|
||||||
|
{field}
|
||||||
|
type="longform"
|
||||||
|
bind:fieldState
|
||||||
|
bind:fieldApi
|
||||||
|
defaultValue="">
|
||||||
|
{#if mounted}
|
||||||
|
<div>
|
||||||
|
<RichText bind:value {options} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
div :global(> div) {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
div :global(.ql-snow.ql-toolbar:after, .ql-snow .ql-toolbar:after) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
div :global(.ql-snow .ql-formats:after) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
div :global(.ql-editor p) {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import StringField from "./StringField.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<StringField {...$$props} type="number" />
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import Picker from "./Picker.svelte"
|
||||||
|
|
||||||
|
export let field
|
||||||
|
export let label
|
||||||
|
export let placeholder
|
||||||
|
|
||||||
|
let fieldState
|
||||||
|
let fieldApi
|
||||||
|
let fieldSchema
|
||||||
|
|
||||||
|
// Picker state
|
||||||
|
let open = false
|
||||||
|
$: options = fieldSchema?.constraints?.inclusion ?? []
|
||||||
|
$: placeholderText = placeholder || "Choose an option"
|
||||||
|
$: isNull = $fieldState?.value == null || $fieldState?.value === ""
|
||||||
|
$: fieldText = isNull ? placeholderText : $fieldState?.value
|
||||||
|
|
||||||
|
const selectOption = value => {
|
||||||
|
fieldApi.setValue(value)
|
||||||
|
open = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
{field}
|
||||||
|
{label}
|
||||||
|
type="options"
|
||||||
|
bind:fieldState
|
||||||
|
bind:fieldApi
|
||||||
|
bind:fieldSchema>
|
||||||
|
{#if fieldState}
|
||||||
|
<Picker
|
||||||
|
bind:open
|
||||||
|
{fieldState}
|
||||||
|
{fieldText}
|
||||||
|
{options}
|
||||||
|
isPlaceholder={isNull}
|
||||||
|
placeholderOption={placeholderText}
|
||||||
|
isOptionSelected={option => option === $fieldState.value}
|
||||||
|
onSelectOption={selectOption} />
|
||||||
|
{/if}
|
||||||
|
</Field>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue