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