1
0
Fork 0
mirror of synced 2024-07-03 05:20:32 +12:00

Merge pull request #1338 from Budibase/datasource-refactor

Datasource Refactor + Spectrum Table
This commit is contained in:
Andrew Kingston 2021-04-01 19:02:39 +01:00 committed by GitHub
commit 6851f59eea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 1420 additions and 1458 deletions

View file

@ -20,7 +20,7 @@ context("Create Bindings", () => {
cy.get("[data-cy=setting-text] input") cy.get("[data-cy=setting-text] input")
.type("{{}{{}{{} Current User._id {}}{}}") .type("{{}{{}{{} Current User._id {}}{}}")
.blur() .blur()
cy.getComponent(componentId).should("have.text", "{{{ user._id }}") cy.getComponent(componentId).should("have.text", "{{{ [user].[_id] }}")
}) })
}) })

View file

@ -43,7 +43,7 @@ context("Create Components", () => {
it("should create a form and reset to match schema", () => { it("should create a form and reset to match schema", () => {
cy.addComponent("Form", "Form").then(() => { cy.addComponent("Form", "Form").then(() => {
cy.get("[data-cy=Settings]").click() cy.get("[data-cy=Settings]").click()
cy.get("[data-cy=setting-datasource]") cy.get("[data-cy=setting-dataSource]")
.contains("Choose option") .contains("Choose option")
.click() .click()
cy.get(".dropdown") cy.get(".dropdown")

View file

@ -1,11 +1,11 @@
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store" import { get } from "svelte/store"
import { findComponent, findComponentPath } from "./storeUtils"
import { store } from "builderStore" import { store } from "builderStore"
import { import {
tables as tablesStore, tables as tablesStore,
queries as queriesStores, queries as queriesStores,
} from "stores/backend/" } from "stores/backend/"
import { findComponentPath } from "./storeUtils"
import { makePropSafe } from "@budibase/string-templates" import { makePropSafe } from "@budibase/string-templates"
import { TableNames } from "../constants" import { TableNames } from "../constants"
@ -39,7 +39,7 @@ export const getDataProviderComponents = (asset, componentId) => {
// Filter by only data provider components // Filter by only data provider components
return path.filter(component => { return path.filter(component => {
const def = store.actions.components.getDefinition(component._component) const def = store.actions.components.getDefinition(component._component)
return def?.dataProvider return def?.context != null
}) })
} }
@ -66,14 +66,25 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
/** /**
* Gets a datasource object for a certain data provider component * Gets a datasource object for a certain data provider component
*/ */
export const getDatasourceForProvider = component => { export const getDatasourceForProvider = (asset, component) => {
const def = store.actions.components.getDefinition(component?._component) const def = store.actions.components.getDefinition(component?._component)
if (!def) { if (!def) {
return null return null
} }
// If this component has a dataProvider setting, go up the stack and use it
const dataProviderSetting = def.settings.find(setting => {
return setting.type === "dataProvider"
})
if (dataProviderSetting) {
const settingValue = component[dataProviderSetting.key]
const providerId = extractLiteralHandlebarsID(settingValue)
const provider = findComponent(asset.props, providerId)
return getDatasourceForProvider(asset, provider)
}
// Extract datasource from component instance // Extract datasource from component instance
const validSettingTypes = ["datasource", "table", "schema"] const validSettingTypes = ["dataSource", "table", "schema"]
const datasourceSetting = def.settings.find(setting => { const datasourceSetting = def.settings.find(setting => {
return validSettingTypes.includes(setting.type) return validSettingTypes.includes(setting.type)
}) })
@ -105,53 +116,68 @@ const getContextBindings = (asset, componentId) => {
// Create bindings for each data provider // Create bindings for each data provider
dataProviders.forEach(component => { dataProviders.forEach(component => {
const isForm = component._component.endsWith("/form") const def = store.actions.components.getDefinition(component._component)
const datasource = getDatasourceForProvider(component) const contextDefinition = def.context
let tableName, schema let schema
let readablePrefix
// Forms are an edge case which do not need table schemas if (contextDefinition.type === "form") {
if (isForm) { // Forms do not need table schemas
// Their schemas are built from their component field names
schema = buildFormSchema(component) schema = buildFormSchema(component)
tableName = "Fields" readablePrefix = "Fields"
} else { } else if (contextDefinition.type === "static") {
// Static contexts are fully defined by the components
schema = {}
const values = contextDefinition.values || []
values.forEach(value => {
schema[value.key] = { name: value.label, type: "string" }
})
} else if (contextDefinition.type === "schema") {
// Schema contexts are generated dynamically depending on their data
const datasource = getDatasourceForProvider(asset, component)
if (!datasource) { if (!datasource) {
return return
} }
const info = getSchemaForDatasource(datasource)
// Get schema and table for the datasource
const info = getSchemaForDatasource(datasource, isForm)
schema = info.schema schema = info.schema
tableName = info.table?.name readablePrefix = info.table?.name
// Add _id and _rev fields for certain types
if (schema && ["table", "link"].includes(datasource.type)) {
schema["_id"] = { type: "string" }
schema["_rev"] = { type: "string" }
}
} }
if (!schema || !tableName) { if (!schema) {
return return
} }
const keys = Object.keys(schema).sort() const keys = Object.keys(schema).sort()
// Create bindable properties for each schema field // Create bindable properties for each schema field
const safeComponentId = makePropSafe(component._id)
keys.forEach(key => { keys.forEach(key => {
const fieldSchema = schema[key] const fieldSchema = schema[key]
// Replace certain bindings with a new property to help display components
// Make safe runtime binding and replace certain bindings with a
// new property to help display components
let runtimeBoundKey = key let runtimeBoundKey = key
if (fieldSchema.type === "link") { if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_text` runtimeBoundKey = `${key}_text`
} else if (fieldSchema.type === "attachment") { } else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first` runtimeBoundKey = `${key}_first`
} }
const runtimeBinding = `${safeComponentId}.${makePropSafe(
runtimeBoundKey
)}`
// Optionally use a prefix with readable bindings
let readableBinding = component._instanceName
if (readablePrefix) {
readableBinding += `.${readablePrefix}`
}
readableBinding += `.${fieldSchema.name || key}`
// Create the binding object
bindings.push({ bindings.push({
type: "context", type: "context",
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe( runtimeBinding,
runtimeBoundKey readableBinding,
)}`,
readableBinding: `${component._instanceName}.${tableName}.${key}`,
// Field schema and provider are required to construct relationship // Field schema and provider are required to construct relationship
// datasource options, based on bindable properties // datasource options, based on bindable properties
fieldSchema, fieldSchema,
@ -168,14 +194,12 @@ const getContextBindings = (asset, componentId) => {
*/ */
const getUserBindings = () => { const getUserBindings = () => {
let bindings = [] let bindings = []
const tables = get(tablesStore).list const { schema } = getSchemaForDatasource({
const userTable = tables.find(table => table._id === TableNames.USERS) type: "table",
const schema = { tableId: TableNames.USERS,
...userTable.schema, })
_id: { type: "string" },
_rev: { type: "string" },
}
const keys = Object.keys(schema).sort() const keys = Object.keys(schema).sort()
const safeUser = makePropSafe("user")
keys.forEach(key => { keys.forEach(key => {
const fieldSchema = schema[key] const fieldSchema = schema[key]
// Replace certain bindings with a new property to help display components // Replace certain bindings with a new property to help display components
@ -188,7 +212,7 @@ const getUserBindings = () => {
bindings.push({ bindings.push({
type: "context", type: "context",
runtimeBinding: `user.${runtimeBoundKey}`, runtimeBinding: `${safeUser}.${makePropSafe(runtimeBoundKey)}`,
readableBinding: `Current User.${key}`, readableBinding: `Current User.${key}`,
// Field schema and provider are required to construct relationship // Field schema and provider are required to construct relationship
// datasource options, based on bindable properties // datasource options, based on bindable properties
@ -212,9 +236,10 @@ const getUrlBindings = asset => {
params.push(part.replace(/:/g, "").replace(/\?/g, "")) params.push(part.replace(/:/g, "").replace(/\?/g, ""))
} }
}) })
const safeURL = makePropSafe("url")
return params.map(param => ({ return params.map(param => ({
type: "context", type: "context",
runtimeBinding: `url.${param}`, runtimeBinding: `${safeURL}.${makePropSafe(param)}`,
readableBinding: `URL.${param}`, readableBinding: `URL.${param}`,
})) }))
} }
@ -236,15 +261,6 @@ export const getSchemaForDatasource = (datasource, isForm = false) => {
if (table) { if (table) {
if (type === "view") { if (type === "view") {
schema = cloneDeep(table.views?.[datasource.name]?.schema) schema = cloneDeep(table.views?.[datasource.name]?.schema)
// Some calc views don't include a "name" property inside the schema
if (schema) {
Object.keys(schema).forEach(field => {
if (!schema[field].name) {
schema[field].name = field
}
})
}
} else if (type === "query" && isForm) { } else if (type === "query" && isForm) {
schema = {} schema = {}
const params = table.parameters || [] const params = table.parameters || []
@ -257,6 +273,21 @@ export const getSchemaForDatasource = (datasource, isForm = false) => {
schema = cloneDeep(table.schema) schema = cloneDeep(table.schema)
} }
} }
// Add _id and _rev fields for certain types
if (schema && !isForm && ["table", "link"].includes(datasource.type)) {
schema["_id"] = { type: "string" }
schema["_rev"] = { type: "string" }
}
// Ensure there are "name" properties for all fields
if (schema) {
Object.keys(schema).forEach(field => {
if (!schema[field].name) {
schema[field].name = field
}
})
}
} }
return { schema, table } return { schema, table }
} }
@ -277,7 +308,7 @@ const buildFormSchema = component => {
if (fieldSetting && component.field) { if (fieldSetting && component.field) {
const type = fieldSetting.type.split("field/")[1] const type = fieldSetting.type.split("field/")[1]
if (type) { if (type) {
schema[component.field] = { name: component.field, type } schema[component.field] = { type }
} }
} }
component._children?.forEach(child => { component._children?.forEach(child => {
@ -330,6 +361,14 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
return result return result
} }
/**
* Extracts a component ID from a handlebars expression setting of
* {{ literal [componentId] }}
*/
function extractLiteralHandlebarsID(value) {
return value?.match(/{{\s*literal[\s[]+([a-fA-F0-9]+)[\s\]]*}}/)?.[1]
}
/** /**
* Converts a readable data binding into a runtime data binding * Converts a readable data binding into a runtime data binding
*/ */

View file

@ -37,7 +37,7 @@ const createScreen = table => {
.customProps({ .customProps({
theme: "spectrum--lightest", theme: "spectrum--lightest",
size: "spectrum--medium", size: "spectrum--medium",
datasource: { dataSource: {
label: table.name, label: table.name,
tableId: table._id, tableId: table._id,
type: "table", type: "table",

View file

@ -25,7 +25,7 @@ export default function(tables) {
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE" export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`) export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
function generateTitleContainer(table, title, formId) { function generateTitleContainer(table, title, formId, repeaterId) {
// have to override style for this, its missing margin // have to override style for this, its missing margin
const saveButton = makeSaveButton(table, formId).normalStyle({ const saveButton = makeSaveButton(table, formId).normalStyle({
background: "#000000", background: "#000000",
@ -61,10 +61,9 @@ function generateTitleContainer(table, title, formId) {
onClick: [ onClick: [
{ {
parameters: { parameters: {
providerId: formId,
rowId: `{{ ${makePropSafe(formId)}._id }}`,
revId: `{{ ${makePropSafe(formId)}._rev }}`,
tableId: table._id, tableId: table._id,
rowId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_id")} }}`,
revId: `{{ ${makePropSafe(repeaterId)}.${makePropSafe("_rev")} }}`,
}, },
"##eventHandlerType": "Delete Row", "##eventHandlerType": "Delete Row",
}, },
@ -84,18 +83,33 @@ function generateTitleContainer(table, title, formId) {
} }
const createScreen = table => { const createScreen = table => {
const screen = new Screen() const provider = new Component("@budibase/standard-components/dataprovider")
.component("@budibase/standard-components/rowdetail") .instanceName(`Data Provider`)
.table(table._id) .customProps({
.instanceName(`${table.name} - Detail`) dataSource: {
.route(rowDetailUrl(table)) label: table.name,
name: `all_${table._id}`,
tableId: table._id,
type: "table",
},
filter: {
_id: `{{ ${makePropSafe("url")}.${makePropSafe("id")} }}`,
},
limit: 1,
})
const repeater = new Component("@budibase/standard-components/repeater")
.instanceName("Repeater")
.customProps({
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
})
const form = makeMainForm() const form = makeMainForm()
.instanceName("Form") .instanceName("Form")
.customProps({ .customProps({
theme: "spectrum--lightest", theme: "spectrum--lightest",
size: "spectrum--medium", size: "spectrum--medium",
datasource: { dataSource: {
label: table.name, label: table.name,
tableId: table._id, tableId: table._id,
type: "table", type: "table",
@ -116,14 +130,24 @@ const createScreen = table => {
// Add all children to the form // Add all children to the form
const formId = form._json._id const formId = form._json._id
const rowDetailId = screen._json.props._id const repeaterId = repeater._json._id
const heading = table.primaryDisplay const heading = table.primaryDisplay
? `{{ ${makePropSafe(rowDetailId)}.${makePropSafe(table.primaryDisplay)} }}` ? `{{ ${makePropSafe(repeaterId)}.${makePropSafe(table.primaryDisplay)} }}`
: null : null
form form
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit")) .addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
.addChild(generateTitleContainer(table, heading || "Edit Row", formId)) .addChild(
generateTitleContainer(table, heading || "Edit Row", formId, repeaterId)
)
.addChild(fieldGroup) .addChild(fieldGroup)
return screen.addChild(form).json() repeater.addChild(form)
provider.addChild(repeater)
return new Screen()
.component("@budibase/standard-components/container")
.instanceName(`${table.name} - Detail`)
.route(rowDetailUrl(table))
.addChild(provider)
.json()
} }

View file

@ -2,6 +2,7 @@ import sanitizeUrl from "./utils/sanitizeUrl"
import { newRowUrl } from "./newRowScreen" import { newRowUrl } from "./newRowScreen"
import { Screen } from "./utils/Screen" import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component" import { Component } from "./utils/Component"
import { makePropSafe } from "@budibase/string-templates"
export default function(tables) { export default function(tables) {
return tables.map(table => { return tables.map(table => {
@ -70,21 +71,56 @@ function generateTitleContainer(table) {
} }
const createScreen = table => { const createScreen = table => {
const datagrid = new Component("@budibase/standard-components/datagrid") const provider = new Component("@budibase/standard-components/dataprovider")
.instanceName(`Data Provider`)
.customProps({ .customProps({
datasource: { dataSource: {
label: table.name, label: table.name,
name: `all_${table._id}`, name: `all_${table._id}`,
tableId: table._id, tableId: table._id,
type: "table", type: "table",
}, },
editable: false,
theme: "alpine",
height: "540",
pagination: true,
detailUrl: `${rowListUrl(table)}/:id`,
}) })
.instanceName("Grid")
const spectrumTable = new Component("@budibase/standard-components/table")
.customProps({
dataProvider: `{{ literal ${makePropSafe(provider._json._id)} }}`,
theme: "spectrum--lightest",
showAutoColumns: false,
quiet: false,
size: "spectrum--medium",
rowCount: 8,
})
.instanceName(`${table.name} Table`)
const safeTableId = makePropSafe(spectrumTable._json._id)
const safeRowId = makePropSafe("_id")
const viewButton = new Component("@budibase/standard-components/button")
.customProps({
text: "View",
onClick: [
{
"##eventHandlerType": "Navigate To",
parameters: {
url: `${rowListUrl(table)}/{{ ${safeTableId}.${safeRowId} }}`,
},
},
],
})
.instanceName("View Button")
.normalStyle({
background: "transparent",
"font-family": "Inter, sans-serif",
"font-weight": "500",
color: "#888",
"border-width": "0",
})
.hoverStyle({
color: "#4285f4",
})
spectrumTable.addChild(viewButton)
provider.addChild(spectrumTable)
const mainContainer = new Component("@budibase/standard-components/container") const mainContainer = new Component("@budibase/standard-components/container")
.normalStyle({ .normalStyle({
@ -105,14 +141,12 @@ const createScreen = table => {
.type("div") .type("div")
.instanceName("Container") .instanceName("Container")
.addChild(generateTitleContainer(table)) .addChild(generateTitleContainer(table))
.addChild(datagrid) .addChild(provider)
return new Screen() return new Screen()
.component("@budibase/standard-components/container") .component("@budibase/standard-components/container")
.mainType("div")
.route(rowListUrl(table)) .route(rowListUrl(table))
.instanceName(`${table.name} - List`) .instanceName(`${table.name} - List`)
.name("")
.addChild(mainContainer) .addChild(mainContainer)
.json() .json()
} }

View file

@ -119,6 +119,7 @@ export function makeSaveButton(table, formId) {
{ {
parameters: { parameters: {
providerId: formId, providerId: formId,
tableId: table._id,
}, },
"##eventHandlerType": "Save Row", "##eventHandlerType": "Save Row",
}, },

View file

@ -1,7 +1,8 @@
[ [
"container", "container",
"datagrid", "dataprovider",
"list", "table",
"repeater",
"button", "button",
"search", "search",
{ {
@ -62,8 +63,7 @@
"children": [ "children": [
"screenslot", "screenslot",
"navigation", "navigation",
"login", "login"
"rowdetail"
] ]
} }
] ]

View file

@ -0,0 +1,25 @@
<script>
import { Select } from "@budibase/bbui"
import { makePropSafe } from "@budibase/string-templates"
import { currentAsset, store } from "builderStore"
import { findComponentPath } from "builderStore/storeUtils"
export let value
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
$: providers = path.filter(
component =>
component._component === "@budibase/standard-components/dataprovider"
)
</script>
<Select thin secondary {value} on:change>
<option value="">Choose option</option>
{#if providers}
{#each providers as component}
<option value={`{{ literal ${makePropSafe(component._id)} }}`}>
{component._instanceName}
</option>
{/each}
{/if}
</Select>

View file

@ -1,50 +1,38 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { import { tables } from "stores/backend"
getDataProviderComponents, import { getBindableProperties } from "builderStore/dataBinding"
getDatasourceForProvider, import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
getSchemaForDatasource,
} from "builderStore/dataBinding"
export let parameters export let parameters
$: dataProviderComponents = getDataProviderComponents( $: tableOptions = $tables || []
$currentAsset, $: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
$store.selectedComponentId
)
$: {
// Automatically set rev and table ID based on row ID
if (parameters.providerId) {
parameters.rowId = `{{ ${parameters.providerId}._id }}`
parameters.revId = `{{ ${parameters.providerId}._rev }}`
const providerComponent = dataProviderComponents.find(
provider => provider._id === parameters.providerId
)
const datasource = getDatasourceForProvider(providerComponent)
const { table } = getSchemaForDatasource(datasource)
if (table) {
parameters.tableId = table._id
}
}
}
</script> </script>
<div class="root"> <div class="root">
{#if dataProviderComponents.length === 0} <Label small>Table</Label>
<div class="cannot-use"> <Select thin secondary bind:value={parameters.tableId}>
Delete row can only be used within a component that provides data, such as <option value="" />
a List {#each tableOptions as table}
</div> <option value={table._id}>{table.name}</option>
{:else} {/each}
<Label small>Datasource</Label> </Select>
<Select thin secondary bind:value={parameters.providerId}>
<option value="" /> <Label small>Row ID</Label>
{#each dataProviderComponents as provider} <DrawerBindableInput
<option value={provider._id}>{provider._instanceName}</option> {bindings}
{/each} title="Row ID to delete"
</Select> value={parameters.rowId}
{/if} on:change={value => (parameters.rowId = value.detail)} />
<Label small>Row Rev</Label>
<DrawerBindableInput
{bindings}
title="Row rev to delete"
value={parameters.revId}
on:change={value => (parameters.revId = value.detail)} />
</div> </div>
<style> <style>
@ -54,11 +42,7 @@
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
align-items: baseline; align-items: baseline;
} max-width: 800px;
margin: 0 auto;
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
margin: auto;
} }
</style> </style>

View file

@ -1,7 +1,7 @@
<script> <script>
import { Select, Label, Spacer } from "@budibase/bbui" import { Select, Label, Spacer } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { datasources, integrations, queries } from 'stores/backend/' import { datasources, integrations, queries } from "stores/backend/"
import { getBindableProperties } from "builderStore/dataBinding" import { getBindableProperties } from "builderStore/dataBinding"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte" import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
@ -18,9 +18,8 @@
) )
function fetchQueryDefinition(query) { function fetchQueryDefinition(query) {
const source = $datasources.list.find( const source = $datasources.list.find(ds => ds._id === query.datasourceId)
ds => ds._id === query.datasourceId .source
).source
return $integrations[source].query[query.queryVerb] return $integrations[source].query[query.queryVerb]
} }
</script> </script>

View file

@ -30,7 +30,9 @@
display: grid; display: grid;
column-gap: var(--spacing-l); column-gap: var(--spacing-l);
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr; grid-template-columns: auto 1fr;
align-items: baseline; align-items: baseline;
max-width: 800px;
margin: 0 auto;
} }
</style> </style>

View file

@ -1,7 +1,14 @@
<div class="root">This action doesn't require any additional settings.</div> <script>
import { Body } from "@budibase/bbui"
</script>
<div class="root">
<Body small grey>This action doesn't require any additional settings.</Body>
</div>
<style> <style>
.root { .root {
font-size: var(--font-size-s); max-width: 800px;
margin: 0 auto;
} }
</style> </style>

View file

@ -27,6 +27,8 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: baseline; align-items: baseline;
max-width: 800px;
margin: 0 auto;
} }
.root :global(> div) { .root :global(> div) {

View file

@ -1,9 +1,9 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label, Body } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables } from "stores/backend"
import { import {
getDataProviderComponents, getDataProviderComponents,
getDatasourceForProvider,
getSchemaForDatasource, getSchemaForDatasource,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
@ -14,14 +14,11 @@
$currentAsset, $currentAsset,
$store.selectedComponentId $store.selectedComponentId
) )
$: providerComponent = dataProviderComponents.find( $: schemaFields = getSchemaFields(parameters?.tableId)
provider => provider._id === parameters.providerId $: tableOptions = $tables || []
)
$: schemaFields = getSchemaFields(providerComponent)
const getSchemaFields = component => { const getSchemaFields = tableId => {
const datasource = getDatasourceForProvider(component) const { schema } = getSchemaForDatasource({ type: "table", tableId })
const { schema } = getSchemaForDatasource(datasource)
return Object.values(schema || {}) return Object.values(schema || {})
} }
@ -31,31 +28,48 @@
</script> </script>
<div class="root"> <div class="root">
{#if !dataProviderComponents.length} <Body small grey>
<div class="cannot-use"> Choosing a Data Source will automatically use the data it provides, but it's
Save Row can only be used within a component that provides data, such as a optional.<br />
Repeater You can always add or override fields manually.
</div> </Body>
{:else} <div class="fields">
<Label small>Datasource</Label> <Label small>Data Source</Label>
<Select thin secondary bind:value={parameters.providerId}> <Select thin secondary bind:value={parameters.providerId}>
<option value="" /> <option value="">None</option>
{#each dataProviderComponents as provider} {#each dataProviderComponents as provider}
<option value={provider._id}>{provider._instanceName}</option> <option value={provider._id}>{provider._instanceName}</option>
{/each} {/each}
</Select> </Select>
{#if parameters.providerId} <Label small>Table</Label>
<Select thin secondary bind:value={parameters.tableId}>
<option value="" />
{#each tableOptions as table}
<option value={table._id}>{table.name}</option>
{/each}
</Select>
{#if parameters.tableId}
<SaveFields <SaveFields
parameterFields={parameters.fields} parameterFields={parameters.fields}
{schemaFields} {schemaFields}
on:change={onFieldsChanged} /> on:change={onFieldsChanged} />
{/if} {/if}
{/if} </div>
</div> </div>
<style> <style>
.root { .root {
max-width: 800px;
margin: 0 auto;
}
.root :global(p) {
line-height: 1.5;
}
.fields {
display: grid; display: grid;
column-gap: var(--spacing-l); column-gap: var(--spacing-l);
row-gap: var(--spacing-s); row-gap: var(--spacing-s);
@ -63,14 +77,9 @@
align-items: baseline; align-items: baseline;
} }
.root :global(> div:nth-child(2)) { .fields :global(> div:nth-child(2)),
.fields :global(> div:nth-child(4)) {
grid-column-start: 2; grid-column-start: 2;
grid-column-end: 6; grid-column-end: 6;
} }
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
margin: auto;
}
</style> </style>

View file

@ -101,6 +101,11 @@
</div> </div>
<style> <style>
.root {
max-width: 800px;
margin: 0 auto;
}
.fields { .fields {
display: grid; display: grid;
column-gap: var(--spacing-l); column-gap: var(--spacing-l);

View file

@ -29,6 +29,8 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: baseline; align-items: baseline;
max-width: 800px;
margin: 0 auto;
} }
.root :global(> div) { .root :global(> div) {

View file

@ -5,19 +5,21 @@
getDatasourceForProvider, getDatasourceForProvider,
getSchemaForDatasource, getSchemaForDatasource,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
export let componentInstance = {} export let componentInstance = {}
export let value = "" export let value = ""
export let onChange = () => {} export let onChange = () => {}
export let multiselect = false export let multiselect = false
export let placeholder
$: datasource = getDatasourceForProvider(componentInstance) $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchemaForDatasource(datasource).schema $: schema = getSchemaForDatasource(datasource).schema
$: options = Object.keys(schema || {}) $: options = Object.keys(schema || {})
</script> </script>
{#if multiselect} {#if multiselect}
<MultiOptionSelect {value} {onChange} {options} /> <MultiOptionSelect {value} {onChange} {options} {placeholder} />
{:else} {:else}
<OptionSelect {value} {onChange} {options} /> <OptionSelect {value} {onChange} {options} {placeholder} />
{/if} {/if}

View file

@ -7,6 +7,7 @@
getSchemaForDatasource, getSchemaForDatasource,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import SaveFields from "./EventsEditor/actions/SaveFields.svelte" import SaveFields from "./EventsEditor/actions/SaveFields.svelte"
import { currentAsset } from "builderStore"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -18,7 +19,7 @@
$: schemaFields = getSchemaFields(componentInstance) $: schemaFields = getSchemaFields(componentInstance)
const getSchemaFields = component => { const getSchemaFields = component => {
const datasource = getDatasourceForProvider(component) const datasource = getDatasourceForProvider($currentAsset, component)
const { schema } = getSchemaForDatasource(datasource) const { schema } = getSchemaForDatasource(datasource)
return Object.values(schema || {}) return Object.values(schema || {})
} }
@ -65,6 +66,8 @@
.root { .root {
padding: var(--spacing-l); padding: var(--spacing-l);
min-height: calc(40vh - 2 * var(--spacing-l)); min-height: calc(40vh - 2 * var(--spacing-l));
max-width: 800px;
margin: 0 auto;
} }
.fields { .fields {

View file

@ -17,7 +17,7 @@
componentInstance._id, componentInstance._id,
component => component._component === "@budibase/standard-components/form" component => component._component === "@budibase/standard-components/form"
) )
$: datasource = getDatasourceForProvider(form) $: datasource = getDatasourceForProvider($currentAsset, form)
$: schema = getSchemaForDatasource(datasource, true).schema $: schema = getSchemaForDatasource(datasource, true).schema
$: options = getOptions(schema, type) $: options = getOptions(schema, type)

View file

@ -4,6 +4,7 @@
export let options = [] export let options = []
export let value = [] export let value = []
export let onChange = () => {} export let onChange = () => {}
export let placeholder
let boundValue = getValidOptions(value, options) let boundValue = getValidOptions(value, options)
@ -26,6 +27,7 @@
align="right" align="right"
extraThin extraThin
secondary secondary
{placeholder}
value={boundValue} value={boundValue}
on:change={setValue}> on:change={setValue}>
{#each options as option} {#each options as option}

View file

@ -7,6 +7,7 @@
export let value = "" export let value = ""
export let styleBindingProperty export let styleBindingProperty
export let onChange = () => {} export let onChange = () => {}
export let placeholder
let open = null let open = null
let rotate = "" let rotate = ""
@ -108,7 +109,7 @@
$: displayLabel = $: displayLabel =
selectedOption && selectedOption.label selectedOption && selectedOption.label
? selectedOption.label ? selectedOption.label
: value || "Choose option" : value || placeholder || "Choose option"
</script> </script>
<div <div

View file

@ -1,7 +1,7 @@
<script> <script>
import DatasourceSelect from "./DatasourceSelect.svelte" import DataSourceSelect from "./DataSourceSelect.svelte"
const otherSources = [{ name: "Custom", label: "Custom" }] const otherSources = [{ name: "Custom", label: "Custom" }]
</script> </script>
<DatasourceSelect on:change {...$$props} showAllQueries={true} {otherSources} /> <DataSourceSelect on:change {...$$props} showAllQueries={true} {otherSources} />

View file

@ -13,7 +13,8 @@
import OptionSelect from "./PropertyControls/OptionSelect.svelte" import OptionSelect from "./PropertyControls/OptionSelect.svelte"
import Checkbox from "./PropertyControls/Checkbox.svelte" import Checkbox from "./PropertyControls/Checkbox.svelte"
import TableSelect from "./PropertyControls/TableSelect.svelte" import TableSelect from "./PropertyControls/TableSelect.svelte"
import DatasourceSelect from "./PropertyControls/DatasourceSelect.svelte" import DataSourceSelect from "./PropertyControls/DataSourceSelect.svelte"
import DataProviderSelect from "./PropertyControls/DataProviderSelect.svelte"
import FieldSelect from "./PropertyControls/FieldSelect.svelte" import FieldSelect from "./PropertyControls/FieldSelect.svelte"
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte" import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte" import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
@ -61,7 +62,8 @@
const controlMap = { const controlMap = {
text: Input, text: Input,
select: OptionSelect, select: OptionSelect,
datasource: DatasourceSelect, dataSource: DataSourceSelect,
dataProvider: DataProviderSelect,
detailScreen: DetailScreenSelect, detailScreen: DetailScreenSelect,
boolean: Checkbox, boolean: Checkbox,
number: Input, number: Input,
@ -108,8 +110,8 @@
componentInstance._id, componentInstance._id,
component => component._component.endsWith("/form") component => component._component.endsWith("/form")
) )
const datasource = form?.datasource const dataSource = form?.dataSource
const fields = makeDatasourceFormComponents(datasource) const fields = makeDatasourceFormComponents(dataSource)
onChange( onChange(
"_children", "_children",
fields.map(field => field.json()) fields.map(field => field.json())

View file

@ -1,11 +1,7 @@
<script> <script>
import { writable, get as svelteGet } from "svelte/store" import { writable, get as svelteGet } from "svelte/store"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { import { store, automationStore, hostingStore } from "builderStore"
store,
automationStore,
hostingStore,
} from "builderStore"
import { string, object } from "yup" import { string, object } from "yup"
import api, { get } from "builderStore/api" import api, { get } from "builderStore/api"
import Form from "@svelteschool/svelte-forms" import Form from "@svelteschool/svelte-forms"
@ -24,7 +20,6 @@
export let template export let template
const infoValidation = { const infoValidation = {
applicationName: string().required("Your application must have a name."), applicationName: string().required("Your application must have a name."),
} }
@ -149,7 +144,6 @@
) )
const pkg = await applicationPkg.json() const pkg = await applicationPkg.json()
if (applicationPkg.ok) { if (applicationPkg.ok) {
// backendUiStore.actions.reset()
await store.actions.initialise(pkg) await store.actions.initialise(pkg)
await automationStore.actions.fetch() await automationStore.actions.fetch()
} else { } else {

View file

@ -1,6 +1,6 @@
<script> <script>
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import { roles } from 'stores/backend/' import { roles } from "stores/backend/"
import { Button } from "@budibase/bbui" import { Button } from "@budibase/bbui"
import SettingsLink from "components/settings/Link.svelte" import SettingsLink from "components/settings/Link.svelte"
import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte" import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
@ -18,7 +18,6 @@
const pkg = await res.json() const pkg = await res.json()
if (res.ok) { if (res.ok) {
// backendUiStore.actions.reset()
await store.actions.initialise(pkg) await store.actions.initialise(pkg)
await automationStore.actions.fetch() await automationStore.actions.fetch()
await roles.fetch() await roles.fetch()

View file

@ -7,31 +7,31 @@ import { executeQuery } from "./queries"
/** /**
* Fetches all rows for a particular Budibase data source. * Fetches all rows for a particular Budibase data source.
*/ */
export const fetchDatasource = async datasource => { export const fetchDatasource = async dataSource => {
if (!datasource || !datasource.type) { if (!dataSource || !dataSource.type) {
return [] return []
} }
// Fetch all rows in data source // Fetch all rows in data source
const { type, tableId, fieldName } = datasource const { type, tableId, fieldName } = dataSource
let rows = [] let rows = []
if (type === "table") { if (type === "table") {
rows = await fetchTableData(tableId) rows = await fetchTableData(tableId)
} else if (type === "view") { } else if (type === "view") {
rows = await fetchViewData(datasource) rows = await fetchViewData(dataSource)
} else if (type === "query") { } else if (type === "query") {
// Set the default query params // Set the default query params
let parameters = cloneDeep(datasource.queryParams || {}) let parameters = cloneDeep(dataSource.queryParams || {})
for (let param of datasource.parameters) { for (let param of dataSource.parameters) {
if (!parameters[param.name]) { if (!parameters[param.name]) {
parameters[param.name] = param.default parameters[param.name] = param.default
} }
} }
rows = await executeQuery({ queryId: datasource._id, parameters }) rows = await executeQuery({ queryId: dataSource._id, parameters })
} else if (type === "link") { } else if (type === "link") {
rows = await fetchRelationshipData({ rows = await fetchRelationshipData({
rowId: datasource.rowId, rowId: dataSource.rowId,
tableId: datasource.rowTableId, tableId: dataSource.rowTableId,
fieldName, fieldName,
}) })
} }

View file

@ -1,4 +1,4 @@
import { notificationStore, datasourceStore } from "../store" import { notificationStore, dataSourceStore } from "../store"
import API from "./api" import API from "./api"
/** /**
@ -20,7 +20,7 @@ export const executeQuery = async ({ queryId, parameters }) => {
notificationStore.danger("An error has occurred") notificationStore.danger("An error has occurred")
} else if (!query.readable) { } else if (!query.readable) {
notificationStore.success("Query executed successfully") notificationStore.success("Query executed successfully")
datasourceStore.actions.invalidateDatasource(query.datasourceId) dataSourceStore.actions.invalidateDataSource(query.datasourceId)
} }
return res return res
} }

View file

@ -1,4 +1,4 @@
import { notificationStore, datasourceStore } from "../store" import { notificationStore, dataSourceStore } from "../store"
import API from "./api" import API from "./api"
import { fetchTableDefinition } from "./tables" import { fetchTableDefinition } from "./tables"
@ -31,7 +31,7 @@ export const saveRow = async row => {
: notificationStore.success("Row saved") : notificationStore.success("Row saved")
// Refresh related datasources // Refresh related datasources
datasourceStore.actions.invalidateDatasource(row.tableId) dataSourceStore.actions.invalidateDataSource(row.tableId)
return res return res
} }
@ -52,7 +52,7 @@ export const updateRow = async row => {
: notificationStore.success("Row updated") : notificationStore.success("Row updated")
// Refresh related datasources // Refresh related datasources
datasourceStore.actions.invalidateDatasource(row.tableId) dataSourceStore.actions.invalidateDataSource(row.tableId)
return res return res
} }
@ -72,7 +72,7 @@ export const deleteRow = async ({ tableId, rowId, revId }) => {
: notificationStore.success("Row deleted") : notificationStore.success("Row deleted")
// Refresh related datasources // Refresh related datasources
datasourceStore.actions.invalidateDatasource(tableId) dataSourceStore.actions.invalidateDataSource(tableId)
return res return res
} }
@ -96,7 +96,7 @@ export const deleteRows = async ({ tableId, rows }) => {
: notificationStore.success(`${rows.length} row(s) deleted`) : notificationStore.success(`${rows.length} row(s) deleted`)
// Refresh related datasources // Refresh related datasources
datasourceStore.actions.invalidateDatasource(tableId) dataSourceStore.actions.invalidateDataSource(tableId)
return res return res
} }

View file

@ -33,7 +33,7 @@
{ {
type: ActionTypes.RefreshDatasource, type: ActionTypes.RefreshDatasource,
callback: () => authStore.actions.fetchUser(), callback: () => authStore.actions.fetchUser(),
metadata: { datasource: { type: "table", tableId: TableNames.USERS } }, metadata: { dataSource: { type: "table", tableId: TableNames.USERS } },
}, },
] ]
</script> </script>

View file

@ -1,6 +1,6 @@
<script> <script>
import { getContext, setContext, onMount } from "svelte" import { getContext, setContext, onMount } from "svelte"
import { datasourceStore, createContextStore } from "../store" import { dataSourceStore, createContextStore } from "../store"
import { ActionTypes } from "../constants" import { ActionTypes } from "../constants"
import { generate } from "shortid" import { generate } from "shortid"
@ -31,9 +31,9 @@
// Register any "refresh datasource" actions with a singleton store // Register any "refresh datasource" actions with a singleton store
// so we can easily refresh data at all levels for any datasource // so we can easily refresh data at all levels for any datasource
if (type === ActionTypes.RefreshDatasource) { if (type === ActionTypes.RefreshDatasource) {
const { datasource } = metadata || {} const { dataSource } = metadata || {}
datasourceStore.actions.registerDatasource( dataSourceStore.actions.registerDataSource(
datasource, dataSource,
instanceId, instanceId,
callback callback
) )
@ -48,7 +48,7 @@
instanceId = generate() instanceId = generate()
// Unregister all datasource instances when unmounting this provider // Unregister all datasource instances when unmounting this provider
return () => datasourceStore.actions.unregisterInstance(instanceId) return () => dataSourceStore.actions.unregisterInstance(instanceId)
}) })
</script> </script>

View file

@ -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" || dataSource.type === "view") {
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()

View file

@ -1,84 +0,0 @@
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" || datasource.type === "view") {
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()

View file

@ -3,7 +3,7 @@ export { notificationStore } from "./notification"
export { routeStore } from "./routes" export { routeStore } from "./routes"
export { screenStore } from "./screens" export { screenStore } from "./screens"
export { builderStore } from "./builder" export { builderStore } from "./builder"
export { datasourceStore } from "./datasource" export { dataSourceStore } from "./dataSource"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"

View file

@ -43,8 +43,8 @@ export const enrichProps = async (props, context) => {
// Enrich all data bindings in top level props // Enrich all data bindings in top level props
let enrichedProps = await enrichDataBindings(validProps, totalContext) let enrichedProps = await enrichDataBindings(validProps, totalContext)
// Enrich button actions if they exist // Enrich click actions if they exist
if (props._component?.endsWith("/button") && enrichedProps.onClick) { if (enrichedProps.onClick) {
enrichedProps.onClick = enrichButtonActions( enrichedProps.onClick = enrichButtonActions(
enrichedProps.onClick, enrichedProps.onClick,
totalContext totalContext

View file

@ -8,47 +8,6 @@
"transitionable": true, "transitionable": true,
"settings": [] "settings": []
}, },
"datagrid": {
"name": "Grid",
"description": "A datagrid component with functionality to add, remove and edit rows.",
"icon": "ri-grid-line",
"styleable": true,
"settings": [
{
"type": "datasource",
"label": "Source",
"key": "datasource"
},
{
"type": "detailScreen",
"label": "Detail URL",
"key": "detailUrl"
},
{
"type": "boolean",
"label": "Editable",
"key": "editable"
},
{
"type": "select",
"label": "Theme",
"key": "theme",
"options": ["alpine", "alpine-dark", "balham", "balham-dark", "material"],
"defaultValue": "alpine"
},
{
"type": "number",
"label": "Height",
"key": "height",
"defaultValue": "500"
},
{
"type": "boolean",
"label": "Pagination",
"key": "pagination"
}
]
},
"screenslot": { "screenslot": {
"name": "Screenslot", "name": "Screenslot",
"icon": "ri-artboard-2-line", "icon": "ri-artboard-2-line",
@ -78,19 +37,17 @@
} }
] ]
}, },
"list": { "repeater": {
"name": "Repeater", "name": "Repeater",
"description": "A configurable data list that attaches to your backend tables.", "description": "A configurable data list that attaches to your backend tables.",
"icon": "ri-list-check-2", "icon": "ri-list-check-2",
"styleable": true, "styleable": true,
"hasChildren": true, "hasChildren": true,
"dataProvider": true,
"actions": ["RefreshDatasource"],
"settings": [ "settings": [
{ {
"type": "datasource", "type": "dataProvider",
"label": "Data", "label": "Data",
"key": "datasource" "key": "dataProvider"
}, },
{ {
"type": "text", "type": "text",
@ -103,7 +60,10 @@
"label": "Filtering", "label": "Filtering",
"key": "filter" "key": "filter"
} }
] ],
"context": {
"type": "schema"
}
}, },
"search": { "search": {
"name": "Search", "name": "Search",
@ -111,7 +71,6 @@
"icon": "ri-search-line", "icon": "ri-search-line",
"styleable": true, "styleable": true,
"hasChildren": true, "hasChildren": true,
"dataProvider": true,
"settings": [ "settings": [
{ {
"type": "table", "type": "table",
@ -136,7 +95,10 @@
"key": "noRowsMessage", "key": "noRowsMessage",
"defaultValue": "No rows found." "defaultValue": "No rows found."
} }
] ],
"context": {
"type": "schema"
}
}, },
"stackedlist": { "stackedlist": {
"name": "Stacked List", "name": "Stacked List",
@ -369,6 +331,11 @@
"label": "Color", "label": "Color",
"key": "color", "key": "color",
"defaultValue": "#000" "defaultValue": "#000"
},
{
"type": "event",
"label": "On Click",
"key": "onClick"
} }
] ]
}, },
@ -438,36 +405,6 @@
} }
] ]
}, },
"rowdetail": {
"name": "Row Detail",
"description": "Loads a row, using an id from the URL, which can be used with {{ context }}, in children",
"icon": "ri-profile-line",
"styleable": true,
"hasChildren": true,
"dataProvider": true,
"settings": [
{
"type": "table",
"label": "Table",
"key": "table"
}
]
},
"newrow": {
"name": "New Row",
"description": "Sets up a new row for creation, which can be used with {{ context }}, in children",
"icon": "ri-profile-line",
"hasChildren": true,
"styleable": true,
"dataProvider": true,
"settings": [
{
"type": "table",
"label": "Table",
"key": "table"
}
]
},
"cardhorizontal": { "cardhorizontal": {
"name": "Horizontal Card", "name": "Horizontal Card",
"description": "A basic card component that can contain content and actions.", "description": "A basic card component that can contain content and actions.",
@ -591,21 +528,21 @@
"key": "title" "key": "title"
}, },
{ {
"type": "datasource", "type": "dataProvider",
"label": "Data", "label": "Data",
"key": "datasource" "key": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Label Col.", "label": "Label Col.",
"key": "labelColumn", "key": "labelColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "multifield", "type": "multifield",
"label": "Data Cols.", "label": "Data Cols.",
"key": "valueColumns", "key": "valueColumns",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "select", "type": "select",
@ -691,21 +628,21 @@
"key": "title" "key": "title"
}, },
{ {
"type": "datasource", "type": "dataProvider",
"label": "Data", "label": "Data",
"key": "datasource" "key": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Label Col.", "label": "Label Col.",
"key": "labelColumn", "key": "labelColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "multifield", "type": "multifield",
"label": "Data Cols.", "label": "Data Cols.",
"key": "valueColumns", "key": "valueColumns",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "select", "type": "select",
@ -792,21 +729,21 @@
"key": "title" "key": "title"
}, },
{ {
"type": "datasource", "type": "dataProvider",
"label": "Data", "label": "Data",
"key": "datasource" "key": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Label Col.", "label": "Label Col.",
"key": "labelColumn", "key": "labelColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "multifield", "type": "multifield",
"label": "Data Cols.", "label": "Data Cols.",
"key": "valueColumns", "key": "valueColumns",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "select", "type": "select",
@ -905,21 +842,21 @@
"key": "title" "key": "title"
}, },
{ {
"type": "datasource", "type": "dataProvider",
"label": "Data", "label": "Data",
"key": "datasource" "key": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Label Col.", "label": "Label Col.",
"key": "labelColumn", "key": "labelColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Data Col.", "label": "Data Col.",
"key": "valueColumn", "key": "valueColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "text", "type": "text",
@ -982,21 +919,21 @@
"key": "title" "key": "title"
}, },
{ {
"type": "datasource", "type": "dataProvider",
"label": "Data", "label": "Data",
"key": "datasource" "key": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Label Col.", "label": "Label Col.",
"key": "labelColumn", "key": "labelColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Data Col.", "label": "Data Col.",
"key": "valueColumn", "key": "valueColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "text", "type": "text",
@ -1059,39 +996,39 @@
"key": "title" "key": "title"
}, },
{ {
"type": "datasource", "type": "dataProvider",
"label": "Data", "label": "Data",
"key": "datasource" "key": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Date Col.", "label": "Date Col.",
"key": "dateColumn", "key": "dateColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Open Col.", "label": "Open Col.",
"key": "openColumn", "key": "openColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Close Col.", "label": "Close Col.",
"key": "closeColumn", "key": "closeColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "High Col.", "label": "High Col.",
"key": "highColumn", "key": "highColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "field", "type": "field",
"label": "Low Col.", "label": "Low Col.",
"key": "lowColumn", "key": "lowColumn",
"dependsOn": "datasource" "dependsOn": "dataProvider"
}, },
{ {
"type": "select", "type": "select",
@ -1134,13 +1071,14 @@
"icon": "ri-file-text-line", "icon": "ri-file-text-line",
"styleable": true, "styleable": true,
"hasChildren": true, "hasChildren": true,
"dataProvider": true, "actions": [
"actions": ["ValidateForm"], "ValidateForm"
],
"settings": [ "settings": [
{ {
"type": "schema", "type": "schema",
"label": "Schema", "label": "Schema",
"key": "datasource" "key": "dataSource"
}, },
{ {
"type": "select", "type": "select",
@ -1188,7 +1126,10 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
} }
] ],
"context": {
"type": "form"
}
}, },
"fieldgroup": { "fieldgroup": {
"name": "Field Group", "name": "Field Group",
@ -1472,5 +1413,145 @@
"defaultValue": false "defaultValue": false
} }
] ]
},
"dataprovider": {
"name": "Data Provider",
"icon": "ri-database-2-line",
"styleable": false,
"hasChildren": true,
"settings": [
{
"type": "dataSource",
"label": "Data",
"key": "dataSource"
},
{
"type": "filter",
"label": "Filtering",
"key": "filter"
},
{
"type": "field",
"label": "Sort Column",
"key": "sortColumn"
},
{
"type": "select",
"label": "Sort Order",
"key": "sortOrder",
"options": ["Ascending", "Descending"],
"defaultValue": "Descending"
},
{
"type": "number",
"label": "Limit",
"key": "limit"
}
],
"context": {
"type": "static",
"values": [
{
"label": "Rows",
"key": "rows"
},
{
"label": "Rows Length",
"key": "rowsLength"
},
{
"label": "Schema",
"key": "schema"
},
{
"label": "Loading",
"key": "loading"
},
{
"label": "Loaded",
"key": "loaded"
}
]
}
},
"table": {
"name": "Table",
"icon": "ri-table-line",
"styleable": true,
"hasChildren": true,
"settings": [
{
"type": "dataProvider",
"label": "Data Provider",
"key": "dataProvider"
},
{
"type": "number",
"label": "Row Count",
"key": "rowCount",
"defaultValue": 8
},
{
"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"
}
]
},
{
"type": "boolean",
"label": "Quiet",
"key": "quiet"
},
{
"type": "multifield",
"label": "Columns",
"key": "columns",
"dependsOn": "dataProvider",
"placeholder": "All columns"
},
{
"type": "boolean",
"label": "Auto Cols.",
"key": "showAutoColumns",
"defaultValue": false
}
],
"context": {
"type": "schema"
}
} }
} }

View file

@ -41,21 +41,23 @@
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.1.0", "@adobe/spectrum-css-workflow-icons": "^1.1.0",
"@budibase/bbui": "^1.58.13", "@budibase/bbui": "^1.58.13",
"@budibase/svelte-ag-grid": "^1.0.4", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actionbutton": "^1.0.0-beta.1", "@spectrum-css/button": "^3.0.1",
"@spectrum-css/button": "^3.0.0-beta.6", "@spectrum-css/checkbox": "^3.0.1",
"@spectrum-css/checkbox": "^3.0.0-beta.6", "@spectrum-css/fieldlabel": "^3.0.1",
"@spectrum-css/fieldlabel": "^3.0.0-beta.7", "@spectrum-css/icon": "^3.0.1",
"@spectrum-css/icon": "^3.0.0-beta.2", "@spectrum-css/inputgroup": "^3.0.1",
"@spectrum-css/inputgroup": "^3.0.0-beta.7", "@spectrum-css/label": "^2.0.9",
"@spectrum-css/menu": "^3.0.0-beta.5", "@spectrum-css/menu": "^3.0.1",
"@spectrum-css/page": "^3.0.0-beta.0", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/picker": "^1.0.0-beta.3", "@spectrum-css/picker": "^1.0.0",
"@spectrum-css/popover": "^3.0.0-beta.6", "@spectrum-css/popover": "^3.0.1",
"@spectrum-css/stepper": "^3.0.0-beta.7", "@spectrum-css/stepper": "^3.0.1",
"@spectrum-css/textfield": "^3.0.0-beta.6", "@spectrum-css/table": "^3.0.1",
"@spectrum-css/vars": "^3.0.0-beta.2", "@spectrum-css/textfield": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
"apexcharts": "^3.22.1", "apexcharts": "^3.22.1",
"dayjs": "^1.10.4",
"flatpickr": "^4.6.6", "flatpickr": "^4.6.6",
"loadicons": "^1.0.0", "loadicons": "^1.0.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",

View file

@ -0,0 +1,108 @@
<script>
import { getContext } from "svelte"
export let dataSource
export let filter
export let sortColumn
export let sortOrder
export let limit
const { API, styleable, Provider, ActionTypes } = getContext("sdk")
const component = getContext("component")
// Loading flag every time data is being fetched
let loading = false
// Loading flag for the initial load
let loaded = false
let allRows = []
let schema = {}
$: fetchData(dataSource)
$: filteredRows = filterRows(allRows, filter)
$: sortedRows = sortRows(filteredRows, sortColumn, sortOrder)
$: rows = limitRows(sortedRows, limit)
$: getSchema(dataSource)
$: actions = [
{
type: ActionTypes.RefreshDatasource,
callback: () => fetchData(dataSource),
metadata: { dataSource },
},
]
$: dataContext = {
rows,
schema,
rowsLength: rows.length,
loading,
loaded,
}
const fetchData = async dataSource => {
loading = true
allRows = await API.fetchDatasource(dataSource)
loading = false
loaded = true
}
const filterRows = (rows, filter) => {
if (!Object.keys(filter || {}).length) {
return rows
}
let filteredData = [...rows]
Object.entries(filter).forEach(([field, value]) => {
if (value != null && value !== "") {
filteredData = filteredData.filter(row => {
return row[field] === value
})
}
})
return filteredData
}
const sortRows = (rows, sortColumn, sortOrder) => {
if (!sortColumn || !sortOrder) {
return rows
}
return rows.slice().sort((a, b) => {
const colA = a[sortColumn]
const colB = b[sortColumn]
if (sortOrder === "Descending") {
return colA > colB ? -1 : 1
} else {
return colA > colB ? 1 : -1
}
})
}
const limitRows = (rows, limit) => {
const numLimit = parseFloat(limit)
if (isNaN(numLimit)) {
return rows
}
return rows.slice(0, numLimit)
}
const getSchema = async dataSource => {
if (dataSource?.schema) {
schema = dataSource.schema
} else if (dataSource?.tableId) {
const definition = await API.fetchTableDefinition(dataSource.tableId)
schema = definition?.schema ?? {}
} else {
schema = {}
}
// Ensure all schema fields have a name property
Object.entries(schema).forEach(([key, value]) => {
if (!value.name) {
value.name = key
}
})
}
</script>
<Provider {actions} data={dataContext}>
<slot />
</Provider>

View file

@ -7,6 +7,7 @@
export let icon = "" export let icon = ""
export let size = "fa-lg" export let size = "fa-lg"
export let color = "#f00" export let color = "#f00"
export let onClick
$: styles = { $: styles = {
...$component.styles, ...$component.styles,
@ -17,4 +18,4 @@
} }
</script> </script>
<i use:styleable={styles} class="{icon} {size}" /> <i use:styleable={styles} class="{icon} {size}" on:click={onClick} />

View file

@ -1,83 +0,0 @@
<script>
import { getContext } from "svelte"
import { isEmpty } from "lodash/fp"
export let datasource
export let noRowsMessage
export let filter
const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
"sdk"
)
const component = getContext("component")
let rows = []
let loaded = false
$: fetchData(datasource)
$: filteredRows = filterRows(rows, filter)
$: actions = [
{
type: ActionTypes.RefreshDatasource,
callback: () => fetchData(datasource),
metadata: { datasource },
},
]
const fetchData = async datasource => {
if (!isEmpty(datasource)) {
rows = await API.fetchDatasource(datasource)
}
loaded = true
}
const filterRows = (rows, filter) => {
if (!Object.keys(filter || {}).length) {
return rows
}
let filteredData = [...rows]
Object.entries(filter).forEach(([field, value]) => {
if (value != null && value !== "") {
filteredData = filteredData.filter(row => {
return row[field] === value
})
}
})
return filteredData
}
</script>
<Provider {actions}>
<div use:styleable={$component.styles}>
{#if filteredRows.length > 0}
{#if $component.children === 0 && $builderStore.inBuilder}
<p><i class="ri-image-line" />Add some components to display.</p>
{:else}
{#each filteredRows as row}
<Provider data={row}>
<slot />
</Provider>
{/each}
{/if}
{:else if loaded && noRowsMessage}
<p><i class="ri-list-check-2" />{noRowsMessage}</p>
{/if}
</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;
}
p i {
margin-bottom: var(--spacing-m);
font-size: 1.5rem;
color: var(--grey-5);
}
</style>

View file

@ -0,0 +1,47 @@
<script>
import { getContext } from "svelte"
export let dataProvider
export let noRowsMessage
const { API, styleable, builderStore, Provider } = getContext("sdk")
const component = getContext("component")
const context = getContext("context")
$: rows = dataProvider?.rows ?? []
$: loaded = dataProvider?.loaded ?? false
</script>
<div use:styleable={$component.styles}>
{#if rows.length > 0}
{#if $component.children === 0 && $builderStore.inBuilder}
<p><i class="ri-image-line" />Add some components to display.</p>
{:else}
{#each rows as row}
<Provider data={row}>
<slot />
</Provider>
{/each}
{/if}
{:else if loaded && noRowsMessage}
<p><i class="ri-list-check-2" />{noRowsMessage}</p>
{/if}
</div>
<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;
}
p i {
margin-bottom: var(--spacing-m);
font-size: 1.5rem;
color: var(--grey-5);
}
</style>

View file

@ -1,57 +0,0 @@
<script>
import { onMount, getContext } from "svelte"
export let table
const {
API,
screenStore,
routeStore,
Provider,
styleable,
ActionTypes,
} = getContext("sdk")
const component = getContext("component")
let headers = []
let row
const fetchFirstRow = async tableId => {
const rows = await API.fetchTableData(tableId)
return Array.isArray(rows) && rows.length ? rows[0] : { tableId }
}
const fetchData = async (rowId, tableId) => {
if (!tableId) {
return
}
const pathParts = window.location.pathname.split("/")
// if srcdoc, then we assume this is the builder preview
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")
}
}
$: 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}>
<slot />
</div>
</Provider>
{/if}

View file

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { chart } from "svelte-apexcharts" import { chart } from "svelte-apexcharts"
const { styleable } = getContext("sdk") const { styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let options export let options
@ -10,7 +10,7 @@
{#if options} {#if options}
<div use:chart={options} use:styleable={$component.styles} /> <div use:chart={options} use:styleable={$component.styles} />
{:else if options === false} {:else if builderStore.inBuilder}
<div use:styleable={$component.styles}> <div use:styleable={$component.styles}>
Use the settings panel to build your chart --> Use the settings panel to build your chart -->
</div> </div>

View file

@ -1,13 +1,9 @@
<script> <script>
import { getContext, onMount } from "svelte"
import { ApexOptionsBuilder } from "./ApexOptionsBuilder" import { ApexOptionsBuilder } from "./ApexOptionsBuilder"
import ApexChart from "./ApexChart.svelte" import ApexChart from "./ApexChart.svelte"
import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk")
export let title export let title
export let datasource export let dataProvider
export let labelColumn export let labelColumn
export let valueColumns export let valueColumns
export let xAxisLabel export let xAxisLabel
@ -22,28 +18,21 @@
export let yAxisUnits export let yAxisUnits
export let palette export let palette
let options $: options = setUpChart(dataProvider)
// Fetch data on mount const setUpChart = provider => {
onMount(async () => {
const allCols = [labelColumn, ...(valueColumns || [null])] const allCols = [labelColumn, ...(valueColumns || [null])]
if (isEmpty(datasource) || allCols.find(x => x == null)) { if (!provider || allCols.find(x => x == null)) {
options = false return null
return
} }
// Fetch, filter and sort data // Fatch data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema const { schema, rows } = provider
const result = await API.fetchDatasource(datasource)
const reducer = row => (valid, column) => valid && row[column] != null const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true) const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = result const data = rows.filter(row => hasAllColumns(row)).slice(0, 100)
.filter(row => hasAllColumns(row))
.slice(0, 20)
.sort((a, b) => (a[labelColumn] > b[labelColumn] ? 1 : -1))
if (!schema || !data.length) { if (!schema || !data.length) {
options = false return null
return
} }
// Initialise default chart // Initialise default chart
@ -63,7 +52,7 @@
// Add data // Add data
let useDates = false let useDates = false
if (datasource.type !== "view" && schema[labelColumn]) { if (schema[labelColumn]) {
const labelFieldType = schema[labelColumn].type const labelFieldType = schema[labelColumn].type
builder = builder.xType(labelFieldType) builder = builder.xType(labelFieldType)
useDates = labelFieldType === "datetime" useDates = labelFieldType === "datetime"
@ -84,8 +73,8 @@
} }
// Build chart options // Build chart options
options = builder.getOptions() return builder.getOptions()
}) }
</script> </script>
<ApexChart {options} /> <ApexChart {options} />

View file

@ -1,13 +1,9 @@
<script> <script>
import { getContext, onMount } from "svelte"
import { ApexOptionsBuilder } from "./ApexOptionsBuilder" import { ApexOptionsBuilder } from "./ApexOptionsBuilder"
import ApexChart from "./ApexChart.svelte" import ApexChart from "./ApexChart.svelte"
import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk")
export let title export let title
export let datasource export let dataProvider
export let dateColumn export let dateColumn
export let openColumn export let openColumn
export let highColumn export let highColumn
@ -20,28 +16,22 @@
export let animate export let animate
export let yAxisUnits export let yAxisUnits
let options $: options = setUpChart(dataProvider)
// Fetch data on mount // Fetch data on mount
onMount(async () => { const setUpChart = provider => {
const allCols = [dateColumn, openColumn, highColumn, lowColumn, closeColumn] const allCols = [dateColumn, openColumn, highColumn, lowColumn, closeColumn]
if (isEmpty(datasource) || allCols.find(x => x == null)) { if (!provider || allCols.find(x => x == null)) {
options = false return null
return
} }
// Fetch, filter and sort data // Fetch data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema const { schema, rows } = provider
const result = await API.fetchDatasource(datasource)
const reducer = row => (valid, column) => valid && row[column] != null const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true) const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = result const data = rows.filter(row => hasAllColumns(row))
.filter(row => hasAllColumns(row))
.slice(0, 100)
.sort((a, b) => (a[dateColumn] > b[dateColumn] ? 1 : -1))
if (!schema || !data.length) { if (!schema || !data.length) {
options = false return null
return
} }
// Initialise default chart // Initialise default chart
@ -66,8 +56,8 @@
builder = builder.series([{ data: chartData }]) builder = builder.series([{ data: chartData }])
// Build chart options // Build chart options
options = builder.getOptions() return builder.getOptions()
}) }
</script> </script>
<ApexChart {options} /> <ApexChart {options} />

View file

@ -1,14 +1,10 @@
<script> <script>
import { getContext, onMount } from "svelte"
import { ApexOptionsBuilder } from "./ApexOptionsBuilder" import { ApexOptionsBuilder } from "./ApexOptionsBuilder"
import ApexChart from "./ApexChart.svelte" import ApexChart from "./ApexChart.svelte"
import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk")
// Common props // Common props
export let title export let title
export let datasource export let dataProvider
export let labelColumn export let labelColumn
export let valueColumns export let valueColumns
export let xAxisLabel export let xAxisLabel
@ -28,28 +24,22 @@
export let stacked export let stacked
export let gradient export let gradient
let options $: options = setUpChart(dataProvider)
// Fetch data on mount // Fetch data on mount
onMount(async () => { const setUpChart = provider => {
const allCols = [labelColumn, ...(valueColumns || [null])] const allCols = [labelColumn, ...(valueColumns || [null])]
if (isEmpty(datasource) || allCols.find(x => x == null)) { if (!provider || allCols.find(x => x == null)) {
options = false return null
return
} }
// Fetch, filter and sort data // Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema const { schema, rows } = provider
const result = await API.fetchDatasource(datasource)
const reducer = row => (valid, column) => valid && row[column] != null const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true) const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = result const data = rows.filter(row => hasAllColumns(row))
.filter(row => hasAllColumns(row))
.slice(0, 100)
.sort((a, b) => (a[labelColumn] > b[labelColumn] ? 1 : -1))
if (!schema || !data.length) { if (!schema || !data.length) {
options = false return null
return
} }
// Initialise default chart // Initialise default chart
@ -71,7 +61,7 @@
// Add data // Add data
let useDates = false let useDates = false
if (datasource.type !== "view" && schema[labelColumn]) { if (schema[labelColumn]) {
const labelFieldType = schema[labelColumn].type const labelFieldType = schema[labelColumn].type
builder = builder.xType(labelFieldType) builder = builder.xType(labelFieldType)
useDates = labelFieldType === "datetime" useDates = labelFieldType === "datetime"
@ -92,8 +82,8 @@
} }
// Build chart options // Build chart options
options = builder.getOptions() return builder.getOptions()
}) }
</script> </script>
<ApexChart {options} /> <ApexChart {options} />

View file

@ -1,13 +1,9 @@
<script> <script>
import { getContext, onMount } from "svelte"
import { ApexOptionsBuilder } from "./ApexOptionsBuilder" import { ApexOptionsBuilder } from "./ApexOptionsBuilder"
import ApexChart from "./ApexChart.svelte" import ApexChart from "./ApexChart.svelte"
import { isEmpty } from "lodash/fp"
const { API } = getContext("sdk")
export let title export let title
export let datasource export let dataProvider
export let labelColumn export let labelColumn
export let valueColumn export let valueColumn
export let height export let height
@ -19,25 +15,21 @@
export let donut export let donut
export let palette export let palette
let options $: options = setUpChart(dataProvider)
// Fetch data on mount // Fetch data on mount
onMount(async () => { const setUpChart = provider => {
if (isEmpty(datasource) || !labelColumn || !valueColumn) { if (!provider || !labelColumn || !valueColumn) {
options = false return null
return
} }
// Fetch, filter and sort data // Fetch, filter and sort data
const schema = (await API.fetchTableDefinition(datasource.tableId)).schema const { schema, rows } = provider
const result = await API.fetchDatasource(datasource) const data = rows
const data = result
.filter(row => row[labelColumn] != null && row[valueColumn] != null) .filter(row => row[labelColumn] != null && row[valueColumn] != null)
.slice(0, 20) .slice(0, 100)
.sort((a, b) => (a[labelColumn] > b[labelColumn] ? 1 : -1))
if (!schema || !data.length) { if (!schema || !data.length) {
options = false return null
return
} }
// Initialise default chart // Initialise default chart
@ -58,8 +50,8 @@
builder = builder.series(series).labels(labels) builder = builder.series(series).labels(labels)
// Build chart options // Build chart options
options = builder.getOptions() return builder.getOptions()
}) }
</script> </script>
<ApexChart {options} /> <ApexChart {options} />

View file

@ -5,7 +5,7 @@
import { createValidatorFromConstraints } from "./validation" import { createValidatorFromConstraints } from "./validation"
import { generateID } from "../helpers" import { generateID } from "../helpers"
export let datasource export let dataSource
export let theme export let theme
export let size export let size
export let disabled = false export let disabled = false
@ -143,15 +143,15 @@
}) })
} }
// Fetches the form schema from this form's datasource, if one exists // Fetches the form schema from this form's dataSource, if one exists
const fetchSchema = async () => { const fetchSchema = async () => {
if (!datasource?.tableId) { if (!dataSource?.tableId) {
schema = {} schema = {}
table = null table = null
} else { } else {
table = await API.fetchTableDefinition(datasource?.tableId) table = await API.fetchTableDefinition(dataSource?.tableId)
if (table) { if (table) {
if (datasource?.type === "query") { if (dataSource?.type === "query") {
schema = {} schema = {}
const params = table.parameters || [] const params = table.parameters || []
params.forEach(param => { params.forEach(param => {
@ -171,7 +171,7 @@
<Provider <Provider
{actions} {actions}
data={{ ...$formState.values, tableId: datasource?.tableId }}> data={{ ...$formState.values, tableId: dataSource?.tableId }}>
<div <div
lang="en" lang="en"
dir="ltr" dir="ltr"

View file

@ -19,7 +19,7 @@
{#if fieldState} {#if fieldState}
<button <button
id={$fieldState.fieldId} id={$fieldState.fieldId}
class="spectrum-Picker" class="spectrum-Picker spectrum-Picker--sizeM"
disabled={$fieldState.disabled} disabled={$fieldState.disabled}
class:is-invalid={!$fieldState.valid} class:is-invalid={!$fieldState.valid}
class:is-open={open} class:is-open={open}

View file

@ -1,6 +0,0 @@
<script>
import AttachmentList from "../../attachments/AttachmentList.svelte"
export let files
</script>
<AttachmentList {files} on:delete />

View file

@ -1,193 +0,0 @@
<script>
// Import valueSetters and custom renderers
import { number } from "./valueSetters"
import { getRenderer } from "./customRenderer"
import { isEmpty } from "lodash/fp"
import { getContext } from "svelte"
import AgGrid from "@budibase/svelte-ag-grid"
import {
TextButton as DeleteButton,
Icon,
Modal,
ModalContent,
} from "@budibase/bbui"
// These maps need to be set up to handle whatever types that are used in the tables.
const setters = new Map([["number", number]])
const SDK = getContext("sdk")
const component = getContext("component")
const { API, styleable } = SDK
export let datasource = {}
export let editable
export let theme = "alpine"
export let height = 500
export let pagination
export let detailUrl
// Add setting height as css var to allow grid to use correct height
$: gridStyles = {
...$component.styles,
normal: {
...$component.styles.normal,
["--grid-height"]: `${height}px`,
},
}
$: fetchData(datasource)
// These can never change at runtime so don't need to be reactive
let canEdit = editable && datasource && datasource.type !== "view"
let canAddDelete = editable && datasource && datasource.type === "table"
let modal
let dataLoaded = false
let data
let columnDefs
let selectedRows = []
let table
let options = {
defaultColDef: {
flex: 1,
minWidth: 150,
filter: true,
},
rowSelection: canEdit ? "multiple" : false,
suppressFieldDotNotation: true,
suppressRowClickSelection: !canEdit,
paginationAutoPageSize: true,
pagination,
}
async function fetchData(datasource) {
if (isEmpty(datasource)) {
return
}
data = await API.fetchDatasource(datasource)
let schema
// Get schema for datasource
// Views with "Calculate" applied provide their own schema.
// For everything else, use the tableId property to pull to table schema
if (datasource.schema) {
schema = datasource.schema
} else {
schema = (await API.fetchTableDefinition(datasource.tableId)).schema
}
columnDefs = Object.keys(schema).map((key, i) => {
return {
headerCheckboxSelection: i === 0 && canEdit,
checkboxSelection: i === 0 && canEdit,
valueSetter: setters.get(schema[key].type),
headerName: key,
field: key,
hide: shouldHideField(key),
sortable: true,
editable: canEdit && schema[key].type !== "link",
cellRenderer: getRenderer(schema[key], canEdit, SDK),
autoHeight: true,
}
})
if (detailUrl) {
columnDefs = [
...columnDefs,
{
headerName: "Detail",
field: "_id",
minWidth: 100,
width: 100,
flex: 0,
editable: false,
sortable: false,
cellRenderer: getRenderer(
{
type: "_id",
options: { detailUrl },
},
false,
SDK
),
autoHeight: true,
pinned: "left",
filter: false,
},
]
}
dataLoaded = true
}
const shouldHideField = name => {
if (name.startsWith("_")) return true
// always 'row'
if (name === "type") return true
// tables are always tied to a single tableId, this is irrelevant
if (name === "tableId") return true
return false
}
const handleUpdate = ({ detail }) => {
data[detail.row] = detail.data
updateRow(detail.data)
}
const updateRow = async row => {
await API.updateRow(row)
}
const deleteRows = async () => {
await API.deleteRows({ rows: selectedRows, tableId: datasource.name })
data = data.filter(row => !selectedRows.includes(row))
selectedRows = []
}
</script>
<div class="container" use:styleable={gridStyles}>
{#if dataLoaded}
{#if canAddDelete}
<div class="controls">
{#if selectedRows.length > 0}
<DeleteButton text small on:click={modal.show()}>
<Icon name="addrow" />
Delete
{selectedRows.length}
row(s)
</DeleteButton>
{/if}
</div>
{/if}
<AgGrid
{theme}
{options}
{data}
{columnDefs}
on:update={handleUpdate}
on:select={({ detail }) => (selectedRows = detail)} />
{/if}
<Modal bind:this={modal}>
<ModalContent
title="Confirm Row Deletion"
confirmText="Delete"
onConfirm={deleteRows}>
<span>Are you sure you want to delete {selectedRows.length} row(s)?</span>
</ModalContent>
</Modal>
</div>
<style>
.container :global(.ag-pinned-left-header .ag-header-cell-label) {
justify-content: center;
}
.controls {
min-height: 15px;
margin-bottom: var(--spacing-s);
display: grid;
grid-gap: var(--spacing-s);
grid-template-columns: auto auto;
justify-content: start;
}
</style>

View file

@ -1,37 +0,0 @@
<script>
import { createEventDispatcher } from "svelte"
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
import Modal from "./Modal.svelte"
const dispatch = createEventDispatcher()
let anchor
let dropdown
export let table
</script>
<div bind:this={anchor}>
<Button text small on:click={dropdown.show}>
<Icon name="addrow" />
Create New Row
</Button>
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<h5>Add New Row</h5>
<Modal
{table}
onClosed={dropdown.hide}
on:newRow={() => dispatch('newRow')} />
</DropdownMenu>
<style>
div {
display: grid;
}
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
</style>

View file

@ -1,144 +0,0 @@
<script>
import { getContext, onMount, createEventDispatcher } from "svelte"
import { Button, Label, DatePicker, RichText } from "@budibase/bbui"
import Dropzone from "../../attachments/Dropzone.svelte"
import debounce from "lodash.debounce"
const dispatch = createEventDispatcher()
const { fetchRow, saveRow, routeStore } = getContext("sdk")
const DEFAULTS_FOR_TYPE = {
string: "",
boolean: false,
number: null,
link: [],
}
export let table
export let onClosed
let row = { tableId: table._id }
let schema = table.schema
let saved = false
let rowId
let isNew = true
let errors = {}
$: fields = schema ? Object.keys(schema) : []
$: errorMessages = Object.entries(errors).map(
([field, message]) => `${field} ${message}`
)
const save = debounce(async () => {
for (let field of fields) {
// Assign defaults to empty fields to prevent validation issues
if (!(field in row)) {
row[field] = DEFAULTS_FOR_TYPE[schema[field].type]
}
}
const response = await saveRow(row)
if (!response.error) {
// store.update(state => {
// state[table._id] = state[table._id]
// ? [...state[table._id], json]
// : [json]
// return state
// })
errors = {}
// wipe form, if new row, otherwise update
// table to get new _rev
row = isNew ? { tableId: table._id } : response
onClosed()
dispatch("newRow")
} else {
errors = [response.error]
}
})
onMount(async () => {
const routeParams = $routeStore.routeParams
rowId =
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !rowId || rowId === "new"
if (isNew) {
row = { tableId: table }
return
}
row = await fetchRow({ tableId: table._id, rowId })
})
</script>
<div class="actions">
{#each errorMessages as error}
<p class="error">{error}</p>
{/each}
<form on:submit|preventDefault>
{#each fields as field}
<div class="form-item">
<Label small forAttr={'form-stacked-text'}>{field}</Label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select bind:value={row[field]}>
{#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'}
<input class="input" type="checkbox" bind:checked={row[field]} />
{:else if schema[field].type === 'number'}
<input class="input" type="number" bind:value={row[field]} />
{:else if schema[field].type === 'string'}
<input class="input" type="text" 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]} />
{/if}
</div>
<hr />
{/each}
</form>
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={save}>Save</Button>
</div>
</footer>
<style>
.actions {
padding: var(--spacing-l) var(--spacing-xl);
}
footer {
padding: 20px 30px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View file

@ -1,5 +0,0 @@
<script>
import { DatePicker } from "@budibase/bbui"
</script>
<DatePicker />

View file

@ -1,75 +0,0 @@
<script>
import { onMount } from "svelte"
export let columnName
export let row
export let SDK
const { API } = SDK
$: count =
row && columnName && Array.isArray(row[columnName])
? row[columnName].length
: 0
let linkedRows = []
let displayColumn
onMount(async () => {
linkedRows = await API.fetchRelationshipData({
tableId: row.tableId,
rowId: row._id,
fieldName: columnName,
})
if (linkedRows && linkedRows.length) {
const table = await API.fetchTableDefinition(linkedRows[0].tableId)
if (table && table.primaryDisplay) {
displayColumn = table.primaryDisplay
}
}
})
async function fetchLinkedRowsData(row, columnName) {
if (!row || !row._id) {
return []
}
return await API.fetchRelationshipData({
tableId: row.tableId,
rowId: row._id,
fieldName: columnName,
})
}
</script>
<div class="container">
{#if linkedRows && linkedRows.length && displayColumn}
{#each linkedRows as linkedRow}
{#if linkedRow[displayColumn] != null && linkedRow[displayColumn] !== ''}
<div class="linked-row">{linkedRow[displayColumn]}</div>
{/if}
{/each}
{:else}{count} related row(s){/if}
</div>
<style>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
width: 100%;
}
/* This styling is opinionated to ensure these always look consistent */
.linked-row {
color: white;
background-color: #616161;
border-radius: var(--border-radius-xs);
padding: var(--spacing-xs) var(--spacing-s) calc(var(--spacing-xs) + 1px)
var(--spacing-s);
line-height: 1;
font-size: 0.8em;
font-family: var(--font-sans);
font-weight: 500;
}
</style>

View file

@ -1,32 +0,0 @@
<script>
export let columnName
export let row
$: items = row?.[columnName] || []
</script>
<div class="container">
{#each items as item}
<div class="item">{item?.primaryDisplay ?? ''}</div>
{/each}
</div>
<style>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
width: 100%;
}
.item {
font-size: var(--font-size-xs);
padding: var(--spacing-xs) var(--spacing-s);
border: 1px solid var(--grey-5);
color: var(--grey-7);
line-height: normal;
border-radius: 4px;
}
</style>

View file

@ -1,17 +0,0 @@
<script>
import { Select } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value
export let options
$: dispatch("change", value)
</script>
<Select label={false} bind:value>
<option value="">Choose an option</option>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</Select>

View file

@ -1,19 +0,0 @@
<script>
import { Button } from "@budibase/bbui"
export let url
export let SDK
const { linkable } = SDK
let link
</script>
<a href={url} bind:this={link} use:linkable />
<Button small translucent on:click={() => link.click()}>View</Button>
<style>
a {
display: none;
}
</style>

View file

@ -1,169 +0,0 @@
// Custom renderers to handle special types
// https://www.ag-grid.com/javascript-grid-cell-rendering-components/
import AttachmentCell from "./AttachmentCell/Button.svelte"
import ViewDetails from "./ViewDetails/Cell.svelte"
import Select from "./Select/Wrapper.svelte"
import DatePicker from "./DateTime/Wrapper.svelte"
import RelationshipLabel from "./Relationship/RelationshipLabel.svelte"
const renderers = new Map([
["boolean", booleanRenderer],
["attachment", attachmentRenderer],
["options", optionsRenderer],
["link", linkedRowRenderer],
["_id", viewDetailsRenderer],
])
export function getRenderer(schema, editable, SDK) {
if (renderers.get(schema.type)) {
return renderers.get(schema.type)(
schema.options,
schema.constraints,
editable,
SDK
)
} else {
return false
}
}
/* eslint-disable no-unused-vars */
function booleanRenderer(options, constraints, editable, SDK) {
return params => {
const toggle = e => {
params.value = !params.value
params.setValue(e.currentTarget.checked)
}
let input = document.createElement("input")
input.style.display = "grid"
input.style.placeItems = "center"
input.style.height = "100%"
input.type = "checkbox"
input.checked = params.value
if (editable) {
input.addEventListener("click", toggle)
} else {
input.disabled = true
}
return input
}
}
/* eslint-disable no-unused-vars */
function attachmentRenderer(options, constraints, editable, SDK) {
return params => {
const container = document.createElement("div")
const attachmentInstance = new AttachmentCell({
target: container,
props: {
files: params.value || [],
SDK,
},
})
const deleteFile = event => {
const newFilesArray = params.value.filter(file => file !== event.detail)
params.setValue(newFilesArray)
}
attachmentInstance.$on("delete", deleteFile)
return container
}
}
/* eslint-disable no-unused-vars */
function dateRenderer(options, constraints, editable, SDK) {
return function(params) {
const container = document.createElement("div")
const toggle = e => {
params.setValue(e.detail[0][0])
}
// Options need to be passed in with minTime and maxTime! Needs bbui update.
new DatePicker({
target: container,
props: {
value: params.value,
SDK,
},
})
return container
}
}
function optionsRenderer(options, constraints, editable, SDK) {
return params => {
if (!editable) return params.value
const container = document.createElement("div")
container.style.display = "grid"
container.style.placeItems = "center"
container.style.height = "100%"
const change = e => {
params.setValue(e.detail)
}
const selectInstance = new Select({
target: container,
props: {
value: params.value,
options: constraints.inclusion,
SDK,
},
})
selectInstance.$on("change", change)
return container
}
}
/* eslint-disable no-unused-vars */
function linkedRowRenderer(options, constraints, editable, SDK) {
return params => {
let container = document.createElement("div")
container.style.display = "grid"
container.style.placeItems = "center"
container.style.height = "100%"
new RelationshipLabel({
target: container,
props: {
row: params.data,
columnName: params.column.colId,
SDK,
},
})
return container
}
}
/* eslint-disable no-unused-vars */
function viewDetailsRenderer(options, constraints, editable, SDK) {
return params => {
let container = document.createElement("div")
container.style.display = "grid"
container.style.alignItems = "center"
container.style.height = "100%"
let url = "/"
if (options.detailUrl) {
url = options.detailUrl.replace(":id", params.data._id)
}
if (!url.startsWith("/")) {
url = `/${url}`
}
new ViewDetails({
target: container,
props: {
url,
SDK,
},
})
return container
}
}

View file

@ -1,6 +0,0 @@
// https://www.ag-grid.com/javascript-grid-value-setters/
// These handles values and makes sure they adhere to the data type provided by the table
export const number = params => {
params.data[params.colDef.field] = parseFloat(params.newValue)
return true
}

View file

@ -13,17 +13,16 @@ import { loadSpectrumIcons } from "./spectrum-icons"
loadSpectrumIcons() loadSpectrumIcons()
export { default as container } from "./Container.svelte" export { default as container } from "./Container.svelte"
export { default as datagrid } from "./grid/Component.svelte" export { default as dataprovider } from "./DataProvider.svelte"
export { default as screenslot } from "./ScreenSlot.svelte" export { default as screenslot } from "./ScreenSlot.svelte"
export { default as button } from "./Button.svelte" export { default as button } from "./Button.svelte"
export { default as list } from "./List.svelte" export { default as repeater } from "./Repeater.svelte"
export { default as stackedlist } from "./StackedList.svelte" export { default as stackedlist } from "./StackedList.svelte"
export { default as card } from "./Card.svelte" export { default as card } from "./Card.svelte"
export { default as text } from "./Text.svelte" export { default as text } from "./Text.svelte"
export { default as login } from "./Login.svelte" export { default as login } from "./Login.svelte"
export { default as navigation } from "./Navigation.svelte" export { default as navigation } from "./Navigation.svelte"
export { default as link } from "./Link.svelte" export { default as link } from "./Link.svelte"
export { default as rowdetail } from "./RowDetail.svelte"
export { default as heading } from "./Heading.svelte" export { default as heading } from "./Heading.svelte"
export { default as image } from "./Image.svelte" export { default as image } from "./Image.svelte"
export { default as embed } from "./Embed.svelte" export { default as embed } from "./Embed.svelte"
@ -34,3 +33,4 @@ export { default as search } from "./Search.svelte"
export { default as backgroundimage } from "./BackgroundImage.svelte" export { default as backgroundimage } from "./BackgroundImage.svelte"
export * from "./charts" export * from "./charts"
export * from "./forms" export * from "./forms"
export * from "./table"

View file

@ -0,0 +1,39 @@
<script>
export let value
const displayLimit = 5
$: attachments = value?.slice(0, displayLimit) ?? []
$: leftover = (value?.length ?? 0) - attachments.length
</script>
{#each attachments as attachment}
{#if attachment.type.startsWith('image')}
<img src={attachment.url} alt={attachment.extension} />
{:else}
<div class="file">{attachment.extension}</div>
{/if}
{/each}
{#if leftover}
<div>+{leftover} more</div>
{/if}
<style>
img {
height: 32px;
max-width: 64px;
}
.file {
height: 32px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 0 8px;
color: var(--spectrum-global-color-gray-800);
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 2px;
text-transform: uppercase;
font-weight: 500;
font-size: 11px;
}
</style>

View file

@ -0,0 +1,38 @@
<script>
import "@spectrum-css/checkbox/dist/index-vars.css"
export let value
</script>
<label
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-Checkbox--emphasized">
<input
type="checkbox"
class="spectrum-Checkbox-input"
id="checkbox-1"
disabled
checked={!!value} />
<span class="spectrum-Checkbox-box">
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Checkbox-checkmark"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
<svg
class="spectrum-Icon spectrum-UIIcon-Dash100 spectrum-Checkbox-partialCheckmark"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Dash100" />
</svg>
</span>
</label>
<style>
.spectrum-Checkbox {
min-height: 0;
}
.spectrum-Checkbox-box {
margin: 0;
}
</style>

View file

@ -0,0 +1,27 @@
<script>
import StringRenderer from "./StringRenderer.svelte"
import BooleanRenderer from "./BooleanRenderer.svelte"
import DateTimeRenderer from "./DateTimeRenderer.svelte"
import RelationshipRenderer from "./RelationshipRenderer.svelte"
import AttachmentRenderer from "./AttachmentRenderer.svelte"
export let schema
export let value
const plainTypes = ["string", "options", "number", "longform"]
$: type = schema?.type ?? "string"
</script>
{#if value != null && value !== ''}
{#if plainTypes.includes(type)}
<StringRenderer {value} />
{:else if type === 'boolean'}
<BooleanRenderer {value} />
{:else if type === 'datetime'}
<DateTimeRenderer {value} />
{:else if type === 'link'}
<RelationshipRenderer {value} />
{:else if type === 'attachment'}
<AttachmentRenderer {value} />
{/if}
{/if}

View file

@ -0,0 +1,13 @@
<script>
import dayjs from "dayjs"
export let value
</script>
<div>{dayjs(value).format('MMMM D YYYY, HH:mm')}</div>
<style>
div {
width: 200px;
}
</style>

View file

@ -0,0 +1,20 @@
<script>
import "@spectrum-css/label/dist/index-vars.css"
export let value
const displayLimit = 5
$: relationships = value?.slice(0, displayLimit) ?? []
$: leftover = (value?.length ?? 0) - relationships.length
</script>
{#each relationships as relationship}
{#if relationship?.primaryDisplay}
<span class="spectrum-Label spectrum-Label--grey">
{relationship.primaryDisplay}
</span>
{/if}
{/each}
{#if leftover}
<div>+{leftover} more</div>
{/if}

View file

@ -0,0 +1,13 @@
<script>
export let value
</script>
<div>{value}</div>
<style>
div {
overflow: hidden;
text-overflow: ellipsis;
width: 150px;
}
</style>

View file

@ -0,0 +1,290 @@
<script>
import { fade } from "svelte/transition"
import "@spectrum-css/table/dist/index-vars.css"
import { getContext } from "svelte"
import CellRenderer from "./CellRenderer.svelte"
export let theme
export let size
export let dataProvider
export let columns
export let showAutoColumns
export let rowCount
export let quiet
const component = getContext("component")
const { styleable, Provider } = getContext("sdk")
// Config
const rowHeight = 55
const headerHeight = 36
const rowPreload = 5
const maxRows = 100
// Sorting state
let sortColumn
let sortOrder
// Table state
$: loaded = dataProvider?.loaded ?? false
$: rows = dataProvider?.rows ?? []
$: visibleRowCount = loaded
? Math.min(rows.length, rowCount || maxRows, maxRows)
: Math.min(8, rowCount || maxRows)
$: scroll = rows.length > visibleRowCount
$: contentStyle = getContentStyle(visibleRowCount, scroll || !loaded)
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
$: schema = dataProvider?.schema ?? {}
$: fields = getFields(schema, columns, showAutoColumns)
// Scrolling state
let timeout
let nextScrollTop = 0
let scrollTop = 0
$: firstVisibleRow = calculateFirstVisibleRow(scrollTop)
$: lastVisibleRow = calculateLastVisibleRow(
firstVisibleRow,
visibleRowCount,
rows.length
)
const getContentStyle = (visibleRows, useFixedHeight) => {
if (!useFixedHeight) {
return ""
}
return `height: ${headerHeight - 1 + visibleRows * (rowHeight + 1)}px;`
}
const sortRows = (rows, sortColumn, sortOrder) => {
if (!sortColumn || !sortOrder) {
return rows
}
return rows.slice().sort((a, b) => {
const colA = a[sortColumn]
const colB = b[sortColumn]
if (sortOrder === "Descending") {
return colA > colB ? -1 : 1
} else {
return colA > colB ? 1 : -1
}
})
}
const sortBy = field => {
if (field === sortColumn) {
sortOrder = sortOrder === "Descending" ? "Ascending" : "Descending"
} else {
sortColumn = field
sortOrder = "Descending"
}
}
const getFields = (schema, customColumns, showAutoColumns) => {
// Check for an invalid column selection
let invalid = false
customColumns?.forEach(column => {
if (schema[column] == null) {
invalid = true
}
})
// Use column selection if it exists
if (!invalid && customColumns?.length) {
return customColumns
}
// Otherwise generate columns
let columns = []
let autoColumns = []
Object.entries(schema).forEach(([field, fieldSchema]) => {
if (!fieldSchema?.autocolumn) {
columns.push(field)
} else if (showAutoColumns) {
autoColumns.push(field)
}
})
return columns.concat(autoColumns)
}
const onScroll = event => {
nextScrollTop = event.target.scrollTop
if (timeout) {
return
}
timeout = setTimeout(() => {
scrollTop = nextScrollTop
timeout = null
}, 50)
}
const calculateFirstVisibleRow = scrollTop => {
return Math.max(Math.floor(scrollTop / (rowHeight + 1)) - rowPreload, 0)
}
const calculateLastVisibleRow = (firstRow, visibleRowCount, allRowCount) => {
return Math.min(firstRow + visibleRowCount + 2 * rowPreload, allRowCount)
}
</script>
{#if !loaded}
<div class="content" style={contentStyle} />
{:else}
<div use:styleable={$component.styles}>
<div
on:scroll={onScroll}
lang="en"
dir="ltr"
class:quiet
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}>
<div class="content" style={contentStyle}>
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
<thead class="spectrum-Table-head">
<tr>
{#if $component.children}
<th class="spectrum-Table-headCell">
<div class="spectrum-Table-headCell-content" />
</th>
{/if}
{#each fields as field}
<th
class="spectrum-Table-headCell is-sortable"
class:is-sorted-desc={sortColumn === field && sortOrder === 'Descending'}
class:is-sorted-asc={sortColumn === field && sortOrder === 'Ascending'}
on:click={() => sortBy(field)}>
<div class="spectrum-Table-headCell-content">
<div class="title">{schema[field]?.name}</div>
<svg
class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon"
class:visible={sortColumn === field}
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Arrow100" />
</svg>
</div>
</th>
{/each}
</tr>
</thead>
<tbody class="spectrum-Table-body">
{#each sortedRows as row, idx}
<tr
class="spectrum-Table-row"
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow}>
{#if idx >= firstVisibleRow && idx <= lastVisibleRow}
{#if $component.children}
<td
class="spectrum-Table-cell spectrum-Table-cell--divider">
<div class="spectrum-Table-cell-content">
<Provider data={row}>
<slot />
</Provider>
</div>
</td>
{/if}
{#each fields as field}
<td class="spectrum-Table-cell">
<div class="spectrum-Table-cell-content">
<CellRenderer
schema={schema[field]}
value={row[field]} />
</div>
</td>
{/each}
{/if}
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
{/if}
<style>
.spectrum {
position: relative;
overflow: auto;
border: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
}
.spectrum.quiet {
border: none !important;
}
table {
width: 100%;
}
.spectrum-Table-sortedIcon {
opacity: 0;
display: block !important;
}
.spectrum-Table-sortedIcon.visible {
opacity: 1;
}
.spectrum,
th {
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
}
th {
vertical-align: middle;
height: var(--header-height);
position: sticky;
top: 0;
background-color: var(--spectrum-global-color-gray-100);
z-index: 2;
}
.spectrum-Table-headCell-content {
white-space: nowrap;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
user-select: none;
}
.spectrum-Table-headCell-content .title {
overflow: hidden;
text-overflow: ellipsis;
}
tbody {
z-index: 1;
}
tbody tr {
height: var(--row-height);
}
tbody tr.hidden {
height: calc(var(--row-height) + 1px);
}
tbody tr.offset {
background-color: red;
display: block;
}
td {
padding-top: 0;
padding-bottom: 0;
border-bottom: none !important;
border-left: none !important;
border-right: none !important;
border-top: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
}
tr:first-child td {
border-top: none !important;
}
.spectrum:not(.quiet) td.spectrum-Table-cell--divider {
width: 1px;
border-right: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
}
.spectrum-Table-cell-content {
height: var(--row-height);
white-space: nowrap;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 4px;
}
</style>

View file

@ -0,0 +1 @@
export { default as table } from "./Table.svelte"

View file

@ -56,13 +56,6 @@
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
turndown "^7.0.0" turndown "^7.0.0"
"@budibase/svelte-ag-grid@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@budibase/svelte-ag-grid/-/svelte-ag-grid-1.0.4.tgz#41cceec4bde2c4aea8b9da8f610fe36055c7709f"
integrity sha512-JZm6qujxnZpqw7Twbegr6se4sHhyWzN0Cibrk5bVBH32hBgzD6dd33fxwrjHKkWFxjys9wRT+cqYgYVlSt9E3w==
dependencies:
ag-grid-community "^24.0.0"
"@polka/url@^0.5.0": "@polka/url@^0.5.0":
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-0.5.0.tgz#b21510597fd601e5d7c95008b76bf0d254ebfd31" resolved "https://registry.yarnpkg.com/@polka/url/-/url-0.5.0.tgz#b21510597fd601e5d7c95008b76bf0d254ebfd31"
@ -132,72 +125,82 @@
estree-walker "^1.0.1" estree-walker "^1.0.1"
picomatch "^2.2.2" picomatch "^2.2.2"
"@spectrum-css/actionbutton@^1.0.0-beta.1": "@spectrum-css/actionbutton@^1.0.1":
version "1.0.0-beta.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/actionbutton/-/actionbutton-1.0.0-beta.1.tgz#a6684cac108d4a9daefe0be6df8201d3c369a0d6" resolved "https://registry.yarnpkg.com/@spectrum-css/actionbutton/-/actionbutton-1.0.1.tgz#9c75da37ea6915919fb574c74bd60dacc03b6577"
integrity sha512-QbrPMTkbkmh+dEBP66TFXmF5z3qSde+BnLR5hnlo2XMvKvnblX2VJStEbQ+hTKuSZXCRFADXyXD5o0NOYDTByQ== integrity sha512-AUqtyNabHF451Aj9i3xz82TxS5Z6k1dttA68/1hMeU9kbPCSS4P6Viw3vaRGs9CSspuR8xnnhDgrq+F+zMy2Hw==
"@spectrum-css/button@^3.0.0-beta.6": "@spectrum-css/button@^3.0.1":
version "3.0.0-beta.6" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.0-beta.6.tgz#007919d3e7a6692e506dc9addcd46aee6b203b1a" resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.1.tgz#6db8c3e851baecd0f1c2d88fef37d49d01c6e643"
integrity sha512-ZoJxezt5Pc006RR7SMG7PfC0VAdWqaGDpd21N8SEykGuz/KmNulqGW8RiSZQGMVX/jk5ZCAthPrH8cI/qtKbMg== integrity sha512-YXrBtjIYisk4Vaxnp0RiE4gdElQX04P2mc4Pi2GlQ27dJKlHmufYcF+kAqGdtiyK5yjdN/vKRcC8y13aA4rusA==
"@spectrum-css/checkbox@^3.0.0-beta.6": "@spectrum-css/checkbox@^3.0.1":
version "3.0.0-beta.6" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/checkbox/-/checkbox-3.0.0-beta.6.tgz#338c4e58c4570ac8023f7332794fcb45f5ae9374" resolved "https://registry.yarnpkg.com/@spectrum-css/checkbox/-/checkbox-3.0.1.tgz#6f36377d8bd556989ddd1dec2506dc295c5fcda8"
integrity sha512-Z0Mwu7yn2b+QcZaBqMpKhliTQiF8T/cRyKgTyaIACtJ0FAK5NBJ4h/X6SWW3iXtoUWCH4+p/Hdtq1iQHAFi1qQ== integrity sha512-fI0q2Cp6yU4ORyE6JWUSMYNgEtGf6AjYViZ2Weg3UPTYBQuWdQd8J0ZTcH38pDMyARFPRdiXgQ3KnyX5Hk5huw==
"@spectrum-css/fieldlabel@^3.0.0-beta.7": "@spectrum-css/fieldlabel@^3.0.1":
version "3.0.0-beta.7" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/fieldlabel/-/fieldlabel-3.0.0-beta.7.tgz#f37797565e21b3609b8fbc2dafcea8ea41ffa114" resolved "https://registry.yarnpkg.com/@spectrum-css/fieldlabel/-/fieldlabel-3.0.1.tgz#39f7c0f25cc2ff402afeff005341b0832f7c588c"
integrity sha512-0pseiPghqlOdALsRtidveWyt2YjfSXTZWDlSkcne/J0/QXBJOQH/7Qfy7TmROQZYRB2LqH1VzmE1zbvGwr5Aog== integrity sha512-LMfwrwIq8wEEvxFLobdLvXRwKrp8o9Fty4iJ9aYl2Rj1uXkfRd8qLz9HGZjLEE1OuJgoTBgamYABl7EvoA5PLw==
"@spectrum-css/icon@^3.0.0-beta.2": "@spectrum-css/icon@^3.0.1":
version "3.0.0-beta.2" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/icon/-/icon-3.0.0-beta.2.tgz#2dd7258ded74501b56e5fc42d0b6f0a3f4936aeb" resolved "https://registry.yarnpkg.com/@spectrum-css/icon/-/icon-3.0.1.tgz#e300a6fc353c85c6b5d6e7a364408a940c31b177"
integrity sha512-BEHJ68YIXSwsNAqTdq/FrS4A+jtbKzqYrsGKXdDf93ql+fHWYXRCh1EVYGHx/1696mY73DhM4snMpKGIFtXGFA== integrity sha512-cGFtIrcQ/7tthdkHK1npuEFiCdYVHLqwmLxghUYQw8Tb8KgJaw3OBO1tpjgsUizexNgu26BjVRIbGxNWuBXIHQ==
"@spectrum-css/inputgroup@^3.0.0-beta.7": "@spectrum-css/inputgroup@^3.0.1":
version "3.0.0-beta.7" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.0-beta.7.tgz#9829812e349bf973fb8835f0586bf013c8c38d23" resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.1.tgz#8c5b257b57b3b2cf04e99355709365fa0d6838cc"
integrity sha512-pZDpYhtTKZUVG31Rtx7imdwK2ohLyVuTEsl+mj2yDKn+2TOwYRxr6LdbfNhFN4xd0GtSqapKYfbgKBWYpIyiSw== integrity sha512-asBRa1jTlld6plkcq4ySO+xl+OJlCMSOLoAFdSSIJowcSlCV0yDy7oeOhf5YQv9mMHFWTKlWUSoAKDZTguIPxA==
"@spectrum-css/menu@^3.0.0-beta.5": "@spectrum-css/label@^2.0.9":
version "3.0.0-beta.5" version "2.0.9"
resolved "https://registry.yarnpkg.com/@spectrum-css/menu/-/menu-3.0.0-beta.5.tgz#99d5ea7f6760b7a89d5d732f4e91b98dd3f82d74" resolved "https://registry.yarnpkg.com/@spectrum-css/label/-/label-2.0.9.tgz#792f34b906ba81118f4d0edcc81a18da1ecd57cb"
integrity sha512-jvPD5GbNdX31rdFBLxCG7KoUVGeeNYLzNXDpiGZsWme/djVTwitljgNe7bhVwCVlXZE7H20Ti/YrdafnE154Rw== integrity sha512-0vXhWIZoQDTg+I6MyMpwmeJ+yQHtxkZ7lLcEqxhJ2y7JXP2ftblz2sO4+9jB11ljepeVlV+B6LF1drU8mMu82A==
"@spectrum-css/page@^3.0.0-beta.0": "@spectrum-css/menu@^3.0.1":
version "3.0.0-beta.0" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/page/-/page-3.0.0-beta.0.tgz#885ea41b44861c5dc3aac904536f9e93c9109b58" resolved "https://registry.yarnpkg.com/@spectrum-css/menu/-/menu-3.0.1.tgz#2a376f991acc24e12ec892bb6b9db2650fc41fbe"
integrity sha512-+OD+l3aLisykxJnHfLkdkxMS1Uj1vKGYpKil7W0r5lSWU44eHyRgb8ZK5Vri1+sUO5SSf/CTybeVwtXME9wMLA== integrity sha512-Qjg0+1O0eC89sb/bRFq2AGnQ8XqhVy23TUXHyffNM8qdcMssnlny3QmhzjURCZKvx/Y5UytCpzhedPQqSpQwZg==
"@spectrum-css/page@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/page/-/page-3.0.1.tgz#5e1c3dd5b1a1ee591f9d636b75f03665f542d846"
integrity sha512-LAlKF8km5BlsGPpZ2SNtwKOQIHn1lz0X93aczGZVZceOg73O4gyeoT5cx4vi1z+KtBRY5VMDWx3XgGtUwwjqwA==
dependencies: dependencies:
"@spectrum-css/vars" "^3.0.0-beta.2" "@spectrum-css/vars" "^3.0.1"
"@spectrum-css/picker@^1.0.0-beta.3": "@spectrum-css/picker@^1.0.0":
version "1.0.0-beta.3" version "1.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/picker/-/picker-1.0.0-beta.3.tgz#476593597b5a9e0105397e4e39350869cf6e7965" resolved "https://registry.yarnpkg.com/@spectrum-css/picker/-/picker-1.0.1.tgz#98991198576d26bd14160824e7b6f3c278ff930b"
integrity sha512-jHzFnS5Frd3JSwZ6B8ymH/sVnNqAUBo9p93Zax4VHTUDsPTtTkvxj/Vxo4POmrJEL9v3qUB2Yk13rD2BSfEzLQ== integrity sha512-Rv4/UBOdNW1gs7WVBCJnPD5VFly8MqP++psDX6kcugUIcfJy0GC3acvElotmKRlCDk8Qxks2W2A0jKeSgphTmA==
"@spectrum-css/popover@^3.0.0-beta.6": "@spectrum-css/popover@^3.0.1":
version "3.0.0-beta.6" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/popover/-/popover-3.0.0-beta.6.tgz#787611f020e091234e6ba7e946b0dbd0ed1a2fa2" resolved "https://registry.yarnpkg.com/@spectrum-css/popover/-/popover-3.0.1.tgz#5863c1efc53f98f9aba2de9186666780041303fc"
integrity sha512-dUJlwxoNpB6jOR0g/ywH2cPoUz2FVsL6xPfkm6BSsLp9ejhYy0/OFF4w0Q32Fu9qJDbWJ9qaoOlPpt7IjQ+/GQ== integrity sha512-LmOSj/yCwQQ9iGmCYnHiJsJR/HfPiGqI1Jl7pkKxBOCxYBMS/5+ans9vfCN2Qnd0eK7WSbfPg72S6mjye7db2Q==
"@spectrum-css/stepper@^3.0.0-beta.7": "@spectrum-css/stepper@^3.0.1":
version "3.0.0-beta.7" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/stepper/-/stepper-3.0.0-beta.7.tgz#fc78435ce878c5e233af13e43ed2c3e8671a2bbc" resolved "https://registry.yarnpkg.com/@spectrum-css/stepper/-/stepper-3.0.1.tgz#7f270f53505e7dbe082591e8ea1c4c8f397e045a"
integrity sha512-TQL2OBcdEgbHBwehMGgqMuWdKZZQPGcBRV5FlF0TUdOT58lEqFAO43Gajqvyte1P23lNmnX8KuMwkRfQdn0RzA== integrity sha512-IvZlGFJ8QPr9tUz5xvVN4hASaTRDPdKu9IIp25q/x0ecgSrKAM55e3EBWEYWy1H1JI3h+zlPnNRuK0VLhDbCYA==
"@spectrum-css/textfield@^3.0.0-beta.6": "@spectrum-css/table@^3.0.1":
version "3.0.0-beta.6" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/textfield/-/textfield-3.0.0-beta.6.tgz#30c044ceb403d6ea82d8046fb8f767f7fe455da6" resolved "https://registry.yarnpkg.com/@spectrum-css/table/-/table-3.0.1.tgz#753e0e2498082c0c36b9600828516aff3ac338cd"
integrity sha512-U7P8C3Xx8h5X+r+dZu1qbxceIxBn7ZSmMvJyC7MPSPcU3EwdzCUepERNGX7NrQdcX91XSNlPUOF7hZUognBwhQ== integrity sha512-XQ+srMTv9hK1H0nctWUtqyzitmvyb5TNR+7mjAmKRdkBRSTQQSipDhenxZp72ekzMtMoSYZVZ77kgo0Iw3Fpug==
"@spectrum-css/vars@^3.0.0-beta.2": "@spectrum-css/textfield@^3.0.1":
version "3.0.0-beta.2" version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.0-beta.2.tgz#f0b3a2db44aa57b1a82e47ab392c716a3056a157" resolved "https://registry.yarnpkg.com/@spectrum-css/textfield/-/textfield-3.0.1.tgz#e875b8e37817378ad08fc4af7d53026df38911e5"
integrity sha512-HpcRDUkSjKVWUi7+jf6zp33YszXs3qFljaaNVTVOf0m0mqjWWXHxgLrvYlFFlHp5ITbNXds5Cb7EgiXCKmVIpA== integrity sha512-MUV5q87CVxbkNdSNoxGrFbgyKc51ft/WWf3aVEoPdPw5yBnXqFe1w1YmAit5zYDOOhhs58sCLAlUcCMlOpkgrA==
"@spectrum-css/vars@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.1.tgz#561fd69098f896a647242dd8d6108af603bfa31e"
integrity sha512-l4oRcCOqInChYXZN6OQhpe3isk6l4OE6Ys8cgdlsiKp53suNoQxyyd9p/eGRbCjZgH3xQ8nK0t4DHa7QYC0S6w==
"@types/color-name@^1.1.1": "@types/color-name@^1.1.1":
version "1.1.1" version "1.1.1"
@ -238,11 +241,6 @@ abstract-leveldown@~0.12.0, abstract-leveldown@~0.12.1:
dependencies: dependencies:
xtend "~3.0.0" xtend "~3.0.0"
ag-grid-community@^24.0.0:
version "24.0.0"
resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-24.0.0.tgz#793a3c4a28e5148c5c8c8c6ff68e8f3c4ee6ca3d"
integrity sha512-TNNqTp918PCRicH/vE8fs1pu/hbWMc9Iem7gCjGsjlabBrGhMxCq7tYiBtguERuxdszbSImznjampnc0NvOoFg==
alphanum-sort@^1.0.0: alphanum-sort@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@ -864,6 +862,11 @@ csso@^4.0.2:
dependencies: dependencies:
css-tree "1.0.0-alpha.39" css-tree "1.0.0-alpha.39"
dayjs@^1.10.4:
version "1.10.4"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==
deep-equal@^1.0.1: deep-equal@^1.0.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"

View file

@ -35,7 +35,7 @@ const HELPERS = [
new Helper(HelperFunctionNames.LITERAL, value => { new Helper(HelperFunctionNames.LITERAL, value => {
const type = typeof value const type = typeof value
const outputVal = type === "object" ? JSON.stringify(value) : value const outputVal = type === "object" ? JSON.stringify(value) : value
return `{{-${LITERAL_MARKER}-${type}-${outputVal}-}}` return `{{${LITERAL_MARKER} ${type}-${outputVal}}}`
}), }),
] ]

View file

@ -21,13 +21,12 @@ module.exports.processors = [
if (!statement.includes(LITERAL_MARKER)) { if (!statement.includes(LITERAL_MARKER)) {
return statement return statement
} }
const splitMarkerIndex = statement.indexOf("-")
const components = statement.split("-") const type = statement.substring(12, splitMarkerIndex)
// pop and shift remove the empty array elements from the first and last dash const value = statement.substring(
components.pop() splitMarkerIndex + 1,
components.shift() statement.length - 2
const type = components[1] )
const value = components[2]
switch (type) { switch (type) {
case "string": case "string":
return value return value

View file

@ -1,15 +1,11 @@
const { const { processString, processObject, isValid } = require("../src/index")
processString,
processObject,
isValid,
} = require("../src/index")
describe("test the custom helpers we have applied", () => { describe("test the custom helpers we have applied", () => {
it("should be able to use the object helper", async () => { it("should be able to use the object helper", async () => {
const output = await processString("object is {{ object obj }}", { const output = await processString("object is {{ object obj }}", {
obj: { a: 1 }, obj: { a: 1 },
}) })
expect(output).toBe("object is {\"a\":1}") expect(output).toBe('object is {"a":1}')
}) })
}) })
@ -64,9 +60,12 @@ describe("test the array helpers", () => {
}) })
it("should allow use of the filter helper", async () => { it("should allow use of the filter helper", async () => {
const output = await processString("{{#filter array \"person\"}}THING{{else}}OTHER{{/filter}}", { const output = await processString(
array, '{{#filter array "person"}}THING{{else}}OTHER{{/filter}}',
}) {
array,
}
)
expect(output).toBe("THING") expect(output).toBe("THING")
}) })
@ -78,7 +77,7 @@ describe("test the array helpers", () => {
}) })
it("should allow use of the join helper", async () => { it("should allow use of the join helper", async () => {
const output = await processString("{{join array \"-\"}}", { const output = await processString('{{join array "-"}}', {
array, array,
}) })
expect(output).toBe("hi-person-how-are-you") expect(output).toBe("hi-person-how-are-you")
@ -86,14 +85,14 @@ describe("test the array helpers", () => {
it("should allow use of the sort helper", async () => { it("should allow use of the sort helper", async () => {
const output = await processString("{{sort array}}", { const output = await processString("{{sort array}}", {
array: ["d", "a", "c", "e"] array: ["d", "a", "c", "e"],
}) })
expect(output).toBe("a,c,d,e") expect(output).toBe("a,c,d,e")
}) })
it("should allow use of the unique helper", async () => { it("should allow use of the unique helper", async () => {
const output = await processString("{{unique array}}", { const output = await processString("{{unique array}}", {
array: ["a", "a", "b"] array: ["a", "a", "b"],
}) })
expect(output).toBe("a,b") expect(output).toBe("a,b")
}) })
@ -102,7 +101,7 @@ describe("test the array helpers", () => {
describe("test the number helpers", () => { describe("test the number helpers", () => {
it("should allow use of the addCommas helper", async () => { it("should allow use of the addCommas helper", async () => {
const output = await processString("{{ addCommas number }}", { const output = await processString("{{ addCommas number }}", {
number: 10000000 number: 10000000,
}) })
expect(output).toBe("10,000,000") expect(output).toBe("10,000,000")
}) })
@ -132,7 +131,7 @@ describe("test the number helpers", () => {
describe("test the url helpers", () => { describe("test the url helpers", () => {
const url = "http://example.com?query=1" const url = "http://example.com?query=1"
it("should allow use of the stripQueryString helper", async () => { it("should allow use of the stripQueryString helper", async () => {
const output = await processString('{{stripQuerystring url }}', { const output = await processString("{{stripQuerystring url }}", {
url, url,
}) })
expect(output).toBe("http://example.com") expect(output).toBe("http://example.com")
@ -149,10 +148,12 @@ describe("test the url helpers", () => {
const output = await processString("{{ object ( urlParse url ) }}", { const output = await processString("{{ object ( urlParse url ) }}", {
url, url,
}) })
expect(output).toBe("{\"protocol\":\"http:\",\"slashes\":true,\"auth\":null,\"host\":\"example.com\"," + expect(output).toBe(
"\"port\":null,\"hostname\":\"example.com\",\"hash\":null,\"search\":\"?query=1\"," + '{"protocol":"http:","slashes":true,"auth":null,"host":"example.com",' +
"\"query\":\"query=1\",\"pathname\":\"/\",\"path\":\"/?query=1\"," + '"port":null,"hostname":"example.com","hash":null,"search":"?query=1",' +
"\"href\":\"http://example.com/?query=1\"}") '"query":"query=1","pathname":"/","path":"/?query=1",' +
'"href":"http://example.com/?query=1"}'
)
}) })
}) })
@ -224,19 +225,25 @@ describe("test the string helpers", () => {
}) })
it("should allow use of the startsWith helper", async () => { it("should allow use of the startsWith helper", async () => {
const output = await processString("{{ #startsWith 'Hello' string }}Hi!{{ else }}Goodbye!{{ /startsWith }}", { const output = await processString(
string: "Hello my name is Mike", "{{ #startsWith 'Hello' string }}Hi!{{ else }}Goodbye!{{ /startsWith }}",
}) {
string: "Hello my name is Mike",
}
)
expect(output).toBe("Hi!") expect(output).toBe("Hi!")
}) })
}) })
describe("test the comparison helpers", () => { describe("test the comparison helpers", () => {
async function compare(func, a, b) { async function compare(func, a, b) {
const output = await processString(`{{ #${func} a b }}Success{{ else }}Fail{{ /${func} }}`, { const output = await processString(
a, `{{ #${func} a b }}Success{{ else }}Fail{{ /${func} }}`,
b, {
}) a,
b,
}
)
expect(output).toBe("Success") expect(output).toBe("Success")
} }
it("should allow use of the lt helper", async () => { it("should allow use of the lt helper", async () => {
@ -256,9 +263,12 @@ describe("test the comparison helpers", () => {
}) })
it("should allow use of gte with a literal value", async () => { it("should allow use of gte with a literal value", async () => {
const output = await processString(`{{ #gte a "50" }}s{{ else }}f{{ /gte }}`, { const output = await processString(
a: 51, `{{ #gte a "50" }}s{{ else }}f{{ /gte }}`,
}) {
a: 51,
}
)
expect(output).toBe("s") expect(output).toBe("s")
}) })
}) })
@ -273,21 +283,31 @@ describe("Test the literal helper", () => {
it("should allow use of the literal specifier for an object", async () => { it("should allow use of the literal specifier for an object", async () => {
const output = await processString(`{{literal a}}`, { const output = await processString(`{{literal a}}`, {
a: {b: 1}, a: { b: 1 },
}) })
expect(output.b).toBe(1) expect(output.b).toBe(1)
}) })
it("should allow use of the literal specifier for an object with dashes", async () => {
const output = await processString(`{{literal a}}`, {
a: { b: "i-have-dashes" },
})
expect(output.b).toBe("i-have-dashes")
})
}) })
describe("Cover a few complex use cases", () => { describe("Cover a few complex use cases", () => {
it("should allow use of three different collection helpers", async () => { it("should allow use of three different collection helpers", async () => {
const output = await processString(`{{ join ( after ( split "My name is: Joe Smith" " " ) 3 ) " " }}`, {}) const output = await processString(
`{{ join ( after ( split "My name is: Joe Smith" " " ) 3 ) " " }}`,
{}
)
expect(output).toBe("Joe Smith") expect(output).toBe("Joe Smith")
}) })
it("should allow a complex array case", async () => { it("should allow a complex array case", async () => {
const output = await processString("{{ last ( sort ( unique array ) ) }}", { const output = await processString("{{ last ( sort ( unique array ) ) }}", {
array: ["a", "a", "d", "c", "e"] array: ["a", "a", "d", "c", "e"],
}) })
expect(output).toBe("e") expect(output).toBe("e")
}) })
@ -299,7 +319,9 @@ describe("Cover a few complex use cases", () => {
}) })
it("should make sure case is valid", () => { it("should make sure case is valid", () => {
const validity = isValid("{{ avg [c355ec2b422e54f988ae553c8acd811ea].[a] [c355ec2b422e54f988ae553c8acd811ea].[b] }}") const validity = isValid(
"{{ avg [c355ec2b422e54f988ae553c8acd811ea].[a] [c355ec2b422e54f988ae553c8acd811ea].[b] }}"
)
expect(validity).toBe(true) expect(validity).toBe(true)
}) })
@ -314,7 +336,9 @@ describe("Cover a few complex use cases", () => {
}) })
it("should confirm a subtraction validity", () => { it("should confirm a subtraction validity", () => {
const validity = isValid("{{ subtract [c390c23a7f1b6441c98d2fe2a51248ef3].[total profit] [c390c23a7f1b6441c98d2fe2a51248ef3].[total revenue] }}") const validity = isValid(
"{{ subtract [c390c23a7f1b6441c98d2fe2a51248ef3].[total profit] [c390c23a7f1b6441c98d2fe2a51248ef3].[total revenue] }}"
)
expect(validity).toBe(true) expect(validity).toBe(true)
}) })
@ -344,9 +368,11 @@ describe("Cover a few complex use cases", () => {
}) })
it("getting a nice date from the user", async () => { it("getting a nice date from the user", async () => {
const input = {text: `{{ date user.subscriptionDue "DD-MM" }}`} const input = { text: `{{ date user.subscriptionDue "DD-MM" }}` }
const context = JSON.parse(`{"user":{"email":"test@test.com","roleId":"ADMIN","type":"user","tableId":"ta_users","subscriptionDue":"2021-01-12T12:00:00.000Z","_id":"ro_ta_users_us_test@test.com","_rev":"2-24cc794985eb54183ecb93e148563f3d"}}`) const context = JSON.parse(
`{"user":{"email":"test@test.com","roleId":"ADMIN","type":"user","tableId":"ta_users","subscriptionDue":"2021-01-12T12:00:00.000Z","_id":"ro_ta_users_us_test@test.com","_rev":"2-24cc794985eb54183ecb93e148563f3d"}}`
)
const output = await processObject(input, context) const output = await processObject(input, context)
expect(output.text).toBe("12-01") expect(output.text).toBe("12-01")
}) })
}) })