mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
80 changed files with 1581 additions and 363 deletions
@ -0,0 +1,121 @@ |
|||||
|
/** |
||||
|
* Gets the schema for a datasource which is targeting a JSON array, including |
||||
|
* nested JSON arrays. The returned schema is a squashed, table-like schema |
||||
|
* which is fully compatible with the rest of the platform. |
||||
|
* @param tableSchema the full schema for the table this JSON field is in |
||||
|
* @param datasource the datasource configuration |
||||
|
*/ |
||||
|
export const getJSONArrayDatasourceSchema = (tableSchema, datasource) => { |
||||
|
let jsonSchema = tableSchema |
||||
|
let keysToSchema = [] |
||||
|
|
||||
|
// If we are already deep inside a JSON field then we need to account
|
||||
|
// for the keys that brought us here, so we can get the schema for the
|
||||
|
// depth we're actually at
|
||||
|
if (datasource.prefixKeys) { |
||||
|
keysToSchema = datasource.prefixKeys.concat(["schema"]) |
||||
|
} |
||||
|
|
||||
|
// We parse the label of the datasource to work out where we are inside
|
||||
|
// the structure. We can use this to know which part of the schema
|
||||
|
// is available underneath our current position.
|
||||
|
keysToSchema = keysToSchema.concat(datasource.label.split(".").slice(2)) |
||||
|
|
||||
|
// Follow the JSON key path until we reach the schema for the level
|
||||
|
// we are at
|
||||
|
for (let i = 0; i < keysToSchema.length; i++) { |
||||
|
jsonSchema = jsonSchema?.[keysToSchema[i]] |
||||
|
if (jsonSchema?.schema) { |
||||
|
jsonSchema = jsonSchema.schema |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// We need to convert the JSON schema into a more typical looking table
|
||||
|
// schema so that it works with the rest of the platform
|
||||
|
return convertJSONSchemaToTableSchema(jsonSchema, { |
||||
|
squashObjects: true, |
||||
|
prefixKeys: keysToSchema, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Converts a JSON field schema (or sub-schema of a nested field) into a schema |
||||
|
* that looks like a typical table schema. |
||||
|
* @param jsonSchema the JSON field schema or sub-schema |
||||
|
* @param options |
||||
|
*/ |
||||
|
export const convertJSONSchemaToTableSchema = (jsonSchema, options) => { |
||||
|
if (!jsonSchema) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
// Add default options
|
||||
|
options = { squashObjects: false, prefixKeys: null, ...options } |
||||
|
|
||||
|
// Immediately strip the wrapper schema for objects, or wrap shallow values in
|
||||
|
// a fake "value" schema
|
||||
|
if (jsonSchema.schema) { |
||||
|
jsonSchema = jsonSchema.schema |
||||
|
} else { |
||||
|
jsonSchema = { |
||||
|
value: jsonSchema, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Extract all deep keys from the schema
|
||||
|
const keys = extractJSONSchemaKeys(jsonSchema, options.squashObjects) |
||||
|
|
||||
|
// Form a full schema from all the deep schema keys
|
||||
|
let schema = {} |
||||
|
keys.forEach(({ key, type }) => { |
||||
|
schema[key] = { type, name: key, prefixKeys: options.prefixKeys } |
||||
|
}) |
||||
|
return schema |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Recursively builds paths to all leaf fields in a JSON field schema structure, |
||||
|
* stopping when leaf nodes or arrays are reached. |
||||
|
* @param jsonSchema the JSON field schema or sub-schema |
||||
|
* @param squashObjects whether to recurse into objects or not |
||||
|
*/ |
||||
|
const extractJSONSchemaKeys = (jsonSchema, squashObjects = false) => { |
||||
|
if (!jsonSchema || !Object.keys(jsonSchema).length) { |
||||
|
return [] |
||||
|
} |
||||
|
|
||||
|
// Iterate through every schema key
|
||||
|
let keys = [] |
||||
|
Object.keys(jsonSchema).forEach(key => { |
||||
|
const type = jsonSchema[key].type |
||||
|
|
||||
|
// If we encounter an object, then only go deeper if we want to squash
|
||||
|
// object paths
|
||||
|
if (type === "json" && squashObjects) { |
||||
|
// Find all keys within this objects schema
|
||||
|
const childKeys = extractJSONSchemaKeys( |
||||
|
jsonSchema[key].schema, |
||||
|
squashObjects |
||||
|
) |
||||
|
|
||||
|
// Append child paths onto the current path to build the full path
|
||||
|
keys = keys.concat( |
||||
|
childKeys.map(childKey => ({ |
||||
|
key: `${key}.${childKey.key}`, |
||||
|
type: childKey.type, |
||||
|
})) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Otherwise add this as a lead node.
|
||||
|
// We transform array types from "array" into "jsonarray" here to avoid
|
||||
|
// confusion with the existing "array" type that represents a multi-select.
|
||||
|
else { |
||||
|
keys.push({ |
||||
|
key, |
||||
|
type: type === "array" ? "jsonarray" : type, |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
return keys |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
import { FIELDS } from "constants/backend" |
||||
|
|
||||
|
function baseConversion(type) { |
||||
|
if (type === "string") { |
||||
|
return { |
||||
|
type: FIELDS.STRING.type, |
||||
|
} |
||||
|
} else if (type === "boolean") { |
||||
|
return { |
||||
|
type: FIELDS.BOOLEAN.type, |
||||
|
} |
||||
|
} else if (type === "number") { |
||||
|
return { |
||||
|
type: FIELDS.NUMBER.type, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function recurse(schemaLevel = {}, objectLevel) { |
||||
|
if (!objectLevel) { |
||||
|
return null |
||||
|
} |
||||
|
const baseType = typeof objectLevel |
||||
|
if (baseType !== "object") { |
||||
|
return baseConversion(baseType) |
||||
|
} |
||||
|
for (let [key, value] of Object.entries(objectLevel)) { |
||||
|
const type = typeof value |
||||
|
// check array first, since arrays are objects
|
||||
|
if (Array.isArray(value)) { |
||||
|
const schema = recurse(schemaLevel[key], value[0]) |
||||
|
if (schema) { |
||||
|
schemaLevel[key] = { |
||||
|
type: FIELDS.ARRAY.type, |
||||
|
schema, |
||||
|
} |
||||
|
} |
||||
|
} else if (type === "object") { |
||||
|
const schema = recurse(schemaLevel[key], objectLevel[key]) |
||||
|
if (schema) { |
||||
|
schemaLevel[key] = schema |
||||
|
} |
||||
|
} else { |
||||
|
schemaLevel[key] = baseConversion(type) |
||||
|
} |
||||
|
} |
||||
|
if (!schemaLevel.type) { |
||||
|
return { type: FIELDS.JSON.type, schema: schemaLevel } |
||||
|
} else { |
||||
|
return schemaLevel |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function generate(object) { |
||||
|
return recurse({}, object).schema |
||||
|
} |
||||
@ -0,0 +1,129 @@ |
|||||
|
<script> |
||||
|
import Editor from "components/integration/QueryEditor.svelte" |
||||
|
import { |
||||
|
ModalContent, |
||||
|
Tabs, |
||||
|
Tab, |
||||
|
Button, |
||||
|
Input, |
||||
|
Select, |
||||
|
Body, |
||||
|
Layout, |
||||
|
} from "@budibase/bbui" |
||||
|
import { onMount, createEventDispatcher } from "svelte" |
||||
|
import { FIELDS } from "constants/backend" |
||||
|
import { generate } from "builderStore/schemaGenerator" |
||||
|
|
||||
|
export let schema = {} |
||||
|
export let json |
||||
|
|
||||
|
let dispatcher = createEventDispatcher() |
||||
|
let mode = "Form" |
||||
|
let fieldCount = 0 |
||||
|
let fieldKeys = {}, |
||||
|
fieldTypes = {} |
||||
|
let keyValueOptions = [ |
||||
|
{ label: "String", value: FIELDS.STRING.type }, |
||||
|
{ label: "Number", value: FIELDS.NUMBER.type }, |
||||
|
{ label: "Boolean", value: FIELDS.BOOLEAN.type }, |
||||
|
{ label: "Object", value: FIELDS.JSON.type }, |
||||
|
{ label: "Array", value: FIELDS.ARRAY.type }, |
||||
|
] |
||||
|
let invalid = false |
||||
|
|
||||
|
async function onJsonUpdate({ detail }) { |
||||
|
const input = detail.value |
||||
|
json = input |
||||
|
try { |
||||
|
// check json valid first |
||||
|
let inputJson = JSON.parse(input) |
||||
|
schema = generate(inputJson) |
||||
|
updateCounts() |
||||
|
invalid = false |
||||
|
} catch (err) { |
||||
|
// json not currently valid |
||||
|
invalid = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function updateCounts() { |
||||
|
if (!schema) { |
||||
|
schema = {} |
||||
|
} |
||||
|
let i = 0 |
||||
|
for (let [key, value] of Object.entries(schema)) { |
||||
|
fieldKeys[i] = key |
||||
|
fieldTypes[i] = value.type |
||||
|
i++ |
||||
|
} |
||||
|
fieldCount = i |
||||
|
} |
||||
|
|
||||
|
function saveSchema() { |
||||
|
for (let i of Object.keys(fieldKeys)) { |
||||
|
const key = fieldKeys[i] |
||||
|
// they were added to schema, rather than generated |
||||
|
if (!schema[key]) { |
||||
|
schema[key] = { |
||||
|
type: fieldTypes[i], |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
dispatcher("save", { schema, json }) |
||||
|
} |
||||
|
|
||||
|
onMount(() => { |
||||
|
updateCounts() |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
title={"JSON Schema Editor"} |
||||
|
confirmText="Save Column" |
||||
|
onConfirm={saveSchema} |
||||
|
bind:disabled={invalid} |
||||
|
size="L" |
||||
|
> |
||||
|
<Tabs selected={mode} noPadding> |
||||
|
<Tab title="Form"> |
||||
|
{#each Array(fieldCount) as _, i} |
||||
|
<div class="horizontal"> |
||||
|
<Input outline label="Key" bind:value={fieldKeys[i]} /> |
||||
|
<Select |
||||
|
label="Type" |
||||
|
options={keyValueOptions} |
||||
|
bind:value={fieldTypes[i]} |
||||
|
getOptionValue={field => field.value} |
||||
|
getOptionLabel={field => field.label} |
||||
|
/> |
||||
|
</div> |
||||
|
{/each} |
||||
|
<div class:add-field-btn={fieldCount !== 0}> |
||||
|
<Button primary text on:click={() => fieldCount++}>Add Field</Button> |
||||
|
</div> |
||||
|
</Tab> |
||||
|
<Tab title="JSON"> |
||||
|
<Layout noPadding gap="XS"> |
||||
|
<Body size="S"> |
||||
|
Provide a sample JSON blob here to automatically determine your |
||||
|
schema. |
||||
|
</Body> |
||||
|
<Editor mode="json" on:change={onJsonUpdate} value={json} /> |
||||
|
</Layout> |
||||
|
</Tab> |
||||
|
</Tabs> |
||||
|
</ModalContent> |
||||
|
|
||||
|
<style> |
||||
|
.horizontal { |
||||
|
display: grid; |
||||
|
grid-template-columns: 30% 1fr; |
||||
|
grid-gap: var(--spacing-s); |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.add-field-btn { |
||||
|
margin-top: var(--spacing-xl); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,152 @@ |
|||||
|
<script> |
||||
|
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" |
||||
|
import { store, currentAsset } from "builderStore" |
||||
|
import { tables } from "stores/backend" |
||||
|
import { |
||||
|
getContextProviderComponents, |
||||
|
getSchemaForDatasource, |
||||
|
} from "builderStore/dataBinding" |
||||
|
import SaveFields from "./SaveFields.svelte" |
||||
|
|
||||
|
export let parameters |
||||
|
export let bindings = [] |
||||
|
|
||||
|
$: formComponents = getContextProviderComponents( |
||||
|
$currentAsset, |
||||
|
$store.selectedComponentId, |
||||
|
"form" |
||||
|
) |
||||
|
$: schemaComponents = getContextProviderComponents( |
||||
|
$currentAsset, |
||||
|
$store.selectedComponentId, |
||||
|
"schema" |
||||
|
) |
||||
|
$: providerOptions = getProviderOptions(formComponents, schemaComponents) |
||||
|
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId) |
||||
|
$: tableOptions = $tables.list || [] |
||||
|
|
||||
|
// Gets a context definition of a certain type from a component definition |
||||
|
const extractComponentContext = (component, contextType) => { |
||||
|
const def = store.actions.components.getDefinition(component?._component) |
||||
|
if (!def) { |
||||
|
return null |
||||
|
} |
||||
|
const contexts = Array.isArray(def.context) ? def.context : [def.context] |
||||
|
return contexts.find(context => context?.type === contextType) |
||||
|
} |
||||
|
|
||||
|
// Gets options for valid context keys which provide valid data to submit |
||||
|
const getProviderOptions = (formComponents, schemaComponents) => { |
||||
|
const formContexts = formComponents.map(component => ({ |
||||
|
component, |
||||
|
context: extractComponentContext(component, "form"), |
||||
|
})) |
||||
|
const schemaContexts = schemaComponents.map(component => ({ |
||||
|
component, |
||||
|
context: extractComponentContext(component, "schema"), |
||||
|
})) |
||||
|
const allContexts = formContexts.concat(schemaContexts) |
||||
|
|
||||
|
return allContexts.map(({ component, context }) => { |
||||
|
let runtimeBinding = component._id |
||||
|
if (context.suffix) { |
||||
|
runtimeBinding += `-${context.suffix}` |
||||
|
} |
||||
|
return { |
||||
|
label: component._instanceName, |
||||
|
value: runtimeBinding, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const getSchemaFields = (asset, tableId) => { |
||||
|
const { schema } = getSchemaForDatasource(asset, { type: "table", tableId }) |
||||
|
delete schema._id |
||||
|
delete schema._rev |
||||
|
return Object.values(schema || {}) |
||||
|
} |
||||
|
|
||||
|
const onFieldsChanged = e => { |
||||
|
parameters.fields = e.detail |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="root"> |
||||
|
<Body size="S"> |
||||
|
Choose the data source that provides the row you would like to duplicate. |
||||
|
<br /> |
||||
|
You can always add or override fields manually. |
||||
|
</Body> |
||||
|
|
||||
|
<div class="params"> |
||||
|
<Label small>Data Source</Label> |
||||
|
<Select |
||||
|
bind:value={parameters.providerId} |
||||
|
options={providerOptions} |
||||
|
placeholder="None" |
||||
|
/> |
||||
|
|
||||
|
<Label small>Duplicate to Table</Label> |
||||
|
<Select |
||||
|
bind:value={parameters.tableId} |
||||
|
options={tableOptions} |
||||
|
getOptionLabel={option => option.name} |
||||
|
getOptionValue={option => option._id} |
||||
|
/> |
||||
|
|
||||
|
<Label small /> |
||||
|
<Checkbox text="Require confirmation" bind:value={parameters.confirm} /> |
||||
|
|
||||
|
{#if parameters.confirm} |
||||
|
<Label small>Confirm text</Label> |
||||
|
<Input |
||||
|
placeholder="Are you sure you want to duplicate this row?" |
||||
|
bind:value={parameters.confirmText} |
||||
|
/> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
{#if parameters.tableId} |
||||
|
<div class="fields"> |
||||
|
<SaveFields |
||||
|
parameterFields={parameters.fields} |
||||
|
{schemaFields} |
||||
|
on:change={onFieldsChanged} |
||||
|
{bindings} |
||||
|
/> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.root { |
||||
|
width: 100%; |
||||
|
max-width: 800px; |
||||
|
margin: 0 auto; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: flex-start; |
||||
|
align-items: stretch; |
||||
|
gap: var(--spacing-xl); |
||||
|
} |
||||
|
|
||||
|
.root :global(p) { |
||||
|
line-height: 1.5; |
||||
|
} |
||||
|
|
||||
|
.params { |
||||
|
display: grid; |
||||
|
column-gap: var(--spacing-l); |
||||
|
row-gap: var(--spacing-s); |
||||
|
grid-template-columns: 100px 1fr; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.fields { |
||||
|
display: grid; |
||||
|
column-gap: var(--spacing-l); |
||||
|
row-gap: var(--spacing-s); |
||||
|
grid-template-columns: 100px 1fr auto 1fr auto; |
||||
|
align-items: center; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,13 @@ |
|||||
|
export { default as NavigateTo } from "./NavigateTo.svelte" |
||||
|
export { default as SaveRow } from "./SaveRow.svelte" |
||||
|
export { default as DeleteRow } from "./DeleteRow.svelte" |
||||
|
export { default as ExecuteQuery } from "./ExecuteQuery.svelte" |
||||
|
export { default as TriggerAutomation } from "./TriggerAutomation.svelte" |
||||
|
export { default as ValidateForm } from "./ValidateForm.svelte" |
||||
|
export { default as LogOut } from "./LogOut.svelte" |
||||
|
export { default as ClearForm } from "./ClearForm.svelte" |
||||
|
export { default as CloseScreenModal } from "./CloseScreenModal.svelte" |
||||
|
export { default as ChangeFormStep } from "./ChangeFormStep.svelte" |
||||
|
export { default as UpdateState } from "./UpdateState.svelte" |
||||
|
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte" |
||||
|
export { default as DuplicateRow } from "./DuplicateRow.svelte" |
||||
@ -0,0 +1,33 @@ |
|||||
|
import * as ActionComponents from "./actions" |
||||
|
import { get } from "svelte/store" |
||||
|
import { store } from "builderStore" |
||||
|
import ActionDefinitions from "./manifest.json" |
||||
|
|
||||
|
// Defines which actions are available to configure in the front end.
|
||||
|
// Unfortunately the "name" property is used as the identifier so please don't
|
||||
|
// change them.
|
||||
|
// The client library removes any spaces when processing actions, so they can
|
||||
|
// be considered as camel case too.
|
||||
|
// There is technical debt here to sanitize all these and standardise them
|
||||
|
// across the packages but it's a breaking change to existing apps.
|
||||
|
export const getAvailableActions = (getAllActions = false) => { |
||||
|
return ActionDefinitions.actions |
||||
|
.filter(action => { |
||||
|
// Filter down actions to those supported by the current client lib version
|
||||
|
if (getAllActions || !action.dependsOnFeature) { |
||||
|
return true |
||||
|
} |
||||
|
return get(store).clientFeatures?.[action.dependsOnFeature] === true |
||||
|
}) |
||||
|
.map(action => { |
||||
|
// Then enrich the actions with real components
|
||||
|
return { |
||||
|
...action, |
||||
|
component: ActionComponents[action.component], |
||||
|
} |
||||
|
}) |
||||
|
.filter(action => { |
||||
|
// Then strip any old actions for which we don't have constructors
|
||||
|
return action.component != null |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
{ |
||||
|
"actions": [ |
||||
|
{ |
||||
|
"name": "Save Row", |
||||
|
"component": "SaveRow", |
||||
|
"context": [ |
||||
|
{ |
||||
|
"label": "Saved row", |
||||
|
"value": "row" |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Duplicate Row", |
||||
|
"component": "DuplicateRow", |
||||
|
"context": [ |
||||
|
{ |
||||
|
"label": "Duplicated row", |
||||
|
"value": "row" |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Delete Row", |
||||
|
"component": "DeleteRow" |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Navigate To", |
||||
|
"component": "NavigateTo" |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Execute Query", |
||||
|
"component": "ExecuteQuery", |
||||
|
"context": [ |
||||
|
{ |
||||
|
"label": "Query result", |
||||
|
"value": "result" |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Trigger Automation", |
||||
|
"component": "TriggerAutomation" |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Validate Form", |
||||
|
"component": "ValidateForm" |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Log Out", |
||||
|
"component": "LogOut" |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Clear Form", |
||||
|
"component": "ClearForm" |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Close Screen Modal", |
||||
|
"component": "CloseScreenModal" |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Change Form Step", |
||||
|
"component": "ChangeFormStep" |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Refresh Data Provider", |
||||
|
"component": "RefreshDataProvider" |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Update State", |
||||
|
"component": "UpdateState", |
||||
|
"dependsOnFeature": "state" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -1,80 +0,0 @@ |
|||||
import { store } from "builderStore" |
|
||||
import { get } from "svelte/store" |
|
||||
|
|
||||
import NavigateTo from "./NavigateTo.svelte" |
|
||||
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 LogOut from "./LogOut.svelte" |
|
||||
import ClearForm from "./ClearForm.svelte" |
|
||||
import CloseScreenModal from "./CloseScreenModal.svelte" |
|
||||
import ChangeFormStep from "./ChangeFormStep.svelte" |
|
||||
import UpdateStateStep from "./UpdateState.svelte" |
|
||||
import RefreshDataProvider from "./RefreshDataProvider.svelte" |
|
||||
|
|
||||
// Defines which actions are available to configure in the front end.
|
|
||||
// Unfortunately the "name" property is used as the identifier so please don't
|
|
||||
// change them.
|
|
||||
// The client library removes any spaces when processing actions, so they can
|
|
||||
// be considered as camel case too.
|
|
||||
// There is technical debt here to sanitize all these and standardise them
|
|
||||
// across the packages but it's a breaking change to existing apps.
|
|
||||
export const getAvailableActions = () => { |
|
||||
let actions = [ |
|
||||
{ |
|
||||
name: "Save Row", |
|
||||
component: SaveRow, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Delete Row", |
|
||||
component: DeleteRow, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Navigate To", |
|
||||
component: NavigateTo, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Execute Query", |
|
||||
component: ExecuteQuery, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Trigger Automation", |
|
||||
component: TriggerAutomation, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Validate Form", |
|
||||
component: ValidateForm, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Log Out", |
|
||||
component: LogOut, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Clear Form", |
|
||||
component: ClearForm, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Close Screen Modal", |
|
||||
component: CloseScreenModal, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Change Form Step", |
|
||||
component: ChangeFormStep, |
|
||||
}, |
|
||||
{ |
|
||||
name: "Refresh Data Provider", |
|
||||
component: RefreshDataProvider, |
|
||||
}, |
|
||||
] |
|
||||
|
|
||||
if (get(store).clientFeatures?.state) { |
|
||||
actions.push({ |
|
||||
name: "Update State", |
|
||||
component: UpdateStateStep, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
return actions |
|
||||
} |
|
||||
@ -1,2 +0,0 @@ |
|||||
import EventsEditor from "./EventPropertyControl.svelte" |
|
||||
export default EventsEditor |
|
||||
@ -0,0 +1,59 @@ |
|||||
|
<script> |
||||
|
import "@spectrum-css/tag/dist/index-vars.css" |
||||
|
import { ClearButton } from "@budibase/bbui" |
||||
|
import { getContext } from "svelte" |
||||
|
|
||||
|
export let onClick |
||||
|
export let text = "" |
||||
|
export let color |
||||
|
export let closable = false |
||||
|
export let size = "M" |
||||
|
|
||||
|
const component = getContext("component") |
||||
|
const { styleable, builderStore } = getContext("sdk") |
||||
|
|
||||
|
// Add color styles to main styles object, otherwise the styleable helper |
||||
|
// overrides the color when it's passed as inline style. |
||||
|
$: styles = enrichStyles($component.styles, color) |
||||
|
$: componentText = getComponentText(text, $builderStore, $component) |
||||
|
|
||||
|
const getComponentText = (text, builderState, componentState) => { |
||||
|
if (!builderState.inBuilder || componentState.editing) { |
||||
|
return text || " " |
||||
|
} |
||||
|
return text || componentState.name || "Placeholder text" |
||||
|
} |
||||
|
|
||||
|
const enrichStyles = (styles, color) => { |
||||
|
if (!color) { |
||||
|
return styles |
||||
|
} |
||||
|
return { |
||||
|
...styles, |
||||
|
normal: { |
||||
|
...styles?.normal, |
||||
|
"background-color": color, |
||||
|
"border-color": color, |
||||
|
color: "white", |
||||
|
"--spectrum-clearbutton-medium-icon-color": "white", |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="spectrum-Tag spectrum-Tag--size{size}" use:styleable={styles}> |
||||
|
<span class="spectrum-Tag-label">{componentText}</span> |
||||
|
{#if closable} |
||||
|
<ClearButton on:click={onClick} /> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.spectrum-Tag--sizeS, |
||||
|
.spectrum-Tag--sizeM { |
||||
|
padding: 0 var(--spectrum-global-dimension-size-100); |
||||
|
} |
||||
|
.spectrum-Tag--sizeL { |
||||
|
padding: 0 var(--spectrum-global-dimension-size-150); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,71 @@ |
|||||
|
<script> |
||||
|
import { CoreTextArea } from "@budibase/bbui" |
||||
|
import Field from "./Field.svelte" |
||||
|
import { getContext } from "svelte" |
||||
|
|
||||
|
export let field |
||||
|
export let label |
||||
|
export let placeholder |
||||
|
export let disabled = false |
||||
|
export let defaultValue = "" |
||||
|
|
||||
|
const component = getContext("component") |
||||
|
const validation = [ |
||||
|
{ |
||||
|
constraint: "json", |
||||
|
type: "json", |
||||
|
error: "JSON syntax is invalid", |
||||
|
}, |
||||
|
] |
||||
|
let fieldState |
||||
|
let fieldApi |
||||
|
|
||||
|
$: height = $component.styles?.normal?.height || "124px" |
||||
|
|
||||
|
const serialiseValue = value => { |
||||
|
return JSON.stringify(value || undefined, null, 4) || "" |
||||
|
} |
||||
|
|
||||
|
const parseValue = value => { |
||||
|
try { |
||||
|
return JSON.parse(value) |
||||
|
} catch (error) { |
||||
|
return value |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Field |
||||
|
{label} |
||||
|
{field} |
||||
|
{disabled} |
||||
|
{validation} |
||||
|
{defaultValue} |
||||
|
type="json" |
||||
|
bind:fieldState |
||||
|
bind:fieldApi |
||||
|
> |
||||
|
{#if fieldState} |
||||
|
<div style="--height: {height};"> |
||||
|
<CoreTextArea |
||||
|
value={serialiseValue(fieldState.value)} |
||||
|
on:change={e => fieldApi.setValue(parseValue(e.detail))} |
||||
|
disabled={fieldState.disabled} |
||||
|
error={fieldState.error} |
||||
|
id={fieldState.fieldId} |
||||
|
{placeholder} |
||||
|
/> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</Field> |
||||
|
|
||||
|
<style> |
||||
|
:global(.spectrum-Form-itemField .spectrum-Textfield--multiline) { |
||||
|
min-height: calc(var(--height) - 24px); |
||||
|
} |
||||
|
:global(.spectrum-Form--labelsAbove |
||||
|
.spectrum-Form-itemField |
||||
|
.spectrum-Textfield--multiline) { |
||||
|
min-height: calc(var(--height) - 24px); |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue