Merge pull request #1125 from Budibase/feature/autocolumns
Feature/autocolumns
This commit is contained in:
commit
7647e346d4
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -13,7 +13,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 EditQueryParamsPopover from "components/backend/DatasourceNavigator/popovers/EditQueryParamsPopover.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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -821,10 +821,10 @@
|
|||
lodash "^4.17.19"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/bbui@^1.58.5":
|
||||
version "1.58.7"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.7.tgz#b9ae18fbd44f830fd8b11fd1dbfaf1212243c88b"
|
||||
integrity sha512-Kp6iNFXov5DXOTR9860kBJv4zgFEycBw13pdZgcPUERI++OWHtisEo6fuHIEfQGeALif3QEcYwaGr/6pu76C4w==
|
||||
"@budibase/bbui@^1.58.8":
|
||||
version "1.58.8"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.58.8.tgz#c50c54a9baa4fd6d013401586162d4c6354af1a7"
|
||||
integrity sha512-4qQ6ijMk3XeUOUZwS6dwT51H0T/vfEHhHhNI5JXCQ1TzEX6CUeLNAklY0E/iIMtNteOgdipEkvXGBxh72yX6FQ==
|
||||
dependencies:
|
||||
markdown-it "^12.0.2"
|
||||
quill "^1.3.7"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,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
|
||||
}
|
|
@ -1135,6 +1135,12 @@
|
|||
"value": "spectrum--large"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1185,6 +1191,12 @@
|
|||
"type": "text",
|
||||
"label": "Placeholder",
|
||||
"key": "placeholder"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1207,6 +1219,12 @@
|
|||
"type": "text",
|
||||
"label": "Placeholder",
|
||||
"key": "placeholder"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1230,6 +1248,12 @@
|
|||
"label": "Placeholder",
|
||||
"key": "placeholder",
|
||||
"placeholder": "Choose an option"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1252,6 +1276,12 @@
|
|||
"type": "text",
|
||||
"label": "Text",
|
||||
"key": "text"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1275,6 +1305,12 @@
|
|||
"label": "Placeholder",
|
||||
"key": "placeholder",
|
||||
"placeholder": "Type something..."
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1303,6 +1339,12 @@
|
|||
"label": "Show Time",
|
||||
"key": "enableTime",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1320,6 +1362,12 @@
|
|||
"type": "text",
|
||||
"label": "Label",
|
||||
"key": "label"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1337,6 +1385,12 @@
|
|||
"type": "text",
|
||||
"label": "Label",
|
||||
"key": "label"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
@ -34,11 +35,23 @@
|
|||
|
||||
// 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
|
||||
|
@ -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