merge with develop

This commit is contained in:
Martin McKeaveney 2021-02-19 12:09:17 +00:00
commit 8a60131c7e
69 changed files with 1826 additions and 1447 deletions

View File

@ -28,7 +28,7 @@ context("Create a View", () => {
const headers = Array.from($headers).map(header =>
header.textContent.trim()
)
expect(headers).to.deep.eq(["group", "age", "rating"])
expect(headers).to.deep.eq([ 'rating', 'age', 'group' ])
})
})
@ -53,27 +53,20 @@ context("Create a View", () => {
cy.wait(50)
cy.get(".menu-container").find("select").eq(1).select("age")
cy.contains("Save").click()
cy.wait(100)
cy.get(".ag-center-cols-viewport").scrollTo("100%")
cy.get("[data-cy=table-header]").then($headers => {
expect($headers).to.have.length(7)
const headers = Array.from($headers).map(header =>
header.textContent.trim()
)
expect(headers).to.deep.eq([
"field",
"sum",
"min",
"max",
"count",
"sumsqr",
"avg",
])
expect(headers).to.deep.eq([ 'avg', 'sumsqr', 'count', 'max', 'min', 'sum', 'field' ])
})
cy.get(".ag-cell").then($values => {
const values = Array.from($values).map(header =>
let values = Array.from($values).map(header =>
header.textContent.trim()
)
expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"])
expect(values).to.deep.eq([ '31', '5347', '5', '49', '20', '155', 'age' ])
})
})
@ -92,15 +85,7 @@ context("Create a View", () => {
.find(".ag-cell")
.then($values => {
const values = Array.from($values).map(value => value.textContent)
expect(values).to.deep.eq([
"Students",
"70",
"20",
"25",
"3",
"1650",
"23.333333333333332",
])
expect(values).to.deep.eq([ 'Students', '23.333333333333332', '1650', '3', '25', '20', '70' ])
})
})

View File

@ -63,7 +63,7 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.58.5",
"@budibase/bbui": "^1.58.8",
"@budibase/client": "^0.7.8",
"@budibase/colorpicker": "1.0.1",
"@budibase/string-templates": "^0.7.8",

View File

@ -11,21 +11,24 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
/**
* Gets all bindable data context fields and instance fields.
*/
export const getBindableProperties = (rootComponent, componentId) => {
return getContextBindings(rootComponent, componentId)
export const getBindableProperties = (asset, componentId) => {
const contextBindings = getContextBindings(asset, componentId)
const userBindings = getUserBindings()
const urlBindings = getUrlBindings(asset, componentId)
return [...contextBindings, ...userBindings, ...urlBindings]
}
/**
* Gets all data provider components above a component.
*/
export const getDataProviderComponents = (rootComponent, componentId) => {
if (!rootComponent || !componentId) {
export const getDataProviderComponents = (asset, componentId) => {
if (!asset || !componentId) {
return []
}
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(rootComponent, componentId)
const path = findComponentPath(asset.props, componentId)
path.pop()
// Filter by only data provider components
@ -38,18 +41,14 @@ export const getDataProviderComponents = (rootComponent, componentId) => {
/**
* Gets all data provider components above a component.
*/
export const getActionProviderComponents = (
rootComponent,
componentId,
actionType
) => {
if (!rootComponent || !componentId) {
export const getActionProviderComponents = (asset, componentId, actionType) => {
if (!asset || !componentId) {
return []
}
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(rootComponent, componentId)
const path = findComponentPath(asset.props, componentId)
path.pop()
// Filter by only data provider components
@ -92,13 +91,12 @@ export const getDatasourceForProvider = component => {
}
/**
* Gets all bindable data contexts. These are fields of schemas of data contexts
* provided by data provider components, such as lists or row detail components.
* Gets all bindable data properties from component data contexts.
*/
export const getContextBindings = (rootComponent, componentId) => {
const getContextBindings = (asset, componentId) => {
// Extract any components which provide data contexts
const dataProviders = getDataProviderComponents(rootComponent, componentId)
let contextBindings = []
const dataProviders = getDataProviderComponents(asset, componentId)
let bindings = []
// Create bindings for each data provider
dataProviders.forEach(component => {
@ -109,7 +107,7 @@ export const getContextBindings = (rootComponent, componentId) => {
// Forms are an edge case which do not need table schemas
if (isForm) {
schema = buildFormSchema(component)
tableName = "Schema"
tableName = "Fields"
} else {
if (!datasource) {
return
@ -143,7 +141,7 @@ export const getContextBindings = (rootComponent, componentId) => {
runtimeBoundKey = `${key}_first`
}
contextBindings.push({
bindings.push({
type: "context",
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
runtimeBoundKey
@ -157,7 +155,14 @@ export const getContextBindings = (rootComponent, componentId) => {
})
})
// Add logged in user bindings
return bindings
}
/**
* Gets all bindable properties from the logged in user.
*/
const getUserBindings = () => {
let bindings = []
const tables = get(backendUiStore).tables
const userTable = tables.find(table => table._id === TableNames.USERS)
const schema = {
@ -176,7 +181,7 @@ export const getContextBindings = (rootComponent, componentId) => {
runtimeBoundKey = `${key}_first`
}
contextBindings.push({
bindings.push({
type: "context",
runtimeBinding: `user.${runtimeBoundKey}`,
readableBinding: `Current User.${key}`,
@ -187,7 +192,26 @@ export const getContextBindings = (rootComponent, componentId) => {
})
})
return contextBindings
return bindings
}
/**
* Gets all bindable properties from URL parameters.
*/
const getUrlBindings = asset => {
const url = asset?.routing?.route ?? ""
const split = url.split("/")
let params = []
split.forEach(part => {
if (part.startsWith(":") && part.length > 1) {
params.push(part.replace(/:/g, "").replace(/\?/g, ""))
}
})
return params.map(param => ({
type: "context",
runtimeBinding: `url.${param}`,
readableBinding: `URL.${param}`,
}))
}
/**

View File

@ -179,6 +179,10 @@ export function makeDatasourceFormComponents(datasource) {
let fields = Object.keys(schema || {})
fields.forEach(field => {
const fieldSchema = schema[field]
// skip autocolumns
if (fieldSchema.autocolumn) {
return
}
const fieldType =
typeof fieldSchema === "object" ? fieldSchema.type : fieldSchema
const componentType = fieldTypeToComponentMap[fieldType]

View File

@ -0,0 +1,55 @@
import { TableNames } from "../constants"
import {
AUTO_COLUMN_DISPLAY_NAMES,
AUTO_COLUMN_SUB_TYPES,
FIELDS,
isAutoColumnUserRelationship,
} from "../constants/backend"
export function getAutoColumnInformation(enabled = true) {
let info = {}
for (let [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
info[subtype] = { enabled, name: AUTO_COLUMN_DISPLAY_NAMES[key] }
}
return info
}
export function buildAutoColumn(tableName, name, subtype) {
let type, constraints
switch (subtype) {
case AUTO_COLUMN_SUB_TYPES.UPDATED_BY:
case AUTO_COLUMN_SUB_TYPES.CREATED_BY:
type = FIELDS.LINK.type
constraints = FIELDS.LINK.constraints
break
case AUTO_COLUMN_SUB_TYPES.AUTO_ID:
type = FIELDS.NUMBER.type
constraints = FIELDS.NUMBER.constraints
break
case AUTO_COLUMN_SUB_TYPES.UPDATED_AT:
case AUTO_COLUMN_SUB_TYPES.CREATED_AT:
type = FIELDS.DATETIME.type
constraints = FIELDS.DATETIME.constraints
break
default:
type = FIELDS.STRING.type
constraints = FIELDS.STRING.constraints
break
}
if (Object.values(AUTO_COLUMN_SUB_TYPES).indexOf(subtype) === -1) {
throw "Cannot build auto column with supplied subtype"
}
const base = {
name,
type,
subtype,
icon: "ri-magic-line",
autocolumn: true,
constraints,
}
if (isAutoColumnUserRelationship(subtype)) {
base.tableId = TableNames.USERS
base.fieldName = `${tableName}-${name}`
}
return base
}

View File

@ -30,20 +30,22 @@
{#if schemaFields.length}
<div class="schema-fields">
{#each schemaFields as [field, schema]}
{#if schemaHasOptions(schema)}
<Select label={field} extraThin secondary bind:value={value[field]}>
<option value="">Choose an option</option>
{#each schema.constraints.inclusion as option}
<option value={option}>{option}</option>
{/each}
</Select>
{:else if schema.type === 'string' || schema.type === 'number'}
<BindableInput
extraThin
bind:value={value[field]}
label={field}
type="string"
{bindings} />
{#if !schema.autocolumn}
{#if schemaHasOptions(schema)}
<Select label={field} extraThin secondary bind:value={value[field]}>
<option value="">Choose an option</option>
{#each schema.constraints.inclusion as option}
<option value={option}>{option}</option>
{/each}
</Select>
{:else if schema.type === 'string' || schema.type === 'number'}
<BindableInput
extraThin
bind:value={value[field]}
label={field}
type="string"
{bindings} />
{/if}
{/if}
{/each}
</div>

View File

@ -6,15 +6,16 @@
import ExportButton from "./buttons/ExportButton.svelte"
import EditRolesButton from "./buttons/EditRolesButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
import * as api from "./api"
import Table from "./Table.svelte"
import { TableNames } from "constants"
import CreateEditUser from "./modals/CreateEditUser.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte"
let hideAutocolumns = true
let data = []
let loading = false
$: isUsersTable = $backendUiStore.selectedTable?._id === TableNames.USERS
$: title = $backendUiStore.selectedTable.name
$: schema = $backendUiStore.selectedTable.schema
@ -41,6 +42,7 @@
tableId={$backendUiStore.selectedTable?._id}
{data}
allowEditing={true}
bind:hideAutocolumns
{loading}>
<CreateColumnButton />
{#if schema && Object.keys(schema).length > 0}
@ -49,9 +51,11 @@
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
<CreateViewButton />
<ManageAccessButton resourceId={$backendUiStore.selectedTable?._id} />
{#if isUsersTable}
<EditRolesButton />
{/if}
<HideAutocolumnButton bind:hideAutocolumns />
<!-- always have the export last -->
<ExportButton view={tableView} />
{/if}
{#if isUsersTable}
<EditRolesButton />
{/if}
</Table>

View File

@ -24,6 +24,7 @@
export let allowEditing = false
export let loading = false
export let theme = "alpine"
export let hideAutocolumns
let columnDefs = []
let selectedRows = []
@ -85,8 +86,12 @@
return !(isUsersTable && ["email", "roleId"].includes(key))
}
Object.entries(schema || {}).forEach(([key, value]) => {
result.push({
for (let [key, value] of Object.entries(schema || {})) {
// skip autocolumns if hiding
if (hideAutocolumns && value.autocolumn) {
continue
}
let config = {
headerCheckboxSelection: false,
headerComponent: TableHeader,
headerComponentParams: {
@ -107,9 +112,14 @@
autoHeight: true,
resizable: true,
minWidth: 200,
})
})
}
// sort auto-columns to the end if they are present
if (value.autocolumn) {
result.push(config)
} else {
result.unshift(config)
}
}
columnDefs = result
}
@ -150,13 +160,15 @@
</div>
</div>
<div class="grid-wrapper">
<AgGrid
{theme}
{options}
{data}
{columnDefs}
{loading}
on:select={({ detail }) => (selectedRows = detail)} />
{#key columnDefs.length}
<AgGrid
{theme}
{options}
{data}
{columnDefs}
{loading}
on:select={({ detail }) => (selectedRows = detail)} />
{/key}
</div>
<style>

View File

@ -59,8 +59,11 @@
on:mouseover={() => (hovered = true)}
on:mouseleave={() => (hovered = false)}>
<div>
<span class="column-header-name">{displayName}</span>
<i class={`${SORT_ICON_MAP[sortDirection]} sort-icon`} />
<div class="col-icon">
{#if field.autocolumn}<i class="auto ri-magic-fill" />{/if}
<span class="column-header-name">{displayName}</span>
</div>
<i class={`${SORT_ICON_MAP[sortDirection]} sort-icon icon`} />
</div>
<Modal bind:this={modal}>
<ModalContent
@ -73,11 +76,11 @@
<section class:show={hovered || filterActive}>
{#if editable && hovered}
<span on:click|stopPropagation={showModal}>
<i class="ri-pencil-line" />
<i class="ri-pencil-line icon" />
</span>
{/if}
<span on:click|stopPropagation={toggleMenu} bind:this={menuButton}>
<i class="ri-filter-line" class:active={filterActive} />
<i class="ri-filter-line icon" class:active={filterActive} />
</span>
</section>
</header>
@ -117,18 +120,29 @@
top: 2px;
}
i {
.icon {
transition: 0.2s all;
font-size: var(--font-size-m);
font-weight: 500;
}
i:hover {
.col-icon {
display: flex;
}
.auto {
font-size: var(--font-size-xs);
transition: none;
margin-right: 6px;
margin-top: 2px;
}
.icon:hover {
color: var(--blue);
}
i.active,
i:hover {
.icon.active,
.icon:hover {
color: var(--blue);
}
</style>

View File

@ -7,8 +7,10 @@
import FilterButton from "./buttons/FilterButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
export let view = {}
let hideAutocolumns = true
let data = []
let loading = false
@ -48,12 +50,18 @@
}
</script>
<Table title={decodeURI(name)} schema={view.schema} {data} {loading}>
<Table
title={decodeURI(name)}
schema={view.schema}
{data}
{loading}
bind:hideAutocolumns>
<FilterButton {view} />
<CalculateButton {view} />
{#if view.calculation}
<GroupByButton {view} />
{/if}
<ManageAccessButton resourceId={decodeURI(name)} />
<HideAutocolumnButton bind:hideAutocolumns />
<ExportButton {view} />
</Table>

View File

@ -0,0 +1,27 @@
<script>
import { TextButton } from "@budibase/bbui"
export let hideAutocolumns
let anchor
let dropdown
function hideOrUnhide() {
hideAutocolumns = !hideAutocolumns
}
</script>
<div bind:this={anchor}>
<TextButton text small on:click={hideOrUnhide}>
{#if hideAutocolumns}
<i class="ri-magic-line" />
Show Auto Columns
{:else}<i class="ri-magic-fill" /> Hide Auto Columns{/if}
</TextButton>
</div>
<style>
i {
margin-right: 4px;
}
</style>

View File

@ -10,12 +10,14 @@
import { cloneDeep } from "lodash/fp"
import { backendUiStore } from "builderStore"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import { FIELDS } from "constants/backend"
import { FIELDS, AUTO_COLUMN_SUB_TYPES } from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import { notifier } from "builderStore/store/notifications"
import ValuesList from "components/common/ValuesList.svelte"
import DatePicker from "components/common/DatePicker.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
const AUTO_COL = "auto"
let fieldDefinitions = cloneDeep(FIELDS)
export let onClosed
@ -43,7 +45,23 @@
$backendUiStore.selectedTable?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(field.name)
// used to select what different options can be displayed for column type
$: canBeSearched =
field.type !== "link" &&
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY
$: canBeDisplay = field.type !== "link" && field.type !== AUTO_COL
$: canBeRequired =
field.type !== "link" && !uneditable && field.type !== AUTO_COL
async function saveColumn() {
if (field.type === AUTO_COL) {
field = buildAutoColumn(
$backendUiStore.draftTable.name,
field.name,
field.subtype
)
}
backendUiStore.update(state => {
backendUiStore.actions.tables.saveField({
originalName,
@ -67,11 +85,12 @@
}
function handleFieldConstraints(event) {
const { type, constraints } = fieldDefinitions[
event.target.value.toUpperCase()
]
field.type = type
field.constraints = constraints
const definition = fieldDefinitions[event.target.value.toUpperCase()]
if (!definition) {
return
}
field.type = definition.type
field.constraints = definition.constraints
}
function onChangeRequired(e) {
@ -124,9 +143,10 @@
{#each Object.values(fieldDefinitions) as field}
<option value={field.type}>{field.name}</option>
{/each}
<option value={AUTO_COL}>Auto Column</option>
</Select>
{#if field.type !== 'link' && !uneditable}
{#if canBeRequired}
<Toggle
checked={required}
on:change={onChangeRequired}
@ -135,7 +155,7 @@
text="Required" />
{/if}
{#if field.type !== 'link'}
{#if canBeDisplay}
<Toggle
bind:checked={primaryDisplay}
on:change={onChangePrimaryDisplay}
@ -143,6 +163,8 @@
text="Use as table display column" />
<Label grey small>Search Indexes</Label>
{/if}
{#if canBeSearched}
<Toggle
checked={indexes[0] === field.name}
disabled={indexes[1] === field.name}
@ -194,9 +216,16 @@
label={`Column Name in Other Table`}
thin
bind:value={field.fieldName} />
{:else if field.type === AUTO_COL}
<Select label="Auto Column Type" thin secondary bind:value={field.subtype}>
<option value="">Choose a subtype</option>
{#each Object.entries(getAutoColumnInformation()) as [subtype, info]}
<option value={subtype}>{info.name}</option>
{/each}
</Select>
{/if}
<footer class="create-column-options">
{#if !uneditable && originalName}
{#if !uneditable && originalName != null}
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
{/if}
<Button secondary on:click={onClosed}>Cancel</Button>

View File

@ -40,9 +40,11 @@
onConfirm={saveRow}>
<ErrorsBox {errors} />
{#each tableSchema as [key, meta]}
<div>
<RowFieldControl {meta} bind:value={row[key]} />
</div>
{#if !meta.autocolumn}
<div>
<RowFieldControl {meta} bind:value={row[key]} />
</div>
{/if}
{/each}
</ModalContent>

View File

@ -30,7 +30,9 @@
Object.keys(viewTable.schema).filter(
field =>
view.calculation === "count" ||
viewTable.schema[field].type === "number"
// don't want to perform calculations based on auto ID
(viewTable.schema[field].type === "number" &&
!viewTable.schema[field].autocolumn)
)
function saveView() {

View File

@ -2,17 +2,11 @@
import { goto } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import {
Input,
Label,
ModalContent,
Button,
Spacer,
Toggle,
} from "@budibase/bbui"
import { Input, Label, ModalContent, Toggle } from "@budibase/bbui"
import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics"
import screenTemplates from "builderStore/store/screenTemplates"
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen"
import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen"
@ -23,15 +17,28 @@
ROW_LIST_TEMPLATE,
]
$: tableNames = $backendUiStore.tables.map(table => table.name)
let modal
let name
let dataImport
let error = ""
let createAutoscreens = true
let autoColumns = getAutoColumnInformation()
function addAutoColumns(tableName, schema) {
for (let [subtype, col] of Object.entries(autoColumns)) {
if (!col.enabled) {
continue
}
schema[col.name] = buildAutoColumn(tableName, col.name, subtype)
}
return schema
}
function checkValid(evt) {
const tableName = evt.target.value
if ($backendUiStore.models?.some(model => model.name === tableName)) {
if (tableNames.includes(tableName)) {
error = `Table with name ${tableName} already exists. Please choose another name.`
return
}
@ -41,7 +48,7 @@
async function saveTable() {
let newTable = {
name,
schema: dataImport.schema || {},
schema: addAutoColumns(name, dataImport.schema || {}),
dataImport,
}
@ -93,6 +100,28 @@
on:input={checkValid}
bind:value={name}
{error} />
<div class="autocolumns">
<Label extraSmall grey>Auto Columns</Label>
<div class="toggles">
<div class="toggle-1">
<Toggle
text="Created by"
bind:checked={autoColumns.createdBy.enabled} />
<Toggle
text="Created at"
bind:checked={autoColumns.createdAt.enabled} />
<Toggle text="Auto ID" bind:checked={autoColumns.autoID.enabled} />
</div>
<div class="toggle-2">
<Toggle
text="Updated by"
bind:checked={autoColumns.updatedBy.enabled} />
<Toggle
text="Updated at"
bind:checked={autoColumns.updatedAt.enabled} />
</div>
</div>
</div>
<Toggle
text="Generate screens in the design section"
bind:checked={createAutoscreens} />
@ -101,3 +130,25 @@
<TableDataImport bind:dataImport />
</div>
</ModalContent>
<style>
.autocolumns {
padding-bottom: 10px;
border-bottom: 3px solid var(--grey-1);
}
.toggles {
display: flex;
width: 100%;
margin-top: 6px;
}
.toggle-1 :global(> *) {
margin-bottom: 10px;
}
.toggle-2 :global(> *) {
margin-bottom: 10px;
margin-left: 20px;
}
</style>

View File

@ -0,0 +1,84 @@
<script>
import { Icon, Input, Drawer, Body, Button } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value = ""
export let bindings = []
let bindingDrawer
let tempValue = value
$: readableValue = runtimeToReadableBinding(bindings, value)
const handleClose = () => {
onChange(tempValue)
bindingDrawer.hide()
}
const onChange = value => {
dispatch("change", readableToRuntimeBinding(bindings, value))
}
</script>
<div class="control">
<Input
thin
value={readableValue}
on:change={event => onChange(event.target.value)}
placeholder="/screen" />
<div class="icon" on:click={bindingDrawer.show}>
<Icon name="lightning" />
</div>
</div>
<Drawer bind:this={bindingDrawer} title="Bindings">
<div slot="description">
<Body extraSmall grey>
Add the objects on the left to enrich your text.
</Body>
</div>
<heading slot="buttons">
<Button thin blue on:click={handleClose}>Save</Button>
</heading>
<div slot="body">
<BindingPanel
value={readableValue}
close={handleClose}
on:update={event => (tempValue = event.detail)}
bindableProperties={bindings} />
</div>
</Drawer>
<style>
.control {
flex: 1;
margin-left: var(--spacing-l);
position: relative;
}
.icon {
right: 2px;
top: 2px;
bottom: 2px;
position: absolute;
align-items: center;
display: flex;
box-sizing: border-box;
padding-left: 7px;
border-left: 1px solid var(--grey-4);
background-color: var(--grey-2);
border-top-right-radius: var(--border-radius-m);
border-bottom-right-radius: var(--border-radius-m);
color: var(--grey-7);
font-size: 14px;
}
.icon:hover {
color: var(--ink);
cursor: pointer;
}
</style>

View File

@ -24,7 +24,7 @@
$: value && checkValid()
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: dispatch("update", value)

View File

@ -47,7 +47,7 @@
type: "query",
}))
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: queryBindableProperties = bindableProperties.map(property => ({

View File

@ -10,7 +10,7 @@
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: {

View File

@ -10,7 +10,7 @@
ds => ds._id === parameters.datasourceId
)
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
).map(property => ({
...property,

View File

@ -1,18 +1,23 @@
<script>
import { DataList, Label } from "@budibase/bbui"
import { allScreens } from "builderStore"
import { Label } from "@budibase/bbui"
import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
export let parameters
let bindingDrawer
let tempValue = parameters.url
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
</script>
<div class="root">
<Label size="m" color="dark">Screen</Label>
<DataList secondary bind:value={parameters.url}>
<option value="" />
{#each $allScreens as screen}
<option value={screen.routing.route}>{screen.props._instanceName}</option>
{/each}
</DataList>
<DrawerBindableInput
value={parameters.url}
on:change={value => (parameters.url = value.detail)}
{bindings} />
</div>
<style>

View File

@ -6,7 +6,7 @@
export let parameters
$: dataProviders = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
</script>

View File

@ -25,7 +25,7 @@
const emptyField = () => ({ name: "", value: "" })
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)

View File

@ -11,7 +11,7 @@
export let parameters
$: dataProviderComponents = getDataProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: providerComponent = dataProviderComponents.find(

View File

@ -6,7 +6,7 @@
export let parameters
$: actionProviders = getActionProviderComponents(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId,
"ValidateForm"
)

View File

@ -24,7 +24,7 @@
let valid
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$currentAsset,
$store.selectedComponentId
)
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)

View File

@ -1,80 +0,0 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, allScreens, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
export let value = ""
$: urls = getUrls($allScreens, $currentAsset, $store.selectedComponentId)
// Update value on blur
const dispatch = createEventDispatcher()
const handleBlur = () => dispatch("change", value)
// Get all valid screen URL, as well as detail screens which can be used in
// the current data context
const getUrls = (screens, asset, componentId) => {
// Get all screens which aren't detail screens
let urls = screens
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({
name: screen.props._instanceName,
url: screen.routing.route,
sort: screen.props._component,
}))
// Add detail screens enriched with the current data context
const bindableProperties = getBindableProperties(asset.props, componentId)
screens
.filter(screen => screen.props._component.endsWith("/rowdetail"))
.forEach(detailScreen => {
// Find any _id bindings that match the detail screen's table
const binding = bindableProperties.find(p => {
return (
p.type === "context" &&
p.runtimeBinding.endsWith("._id") &&
p.tableId === detailScreen.props.table
)
})
if (binding) {
urls.push({
name: detailScreen.props._instanceName,
url: detailScreen.routing.route.replace(
":id",
`{{ ${binding.runtimeBinding} }}`
),
sort: detailScreen.props._component,
})
}
})
return urls
}
</script>
<div>
<DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" />
{#each urls as url}
<option value={url.url}>{url.name}</option>
{/each}
</DataList>
</div>
<style>
div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
div :global(> div) {
flex: 1 1 auto;
}
</style>

View File

@ -18,7 +18,6 @@
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
import EventsEditor from "./PropertyControls/EventsEditor"
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
import { IconSelect } from "./PropertyControls/IconSelect"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
@ -62,7 +61,6 @@
text: Input,
select: OptionSelect,
datasource: DatasourceSelect,
screen: ScreenSelect,
detailScreen: DetailScreenSelect,
boolean: Checkbox,
number: Input,

View File

@ -14,7 +14,6 @@
} from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import api from "builderStore/api"
import { FIELDS } from "constants/backend"
import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"

View File

@ -82,6 +82,22 @@ export const FIELDS = {
},
}
export const AUTO_COLUMN_SUB_TYPES = {
AUTO_ID: "autoID",
CREATED_BY: "createdBy",
CREATED_AT: "createdAt",
UPDATED_BY: "updatedBy",
UPDATED_AT: "updatedAt",
}
export const AUTO_COLUMN_DISPLAY_NAMES = {
AUTO_ID: "Auto ID",
CREATED_BY: "Created By",
CREATED_AT: "Created At",
UPDATED_BY: "Updated By",
UPDATED_AT: "Updated At",
}
export const FILE_TYPES = {
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],
@ -100,3 +116,10 @@ export const Roles = {
PUBLIC: "PUBLIC",
BUILDER: "BUILDER",
}
export function isAutoColumnUserRelationship(subtype) {
return (
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY ||
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
<script>
import { getContext, setContext } from "svelte"
import { getContext } from "svelte"
import Router from "svelte-spa-router"
import { routeStore } from "../store"
import Screen from "./Screen.svelte"
@ -10,12 +10,10 @@
// Only wrap this as an array to take advantage of svelte keying,
// to ensure the svelte-spa-router is fully remounted when route config
// changes
$: configs = [
{
routes: getRouterConfig($routeStore.routes),
id: $routeStore.routeSessionId,
},
]
$: config = {
routes: getRouterConfig($routeStore.routes),
id: $routeStore.routeSessionId,
}
const getRouterConfig = routes => {
let config = {}
@ -33,11 +31,11 @@
}
</script>
{#each configs as config (config.id)}
{#key config.id}
<div use:styleable={$component.styles}>
<Router on:routeLoading={onRouteLoading} routes={config.routes} />
</div>
{/each}
{/key}
<style>
div {

View File

@ -2,6 +2,7 @@
import { fade } from "svelte/transition"
import { screenStore, routeStore } from "../store"
import Component from "./Component.svelte"
import Provider from "./Provider.svelte"
// Keep route params up to date
export let params = {}
@ -12,16 +13,16 @@
// Redirect to home layout if no matching route
$: screenDefinition == null && routeStore.actions.navigate("/")
// Make a screen array so we can use keying to properly re-render each screen
$: screens = screenDefinition ? [screenDefinition] : []
</script>
{#each screens as screen (screen._id)}
<div in:fade>
<Component definition={screen} />
</div>
{/each}
<!-- Ensure to fully remount when screen changes -->
{#key screenDefinition?._id}
<Provider key="url" data={params}>
<div in:fade>
<Component definition={screenDefinition} />
</div>
</Provider>
{/key}
<style>
div {

View File

@ -44,7 +44,7 @@ export const enrichProps = async (props, context) => {
let enrichedProps = await enrichDataBindings(validProps, totalContext)
// Enrich button actions if they exist
if (props._component.endsWith("/button") && enrichedProps.onClick) {
if (props._component?.endsWith("/button") && enrichedProps.onClick) {
enrichedProps.onClick = enrichButtonActions(
enrichedProps.onClick,
totalContext

View File

@ -14,7 +14,6 @@ exports.fetchInfo = async ctx => {
}
exports.save = async ctx => {
console.trace("DID A SAVE!")
const db = new CouchDB(BUILDER_CONFIG_DB)
const { type } = ctx.request.body
if (type === HostingTypes.CLOUD && ctx.request.body._rev) {

View File

@ -1,5 +1,5 @@
const {
BUILTIN_PERMISSIONS,
getBuiltinPermissions,
PermissionLevels,
isPermissionLevelHigherThanRead,
higherPermission,
@ -8,11 +8,10 @@ const {
isBuiltin,
getDBRoleID,
getExternalRoleID,
BUILTIN_ROLES,
getBuiltinRoles,
} = require("../../utilities/security/roles")
const { getRoleParams } = require("../../db/utils")
const CouchDB = require("../../db")
const { cloneDeep } = require("lodash/fp")
const {
CURRENTLY_SUPPORTED_LEVELS,
getBasePermissions,
@ -65,7 +64,7 @@ async function updatePermissionOnRole(
// the permission is for a built in, make sure it exists
if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) {
const builtin = cloneDeep(BUILTIN_ROLES[roleId])
const builtin = getBuiltinRoles()[roleId]
builtin._id = getDBRoleID(builtin._id)
dbRoles.push(builtin)
}
@ -110,7 +109,7 @@ async function updatePermissionOnRole(
}
exports.fetchBuiltin = function(ctx) {
ctx.body = Object.values(BUILTIN_PERMISSIONS)
ctx.body = Object.values(getBuiltinPermissions())
}
exports.fetchLevels = function(ctx) {

View File

@ -1,6 +1,6 @@
const CouchDB = require("../../db")
const {
BUILTIN_ROLES,
getBuiltinRoles,
BUILTIN_ROLE_IDS,
Role,
getRole,
@ -58,10 +58,11 @@ exports.fetch = async function(ctx) {
})
)
let roles = body.rows.map(row => row.doc)
const builtinRoles = getBuiltinRoles()
// need to combine builtin with any DB record of them (for sake of permissions)
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
const builtinRole = BUILTIN_ROLES[builtinRoleId]
const builtinRole = builtinRoles[builtinRoleId]
const dbBuiltin = roles.filter(
dbRole => getExternalRoleID(dbRole._id) === builtinRoleId
)[0]

View File

@ -9,7 +9,11 @@ const {
ViewNames,
} = require("../../db/utils")
const usersController = require("./user")
const { coerceRowValues, enrichRows } = require("../../utilities")
const {
inputProcessing,
outputProcessing,
} = require("../../utilities/rowProcessor")
const { FieldTypes } = require("../../constants")
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
@ -54,18 +58,17 @@ async function findRow(db, appId, tableId, rowId) {
exports.patch = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
let row = await db.get(ctx.params.rowId)
const table = await db.get(row.tableId)
let dbRow = await db.get(ctx.params.rowId)
let dbTable = await db.get(dbRow.tableId)
const patchfields = ctx.request.body
// need to build up full patch fields before coerce
for (let key of Object.keys(patchfields)) {
if (!table.schema[key]) continue
row[key] = patchfields[key]
if (!dbTable.schema[key]) continue
dbRow[key] = patchfields[key]
}
row = coerceRowValues(row, table)
// this returns the table and row incase they have been updated
let { table, row } = await inputProcessing(ctx.user, dbTable, dbRow)
const validateResult = await validate({
row,
table,
@ -110,32 +113,34 @@ exports.patch = async function(ctx) {
exports.save = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
let row = ctx.request.body
row.tableId = ctx.params.tableId
let inputs = ctx.request.body
inputs.tableId = ctx.params.tableId
// TODO: find usage of this and break out into own endpoint
if (ctx.request.body.type === "delete") {
if (inputs.type === "delete") {
await bulkDelete(ctx)
ctx.body = ctx.request.body.rows
ctx.body = inputs.rows
return
}
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
if (existingRow) {
ctx.params.rowId = row._id
ctx.params.rowId = inputs._id
await exports.patch(ctx)
return
}
if (!row._rev && !row._id) {
row._id = generateRowID(row.tableId)
if (!inputs._rev && !inputs._id) {
inputs._id = generateRowID(inputs.tableId)
}
const table = await db.get(row.tableId)
row = coerceRowValues(row, table)
// this returns the table and row incase they have been updated
let { table, row } = await inputProcessing(
ctx.user,
await db.get(inputs.tableId),
inputs
)
const validateResult = await validate({
row,
table,
@ -204,7 +209,7 @@ exports.fetchView = async function(ctx) {
schema: {},
}
}
ctx.body = await enrichRows(appId, table, response.rows)
ctx.body = await outputProcessing(appId, table, response.rows)
}
if (calculation === CALCULATION_TYPES.STATS) {
@ -258,7 +263,7 @@ exports.search = async function(ctx) {
const table = await db.get(ctx.params.tableId)
ctx.body = await enrichRows(appId, table, rows)
ctx.body = await outputProcessing(appId, table, rows)
}
exports.fetchTableRows = async function(ctx) {
@ -279,7 +284,7 @@ exports.fetchTableRows = async function(ctx) {
)
rows = response.rows.map(row => row.doc)
}
ctx.body = await enrichRows(appId, table, rows)
ctx.body = await outputProcessing(appId, table, rows)
}
exports.find = async function(ctx) {
@ -288,7 +293,7 @@ exports.find = async function(ctx) {
try {
const table = await db.get(ctx.params.tableId)
const row = await findRow(db, appId, ctx.params.tableId, ctx.params.rowId)
ctx.body = await enrichRows(appId, table, row)
ctx.body = await outputProcessing(appId, table, row)
} catch (err) {
ctx.throw(400, err)
}
@ -373,7 +378,7 @@ exports.fetchEnrichedRow = async function(ctx) {
keys: linkVals.map(linkVal => linkVal.id),
})
// need to include the IDs in these rows for any links they may have
let linkedRows = await enrichRows(
let linkedRows = await outputProcessing(
appId,
table,
response.rows.map(row => row.doc)
@ -381,9 +386,14 @@ exports.fetchEnrichedRow = async function(ctx) {
// insert the link rows in the correct place throughout the main row
for (let fieldName of Object.keys(table.schema)) {
let field = table.schema[fieldName]
if (field.type === "link") {
row[fieldName] = linkedRows.filter(
linkRow => linkRow.tableId === field.tableId
if (field.type === FieldTypes.LINK) {
// find the links that pertain to this field, get their indexes
const linkIndexes = linkVals
.filter(link => link.fieldName === fieldName)
.map(link => linkVals.indexOf(link))
// find the rows that the links state are linked to this field
row[fieldName] = linkedRows.filter((linkRow, index) =>
linkIndexes.includes(index)
)
}
}

View File

@ -8,6 +8,7 @@ const {
generateRowID,
} = require("../../db/utils")
const { isEqual } = require("lodash/fp")
const { FieldTypes, AutoFieldSubTypes } = require("../../constants")
async function checkForColumnUpdates(db, oldTable, updatedTable) {
let updatedRows
@ -39,6 +40,27 @@ async function checkForColumnUpdates(db, oldTable, updatedTable) {
return updatedRows
}
// makes sure the passed in table isn't going to reset the auto ID
function makeSureTableUpToDate(table, tableToSave) {
if (!table) {
return tableToSave
}
// sure sure rev is up to date
tableToSave._rev = table._rev
// make sure auto IDs are always updated - these are internal
// so the client may not know they have changed
for (let [field, column] of Object.entries(table.schema)) {
if (
column.autocolumn &&
column.subtype === AutoFieldSubTypes.AUTO_ID &&
tableToSave.schema[field]
) {
tableToSave.schema[field].lastID = column.lastID
}
}
return tableToSave
}
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const body = await db.allDocs(
@ -58,7 +80,7 @@ exports.save = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const { dataImport, ...rest } = ctx.request.body
const tableToSave = {
let tableToSave = {
type: "table",
_id: generateTableID(),
views: {},
@ -69,6 +91,7 @@ exports.save = async function(ctx) {
let oldTable
if (ctx.request.body && ctx.request.body._id) {
oldTable = await db.get(ctx.request.body._id)
tableToSave = makeSureTableUpToDate(oldTable, tableToSave)
}
// make sure that types don't change of a column, have to remove
@ -91,7 +114,7 @@ exports.save = async function(ctx) {
}
// rename row fields when table column is renamed
if (_rename && tableToSave.schema[_rename.updated].type === "link") {
if (_rename && tableToSave.schema[_rename.updated].type === FieldTypes.LINK) {
ctx.throw(400, "Cannot rename a linked column.")
} else if (_rename && tableToSave.primaryDisplay === _rename.old) {
ctx.throw(400, "Cannot rename the display column.")

View File

@ -7,9 +7,25 @@ const {
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const joiValidator = require("../../middleware/joi-validator")
const Joi = require("joi")
const router = Router()
function generateSaveValidator() {
// prettier-ignore
return joiValidator.body(Joi.object({
_id: Joi.string(),
_rev: Joi.string(),
type: Joi.string().valid("table"),
primaryDisplay: Joi.string(),
schema: Joi.object().required(),
name: Joi.string().required(),
views: Joi.object(),
dataImport: Joi.object(),
}).unknown(true))
}
router
.get("/api/tables", authorized(BUILDER), tableController.fetch)
.get(
@ -23,6 +39,7 @@ router
// allows control over updating a table
bodyResource("_id"),
authorized(BUILDER),
generateSaveValidator(),
tableController.save
)
.post(

View File

@ -7,7 +7,7 @@ const {
createAttachmentTable,
makeBasicRow,
} = require("./couchTestUtils");
const { enrichRows } = require("../../../utilities")
const { outputProcessing } = require("../../../utilities/rowProcessor")
const env = require("../../../environment")
describe("/rows", () => {
@ -285,7 +285,7 @@ describe("/rows", () => {
link: [firstRow._id],
tableId: table._id,
})).body
const enriched = await enrichRows(appId, table, [secondRow])
const enriched = await outputProcessing(appId, table, [secondRow])
expect(enriched[0].link.length).toBe(1)
expect(enriched[0].link[0]).toBe(firstRow._id)
})
@ -304,7 +304,7 @@ describe("/rows", () => {
// the environment needs configured for this
env.CLOUD = 1
env.SELF_HOSTED = 1
const enriched = await enrichRows(appId, table, [row])
const enriched = await outputProcessing(appId, table, [row])
expect(enriched[0].attachment[0].url).toBe(`/app-assets/assets/${appId}/test/thing`)
// remove env config
env.CLOUD = undefined

View File

@ -53,6 +53,10 @@ module.exports.getAction = async function(actionName) {
if (BUILTIN_ACTIONS[actionName] != null) {
return BUILTIN_ACTIONS[actionName]
}
// worker pools means that a worker may not have manifest
if (env.CLOUD && MANIFEST == null) {
MANIFEST = await module.exports.init()
}
// env setup to get async packages
if (!MANIFEST || !MANIFEST.packages || !MANIFEST.packages[actionName]) {
return null
@ -86,8 +90,10 @@ module.exports.init = async function() {
? Object.assign(MANIFEST.packages, BUILTIN_DEFINITIONS)
: BUILTIN_DEFINITIONS
} catch (err) {
console.error(err)
Sentry.captureException(err)
}
return MANIFEST
}
module.exports.DEFINITIONS = BUILTIN_DEFINITIONS

View File

@ -34,7 +34,7 @@ module.exports.init = function() {
actions.init().then(() => {
triggers.automationQueue.process(async job => {
try {
if (env.CLOUD && job.data.automation) {
if (env.CLOUD && job.data.automation && !env.SELF_HOSTED) {
job.data.automation.apiKey = await updateQuota(job.data.automation)
}
if (env.BUDIBASE_ENVIRONMENT === "PRODUCTION") {

View File

@ -2,7 +2,7 @@ const CouchDB = require("../db")
const emitter = require("../events/index")
const InMemoryQueue = require("../utilities/queue/inMemoryQueue")
const { getAutomationParams } = require("../db/utils")
const { coerceValue } = require("../utilities")
const { coerce } = require("../utilities/rowProcessor")
let automationQueue = new InMemoryQueue("automationQueue")
@ -240,8 +240,8 @@ module.exports.externalTrigger = async function(automation, params) {
// values are likely to be submitted as strings, so we shall convert to correct type
const coercedFields = {}
const fields = automation.definition.trigger.inputs.fields
for (let key in fields) {
coercedFields[key] = coerceValue(params.fields[key], fields[key])
for (let key of Object.keys(fields)) {
coercedFields[key] = coerce(params.fields[key], fields[key])
}
params.fields = coercedFields
}

View File

@ -39,6 +39,26 @@ const USERS_TABLE_SCHEMA = {
primaryDisplay: "email",
}
exports.FieldTypes = {
STRING: "string",
LONGFORM: "longform",
OPTIONS: "options",
NUMBER: "number",
BOOLEAN: "boolean",
DATETIME: "datetime",
ATTACHMENT: "attachment",
LINK: "link",
AUTO: "auto",
}
exports.AutoFieldSubTypes = {
CREATED_BY: "createdBy",
CREATED_AT: "createdAt",
UPDATED_BY: "updatedBy",
UPDATED_AT: "updatedAt",
AUTO_ID: "autoID",
}
exports.AuthTypes = AuthTypes
exports.USERS_TABLE_SCHEMA = USERS_TABLE_SCHEMA
exports.BUILDER_CONFIG_DB = "builder-config-db"

View File

@ -2,6 +2,7 @@ const CouchDB = require("../index")
const { IncludeDocs, getLinkDocuments } = require("./linkUtils")
const { generateLinkID } = require("../utils")
const Sentry = require("@sentry/node")
const { FieldTypes } = require("../../constants")
/**
* Creates a new link document structure which can be put to the database. It is important to
@ -24,9 +25,16 @@ function LinkDocument(
rowId2
) {
// build the ID out of unique references to this link document
this._id = generateLinkID(tableId1, tableId2, rowId1, rowId2)
this._id = generateLinkID(
tableId1,
tableId2,
rowId1,
rowId2,
fieldName1,
fieldName2
)
// required for referencing in view
this.type = "link"
this.type = FieldTypes.LINK
this.doc1 = {
tableId: tableId1,
fieldName: fieldName1,
@ -75,7 +83,7 @@ class LinkController {
}
for (let fieldName of Object.keys(table.schema)) {
const { type } = table.schema[fieldName]
if (type === "link") {
if (type === FieldTypes.LINK) {
return true
}
}
@ -123,7 +131,7 @@ class LinkController {
// get the links this row wants to make
const rowField = row[fieldName]
const field = table.schema[fieldName]
if (field.type === "link" && rowField != null) {
if (field.type === FieldTypes.LINK && rowField != null) {
// check which links actual pertain to the update in this row
const thisFieldLinkDocs = linkDocs.filter(
linkDoc =>
@ -241,7 +249,7 @@ class LinkController {
const schema = table.schema
for (let fieldName of Object.keys(schema)) {
const field = schema[fieldName]
if (field.type === "link") {
if (field.type === FieldTypes.LINK) {
// handle this in a separate try catch, want
// the put to bubble up as an error, if can't update
// table for some reason
@ -251,14 +259,18 @@ class LinkController {
} catch (err) {
continue
}
// create the link field in the other table
linkedTable.schema[field.fieldName] = {
const linkConfig = {
name: field.fieldName,
type: "link",
type: FieldTypes.LINK,
// these are the props of the table that initiated the link
tableId: table._id,
fieldName: fieldName,
}
if (field.autocolumn) {
linkConfig.autocolumn = field.autocolumn
}
// create the link field in the other table
linkedTable.schema[field.fieldName] = linkConfig
const response = await this._db.put(linkedTable)
// special case for when linking back to self, make sure rev updated
if (linkedTable._id === table._id) {
@ -281,7 +293,10 @@ class LinkController {
for (let fieldName of Object.keys(oldTable.schema)) {
const field = oldTable.schema[fieldName]
// this field has been removed from the table schema
if (field.type === "link" && newTable.schema[fieldName] == null) {
if (
field.type === FieldTypes.LINK &&
newTable.schema[fieldName] == null
) {
await this.removeFieldFromTable(fieldName)
}
}
@ -302,7 +317,7 @@ class LinkController {
for (let fieldName of Object.keys(schema)) {
const field = schema[fieldName]
try {
if (field.type === "link") {
if (field.type === FieldTypes.LINK) {
const linkedTable = await this._db.get(field.tableId)
delete linkedTable.schema[field.fieldName]
await this._db.put(linkedTable)

View File

@ -5,7 +5,7 @@ const {
createLinkView,
getUniqueByProp,
} = require("./linkUtils")
const _ = require("lodash")
const { flatten } = require("lodash")
/**
* This functionality makes sure that when rows with links are created, updated or deleted they are processed
@ -101,7 +101,7 @@ exports.attachLinkInfo = async (appId, rows) => {
}
let tableIds = [...new Set(rows.map(el => el.tableId))]
// start by getting all the link values for performance reasons
let responses = _.flatten(
let responses = flatten(
await Promise.all(
tableIds.map(tableId =>
getLinkDocuments({
@ -118,8 +118,12 @@ exports.attachLinkInfo = async (appId, rows) => {
// have to get unique as the previous table query can
// return duplicates, could be querying for both tables in a relation
const linkVals = getUniqueByProp(
responses.filter(el => el.thisId === row._id),
"id"
responses
// find anything that matches the row's ID we are searching for
.filter(el => el.thisId === row._id)
// create a unique ID which we can use for getting only unique ones
.map(el => ({ ...el, unique: el.id + el.fieldName })),
"unique"
)
for (let linkVal of linkVals) {
// work out which link pertains to this row

View File

@ -23,6 +23,7 @@ exports.createLinkView = async appId => {
const designDoc = await db.get("_design/database")
const view = {
map: function(doc) {
// everything in this must remain constant as its going to Pouch, no external variables
if (doc.type === "link") {
let doc1 = doc.doc1
let doc2 = doc.doc2

View File

@ -138,10 +138,22 @@ exports.generateAutomationID = () => {
* @param {string} tableId2 The ID of the linked table.
* @param {string} rowId1 The ID of the linker row.
* @param {string} rowId2 The ID of the linked row.
* @param {string} fieldName1 The name of the field in the linker row.
* @param {string} fieldName2 the name of the field in the linked row.
* @returns {string} The new link doc ID which the automation doc can be stored under.
*/
exports.generateLinkID = (tableId1, tableId2, rowId1, rowId2) => {
return `${DocumentTypes.LINK}${SEPARATOR}${tableId1}${SEPARATOR}${tableId2}${SEPARATOR}${rowId1}${SEPARATOR}${rowId2}`
exports.generateLinkID = (
tableId1,
tableId2,
rowId1,
rowId2,
fieldName1,
fieldName2
) => {
const tables = `${SEPARATOR}${tableId1}${SEPARATOR}${tableId2}`
const rows = `${SEPARATOR}${rowId1}${SEPARATOR}${rowId2}`
const fields = `${SEPARATOR}${fieldName1}${SEPARATOR}${fieldName2}`
return `${DocumentTypes.LINK}${tables}${rows}${fields}`
}
/**

View File

@ -1,6 +1,6 @@
const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes")
const { getRole, BUILTIN_ROLES } = require("../utilities/security/roles")
const { getRole, getBuiltinRoles } = require("../utilities/security/roles")
const { AuthTypes } = require("../constants")
const {
getAppId,
@ -20,6 +20,7 @@ module.exports = async (ctx, next) => {
// we hold it in state as a
let appId = getAppId(ctx)
const cookieAppId = ctx.cookies.get(getCookieName("currentapp"))
const builtinRoles = getBuiltinRoles()
if (appId && cookieAppId !== appId) {
setCookie(ctx, appId, "currentapp")
} else if (cookieAppId) {
@ -40,7 +41,7 @@ module.exports = async (ctx, next) => {
ctx.appId = appId
ctx.user = {
appId,
role: BUILTIN_ROLES.PUBLIC,
role: builtinRoles.PUBLIC,
}
await next()
return

View File

@ -1,68 +1,10 @@
const env = require("../environment")
const { DocumentTypes, SEPARATOR } = require("../db/utils")
const fs = require("fs")
const { cloneDeep } = require("lodash/fp")
const CouchDB = require("../db")
const { OBJ_STORE_DIRECTORY } = require("../constants")
const linkRows = require("../db/linkedRows")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
/**
* A map of how we convert various properties in rows to each other based on the row type.
*/
const TYPE_TRANSFORM_MAP = {
link: {
"": [],
[null]: [],
[undefined]: undefined,
parse: link => {
if (typeof link === "string") {
return [link]
}
return link
},
},
options: {
"": "",
[null]: "",
[undefined]: undefined,
},
string: {
"": "",
[null]: "",
[undefined]: undefined,
},
longform: {
"": "",
[null]: "",
[undefined]: undefined,
},
number: {
"": null,
[null]: null,
[undefined]: undefined,
parse: n => parseFloat(n),
},
datetime: {
"": null,
[undefined]: undefined,
[null]: null,
},
attachment: {
"": [],
[null]: [],
[undefined]: undefined,
},
boolean: {
"": null,
[null]: null,
[undefined]: undefined,
true: true,
false: false,
},
}
function confirmAppId(possibleAppId) {
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
? possibleAppId
@ -165,43 +107,6 @@ exports.walkDir = (dirPath, callback) => {
}
}
/**
* This will coerce a value to the correct types based on the type transform map
* @param {object} row The value to coerce
* @param {object} type The type fo coerce to
* @returns {object} The coerced value
*/
exports.coerceValue = (row, type) => {
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) {
return TYPE_TRANSFORM_MAP[type][row]
} else if (TYPE_TRANSFORM_MAP[type].parse) {
return TYPE_TRANSFORM_MAP[type].parse(row)
}
return row
}
/**
* This will coerce the values in a row to the correct types based on the type transform map and the
* table schema.
* @param {object} row The row which is to be coerced to correct values based on schema, this input
* row will not be updated.
* @param {object} table The table that has been retrieved from DB, this must contain the expected
* schema for the rows.
* @returns {object} The updated row will be returned with all values coerced.
*/
exports.coerceRowValues = (row, table) => {
const clonedRow = cloneDeep(row)
for (let [key, value] of Object.entries(clonedRow)) {
const field = table.schema[key]
if (!field) continue
clonedRow[key] = exports.coerceValue(value, field.type)
}
return clonedRow
}
exports.getLogoUrl = () => {
return "https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
}
@ -219,34 +124,3 @@ exports.getAllApps = async () => {
.map(({ value }) => value)
}
}
/**
* This function "enriches" the input rows with anything they are supposed to contain, for example
* link records or attachment links.
* @param {string} appId the ID of the application for which rows are being enriched.
* @param {object} table the table from which these rows came from originally, this is used to determine
* the schema of the rows and then enrich.
* @param {object[]} rows the rows which are to be enriched.
* @returns {object[]} the enriched rows will be returned.
*/
exports.enrichRows = async (appId, table, rows) => {
// attach any linked row information
const enriched = await linkRows.attachLinkInfo(appId, rows)
// update the attachments URL depending on hosting
if (env.CLOUD && env.SELF_HOSTED) {
for (let [property, column] of Object.entries(table.schema)) {
if (column.type === "attachment") {
for (let row of enriched) {
if (row[property] == null || row[property].length === 0) {
continue
}
row[property].forEach(attachment => {
attachment.url = `${OBJ_STORE_DIRECTORY}/${appId}/${attachment.url}`
attachment.url = attachment.url.replace("//", "/")
})
}
}
}
}
return enriched
}

View File

@ -0,0 +1,188 @@
const env = require("../environment")
const { OBJ_STORE_DIRECTORY } = require("../constants")
const linkRows = require("../db/linkedRows")
const { cloneDeep } = require("lodash/fp")
const { FieldTypes, AutoFieldSubTypes } = require("../constants")
const CouchDB = require("../db")
const BASE_AUTO_ID = 1
/**
* A map of how we convert various properties in rows to each other based on the row type.
*/
const TYPE_TRANSFORM_MAP = {
[FieldTypes.LINK]: {
"": [],
[null]: [],
[undefined]: undefined,
parse: link => {
if (typeof link === "string") {
return [link]
}
return link
},
},
[FieldTypes.OPTIONS]: {
"": "",
[null]: "",
[undefined]: undefined,
},
[FieldTypes.STRING]: {
"": "",
[null]: "",
[undefined]: undefined,
},
[FieldTypes.LONGFORM]: {
"": "",
[null]: "",
[undefined]: undefined,
},
[FieldTypes.NUMBER]: {
"": null,
[null]: null,
[undefined]: undefined,
parse: n => parseFloat(n),
},
[FieldTypes.DATETIME]: {
"": null,
[undefined]: undefined,
[null]: null,
},
[FieldTypes.ATTACHMENT]: {
"": [],
[null]: [],
[undefined]: undefined,
},
[FieldTypes.BOOLEAN]: {
"": null,
[null]: null,
[undefined]: undefined,
true: true,
false: false,
},
[FieldTypes.AUTO]: {
parse: () => undefined,
},
}
/**
* This will update any auto columns that are found on the row/table with the correct information based on
* time now and the current logged in user making the request.
* @param {Object} user The user to be used for an appId as well as the createdBy and createdAt fields.
* @param {Object} table The table which is to be used for the schema, as well as handling auto IDs incrementing.
* @param {Object} row The row which is to be updated with information for the auto columns.
* @returns {Promise<{row: Object, table: Object}>} The updated row and table, the table may need to be updated
* for automatic ID purposes.
*/
async function processAutoColumn(user, table, row) {
let now = new Date().toISOString()
// if a row doesn't have a revision then it doesn't exist yet
const creating = !row._rev
let tableUpdated = false
for (let [key, schema] of Object.entries(table.schema)) {
if (!schema.autocolumn) {
continue
}
switch (schema.subtype) {
case AutoFieldSubTypes.CREATED_BY:
if (creating) {
row[key] = [user.userId]
}
break
case AutoFieldSubTypes.CREATED_AT:
if (creating) {
row[key] = now
}
break
case AutoFieldSubTypes.UPDATED_BY:
row[key] = [user.userId]
break
case AutoFieldSubTypes.UPDATED_AT:
row[key] = now
break
case AutoFieldSubTypes.AUTO_ID:
if (creating) {
schema.lastID = !schema.lastID ? BASE_AUTO_ID : schema.lastID + 1
row[key] = schema.lastID
tableUpdated = true
}
break
}
}
if (tableUpdated) {
const db = new CouchDB(user.appId)
const response = await db.put(table)
// update the revision
table._rev = response._rev
}
return { table, row }
}
/**
* This will coerce a value to the correct types based on the type transform map
* @param {object} row The value to coerce
* @param {object} type The type fo coerce to
* @returns {object} The coerced value
*/
exports.coerce = (row, type) => {
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) {
return TYPE_TRANSFORM_MAP[type][row]
} else if (TYPE_TRANSFORM_MAP[type].parse) {
return TYPE_TRANSFORM_MAP[type].parse(row)
}
return row
}
/**
* Given an input route this function will apply all the necessary pre-processing to it, such as coercion
* of column values or adding auto-column values.
* @param {object} user the user which is performing the input.
* @param {object} row the row which is being created/updated.
* @param {object} table the table which the row is being saved to.
* @returns {object} the row which has been prepared to be written to the DB.
*/
exports.inputProcessing = async (user, table, row) => {
let clonedRow = cloneDeep(row)
for (let [key, value] of Object.entries(clonedRow)) {
const field = table.schema[key]
if (!field) {
continue
}
clonedRow[key] = exports.coerce(value, field.type)
}
// handle auto columns - this returns an object like {table, row}
return processAutoColumn(user, table, clonedRow)
}
/**
* This function enriches the input rows with anything they are supposed to contain, for example
* link records or attachment links.
* @param {string} appId the ID of the application for which rows are being enriched.
* @param {object} table the table from which these rows came from originally, this is used to determine
* the schema of the rows and then enrich.
* @param {object[]} rows the rows which are to be enriched.
* @returns {object[]} the enriched rows will be returned.
*/
exports.outputProcessing = async (appId, table, rows) => {
// attach any linked row information
const outputRows = await linkRows.attachLinkInfo(appId, rows)
// update the attachments URL depending on hosting
if (env.CLOUD && env.SELF_HOSTED) {
for (let [property, column] of Object.entries(table.schema)) {
if (column.type === FieldTypes.ATTACHMENT) {
for (let row of outputRows) {
if (row[property] == null || row[property].length === 0) {
continue
}
row[property].forEach(attachment => {
attachment.url = `${OBJ_STORE_DIRECTORY}/${appId}/${attachment.url}`
attachment.url = attachment.url.replace("//", "/")
})
}
}
}
}
return outputRows
}

View File

@ -1,4 +1,5 @@
const { flatten } = require("lodash")
const { cloneDeep } = require("lodash/fp")
const PermissionLevels = {
READ: "read",
@ -70,7 +71,7 @@ exports.BUILTIN_PERMISSION_IDS = {
POWER: "power",
}
exports.BUILTIN_PERMISSIONS = {
const BUILTIN_PERMISSIONS = {
PUBLIC: {
_id: exports.BUILTIN_PERMISSION_IDS.PUBLIC,
name: "Public",
@ -121,8 +122,12 @@ exports.BUILTIN_PERMISSIONS = {
},
}
exports.getBuiltinPermissions = () => {
return cloneDeep(BUILTIN_PERMISSIONS)
}
exports.getBuiltinPermissionByID = id => {
const perms = Object.values(exports.BUILTIN_PERMISSIONS)
const perms = Object.values(BUILTIN_PERMISSIONS)
return perms.find(perm => perm._id === id)
}
@ -155,7 +160,7 @@ exports.doesHaveResourcePermission = (
}
exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => {
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
const builtins = Object.values(BUILTIN_PERMISSIONS)
let permissions = flatten(
builtins
.filter(builtin => permissionIds.indexOf(builtin._id) !== -1)

View File

@ -26,7 +26,7 @@ Role.prototype.addInheritance = function(inherits) {
return this
}
exports.BUILTIN_ROLES = {
const BUILTIN_ROLES = {
ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin")
.addPermission(BUILTIN_PERMISSION_IDS.ADMIN)
.addInheritance(BUILTIN_IDS.POWER),
@ -44,11 +44,15 @@ exports.BUILTIN_ROLES = {
),
}
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
exports.getBuiltinRoles = () => {
return cloneDeep(BUILTIN_ROLES)
}
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(BUILTIN_ROLES).map(
role => role._id
)
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(BUILTIN_ROLES).map(
role => role.name
)
@ -60,17 +64,18 @@ function isBuiltin(role) {
* Works through the inheritance ranks to see how far up the builtin stack this ID is.
*/
function builtinRoleToNumber(id) {
const builtins = exports.getBuiltinRoles()
const MAX = Object.values(BUILTIN_IDS).length + 1
if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) {
return MAX
}
let role = exports.BUILTIN_ROLES[id],
let role = builtins[id],
count = 0
do {
if (!role) {
break
}
role = exports.BUILTIN_ROLES[role.inherits]
role = builtins[role.inherits]
count++
} while (role !== null)
return count
@ -107,7 +112,7 @@ exports.getRole = async (appId, roleId) => {
// but can be extended by a doc stored about them (e.g. permissions)
if (isBuiltin(roleId)) {
role = cloneDeep(
Object.values(exports.BUILTIN_ROLES).find(role => role._id === roleId)
Object.values(BUILTIN_ROLES).find(role => role._id === roleId)
)
}
try {

View File

@ -6,7 +6,7 @@ const {
} = require("../../utilities/security/permissions")
const {
lowerBuiltinRoleID,
BUILTIN_ROLES,
getBuiltinRoles,
} = require("../../utilities/security/roles")
const { DocumentTypes } = require("../../db/utils")
@ -44,7 +44,7 @@ exports.getPermissionType = resourceId => {
exports.getBasePermissions = resourceId => {
const type = exports.getPermissionType(resourceId)
const permissions = {}
for (let [roleId, role] of Object.entries(BUILTIN_ROLES)) {
for (let [roleId, role] of Object.entries(getBuiltinRoles())) {
if (!role.permissionId) {
continue
}

View File

@ -50,6 +50,9 @@ exports.Properties = {
}
exports.getAPIKey = async appId => {
if (env.SELF_HOSTED) {
return { apiKey: null }
}
return apiKeyTable.get({ primary: appId })
}
@ -63,7 +66,7 @@ exports.getAPIKey = async appId => {
*/
exports.update = async (apiKey, property, usage) => {
// don't try validate in builder
if (!env.CLOUD) {
if (!env.CLOUD || env.SELF_HOSTED) {
return
}
try {

View File

@ -176,9 +176,10 @@
"key": "subheading"
},
{
"type": "screen",
"type": "text",
"label": "Link URL",
"key": "destinationUrl"
"key": "destinationUrl",
"placeholder": "/screen"
}
]
},
@ -209,9 +210,10 @@
"key": "linkText"
},
{
"type": "screen",
"label": "Link Url",
"key": "linkUrl"
"type": "text",
"label": "Link URL",
"key": "linkUrl",
"placeholder": "/screen"
},
{
"type": "color",
@ -378,9 +380,10 @@
"key": "text"
},
{
"type": "screen",
"type": "text",
"label": "URL",
"key": "url"
"key": "url",
"placeholder": "/screen"
},
{
"type": "boolean",
@ -451,9 +454,10 @@
"key": "linkText"
},
{
"type": "screen",
"type": "text",
"label": "Link URL",
"key": "linkUrl"
"key": "linkUrl",
"placeholder": "/screen"
},
{
"type": "color",
@ -1131,6 +1135,12 @@
"value": "spectrum--large"
}
]
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
@ -1181,6 +1191,12 @@
"type": "text",
"label": "Placeholder",
"key": "placeholder"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
@ -1203,6 +1219,12 @@
"type": "text",
"label": "Placeholder",
"key": "placeholder"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
@ -1226,6 +1248,12 @@
"label": "Placeholder",
"key": "placeholder",
"placeholder": "Choose an option"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
@ -1248,6 +1276,12 @@
"type": "text",
"label": "Text",
"key": "text"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
@ -1271,6 +1305,12 @@
"label": "Placeholder",
"key": "placeholder",
"placeholder": "Type something..."
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
@ -1299,6 +1339,12 @@
"label": "Show Time",
"key": "enableTime",
"defaultValue": true
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
@ -1316,6 +1362,12 @@
"type": "text",
"label": "Label",
"key": "label"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
},
@ -1333,6 +1385,12 @@
"type": "text",
"label": "Label",
"key": "label"
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false
}
]
}

View File

@ -1,59 +0,0 @@
<script>
import { Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "./helpers"
import { getContext } from "svelte"
const { API } = getContext("sdk")
export let schema = {}
export let linkedRows = []
export let showLabel = true
export let secondary
let linkedTable
let allRows = []
$: label = capitalise(schema.name)
$: linkedTableId = schema.tableId
$: fetchRows(linkedTableId)
$: fetchTable(linkedTableId)
async function fetchTable(id) {
if (id != null) {
linkedTable = await API.fetchTableDefinition(id)
}
}
async function fetchRows(id) {
if (id != null) {
allRows = await API.fetchTableData(id)
}
}
function getPrettyName(row) {
return row[(linkedTable && linkedTable.primaryDisplay) || "_id"]
}
</script>
{#if linkedTable != null}
{#if linkedTable.primaryDisplay == null}
{#if showLabel}
<Label extraSmall grey>{label}</Label>
{/if}
<Label small black>
Please choose a display column for the
<b>{linkedTable.name}</b>
table.
</Label>
{:else}
<Multiselect
{secondary}
bind:value={linkedRows}
label={showLabel ? label : null}
placeholder="Choose some options">
{#each allRows as row}
<option value={row._id}>{getPrettyName(row)}</option>
{/each}
</Multiselect>
{/if}
{/if}

View File

@ -5,6 +5,7 @@
export let field
export let label
export let disabled = false
let fieldState
let fieldApi
@ -24,11 +25,21 @@
<Field
{label}
{field}
{disabled}
type="attachment"
bind:fieldState
bind:fieldApi
defaultValue={[]}>
{#if mounted}
<Dropzone bind:files={value} />
<div class:disabled={$fieldState.disabled}>
<Dropzone bind:files={value} />
</div>
{/if}
</Field>
<style>
div.disabled :global(> *) {
background-color: var(--spectrum-global-color-gray-200) !important;
pointer-events: none !important;
}
</style>

View File

@ -5,6 +5,7 @@
export let field
export let label
export let text
export let disabled = false
let fieldState
let fieldApi
@ -17,6 +18,7 @@
<Field
{label}
{field}
{disabled}
type="boolean"
bind:fieldState
bind:fieldApi
@ -26,6 +28,7 @@
<label class="spectrum-Checkbox" class:is-invalid={!$fieldState.valid}>
<input
checked={$fieldState.value}
disabled={$fieldState.disabled}
on:change={onChange}
type="checkbox"
class="spectrum-Checkbox-input"

View File

@ -9,6 +9,7 @@
export let label
export let placeholder
export let enableTime
export let disabled = false
let fieldState
let fieldApi
@ -53,7 +54,7 @@
}
</script>
<Field {label} {field} type="datetime" bind:fieldState bind:fieldApi>
<Field {label} {field} {disabled} type="datetime" bind:fieldState bind:fieldApi>
{#if fieldState}
<Flatpickr
bind:flatpickr
@ -67,6 +68,7 @@
id={flatpickrId}
aria-disabled="false"
aria-invalid={!$fieldState.valid}
class:is-disabled={$fieldState.disabled}
class:is-invalid={!$fieldState.valid}
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open}
@ -76,6 +78,7 @@
<div
on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={$fieldState.disabled}
class:is-invalid={!$fieldState.valid}>
{#if !$fieldState.valid}
<svg
@ -88,6 +91,7 @@
<input
data-input
type="text"
disabled={$fieldState.disabled}
class="spectrum-Textfield-input spectrum-InputGroup-input"
aria-invalid={!$fieldState.valid}
{placeholder}
@ -98,6 +102,7 @@
type="button"
class="spectrum-Picker spectrum-InputGroup-button"
tabindex="-1"
disabled={$fieldState.disabled}
class:is-invalid={!$fieldState.valid}
on:click={flatpickr?.open}>
<svg
@ -120,7 +125,7 @@
.spectrum-Textfield-input {
pointer-events: none;
}
.spectrum-Textfield:hover {
.spectrum-Textfield:not(.is-disabled):hover {
cursor: pointer;
}
.flatpickr {

View File

@ -10,6 +10,7 @@
export let fieldSchema
export let defaultValue
export let type
export let disabled = false
// Get contexts
const formContext = getContext("form")
@ -20,7 +21,7 @@
// Register field with form
const formApi = formContext?.formApi
const labelPosition = fieldGroupContext?.labelPosition || "above"
const formField = formApi?.registerField(field, defaultValue)
const formField = formApi?.registerField(field, defaultValue, disabled)
// Expose field properties to parent component
fieldState = formField?.fieldState

View File

@ -8,6 +8,7 @@
export let datasource
export let theme
export let size
export let disabled = false
const component = getContext("component")
const context = getContext("context")
@ -18,27 +19,39 @@
let table
let fieldMap = {}
// Checks if the closest data context matches the model for this forms
// datasource, and use it as the initial form values if so
// Returns the closes data context which isn't a built in context
const getInitialValues = context => {
return context && context.tableId === datasource?.tableId ? context : {}
if (["user", "url"].includes(context.closestComponentId)) {
return {}
}
return context[`${context.closestComponentId}`] || {}
}
// Use the closest data context as the initial form values if it matches
const initialValues = getInitialValues(
$context[`${$context.closestComponentId}`]
)
// Use the closest data context as the initial form values
const initialValues = getInitialValues($context)
// Form state contains observable data about the form
const formState = writable({ values: initialValues, errors: {}, valid: true })
// Form API contains functions to control the form
const formApi = {
registerField: (field, defaultValue = null) => {
registerField: (field, defaultValue = null, fieldDisabled = false) => {
if (!field) {
return
}
// Auto columns are always disabled
const isAutoColumn = !!schema?.[field]?.autocolumn
if (fieldMap[field] != null) {
// Update disabled property just so that toggling the disabled field
// state in the builder makes updates in real time.
// We only need this because of optimisations which prevent fully
// remounting when settings change.
fieldMap[field].fieldState.update(state => {
state.disabled = disabled || fieldDisabled || isAutoColumn
return state
})
return fieldMap[field]
}
@ -47,7 +60,11 @@
const validate = createValidatorFromConstraints(constraints, field, table)
fieldMap[field] = {
fieldState: makeFieldState(field, defaultValue),
fieldState: makeFieldState(
field,
defaultValue,
disabled || fieldDisabled || isAutoColumn
),
fieldApi: makeFieldApi(field, defaultValue, validate),
fieldSchema: schema?.[field] ?? {},
}
@ -115,13 +132,14 @@
}
// Creates observable state data about a specific field
const makeFieldState = (field, defaultValue) => {
const makeFieldState = (field, defaultValue, fieldDisabled) => {
return writable({
field,
fieldId: `id-${generateID()}`,
value: initialValues[field] ?? defaultValue,
error: null,
valid: true,
disabled: fieldDisabled,
})
}

View File

@ -6,6 +6,7 @@
export let field
export let label
export let placeholder
export let disabled = false
let fieldState
let fieldApi
@ -41,12 +42,13 @@
<Field
{label}
{field}
{disabled}
type="longform"
bind:fieldState
bind:fieldApi
defaultValue="">
{#if mounted}
<div>
<div class:disabled={$fieldState.disabled}>
<RichText bind:value {options} />
</div>
{/if}
@ -68,4 +70,12 @@
div :global(.ql-editor p) {
word-break: break-all;
}
div.disabled {
pointer-events: none !important;
background-color: rgb(244, 244, 244);
}
div.disabled :global(.ql-container *) {
color: var(--spectrum-alias-text-color-disabled) !important;
}
</style>

View File

@ -5,6 +5,7 @@
export let field
export let label
export let placeholder
export let disabled = false
let fieldState
let fieldApi
@ -26,6 +27,7 @@
<Field
{field}
{label}
{disabled}
type="options"
bind:fieldState
bind:fieldApi

View File

@ -20,6 +20,7 @@
<button
id={$fieldState.fieldId}
class="spectrum-Picker"
disabled={$fieldState.disabled}
class:is-invalid={!$fieldState.valid}
class:is-open={open}
aria-haspopup="listbox"

View File

@ -7,6 +7,7 @@
export let field
export let label
export let disabled = false
let fieldState
let fieldApi
@ -24,7 +25,7 @@
$: fetchTable(linkedTableId)
const fetchTable = async id => {
if (id != null) {
if (id) {
const result = await API.fetchTableDefinition(id)
if (!result.error) {
tableDefinition = result
@ -33,7 +34,7 @@
}
const fetchRows = async id => {
if (id != null) {
if (id) {
const rows = await API.fetchTableData(id)
options = rows && !rows.error ? rows : []
}
@ -65,6 +66,7 @@
<Field
{label}
{field}
{disabled}
type="link"
bind:fieldState
bind:fieldApi

View File

@ -6,6 +6,7 @@
export let label
export let placeholder
export let type = "text"
export let disabled = false
let fieldState
let fieldApi
@ -33,11 +34,15 @@
<Field
{label}
{field}
{disabled}
type={type === 'number' ? 'number' : 'string'}
bind:fieldState
bind:fieldApi>
{#if fieldState}
<div class="spectrum-Textfield" class:is-invalid={!$fieldState.valid}>
<div
class="spectrum-Textfield"
class:is-invalid={!$fieldState.valid}
class:is-disabled={$fieldState.disabled}>
{#if !$fieldState.valid}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
@ -49,6 +54,7 @@
<input
on:keyup={updateValueOnEnter}
bind:this={input}
disabled={$fieldState.disabled}
id={$fieldState.fieldId}
value={$fieldState.value || ''}
placeholder={placeholder || ''}