mirror of https://github.com/Budibase/budibase.git
69 changed files with 1818 additions and 1439 deletions
@ -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 |
|||
} |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
File diff suppressed because it is too large
@ -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 |
|||
} |
|||
@ -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} |
|||
Loading…
Reference in new issue