merge with develop
This commit is contained in:
commit
8a60131c7e
|
@ -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' ])
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}`,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -30,6 +30,7 @@
|
|||
{#if schemaFields.length}
|
||||
<div class="schema-fields">
|
||||
{#each schemaFields as [field, schema]}
|
||||
{#if !schema.autocolumn}
|
||||
{#if schemaHasOptions(schema)}
|
||||
<Select label={field} extraThin secondary bind:value={value[field]}>
|
||||
<option value="">Choose an option</option>
|
||||
|
@ -45,6 +46,7 @@
|
|||
type="string"
|
||||
{bindings} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -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} />
|
||||
<ExportButton view={tableView} />
|
||||
{/if}
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<!-- always have the export last -->
|
||||
<ExportButton view={tableView} />
|
||||
{/if}
|
||||
</Table>
|
||||
|
|
|
@ -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,6 +160,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="grid-wrapper">
|
||||
{#key columnDefs.length}
|
||||
<AgGrid
|
||||
{theme}
|
||||
{options}
|
||||
|
@ -157,6 +168,7 @@
|
|||
{columnDefs}
|
||||
{loading}
|
||||
on:select={({ detail }) => (selectedRows = detail)} />
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -59,8 +59,11 @@
|
|||
on:mouseover={() => (hovered = true)}
|
||||
on:mouseleave={() => (hovered = false)}>
|
||||
<div>
|
||||
<div class="col-icon">
|
||||
{#if field.autocolumn}<i class="auto ri-magic-fill" />{/if}
|
||||
<span class="column-header-name">{displayName}</span>
|
||||
<i class={`${SORT_ICON_MAP[sortDirection]} sort-icon`} />
|
||||
</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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -40,9 +40,11 @@
|
|||
onConfirm={saveRow}>
|
||||
<ErrorsBox {errors} />
|
||||
{#each tableSchema as [key, meta]}
|
||||
{#if !meta.autocolumn}
|
||||
<div>
|
||||
<RowFieldControl {meta} bind:value={row[key]} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</ModalContent>
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
$: value && checkValid()
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: dispatch("update", value)
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
type: "query",
|
||||
}))
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: queryBindableProperties = bindableProperties.map(property => ({
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
export let parameters
|
||||
|
||||
$: dataProviderComponents = getDataProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
ds => ds._id === parameters.datasourceId
|
||||
)
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
).map(property => ({
|
||||
...property,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
export let parameters
|
||||
|
||||
$: dataProviders = getDataProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
</script>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
const emptyField = () => ({ name: "", value: "" })
|
||||
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
export let parameters
|
||||
|
||||
$: dataProviderComponents = getDataProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: providerComponent = dataProviderComponents.find(
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
export let parameters
|
||||
|
||||
$: actionProviders = getActionProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
"ValidateForm"
|
||||
)
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
let valid
|
||||
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
@ -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 = [
|
||||
{
|
||||
$: 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 {
|
||||
|
|
|
@ -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)}
|
||||
<!-- Ensure to fully remount when screen changes -->
|
||||
{#key screenDefinition?._id}
|
||||
<Provider key="url" data={params}>
|
||||
<div in:fade>
|
||||
<Component definition={screen} />
|
||||
<Component definition={screenDefinition} />
|
||||
</div>
|
||||
{/each}
|
||||
</Provider>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
div {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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}
|
|
@ -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}
|
||||
<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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 || ''}
|
||||
|
|
Loading…
Reference in New Issue