commit
9dc94fbed6
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.58.3",
|
||||
"@budibase/bbui": "^1.58.5",
|
||||
"@budibase/client": "^0.7.6",
|
||||
"@budibase/colorpicker": "1.0.1",
|
||||
"@budibase/string-templates": "^0.7.6",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { get } from "svelte/store"
|
||||
import { backendUiStore, store } from "builderStore"
|
||||
import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
|
||||
import { findComponentPath } from "./storeUtils"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
import { TableNames } from "../constants"
|
||||
|
||||
|
@ -12,9 +12,7 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
|||
* Gets all bindable data context fields and instance fields.
|
||||
*/
|
||||
export const getBindableProperties = (rootComponent, componentId) => {
|
||||
const contextBindings = getContextBindings(rootComponent, componentId)
|
||||
const componentBindings = getComponentBindings(rootComponent)
|
||||
return [...contextBindings, ...componentBindings]
|
||||
return getContextBindings(rootComponent, componentId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -47,8 +69,9 @@ export const getDatasourceForProvider = component => {
|
|||
}
|
||||
|
||||
// Extract datasource from component instance
|
||||
const validSettingTypes = ["datasource", "table", "schema"]
|
||||
const datasourceSetting = def.settings.find(setting => {
|
||||
return setting.type === "datasource" || setting.type === "table"
|
||||
return validSettingTypes.includes(setting.type)
|
||||
})
|
||||
if (!datasourceSetting) {
|
||||
return null
|
||||
|
@ -58,15 +81,14 @@ export const getDatasourceForProvider = component => {
|
|||
// example an actual datasource object, or a table ID string.
|
||||
// Convert the datasource setting into a proper datasource object so that
|
||||
// we can use it properly
|
||||
if (datasourceSetting.type === "datasource") {
|
||||
return component[datasourceSetting?.key]
|
||||
} else if (datasourceSetting.type === "table") {
|
||||
if (datasourceSetting.type === "table") {
|
||||
return {
|
||||
tableId: component[datasourceSetting?.key],
|
||||
type: "table",
|
||||
}
|
||||
} else {
|
||||
return component[datasourceSetting?.key]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,21 +99,37 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
// Extract any components which provide data contexts
|
||||
const dataProviders = getDataProviderComponents(rootComponent, componentId)
|
||||
let contextBindings = []
|
||||
|
||||
// Create bindings for each data provider
|
||||
dataProviders.forEach(component => {
|
||||
const isForm = component._component.endsWith("/form")
|
||||
const datasource = getDatasourceForProvider(component)
|
||||
let tableName, schema
|
||||
|
||||
// Forms are an edge case which do not need table schemas
|
||||
if (isForm) {
|
||||
schema = buildFormSchema(component)
|
||||
tableName = "Schema"
|
||||
} else {
|
||||
if (!datasource) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get schema and add _id and _rev fields for certain types
|
||||
let { schema, table } = getSchemaForDatasource(datasource)
|
||||
if (!schema || !table) {
|
||||
return
|
||||
}
|
||||
// Get schema and table for the datasource
|
||||
const info = getSchemaForDatasource(datasource, isForm)
|
||||
schema = info.schema
|
||||
tableName = info.table?.name
|
||||
|
||||
// Add _id and _rev fields for certain types
|
||||
if (datasource.type === "table" || datasource.type === "link") {
|
||||
schema["_id"] = { type: "string" }
|
||||
schema["_rev"] = { type: "string " }
|
||||
schema["_rev"] = { type: "string" }
|
||||
}
|
||||
}
|
||||
if (!schema || !tableName) {
|
||||
return
|
||||
}
|
||||
|
||||
const keys = Object.keys(schema).sort()
|
||||
|
||||
// Create bindable properties for each schema field
|
||||
|
@ -110,11 +148,11 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
|
||||
runtimeBoundKey
|
||||
)}`,
|
||||
readableBinding: `${component._instanceName}.${table.name}.${key}`,
|
||||
readableBinding: `${component._instanceName}.${tableName}.${key}`,
|
||||
// Field schema and provider are required to construct relationship
|
||||
// datasource options, based on bindable properties
|
||||
fieldSchema,
|
||||
providerId: component._id,
|
||||
tableId: datasource.tableId,
|
||||
field: key,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -142,44 +180,20 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
type: "context",
|
||||
runtimeBinding: `user.${runtimeBoundKey}`,
|
||||
readableBinding: `Current User.${key}`,
|
||||
// Field schema and provider are required to construct relationship
|
||||
// datasource options, based on bindable properties
|
||||
fieldSchema,
|
||||
providerId: "user",
|
||||
tableId: TableNames.USERS,
|
||||
field: key,
|
||||
})
|
||||
})
|
||||
|
||||
return contextBindings
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const getSchemaForDatasource = datasource => {
|
||||
export const getSchemaForDatasource = (datasource, isForm = false) => {
|
||||
let schema, table
|
||||
if (datasource) {
|
||||
const { type } = datasource
|
||||
|
@ -193,6 +207,14 @@ export const getSchemaForDatasource = datasource => {
|
|||
if (table) {
|
||||
if (type === "view") {
|
||||
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 {
|
||||
schema = cloneDeep(table.schema)
|
||||
}
|
||||
|
@ -201,6 +223,32 @@ export const getSchemaForDatasource = datasource => {
|
|||
return { schema, table }
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a form schema given a form component.
|
||||
* A form schema is a schema of all the fields nested anywhere within a form.
|
||||
*/
|
||||
const buildFormSchema = component => {
|
||||
let schema = {}
|
||||
if (!component) {
|
||||
return schema
|
||||
}
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
const fieldSetting = def?.settings?.find(
|
||||
setting => setting.key === "field" && setting.type.startsWith("field/")
|
||||
)
|
||||
if (fieldSetting && component.field) {
|
||||
const type = fieldSetting.type.split("field/")[1]
|
||||
if (type) {
|
||||
schema[component.field] = { name: component.field, type }
|
||||
}
|
||||
}
|
||||
component._children?.forEach(child => {
|
||||
const childSchema = buildFormSchema(child)
|
||||
schema = { ...schema, ...childSchema }
|
||||
})
|
||||
return schema
|
||||
}
|
||||
|
||||
/**
|
||||
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
|
||||
*/
|
||||
|
|
|
@ -416,7 +416,14 @@ export const getFrontendStore = () => {
|
|||
if (cut) {
|
||||
state.componentToPaste = null
|
||||
} else {
|
||||
componentToPaste._id = uuid()
|
||||
const randomizeIds = component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
component._id = uuid()
|
||||
component._children?.forEach(randomizeIds)
|
||||
}
|
||||
randomizeIds(componentToPaste)
|
||||
}
|
||||
|
||||
if (mode === "inside") {
|
||||
|
|
|
@ -9,5 +9,6 @@ const createScreen = () => {
|
|||
return new Screen()
|
||||
.mainType("div")
|
||||
.component("@budibase/standard-components/container")
|
||||
.instanceName("New Screen")
|
||||
.json()
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import { Screen } from "./utils/Screen"
|
||||
|
||||
export default {
|
||||
name: `New Row (Empty)`,
|
||||
create: () => createScreen(),
|
||||
}
|
||||
|
||||
const createScreen = () => {
|
||||
return new Screen()
|
||||
.component("@budibase/standard-components/newrow")
|
||||
.table("")
|
||||
.json()
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import { Screen } from "./utils/Screen"
|
||||
|
||||
export default {
|
||||
name: `Row Detail (Empty)`,
|
||||
create: () => createScreen(),
|
||||
}
|
||||
|
||||
const createScreen = () => {
|
||||
return new Screen()
|
||||
.component("@budibase/standard-components/rowdetail")
|
||||
.table("")
|
||||
.json()
|
||||
}
|
|
@ -1,17 +1,12 @@
|
|||
import newRowScreen from "./newRowScreen"
|
||||
import rowDetailScreen from "./rowDetailScreen"
|
||||
import rowListScreen from "./rowListScreen"
|
||||
import emptyNewRowScreen from "./emptyNewRowScreen"
|
||||
import createFromScratchScreen from "./createFromScratchScreen"
|
||||
import emptyRowDetailScreen from "./emptyRowDetailScreen"
|
||||
|
||||
const allTemplates = tables => [
|
||||
createFromScratchScreen,
|
||||
...newRowScreen(tables),
|
||||
...rowDetailScreen(tables),
|
||||
...rowListScreen(tables),
|
||||
emptyNewRowScreen,
|
||||
emptyRowDetailScreen,
|
||||
]
|
||||
|
||||
// Allows us to apply common behaviour to all create() functions
|
||||
|
@ -22,8 +17,18 @@ const createTemplateOverride = (frontendState, create) => () => {
|
|||
return screen
|
||||
}
|
||||
|
||||
export default (frontendState, tables) =>
|
||||
allTemplates(tables).map(template => ({
|
||||
export default (frontendState, tables) => {
|
||||
const enrichTemplate = template => ({
|
||||
...template,
|
||||
create: createTemplateOverride(frontendState, template.create),
|
||||
}))
|
||||
})
|
||||
|
||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||
const tableTemplates = allTemplates(tables).map(enrichTemplate)
|
||||
return [
|
||||
fromScratch,
|
||||
...tableTemplates.sort((templateA, templateB) => {
|
||||
return templateA.name > templateB.name ? 1 : -1
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import sanitizeUrl from "./utils/sanitizeUrl"
|
||||
import { Component } from "./utils/Component"
|
||||
import { Screen } from "./utils/Screen"
|
||||
import { Component } from "./utils/Component"
|
||||
import {
|
||||
makeBreadcrumbContainer,
|
||||
makeMainContainer,
|
||||
makeMainForm,
|
||||
makeTitleContainer,
|
||||
makeSaveButton,
|
||||
makeDatasourceFormComponents,
|
||||
} from "./utils/commonComponents"
|
||||
|
||||
export default function(tables) {
|
||||
|
@ -21,29 +22,46 @@ export default function(tables) {
|
|||
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
|
||||
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
|
||||
|
||||
function generateTitleContainer(table, providerId) {
|
||||
return makeTitleContainer("New Row").addChild(
|
||||
makeSaveButton(table, providerId)
|
||||
)
|
||||
function generateTitleContainer(table, formId) {
|
||||
return makeTitleContainer("New Row").addChild(makeSaveButton(table, formId))
|
||||
}
|
||||
|
||||
const createScreen = table => {
|
||||
const screen = new Screen()
|
||||
.component("@budibase/standard-components/newrow")
|
||||
.table(table._id)
|
||||
.route(newRowUrl(table))
|
||||
.component("@budibase/standard-components/container")
|
||||
.instanceName(`${table.name} - New`)
|
||||
.name("")
|
||||
.route(newRowUrl(table))
|
||||
|
||||
const dataform = new Component(
|
||||
"@budibase/standard-components/dataformwide"
|
||||
).instanceName("Form")
|
||||
const form = makeMainForm()
|
||||
.instanceName("Form")
|
||||
.customProps({
|
||||
theme: "spectrum--lightest",
|
||||
size: "spectrum--medium",
|
||||
datasource: {
|
||||
label: table.name,
|
||||
tableId: table._id,
|
||||
type: "table",
|
||||
},
|
||||
})
|
||||
|
||||
const providerId = screen._json.props._id
|
||||
const container = makeMainContainer()
|
||||
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
|
||||
.instanceName("Field Group")
|
||||
.customProps({
|
||||
labelPosition: "left",
|
||||
})
|
||||
|
||||
// Add all form fields from this schema to the field group
|
||||
const datasource = { type: "table", tableId: table._id }
|
||||
makeDatasourceFormComponents(datasource).forEach(component => {
|
||||
fieldGroup.addChild(component)
|
||||
})
|
||||
|
||||
// Add all children to the form
|
||||
const formId = form._json._id
|
||||
form
|
||||
.addChild(makeBreadcrumbContainer(table.name, "New"))
|
||||
.addChild(generateTitleContainer(table, providerId))
|
||||
.addChild(dataform)
|
||||
.addChild(generateTitleContainer(table, formId))
|
||||
.addChild(fieldGroup)
|
||||
|
||||
return screen.addChild(container).json()
|
||||
return screen.addChild(form).json()
|
||||
}
|
||||
|
|
|
@ -4,20 +4,19 @@ import { Screen } from "./utils/Screen"
|
|||
import { Component } from "./utils/Component"
|
||||
import { makePropSafe } from "@budibase/string-templates"
|
||||
import {
|
||||
makeMainContainer,
|
||||
makeBreadcrumbContainer,
|
||||
makeTitleContainer,
|
||||
makeSaveButton,
|
||||
makeMainForm,
|
||||
spectrumColor,
|
||||
makeDatasourceFormComponents,
|
||||
} from "./utils/commonComponents"
|
||||
|
||||
export default function(tables) {
|
||||
return tables.map(table => {
|
||||
const heading = table.primaryDisplay
|
||||
? `{{ data.${makePropSafe(table.primaryDisplay)} }}`
|
||||
: null
|
||||
return {
|
||||
name: `${table.name} - Detail`,
|
||||
create: () => createScreen(table, heading),
|
||||
create: () => createScreen(table),
|
||||
id: ROW_DETAIL_TEMPLATE,
|
||||
}
|
||||
})
|
||||
|
@ -26,9 +25,9 @@ export default function(tables) {
|
|||
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
|
||||
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
|
||||
|
||||
function generateTitleContainer(table, title, providerId) {
|
||||
function generateTitleContainer(table, title, formId) {
|
||||
// have to override style for this, its missing margin
|
||||
const saveButton = makeSaveButton(table, providerId).normalStyle({
|
||||
const saveButton = makeSaveButton(table, formId).normalStyle({
|
||||
background: "#000000",
|
||||
"border-width": "0",
|
||||
"border-style": "None",
|
||||
|
@ -54,6 +53,7 @@ function generateTitleContainer(table, title, providerId) {
|
|||
background: "transparent",
|
||||
color: "#4285f4",
|
||||
})
|
||||
.customStyle(spectrumColor(700))
|
||||
.text("Delete")
|
||||
.customProps({
|
||||
className: "",
|
||||
|
@ -61,8 +61,9 @@ function generateTitleContainer(table, title, providerId) {
|
|||
onClick: [
|
||||
{
|
||||
parameters: {
|
||||
rowId: `{{ ${makePropSafe(providerId)}._id }}`,
|
||||
revId: `{{ ${makePropSafe(providerId)}._rev }}`,
|
||||
providerId: formId,
|
||||
rowId: `{{ ${makePropSafe(formId)}._id }}`,
|
||||
revId: `{{ ${makePropSafe(formId)}._rev }}`,
|
||||
tableId: table._id,
|
||||
},
|
||||
"##eventHandlerType": "Delete Row",
|
||||
|
@ -82,23 +83,47 @@ function generateTitleContainer(table, title, providerId) {
|
|||
.addChild(saveButton)
|
||||
}
|
||||
|
||||
const createScreen = (table, heading) => {
|
||||
const createScreen = table => {
|
||||
const screen = new Screen()
|
||||
.component("@budibase/standard-components/rowdetail")
|
||||
.table(table._id)
|
||||
.instanceName(`${table.name} - Detail`)
|
||||
.route(rowDetailUrl(table))
|
||||
.name("")
|
||||
|
||||
const dataform = new Component(
|
||||
"@budibase/standard-components/dataformwide"
|
||||
).instanceName("Form")
|
||||
const form = makeMainForm()
|
||||
.instanceName("Form")
|
||||
.customProps({
|
||||
theme: "spectrum--lightest",
|
||||
size: "spectrum--medium",
|
||||
datasource: {
|
||||
label: table.name,
|
||||
tableId: table._id,
|
||||
type: "table",
|
||||
},
|
||||
})
|
||||
|
||||
const providerId = screen._json.props._id
|
||||
const container = makeMainContainer()
|
||||
const fieldGroup = new Component("@budibase/standard-components/fieldgroup")
|
||||
.instanceName("Field Group")
|
||||
.customProps({
|
||||
labelPosition: "left",
|
||||
})
|
||||
|
||||
// Add all form fields from this schema to the field group
|
||||
const datasource = { type: "table", tableId: table._id }
|
||||
makeDatasourceFormComponents(datasource).forEach(component => {
|
||||
fieldGroup.addChild(component)
|
||||
})
|
||||
|
||||
// Add all children to the form
|
||||
const formId = form._json._id
|
||||
const rowDetailId = screen._json.props._id
|
||||
const heading = table.primaryDisplay
|
||||
? `{{ ${makePropSafe(rowDetailId)}.${makePropSafe(table.primaryDisplay)} }}`
|
||||
: null
|
||||
form
|
||||
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
|
||||
.addChild(generateTitleContainer(table, heading || "Edit Row", providerId))
|
||||
.addChild(dataform)
|
||||
.addChild(generateTitleContainer(table, heading || "Edit Row", formId))
|
||||
.addChild(fieldGroup)
|
||||
|
||||
return screen.addChild(container).json()
|
||||
return screen.addChild(form).json()
|
||||
}
|
||||
|
|
|
@ -14,17 +14,11 @@ export class Component extends BaseStructure {
|
|||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
type: "",
|
||||
_instanceName: "",
|
||||
_children: [],
|
||||
}
|
||||
}
|
||||
|
||||
type(type) {
|
||||
this._json.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
normalStyle(styling) {
|
||||
this._json._styles.normal = styling
|
||||
return this
|
||||
|
@ -35,14 +29,25 @@ export class Component extends BaseStructure {
|
|||
return this
|
||||
}
|
||||
|
||||
text(text) {
|
||||
this._json.text = text
|
||||
customStyle(styling) {
|
||||
this._json._styles.custom = styling
|
||||
return this
|
||||
}
|
||||
|
||||
// TODO: do we need this
|
||||
instanceName(name) {
|
||||
this._json._instanceName = name
|
||||
return this
|
||||
}
|
||||
|
||||
// Shorthand for custom props "type"
|
||||
type(type) {
|
||||
this._json.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
// Shorthand for custom props "text"
|
||||
text(text) {
|
||||
this._json.text = text
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
import { Component } from "./Component"
|
||||
import { rowListUrl } from "../rowListScreen"
|
||||
import { getSchemaForDatasource } from "../../../dataBinding"
|
||||
|
||||
export function spectrumColor(number) {
|
||||
// Acorn throws a parsing error in this file if the word g-l-o-b-a-l is found
|
||||
// (without dashes - I can't even type it in a comment).
|
||||
// God knows why. It seems to think optional chaining further down the
|
||||
// file is invalid if the word g-l-o-b-a-l is found - hence the reason this
|
||||
// statement is split into parts.
|
||||
return "color: var(--spectrum-glo" + `bal-color-gray-${number});`
|
||||
}
|
||||
|
||||
export function makeLinkComponent(tableName) {
|
||||
return new Component("@budibase/standard-components/link")
|
||||
|
@ -10,6 +20,7 @@ export function makeLinkComponent(tableName) {
|
|||
.hoverStyle({
|
||||
color: "#4285f4",
|
||||
})
|
||||
.customStyle(spectrumColor(700))
|
||||
.text(tableName)
|
||||
.customProps({
|
||||
url: `/${tableName.toLowerCase()}`,
|
||||
|
@ -22,13 +33,12 @@ export function makeLinkComponent(tableName) {
|
|||
})
|
||||
}
|
||||
|
||||
export function makeMainContainer() {
|
||||
return new Component("@budibase/standard-components/container")
|
||||
export function makeMainForm() {
|
||||
return new Component("@budibase/standard-components/form")
|
||||
.type("div")
|
||||
.normalStyle({
|
||||
width: "700px",
|
||||
padding: "0px",
|
||||
background: "white",
|
||||
"border-radius": "0.5rem",
|
||||
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
||||
margin: "auto",
|
||||
|
@ -39,7 +49,7 @@ export function makeMainContainer() {
|
|||
"padding-left": "48px",
|
||||
"margin-bottom": "20px",
|
||||
})
|
||||
.instanceName("Container")
|
||||
.instanceName("Form")
|
||||
}
|
||||
|
||||
export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
||||
|
@ -51,6 +61,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
"margin-right": "4px",
|
||||
"margin-left": "4px",
|
||||
})
|
||||
.customStyle(spectrumColor(700))
|
||||
.text(">")
|
||||
.instanceName("Arrow")
|
||||
|
||||
|
@ -63,6 +74,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
const identifierText = new Component("@budibase/standard-components/text")
|
||||
.type("none")
|
||||
.normalStyle(textStyling)
|
||||
.customStyle(spectrumColor(700))
|
||||
.text(text)
|
||||
.instanceName("Identifier")
|
||||
|
||||
|
@ -78,7 +90,7 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
.addChild(identifierText)
|
||||
}
|
||||
|
||||
export function makeSaveButton(table, providerId) {
|
||||
export function makeSaveButton(table, formId) {
|
||||
return new Component("@budibase/standard-components/button")
|
||||
.normalStyle({
|
||||
background: "#000000",
|
||||
|
@ -99,8 +111,14 @@ export function makeSaveButton(table, providerId) {
|
|||
disabled: false,
|
||||
onClick: [
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
parameters: {
|
||||
providerId,
|
||||
componentId: formId,
|
||||
},
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
providerId: formId,
|
||||
},
|
||||
"##eventHandlerType": "Save Row",
|
||||
},
|
||||
|
@ -125,6 +143,7 @@ export function makeTitleContainer(title) {
|
|||
"margin-left": "0px",
|
||||
flex: "1 1 auto",
|
||||
})
|
||||
.customStyle(spectrumColor(900))
|
||||
.type("h3")
|
||||
.instanceName("Title")
|
||||
.text(title)
|
||||
|
@ -142,3 +161,44 @@ export function makeTitleContainer(title) {
|
|||
.instanceName("Title Container")
|
||||
.addChild(heading)
|
||||
}
|
||||
|
||||
const fieldTypeToComponentMap = {
|
||||
string: "stringfield",
|
||||
number: "numberfield",
|
||||
options: "optionsfield",
|
||||
boolean: "booleanfield",
|
||||
longform: "longformfield",
|
||||
datetime: "datetimefield",
|
||||
attachment: "attachmentfield",
|
||||
link: "relationshipfield",
|
||||
}
|
||||
|
||||
export function makeDatasourceFormComponents(datasource) {
|
||||
const { schema } = getSchemaForDatasource(datasource, true)
|
||||
let components = []
|
||||
let fields = Object.keys(schema || {})
|
||||
fields.forEach(field => {
|
||||
const fieldSchema = schema[field]
|
||||
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
|
||||
* type.
|
||||
* Recurses through the component tree and finds all components which match
|
||||
* a certain selector
|
||||
*/
|
||||
export const findAllMatchingComponents = (rootComponent, selector) => {
|
||||
if (!rootComponent || !selector) {
|
||||
|
@ -81,6 +81,26 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
|
|||
return components.reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the closes parent component which matches certain criteria
|
||||
*/
|
||||
export const findClosestMatchingComponent = (
|
||||
rootComponent,
|
||||
componentId,
|
||||
selector
|
||||
) => {
|
||||
if (!selector) {
|
||||
return null
|
||||
}
|
||||
const componentPath = findComponentPath(rootComponent, componentId).reverse()
|
||||
for (let component of componentPath) {
|
||||
if (selector(component)) {
|
||||
return component
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses through a component tree evaluating a matching function against
|
||||
* components until a match is found
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import groupBy from "lodash/fp/groupBy"
|
||||
import {
|
||||
TextArea,
|
||||
Label,
|
||||
Input,
|
||||
Heading,
|
||||
Body,
|
||||
|
|
|
@ -36,7 +36,9 @@
|
|||
{:else if type === 'boolean'}
|
||||
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
||||
{:else if type === 'link'}
|
||||
<div>
|
||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||
</div>
|
||||
{:else if type === 'longform'}
|
||||
<div>
|
||||
<Label extraSmall grey>{label}</Label>
|
||||
|
|
|
@ -8,11 +8,16 @@
|
|||
"name": "Form",
|
||||
"icon": "ri-file-edit-line",
|
||||
"children": [
|
||||
"dataform",
|
||||
"dataformwide",
|
||||
"input",
|
||||
"richtext",
|
||||
"datepicker"
|
||||
"form",
|
||||
"fieldgroup",
|
||||
"stringfield",
|
||||
"numberfield",
|
||||
"optionsfield",
|
||||
"booleanfield",
|
||||
"longformfield",
|
||||
"datetimefield",
|
||||
"attachmentfield",
|
||||
"relationshipfield"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -56,8 +61,8 @@
|
|||
"screenslot",
|
||||
"navigation",
|
||||
"login",
|
||||
"rowdetail",
|
||||
"newrow"
|
||||
"rowdetail"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -11,9 +11,6 @@
|
|||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<script src='/assets/budibase-client.js'></script>
|
||||
<script>
|
||||
|
|
|
@ -13,9 +13,8 @@
|
|||
let dropdown
|
||||
let anchor
|
||||
|
||||
$: noChildrenAllowed =
|
||||
!component ||
|
||||
!store.actions.components.getDefinition(component._component)?.hasChildren
|
||||
$: definition = store.actions.components.getDefinition(component?._component)
|
||||
$: noChildrenAllowed = !component || !definition?.hasChildren
|
||||
$: noPaste = !$store.componentToPaste
|
||||
|
||||
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
||||
|
@ -130,7 +129,7 @@
|
|||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`}
|
||||
body={`Are you sure you wish to delete this '${definition?.name}' component?`}
|
||||
okText="Delete Component"
|
||||
onOk={deleteComponent} />
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} type="attachment" />
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} type="boolean" />
|
|
@ -19,6 +19,7 @@
|
|||
let drawer
|
||||
|
||||
export let value = {}
|
||||
export let otherSources
|
||||
|
||||
$: tables = $backendUiStore.tables.map(m => ({
|
||||
label: m.name,
|
||||
|
@ -88,7 +89,7 @@
|
|||
class="dropdownbutton"
|
||||
bind:this={anchorRight}
|
||||
on:click={dropdownRight.show}>
|
||||
<span>{value?.label ? value.label : 'Choose option'}</span>
|
||||
<span>{value?.label ?? 'Choose option'}</span>
|
||||
<Icon name="arrowdown" />
|
||||
</div>
|
||||
{#if value?.type === 'query'}
|
||||
|
@ -175,6 +176,22 @@
|
|||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if otherSources?.length}
|
||||
<hr />
|
||||
<div class="title">
|
||||
<Heading extraSmall>Other</Heading>
|
||||
</div>
|
||||
<ul>
|
||||
{#each otherSources as source}
|
||||
<li
|
||||
class:selected={value === source}
|
||||
on:click={() => handleSelected(source)}>
|
||||
{source.label}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} type="datetime" />
|
|
@ -1,15 +1,6 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Body,
|
||||
DropdownMenu,
|
||||
ModalContent,
|
||||
Spacer,
|
||||
} from "@budibase/bbui"
|
||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||
import { Button, DropdownMenu, Spacer } from "@budibase/bbui"
|
||||
import actionTypes from "./actions"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
|
||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
)
|
||||
$: {
|
||||
// Automatically set rev and table ID based on row ID
|
||||
if (parameters.rowId) {
|
||||
parameters.revId = parameters.rowId.replace("_id", "_rev")
|
||||
if (parameters.providerId) {
|
||||
parameters.rowId = `{{ ${parameters.providerId}._id }}`
|
||||
parameters.revId = `{{ ${parameters.providerId}._rev }}`
|
||||
const providerComponent = dataProviderComponents.find(
|
||||
provider => provider._id === parameters.providerId
|
||||
)
|
||||
|
@ -37,12 +38,10 @@
|
|||
</div>
|
||||
{:else}
|
||||
<Label size="m" color="dark">Datasource</Label>
|
||||
<Select secondary bind:value={parameters.rowId}>
|
||||
<Select secondary bind:value={parameters.providerId}>
|
||||
<option value="" />
|
||||
{#each dataProviderComponents as provider}
|
||||
<option value={`{{ ${provider._id}._id }}`}>
|
||||
{provider._instanceName}
|
||||
</option>
|
||||
<option value={provider._id}>{provider._instanceName}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/if}
|
||||
|
|
|
@ -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
|
||||
$: fields =
|
||||
fields ||
|
||||
Object.keys(parameterFields || { "": "" }).map(name => ({
|
||||
Object.keys(parameterFields || {}).map(name => ({
|
||||
name,
|
||||
value:
|
||||
(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 ExecuteQuery from "./ExecuteQuery.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
|
||||
// the component is the setup panel for the action
|
||||
|
@ -30,4 +32,12 @@ export default [
|
|||
name: "Trigger Automation",
|
||||
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 =
|
||||
selectedOption && selectedOption.label ? selectedOption.label : value || ""
|
||||
selectedOption && selectedOption.label
|
||||
? selectedOption.label
|
||||
: value || "Choose option"
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -129,11 +131,16 @@
|
|||
on:keydown={handleEscape}
|
||||
class="bb-select-menu">
|
||||
<ul>
|
||||
<li
|
||||
on:click|self={() => handleClick(null)}
|
||||
class:selected={value == null || value === ''}>
|
||||
Choose option
|
||||
</li>
|
||||
{#if isOptionsObject}
|
||||
{#each options as { value: v, label }}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
on:click|self={() => handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{label}
|
||||
</li>
|
||||
|
@ -142,7 +149,7 @@
|
|||
{#each options as v}
|
||||
<li
|
||||
{...handleStyleBind(v)}
|
||||
on:click|self={handleClick(v)}
|
||||
on:click|self={() => handleClick(v)}
|
||||
class:selected={value === v}>
|
||||
{v}
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} type="options" />
|
|
@ -144,7 +144,7 @@
|
|||
align-items: center;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding-left: var(--spacing-xs);
|
||||
padding-left: 7px;
|
||||
border-left: 1px solid var(--grey-4);
|
||||
background-color: var(--grey-2);
|
||||
border-top-right-radius: var(--border-radius-m);
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
|
||||
{#if open}
|
||||
<div>
|
||||
{#each properties as prop}
|
||||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} type="link" />
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
import DatasourceSelect from "./DatasourceSelect.svelte"
|
||||
|
||||
const otherSources = [{ name: "Custom", label: "Custom" }]
|
||||
</script>
|
||||
|
||||
<DatasourceSelect on:change {...$$props} {otherSources} />
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
</script>
|
||||
|
||||
<FormFieldSelect {...$$props} type="string" />
|
|
@ -1,22 +1,35 @@
|
|||
<script>
|
||||
import { get } from "lodash"
|
||||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
import { Button } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
|
||||
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
||||
import Input from "./PropertyControls/Input.svelte"
|
||||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
||||
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
||||
import OptionSelect from "./PropertyControls/OptionSelect.svelte"
|
||||
import MultiTableViewFieldSelect from "./PropertyControls/MultiTableViewFieldSelect.svelte"
|
||||
import Checkbox from "./PropertyControls/Checkbox.svelte"
|
||||
import TableSelect from "./PropertyControls/TableSelect.svelte"
|
||||
import TableViewSelect from "./PropertyControls/TableViewSelect.svelte"
|
||||
import TableViewFieldSelect from "./PropertyControls/TableViewFieldSelect.svelte"
|
||||
import DatasourceSelect from "./PropertyControls/DatasourceSelect.svelte"
|
||||
import FieldSelect from "./PropertyControls/FieldSelect.svelte"
|
||||
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
|
||||
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
|
||||
import EventsEditor from "./PropertyControls/EventsEditor"
|
||||
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
|
||||
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
|
||||
import { IconSelect } from "./PropertyControls/IconSelect"
|
||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
||||
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
|
||||
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
|
||||
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
|
||||
import BooleanFieldSelect from "./PropertyControls/BooleanFieldSelect.svelte"
|
||||
import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte"
|
||||
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
|
||||
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
|
||||
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
|
||||
|
||||
export let componentDefinition = {}
|
||||
export let componentInstance = {}
|
||||
|
@ -39,6 +52,7 @@
|
|||
"layoutId",
|
||||
"routing.roleId",
|
||||
]
|
||||
let confirmResetFieldsDialog
|
||||
|
||||
$: settings = componentDefinition?.settings ?? []
|
||||
$: isLayout = assetInstance && assetInstance.favicon
|
||||
|
@ -47,7 +61,7 @@
|
|||
const controlMap = {
|
||||
text: Input,
|
||||
select: OptionSelect,
|
||||
datasource: TableViewSelect,
|
||||
datasource: DatasourceSelect,
|
||||
screen: ScreenSelect,
|
||||
detailScreen: DetailScreenSelect,
|
||||
boolean: Checkbox,
|
||||
|
@ -56,8 +70,17 @@
|
|||
table: TableSelect,
|
||||
color: ColorPicker,
|
||||
icon: IconSelect,
|
||||
field: TableViewFieldSelect,
|
||||
multifield: MultiTableViewFieldSelect,
|
||||
field: FieldSelect,
|
||||
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 => {
|
||||
|
@ -78,6 +101,20 @@
|
|||
const onInstanceNameChange = name => {
|
||||
onChange("_instanceName", name)
|
||||
}
|
||||
|
||||
const resetFormFields = () => {
|
||||
const form = findClosestMatchingComponent(
|
||||
$currentAsset.props,
|
||||
componentInstance._id,
|
||||
component => component._component.endsWith("/form")
|
||||
)
|
||||
const datasource = form?.datasource
|
||||
const fields = makeDatasourceFormComponents(datasource)
|
||||
onChange(
|
||||
"_children",
|
||||
fields.map(field => field.json())
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="settings-view-container">
|
||||
|
@ -114,7 +151,7 @@
|
|||
value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue}
|
||||
{componentInstance}
|
||||
onChange={val => onChange(setting.key, val)}
|
||||
props={{ options: setting.options }} />
|
||||
props={{ options: setting.options, placeholder: setting.placeholder }} />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
|
@ -122,7 +159,19 @@
|
|||
This component doesn't have any additional settings.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if componentDefinition?.component?.endsWith('/fieldgroup')}
|
||||
<Button secondary wide on:click={() => confirmResetFieldsDialog?.show()}>
|
||||
Reset Fields
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
bind:this={confirmResetFieldsDialog}
|
||||
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to reset this Field Group?`}
|
||||
okText="Reset"
|
||||
onOk={resetFormFields}
|
||||
title="Confirm Reset Fields" />
|
||||
|
||||
<style>
|
||||
.settings-view-container {
|
||||
|
|
|
@ -9,7 +9,6 @@ export const layout = [
|
|||
key: "display",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Block", value: "block" },
|
||||
{ label: "Inline Block", value: "inline-block" },
|
||||
{ label: "Flex", value: "flex" },
|
||||
|
@ -37,7 +36,6 @@ export const layout = [
|
|||
key: "justify-content",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Flex Start", value: "flex-start" },
|
||||
{ label: "Flex End", value: "flex-end" },
|
||||
{ label: "Center", value: "center" },
|
||||
|
@ -51,7 +49,6 @@ export const layout = [
|
|||
key: "align-items",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Flex Start", value: "flex-start" },
|
||||
{ label: "Flex End", value: "flex-end" },
|
||||
{ label: "Center", value: "center" },
|
||||
|
@ -64,7 +61,6 @@ export const layout = [
|
|||
key: "flex-wrap",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Wrap", value: "wrap" },
|
||||
{ label: "No wrap", value: "nowrap" },
|
||||
],
|
||||
|
@ -74,7 +70,6 @@ export const layout = [
|
|||
key: "gap",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -93,7 +88,6 @@ export const margin = [
|
|||
key: "margin",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -113,7 +107,6 @@ export const margin = [
|
|||
key: "margin-top",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -133,7 +126,6 @@ export const margin = [
|
|||
key: "margin-right",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -153,7 +145,6 @@ export const margin = [
|
|||
key: "margin-bottom",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -173,7 +164,6 @@ export const margin = [
|
|||
key: "margin-left",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -196,7 +186,6 @@ export const padding = [
|
|||
key: "padding",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -214,7 +203,6 @@ export const padding = [
|
|||
key: "padding-top",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -232,7 +220,6 @@ export const padding = [
|
|||
key: "padding-right",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -250,7 +237,6 @@ export const padding = [
|
|||
key: "padding-bottom",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -268,7 +254,6 @@ export const padding = [
|
|||
key: "padding-left",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
|
@ -289,7 +274,6 @@ export const size = [
|
|||
key: "flex",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Shrink", value: "0 1 auto" },
|
||||
{ label: "Grow", value: "1 1 auto" },
|
||||
],
|
||||
|
@ -338,7 +322,6 @@ export const position = [
|
|||
key: "position",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Static", value: "static" },
|
||||
{ label: "Relative", value: "relative" },
|
||||
{ label: "Fixed", value: "fixed" },
|
||||
|
@ -375,7 +358,6 @@ export const position = [
|
|||
key: "z-index",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "-9999", value: "-9999" },
|
||||
{ label: "-3", value: "-3" },
|
||||
{ label: "-2", value: "-2" },
|
||||
|
@ -395,7 +377,6 @@ export const typography = [
|
|||
key: "font-family",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Arial", value: "Arial" },
|
||||
{ label: "Arial Black", value: "Arial Black" },
|
||||
{ label: "Cursive", value: "Cursive" },
|
||||
|
@ -418,7 +399,6 @@ export const typography = [
|
|||
key: "font-weight",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "200", value: "200" },
|
||||
{ label: "300", value: "300" },
|
||||
{ label: "400", value: "400" },
|
||||
|
@ -434,7 +414,6 @@ export const typography = [
|
|||
key: "font-size",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "10px", value: "10px" },
|
||||
{ label: "12px", value: "12px" },
|
||||
|
@ -454,7 +433,6 @@ export const typography = [
|
|||
key: "line-height",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "1", value: "1" },
|
||||
{ label: "1.25", value: "1.25" },
|
||||
{ label: "1.5", value: "1.5" },
|
||||
|
@ -496,7 +474,6 @@ export const typography = [
|
|||
key: "text-decoration-line",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Underline", value: "underline" },
|
||||
{ label: "Overline", value: "overline" },
|
||||
{ label: "Line-through", value: "line-through" },
|
||||
|
@ -516,7 +493,6 @@ export const background = [
|
|||
key: "background-image",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "none" },
|
||||
{
|
||||
label: "Warm Flame",
|
||||
|
@ -603,7 +579,6 @@ export const border = [
|
|||
key: "border-radius",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "X Small", value: "0.125rem" },
|
||||
{ label: "Small", value: "0.25rem" },
|
||||
|
@ -619,7 +594,6 @@ export const border = [
|
|||
key: "border-width",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "X Small", value: "0.5px" },
|
||||
{ label: "Small", value: "1px" },
|
||||
|
@ -638,7 +612,6 @@ export const border = [
|
|||
key: "border-style",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "Hidden", value: "hidden" },
|
||||
{ label: "Dotted", value: "dotted" },
|
||||
|
@ -659,7 +632,6 @@ export const effects = [
|
|||
key: "opacity",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "0", value: "0" },
|
||||
{ label: "0.2", value: "0.2" },
|
||||
{ label: "0.4", value: "0.4" },
|
||||
|
@ -673,7 +645,6 @@ export const effects = [
|
|||
key: "transform",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "45 deg", value: "rotate(45deg)" },
|
||||
{ label: "90 deg", value: "rotate(90deg)" },
|
||||
|
@ -690,7 +661,6 @@ export const effects = [
|
|||
key: "box-shadow",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
|
||||
{
|
||||
|
@ -723,7 +693,6 @@ export const transitions = [
|
|||
key: "transition-property",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Background Color", value: "background color" },
|
||||
|
@ -745,7 +714,6 @@ export const transitions = [
|
|||
control: OptionSelect,
|
||||
placeholder: "sec",
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "0.4s", value: "0.4s" },
|
||||
{ label: "0.6s", value: "0.6s" },
|
||||
{ label: "0.8s", value: "0.8s" },
|
||||
|
@ -759,7 +727,6 @@ export const transitions = [
|
|||
key: "transition-timing-function",
|
||||
control: OptionSelect,
|
||||
options: [
|
||||
{ label: "Choose option", value: "" },
|
||||
{ label: "Linear", value: "linear" },
|
||||
{ label: "Ease", value: "ease" },
|
||||
{ label: "Ease in", value: "ease-in" },
|
||||
|
|
|
@ -842,10 +842,10 @@
|
|||
lodash "^4.17.19"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/bbui@^1.58.3":
|
||||
version "1.58.3"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.3.tgz#86ad6aa68eec7426e1ccdf1f7e7fc957cb57d3a3"
|
||||
integrity sha512-PpbxfBhVpmP0EO1nPBhrz486EHCIgtJlXELC/ElzjG+FCQZSCvDSM7mq/97FOW35iYdTiQTlwFgOtvOgT1P8IQ==
|
||||
"@budibase/bbui@^1.58.5":
|
||||
version "1.58.5"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.5.tgz#c9ce712941760825c7774a1de77594e989db4561"
|
||||
integrity sha512-0j1I7BetJ2GzB1BXKyvvlkuFphLmADJh2U/Ihubwxx5qUDY8REoVzLgAB4c24zt0CGVTF9VMmOoMLd0zD0QwdQ==
|
||||
dependencies:
|
||||
markdown-it "^12.0.2"
|
||||
quill "^1.3.7"
|
||||
|
|
|
@ -18,14 +18,12 @@ const handleError = error => {
|
|||
const makeApiCall = async ({ method, url, body, json = true }) => {
|
||||
try {
|
||||
const requestBody = json ? JSON.stringify(body) : body
|
||||
let headers = {
|
||||
const inBuilder = window["##BUDIBASE_IN_BUILDER##"]
|
||||
const headers = {
|
||||
Accept: "application/json",
|
||||
...(json && { "Content-Type": "application/json" }),
|
||||
"x-budibase-app-id": window["##BUDIBASE_APP_ID##"],
|
||||
}
|
||||
|
||||
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
||||
headers["x-budibase-type"] = "client"
|
||||
...(json && { "Content-Type": "application/json" }),
|
||||
...(!inBuilder && { "x-budibase-type": "client" }),
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { fetchTableData, searchTableData } from "./tables"
|
||||
import { fetchTableData } from "./tables"
|
||||
import { fetchViewData } from "./views"
|
||||
import { fetchRelationshipData } from "./relationships"
|
||||
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.
|
||||
|
@ -33,7 +27,7 @@ export const fetchDatasource = async datasource => {
|
|||
parameters[param.name] = param.default
|
||||
}
|
||||
}
|
||||
return await executeQuery({ queryId: datasource._id, parameters })
|
||||
rows = await executeQuery({ queryId: datasource._id, parameters })
|
||||
} else if (type === "link") {
|
||||
rows = await fetchRelationshipData({
|
||||
rowId: datasource.rowId,
|
||||
|
@ -42,6 +36,6 @@ export const fetchDatasource = async datasource => {
|
|||
})
|
||||
}
|
||||
|
||||
// Enrich rows so they can displayed properly
|
||||
return await enrichRows(rows, tableId)
|
||||
// Enrich the result is always an array
|
||||
return Array.isArray(rows) ? rows : []
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import { notificationStore } from "../store/notification"
|
||||
import { notificationStore, datasourceStore } from "../store"
|
||||
import API from "./api"
|
||||
|
||||
/**
|
||||
* Executes a query against an external data connector.
|
||||
*/
|
||||
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({
|
||||
url: `/api/queries/${queryId}`,
|
||||
body: {
|
||||
|
@ -13,6 +18,9 @@ export const executeQuery = async ({ queryId, parameters }) => {
|
|||
})
|
||||
if (res.error) {
|
||||
notificationStore.danger("An error has occurred")
|
||||
} else if (!query.readable) {
|
||||
notificationStore.success("Query executed successfully")
|
||||
datasourceStore.actions.invalidateDatasource(query.datasourceId)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { notificationStore } from "../store/notification"
|
||||
import { notificationStore, datasourceStore } from "../store"
|
||||
import API from "./api"
|
||||
import { fetchTableDefinition } from "./tables"
|
||||
|
||||
|
@ -6,6 +6,9 @@ import { fetchTableDefinition } from "./tables"
|
|||
* Fetches data about a certain row in a table.
|
||||
*/
|
||||
export const fetchRow = async ({ tableId, rowId }) => {
|
||||
if (!tableId || !rowId) {
|
||||
return
|
||||
}
|
||||
const row = await API.get({
|
||||
url: `/api/${tableId}/rows/${rowId}`,
|
||||
})
|
||||
|
@ -16,6 +19,9 @@ export const fetchRow = async ({ tableId, rowId }) => {
|
|||
* Creates a row in a table.
|
||||
*/
|
||||
export const saveRow = async row => {
|
||||
if (!row?.tableId) {
|
||||
return
|
||||
}
|
||||
const res = await API.post({
|
||||
url: `/api/${row.tableId}/rows`,
|
||||
body: row,
|
||||
|
@ -23,6 +29,10 @@ export const saveRow = async row => {
|
|||
res.error
|
||||
? notificationStore.danger("An error has occurred")
|
||||
: notificationStore.success("Row saved")
|
||||
|
||||
// Refresh related datasources
|
||||
datasourceStore.actions.invalidateDatasource(row.tableId)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -30,6 +40,9 @@ export const saveRow = async row => {
|
|||
* Updates a row in a table.
|
||||
*/
|
||||
export const updateRow = async row => {
|
||||
if (!row?.tableId || !row?._id) {
|
||||
return
|
||||
}
|
||||
const res = await API.patch({
|
||||
url: `/api/${row.tableId}/rows/${row._id}`,
|
||||
body: row,
|
||||
|
@ -37,6 +50,10 @@ export const updateRow = async row => {
|
|||
res.error
|
||||
? notificationStore.danger("An error has occurred")
|
||||
: notificationStore.success("Row updated")
|
||||
|
||||
// Refresh related datasources
|
||||
datasourceStore.actions.invalidateDatasource(row.tableId)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -44,12 +61,19 @@ export const updateRow = async row => {
|
|||
* Deletes a row from a table.
|
||||
*/
|
||||
export const deleteRow = async ({ tableId, rowId, revId }) => {
|
||||
if (!tableId || !rowId || !revId) {
|
||||
return
|
||||
}
|
||||
const res = await API.del({
|
||||
url: `/api/${tableId}/rows/${rowId}/${revId}`,
|
||||
})
|
||||
res.error
|
||||
? notificationStore.danger("An error has occurred")
|
||||
: notificationStore.success("Row deleted")
|
||||
|
||||
// Refresh related datasources
|
||||
datasourceStore.actions.invalidateDatasource(tableId)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -57,6 +81,9 @@ export const deleteRow = async ({ tableId, rowId, revId }) => {
|
|||
* Deletes many rows from a table.
|
||||
*/
|
||||
export const deleteRows = async ({ tableId, rows }) => {
|
||||
if (!tableId || !rows) {
|
||||
return
|
||||
}
|
||||
const res = await API.post({
|
||||
url: `/api/${tableId}/rows`,
|
||||
body: {
|
||||
|
@ -67,6 +94,10 @@ export const deleteRows = async ({ tableId, rows }) => {
|
|||
res.error
|
||||
? notificationStore.danger("An error has occurred")
|
||||
: notificationStore.success(`${rows.length} row(s) deleted`)
|
||||
|
||||
// Refresh related datasources
|
||||
datasourceStore.actions.invalidateDatasource(tableId)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -75,7 +106,10 @@ export const deleteRows = async ({ tableId, rows }) => {
|
|||
* be properly displayed.
|
||||
*/
|
||||
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
|
||||
const tableDefinition = await fetchTableDefinition(tableId)
|
||||
const schema = tableDefinition && tableDefinition.schema
|
||||
|
|
|
@ -3,14 +3,19 @@
|
|||
import { setContext, onMount } from "svelte"
|
||||
import Component from "./Component.svelte"
|
||||
import NotificationDisplay from "./NotificationDisplay.svelte"
|
||||
import Provider from "./Provider.svelte"
|
||||
import SDK from "../sdk"
|
||||
import { createDataStore, initialise, screenStore, authStore } from "../store"
|
||||
import {
|
||||
createContextStore,
|
||||
initialise,
|
||||
screenStore,
|
||||
authStore,
|
||||
} from "../store"
|
||||
|
||||
// Provide contexts
|
||||
setContext("sdk", SDK)
|
||||
setContext("component", writable({}))
|
||||
setContext("data", createDataStore())
|
||||
setContext("screenslot", false)
|
||||
setContext("context", createContextStore())
|
||||
|
||||
let loaded = false
|
||||
|
||||
|
@ -23,6 +28,8 @@
|
|||
</script>
|
||||
|
||||
{#if loaded && $screenStore.activeLayout}
|
||||
<Provider key="user" data={$authStore}>
|
||||
<Component definition={$screenStore.activeLayout.props} />
|
||||
<NotificationDisplay />
|
||||
</Provider>
|
||||
{/if}
|
||||
<NotificationDisplay />
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
<script>
|
||||
import { getContext, setContext } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
import * as ComponentLibrary from "@budibase/standard-components"
|
||||
import Router from "./Router.svelte"
|
||||
import { enrichProps, propsAreSame } from "../utils/componentProps"
|
||||
import { authStore, bindingStore, builderStore } from "../store"
|
||||
import { builderStore } from "../store"
|
||||
import { hashString } from "../utils/hash"
|
||||
|
||||
export let definition = {}
|
||||
|
||||
let enrichedProps
|
||||
// Props that will be passed to the component instance
|
||||
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
|
||||
const dataContext = getContext("data")
|
||||
const screenslotContext = getContext("screenslot")
|
||||
const context = getContext("context")
|
||||
|
||||
// Create component context
|
||||
const componentStore = writable({})
|
||||
|
@ -23,39 +32,16 @@
|
|||
$: constructor = getComponentConstructor(definition._component)
|
||||
$: children = definition._children || []
|
||||
$: id = definition._id
|
||||
$: enrichComponentProps(definition, $dataContext, $bindingStore, $authStore)
|
||||
$: updateProps(enrichedProps)
|
||||
$: updateComponentProps(definition, $context)
|
||||
$: 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
|
||||
$: componentStore.set({
|
||||
id,
|
||||
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
|
||||
const getComponentConstructor = component => {
|
||||
const split = component?.split("/")
|
||||
|
@ -67,25 +53,53 @@
|
|||
}
|
||||
|
||||
// Enriches any string component props using handlebars
|
||||
const enrichComponentProps = async (definition, context, bindings, user) => {
|
||||
enrichedProps = await enrichProps(definition, context, bindings, user)
|
||||
const updateComponentProps = async (definition, context) => {
|
||||
// Record the timestamp so we can reference it after enrichment
|
||||
latestUpdateTime = Date.now()
|
||||
const enrichmentTime = latestUpdateTime
|
||||
|
||||
// Enrich props with context
|
||||
const enrichedProps = await enrichProps(definition, context)
|
||||
|
||||
// Abandon this update if a newer update has started
|
||||
if (enrichmentTime !== latestUpdateTime) {
|
||||
return
|
||||
}
|
||||
|
||||
// Returns a unique key to let svelte know when to remount components.
|
||||
// If a component is selected we want to remount it every time any props
|
||||
// change.
|
||||
const getChildKey = childId => {
|
||||
const selected = childId === $builderStore.selectedComponentId
|
||||
return selected ? `${childId}-${$builderStore.previewId}` : childId
|
||||
// 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>
|
||||
|
||||
{#if constructor && componentProps}
|
||||
{#key propsHash}
|
||||
<svelte:component this={constructor} {...componentProps}>
|
||||
{#if children.length}
|
||||
{#each children as child (getChildKey(child._id))}
|
||||
{#each children as child (child._id)}
|
||||
<svelte:self definition={child} />
|
||||
{/each}
|
||||
{/if}
|
||||
</svelte:component>
|
||||
{/key}
|
||||
{/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 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,
|
||||
// to ensure the svelte-spa-router is fully remounted when route config
|
||||
// changes
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
export const TableNames = {
|
||||
USERS: "ta_users",
|
||||
}
|
||||
|
||||
export const ActionTypes = {
|
||||
ValidateForm: "ValidateForm",
|
||||
RefreshDatasource: "RefreshDatasource",
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ import {
|
|||
notificationStore,
|
||||
routeStore,
|
||||
screenStore,
|
||||
bindingStore,
|
||||
builderStore,
|
||||
} from "./store"
|
||||
import { styleable } from "./utils/styleable"
|
||||
import { linkable } from "./utils/linkable"
|
||||
import DataProvider from "./components/DataProvider.svelte"
|
||||
import Provider from "./components/Provider.svelte"
|
||||
import { ActionTypes } from "./constants"
|
||||
|
||||
export default {
|
||||
API,
|
||||
|
@ -20,6 +20,6 @@ export default {
|
|||
builderStore,
|
||||
styleable,
|
||||
linkable,
|
||||
DataProvider,
|
||||
setBindableValue: bindingStore.actions.setBindableValue,
|
||||
Provider,
|
||||
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 actions = {
|
||||
selectComponent: id => {
|
||||
if (id) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("bb-select-component", { detail: id })
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
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 { screenStore } from "./screens"
|
||||
export { builderStore } from "./builder"
|
||||
export { bindingStore } from "./binding"
|
||||
export { datasourceStore } from "./datasource"
|
||||
|
||||
// Data stores are layered and duplicated, so it is not a singleton
|
||||
export { createDataStore, dataStore } from "./data"
|
||||
// Context stores are layered and duplicated, so it is not a singleton
|
||||
export { createContextStore } from "./context"
|
||||
|
||||
// Initialises an app by loading screens and routes
|
||||
export { initialise } from "./initialise"
|
||||
|
|
|
@ -5,13 +5,22 @@ const NOTIFICATION_TIMEOUT = 3000
|
|||
|
||||
const createNotificationStore = () => {
|
||||
const _notifications = writable([])
|
||||
let block = false
|
||||
|
||||
const send = (message, type = "default") => {
|
||||
if (block) {
|
||||
return
|
||||
}
|
||||
_notifications.update(state => {
|
||||
return [...state, { id: generate(), type, message }]
|
||||
})
|
||||
}
|
||||
|
||||
const blockNotifications = (timeout = 1000) => {
|
||||
block = true
|
||||
setTimeout(() => (block = false), timeout)
|
||||
}
|
||||
|
||||
const notifications = derived(_notifications, ($_notifications, set) => {
|
||||
set($_notifications)
|
||||
if ($_notifications.length > 0) {
|
||||
|
@ -36,6 +45,7 @@ const createNotificationStore = () => {
|
|||
warning: msg => send(msg, "warning"),
|
||||
info: msg => send(msg, "info"),
|
||||
success: msg => send(msg, "success"),
|
||||
blockNotifications,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,43 +1,34 @@
|
|||
import { get } from "svelte/store"
|
||||
import { enrichDataBinding, enrichDataBindings } from "./enrichDataBinding"
|
||||
import { routeStore, builderStore } from "../store"
|
||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
|
||||
import { ActionTypes } from "../constants"
|
||||
|
||||
const saveRowHandler = async (action, context) => {
|
||||
const { fields, providerId } = action.parameters
|
||||
if (providerId) {
|
||||
let draft = context[`${providerId}_draft`]
|
||||
let draft = context[providerId]
|
||||
if (fields) {
|
||||
for (let [key, entry] of Object.entries(fields)) {
|
||||
draft[key] = await enrichDataBinding(entry.value, context)
|
||||
draft[key] = entry.value
|
||||
}
|
||||
}
|
||||
await saveRow(draft)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteRowHandler = async (action, context) => {
|
||||
const deleteRowHandler = async action => {
|
||||
const { tableId, revId, rowId } = action.parameters
|
||||
if (tableId && revId && rowId) {
|
||||
const [enrichTable, enrichRow, enrichRev] = await Promise.all([
|
||||
enrichDataBinding(tableId, context),
|
||||
enrichDataBinding(rowId, context),
|
||||
enrichDataBinding(revId, context),
|
||||
])
|
||||
await deleteRow({
|
||||
tableId: enrichTable,
|
||||
rowId: enrichRow,
|
||||
revId: enrichRev,
|
||||
})
|
||||
await deleteRow({ tableId, rowId, revId })
|
||||
}
|
||||
}
|
||||
|
||||
const triggerAutomationHandler = async (action, context) => {
|
||||
const { fields } = action.parameters()
|
||||
const triggerAutomationHandler = async action => {
|
||||
const { fields } = action.parameters
|
||||
if (fields) {
|
||||
const params = {}
|
||||
for (let field in fields) {
|
||||
params[field] = await enrichDataBinding(fields[field].value, context)
|
||||
params[field] = fields[field].value
|
||||
}
|
||||
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 enrichedQueryParameters = await enrichDataBindings(
|
||||
queryParams || {},
|
||||
context
|
||||
)
|
||||
await executeQuery({
|
||||
datasourceId,
|
||||
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 = {
|
||||
["Save Row"]: saveRowHandler,
|
||||
["Delete Row"]: deleteRowHandler,
|
||||
["Navigate To"]: navigationHandler,
|
||||
["Execute Query"]: queryExecutionHandler,
|
||||
["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"]])
|
||||
return async () => {
|
||||
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.
|
||||
* 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
|
||||
let validProps = {}
|
||||
Object.entries(props)
|
||||
|
@ -32,20 +32,23 @@ export const enrichProps = async (props, dataContexts, dataBindings, user) => {
|
|||
|
||||
// Create context of all bindings and data contexts
|
||||
// Duplicate the closest context as "data" which the builder requires
|
||||
const context = {
|
||||
...dataContexts,
|
||||
...dataBindings,
|
||||
user,
|
||||
data: dataContexts[dataContexts.closestComponentId],
|
||||
data_draft: dataContexts[`${dataContexts.closestComponentId}_draft`],
|
||||
const totalContext = {
|
||||
...context,
|
||||
|
||||
// This is only required for legacy bindings that used "data" rather than a
|
||||
// component ID.
|
||||
data: context[context.closestComponentId],
|
||||
}
|
||||
|
||||
// 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
|
||||
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
||||
enrichedProps.onClick = enrichButtonActions(enrichedProps.onClick, context)
|
||||
enrichedProps.onClick = enrichButtonActions(
|
||||
enrichedProps.onClick,
|
||||
totalContext
|
||||
)
|
||||
}
|
||||
|
||||
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 { builderStore } from "../store"
|
||||
|
||||
const selectedComponentWidth = 2
|
||||
const selectedComponentColor = "#4285f4"
|
||||
|
||||
/**
|
||||
* Helper to build a CSS string from a style object.
|
||||
*/
|
||||
const buildStyleString = (styleObject, customStyles) => {
|
||||
let str = ""
|
||||
Object.entries(styleObject).forEach(([style, value]) => {
|
||||
Object.entries(styleObject || {}).forEach(([style, value]) => {
|
||||
if (style && value != null) {
|
||||
str += `${style}: ${value}; `
|
||||
}
|
||||
|
@ -23,24 +20,14 @@ const buildStyleString = (styleObject, customStyles) => {
|
|||
* events for any selectable components (overriding the blanket ban on pointer
|
||||
* events in the iframe HTML).
|
||||
*/
|
||||
const addBuilderPreviewStyles = (styleString, componentId, selectable) => {
|
||||
let str = styleString
|
||||
|
||||
// Apply extra styles if we're in the builder preview
|
||||
const state = get(builderStore)
|
||||
if (state.inBuilder) {
|
||||
// Allow pointer events and always enable cursor
|
||||
if (selectable) {
|
||||
str += ";pointer-events: all !important; cursor: pointer !important;"
|
||||
const addBuilderPreviewStyles = (node, styleString, componentId) => {
|
||||
if (componentId === get(builderStore).selectedComponentId) {
|
||||
const style = window.getComputedStyle(node)
|
||||
const property = style?.display === "table-row" ? "outline" : "border"
|
||||
return styleString + `;${property}: 2px solid #4285f4 !important;`
|
||||
} else {
|
||||
return styleString
|
||||
}
|
||||
|
||||
// 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 selectComponent
|
||||
|
||||
// Kill JS even bubbling
|
||||
const blockEvent = event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return false
|
||||
}
|
||||
|
||||
// Creates event listeners and applies initial styles
|
||||
const setupStyles = newStyles => {
|
||||
const setupStyles = (newStyles = {}) => {
|
||||
const componentId = newStyles.id
|
||||
const selectable = newStyles.allowSelection
|
||||
const customStyles = newStyles.custom
|
||||
const normalStyles = newStyles.normal
|
||||
const customStyles = newStyles.custom || ""
|
||||
const normalStyles = newStyles.normal || {}
|
||||
const hoverStyles = {
|
||||
...normalStyles,
|
||||
...newStyles.hover,
|
||||
...(newStyles.hover || {}),
|
||||
}
|
||||
|
||||
// Applies a style string to a DOM node, enriching it for the builder
|
||||
// preview
|
||||
// Applies a style string to a DOM node
|
||||
const applyStyles = styleString => {
|
||||
node.style = addBuilderPreviewStyles(styleString, componentId, selectable)
|
||||
node.style = addBuilderPreviewStyles(node, styleString, componentId)
|
||||
node.dataset.componentId = componentId
|
||||
}
|
||||
|
||||
// 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
|
||||
// builder preview
|
||||
selectComponent = event => {
|
||||
builderStore.actions.selectComponent(newStyles.id)
|
||||
return blockEvent(event)
|
||||
builderStore.actions.selectComponent(componentId)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return false
|
||||
}
|
||||
|
||||
// Add listeners to toggle hover styles
|
||||
|
@ -100,10 +81,6 @@ export const styleable = (node, styles = {}) => {
|
|||
// Add builder preview click listener
|
||||
if (get(builderStore).inBuilder) {
|
||||
node.addEventListener("click", selectComponent, false)
|
||||
|
||||
// Kill other interaction events
|
||||
node.addEventListener("mousedown", blockEvent)
|
||||
node.addEventListener("mouseup", blockEvent)
|
||||
}
|
||||
|
||||
// Apply initial normal styles
|
||||
|
@ -118,8 +95,6 @@ export const styleable = (node, styles = {}) => {
|
|||
// Remove builder preview click listener
|
||||
if (get(builderStore).inBuilder) {
|
||||
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 { generateQueryID, getQueryParams } = require("../../db/utils")
|
||||
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) {
|
||||
if (typeof resp === "string") {
|
||||
|
@ -21,7 +35,7 @@ exports.fetch = async function(ctx) {
|
|||
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) {
|
||||
|
@ -61,6 +75,18 @@ async function enrichQueryFields(fields, parameters) {
|
|||
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) {
|
||||
const db = new CouchDB(ctx.user.appId)
|
||||
|
||||
|
|
|
@ -16,13 +16,6 @@ const {
|
|||
|
||||
const router = Router()
|
||||
|
||||
const QueryVerb = {
|
||||
Create: "create",
|
||||
Read: "read",
|
||||
Update: "update",
|
||||
Delete: "delete",
|
||||
}
|
||||
|
||||
function generateQueryValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
|
@ -36,7 +29,7 @@ function generateQueryValidation() {
|
|||
name: Joi.string(),
|
||||
default: Joi.string()
|
||||
})),
|
||||
queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(),
|
||||
queryVerb: Joi.string().allow().required(),
|
||||
schema: Joi.object({}).required().unknown(true)
|
||||
}))
|
||||
}
|
||||
|
@ -45,7 +38,7 @@ function generateQueryPreviewValidation() {
|
|||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
fields: Joi.object().required(),
|
||||
queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(),
|
||||
queryVerb: Joi.string().allow().required(),
|
||||
datasourceId: Joi.string().required(),
|
||||
parameters: Joi.object({}).required().unknown(true)
|
||||
}))
|
||||
|
@ -67,6 +60,11 @@ router
|
|||
generateQueryPreviewValidation(),
|
||||
queryController.preview
|
||||
)
|
||||
.get(
|
||||
"/api/queries/:queryId",
|
||||
authorized(PermissionTypes.QUERY, PermissionLevels.READ),
|
||||
queryController.find
|
||||
)
|
||||
.post(
|
||||
"/api/queries/:queryId",
|
||||
paramResource("queryId"),
|
||||
|
|
|
@ -4,7 +4,7 @@ const {
|
|||
defaultHeaders,
|
||||
builderEndpointShouldBlockNormalUsers,
|
||||
getDocument,
|
||||
insertDocument
|
||||
insertDocument,
|
||||
} = require("./couchTestUtils")
|
||||
let { generateDatasourceID, generateQueryID } = require("../../../db/utils")
|
||||
|
||||
|
@ -21,11 +21,11 @@ const TEST_DATASOURCE = {
|
|||
const TEST_QUERY = {
|
||||
_id: generateQueryID(DATASOURCE_ID),
|
||||
datasourceId: DATASOURCE_ID,
|
||||
name:"New Query",
|
||||
parameters:[],
|
||||
fields:{},
|
||||
schema:{},
|
||||
queryVerb:"read",
|
||||
name: "New Query",
|
||||
parameters: [],
|
||||
fields: {},
|
||||
schema: {},
|
||||
queryVerb: "read",
|
||||
}
|
||||
|
||||
describe("/queries", () => {
|
||||
|
@ -37,8 +37,8 @@ describe("/queries", () => {
|
|||
let query
|
||||
|
||||
beforeAll(async () => {
|
||||
({ request, server } = await supertest())
|
||||
});
|
||||
;({ request, server } = await supertest())
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
|
@ -47,7 +47,7 @@ describe("/queries", () => {
|
|||
beforeEach(async () => {
|
||||
app = await createApplication(request)
|
||||
appId = app.instance._id
|
||||
});
|
||||
})
|
||||
|
||||
async function createDatasource() {
|
||||
return await insertDocument(appId, TEST_DATASOURCE)
|
||||
|
@ -63,43 +63,46 @@ describe("/queries", () => {
|
|||
.post(`/api/queries`)
|
||||
.send(TEST_QUERY)
|
||||
.set(defaultHeaders(appId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.res.statusMessage).toEqual(`Query ${TEST_QUERY.name} saved successfully.`);
|
||||
expect(res.res.statusMessage).toEqual(
|
||||
`Query ${TEST_QUERY.name} saved successfully.`
|
||||
)
|
||||
expect(res.body).toEqual({
|
||||
_rev: res.body._rev,
|
||||
...TEST_QUERY,
|
||||
});
|
||||
})
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
let datasource
|
||||
|
||||
beforeEach(async () => {
|
||||
datasource = await createDatasource()
|
||||
});
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete datasource._rev
|
||||
});
|
||||
})
|
||||
|
||||
it("returns all the queries from the server", async () => {
|
||||
const query = await createQuery()
|
||||
const res = await request
|
||||
.get(`/api/queries`)
|
||||
.set(defaultHeaders(appId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
const queries = res.body;
|
||||
const queries = res.body
|
||||
expect(queries).toEqual([
|
||||
{
|
||||
"_rev": query.rev,
|
||||
...TEST_QUERY
|
||||
}
|
||||
]);
|
||||
_rev: query.rev,
|
||||
...TEST_QUERY,
|
||||
readable: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
|
@ -110,18 +113,18 @@ describe("/queries", () => {
|
|||
appId: appId,
|
||||
})
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
describe("destroy", () => {
|
||||
let datasource;
|
||||
let datasource
|
||||
|
||||
beforeEach(async () => {
|
||||
datasource = await createDatasource()
|
||||
});
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete datasource._rev
|
||||
});
|
||||
})
|
||||
|
||||
it("deletes a query and returns a success message", async () => {
|
||||
const query = await createQuery()
|
||||
|
@ -134,7 +137,7 @@ describe("/queries", () => {
|
|||
const res = await request
|
||||
.get(`/api/queries`)
|
||||
.set(defaultHeaders(appId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual([])
|
||||
|
@ -148,5 +151,5 @@ describe("/queries", () => {
|
|||
appId: appId,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
|
@ -44,3 +44,9 @@ exports.USERS_TABLE_SCHEMA = USERS_TABLE_SCHEMA
|
|||
exports.BUILDER_CONFIG_DB = "builder-config-db"
|
||||
exports.HOSTING_DOC = "hosting-doc"
|
||||
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
|
||||
for (let linkId of rowField) {
|
||||
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(
|
||||
new LinkDocument(
|
||||
table._id,
|
||||
|
|
|
@ -16,6 +16,12 @@ const TYPE_TRANSFORM_MAP = {
|
|||
"": [],
|
||||
[null]: [],
|
||||
[undefined]: undefined,
|
||||
parse: link => {
|
||||
if (typeof link === "string") {
|
||||
return [link]
|
||||
}
|
||||
return link
|
||||
},
|
||||
},
|
||||
options: {
|
||||
"": "",
|
||||
|
@ -165,15 +171,15 @@ exports.walkDir = (dirPath, callback) => {
|
|||
* @param {object} type The type fo coerce to
|
||||
* @returns {object} The coerced value
|
||||
*/
|
||||
exports.coerceValue = (value, type) => {
|
||||
exports.coerceValue = (row, type) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(value)) {
|
||||
return TYPE_TRANSFORM_MAP[type][value]
|
||||
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) {
|
||||
return TYPE_TRANSFORM_MAP[type][row]
|
||||
} 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,
|
||||
"hasChildren": true,
|
||||
"dataProvider": true,
|
||||
"actions": ["RefreshDatasource"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "datasource",
|
||||
|
@ -114,8 +115,9 @@
|
|||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "No Rows Message",
|
||||
"key": "noRowsMessage"
|
||||
"label": "Empty Text",
|
||||
"key": "noRowsMessage",
|
||||
"defaultValue": "No rows found."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -140,66 +142,15 @@
|
|||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Rows Per Page",
|
||||
"label": "Rows/Page",
|
||||
"defaultValue": 25,
|
||||
"key": "pageSize"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "No Rows Message",
|
||||
"label": "Empty Text",
|
||||
"key": "noRowsMessage",
|
||||
"defaultValue": "No Rows"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
"defaultValue": "No rows found."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1127,5 +1078,262 @@
|
|||
"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-postcss": "^3.1.5",
|
||||
"rollup-plugin-svelte": "^6.1.1",
|
||||
"rollup-plugin-svg": "^2.0.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sirv-cli": "^0.4.4",
|
||||
"svelte": "^3.30.0"
|
||||
|
@ -38,10 +39,25 @@
|
|||
"license": "MIT",
|
||||
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
||||
"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",
|
||||
"@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",
|
||||
"flatpickr": "^4.6.6",
|
||||
"loadicons": "^1.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"markdown-it": "^12.0.2",
|
||||
"quill": "^1.3.7",
|
||||
|
|
|
@ -4,6 +4,7 @@ import svelte from "rollup-plugin-svelte"
|
|||
import postcss from "rollup-plugin-postcss"
|
||||
import json from "@rollup/plugin-json"
|
||||
import { terser } from "rollup-plugin-terser"
|
||||
import svg from "rollup-plugin-svg"
|
||||
|
||||
import builtins from "rollup-plugin-node-builtins"
|
||||
|
||||
|
@ -33,5 +34,6 @@ export default {
|
|||
}),
|
||||
commonjs(),
|
||||
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 { 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")
|
||||
|
||||
export let datasource = []
|
||||
export let noRowsMessage = "Feed me some data"
|
||||
|
||||
let rows = []
|
||||
let loaded = false
|
||||
|
||||
$: fetchData(datasource)
|
||||
$: actions = [
|
||||
{
|
||||
type: ActionTypes.RefreshDatasource,
|
||||
callback: () => fetchData(datasource),
|
||||
metadata: { datasource },
|
||||
},
|
||||
]
|
||||
|
||||
async function fetchData(datasource) {
|
||||
if (!isEmpty(datasource)) {
|
||||
|
@ -21,28 +29,38 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div use:styleable={$component.styles}>
|
||||
<Provider {actions}>
|
||||
<div use:styleable={$component.styles}>
|
||||
{#if rows.length > 0}
|
||||
{#if $component.children === 0 && $builderStore.inBuilder}
|
||||
<p>Add some components too</p>
|
||||
<p><i class="ri-image-line" />Add some components to display.</p>
|
||||
{:else}
|
||||
{#each rows as row}
|
||||
<DataProvider {row}>
|
||||
<Provider data={row}>
|
||||
<slot />
|
||||
</DataProvider>
|
||||
</Provider>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if loaded && $builderStore.inBuilder}
|
||||
<p>{noRowsMessage}</p>
|
||||
{:else if loaded && noRowsMessage}
|
||||
<p><i class="ri-list-check-2" />{noRowsMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Provider>
|
||||
|
||||
<style>
|
||||
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;
|
||||
place-items: center;
|
||||
background: #f5f5f5;
|
||||
border: #ccc 1px solid;
|
||||
padding: var(--spacing-m);
|
||||
}
|
||||
p i {
|
||||
margin-bottom: var(--spacing-m);
|
||||
font-size: 1.5rem;
|
||||
color: var(--grey-5);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const ENTER_KEY = 13
|
||||
|
||||
const { authStore, styleable } = getContext("sdk")
|
||||
const { authStore, styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let buttonText = "Log In"
|
||||
|
@ -25,13 +23,16 @@
|
|||
}
|
||||
|
||||
const login = async () => {
|
||||
if ($builderStore.inBuilder) {
|
||||
return
|
||||
}
|
||||
loading = true
|
||||
await authStore.actions.logIn({ email, password })
|
||||
loading = false
|
||||
}
|
||||
|
||||
function handleKeydown(evt) {
|
||||
if (evt.keyCode === ENTER_KEY) {
|
||||
if (evt.key === "Enter") {
|
||||
login()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { authStore, linkable, styleable } = getContext("sdk")
|
||||
const { authStore, linkable, styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let logoUrl
|
||||
|
||||
const logOut = async () => {
|
||||
if ($builderStore.inBuilder) {
|
||||
return
|
||||
}
|
||||
await authStore.actions.logOut()
|
||||
}
|
||||
</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>
|
||||
import { onMount, getContext } from "svelte"
|
||||
|
||||
const { API, screenStore, routeStore, DataProvider, styleable } = getContext(
|
||||
"sdk"
|
||||
)
|
||||
const component = getContext("component")
|
||||
|
||||
export let table
|
||||
|
||||
const {
|
||||
API,
|
||||
screenStore,
|
||||
routeStore,
|
||||
Provider,
|
||||
styleable,
|
||||
ActionTypes,
|
||||
} = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
let headers = []
|
||||
let row
|
||||
|
||||
async function fetchFirstRow() {
|
||||
const rows = await API.fetchTableData(table)
|
||||
return Array.isArray(rows) && rows.length ? rows[0] : { tableId: table }
|
||||
const fetchFirstRow = async tableId => {
|
||||
const rows = await API.fetchTableData(tableId)
|
||||
return Array.isArray(rows) && rows.length ? rows[0] : { tableId }
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
if (!table) {
|
||||
const fetchData = async (rowId, tableId) => {
|
||||
if (!tableId) {
|
||||
return
|
||||
}
|
||||
|
||||
const pathParts = window.location.pathname.split("/")
|
||||
const routeParamId = $routeStore.routeParams.id
|
||||
|
||||
// if srcdoc, then we assume this is the builder preview
|
||||
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && table) {
|
||||
row = await fetchFirstRow()
|
||||
} else if (routeParamId) {
|
||||
row = await API.fetchRow({ tableId: table, rowId: routeParamId })
|
||||
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && tableId) {
|
||||
row = await fetchFirstRow(tableId)
|
||||
} else if (rowId) {
|
||||
row = await API.fetchRow({ tableId, rowId })
|
||||
} else {
|
||||
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>
|
||||
|
||||
{#if row}
|
||||
<Provider data={row} {actions}>
|
||||
<div use:styleable={$component.styles}>
|
||||
<DataProvider {row}>
|
||||
<slot />
|
||||
</DataProvider>
|
||||
</div>
|
||||
</Provider>
|
||||
{/if}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { isEmpty } from "lodash/fp"
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
|
@ -10,10 +9,12 @@
|
|||
Input,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
const { API, styleable, DataProvider, builderStore } = getContext("sdk")
|
||||
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
|
||||
"sdk"
|
||||
)
|
||||
const component = getContext("component")
|
||||
|
||||
export let table = []
|
||||
export let table
|
||||
export let columns = []
|
||||
export let pageSize
|
||||
export let noRowsMessage
|
||||
|
@ -34,12 +35,19 @@
|
|||
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) {
|
||||
if (!isEmpty(table)) {
|
||||
if (table) {
|
||||
const tableDef = await API.fetchTableDefinition(table)
|
||||
schema = tableDef.schema
|
||||
rows = await API.searchTable({
|
||||
rows = await API.searchTableData({
|
||||
tableId: table,
|
||||
search: parsedSearch,
|
||||
pagination: {
|
||||
|
@ -60,7 +68,8 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div use:styleable={$component.styles}>
|
||||
<Provider {actions}>
|
||||
<div use:styleable={$component.styles}>
|
||||
<div class="query-builder">
|
||||
{#if schema}
|
||||
{#each columns as field}
|
||||
|
@ -107,18 +116,16 @@
|
|||
{#if loaded}
|
||||
{#if rows.length > 0}
|
||||
{#if $component.children === 0 && $builderStore.inBuilder}
|
||||
<p>Add some components too</p>
|
||||
<p><i class="ri-image-line" />Add some components to display.</p>
|
||||
{:else}
|
||||
{#each rows as row}
|
||||
<DataProvider {row}>
|
||||
<Provider data={row}>
|
||||
<slot />
|
||||
</DataProvider>
|
||||
</Provider>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if $builderStore.inBuilder}
|
||||
<p>Feed me some data</p>
|
||||
{:else}
|
||||
<p>{noRowsMessage}</p>
|
||||
{:else if noRowsMessage}
|
||||
<p><i class="ri-search-2-line" />{noRowsMessage}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="pagination">
|
||||
|
@ -129,15 +136,24 @@
|
|||
<Button primary on:click={nextPage}>Next</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Provider>
|
||||
|
||||
<style>
|
||||
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;
|
||||
place-items: center;
|
||||
background: #f5f5f5;
|
||||
border: #ccc 1px solid;
|
||||
padding: var(--spacing-m);
|
||||
}
|
||||
p i {
|
||||
margin-bottom: var(--spacing-m);
|
||||
font-size: 1.5rem;
|
||||
color: var(--grey-5);
|
||||
}
|
||||
|
||||
.query-builder {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
const dataContext = getContext("data")
|
||||
|
||||
export let title
|
||||
export let datasource
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
const dataContext = getContext("data")
|
||||
|
||||
export let title
|
||||
export let datasource
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
const dataContext = getContext("data")
|
||||
|
||||
// Common props
|
||||
export let title
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { isEmpty } from "lodash/fp"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
const dataContext = getContext("data")
|
||||
|
||||
export let title
|
||||
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