Merge branch 'develop' of github.com:Budibase/budibase into repeater-filtering
This commit is contained in:
commit
8c4cf0bb8d
|
@ -30,7 +30,7 @@ context("Create a Table", () => {
|
||||||
// Unset table display column
|
// Unset table display column
|
||||||
cy.contains("display column").click()
|
cy.contains("display column").click()
|
||||||
cy.contains("Save Column").click()
|
cy.contains("Save Column").click()
|
||||||
cy.contains("nameupdated").should("have.text", "nameupdated")
|
cy.contains("nameupdated ").should("have.text", "nameupdated ")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("edits a row", () => {
|
it("edits a row", () => {
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
function removeSpacing(headers) {
|
||||||
|
let newHeaders = []
|
||||||
|
for (let header of headers) {
|
||||||
|
newHeaders.push(header.replace(/\s\s+/g, " "))
|
||||||
|
}
|
||||||
|
return newHeaders
|
||||||
|
}
|
||||||
|
|
||||||
context("Create a View", () => {
|
context("Create a View", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.visit("localhost:4001/_builder")
|
cy.visit("localhost:4001/_builder")
|
||||||
|
@ -28,7 +36,7 @@ context("Create a View", () => {
|
||||||
const headers = Array.from($headers).map(header =>
|
const headers = Array.from($headers).map(header =>
|
||||||
header.textContent.trim()
|
header.textContent.trim()
|
||||||
)
|
)
|
||||||
expect(headers).to.deep.eq([ 'rating', 'age', 'group' ])
|
expect(removeSpacing(headers)).to.deep.eq([ "rating Number", "age Number", "group Text" ])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -60,13 +68,19 @@ context("Create a View", () => {
|
||||||
const headers = Array.from($headers).map(header =>
|
const headers = Array.from($headers).map(header =>
|
||||||
header.textContent.trim()
|
header.textContent.trim()
|
||||||
)
|
)
|
||||||
expect(headers).to.deep.eq([ 'avg', 'sumsqr', 'count', 'max', 'min', 'sum', 'field' ])
|
expect(removeSpacing(headers)).to.deep.eq([ "avg Number",
|
||||||
|
"sumsqr Number",
|
||||||
|
"count Number",
|
||||||
|
"max Number",
|
||||||
|
"min Number",
|
||||||
|
"sum Number",
|
||||||
|
"field Text" ])
|
||||||
})
|
})
|
||||||
cy.get(".ag-cell").then($values => {
|
cy.get(".ag-cell").then($values => {
|
||||||
let values = Array.from($values).map(header =>
|
let values = Array.from($values).map(header =>
|
||||||
header.textContent.trim()
|
header.textContent.trim()
|
||||||
)
|
)
|
||||||
expect(values).to.deep.eq([ '31', '5347', '5', '49', '20', '155', 'age' ])
|
expect(values).to.deep.eq([ "31", "5347", "5", "49", "20", "155", "age" ])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -85,7 +99,7 @@ context("Create a View", () => {
|
||||||
.find(".ag-cell")
|
.find(".ag-cell")
|
||||||
.then($values => {
|
.then($values => {
|
||||||
const values = Array.from($values).map(value => value.textContent)
|
const values = Array.from($values).map(value => value.textContent)
|
||||||
expect(values).to.deep.eq([ 'Students', '23.333333333333332', '1650', '3', '25', '20', '70' ])
|
expect(values).to.deep.eq([ "Students", "23.333333333333332", "1650", "3", "25", "20", "70" ])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.58.8",
|
"@budibase/bbui": "^1.58.12",
|
||||||
"@budibase/client": "^0.7.8",
|
"@budibase/client": "^0.7.8",
|
||||||
"@budibase/colorpicker": "1.0.1",
|
"@budibase/colorpicker": "1.0.1",
|
||||||
"@budibase/string-templates": "^0.7.8",
|
"@budibase/string-templates": "^0.7.8",
|
||||||
|
|
|
@ -136,7 +136,7 @@ const getContextBindings = (asset, componentId) => {
|
||||||
// Replace certain bindings with a new property to help display components
|
// Replace certain bindings with a new property to help display components
|
||||||
let runtimeBoundKey = key
|
let runtimeBoundKey = key
|
||||||
if (fieldSchema.type === "link") {
|
if (fieldSchema.type === "link") {
|
||||||
runtimeBoundKey = `${key}_count`
|
runtimeBoundKey = `${key}_text`
|
||||||
} else if (fieldSchema.type === "attachment") {
|
} else if (fieldSchema.type === "attachment") {
|
||||||
runtimeBoundKey = `${key}_first`
|
runtimeBoundKey = `${key}_first`
|
||||||
}
|
}
|
||||||
|
@ -176,7 +176,7 @@ const getUserBindings = () => {
|
||||||
// Replace certain bindings with a new property to help display components
|
// Replace certain bindings with a new property to help display components
|
||||||
let runtimeBoundKey = key
|
let runtimeBoundKey = key
|
||||||
if (fieldSchema.type === "link") {
|
if (fieldSchema.type === "link") {
|
||||||
runtimeBoundKey = `${key}_count`
|
runtimeBoundKey = `${key}_text`
|
||||||
} else if (fieldSchema.type === "attachment") {
|
} else if (fieldSchema.type === "attachment") {
|
||||||
runtimeBoundKey = `${key}_first`
|
runtimeBoundKey = `${key}_first`
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,6 +198,13 @@ export function makeDatasourceFormComponents(datasource) {
|
||||||
if (fieldType === "options") {
|
if (fieldType === "options") {
|
||||||
component.customProps({ placeholder: "Choose an option " })
|
component.customProps({ placeholder: "Choose an option " })
|
||||||
}
|
}
|
||||||
|
if (fieldType === "link") {
|
||||||
|
let placeholder =
|
||||||
|
fieldSchema.relationshipType === "one-to-many"
|
||||||
|
? "Choose an option"
|
||||||
|
: "Choose some options"
|
||||||
|
component.customProps({ placeholder })
|
||||||
|
}
|
||||||
if (fieldType === "boolean") {
|
if (fieldType === "boolean") {
|
||||||
component.customProps({ text: field, label: "" })
|
component.customProps({ text: field, label: "" })
|
||||||
}
|
}
|
||||||
|
|
|
@ -301,4 +301,13 @@
|
||||||
padding-top: var(--spacing-xs);
|
padding-top: var(--spacing-xs);
|
||||||
padding-bottom: var(--spacing-xs);
|
padding-bottom: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.ag-header) {
|
||||||
|
height: 61px !important;
|
||||||
|
min-height: 61px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ag-header-row) {
|
||||||
|
height: 60px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { Modal, ModalContent } from "@budibase/bbui"
|
import { Modal, ModalContent } from "@budibase/bbui"
|
||||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||||
|
import { FIELDS } from "constants/backend"
|
||||||
|
|
||||||
const SORT_ICON_MAP = {
|
const SORT_ICON_MAP = {
|
||||||
asc: "ri-arrow-down-fill",
|
asc: "ri-arrow-down-fill",
|
||||||
|
@ -51,6 +52,8 @@
|
||||||
column.removeEventListener("sortChanged", setSort)
|
column.removeEventListener("sortChanged", setSort)
|
||||||
column.removeEventListener("filterActiveChanged", setFilterActive)
|
column.removeEventListener("filterActiveChanged", setFilterActive)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$: type = FIELDS[field?.type?.toUpperCase()]?.name
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
|
@ -58,12 +61,17 @@
|
||||||
data-cy="table-header"
|
data-cy="table-header"
|
||||||
on:mouseover={() => (hovered = true)}
|
on:mouseover={() => (hovered = true)}
|
||||||
on:mouseleave={() => (hovered = false)}>
|
on:mouseleave={() => (hovered = false)}>
|
||||||
<div>
|
<div class="column-header">
|
||||||
<div class="col-icon">
|
<div class="column-header-text">
|
||||||
|
<div class="column-header-name">
|
||||||
|
{displayName}
|
||||||
{#if field.autocolumn}<i class="auto ri-magic-fill" />{/if}
|
{#if field.autocolumn}<i class="auto ri-magic-fill" />{/if}
|
||||||
<span class="column-header-name">{displayName}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<i class={`${SORT_ICON_MAP[sortDirection]} sort-icon icon`} />
|
{#if type}
|
||||||
|
<div class="column-header-type">{type}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<i class={`${SORT_ICON_MAP[sortDirection]} icon`} />
|
||||||
</div>
|
</div>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -106,6 +114,23 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header-text {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.column-header-name {
|
.column-header-name {
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -115,9 +140,9 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-icon {
|
.column-header-type {
|
||||||
position: relative;
|
font-size: var(--font-size-xs);
|
||||||
top: 2px;
|
color: var(--grey-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -125,16 +150,13 @@
|
||||||
font-size: var(--font-size-m);
|
font-size: var(--font-size-m);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-icon {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auto {
|
.auto {
|
||||||
font-size: var(--font-size-xs);
|
font-size: 9px;
|
||||||
transition: none;
|
transition: none;
|
||||||
margin-right: 6px;
|
position: relative;
|
||||||
margin-top: 2px;
|
margin-left: 2px;
|
||||||
|
top: -3px;
|
||||||
|
color: var(--grey-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon:hover {
|
.icon:hover {
|
||||||
|
|
|
@ -3,24 +3,43 @@
|
||||||
export let row
|
export let row
|
||||||
export let selectRelationship
|
export let selectRelationship
|
||||||
|
|
||||||
$: count =
|
$: items = row?.[columnName] || []
|
||||||
row && columnName && Array.isArray(row[columnName])
|
|
||||||
? row[columnName].length
|
|
||||||
: 0
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class:link={count} on:click={() => selectRelationship(row, columnName)}>
|
<div
|
||||||
{count}
|
class="container"
|
||||||
related row(s)
|
class:link={!!items.length}
|
||||||
|
on:click={() => selectRelationship(row, columnName)}>
|
||||||
|
{#each items as item}
|
||||||
|
<div class="item">{item}</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.link {
|
.container {
|
||||||
text-decoration: underline;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.link:hover {
|
.link:hover {
|
||||||
color: var(--grey-6);
|
color: var(--grey-6);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.link:hover .item {
|
||||||
|
color: var(--ink);
|
||||||
|
border-color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: var(--spacing-xs) var(--spacing-s);
|
||||||
|
border: 1px solid var(--grey-5);
|
||||||
|
color: var(--grey-7);
|
||||||
|
line-height: normal;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
TextButton,
|
TextButton,
|
||||||
Select,
|
Select,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
Radio,
|
||||||
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
@ -18,6 +20,7 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
const AUTO_COL = "auto"
|
const AUTO_COL = "auto"
|
||||||
|
const LINK_TYPE = FIELDS.LINK.type
|
||||||
let fieldDefinitions = cloneDeep(FIELDS)
|
let fieldDefinitions = cloneDeep(FIELDS)
|
||||||
|
|
||||||
export let onClosed
|
export let onClosed
|
||||||
|
@ -33,6 +36,15 @@
|
||||||
let primaryDisplay =
|
let primaryDisplay =
|
||||||
$backendUiStore.selectedTable.primaryDisplay == null ||
|
$backendUiStore.selectedTable.primaryDisplay == null ||
|
||||||
$backendUiStore.selectedTable.primaryDisplay === field.name
|
$backendUiStore.selectedTable.primaryDisplay === field.name
|
||||||
|
|
||||||
|
let relationshipTypes = [
|
||||||
|
{text: 'Many to many (N:N)', value: 'many-to-many',},
|
||||||
|
{text: 'One to many (1:N)', value: 'one-to-many',}
|
||||||
|
]
|
||||||
|
let types = ['Many to many (N:N)', 'One to many (1:N)']
|
||||||
|
|
||||||
|
let selectedRelationshipType = relationshipTypes.find(type => type.value === field.relationshipType)?.text || 'Many to many (N:N)'
|
||||||
|
|
||||||
let indexes = [...($backendUiStore.selectedTable.indexes || [])]
|
let indexes = [...($backendUiStore.selectedTable.indexes || [])]
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let deletion
|
let deletion
|
||||||
|
@ -44,17 +56,23 @@
|
||||||
$: uneditable =
|
$: uneditable =
|
||||||
$backendUiStore.selectedTable?._id === TableNames.USERS &&
|
$backendUiStore.selectedTable?._id === TableNames.USERS &&
|
||||||
UNEDITABLE_USER_FIELDS.includes(field.name)
|
UNEDITABLE_USER_FIELDS.includes(field.name)
|
||||||
|
$: invalid = field.type === FIELDS.LINK.type && !field.tableId
|
||||||
|
|
||||||
// used to select what different options can be displayed for column type
|
// used to select what different options can be displayed for column type
|
||||||
$: canBeSearched =
|
$: canBeSearched =
|
||||||
field.type !== "link" &&
|
field.type !== LINK_TYPE &&
|
||||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
|
field.subtype !== AUTO_COLUMN_SUB_TYPES.CREATED_BY &&
|
||||||
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
field.subtype !== AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
||||||
$: canBeDisplay = field.type !== "link" && field.type !== AUTO_COL
|
$: canBeDisplay = field.type !== LINK_TYPE && field.type !== AUTO_COL
|
||||||
$: canBeRequired =
|
$: canBeRequired =
|
||||||
field.type !== "link" && !uneditable && field.type !== AUTO_COL
|
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_COL
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
|
// Set relationship type if it's
|
||||||
|
if (field.type === 'link') {
|
||||||
|
field.relationshipType = relationshipTypes.find(type => type.text === selectedRelationshipType).value
|
||||||
|
}
|
||||||
|
|
||||||
if (field.type === AUTO_COL) {
|
if (field.type === AUTO_COL) {
|
||||||
field = buildAutoColumn(
|
field = buildAutoColumn(
|
||||||
$backendUiStore.draftTable.name,
|
$backendUiStore.draftTable.name,
|
||||||
|
@ -84,13 +102,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFieldConstraints(event) {
|
function handleTypeChange(event) {
|
||||||
const definition = fieldDefinitions[event.target.value.toUpperCase()]
|
const definition = fieldDefinitions[event.target.value.toUpperCase()]
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
field.type = definition.type
|
field.type = definition.type
|
||||||
field.constraints = definition.constraints
|
field.constraints = definition.constraints
|
||||||
|
// remove any extra fields that may not be related to this type
|
||||||
|
delete field.autocolumn
|
||||||
|
delete field.subtype
|
||||||
|
delete field.tableId
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeRequired(e) {
|
function onChangeRequired(e) {
|
||||||
|
@ -138,7 +160,7 @@
|
||||||
secondary
|
secondary
|
||||||
thin
|
thin
|
||||||
label="Type"
|
label="Type"
|
||||||
on:change={handleFieldConstraints}
|
on:change={handleTypeChange}
|
||||||
bind:value={field.type}>
|
bind:value={field.type}>
|
||||||
{#each Object.values(fieldDefinitions) as field}
|
{#each Object.values(fieldDefinitions) as field}
|
||||||
<option value={field.type}>{field.name}</option>
|
<option value={field.type}>{field.name}</option>
|
||||||
|
@ -206,6 +228,16 @@
|
||||||
label="Max Value"
|
label="Max Value"
|
||||||
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
||||||
{:else if field.type === 'link'}
|
{:else if field.type === 'link'}
|
||||||
|
<div>
|
||||||
|
<Label grey extraSmall>Select relationship type</Label>
|
||||||
|
<div class="radio-buttons">
|
||||||
|
{#each types as type}
|
||||||
|
<Radio disabled={originalName} name="Relationship type" value={type} bind:group={selectedRelationshipType}>
|
||||||
|
<label for={type}>{type}</label>
|
||||||
|
</Radio>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Select label="Table" thin secondary bind:value={field.tableId}>
|
<Select label="Table" thin secondary bind:value={field.tableId}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each tableOptions as table}
|
{#each tableOptions as table}
|
||||||
|
@ -229,7 +261,9 @@
|
||||||
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
|
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
|
||||||
{/if}
|
{/if}
|
||||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
<Button primary on:click={saveColumn}>Save Column</Button>
|
<Button primary on:click={saveColumn} bind:disabled={invalid}>
|
||||||
|
Save Column
|
||||||
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
@ -241,6 +275,15 @@
|
||||||
title="Confirm Deletion" />
|
title="Confirm Deletion" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
label {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.radio-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
font-size: var(--font-size-xs)
|
||||||
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-xl);
|
grid-gap: var(--spacing-xl);
|
||||||
|
|
|
@ -1,15 +1,37 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, TextArea, Spacer } from "@budibase/bbui"
|
import { Label, Input, TextArea, Spacer } from "@budibase/bbui"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
|
|
||||||
|
let unsaved = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
{#each Object.keys(integration) as configKey}
|
{#each Object.keys(integration) as configKey}
|
||||||
|
{#if typeof integration[configKey] === 'object'}
|
||||||
|
<Label small>{configKey}</Label>
|
||||||
|
<Spacer small />
|
||||||
|
<KeyValueBuilder bind:object={integration[configKey]} on:change />
|
||||||
|
{:else}
|
||||||
|
<div class="form-row">
|
||||||
|
<Label small>{configKey}</Label>
|
||||||
<Input
|
<Input
|
||||||
|
outline
|
||||||
type={integration[configKey].type}
|
type={integration[configKey].type}
|
||||||
label={configKey}
|
on:change
|
||||||
bind:value={integration[configKey]} />
|
bind:value={integration[configKey]} />
|
||||||
<Spacer large />
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { Input, TextArea, Spacer } from "@budibase/bbui"
|
import { Input, Label, TextArea, Spacer } from "@budibase/bbui"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import ICONS from "../icons"
|
import ICONS from "../icons"
|
||||||
|
|
||||||
export let integration = {}
|
export let integration = {}
|
||||||
|
@ -49,17 +50,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if schema}
|
|
||||||
{#each Object.keys(schema) as configKey}
|
|
||||||
<Input
|
|
||||||
thin
|
|
||||||
type={schema[configKey].type}
|
|
||||||
label={configKey}
|
|
||||||
bind:value={integration[configKey]} />
|
|
||||||
<Spacer medium />
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script>
|
||||||
|
export let width = "100"
|
||||||
|
export let height = "100"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
|
viewBox="0 0 120 120"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M103.125 20.625H68.25L60.375
|
||||||
|
36.375H16.5V99.375H111V20.625H103.125ZM103.125 36.375H72.375L76.5
|
||||||
|
28.5H103.125V36.375Z"
|
||||||
|
fill="#FFBA58" />
|
||||||
|
<path
|
||||||
|
d="M75 46.875V52.5H60C58.0127 52.5059 56.1085 53.298 54.7033 54.7033C53.298
|
||||||
|
56.1085 52.5059 58.0127 52.5 60V75H46.875C44.3886 75 42.004 75.9877 40.2459
|
||||||
|
77.7459C38.4877 79.504 37.5 81.8886 37.5 84.375C37.5 86.8614 38.4877 89.246
|
||||||
|
40.2459 91.0041C42.004 92.7623 44.3886 93.75 46.875
|
||||||
|
93.75H52.5V108.75C52.5059 110.737 53.298 112.642 54.7033 114.047C56.1085
|
||||||
|
115.452 58.0127 116.244 60 116.25H74.25V110.625C74.25 107.94 75.3167 105.364
|
||||||
|
77.2155 103.466C79.1143 101.567 81.6897 100.5 84.375 100.5C87.0603 100.5
|
||||||
|
89.6357 101.567 91.5345 103.466C93.4333 105.364 94.5 107.94 94.5
|
||||||
|
110.625V116.25H108.75C110.737 116.244 112.642 115.452 114.047
|
||||||
|
114.047C115.452 112.642 116.244 110.737 116.25 108.75V94.5H110.625C107.94
|
||||||
|
94.5 105.364 93.4333 103.466 91.5345C101.567 89.6357 100.5 87.0603 100.5
|
||||||
|
84.375C100.5 81.6897 101.567 79.1143 103.466 77.2155C105.364 75.3167 107.94
|
||||||
|
74.25 110.625 74.25H116.25V60C116.244 58.0127 115.452 56.1085 114.047
|
||||||
|
54.7033C112.642 53.298 110.737 52.5059 108.75 52.5H93.75V46.875C93.75
|
||||||
|
44.3886 92.7623 42.004 91.0041 40.2459C89.246 38.4877 86.8614 37.5 84.375
|
||||||
|
37.5C81.8886 37.5 79.504 38.4877 77.7459 40.2459C75.9877 42.004 75 44.3886
|
||||||
|
75 46.875Z"
|
||||||
|
fill="#E76A00" />
|
||||||
|
</svg>
|
|
@ -8,6 +8,7 @@ import Airtable from "./Airtable.svelte"
|
||||||
import SqlServer from "./SQLServer.svelte"
|
import SqlServer from "./SQLServer.svelte"
|
||||||
import MySQL from "./MySQL.svelte"
|
import MySQL from "./MySQL.svelte"
|
||||||
import ArangoDB from "./ArangoDB.svelte"
|
import ArangoDB from "./ArangoDB.svelte"
|
||||||
|
import Rest from "./Rest.svelte"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
POSTGRES: Postgres,
|
POSTGRES: Postgres,
|
||||||
|
@ -20,4 +21,5 @@ export default {
|
||||||
AIRTABLE: Airtable,
|
AIRTABLE: Airtable,
|
||||||
MYSQL: MySQL,
|
MYSQL: MySQL,
|
||||||
ARANGODB: ArangoDB,
|
ARANGODB: ArangoDB,
|
||||||
|
REST: Rest,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto, params } from "@sveltech/routify"
|
|
||||||
import { backendUiStore, store } from "builderStore"
|
|
||||||
import { notifier } from "builderStore/store/notifications"
|
|
||||||
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui"
|
|
||||||
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
|
|
||||||
import analytics from "analytics"
|
|
||||||
|
|
||||||
let modal
|
|
||||||
let error = ""
|
|
||||||
|
|
||||||
let name
|
|
||||||
let source
|
|
||||||
let integration
|
|
||||||
let datasource
|
|
||||||
|
|
||||||
function checkValid(evt) {
|
|
||||||
const datasourceName = evt.target.value
|
|
||||||
if (
|
|
||||||
$backendUiStore.datasources?.some(
|
|
||||||
datasource => datasource.name === datasourceName
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
error = `Datasource with name ${tableName} already exists. Please choose another name.`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
error = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveDatasource() {
|
|
||||||
const { type, ...config } = integration
|
|
||||||
|
|
||||||
// Create datasource
|
|
||||||
await backendUiStore.actions.datasources.save({
|
|
||||||
name,
|
|
||||||
source: type,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
notifier.success(`Datasource ${name} created successfully.`)
|
|
||||||
analytics.captureEvent("Datasource Created", { name })
|
|
||||||
|
|
||||||
// Navigate to new datasource
|
|
||||||
$goto(`./datasource/${datasource._id}`)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
title="Create Datasource"
|
|
||||||
confirmText="Create"
|
|
||||||
onConfirm={saveDatasource}
|
|
||||||
disabled={error || !name}>
|
|
||||||
<Input
|
|
||||||
data-cy="datasource-name-input"
|
|
||||||
thin
|
|
||||||
label="Datasource Name"
|
|
||||||
on:input={checkValid}
|
|
||||||
bind:value={name}
|
|
||||||
{error} />
|
|
||||||
<Label grey extraSmall>Create Integrated Table from External Source</Label>
|
|
||||||
<TableIntegrationMenu bind:integration />
|
|
||||||
</ModalContent>
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { DropdownMenu, Button, Input } from "@budibase/bbui"
|
import { DropdownMenu, Button, Input } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
|
|
||||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
<script>
|
|
||||||
import { backendUiStore, store, allScreens } from "builderStore"
|
|
||||||
import { notifier } from "builderStore/store/notifications"
|
|
||||||
import { DropdownMenu, Button, Input, TextButton, Icon } from "@budibase/bbui"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
|
|
||||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
|
||||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
|
||||||
|
|
||||||
export let bindable
|
|
||||||
export let parameters
|
|
||||||
|
|
||||||
let anchor
|
|
||||||
let dropdown
|
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
function hideEditor() {
|
|
||||||
dropdown?.hide()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div on:click|stopPropagation bind:this={anchor}>
|
|
||||||
<TextButton text on:click={dropdown.show} active={false}>
|
|
||||||
<Icon name="add" />
|
|
||||||
Add Parameters
|
|
||||||
</TextButton>
|
|
||||||
<DropdownMenu align="right" {anchor} bind:this={dropdown}>
|
|
||||||
<div class="wrapper">
|
|
||||||
<ParameterBuilder bind:parameters {bindable} />
|
|
||||||
</div>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { DropdownMenu, Button, Input } from "@budibase/bbui"
|
import { DropdownMenu, Button, Input } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
|
|
||||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||||
|
|
||||||
export let query
|
export let query
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
export let icon
|
export let icon
|
||||||
export let title
|
export let title
|
||||||
export let subtitle
|
export let subtitle = undefined
|
||||||
export let disabled
|
export let disabled = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dropdown-item" class:disabled on:click {...$$restProps}>
|
<div class="dropdown-item" class:disabled on:click {...$$restProps}>
|
||||||
|
|
|
@ -41,6 +41,14 @@
|
||||||
table.
|
table.
|
||||||
</Label>
|
</Label>
|
||||||
{:else}
|
{:else}
|
||||||
|
{#if schema.relationshipType === 'one-to-many'}
|
||||||
|
<Select thin secondary on:change={e => linkedRows = [e.target.value]} name={label} {label}>
|
||||||
|
<option value="">Choose an option</option>
|
||||||
|
{#each rows as row}
|
||||||
|
<option selected={row._id === linkedRows[0]} value={row._id}>{getPrettyName(row)}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
{:else}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
secondary
|
secondary
|
||||||
bind:value={linkedRows}
|
bind:value={linkedRows}
|
||||||
|
@ -50,4 +58,5 @@
|
||||||
<option value={row._id}>{getPrettyName(row)}</option>
|
<option value={row._id}>{getPrettyName(row)}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Multiselect>
|
</Multiselect>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
timeOnly: {
|
timeOnly: {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "numeric",
|
minute: "numeric",
|
||||||
hour12: true,
|
hourCycle: "h12",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const POLL_INTERVAL = 5000
|
const POLL_INTERVAL = 5000
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
import { getSchemaForDatasource } from "builderStore/dataBinding"
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let anchorRight, dropdownRight
|
let anchorRight, dropdownRight
|
||||||
|
@ -96,7 +95,7 @@
|
||||||
</div>
|
</div>
|
||||||
{#if value?.type === 'query'}
|
{#if value?.type === 'query'}
|
||||||
<i class="ri-settings-5-line" on:click={drawer.show} />
|
<i class="ri-settings-5-line" on:click={drawer.show} />
|
||||||
<Drawer title={'Query'} bind:this={drawer}>
|
<Drawer title={'Query Parameters'} bind:this={drawer}>
|
||||||
<div slot="buttons">
|
<div slot="buttons">
|
||||||
<Button
|
<Button
|
||||||
blue
|
blue
|
||||||
|
@ -116,10 +115,12 @@
|
||||||
parameters={queries.find(query => query._id === value._id).parameters}
|
parameters={queries.find(query => query._id === value._id).parameters}
|
||||||
bindings={queryBindableProperties} />
|
bindings={queryBindableProperties} />
|
||||||
{/if}
|
{/if}
|
||||||
|
<!-- <Spacer large />-->
|
||||||
<IntegrationQueryEditor
|
<IntegrationQueryEditor
|
||||||
height={200}
|
height={200}
|
||||||
query={value}
|
query={value}
|
||||||
schema={fetchQueryDefinition(value)}
|
schema={fetchQueryDefinition(value)}
|
||||||
|
datasource={$backendUiStore.datasources.find(ds => ds._id === value.datasourceId)}
|
||||||
editable={false} />
|
editable={false} />
|
||||||
<Spacer large />
|
<Spacer large />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Drawer, Spacer } from "@budibase/bbui"
|
import { Button, Drawer, Spacer, Body } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import {
|
import {
|
||||||
|
@ -41,15 +41,15 @@
|
||||||
</heading>
|
</heading>
|
||||||
<div slot="body">
|
<div slot="body">
|
||||||
<div class="root">
|
<div class="root">
|
||||||
|
<Body small grey>
|
||||||
{#if !Object.keys(tempValue || {}).length}
|
{#if !Object.keys(tempValue || {}).length}
|
||||||
<p>Add your first filter column.</p>
|
Add your first filter column.
|
||||||
{:else}
|
{:else}
|
||||||
<p>
|
|
||||||
Results are filtered to only those which match all of the following
|
Results are filtered to only those which match all of the following
|
||||||
constaints.
|
constaints.
|
||||||
</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
<Spacer small />
|
</Body>
|
||||||
|
<Spacer medium />
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<SaveFields
|
<SaveFields
|
||||||
parameterFields={value}
|
parameterFields={value}
|
||||||
|
@ -67,11 +67,6 @@
|
||||||
min-height: calc(40vh - 2 * var(--spacing-l));
|
min-height: calc(40vh - 2 * var(--spacing-l));
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0 0 var(--spacing-s) 0;
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-l);
|
column-gap: var(--spacing-l);
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Input } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let object = {}
|
||||||
|
export let readOnly
|
||||||
|
|
||||||
|
let fields = Object.entries(object).map(([name, value]) => ({ name, value }))
|
||||||
|
|
||||||
|
$: object = fields.reduce(
|
||||||
|
(acc, next) => ({ ...acc, [next.name]: next.value }),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
function addEntry() {
|
||||||
|
fields = [...fields, {}]
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEntry(idx) {
|
||||||
|
fields.splice(idx, 1)
|
||||||
|
fields = fields
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Builds Objects with Key Value Pairs. Useful for building things like Request Headers. -->
|
||||||
|
<div class="container" class:readOnly>
|
||||||
|
{#each fields as field, idx}
|
||||||
|
<Input placeholder="Key" thin outline bind:value={field.name} />
|
||||||
|
<Input placeholder="Value" thin outline bind:value={field.value} />
|
||||||
|
{#if !readOnly}
|
||||||
|
<i class="ri-close-circle-fill" on:click={() => deleteEntry(idx)} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if !readOnly}
|
||||||
|
<Button secondary thin outline on:click={addEntry}>Add</Button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 20px;
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ri-close-circle-fill {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import CodeMirror from "./codemirror"
|
import CodeMirror from "./codemirror"
|
||||||
|
import { Label, Spacer } from "@budibase/bbui"
|
||||||
import { onMount, createEventDispatcher } from "svelte"
|
import { onMount, createEventDispatcher } from "svelte"
|
||||||
import { themeStore } from "builderStore"
|
import { themeStore } from "builderStore"
|
||||||
import { handlebarsCompletions } from "constants/completions"
|
import { handlebarsCompletions } from "constants/completions"
|
||||||
|
@ -11,6 +12,7 @@
|
||||||
LIGHT: "default",
|
LIGHT: "default",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let label
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let readOnly = false
|
export let readOnly = false
|
||||||
export let lineNumbers = true
|
export let lineNumbers = true
|
||||||
|
@ -170,6 +172,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if label}
|
||||||
|
<Label small>{label}</Label>
|
||||||
|
<Spacer medium />
|
||||||
|
{/if}
|
||||||
<div style={`--code-mirror-height: ${editorHeight}px`}>
|
<div style={`--code-mirror-height: ${editorHeight}px`}>
|
||||||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input } from "@budibase/bbui"
|
import { Label, Spacer, Input } from "@budibase/bbui"
|
||||||
import Editor from "./QueryEditor.svelte"
|
import Editor from "./QueryEditor.svelte"
|
||||||
|
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||||
|
|
||||||
export let fields = {}
|
export let fields = {}
|
||||||
export let schema
|
export let schema
|
||||||
|
@ -19,13 +20,33 @@
|
||||||
<form on:submit|preventDefault>
|
<form on:submit|preventDefault>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{#each schemaKeys as field}
|
{#each schemaKeys as field}
|
||||||
|
{#if schema.fields[field]?.type === 'object'}
|
||||||
|
<div>
|
||||||
|
<Label small>{field}</Label>
|
||||||
|
<Spacer small />
|
||||||
|
<KeyValueBuilder readOnly={!editable} bind:object={fields[field]} />
|
||||||
|
</div>
|
||||||
|
{:else if schema.fields[field]?.type === 'json'}
|
||||||
|
<div>
|
||||||
|
<Label extraSmall grey>{field}</Label>
|
||||||
|
<Editor
|
||||||
|
mode="json"
|
||||||
|
on:change={({ detail }) => (fields[field] = detail.value)}
|
||||||
|
readOnly={!editable}
|
||||||
|
value={fields[field]} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="horizontal">
|
||||||
|
<Label small>{field}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter {field} name"
|
placeholder="Enter {field}"
|
||||||
outline
|
outline
|
||||||
disabled={!editable}
|
disabled={!editable}
|
||||||
type={schema.fields[field]?.type}
|
type={schema.fields[field]?.type}
|
||||||
required={schema.fields[field]?.required}
|
required={schema.fields[field]?.required}
|
||||||
bind:value={fields[field]} />
|
bind:value={fields[field]} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -42,8 +63,15 @@
|
||||||
.field {
|
.field {
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-gap: var(--spacing-m);
|
grid-gap: var(--spacing-m);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.horizontal {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Input, Label } from "@budibase/bbui"
|
import { Body, Button, Input, Heading, Spacer } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
|
@ -30,7 +30,22 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<Label small>Parameters</Label>
|
<div class="controls">
|
||||||
|
<Heading small lh>Parameters</Heading>
|
||||||
|
{#if !bindable}
|
||||||
|
<Button secondary on:click={newQueryParameter}>Add Param</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Body small grey>
|
||||||
|
{#if !bindable}
|
||||||
|
Parameters come in two parts: the parameter name, and a default/fallback
|
||||||
|
value.
|
||||||
|
{:else}
|
||||||
|
Enter a value for each parameter. The default values will be used for any
|
||||||
|
values left blank.
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
<Spacer large />
|
||||||
<div class="parameters" class:bindable>
|
<div class="parameters" class:bindable>
|
||||||
{#each parameters as parameter, idx}
|
{#each parameters as parameter, idx}
|
||||||
<Input
|
<Input
|
||||||
|
@ -58,9 +73,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if !bindable}
|
|
||||||
<Button secondary on:click={newQueryParameter}>Add Parameter</Button>
|
|
||||||
{/if}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -68,6 +80,13 @@
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.parameters {
|
.parameters {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 5%;
|
grid-template-columns: 1fr 1fr 5%;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
Button,
|
Button,
|
||||||
|
Body,
|
||||||
Label,
|
Label,
|
||||||
Input,
|
Input,
|
||||||
TextArea,
|
TextArea,
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
||||||
import EditQueryParamsPopover from "components/backend/DatasourceNavigator/popovers/EditQueryParamsPopover.svelte"
|
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
|
||||||
const PREVIEW_HEADINGS = [
|
const PREVIEW_HEADINGS = [
|
||||||
|
@ -59,10 +60,10 @@
|
||||||
|
|
||||||
$: datasourceType = datasource?.source
|
$: datasourceType = datasource?.source
|
||||||
|
|
||||||
$: config = $backendUiStore.integrations[datasourceType]?.query
|
$: integrationInfo = $backendUiStore.integrations[datasourceType]
|
||||||
$: docsLink = $backendUiStore.integrations[datasourceType]?.docs
|
$: queryConfig = integrationInfo?.query
|
||||||
|
|
||||||
$: shouldShowQueryConfig = config && query.queryVerb
|
$: shouldShowQueryConfig = queryConfig && query.queryVerb
|
||||||
|
|
||||||
function newField() {
|
function newField() {
|
||||||
fields = [...fields, {}]
|
fields = [...fields, {}]
|
||||||
|
@ -129,58 +130,88 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<section class="config">
|
||||||
<div class="input">
|
<Heading medium lh>Query {integrationInfo?.friendlyName}</Heading>
|
||||||
<div class="label">Enter query name:</div>
|
<hr />
|
||||||
<Input outline border bind:value={query.name} />
|
<Heading small lh>Config</Heading>
|
||||||
|
<Body small grey>Provide a name for your query and select its function.</Body>
|
||||||
|
<Spacer medium />
|
||||||
|
<div class="config-field">
|
||||||
|
<Label small>Query Name</Label>
|
||||||
|
<Input thin outline bind:value={query.name} />
|
||||||
</div>
|
</div>
|
||||||
{#if config}
|
<Spacer medium />
|
||||||
<div class="props">
|
{#if queryConfig}
|
||||||
<div class="query-type">
|
<div class="config-field">
|
||||||
Query type:
|
<Label small>Function</Label>
|
||||||
<span class="query-type-span">{config[query.queryVerb].type}</span>
|
<Select primary outline thin bind:value={query.queryVerb}>
|
||||||
</div>
|
{#each Object.keys(queryConfig) as queryVerb}
|
||||||
<div class="select">
|
<option value={queryVerb}>
|
||||||
<Select primary thin bind:value={query.queryVerb}>
|
{queryConfig[queryVerb]?.displayName || queryVerb}
|
||||||
{#each Object.keys(config) as queryVerb}
|
</option>
|
||||||
<option value={queryVerb}>{queryVerb}</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<hr />
|
||||||
<EditQueryParamsPopover
|
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
|
||||||
bind:parameters={query.parameters}
|
<hr />
|
||||||
bindable={false} />
|
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</section>
|
||||||
<Spacer extraLarge />
|
|
||||||
|
|
||||||
{#if shouldShowQueryConfig}
|
{#if shouldShowQueryConfig}
|
||||||
<section>
|
<section>
|
||||||
<div class="config">
|
<div class="config">
|
||||||
|
<Heading small lh>Fields</Heading>
|
||||||
|
<Body small grey>Fill in the fields specific to this query.</Body>
|
||||||
|
<Spacer medium />
|
||||||
<IntegrationQueryEditor
|
<IntegrationQueryEditor
|
||||||
|
{datasource}
|
||||||
{query}
|
{query}
|
||||||
schema={config[query.queryVerb]}
|
schema={queryConfig[query.queryVerb]}
|
||||||
bind:parameters />
|
bind:parameters />
|
||||||
|
|
||||||
<Spacer extraLarge />
|
<hr />
|
||||||
<Spacer large />
|
|
||||||
|
|
||||||
<div class="viewer-controls">
|
<div class="viewer-controls">
|
||||||
|
<Heading small lh>Query Results</Heading>
|
||||||
|
<div class="button-container">
|
||||||
<Button
|
<Button
|
||||||
blue
|
secondary
|
||||||
|
thin
|
||||||
disabled={data.length === 0 || !query.name}
|
disabled={data.length === 0 || !query.name}
|
||||||
on:click={saveQuery}>
|
on:click={saveQuery}>
|
||||||
Save Query
|
Save Query
|
||||||
</Button>
|
</Button>
|
||||||
<Button primary on:click={previewQuery}>Run Query</Button>
|
<Spacer medium />
|
||||||
|
<Button thin primary on:click={previewQuery}>Run Query</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<Body small grey>
|
||||||
|
Below, you can preview the results from your query and change the
|
||||||
|
schema.
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
<Spacer large />
|
||||||
|
|
||||||
<section class="viewer">
|
<section class="viewer">
|
||||||
{#if data}
|
{#if data}
|
||||||
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
|
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
|
||||||
{#if tab === 'JSON'}
|
{#if tab === 'JSON'}
|
||||||
<pre class="preview">{JSON.stringify(data[0], undefined, 2)}</pre>
|
<pre class="preview">
|
||||||
|
{#if !data[0]}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Please run your query to fetch some data.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
{JSON.stringify(data[0], undefined, 2)}
|
||||||
|
{/if}
|
||||||
|
</pre>
|
||||||
{:else if tab === 'PREVIEW'}
|
{:else if tab === 'PREVIEW'}
|
||||||
<ExternalDataSourceTable {query} {data} />
|
<ExternalDataSourceTable {query} {data} />
|
||||||
{:else if tab === 'SCHEMA'}
|
{:else if tab === 'SCHEMA'}
|
||||||
|
@ -215,33 +246,26 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.input {
|
.config-field {
|
||||||
width: 500px;
|
display: grid;
|
||||||
display: flex;
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select {
|
|
||||||
width: 200px;
|
|
||||||
margin-right: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.props {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-left: auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--layout-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 50px;
|
grid-template-columns: 1fr 1fr 5%;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.button-container {
|
||||||
font-size: var(--font-size-s);
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-top: var(--layout-m);
|
||||||
|
margin-bottom: var(--layout-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.config {
|
.config {
|
||||||
|
@ -253,16 +277,6 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-type {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
color: var(--grey-8);
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-type-span {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
width: 800px;
|
width: 800px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -271,31 +285,12 @@
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewer-controls {
|
.viewer-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-left: auto;
|
justify-content: space-between;
|
||||||
direction: rtl;
|
|
||||||
z-index: 5;
|
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
align-items: center;
|
||||||
|
|
||||||
.viewer {
|
|
||||||
margin-top: -28px;
|
|
||||||
z-index: -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
color: var(--grey-8);
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
margin-right: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Editor from "./QueryEditor.svelte"
|
import Editor from "./QueryEditor.svelte"
|
||||||
import FieldsBuilder from "./QueryFieldsBuilder.svelte"
|
import FieldsBuilder from "./QueryFieldsBuilder.svelte"
|
||||||
|
import { Label, Input } from "@budibase/bbui"
|
||||||
|
|
||||||
const QueryTypes = {
|
const QueryTypes = {
|
||||||
SQL: "sql",
|
SQL: "sql",
|
||||||
|
@ -9,10 +10,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
export let query
|
export let query
|
||||||
|
export let datasource
|
||||||
export let schema
|
export let schema
|
||||||
export let editable = true
|
export let editable = true
|
||||||
export let height = 500
|
export let height = 500
|
||||||
|
|
||||||
|
$: urlDisplay =
|
||||||
|
schema.urlDisplay &&
|
||||||
|
`${datasource.config.url}${query.fields.path}${query.fields.queryString}`
|
||||||
|
|
||||||
function updateQuery({ detail }) {
|
function updateQuery({ detail }) {
|
||||||
query.fields[schema.type] = detail.value
|
query.fields[schema.type] = detail.value
|
||||||
}
|
}
|
||||||
|
@ -40,6 +46,21 @@
|
||||||
parameters={query.parameters} />
|
parameters={query.parameters} />
|
||||||
{:else if schema.type === QueryTypes.FIELDS}
|
{:else if schema.type === QueryTypes.FIELDS}
|
||||||
<FieldsBuilder bind:fields={query.fields} {schema} {editable} />
|
<FieldsBuilder bind:fields={query.fields} {schema} {editable} />
|
||||||
|
{#if schema.urlDisplay}
|
||||||
|
<div class="url-row">
|
||||||
|
<Label small>URL</Label>
|
||||||
|
<Input thin outline disabled value={urlDisplay} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.url-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { params } from "@sveltech/routify"
|
import { params } from "@sveltech/routify"
|
||||||
import { Switcher, Modal } from "@budibase/bbui"
|
import { Button, Switcher, Modal } from "@budibase/bbui"
|
||||||
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
||||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||||
|
@ -8,11 +8,11 @@
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
title: "Tables",
|
title: "Internal",
|
||||||
key: "table",
|
key: "table",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Data Sources",
|
title: "External",
|
||||||
key: "datasource",
|
key: "datasource",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -67,6 +67,7 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
|
|
|
@ -36,6 +36,8 @@
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
|
width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto, beforeUrlChange } from "@sveltech/routify"
|
||||||
import { Button, Spacer, Icon } from "@budibase/bbui"
|
import { Button, Heading, Body, Spacer, Icon } from "@budibase/bbui"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
|
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||||
|
|
||||||
|
let unsaved = false
|
||||||
|
|
||||||
$: datasource = $backendUiStore.datasources.find(
|
$: datasource = $backendUiStore.datasources.find(
|
||||||
ds => ds._id === $backendUiStore.selectedDatasourceId
|
ds => ds._id === $backendUiStore.selectedDatasourceId
|
||||||
)
|
)
|
||||||
|
$: integration = datasource && $backendUiStore.integrations[datasource.source]
|
||||||
|
|
||||||
async function saveDatasource() {
|
async function saveDatasource() {
|
||||||
// Create datasource
|
// Create datasource
|
||||||
await backendUiStore.actions.datasources.save(datasource)
|
await backendUiStore.actions.datasources.save(datasource)
|
||||||
notifier.success(`Datasource ${name} saved successfully.`)
|
notifier.success(`Datasource ${name} saved successfully.`)
|
||||||
|
unsaved = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickQuery(query) {
|
function onClickQuery(query) {
|
||||||
|
@ -22,28 +27,60 @@
|
||||||
backendUiStore.actions.queries.select(query)
|
backendUiStore.actions.queries.select(query)
|
||||||
$goto(`../${query._id}`)
|
$goto(`../${query._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setUnsaved() {
|
||||||
|
unsaved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
$beforeUrlChange((event, store) => {
|
||||||
|
if (unsaved) {
|
||||||
|
notifier.danger(
|
||||||
|
"Unsaved changes. Please save your datasource configuration before leaving."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if datasource}
|
{#if datasource}
|
||||||
<section>
|
<section>
|
||||||
<Spacer medium />
|
<Spacer medium />
|
||||||
<header>
|
<header>
|
||||||
|
<div class="datasource-icon">
|
||||||
|
<svelte:component
|
||||||
|
this={ICONS[datasource.source]}
|
||||||
|
height="30"
|
||||||
|
width="30" />
|
||||||
|
</div>
|
||||||
<h3 class="section-title">{datasource.name}</h3>
|
<h3 class="section-title">{datasource.name}</h3>
|
||||||
</header>
|
</header>
|
||||||
<Spacer extraLarge />
|
|
||||||
|
<Body small grey lh>{integration.description}</Body>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
<h5>Configuration</h5>
|
<Heading small>Configuration</Heading>
|
||||||
<Button secondary on:click={saveDatasource}>Save</Button>
|
<Button secondary on:click={saveDatasource}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Body small grey>
|
||||||
|
Connect your database to Budibase using the config below.
|
||||||
|
</Body>
|
||||||
|
|
||||||
<Spacer medium />
|
<Spacer medium />
|
||||||
<IntegrationConfigForm integration={datasource.config} />
|
<IntegrationConfigForm
|
||||||
</div>
|
integration={datasource.config}
|
||||||
<Spacer extraLarge />
|
on:change={setUnsaved} />
|
||||||
<div class="container">
|
<Spacer medium />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
<div class="query-header">
|
<div class="query-header">
|
||||||
<h5>Queries</h5>
|
<Heading small>Queries</Heading>
|
||||||
<Button blue on:click={() => $goto('../new')}>Create Query</Button>
|
<Button secondary on:click={() => $goto('../new')}>Add Query</Button>
|
||||||
</div>
|
</div>
|
||||||
<Spacer extraLarge />
|
<Spacer extraLarge />
|
||||||
<div class="query-list">
|
<div class="query-list">
|
||||||
|
@ -54,7 +91,6 @@
|
||||||
<p>→</p>
|
<p>→</p>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<Spacer medium />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -64,13 +100,20 @@
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 800px;
|
width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-bottom: var(--layout-m);
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
margin: 0 0 var(--spacing-xs) 0;
|
margin: 0 0 var(--spacing-xs) 0;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
|
@ -85,13 +128,12 @@
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
border-radius: var(--border-radius-m);
|
border-radius: var(--border-radius-m);
|
||||||
background: var(--background);
|
|
||||||
padding: var(--layout-s);
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
font-size: var(--font-size-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-header {
|
.query-header {
|
||||||
|
@ -115,7 +157,8 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 0.75fr 20px;
|
grid-template-columns: 2fr 0.75fr 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing-m) var(--layout-xs);
|
padding-left: var(--spacing-m);
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
gap: var(--layout-xs);
|
gap: var(--layout-xs);
|
||||||
transition: 200ms background ease;
|
transition: 200ms background ease;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -119,8 +119,8 @@ export const enrichRows = async (rows, tableId) => {
|
||||||
for (let key of keys) {
|
for (let key of keys) {
|
||||||
const type = schema[key].type
|
const type = schema[key].type
|
||||||
if (type === "link") {
|
if (type === "link") {
|
||||||
// Enrich row with the count of any relationship fields
|
// Enrich row a string join of relationship fields
|
||||||
row[`${key}_count`] = Array.isArray(row[key]) ? row[key].length : 0
|
row[`${key}_text`] = row[key]?.join(", ") || ""
|
||||||
} else if (type === "attachment") {
|
} else if (type === "attachment") {
|
||||||
// Enrich row with the first image URL for any attachment fields
|
// Enrich row with the first image URL for any attachment fields
|
||||||
let url = null
|
let url = null
|
||||||
|
|
|
@ -5,6 +5,8 @@ const env = require("../../environment")
|
||||||
const { getAPIKey } = require("../../utilities/usageQuota")
|
const { getAPIKey } = require("../../utilities/usageQuota")
|
||||||
const { generateUserID } = require("../../db/utils")
|
const { generateUserID } = require("../../db/utils")
|
||||||
const { setCookie } = require("../../utilities")
|
const { setCookie } = require("../../utilities")
|
||||||
|
const { outputProcessing } = require("../../utilities/rowProcessor")
|
||||||
|
const { ViewNames } = require("../../db/utils")
|
||||||
|
|
||||||
exports.authenticate = async ctx => {
|
exports.authenticate = async ctx => {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
|
@ -62,12 +64,14 @@ exports.fetchSelf = async ctx => {
|
||||||
const { userId, appId } = ctx.user
|
const { userId, appId } = ctx.user
|
||||||
if (!userId || !appId) {
|
if (!userId || !appId) {
|
||||||
ctx.body = {}
|
ctx.body = {}
|
||||||
} else {
|
return
|
||||||
const database = new CouchDB(appId)
|
}
|
||||||
const user = await database.get(userId)
|
const db = new CouchDB(appId)
|
||||||
|
const user = await db.get(userId)
|
||||||
|
const userTable = await db.get(ViewNames.USERS)
|
||||||
if (user) {
|
if (user) {
|
||||||
delete user.password
|
delete user.password
|
||||||
}
|
}
|
||||||
ctx.body = user
|
// specifically needs to make sure is enriched
|
||||||
}
|
ctx.body = await outputProcessing(appId, userTable, user)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,13 +58,25 @@ async function enrichQueryFields(fields, parameters) {
|
||||||
|
|
||||||
// enrich the fields with dynamic parameters
|
// enrich the fields with dynamic parameters
|
||||||
for (let key of Object.keys(fields)) {
|
for (let key of Object.keys(fields)) {
|
||||||
|
if (typeof fields[key] === "object") {
|
||||||
|
// enrich nested fields object
|
||||||
|
enrichedQuery[key] = await enrichQueryFields(fields[key], parameters)
|
||||||
|
} else {
|
||||||
|
// enrich string value as normal
|
||||||
enrichedQuery[key] = await processString(fields[key], parameters)
|
enrichedQuery[key] = await processString(fields[key], parameters)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (enrichedQuery.json || enrichedQuery.customData) {
|
if (
|
||||||
|
enrichedQuery.json ||
|
||||||
|
enrichedQuery.customData ||
|
||||||
|
enrichedQuery.requestBody
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
enrichedQuery.json = JSON.parse(
|
enrichedQuery.json = JSON.parse(
|
||||||
enrichedQuery.json || enrichedQuery.customData
|
enrichedQuery.json ||
|
||||||
|
enrichedQuery.customData ||
|
||||||
|
enrichedQuery.requestBody
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw { message: `JSON Invalid - error: ${err}` }
|
throw { message: `JSON Invalid - error: ${err}` }
|
||||||
|
|
|
@ -14,6 +14,7 @@ const {
|
||||||
outputProcessing,
|
outputProcessing,
|
||||||
} = require("../../utilities/rowProcessor")
|
} = require("../../utilities/rowProcessor")
|
||||||
const { FieldTypes } = require("../../constants")
|
const { FieldTypes } = require("../../constants")
|
||||||
|
const { isEqual } = require("lodash")
|
||||||
|
|
||||||
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
||||||
|
|
||||||
|
@ -68,7 +69,7 @@ exports.patch = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// this returns the table and row incase they have been updated
|
// this returns the table and row incase they have been updated
|
||||||
let { table, row } = await inputProcessing(ctx.user, dbTable, dbRow)
|
let { table, row } = inputProcessing(ctx.user, dbTable, dbRow)
|
||||||
const validateResult = await validate({
|
const validateResult = await validate({
|
||||||
row,
|
row,
|
||||||
table,
|
table,
|
||||||
|
@ -101,6 +102,10 @@ exports.patch = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await db.put(row)
|
const response = await db.put(row)
|
||||||
|
// don't worry about rev, tables handle rev/lastID updates
|
||||||
|
if (!isEqual(dbTable, table)) {
|
||||||
|
await db.put(table)
|
||||||
|
}
|
||||||
row._rev = response.rev
|
row._rev = response.rev
|
||||||
row.type = "row"
|
row.type = "row"
|
||||||
|
|
||||||
|
@ -136,11 +141,8 @@ exports.save = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// this returns the table and row incase they have been updated
|
// this returns the table and row incase they have been updated
|
||||||
let { table, row } = await inputProcessing(
|
const dbTable = await db.get(inputs.tableId)
|
||||||
ctx.user,
|
let { table, row } = inputProcessing(ctx.user, dbTable, inputs)
|
||||||
await db.get(inputs.tableId),
|
|
||||||
inputs
|
|
||||||
)
|
|
||||||
const validateResult = await validate({
|
const validateResult = await validate({
|
||||||
row,
|
row,
|
||||||
table,
|
table,
|
||||||
|
@ -174,6 +176,10 @@ exports.save = async function(ctx) {
|
||||||
|
|
||||||
row.type = "row"
|
row.type = "row"
|
||||||
const response = await db.put(row)
|
const response = await db.put(row)
|
||||||
|
// don't worry about rev, tables handle rev/lastID updates
|
||||||
|
if (!isEqual(dbTable, table)) {
|
||||||
|
await db.put(table)
|
||||||
|
}
|
||||||
row._rev = response.rev
|
row._rev = response.rev
|
||||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
|
||||||
ctx.body = row
|
ctx.body = row
|
||||||
|
|
|
@ -9,6 +9,7 @@ const {
|
||||||
} = require("../../db/utils")
|
} = require("../../db/utils")
|
||||||
const { isEqual } = require("lodash/fp")
|
const { isEqual } = require("lodash/fp")
|
||||||
const { FieldTypes, AutoFieldSubTypes } = require("../../constants")
|
const { FieldTypes, AutoFieldSubTypes } = require("../../constants")
|
||||||
|
const { inputProcessing } = require("../../utilities/rowProcessor")
|
||||||
|
|
||||||
async function checkForColumnUpdates(db, oldTable, updatedTable) {
|
async function checkForColumnUpdates(db, oldTable, updatedTable) {
|
||||||
let updatedRows
|
let updatedRows
|
||||||
|
@ -61,6 +62,82 @@ function makeSureTableUpToDate(table, tableToSave) {
|
||||||
return tableToSave
|
return tableToSave
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDataImport(user, table, dataImport) {
|
||||||
|
const db = new CouchDB(user.appId)
|
||||||
|
if (dataImport && dataImport.csvString) {
|
||||||
|
// Populate the table with rows imported from CSV in a bulk update
|
||||||
|
const data = await csvParser.transform(dataImport)
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
let row = data[i]
|
||||||
|
row._id = generateRowID(table._id)
|
||||||
|
row.tableId = table._id
|
||||||
|
const processed = inputProcessing(user, table, row)
|
||||||
|
row = processed.row
|
||||||
|
// these auto-fields will never actually link anywhere (always builder)
|
||||||
|
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
||||||
|
if (
|
||||||
|
schema.autocolumn &&
|
||||||
|
(schema.subtype === AutoFieldSubTypes.CREATED_BY ||
|
||||||
|
schema.subtype === AutoFieldSubTypes.UPDATED_BY)
|
||||||
|
) {
|
||||||
|
delete row[fieldName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table = processed.table
|
||||||
|
data[i] = row
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.bulkDocs(data)
|
||||||
|
let response = await db.put(table)
|
||||||
|
table._rev = response._rev
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearchIndexes(db, table) {
|
||||||
|
// create relevant search indexes
|
||||||
|
if (table.indexes && table.indexes.length > 0) {
|
||||||
|
const currentIndexes = await db.getIndexes()
|
||||||
|
const indexName = `search:${table._id}`
|
||||||
|
|
||||||
|
const existingIndex = currentIndexes.indexes.find(
|
||||||
|
existing => existing.name === indexName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existingIndex) {
|
||||||
|
const currentFields = existingIndex.def.fields.map(
|
||||||
|
field => Object.keys(field)[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
// if index fields have changed, delete the original index
|
||||||
|
if (!isEqual(currentFields, table.indexes)) {
|
||||||
|
await db.deleteIndex(existingIndex)
|
||||||
|
// create/recreate the index with fields
|
||||||
|
await db.createIndex({
|
||||||
|
index: {
|
||||||
|
fields: table.indexes,
|
||||||
|
name: indexName,
|
||||||
|
ddoc: "search_ddoc",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// create/recreate the index with fields
|
||||||
|
await db.createIndex({
|
||||||
|
index: {
|
||||||
|
fields: table.indexes,
|
||||||
|
name: indexName,
|
||||||
|
ddoc: "search_ddoc",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.appId)
|
const db = new CouchDB(ctx.user.appId)
|
||||||
const body = await db.allDocs(
|
const body = await db.allDocs(
|
||||||
|
@ -152,61 +229,12 @@ exports.save = async function(ctx) {
|
||||||
const result = await db.post(tableToSave)
|
const result = await db.post(tableToSave)
|
||||||
tableToSave._rev = result.rev
|
tableToSave._rev = result.rev
|
||||||
|
|
||||||
// create relevant search indexes
|
tableToSave = await handleSearchIndexes(db, tableToSave)
|
||||||
if (tableToSave.indexes && tableToSave.indexes.length > 0) {
|
tableToSave = await handleDataImport(ctx.user, tableToSave, dataImport)
|
||||||
const currentIndexes = await db.getIndexes()
|
|
||||||
const indexName = `search:${result.id}`
|
|
||||||
|
|
||||||
const existingIndex = currentIndexes.indexes.find(
|
|
||||||
existing => existing.name === indexName
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingIndex) {
|
|
||||||
const currentFields = existingIndex.def.fields.map(
|
|
||||||
field => Object.keys(field)[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
// if index fields have changed, delete the original index
|
|
||||||
if (!isEqual(currentFields, tableToSave.indexes)) {
|
|
||||||
await db.deleteIndex(existingIndex)
|
|
||||||
// create/recreate the index with fields
|
|
||||||
await db.createIndex({
|
|
||||||
index: {
|
|
||||||
fields: tableToSave.indexes,
|
|
||||||
name: indexName,
|
|
||||||
ddoc: "search_ddoc",
|
|
||||||
type: "json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// create/recreate the index with fields
|
|
||||||
await db.createIndex({
|
|
||||||
index: {
|
|
||||||
fields: tableToSave.indexes,
|
|
||||||
name: indexName,
|
|
||||||
ddoc: "search_ddoc",
|
|
||||||
type: "json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
ctx.eventEmitter.emitTable(`table:save`, appId, tableToSave)
|
ctx.eventEmitter.emitTable(`table:save`, appId, tableToSave)
|
||||||
|
|
||||||
if (dataImport && dataImport.csvString) {
|
|
||||||
// Populate the table with rows imported from CSV in a bulk update
|
|
||||||
const data = await csvParser.transform(dataImport)
|
|
||||||
|
|
||||||
for (let row of data) {
|
|
||||||
row._id = generateRowID(tableToSave._id)
|
|
||||||
row.tableId = tableToSave._id
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.bulkDocs(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.message = `Table ${ctx.request.body.name} saved successfully.`
|
ctx.message = `Table ${ctx.request.body.name} saved successfully.`
|
||||||
ctx.body = tableToSave
|
ctx.body = tableToSave
|
||||||
|
|
|
@ -137,6 +137,7 @@ exports.addPermission = async (
|
||||||
exports.createLinkedTable = async (request, appId) => {
|
exports.createLinkedTable = async (request, appId) => {
|
||||||
// get the ID to link to
|
// get the ID to link to
|
||||||
const table = await exports.createTable(request, appId)
|
const table = await exports.createTable(request, appId)
|
||||||
|
table.primaryDisplay = "name"
|
||||||
table.schema.link = {
|
table.schema.link = {
|
||||||
type: "link",
|
type: "link",
|
||||||
fieldName: "link",
|
fieldName: "link",
|
||||||
|
|
|
@ -287,7 +287,7 @@ describe("/rows", () => {
|
||||||
})).body
|
})).body
|
||||||
const enriched = await outputProcessing(appId, table, [secondRow])
|
const enriched = await outputProcessing(appId, table, [secondRow])
|
||||||
expect(enriched[0].link.length).toBe(1)
|
expect(enriched[0].link.length).toBe(1)
|
||||||
expect(enriched[0].link[0]).toBe(firstRow._id)
|
expect(enriched[0].link[0]).toBe("Test Contact")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,13 @@ const {
|
||||||
getLinkDocuments,
|
getLinkDocuments,
|
||||||
createLinkView,
|
createLinkView,
|
||||||
getUniqueByProp,
|
getUniqueByProp,
|
||||||
|
getRelatedTableForField,
|
||||||
|
getLinkedTableIDs,
|
||||||
|
getLinkedTable,
|
||||||
} = require("./linkUtils")
|
} = require("./linkUtils")
|
||||||
const { flatten } = require("lodash")
|
const { flatten } = require("lodash")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
const { getMultiIDParams } = require("../../db/utils")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This functionality makes sure that when rows with links are created, updated or deleted they are processed
|
* This functionality makes sure that when rows with links are created, updated or deleted they are processed
|
||||||
|
@ -27,6 +32,30 @@ exports.IncludeDocs = IncludeDocs
|
||||||
exports.getLinkDocuments = getLinkDocuments
|
exports.getLinkDocuments = getLinkDocuments
|
||||||
exports.createLinkView = createLinkView
|
exports.createLinkView = createLinkView
|
||||||
|
|
||||||
|
async function getLinksForRows(appId, rows) {
|
||||||
|
const tableIds = [...new Set(rows.map(el => el.tableId))]
|
||||||
|
// start by getting all the link values for performance reasons
|
||||||
|
const responses = flatten(
|
||||||
|
await Promise.all(
|
||||||
|
tableIds.map(tableId =>
|
||||||
|
getLinkDocuments({
|
||||||
|
appId,
|
||||||
|
tableId: tableId,
|
||||||
|
includeDocs: IncludeDocs.EXCLUDE,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// have to get unique as the previous table query can
|
||||||
|
// return duplicates, could be querying for both tables in a relation
|
||||||
|
return getUniqueByProp(
|
||||||
|
responses
|
||||||
|
// create a unique ID which we can use for getting only unique ones
|
||||||
|
.map(el => ({ ...el, unique: el.id + el.fieldName })),
|
||||||
|
"unique"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update link documents for a row or table - this is to be called by the API controller when a change is occurring.
|
* Update link documents for a row or table - this is to be called by the API controller when a change is occurring.
|
||||||
* @param {string} eventType states what type of change which is occurring, means this can be expanded upon in the
|
* @param {string} eventType states what type of change which is occurring, means this can be expanded upon in the
|
||||||
|
@ -92,49 +121,66 @@ exports.updateLinks = async function({
|
||||||
* @returns {Promise<object>} The updated row (this may be the same if no links were found). If an array was input
|
* @returns {Promise<object>} The updated row (this may be the same if no links were found). If an array was input
|
||||||
* then an array will be output, object input -> object output.
|
* then an array will be output, object input -> object output.
|
||||||
*/
|
*/
|
||||||
exports.attachLinkInfo = async (appId, rows) => {
|
exports.attachLinkIDs = async (appId, rows) => {
|
||||||
// handle a single row as well as multiple
|
const links = await getLinksForRows(appId, rows)
|
||||||
let wasArray = true
|
|
||||||
if (!(rows instanceof Array)) {
|
|
||||||
rows = [rows]
|
|
||||||
wasArray = false
|
|
||||||
}
|
|
||||||
let tableIds = [...new Set(rows.map(el => el.tableId))]
|
|
||||||
// start by getting all the link values for performance reasons
|
|
||||||
let responses = flatten(
|
|
||||||
await Promise.all(
|
|
||||||
tableIds.map(tableId =>
|
|
||||||
getLinkDocuments({
|
|
||||||
appId,
|
|
||||||
tableId: tableId,
|
|
||||||
includeDocs: IncludeDocs.EXCLUDE,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// now iterate through the rows and all field information
|
// now iterate through the rows and all field information
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
// get all links for row, ignore fieldName for now
|
// find anything that matches the row's ID we are searching for and join it
|
||||||
// have to get unique as the previous table query can
|
links
|
||||||
// return duplicates, could be querying for both tables in a relation
|
|
||||||
const linkVals = getUniqueByProp(
|
|
||||||
responses
|
|
||||||
// find anything that matches the row's ID we are searching for
|
|
||||||
.filter(el => el.thisId === row._id)
|
.filter(el => el.thisId === row._id)
|
||||||
// create a unique ID which we can use for getting only unique ones
|
.forEach(link => {
|
||||||
.map(el => ({ ...el, unique: el.id + el.fieldName })),
|
if (row[link.fieldName] == null) {
|
||||||
"unique"
|
row[link.fieldName] = []
|
||||||
)
|
|
||||||
for (let linkVal of linkVals) {
|
|
||||||
// work out which link pertains to this row
|
|
||||||
if (!(row[linkVal.fieldName] instanceof Array)) {
|
|
||||||
row[linkVal.fieldName] = [linkVal.id]
|
|
||||||
} else {
|
|
||||||
row[linkVal.fieldName].push(linkVal.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
row[link.fieldName].push(link.id)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// if it was an array when it came in then handle it as an array in response
|
// if it was an array when it came in then handle it as an array in response
|
||||||
// otherwise return the first element as there was only one input
|
// otherwise return the first element as there was only one input
|
||||||
return wasArray ? rows : rows[0]
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given information about the table we can extract the display name from the linked rows, this
|
||||||
|
* is what we do for showing the display name of each linked row when in a table format.
|
||||||
|
* @param {string} appId The app in which the tables/rows/links exist.
|
||||||
|
* @param {object} table The table from which the rows originated.
|
||||||
|
* @param {array<object>} rows The rows which are to be enriched with the linked display names/IDs.
|
||||||
|
* @returns {Promise<Array>} The enriched rows after having display names/IDs attached to the linked fields.
|
||||||
|
*/
|
||||||
|
exports.attachLinkedPrimaryDisplay = async (appId, table, rows) => {
|
||||||
|
const linkedTableIds = getLinkedTableIDs(table)
|
||||||
|
if (linkedTableIds.length === 0) {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
const db = new CouchDB(appId)
|
||||||
|
const links = (await getLinksForRows(appId, rows)).filter(link =>
|
||||||
|
rows.some(row => row._id === link.thisId)
|
||||||
|
)
|
||||||
|
const linkedRowIds = links.map(link => link.id)
|
||||||
|
const linked = (await db.allDocs(getMultiIDParams(linkedRowIds))).rows.map(
|
||||||
|
row => row.doc
|
||||||
|
)
|
||||||
|
// will populate this as we find them
|
||||||
|
const linkedTables = []
|
||||||
|
for (let row of rows) {
|
||||||
|
for (let link of links.filter(link => link.thisId === row._id)) {
|
||||||
|
if (row[link.fieldName] == null) {
|
||||||
|
row[link.fieldName] = []
|
||||||
|
}
|
||||||
|
const linkedRow = linked.find(row => row._id === link.id)
|
||||||
|
const linkedTableId =
|
||||||
|
linkedRow.tableId || getRelatedTableForField(table, link.fieldName)
|
||||||
|
const linkedTable = await getLinkedTable(db, linkedTableId, linkedTables)
|
||||||
|
if (!linkedRow || !linkedTable) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// need to handle an edge case where relationship just wasn't found
|
||||||
|
const value = linkedRow[linkedTable.primaryDisplay] || linkedRow._id
|
||||||
|
if (value) {
|
||||||
|
row[link.fieldName].push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const CouchDB = require("../index")
|
const CouchDB = require("../index")
|
||||||
const Sentry = require("@sentry/node")
|
const Sentry = require("@sentry/node")
|
||||||
const { ViewNames, getQueryIndex } = require("../utils")
|
const { ViewNames, getQueryIndex } = require("../utils")
|
||||||
|
const { FieldTypes } = require("../../constants")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only needed so that boolean parameters are being used for includeDocs
|
* Only needed so that boolean parameters are being used for includeDocs
|
||||||
|
@ -120,3 +121,35 @@ exports.getUniqueByProp = (array, prop) => {
|
||||||
return arr.map(mapObj => mapObj[prop]).indexOf(obj[prop]) === pos
|
return arr.map(mapObj => mapObj[prop]).indexOf(obj[prop]) === pos
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getLinkedTableIDs = table => {
|
||||||
|
return Object.values(table.schema)
|
||||||
|
.filter(column => column.type === FieldTypes.LINK)
|
||||||
|
.map(column => column.tableId)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getLinkedTable = async (db, id, tables) => {
|
||||||
|
let linkedTable = tables.find(table => table._id === id)
|
||||||
|
if (linkedTable) {
|
||||||
|
return linkedTable
|
||||||
|
}
|
||||||
|
linkedTable = await db.get(id)
|
||||||
|
if (linkedTable) {
|
||||||
|
tables.push(linkedTable)
|
||||||
|
}
|
||||||
|
return linkedTable
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getRelatedTableForField = (table, fieldName) => {
|
||||||
|
// look to see if its on the table, straight in the schema
|
||||||
|
const field = table.schema[fieldName]
|
||||||
|
if (field != null) {
|
||||||
|
return field.tableId
|
||||||
|
}
|
||||||
|
for (let column of Object.values(table.schema)) {
|
||||||
|
if (column.type === FieldTypes.LINK && column.fieldName === fieldName) {
|
||||||
|
return column.tableId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
|
@ -277,3 +277,13 @@ exports.getQueryParams = (datasourceId = null, otherProps = {}) => {
|
||||||
otherProps
|
otherProps
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This can be used with the db.allDocs to get a list of IDs
|
||||||
|
*/
|
||||||
|
exports.getMultiIDParams = ids => {
|
||||||
|
return {
|
||||||
|
keys: ids,
|
||||||
|
include_docs: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,4 +9,6 @@ exports.FIELD_TYPES = {
|
||||||
NUMBER: "number",
|
NUMBER: "number",
|
||||||
PASSWORD: "password",
|
PASSWORD: "password",
|
||||||
LIST: "list",
|
LIST: "list",
|
||||||
|
OBJECT: "object",
|
||||||
|
JSON: "json",
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
||||||
|
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs: "https://airtable.com/api",
|
docs: "https://airtable.com/api",
|
||||||
|
description:
|
||||||
|
"Airtable is a spreadsheet-database hybrid, with the features of a database but applied to a spreadsheet.",
|
||||||
|
friendlyName: "Airtable",
|
||||||
datasource: {
|
datasource: {
|
||||||
apiKey: {
|
apiKey: {
|
||||||
type: FIELD_TYPES.STRING,
|
type: FIELD_TYPES.STRING,
|
||||||
|
@ -50,7 +53,7 @@ const SCHEMA = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
type: FIELD_TYPES.JSON,
|
type: QUERY_TYPES.JSON,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
||||||
|
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs: "https://github.com/arangodb/arangojs",
|
docs: "https://github.com/arangodb/arangojs",
|
||||||
|
friendlyName: "ArangoDB",
|
||||||
|
description:
|
||||||
|
"ArangoDB is a scalable open-source multi-model database natively supporting graph, document and search. All supported data models & access patterns can be combined in queries allowing for maximal flexibility. ",
|
||||||
datasource: {
|
datasource: {
|
||||||
url: {
|
url: {
|
||||||
type: FIELD_TYPES.STRING,
|
type: FIELD_TYPES.STRING,
|
||||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
||||||
|
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs: "https://docs.couchdb.org/en/stable/",
|
docs: "https://docs.couchdb.org/en/stable/",
|
||||||
|
friendlyName: "CouchDB",
|
||||||
|
description:
|
||||||
|
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.",
|
||||||
datasource: {
|
datasource: {
|
||||||
url: {
|
url: {
|
||||||
type: FIELD_TYPES.STRING,
|
type: FIELD_TYPES.STRING,
|
||||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
||||||
|
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs: "https://github.com/dabit3/dynamodb-documentclient-cheat-sheet",
|
docs: "https://github.com/dabit3/dynamodb-documentclient-cheat-sheet",
|
||||||
|
description:
|
||||||
|
"Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale.",
|
||||||
|
friendlyName: "DynamoDB",
|
||||||
datasource: {
|
datasource: {
|
||||||
region: {
|
region: {
|
||||||
type: FIELD_TYPES.STRING,
|
type: FIELD_TYPES.STRING,
|
||||||
|
|
|
@ -4,6 +4,9 @@ const { QUERY_TYPES, FIELD_TYPES } = require("./Integration")
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs:
|
docs:
|
||||||
"https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html",
|
"https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html",
|
||||||
|
description:
|
||||||
|
"Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.",
|
||||||
|
friendlyName: "ElasticSearch",
|
||||||
datasource: {
|
datasource: {
|
||||||
url: {
|
url: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
|
@ -8,6 +8,7 @@ const s3 = require("./s3")
|
||||||
const airtable = require("./airtable")
|
const airtable = require("./airtable")
|
||||||
const mysql = require("./mysql")
|
const mysql = require("./mysql")
|
||||||
const arangodb = require("./arangodb")
|
const arangodb = require("./arangodb")
|
||||||
|
const rest = require("./rest")
|
||||||
|
|
||||||
const DEFINITIONS = {
|
const DEFINITIONS = {
|
||||||
POSTGRES: postgres.schema,
|
POSTGRES: postgres.schema,
|
||||||
|
@ -20,6 +21,7 @@ const DEFINITIONS = {
|
||||||
AIRTABLE: airtable.schema,
|
AIRTABLE: airtable.schema,
|
||||||
MYSQL: mysql.schema,
|
MYSQL: mysql.schema,
|
||||||
ARANGODB: arangodb.schema,
|
ARANGODB: arangodb.schema,
|
||||||
|
REST: rest.schema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const INTEGRATIONS = {
|
const INTEGRATIONS = {
|
||||||
|
@ -33,6 +35,7 @@ const INTEGRATIONS = {
|
||||||
AIRTABLE: airtable.integration,
|
AIRTABLE: airtable.integration,
|
||||||
MYSQL: mysql.integration,
|
MYSQL: mysql.integration,
|
||||||
ARANGODB: arangodb.integration,
|
ARANGODB: arangodb.integration,
|
||||||
|
REST: rest.integration,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES } = require("./Integration")
|
||||||
|
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs: "https://github.com/tediousjs/node-mssql",
|
docs: "https://github.com/tediousjs/node-mssql",
|
||||||
|
description:
|
||||||
|
"Microsoft SQL Server is a relational database management system developed by Microsoft. ",
|
||||||
|
friendlyName: "MS SQL Server",
|
||||||
datasource: {
|
datasource: {
|
||||||
user: {
|
user: {
|
||||||
type: FIELD_TYPES.STRING,
|
type: FIELD_TYPES.STRING,
|
||||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
||||||
|
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs: "https://github.com/mongodb/node-mongodb-native",
|
docs: "https://github.com/mongodb/node-mongodb-native",
|
||||||
|
friendlyName: "MongoDB",
|
||||||
|
description:
|
||||||
|
"MongoDB is a general purpose, document-based, distributed database built for modern application developers and for the cloud era.",
|
||||||
datasource: {
|
datasource: {
|
||||||
connectionString: {
|
connectionString: {
|
||||||
type: FIELD_TYPES.STRING,
|
type: FIELD_TYPES.STRING,
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
const mysql = require("mysql")
|
const mysql = require("mysql")
|
||||||
const { FIELD_TYPES } = require("./Integration")
|
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
||||||
|
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs: "https://github.com/mysqljs/mysql",
|
docs: "https://github.com/mysqljs/mysql",
|
||||||
|
friendlyName: "MySQL",
|
||||||
|
description:
|
||||||
|
"MySQL Database Service is a fully managed database service to deploy cloud-native applications. ",
|
||||||
datasource: {
|
datasource: {
|
||||||
host: {
|
host: {
|
||||||
type: FIELD_TYPES.STRING,
|
type: FIELD_TYPES.STRING,
|
||||||
|
@ -31,16 +34,16 @@ const SCHEMA = {
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
create: {
|
create: {
|
||||||
type: "sql",
|
type: QUERY_TYPES.SQL,
|
||||||
},
|
},
|
||||||
read: {
|
read: {
|
||||||
type: "sql",
|
type: QUERY_TYPES.SQL,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
type: "sql",
|
type: QUERY_TYPES.SQL,
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
type: "sql",
|
type: QUERY_TYPES.SQL,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@ const { Client } = require("pg")
|
||||||
|
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs: "https://node-postgres.com",
|
docs: "https://node-postgres.com",
|
||||||
|
friendlyName: "PostgreSQL",
|
||||||
|
description:
|
||||||
|
"PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.",
|
||||||
datasource: {
|
datasource: {
|
||||||
host: {
|
host: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
const fetch = require("node-fetch")
|
||||||
|
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
||||||
|
|
||||||
|
const SCHEMA = {
|
||||||
|
docs: "https://github.com/node-fetch/node-fetch",
|
||||||
|
description:
|
||||||
|
"Representational state transfer (REST) is a de-facto standard for a software architecture for interactive applications that typically use multiple Web services. ",
|
||||||
|
friendlyName: "REST API",
|
||||||
|
datasource: {
|
||||||
|
url: {
|
||||||
|
type: FIELD_TYPES.STRING,
|
||||||
|
default: "localhost",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
defaultHeaders: {
|
||||||
|
type: FIELD_TYPES.OBJECT,
|
||||||
|
required: false,
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
create: {
|
||||||
|
displayName: "POST",
|
||||||
|
type: QUERY_TYPES.FIELDS,
|
||||||
|
urlDisplay: true,
|
||||||
|
fields: {
|
||||||
|
path: {
|
||||||
|
type: FIELD_TYPES.STRING,
|
||||||
|
},
|
||||||
|
queryString: {
|
||||||
|
type: FIELD_TYPES.STRING,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: FIELD_TYPES.OBJECT,
|
||||||
|
},
|
||||||
|
requestBody: {
|
||||||
|
type: FIELD_TYPES.JSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
displayName: "GET",
|
||||||
|
type: QUERY_TYPES.FIELDS,
|
||||||
|
urlDisplay: true,
|
||||||
|
fields: {
|
||||||
|
path: {
|
||||||
|
type: FIELD_TYPES.STRING,
|
||||||
|
},
|
||||||
|
queryString: {
|
||||||
|
type: FIELD_TYPES.STRING,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: FIELD_TYPES.OBJECT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
displayName: "PUT",
|
||||||
|
type: QUERY_TYPES.FIELDS,
|
||||||
|
urlDisplay: true,
|
||||||
|
fields: {
|
||||||
|
path: {
|
||||||
|
type: FIELD_TYPES.STRING,
|
||||||
|
},
|
||||||
|
queryString: {
|
||||||
|
type: FIELD_TYPES.STRING,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: FIELD_TYPES.OBJECT,
|
||||||
|
},
|
||||||
|
requestBody: {
|
||||||
|
type: FIELD_TYPES.JSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
displayName: "DELETE",
|
||||||
|
type: QUERY_TYPES.FIELDS,
|
||||||
|
urlDisplay: true,
|
||||||
|
fields: {
|
||||||
|
path: {
|
||||||
|
type: FIELD_TYPES.STRING,
|
||||||
|
},
|
||||||
|
queryString: {
|
||||||
|
type: FIELD_TYPES.STRING,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: FIELD_TYPES.OBJECT,
|
||||||
|
},
|
||||||
|
requestBody: {
|
||||||
|
type: FIELD_TYPES.JSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestIntegration {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseResponse(response) {
|
||||||
|
switch (this.headers.Accept) {
|
||||||
|
case "application/json":
|
||||||
|
return await response.json()
|
||||||
|
case "text/html":
|
||||||
|
return await response.text()
|
||||||
|
default:
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create({ path, queryString, headers = {}, json }) {
|
||||||
|
this.headers = {
|
||||||
|
...this.config.defaultHeaders,
|
||||||
|
...headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(this.config.url + path + queryString, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify(json),
|
||||||
|
})
|
||||||
|
|
||||||
|
return await this.parseResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async read({ path, queryString, headers = {} }) {
|
||||||
|
this.headers = {
|
||||||
|
...this.config.defaultHeaders,
|
||||||
|
...headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(this.config.url + path + queryString, {
|
||||||
|
headers: this.headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
return await this.parseResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async update({ path, queryString, headers = {}, json }) {
|
||||||
|
this.headers = {
|
||||||
|
...this.config.defaultHeaders,
|
||||||
|
...headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(this.config.url + path + queryString, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify(json),
|
||||||
|
})
|
||||||
|
|
||||||
|
return await this.parseResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete({ path, queryString, headers = {} }) {
|
||||||
|
this.headers = {
|
||||||
|
...this.config.defaultHeaders,
|
||||||
|
...headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(this.config.url + path + queryString, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: this.headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
return await this.parseResponse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
schema: SCHEMA,
|
||||||
|
integration: RestIntegration,
|
||||||
|
}
|
|
@ -2,6 +2,9 @@ const AWS = require("aws-sdk")
|
||||||
|
|
||||||
const SCHEMA = {
|
const SCHEMA = {
|
||||||
docs: "https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
|
docs: "https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
|
||||||
|
description:
|
||||||
|
"Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.",
|
||||||
|
friendlyName: "Amazon S3",
|
||||||
datasource: {
|
datasource: {
|
||||||
region: {
|
region: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
|
@ -3,7 +3,6 @@ const { OBJ_STORE_DIRECTORY } = require("../constants")
|
||||||
const linkRows = require("../db/linkedRows")
|
const linkRows = require("../db/linkedRows")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { FieldTypes, AutoFieldSubTypes } = require("../constants")
|
const { FieldTypes, AutoFieldSubTypes } = require("../constants")
|
||||||
const CouchDB = require("../db")
|
|
||||||
|
|
||||||
const BASE_AUTO_ID = 1
|
const BASE_AUTO_ID = 1
|
||||||
|
|
||||||
|
@ -71,14 +70,13 @@ const TYPE_TRANSFORM_MAP = {
|
||||||
* @param {Object} user The user to be used for an appId as well as the createdBy and createdAt fields.
|
* @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} 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.
|
* @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
|
* @returns {{row: Object, table: Object}} The updated row and table, the table may need to be updated
|
||||||
* for automatic ID purposes.
|
* for automatic ID purposes.
|
||||||
*/
|
*/
|
||||||
async function processAutoColumn(user, table, row) {
|
function processAutoColumn(user, table, row) {
|
||||||
let now = new Date().toISOString()
|
let now = new Date().toISOString()
|
||||||
// if a row doesn't have a revision then it doesn't exist yet
|
// if a row doesn't have a revision then it doesn't exist yet
|
||||||
const creating = !row._rev
|
const creating = !row._rev
|
||||||
let tableUpdated = false
|
|
||||||
for (let [key, schema] of Object.entries(table.schema)) {
|
for (let [key, schema] of Object.entries(table.schema)) {
|
||||||
if (!schema.autocolumn) {
|
if (!schema.autocolumn) {
|
||||||
continue
|
continue
|
||||||
|
@ -104,17 +102,10 @@ async function processAutoColumn(user, table, row) {
|
||||||
if (creating) {
|
if (creating) {
|
||||||
schema.lastID = !schema.lastID ? BASE_AUTO_ID : schema.lastID + 1
|
schema.lastID = !schema.lastID ? BASE_AUTO_ID : schema.lastID + 1
|
||||||
row[key] = schema.lastID
|
row[key] = schema.lastID
|
||||||
tableUpdated = true
|
|
||||||
}
|
}
|
||||||
break
|
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 }
|
return { table, row }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +134,7 @@ exports.coerce = (row, type) => {
|
||||||
* @param {object} table the table which the row is being saved to.
|
* @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.
|
* @returns {object} the row which has been prepared to be written to the DB.
|
||||||
*/
|
*/
|
||||||
exports.inputProcessing = async (user, table, row) => {
|
exports.inputProcessing = (user, table, row) => {
|
||||||
let clonedRow = cloneDeep(row)
|
let clonedRow = cloneDeep(row)
|
||||||
for (let [key, value] of Object.entries(clonedRow)) {
|
for (let [key, value] of Object.entries(clonedRow)) {
|
||||||
const field = table.schema[key]
|
const field = table.schema[key]
|
||||||
|
@ -166,8 +157,17 @@ exports.inputProcessing = async (user, table, row) => {
|
||||||
* @returns {object[]} the enriched rows will be returned.
|
* @returns {object[]} the enriched rows will be returned.
|
||||||
*/
|
*/
|
||||||
exports.outputProcessing = async (appId, table, rows) => {
|
exports.outputProcessing = async (appId, table, rows) => {
|
||||||
|
let wasArray = true
|
||||||
|
if (!(rows instanceof Array)) {
|
||||||
|
rows = [rows]
|
||||||
|
wasArray = false
|
||||||
|
}
|
||||||
// attach any linked row information
|
// attach any linked row information
|
||||||
const outputRows = await linkRows.attachLinkInfo(appId, rows)
|
const outputRows = await linkRows.attachLinkedPrimaryDisplay(
|
||||||
|
appId,
|
||||||
|
table,
|
||||||
|
rows
|
||||||
|
)
|
||||||
// update the attachments URL depending on hosting
|
// update the attachments URL depending on hosting
|
||||||
if (env.CLOUD && env.SELF_HOSTED) {
|
if (env.CLOUD && env.SELF_HOSTED) {
|
||||||
for (let [property, column] of Object.entries(table.schema)) {
|
for (let [property, column] of Object.entries(table.schema)) {
|
||||||
|
@ -184,5 +184,5 @@ exports.outputProcessing = async (appId, table, rows) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return outputRows
|
return wasArray ? outputRows : outputRows[0]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1391,6 +1391,11 @@
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"key": "label"
|
"key": "label"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Placeholder",
|
||||||
|
"key": "placeholder"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Disabled",
|
"label": "Disabled",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
export let field
|
export let field
|
||||||
export let label
|
export let label
|
||||||
|
export let placeholder
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
|
@ -16,8 +17,26 @@
|
||||||
// Picker state
|
// Picker state
|
||||||
let options = []
|
let options = []
|
||||||
let tableDefinition
|
let tableDefinition
|
||||||
|
let fieldText = ""
|
||||||
|
|
||||||
$: fieldText = `${$fieldState?.value?.length ?? 0} selected rows`
|
const setFieldText = (value) => {
|
||||||
|
if (fieldSchema?.relationshipType === 'one-to-many') {
|
||||||
|
if (value?.length && options?.length) {
|
||||||
|
const row = options.find(row => row._id === value[0])
|
||||||
|
return row.name
|
||||||
|
} else {
|
||||||
|
return placeholder || 'Choose an option'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (value?.length) {
|
||||||
|
return `${value?.length ?? 0} selected rows`
|
||||||
|
} else {
|
||||||
|
return placeholder || 'Choose some options'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: options, fieldText = setFieldText($fieldState?.value)
|
||||||
$: valueLookupMap = getValueLookupMap($fieldState?.value)
|
$: valueLookupMap = getValueLookupMap($fieldState?.value)
|
||||||
$: isOptionSelected = option => valueLookupMap[option] === true
|
$: isOptionSelected = option => valueLookupMap[option] === true
|
||||||
$: linkedTableId = fieldSchema?.tableId
|
$: linkedTableId = fieldSchema?.tableId
|
||||||
|
@ -55,12 +74,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleOption = option => {
|
const toggleOption = option => {
|
||||||
|
if (fieldSchema.type === 'one-to-many') {
|
||||||
|
fieldApi.setValue([option])
|
||||||
|
} else {
|
||||||
if ($fieldState.value.includes(option)) {
|
if ($fieldState.value.includes(option)) {
|
||||||
fieldApi.setValue($fieldState.value.filter(x => x !== option))
|
fieldApi.setValue($fieldState.value.filter(x => x !== option))
|
||||||
} else {
|
} else {
|
||||||
fieldApi.setValue([...$fieldState.value, option])
|
fieldApi.setValue([...$fieldState.value, option])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
export let columnName
|
export let columnName
|
||||||
export let row
|
export let row
|
||||||
|
|
||||||
$: count =
|
$: items = row?.[columnName] || []
|
||||||
row && columnName && Array.isArray(row[columnName])
|
|
||||||
? row[columnName].length
|
|
||||||
: 0
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">{count} related row(s)</div>
|
<div class="container">
|
||||||
|
{#each items as item}
|
||||||
|
<div class="item">{item}</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
|
@ -19,4 +20,13 @@
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: var(--spacing-xs) var(--spacing-s);
|
||||||
|
border: 1px solid var(--grey-5);
|
||||||
|
color: var(--grey-7);
|
||||||
|
line-height: normal;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue