commit
ceeb968531
|
@ -28,8 +28,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn lint
|
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
|
- run: yarn lint
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -37,7 +37,8 @@
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
"lint:fix:eslint": "eslint --fix packages",
|
"lint:fix:eslint": "eslint --fix packages",
|
||||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,svelte}\"",
|
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,svelte}\"",
|
||||||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
"lint:fix:ts": "lerna run lint:fix",
|
||||||
|
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||||
"test:e2e": "lerna run cy:test",
|
"test:e2e": "lerna run cy:test",
|
||||||
"test:e2e:ci": "lerna run cy:ci",
|
"test:e2e:ci": "lerna run cy:ci",
|
||||||
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -",
|
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -",
|
||||||
|
|
|
@ -18,6 +18,8 @@ function connectionError(timeout, err) {
|
||||||
if (CLOSED) {
|
if (CLOSED) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
CLIENT.end()
|
||||||
|
CLOSED = true
|
||||||
// always clear this on error
|
// always clear this on error
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
CONNECTED = false
|
CONNECTED = false
|
||||||
|
@ -41,7 +43,7 @@ function init() {
|
||||||
// start the timer - only allowed 5 seconds to connect
|
// start the timer - only allowed 5 seconds to connect
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
if (!CONNECTED) {
|
if (!CONNECTED) {
|
||||||
connectionError(timeout)
|
connectionError(timeout, "Did not successfully connect in timeout")
|
||||||
}
|
}
|
||||||
}, STARTUP_TIMEOUT_MS)
|
}, STARTUP_TIMEOUT_MS)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,239 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
|
import clickOutside from "../Actions/click_outside"
|
||||||
|
import { fly } from "svelte/transition"
|
||||||
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
import Input from "../Form/Input.svelte"
|
||||||
|
import { capitalise } from "../utils/helpers"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let size = "M"
|
||||||
|
|
||||||
|
let open = false
|
||||||
|
|
||||||
|
$: color = value || "transparent"
|
||||||
|
$: customValue = getCustomValue(value)
|
||||||
|
$: checkColor = getCheckColor(value)
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const categories = [
|
||||||
|
{
|
||||||
|
label: "Grays",
|
||||||
|
colors: [
|
||||||
|
"white",
|
||||||
|
"gray-100",
|
||||||
|
"gray-200",
|
||||||
|
"gray-300",
|
||||||
|
"gray-400",
|
||||||
|
"gray-500",
|
||||||
|
"gray-600",
|
||||||
|
"gray-700",
|
||||||
|
"gray-800",
|
||||||
|
"gray-900",
|
||||||
|
"black",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Colors",
|
||||||
|
colors: [
|
||||||
|
"red-400",
|
||||||
|
"orange-400",
|
||||||
|
"yellow-400",
|
||||||
|
"green-400",
|
||||||
|
"seafoam-400",
|
||||||
|
"blue-400",
|
||||||
|
"indigo-400",
|
||||||
|
"magenta-400",
|
||||||
|
|
||||||
|
"red-500",
|
||||||
|
"orange-500",
|
||||||
|
"yellow-500",
|
||||||
|
"green-500",
|
||||||
|
"seafoam-500",
|
||||||
|
"blue-500",
|
||||||
|
"indigo-500",
|
||||||
|
"magenta-500",
|
||||||
|
|
||||||
|
"red-600",
|
||||||
|
"orange-600",
|
||||||
|
"yellow-600",
|
||||||
|
"green-600",
|
||||||
|
"seafoam-600",
|
||||||
|
"blue-600",
|
||||||
|
"indigo-600",
|
||||||
|
"magenta-600",
|
||||||
|
|
||||||
|
"red-700",
|
||||||
|
"orange-700",
|
||||||
|
"yellow-700",
|
||||||
|
"green-700",
|
||||||
|
"seafoam-700",
|
||||||
|
"blue-700",
|
||||||
|
"indigo-700",
|
||||||
|
"magenta-700",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const onChange = value => {
|
||||||
|
dispatch("change", value)
|
||||||
|
open = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCustomValue = value => {
|
||||||
|
if (!value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
let found = false
|
||||||
|
const comparisonValue = value.substring(35, value.length - 1)
|
||||||
|
for (let category of categories) {
|
||||||
|
found = category.colors.includes(comparisonValue)
|
||||||
|
if (found) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found ? null : value
|
||||||
|
}
|
||||||
|
|
||||||
|
const prettyPrint = color => {
|
||||||
|
return capitalise(color.split("-").join(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCheckColor = value => {
|
||||||
|
return /^.*(white|(gray-(50|75|100|200|300|400|500)))\)$/.test(value)
|
||||||
|
? "black"
|
||||||
|
: "white"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div
|
||||||
|
class="preview size--{size || 'M'}"
|
||||||
|
style="background: {color};"
|
||||||
|
on:click={() => (open = true)}
|
||||||
|
/>
|
||||||
|
{#if open}
|
||||||
|
<div
|
||||||
|
use:clickOutside={() => (open = false)}
|
||||||
|
transition:fly={{ y: -20, duration: 200 }}
|
||||||
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
|
>
|
||||||
|
{#each categories as category}
|
||||||
|
<div class="category">
|
||||||
|
<div class="heading">{category.label}</div>
|
||||||
|
<div class="colors">
|
||||||
|
{#each category.colors as color}
|
||||||
|
<div
|
||||||
|
on:click={() => {
|
||||||
|
onChange(`var(--spectrum-global-color-static-${color})`)
|
||||||
|
}}
|
||||||
|
class="color"
|
||||||
|
style="background: var(--spectrum-global-color-static-{color}); color: {checkColor};"
|
||||||
|
title={prettyPrint(color)}
|
||||||
|
>
|
||||||
|
{#if value === `var(--spectrum-global-color-static-${color})`}
|
||||||
|
<Icon name="Checkmark" size="S" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="category category--custom">
|
||||||
|
<div class="heading">Custom</div>
|
||||||
|
<div class="custom">
|
||||||
|
<Input
|
||||||
|
updateOnChange={false}
|
||||||
|
quiet
|
||||||
|
placeholder="Hex, RGB, HSL..."
|
||||||
|
value={customValue}
|
||||||
|
on:change
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
size="S"
|
||||||
|
name="Close"
|
||||||
|
hoverable
|
||||||
|
on:click={() => onChange(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 100%;
|
||||||
|
transition: border-color 130ms ease-in-out;
|
||||||
|
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
.preview:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
.size--S {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.size--M {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.size--L {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
.spectrum-Popover {
|
||||||
|
width: 210px;
|
||||||
|
z-index: 999;
|
||||||
|
top: 100%;
|
||||||
|
padding: var(--spacing-l) var(--spacing-xl);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.colors {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
.heading {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.14px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-transform: uppercase;
|
||||||
|
grid-column: 1 / 5;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.color {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.color:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
.custom {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
.category--custom .heading {
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,13 +2,15 @@
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let name
|
||||||
|
export let show = false
|
||||||
|
export let collapsible = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let thin = false
|
|
||||||
export let name,
|
|
||||||
show = false
|
|
||||||
|
|
||||||
const onHeaderClick = () => {
|
const onHeaderClick = () => {
|
||||||
|
if (!collapsible) {
|
||||||
|
return
|
||||||
|
}
|
||||||
show = !show
|
show = !show
|
||||||
if (show) {
|
if (show) {
|
||||||
dispatch("open")
|
dispatch("open")
|
||||||
|
@ -16,14 +18,14 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="property-group-container" class:thin>
|
<div class="property-group-container">
|
||||||
<div class="property-group-name" on:click={onHeaderClick}>
|
<div class="property-group-name" on:click={onHeaderClick}>
|
||||||
<div class:thin class="name">{name}</div>
|
<div class="name">{name}</div>
|
||||||
<div class="icon">
|
{#if collapsible}
|
||||||
<Icon size="S" name={show ? "Remove" : "Add"} />
|
<Icon size="S" name={show ? "Remove" : "Add"} />
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="property-panel" class:show>
|
<div class="property-panel" class:show={show || !collapsible}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,10 +34,9 @@
|
||||||
.property-group-container {
|
.property-group-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: auto;
|
justify-content: flex-start;
|
||||||
justify-content: center;
|
align-items: stretch;
|
||||||
border-radius: var(--border-radius-m);
|
border-bottom: var(--border-light);
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-group-name {
|
.property-group-name {
|
||||||
|
@ -45,42 +46,38 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: var(--spacing-m) var(--spacing-xl);
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
transition: color 130ms ease-in-out;
|
||||||
|
}
|
||||||
|
.property-group-name:hover {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 14px;
|
font-size: var(--font-size-s);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.14px;
|
letter-spacing: 0.14px;
|
||||||
color: var(--ink);
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
text-transform: capitalize;
|
text-transform: uppercase;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.name.thin {
|
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
flex: 0 0 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-panel {
|
.property-panel {
|
||||||
/* height: 0px;
|
|
||||||
overflow: hidden; */
|
|
||||||
display: none;
|
display: none;
|
||||||
|
padding: var(--spacing-s) var(--spacing-xl) var(--spacing-xl)
|
||||||
|
var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.show {
|
.show {
|
||||||
/* overflow: auto;
|
|
||||||
height: auto; */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
justify-content: flex-start;
|
||||||
margin-top: var(--spacing-m);
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
export let open = false
|
export let open = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
|
export let autoWidth = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
|
@ -41,7 +42,11 @@
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
on:mousedown={onClick}
|
on:mousedown={onClick}
|
||||||
>
|
>
|
||||||
<span class="spectrum-Picker-label" class:is-placeholder={isPlaceholder}>
|
<span
|
||||||
|
class="spectrum-Picker-label"
|
||||||
|
class:is-placeholder={isPlaceholder}
|
||||||
|
class:auto-width={autoWidth}
|
||||||
|
>
|
||||||
{fieldText}
|
{fieldText}
|
||||||
</span>
|
</span>
|
||||||
{#if error}
|
{#if error}
|
||||||
|
@ -67,11 +72,12 @@
|
||||||
use:clickOutside={() => (open = false)}
|
use:clickOutside={() => (open = false)}
|
||||||
transition:fly={{ y: -20, duration: 200 }}
|
transition:fly={{ y: -20, duration: 200 }}
|
||||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
|
class:auto-width={autoWidth}
|
||||||
>
|
>
|
||||||
<ul class="spectrum-Menu" role="listbox">
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
{#if placeholderOption}
|
{#if placeholderOption}
|
||||||
<li
|
<li
|
||||||
class="spectrum-Menu-item"
|
class="spectrum-Menu-item placeholder"
|
||||||
class:is-selected={isPlaceholder}
|
class:is-selected={isPlaceholder}
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected="true"
|
aria-selected="true"
|
||||||
|
@ -118,17 +124,28 @@
|
||||||
<style>
|
<style>
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
max-height: 240px;
|
max-height: 240px;
|
||||||
width: 100%;
|
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
}
|
}
|
||||||
|
.spectrum-Popover:not(.auto-width) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.spectrum-Popover.auto-width :global(.spectrum-Menu-itemLabel) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.spectrum-Picker {
|
.spectrum-Picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.spectrum-Picker-label {
|
.spectrum-Picker-label:not(.auto-width) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
.placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.spectrum-Picker-label.auto-width.is-placeholder {
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = option => option
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
|
export let autoWidth = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let open = false
|
let open = false
|
||||||
|
@ -51,6 +52,7 @@
|
||||||
{readonly}
|
{readonly}
|
||||||
{fieldText}
|
{fieldText}
|
||||||
{options}
|
{options}
|
||||||
|
{autoWidth}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
isPlaceholder={value == null || value === ""}
|
isPlaceholder={value == null || value === ""}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let id = null
|
export let id = null
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
|
export let quiet = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let focus = false
|
let focus = false
|
||||||
|
@ -59,6 +60,7 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="spectrum-Textfield"
|
class="spectrum-Textfield"
|
||||||
|
class:spectrum-Textfield--quiet={quiet}
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
class:is-focused={focus}
|
class:is-focused={focus}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let error = null
|
export let error = null
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
|
export let quiet = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
{value}
|
{value}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{type}
|
{type}
|
||||||
|
{quiet}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
on:input
|
on:input
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
export let getOptionLabel = option => extractProperty(option, "label")
|
export let getOptionLabel = option => extractProperty(option, "label")
|
||||||
export let getOptionValue = option => extractProperty(option, "value")
|
export let getOptionValue = option => extractProperty(option, "value")
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
|
export let autoWidth = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
{value}
|
{value}
|
||||||
{options}
|
{options}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
|
{autoWidth}
|
||||||
{getOptionLabel}
|
{getOptionLabel}
|
||||||
{getOptionValue}
|
{getOptionValue}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
export let selected
|
export let selected
|
||||||
export let vertical = false
|
export let vertical = false
|
||||||
|
export let noPadding = false
|
||||||
|
|
||||||
let _id = id()
|
let _id = id()
|
||||||
const tab = writable({ title: selected, id: _id })
|
const tab = writable({ title: selected, id: _id })
|
||||||
setContext("tab", tab)
|
setContext("tab", tab)
|
||||||
|
@ -63,14 +65,17 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="spectrum-Tabs-content spectrum-Tabs-content-{_id}" />
|
<div
|
||||||
|
class="spectrum-Tabs-content spectrum-Tabs-content-{_id}"
|
||||||
|
class:noPadding
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spectrum-Tabs {
|
.spectrum-Tabs {
|
||||||
padding-left: var(--spacing-xl);
|
padding-left: var(--spacing-xl);
|
||||||
padding-right: var(--spacing-xl);
|
padding-right: var(--spacing-xl);
|
||||||
position: relative;
|
position: relative;
|
||||||
border-width: 1px !important;
|
border-bottom: var(--border-light);
|
||||||
}
|
}
|
||||||
.spectrum-Tabs-content {
|
.spectrum-Tabs-content {
|
||||||
margin-top: var(--spectrum-global-dimension-static-size-150);
|
margin-top: var(--spectrum-global-dimension-static-size-150);
|
||||||
|
@ -81,4 +86,7 @@
|
||||||
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
|
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
|
||||||
bottom: 0 !important;
|
bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
.noPadding {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -55,6 +55,7 @@ export { default as Search } from "./Form/Search.svelte"
|
||||||
export { default as Pagination } from "./Pagination/Pagination.svelte"
|
export { default as Pagination } from "./Pagination/Pagination.svelte"
|
||||||
export { default as Badge } from "./Badge/Badge.svelte"
|
export { default as Badge } from "./Badge/Badge.svelte"
|
||||||
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||||
|
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
export { default as Body } from "./Typography/Body.svelte"
|
export { default as Body } from "./Typography/Body.svelte"
|
||||||
|
|
|
@ -4,3 +4,5 @@ export const generateID = () => {
|
||||||
// Starts with a letter so that its a valid DOM ID
|
// Starts with a letter so that its a valid DOM ID
|
||||||
return `A${rand}`
|
return `A${rand}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||||
|
|
|
@ -4,6 +4,8 @@ const path = require("path")
|
||||||
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
||||||
|
|
||||||
// these run on ports we don't normally use so that they can run alongside the
|
// these run on ports we don't normally use so that they can run alongside the
|
||||||
|
const fs = require("fs")
|
||||||
|
|
||||||
// normal development system
|
// normal development system
|
||||||
const WORKER_PORT = "10002"
|
const WORKER_PORT = "10002"
|
||||||
const MAIN_PORT = cypressConfig.env.PORT
|
const MAIN_PORT = cypressConfig.env.PORT
|
||||||
|
@ -27,10 +29,14 @@ process.env.LOG_LEVEL = "error"
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
// require("dotenv").config({ path: resolve(dir, ".env") })
|
// require("dotenv").config({ path: resolve(dir, ".env") })
|
||||||
|
if (!fs.existsSync("../server/dist")) {
|
||||||
|
console.error("Unable to run cypress, need to build server first")
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
// dont make this a variable or top level require
|
// dont make this a variable or top level require
|
||||||
// it will cause environment module to be loaded prematurely
|
// it will cause environment module to be loaded prematurely
|
||||||
const server = require("../../server/src/app")
|
const server = require("../../server/dist/app")
|
||||||
process.env.PORT = WORKER_PORT
|
process.env.PORT = WORKER_PORT
|
||||||
const worker = require("../../worker/src/index")
|
const worker = require("../../worker/src/index")
|
||||||
// reload main port for rest of system
|
// reload main port for rest of system
|
||||||
|
|
|
@ -488,12 +488,12 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
},
|
},
|
||||||
updateStyle: async (type, name, value) => {
|
updateStyle: async (name, value) => {
|
||||||
const selected = get(selectedComponent)
|
const selected = get(selectedComponent)
|
||||||
if (value == null || value === "") {
|
if (value == null || value === "") {
|
||||||
delete selected._styles[type][name]
|
delete selected._styles.normal[name]
|
||||||
} else {
|
} else {
|
||||||
selected._styles[type][name] = value
|
selected._styles.normal[name] = value
|
||||||
}
|
}
|
||||||
await store.actions.preview.saveSelected()
|
await store.actions.preview.saveSelected()
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,6 +54,10 @@ function generateTitleContainer(table) {
|
||||||
.type("h2")
|
.type("h2")
|
||||||
.instanceName("Title")
|
.instanceName("Title")
|
||||||
.text(table.name)
|
.text(table.name)
|
||||||
|
.customProps({
|
||||||
|
size: "M",
|
||||||
|
align: "left",
|
||||||
|
})
|
||||||
|
|
||||||
return new Component("@budibase/standard-components/container")
|
return new Component("@budibase/standard-components/container")
|
||||||
.normalStyle({
|
.normalStyle({
|
||||||
|
|
|
@ -21,6 +21,7 @@ export class Screen extends BaseStructure {
|
||||||
hAlign: "stretch",
|
hAlign: "stretch",
|
||||||
vAlign: "top",
|
vAlign: "top",
|
||||||
size: "grow",
|
size: "grow",
|
||||||
|
gap: "M",
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
route: "",
|
route: "",
|
||||||
|
|
|
@ -25,11 +25,8 @@ export function makeLinkComponent(tableName) {
|
||||||
.customProps({
|
.customProps({
|
||||||
url: `/${tableName.toLowerCase()}`,
|
url: `/${tableName.toLowerCase()}`,
|
||||||
openInNewTab: false,
|
openInNewTab: false,
|
||||||
color: "",
|
size: "S",
|
||||||
hoverColor: "",
|
align: "left",
|
||||||
underline: false,
|
|
||||||
fontSize: "",
|
|
||||||
fontFamily: "initial",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +59,10 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
||||||
.customStyle(spectrumColor(700))
|
.customStyle(spectrumColor(700))
|
||||||
.text(">")
|
.text(">")
|
||||||
.instanceName("Arrow")
|
.instanceName("Arrow")
|
||||||
|
.customProps({
|
||||||
|
size: "S",
|
||||||
|
align: "left",
|
||||||
|
})
|
||||||
|
|
||||||
const textStyling = {
|
const textStyling = {
|
||||||
color: "#000000",
|
color: "#000000",
|
||||||
|
@ -77,6 +78,10 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
||||||
.customStyle(spectrumColor(700))
|
.customStyle(spectrumColor(700))
|
||||||
.text(text)
|
.text(text)
|
||||||
.instanceName("Identifier")
|
.instanceName("Identifier")
|
||||||
|
.customProps({
|
||||||
|
size: "S",
|
||||||
|
align: "left",
|
||||||
|
})
|
||||||
|
|
||||||
return new Component("@budibase/standard-components/container")
|
return new Component("@budibase/standard-components/container")
|
||||||
.normalStyle({
|
.normalStyle({
|
||||||
|
@ -148,6 +153,10 @@ export function makeTitleContainer(title) {
|
||||||
.type("h2")
|
.type("h2")
|
||||||
.instanceName("Title")
|
.instanceName("Title")
|
||||||
.text(title)
|
.text(title)
|
||||||
|
.customProps({
|
||||||
|
size: "M",
|
||||||
|
align: "left",
|
||||||
|
})
|
||||||
|
|
||||||
return new Component("@budibase/standard-components/container")
|
return new Component("@budibase/standard-components/container")
|
||||||
.normalStyle({
|
.normalStyle({
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
size="L"
|
size="L"
|
||||||
confirmText="Create"
|
confirmText="Create"
|
||||||
onConfirm={saveDatasource}
|
onConfirm={saveDatasource}
|
||||||
disabled={error || !name}
|
disabled={error || !name || !integration?.type}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
data-cy="datasource-name-input"
|
data-cy="datasource-name-input"
|
||||||
|
|
|
@ -4,10 +4,13 @@
|
||||||
import iframeTemplate from "./iframeTemplate"
|
import iframeTemplate from "./iframeTemplate"
|
||||||
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
||||||
import { FrontendTypes } from "constants"
|
import { FrontendTypes } from "constants"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
let iframe
|
let iframe
|
||||||
let layout
|
let layout
|
||||||
let screen
|
let screen
|
||||||
|
let confirmDeleteDialog
|
||||||
|
let idToDelete
|
||||||
|
|
||||||
// Create screen slot placeholder for use when a page is selected rather
|
// Create screen slot placeholder for use when a page is selected rather
|
||||||
// than a screen
|
// than a screen
|
||||||
|
@ -73,15 +76,26 @@
|
||||||
// Add listener for events sent by cliebt library in preview
|
// Add listener for events sent by cliebt library in preview
|
||||||
iframe.contentWindow.addEventListener("bb-event", event => {
|
iframe.contentWindow.addEventListener("bb-event", event => {
|
||||||
const { type, data } = event.detail
|
const { type, data } = event.detail
|
||||||
if (type === "select-component") {
|
if (type === "select-component" && data.id) {
|
||||||
store.actions.components.select({ _id: data.id })
|
store.actions.components.select({ _id: data.id })
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
store.actions.components.updateProp(data.prop, data.value)
|
store.actions.components.updateProp(data.prop, data.value)
|
||||||
|
} else if (type === "delete-component" && data.id) {
|
||||||
|
idToDelete = data.id
|
||||||
|
confirmDeleteDialog.show()
|
||||||
} else {
|
} else {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const deleteComponent = () => {
|
||||||
|
store.actions.components.delete({ _id: idToDelete })
|
||||||
|
idToDelete = null
|
||||||
|
}
|
||||||
|
const cancelDeleteComponent = () => {
|
||||||
|
idToDelete = null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="component-container">
|
<div class="component-container">
|
||||||
|
@ -92,6 +106,14 @@
|
||||||
srcdoc={template}
|
srcdoc={template}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
body={`Are you sure you want to delete this component?`}
|
||||||
|
okText="Delete component"
|
||||||
|
onOk={deleteComponent}
|
||||||
|
onCancel={cancelDeleteComponent}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.component-container {
|
.component-container {
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
<script>
|
|
||||||
export let categories = []
|
|
||||||
export let selectedCategory = {}
|
|
||||||
export let onClick = () => {}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="tabs">
|
|
||||||
{#each categories as category}
|
|
||||||
<li
|
|
||||||
data-cy={category.name}
|
|
||||||
on:click={() => onClick(category)}
|
|
||||||
class:active={selectedCategory === category}
|
|
||||||
>
|
|
||||||
{category.name}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
list-style: none;
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
font-weight: 600;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
color: var(--grey-5);
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,11 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { get } from "lodash"
|
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
import { Button, Checkbox, Input, Select } from "@budibase/bbui"
|
import {
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
Checkbox,
|
||||||
import { currentAsset } from "builderStore"
|
Input,
|
||||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
Select,
|
||||||
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
|
DetailSummary,
|
||||||
|
ColorPicker,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
||||||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
||||||
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
||||||
|
@ -20,7 +22,6 @@
|
||||||
import EventsEditor from "./PropertyControls/EventsEditor"
|
import EventsEditor from "./PropertyControls/EventsEditor"
|
||||||
import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte"
|
import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte"
|
||||||
import { IconSelect } from "./PropertyControls/IconSelect"
|
import { IconSelect } from "./PropertyControls/IconSelect"
|
||||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
|
||||||
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
|
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
|
||||||
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
|
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
|
||||||
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
|
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
|
||||||
|
@ -29,13 +30,11 @@
|
||||||
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
|
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
|
||||||
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
|
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
|
||||||
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
|
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
|
||||||
|
import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte"
|
||||||
|
|
||||||
export let componentDefinition = {}
|
export let componentDefinition
|
||||||
export let componentInstance = {}
|
export let componentInstance
|
||||||
export let assetInstance
|
export let assetInstance
|
||||||
export let onChange = () => {}
|
|
||||||
export let onScreenPropChange = () => {}
|
|
||||||
export let showDisplayName = false
|
|
||||||
|
|
||||||
const layoutDefinition = []
|
const layoutDefinition = []
|
||||||
const screenDefinition = [
|
const screenDefinition = [
|
||||||
|
@ -44,12 +43,12 @@
|
||||||
{ key: "routing.roleId", label: "Access", control: RoleSelect },
|
{ key: "routing.roleId", label: "Access", control: RoleSelect },
|
||||||
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
||||||
]
|
]
|
||||||
let confirmResetFieldsDialog
|
|
||||||
|
|
||||||
$: settings = componentDefinition?.settings ?? []
|
$: settings = componentDefinition?.settings ?? []
|
||||||
$: isLayout = assetInstance && assetInstance.favicon
|
$: isLayout = assetInstance && assetInstance.favicon
|
||||||
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
|
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
|
||||||
|
|
||||||
|
const updateProp = store.actions.components.updateProp
|
||||||
const controlMap = {
|
const controlMap = {
|
||||||
text: Input,
|
text: Input,
|
||||||
select: Select,
|
select: Select,
|
||||||
|
@ -91,51 +90,19 @@
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInstanceNameChange = name => {
|
|
||||||
onChange("_instanceName", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetFormFields = () => {
|
|
||||||
const form = findClosestMatchingComponent(
|
|
||||||
$currentAsset.props,
|
|
||||||
componentInstance._id,
|
|
||||||
component => component._component.endsWith("/form")
|
|
||||||
)
|
|
||||||
const dataSource = form?.dataSource
|
|
||||||
const fields = makeDatasourceFormComponents(dataSource)
|
|
||||||
onChange(
|
|
||||||
"_children",
|
|
||||||
fields.map(field => field.json())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="settings-view-container">
|
<DetailSummary name="General" collapsible={false}>
|
||||||
{#if assetInstance}
|
{#if !componentInstance._component.endsWith("/layout")}
|
||||||
{#each assetDefinition as def (`${componentInstance._id}-${def.key}`)}
|
|
||||||
<PropertyControl
|
|
||||||
bindable={false}
|
|
||||||
control={def.control}
|
|
||||||
label={def.label}
|
|
||||||
key={def.key}
|
|
||||||
value={get(assetInstance, def.key)}
|
|
||||||
onChange={val => onScreenPropChange(def.key, val)}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if showDisplayName}
|
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
bindable={false}
|
bindable={false}
|
||||||
control={Input}
|
control={Input}
|
||||||
label="Name"
|
label="Name"
|
||||||
key="_instanceName"
|
key="_instanceName"
|
||||||
value={componentInstance._instanceName}
|
value={componentInstance._instanceName}
|
||||||
onChange={onInstanceNameChange}
|
onChange={val => updateProp("_instanceName", val)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if settings && settings.length > 0}
|
{#if settings && settings.length > 0}
|
||||||
{#each settings as setting (`${componentInstance._id}-${setting.key}`)}
|
{#each settings as setting (`${componentInstance._id}-${setting.key}`)}
|
||||||
{#if canRenderControl(setting)}
|
{#if canRenderControl(setting)}
|
||||||
|
@ -147,52 +114,28 @@
|
||||||
value={componentInstance[setting.key] ??
|
value={componentInstance[setting.key] ??
|
||||||
componentInstance[setting.key]?.defaultValue}
|
componentInstance[setting.key]?.defaultValue}
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
onChange={val => onChange(setting.key, val)}
|
onChange={val => updateProp(setting.key, val)}
|
||||||
props={{ options: setting.options, placeholder: setting.placeholder }}
|
props={{
|
||||||
|
options: setting.options,
|
||||||
|
placeholder: setting.placeholder,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{/if}
|
||||||
<div class="text">This component doesn't have any additional settings.</div>
|
{#if componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||||
|
<ResetFieldsButton {componentInstance} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if componentDefinition?.info}
|
{#if componentDefinition?.info}
|
||||||
<div class="text">
|
<div class="text">
|
||||||
{@html componentDefinition?.info}
|
{@html componentDefinition?.info}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</DetailSummary>
|
||||||
{#if componentDefinition?.component?.endsWith("/fieldgroup")}
|
|
||||||
<div class="buttonWrapper">
|
|
||||||
<Button secondary wide on:click={() => confirmResetFieldsDialog?.show()}>
|
|
||||||
Update Form Fields
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmResetFieldsDialog}
|
|
||||||
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to update this Field Group?`}
|
|
||||||
okText="Update"
|
|
||||||
onOk={resetFormFields}
|
|
||||||
title="Confirm Form Field Update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.settings-view-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
.text {
|
.text {
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
margin-top: var(--spacing-m);
|
|
||||||
color: var(--grey-6);
|
color: var(--grey-6);
|
||||||
}
|
}
|
||||||
.buttonWrapper {
|
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
TextArea,
|
||||||
|
DetailSummary,
|
||||||
|
ActionButton,
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
Layout,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
export let componentInstance
|
||||||
|
|
||||||
|
let tempValue
|
||||||
|
let drawer
|
||||||
|
|
||||||
|
const openDrawer = () => {
|
||||||
|
tempValue = componentInstance?._styles?.custom
|
||||||
|
drawer.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
store.actions.components.updateCustomStyle(tempValue)
|
||||||
|
drawer.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DetailSummary
|
||||||
|
name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`}
|
||||||
|
collapsible={false}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton>
|
||||||
|
</div>
|
||||||
|
</DetailSummary>
|
||||||
|
<Drawer bind:this={drawer} title="Custom CSS">
|
||||||
|
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||||
|
<DrawerContent slot="body">
|
||||||
|
<div class="content">
|
||||||
|
<Layout gap="S">
|
||||||
|
<Body size="S">Custom CSS overrides all other component styles.</Body>
|
||||||
|
<TextArea bind:value={tempValue} placeholder="Enter some CSS..." />
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.content :global(textarea) {
|
||||||
|
font-family: monospace;
|
||||||
|
min-height: 240px !important;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script>
|
||||||
|
import StyleSection from "./StyleSection.svelte"
|
||||||
|
import * as ComponentStyles from "./componentStyles"
|
||||||
|
|
||||||
|
export let componentDefinition
|
||||||
|
export let componentInstance
|
||||||
|
|
||||||
|
const getStyles = def => {
|
||||||
|
if (!def?.styles?.length) {
|
||||||
|
return [...ComponentStyles.all]
|
||||||
|
}
|
||||||
|
let styles = [...ComponentStyles.all]
|
||||||
|
def.styles.forEach(style => {
|
||||||
|
if (ComponentStyles[style]) {
|
||||||
|
styles.push(ComponentStyles[style])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return styles
|
||||||
|
}
|
||||||
|
|
||||||
|
$: styles = getStyles(componentDefinition)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if styles?.length > 0}
|
||||||
|
{#each styles as style}
|
||||||
|
<StyleSection
|
||||||
|
{style}
|
||||||
|
name={style.label}
|
||||||
|
columns={style.columns}
|
||||||
|
properties={style.settings}
|
||||||
|
{componentInstance}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
|
@ -1,111 +0,0 @@
|
||||||
<script>
|
|
||||||
import { TextArea, DetailSummary, Button } from "@budibase/bbui"
|
|
||||||
import PropertyGroup from "./PropertyControls/PropertyGroup.svelte"
|
|
||||||
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup"
|
|
||||||
import { allStyles } from "./componentStyles"
|
|
||||||
|
|
||||||
export let componentDefinition = {}
|
|
||||||
export let componentInstance = {}
|
|
||||||
export let onStyleChanged = () => {}
|
|
||||||
export let onCustomStyleChanged = () => {}
|
|
||||||
export let onResetStyles = () => {}
|
|
||||||
|
|
||||||
let selectedCategory = "normal"
|
|
||||||
let currentGroup
|
|
||||||
|
|
||||||
function onChange(category) {
|
|
||||||
selectedCategory = category
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonProps = [
|
|
||||||
{ value: "normal", text: "Normal" },
|
|
||||||
{ value: "hover", text: "Hover" },
|
|
||||||
{ value: "active", text: "Active" },
|
|
||||||
]
|
|
||||||
|
|
||||||
$: groups = componentDefinition?.styleable ? Object.keys(allStyles) : []
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="state-categories">
|
|
||||||
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="positioned-wrapper">
|
|
||||||
<div class="property-groups">
|
|
||||||
{#if groups.length > 0}
|
|
||||||
{#each groups as groupName}
|
|
||||||
<PropertyGroup
|
|
||||||
name={groupName}
|
|
||||||
properties={allStyles[groupName]}
|
|
||||||
styleCategory={selectedCategory}
|
|
||||||
{onStyleChanged}
|
|
||||||
{componentInstance}
|
|
||||||
open={currentGroup === groupName}
|
|
||||||
on:open={() => (currentGroup = groupName)}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
<DetailSummary
|
|
||||||
name={`Custom Styles${componentInstance._styles.custom ? " *" : ""}`}
|
|
||||||
on:open={() => (currentGroup = "custom")}
|
|
||||||
show={currentGroup === "custom"}
|
|
||||||
thin
|
|
||||||
>
|
|
||||||
<div class="custom-styles">
|
|
||||||
<TextArea
|
|
||||||
value={componentInstance._styles.custom}
|
|
||||||
on:change={event => onCustomStyleChanged(event.detail)}
|
|
||||||
placeholder="Enter some CSS..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DetailSummary>
|
|
||||||
<Button secondary wide on:click={onResetStyles}>Reset Styles</Button>
|
|
||||||
{:else}
|
|
||||||
<div class="no-design">
|
|
||||||
This component doesn't have any design properties.
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
.positioned-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-groups {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
min-height: 0;
|
|
||||||
margin: 0 -20px;
|
|
||||||
padding: 0 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-design {
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
color: var(--grey-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-styles :global(textarea) {
|
|
||||||
font-family: monospace;
|
|
||||||
min-height: 120px;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,84 +1,33 @@
|
||||||
<script>
|
<script>
|
||||||
import { get } from "svelte/store"
|
import { store, selectedComponent } from "builderStore"
|
||||||
import { store, selectedComponent, currentAsset } from "builderStore"
|
|
||||||
import { Tabs, Tab } from "@budibase/bbui"
|
import { Tabs, Tab } from "@budibase/bbui"
|
||||||
import { FrontendTypes } from "constants"
|
import ScreenSettingsSection from "./ScreenSettingsSection.svelte"
|
||||||
import DesignView from "./DesignView.svelte"
|
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||||
import SettingsView from "./SettingsView.svelte"
|
import DesignSection from "./DesignSection.svelte"
|
||||||
import { setWith } from "lodash"
|
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||||
|
|
||||||
$: definition = store.actions.components.getDefinition(
|
$: componentInstance = $selectedComponent
|
||||||
|
$: componentDefinition = store.actions.components.getDefinition(
|
||||||
$selectedComponent?._component
|
$selectedComponent?._component
|
||||||
)
|
)
|
||||||
$: isComponentOrScreen =
|
|
||||||
$store.currentView === "component" ||
|
|
||||||
$store.currentFrontEndType === FrontendTypes.SCREEN
|
|
||||||
$: isNotScreenslot = !$selectedComponent._component.endsWith("screenslot")
|
|
||||||
$: showDisplayName = isComponentOrScreen && isNotScreenslot
|
|
||||||
|
|
||||||
const onStyleChanged = store.actions.components.updateStyle
|
|
||||||
const onCustomStyleChanged = store.actions.components.updateCustomStyle
|
|
||||||
const onResetStyles = store.actions.components.resetStyles
|
|
||||||
|
|
||||||
function setAssetProps(name, value) {
|
|
||||||
const selectedAsset = get(currentAsset)
|
|
||||||
store.update(state => {
|
|
||||||
if (
|
|
||||||
name === "_instanceName" &&
|
|
||||||
state.currentFrontEndType === FrontendTypes.SCREEN
|
|
||||||
) {
|
|
||||||
selectedAsset.props._instanceName = value
|
|
||||||
} else {
|
|
||||||
setWith(selectedAsset, name.split("."), value, Object)
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
store.actions.preview.saveSelected()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tabs selected="Settings">
|
<Tabs selected="Settings" noPadding>
|
||||||
<Tab title="Settings">
|
<Tab title="Settings">
|
||||||
<div class="tab-content-padding">
|
<div class="container">
|
||||||
{#if definition && definition.name}
|
<ScreenSettingsSection {componentInstance} {componentDefinition} />
|
||||||
<div class="instance-name">{definition.name}</div>
|
<ComponentSettingsSection {componentInstance} {componentDefinition} />
|
||||||
{/if}
|
<DesignSection {componentInstance} {componentDefinition} />
|
||||||
<SettingsView
|
<CustomStylesSection {componentInstance} {componentDefinition} />
|
||||||
componentInstance={$selectedComponent}
|
|
||||||
componentDefinition={definition}
|
|
||||||
{showDisplayName}
|
|
||||||
onChange={store.actions.components.updateProp}
|
|
||||||
onScreenPropChange={setAssetProps}
|
|
||||||
assetInstance={$store.currentView !== "component" && $currentAsset}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Design">
|
|
||||||
<div class="tab-content-padding">
|
|
||||||
{#if definition && definition.name}
|
|
||||||
<div class="instance-name">{definition.name}</div>
|
|
||||||
{/if}
|
|
||||||
<DesignView
|
|
||||||
componentInstance={$selectedComponent}
|
|
||||||
componentDefinition={definition}
|
|
||||||
{onStyleChanged}
|
|
||||||
{onCustomStyleChanged}
|
|
||||||
{onResetStyles}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.tab-content-padding {
|
.container {
|
||||||
padding: 0 var(--spacing-xl);
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
.instance-name {
|
align-items: stretch;
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
|
||||||
margin-bottom: var(--spacing-m);
|
|
||||||
margin-top: var(--spacing-xs);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--grey-7);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Icon, Drawer } from "@budibase/bbui"
|
import { Button, Icon, Drawer, Label } from "@budibase/bbui"
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import {
|
import {
|
||||||
getBindableProperties,
|
getBindableProperties,
|
||||||
|
@ -70,7 +70,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}>
|
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}>
|
||||||
<div class="label">{label}</div>
|
{#if type !== "boolean"}
|
||||||
|
<div class="label">
|
||||||
|
<Label>{label}</Label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div data-cy={`${key}-prop-control`} class="control">
|
<div data-cy={`${key}-prop-control`} class="control">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={control}
|
this={control}
|
||||||
|
@ -79,63 +83,55 @@
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
name={key}
|
||||||
|
text={label}
|
||||||
{type}
|
{type}
|
||||||
{...props}
|
{...props}
|
||||||
name={key}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
{#if bindable && !key.startsWith("_") && type === "text"}
|
||||||
{#if bindable && !key.startsWith("_") && type === "text"}
|
<div
|
||||||
<div
|
class="icon"
|
||||||
class="icon"
|
data-cy={`${key}-binding-button`}
|
||||||
data-cy={`${key}-binding-button`}
|
on:click={bindingDrawer.show}
|
||||||
on:click={bindingDrawer.show}
|
|
||||||
>
|
|
||||||
<Icon size="S" name="FlashOn" />
|
|
||||||
</div>
|
|
||||||
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Add the objects on the left to enrich your text.
|
|
||||||
</svelte:fragment>
|
|
||||||
<Button cta slot="buttons" disabled={!valid} on:click={handleClose}
|
|
||||||
>Save</Button
|
|
||||||
>
|
>
|
||||||
<BindingPanel
|
<Icon size="S" name="FlashOn" />
|
||||||
slot="body"
|
</div>
|
||||||
bind:valid
|
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
|
||||||
value={safeValue}
|
<svelte:fragment slot="description">
|
||||||
close={handleClose}
|
Add the objects on the left to enrich your text.
|
||||||
on:update={e => (temporaryBindableValue = e.detail)}
|
</svelte:fragment>
|
||||||
{bindableProperties}
|
<Button cta slot="buttons" disabled={!valid} on:click={handleClose}>
|
||||||
/>
|
Save
|
||||||
</Drawer>
|
</Button>
|
||||||
{/if}
|
<BindingPanel
|
||||||
|
slot="body"
|
||||||
|
bind:valid
|
||||||
|
value={safeValue}
|
||||||
|
close={handleClose}
|
||||||
|
on:update={e => (temporaryBindableValue = e.detail)}
|
||||||
|
{bindableProperties}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.property-control {
|
.property-control {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row;
|
flex-direction: column;
|
||||||
align-items: center;
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
flex: 0 0 80px;
|
|
||||||
text-align: left;
|
|
||||||
color: var(--ink);
|
|
||||||
margin-right: auto;
|
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
padding-bottom: var(--spectrum-global-dimension-size-65);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control {
|
.control {
|
||||||
flex: 1;
|
position: relative;
|
||||||
display: inline-block;
|
|
||||||
padding-left: 2px;
|
|
||||||
width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
<script>
|
|
||||||
import PropertyControl from "./PropertyControl.svelte"
|
|
||||||
import { DetailSummary } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let name = ""
|
|
||||||
export let styleCategory = "normal"
|
|
||||||
export let properties = []
|
|
||||||
export let componentInstance = {}
|
|
||||||
export let onStyleChanged = () => {}
|
|
||||||
export let open = false
|
|
||||||
|
|
||||||
$: style = componentInstance["_styles"][styleCategory] || {}
|
|
||||||
$: changed = properties.some(prop => hasPropChanged(style, prop))
|
|
||||||
|
|
||||||
const hasPropChanged = (style, prop) => {
|
|
||||||
return style[prop.key] != null && style[prop.key] !== ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const getControlProps = props => {
|
|
||||||
let controlProps = { ...(props || {}) }
|
|
||||||
delete controlProps.label
|
|
||||||
delete controlProps.key
|
|
||||||
delete controlProps.control
|
|
||||||
return controlProps
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DetailSummary name={`${name}${changed ? " *" : ""}`} on:open show={open} thin>
|
|
||||||
{#if open}
|
|
||||||
<div>
|
|
||||||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
|
|
||||||
<PropertyControl
|
|
||||||
bindable={false}
|
|
||||||
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
|
|
||||||
control={prop.control}
|
|
||||||
key={prop.key}
|
|
||||||
value={style[prop.key]}
|
|
||||||
onChange={value => onStyleChanged(styleCategory, prop.key, value)}
|
|
||||||
props={getControlProps(prop)}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</DetailSummary>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
import { currentAsset, store } from "builderStore"
|
||||||
|
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||||
|
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
|
export let componentInstance
|
||||||
|
|
||||||
|
let confirmResetFieldsDialog
|
||||||
|
|
||||||
|
const resetFormFields = () => {
|
||||||
|
const form = findClosestMatchingComponent(
|
||||||
|
$currentAsset.props,
|
||||||
|
componentInstance._id,
|
||||||
|
component => component._component.endsWith("/form")
|
||||||
|
)
|
||||||
|
const dataSource = form?.dataSource
|
||||||
|
const fields = makeDatasourceFormComponents(dataSource)
|
||||||
|
store.actions.components.updateProp(
|
||||||
|
"_children",
|
||||||
|
fields.map(field => field.json())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ActionButton
|
||||||
|
secondary
|
||||||
|
wide
|
||||||
|
on:click={() => confirmResetFieldsDialog?.show()}
|
||||||
|
>
|
||||||
|
Update form fields
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmResetFieldsDialog}
|
||||||
|
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to update this Field Group?`}
|
||||||
|
okText="Update"
|
||||||
|
onOk={resetFormFields}
|
||||||
|
title="Confirm Form Field Update"
|
||||||
|
/>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script>
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { get as deepGet, setWith } from "lodash"
|
||||||
|
import { Input, DetailSummary } from "@budibase/bbui"
|
||||||
|
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
||||||
|
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
||||||
|
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
||||||
|
import { currentAsset, store } from "builderStore"
|
||||||
|
import { FrontendTypes } from "constants"
|
||||||
|
|
||||||
|
export let componentInstance
|
||||||
|
|
||||||
|
function setAssetProps(name, value) {
|
||||||
|
const selectedAsset = get(currentAsset)
|
||||||
|
store.update(state => {
|
||||||
|
if (
|
||||||
|
name === "_instanceName" &&
|
||||||
|
state.currentFrontEndType === FrontendTypes.SCREEN
|
||||||
|
) {
|
||||||
|
selectedAsset.props._instanceName = value
|
||||||
|
} else {
|
||||||
|
setWith(selectedAsset, name.split("."), value, Object)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
store.actions.preview.saveSelected()
|
||||||
|
}
|
||||||
|
|
||||||
|
const screenSettings = [
|
||||||
|
// { key: "description", label: "Description", control: Input },
|
||||||
|
{ key: "routing.route", label: "Route", control: Input },
|
||||||
|
{ key: "routing.roleId", label: "Access", control: RoleSelect },
|
||||||
|
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $store.currentView !== "component" && $currentAsset && $store.currentFrontEndType === FrontendTypes.SCREEN}
|
||||||
|
<DetailSummary name="Screen" collapsible={false}>
|
||||||
|
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)}
|
||||||
|
<PropertyControl
|
||||||
|
bindable={false}
|
||||||
|
control={def.control}
|
||||||
|
label={def.label}
|
||||||
|
key={def.key}
|
||||||
|
value={deepGet($currentAsset, def.key)}
|
||||||
|
onChange={val => setAssetProps(def.key, val)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</DetailSummary>
|
||||||
|
{/if}
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script>
|
||||||
|
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
||||||
|
import { DetailSummary } from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
export let name
|
||||||
|
export let columns
|
||||||
|
export let properties
|
||||||
|
export let componentInstance
|
||||||
|
|
||||||
|
$: style = componentInstance._styles.normal || {}
|
||||||
|
$: changed = properties?.some(prop => hasPropChanged(style, prop)) ?? false
|
||||||
|
|
||||||
|
const hasPropChanged = (style, prop) => {
|
||||||
|
return style[prop.key] != null && style[prop.key] !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const getControlProps = props => {
|
||||||
|
let controlProps = { ...(props || {}) }
|
||||||
|
delete controlProps.label
|
||||||
|
delete controlProps.key
|
||||||
|
delete controlProps.control
|
||||||
|
return controlProps
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DetailSummary collapsible={false} name={`${name}${changed ? " *" : ""}`}>
|
||||||
|
<div class="group-content" style="grid-template-columns: {columns || '1fr'}">
|
||||||
|
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
|
||||||
|
<div style="grid-column: {prop.column || 'auto'}">
|
||||||
|
<PropertyControl
|
||||||
|
bindable={false}
|
||||||
|
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
|
||||||
|
control={prop.control}
|
||||||
|
key={prop.key}
|
||||||
|
value={style[prop.key]}
|
||||||
|
onChange={val => store.actions.components.updateStyle(prop.key, val)}
|
||||||
|
props={getControlProps(prop)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</DetailSummary>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.group-content {
|
||||||
|
display: grid;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
File diff suppressed because it is too large
Load Diff
|
@ -62,15 +62,17 @@
|
||||||
{#if datasource && integration}
|
{#if datasource && integration}
|
||||||
<section>
|
<section>
|
||||||
<Layout>
|
<Layout>
|
||||||
<header>
|
<Layout gap="XS" noPadding>
|
||||||
<svelte:component
|
<header>
|
||||||
this={ICONS[datasource.source]}
|
<svelte:component
|
||||||
height="26"
|
this={ICONS[datasource.source]}
|
||||||
width="26"
|
height="26"
|
||||||
/>
|
width="26"
|
||||||
<Heading size="M">{datasource.name}</Heading>
|
/>
|
||||||
</header>
|
<Heading size="M">{datasource.name}</Heading>
|
||||||
<Body size="S" grey lh>{integration.description}</Body>
|
</header>
|
||||||
|
<Body size="M">{integration.description}</Body>
|
||||||
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
|
@ -139,7 +141,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
margin: 0 0 var(--spacing-xs) 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -14,14 +14,16 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<Layout>
|
<Layout>
|
||||||
<header>
|
<Layout gap="XS" noPadding>
|
||||||
<svelte:component this={ICONS.BUDIBASE} height="26" width="26" />
|
<header>
|
||||||
<Heading size="M">Budibase Internal</Heading>
|
<svelte:component this={ICONS.BUDIBASE} height="26" width="26" />
|
||||||
</header>
|
<Heading size="M">Budibase Internal</Heading>
|
||||||
<Body size="S" grey lh
|
</header>
|
||||||
>Budibase internal tables are part of your app, the data will be stored in
|
<Body size="M">
|
||||||
your apps context.</Body
|
Budibase internal tables are part of your app, so the data will be
|
||||||
>
|
stored in your apps context.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Heading size="S">Tables</Heading>
|
<Heading size="S">Tables</Heading>
|
||||||
<div class="table-list">
|
<div class="table-list">
|
||||||
|
@ -32,7 +34,7 @@
|
||||||
>
|
>
|
||||||
<Body size="S">{table.name}</Body>
|
<Body size="S">{table.name}</Body>
|
||||||
{#if table.primaryDisplay}
|
{#if table.primaryDisplay}
|
||||||
<Body size="S">display column: {table.primaryDisplay}</Body>
|
<Body size="S">Display column: {table.primaryDisplay}</Body>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -50,7 +52,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
margin: 0 0 var(--spacing-xs) 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -91,14 +91,15 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#spectrum-root {
|
#spectrum-root,
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#app-root {
|
#app-root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#app-root {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -23,6 +23,11 @@
|
||||||
// props with old ones, depending on how long enrichment takes.
|
// props with old ones, depending on how long enrichment takes.
|
||||||
let latestUpdateTime
|
let latestUpdateTime
|
||||||
|
|
||||||
|
// Keep track of stringified representations of context and instance
|
||||||
|
// to avoid enriching bindings as much as possible
|
||||||
|
let lastContextKey
|
||||||
|
let lastInstanceKey
|
||||||
|
|
||||||
// Get contexts
|
// Get contexts
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const insideScreenslot = !!getContext("screenslot")
|
const insideScreenslot = !!getContext("screenslot")
|
||||||
|
@ -42,7 +47,9 @@
|
||||||
definition?.hasChildren &&
|
definition?.hasChildren &&
|
||||||
definition?.showEmptyState !== false &&
|
definition?.showEmptyState !== false &&
|
||||||
$builderStore.inBuilder
|
$builderStore.inBuilder
|
||||||
$: updateComponentProps(instance, $context)
|
$: rawProps = getRawProps(instance)
|
||||||
|
$: instanceKey = JSON.stringify(rawProps)
|
||||||
|
$: updateComponentProps(rawProps, instanceKey, $context)
|
||||||
$: selected =
|
$: selected =
|
||||||
$builderStore.inBuilder &&
|
$builderStore.inBuilder &&
|
||||||
$builderStore.selectedComponentId === instance._id
|
$builderStore.selectedComponentId === instance._id
|
||||||
|
@ -59,6 +66,16 @@
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getRawProps = instance => {
|
||||||
|
let validProps = {}
|
||||||
|
Object.entries(instance)
|
||||||
|
.filter(([name]) => !name.startsWith("_"))
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
validProps[key] = value
|
||||||
|
})
|
||||||
|
return validProps
|
||||||
|
}
|
||||||
|
|
||||||
// Gets the component constructor for the specified component
|
// Gets the component constructor for the specified component
|
||||||
const getComponentConstructor = component => {
|
const getComponentConstructor = component => {
|
||||||
const split = component?.split("/")
|
const split = component?.split("/")
|
||||||
|
@ -76,13 +93,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enriches any string component props using handlebars
|
// Enriches any string component props using handlebars
|
||||||
const updateComponentProps = (instance, context) => {
|
const updateComponentProps = (rawProps, instanceKey, context) => {
|
||||||
|
const instanceSame = instanceKey === lastInstanceKey
|
||||||
|
const contextSame = context.key === lastContextKey
|
||||||
|
|
||||||
|
if (instanceSame && contextSame) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
lastInstanceKey = instanceKey
|
||||||
|
lastContextKey = context.key
|
||||||
|
}
|
||||||
|
|
||||||
// Record the timestamp so we can reference it after enrichment
|
// Record the timestamp so we can reference it after enrichment
|
||||||
latestUpdateTime = Date.now()
|
latestUpdateTime = Date.now()
|
||||||
const enrichmentTime = latestUpdateTime
|
const enrichmentTime = latestUpdateTime
|
||||||
|
|
||||||
// Enrich props with context
|
// Enrich props with context
|
||||||
const enrichedProps = enrichProps(instance, context)
|
const enrichedProps = enrichProps(rawProps, context)
|
||||||
|
|
||||||
// Abandon this update if a newer update has started
|
// Abandon this update if a newer update has started
|
||||||
if (enrichmentTime !== latestUpdateTime) {
|
if (enrichmentTime !== latestUpdateTime) {
|
||||||
|
|
|
@ -14,18 +14,32 @@
|
||||||
const newContext = createContextStore(context)
|
const newContext = createContextStore(context)
|
||||||
setContext("context", newContext)
|
setContext("context", newContext)
|
||||||
|
|
||||||
$: providerKey = key || $component.id
|
const providerKey = key || $component.id
|
||||||
|
|
||||||
// Add data context
|
// Generate a permanent unique ID for this component and use it to register
|
||||||
$: newContext.actions.provideData(providerKey, data)
|
// any datasource actions
|
||||||
|
const instanceId = generate()
|
||||||
|
|
||||||
// Instance ID is unique to each instance of a provider
|
// Keep previous state around so we can avoid updating unless necessary
|
||||||
let instanceId
|
let lastDataKey
|
||||||
|
let lastActionsKey
|
||||||
|
|
||||||
// Add actions context
|
$: provideData(data)
|
||||||
$: {
|
$: provideActions(actions, instanceId)
|
||||||
if (instanceId) {
|
|
||||||
actions?.forEach(({ type, callback, metadata }) => {
|
const provideData = newData => {
|
||||||
|
const dataKey = JSON.stringify(newData)
|
||||||
|
if (dataKey !== lastDataKey) {
|
||||||
|
newContext.actions.provideData(providerKey, newData)
|
||||||
|
lastDataKey = dataKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const provideActions = newActions => {
|
||||||
|
const actionsKey = JSON.stringify(newActions)
|
||||||
|
if (actionsKey !== lastActionsKey) {
|
||||||
|
lastActionsKey = actionsKey
|
||||||
|
newActions?.forEach(({ type, callback, metadata }) => {
|
||||||
newContext.actions.provideAction(providerKey, type, callback)
|
newContext.actions.provideAction(providerKey, type, callback)
|
||||||
|
|
||||||
// Register any "refresh datasource" actions with a singleton store
|
// Register any "refresh datasource" actions with a singleton store
|
||||||
|
@ -43,10 +57,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Generate a permanent unique ID for this component and use it to register
|
|
||||||
// any datasource actions
|
|
||||||
instanceId = generate()
|
|
||||||
|
|
||||||
// Unregister all datasource instances when unmounting this provider
|
// Unregister all datasource instances when unmounting this provider
|
||||||
return () => dataSourceStore.actions.unregisterInstance(instanceId)
|
return () => dataSourceStore.actions.unregisterInstance(instanceId)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import SettingsButton from "./SettingsButton.svelte"
|
import SettingsButton from "./SettingsButton.svelte"
|
||||||
|
import SettingsColorPicker from "./SettingsColorPicker.svelte"
|
||||||
|
import SettingsPicker from "./SettingsPicker.svelte"
|
||||||
import { builderStore } from "../../store"
|
import { builderStore } from "../../store"
|
||||||
import { domDebounce } from "../../utils/domDebounce"
|
import { domDebounce } from "../../utils/domDebounce"
|
||||||
|
|
||||||
|
@ -87,19 +89,44 @@
|
||||||
>
|
>
|
||||||
{#each settings as setting, idx}
|
{#each settings as setting, idx}
|
||||||
{#if setting.type === "select"}
|
{#if setting.type === "select"}
|
||||||
{#each setting.options as option}
|
{#if setting.barStyle === "buttons"}
|
||||||
<SettingsButton
|
{#each setting.options as option}
|
||||||
|
<SettingsButton
|
||||||
|
prop={setting.key}
|
||||||
|
value={option.value}
|
||||||
|
icon={option.barIcon}
|
||||||
|
title={option.barTitle}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<SettingsPicker
|
||||||
prop={setting.key}
|
prop={setting.key}
|
||||||
value={option.value}
|
options={setting.options}
|
||||||
icon={option.barIcon}
|
label={setting.label}
|
||||||
title={option.barTitle}
|
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/if}
|
||||||
|
{:else if setting.type === "boolean"}
|
||||||
|
<SettingsButton
|
||||||
|
prop={setting.key}
|
||||||
|
icon={setting.barIcon}
|
||||||
|
title={setting.barTitle}
|
||||||
|
bool
|
||||||
|
/>
|
||||||
|
{:else if setting.type === "color"}
|
||||||
|
<SettingsColorPicker prop={setting.key} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if idx < settings.length - 1}
|
{#if setting.barSeparator !== false}
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
<SettingsButton
|
||||||
|
icon="Delete"
|
||||||
|
on:click={() => {
|
||||||
|
builderStore.actions.deleteComponent(
|
||||||
|
$builderStore.selectedComponent._id
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { builderStore } from "../../store"
|
import { builderStore } from "../../store"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let prop
|
export let prop
|
||||||
export let value
|
export let value
|
||||||
|
@ -9,6 +10,7 @@
|
||||||
export let rotate = false
|
export let rotate = false
|
||||||
export let bool = false
|
export let bool = false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
$: currentValue = $builderStore.selectedComponent?.[prop]
|
$: currentValue = $builderStore.selectedComponent?.[prop]
|
||||||
$: active = prop && (bool ? !!currentValue : currentValue === value)
|
$: active = prop && (bool ? !!currentValue : currentValue === value)
|
||||||
</script>
|
</script>
|
||||||
|
@ -22,6 +24,7 @@
|
||||||
const newValue = bool ? !currentValue : value
|
const newValue = bool ? !currentValue : value
|
||||||
builderStore.actions.updateProp(prop, newValue)
|
builderStore.actions.updateProp(prop, newValue)
|
||||||
}
|
}
|
||||||
|
dispatch("click")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name={icon} size="S" />
|
<Icon name={icon} size="S" />
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script>
|
||||||
|
import { ColorPicker } from "@budibase/bbui"
|
||||||
|
import { builderStore } from "../../store"
|
||||||
|
|
||||||
|
export let prop
|
||||||
|
|
||||||
|
$: currentValue = $builderStore.selectedComponent?.[prop]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ColorPicker
|
||||||
|
size="S"
|
||||||
|
value={currentValue}
|
||||||
|
on:change={e => {
|
||||||
|
if (prop) {
|
||||||
|
builderStore.actions.updateProp(prop, e.detail)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
import { Select } from "@budibase/bbui"
|
||||||
|
import { builderStore } from "../../store"
|
||||||
|
|
||||||
|
export let prop
|
||||||
|
export let options
|
||||||
|
export let label
|
||||||
|
|
||||||
|
$: currentValue = $builderStore.selectedComponent?.[prop]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
quiet
|
||||||
|
autoWidth
|
||||||
|
placeholder={label}
|
||||||
|
{options}
|
||||||
|
value={currentValue}
|
||||||
|
on:change={e => {
|
||||||
|
if (prop) {
|
||||||
|
builderStore.actions.updateProp(prop, e.detail)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -56,13 +56,14 @@ const createBuilderStore = () => {
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
selectComponent: id => {
|
selectComponent: id => {
|
||||||
if (id) {
|
dispatchEvent("select-component", { id })
|
||||||
dispatchEvent("select-component", { id })
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
updateProp: (prop, value) => {
|
updateProp: (prop, value) => {
|
||||||
dispatchEvent("update-prop", { prop, value })
|
dispatchEvent("update-prop", { prop, value })
|
||||||
},
|
},
|
||||||
|
deleteComponent: id => {
|
||||||
|
dispatchEvent("delete-component", { id })
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...writableStore,
|
...writableStore,
|
||||||
|
|
|
@ -4,7 +4,21 @@ export const createContextStore = oldContext => {
|
||||||
const newContext = writable({})
|
const newContext = writable({})
|
||||||
const contexts = oldContext ? [oldContext, newContext] : [newContext]
|
const contexts = oldContext ? [oldContext, newContext] : [newContext]
|
||||||
const totalContext = derived(contexts, $contexts => {
|
const totalContext = derived(contexts, $contexts => {
|
||||||
return $contexts.reduce((total, context) => ({ ...total, ...context }), {})
|
// The key is the serialized representation of context
|
||||||
|
let key = ""
|
||||||
|
for (let i = 0; i < $contexts.length - 1; i++) {
|
||||||
|
key += $contexts[i].key
|
||||||
|
}
|
||||||
|
key += JSON.stringify($contexts[$contexts.length - 1])
|
||||||
|
|
||||||
|
// Reduce global state
|
||||||
|
const reducer = (total, context) => ({ ...total, ...context })
|
||||||
|
const context = $contexts.reduce(reducer, {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...context,
|
||||||
|
key,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Adds a data context layer to the tree
|
// Adds a data context layer to the tree
|
||||||
|
|
|
@ -31,8 +31,14 @@ const triggerAutomationHandler = async action => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigationHandler = action => {
|
const navigationHandler = action => {
|
||||||
if (action.parameters.url) {
|
const { url } = action.parameters
|
||||||
routeStore.actions.navigate(action.parameters.url)
|
if (url) {
|
||||||
|
const external = !url.startsWith("/")
|
||||||
|
if (external) {
|
||||||
|
window.location.href = url
|
||||||
|
} else {
|
||||||
|
routeStore.actions.navigate(action.parameters.url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,14 +22,6 @@ export const propsAreSame = (a, b) => {
|
||||||
* Data bindings are enriched, and button actions are enriched.
|
* Data bindings are enriched, and button actions are enriched.
|
||||||
*/
|
*/
|
||||||
export const enrichProps = (props, context) => {
|
export const enrichProps = (props, context) => {
|
||||||
// Exclude all private props that start with an underscore
|
|
||||||
let validProps = {}
|
|
||||||
Object.entries(props)
|
|
||||||
.filter(([name]) => !name.startsWith("_"))
|
|
||||||
.forEach(([key, value]) => {
|
|
||||||
validProps[key] = value
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create context of all bindings and data contexts
|
// Create context of all bindings and data contexts
|
||||||
// Duplicate the closest context as "data" which the builder requires
|
// Duplicate the closest context as "data" which the builder requires
|
||||||
const totalContext = {
|
const totalContext = {
|
||||||
|
@ -41,7 +33,7 @@ export const enrichProps = (props, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich all data bindings in top level props
|
// Enrich all data bindings in top level props
|
||||||
let enrichedProps = enrichDataBindings(validProps, totalContext)
|
let enrichedProps = enrichDataBindings(props, totalContext)
|
||||||
|
|
||||||
// Enrich click actions if they exist
|
// Enrich click actions if they exist
|
||||||
if (enrichedProps.onClick) {
|
if (enrichedProps.onClick) {
|
||||||
|
|
|
@ -31,6 +31,12 @@ export const styleable = (node, styles = {}) => {
|
||||||
if (newStyles.empty) {
|
if (newStyles.empty) {
|
||||||
baseStyles.border = "2px dashed var(--grey-5)"
|
baseStyles.border = "2px dashed var(--grey-5)"
|
||||||
baseStyles.padding = "var(--spacing-l)"
|
baseStyles.padding = "var(--spacing-l)"
|
||||||
|
baseStyles.overflow = "hidden"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append border-style css if border-width is specified
|
||||||
|
if (newStyles.normal?.["border-width"]) {
|
||||||
|
baseStyles["border-style"] = "solid"
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentId = newStyles.id
|
const componentId = newStyles.id
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"emit": true,
|
"emit": true,
|
||||||
"key": true
|
"key": true
|
||||||
},
|
},
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
"env": {
|
"env": {
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,6 +9,7 @@ ENV BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
# copy files and install dependencies
|
# copy files and install dependencies
|
||||||
COPY . ./
|
COPY . ./
|
||||||
RUN yarn
|
RUN yarn
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
EXPOSE 4001
|
EXPOSE 4001
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const elastic = {}
|
const elastic: any = {}
|
||||||
|
|
||||||
elastic.Client = function () {
|
elastic.Client = function () {
|
||||||
this.index = jest.fn().mockResolvedValue({ body: [] })
|
this.index = jest.fn().mockResolvedValue({ body: [] })
|
|
@ -1,18 +0,0 @@
|
||||||
class Email {
|
|
||||||
constructor() {
|
|
||||||
this.apiKey = null
|
|
||||||
}
|
|
||||||
|
|
||||||
setApiKey(apiKey) {
|
|
||||||
this.apiKey = apiKey
|
|
||||||
}
|
|
||||||
|
|
||||||
async send(msg) {
|
|
||||||
if (msg.to === "invalid@test.com") {
|
|
||||||
throw "Invalid"
|
|
||||||
}
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new Email()
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
module SendgridMock {
|
||||||
|
class Email {
|
||||||
|
constructor() {
|
||||||
|
// @ts-ignore
|
||||||
|
this.apiKey = null
|
||||||
|
}
|
||||||
|
|
||||||
|
setApiKey(apiKey: any) {
|
||||||
|
// @ts-ignore
|
||||||
|
this.apiKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(msg: any) {
|
||||||
|
if (msg.to === "invalid@test.com") {
|
||||||
|
throw "Invalid"
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Email()
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
function Airtable() {
|
|
||||||
this.base = jest.fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Airtable
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
module AirtableMock {
|
||||||
|
function Airtable() {
|
||||||
|
// @ts-ignore
|
||||||
|
this.base = jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Airtable
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
const arangodb = {}
|
|
||||||
|
|
||||||
arangodb.Database = function () {
|
|
||||||
this.query = jest.fn(() => ({
|
|
||||||
all: jest.fn(),
|
|
||||||
}))
|
|
||||||
this.collection = jest.fn(() => "collection")
|
|
||||||
this.close = jest.fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
arangodb.aql = (strings, ...args) => {
|
|
||||||
let str = strings.join("{}")
|
|
||||||
|
|
||||||
for (let arg of args) {
|
|
||||||
str = str.replace("{}", arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = arangodb
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
module ArangoMock {
|
||||||
|
const arangodb: any = {}
|
||||||
|
|
||||||
|
arangodb.Database = function () {
|
||||||
|
this.query = jest.fn(() => ({
|
||||||
|
all: jest.fn(),
|
||||||
|
}))
|
||||||
|
this.collection = jest.fn(() => "collection")
|
||||||
|
this.close = jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
arangodb.aql = (strings, ...args) => {
|
||||||
|
let str = strings.join("{}")
|
||||||
|
|
||||||
|
for (let arg of args) {
|
||||||
|
str = str.replace("{}", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = arangodb
|
||||||
|
}
|
|
@ -1,38 +0,0 @@
|
||||||
const aws = {}
|
|
||||||
|
|
||||||
const response = body => () => ({ promise: () => body })
|
|
||||||
|
|
||||||
function DocumentClient() {
|
|
||||||
this.put = jest.fn(response({}))
|
|
||||||
this.query = jest.fn(
|
|
||||||
response({
|
|
||||||
Items: [],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
this.scan = jest.fn(
|
|
||||||
response({
|
|
||||||
Items: [
|
|
||||||
{
|
|
||||||
Name: "test",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
this.get = jest.fn(response({}))
|
|
||||||
this.update = jest.fn(response({}))
|
|
||||||
this.delete = jest.fn(response({}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function S3() {
|
|
||||||
this.listObjects = jest.fn(
|
|
||||||
response({
|
|
||||||
Contents: {},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
aws.DynamoDB = { DocumentClient }
|
|
||||||
aws.S3 = S3
|
|
||||||
aws.config = { update: jest.fn() }
|
|
||||||
|
|
||||||
module.exports = aws
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
module AwsMock {
|
||||||
|
const aws: any = {}
|
||||||
|
|
||||||
|
const response = (body: any) => () => ({promise: () => body})
|
||||||
|
|
||||||
|
function DocumentClient() {
|
||||||
|
// @ts-ignore
|
||||||
|
this.put = jest.fn(response({}))
|
||||||
|
// @ts-ignore
|
||||||
|
this.query = jest.fn(
|
||||||
|
response({
|
||||||
|
Items: [],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// @ts-ignore
|
||||||
|
this.scan = jest.fn(
|
||||||
|
response({
|
||||||
|
Items: [
|
||||||
|
{
|
||||||
|
Name: "test",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// @ts-ignore
|
||||||
|
this.get = jest.fn(response({}))
|
||||||
|
// @ts-ignore
|
||||||
|
this.update = jest.fn(response({}))
|
||||||
|
// @ts-ignore
|
||||||
|
this.delete = jest.fn(response({}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function S3() {
|
||||||
|
// @ts-ignore
|
||||||
|
this.listObjects = jest.fn(
|
||||||
|
response({
|
||||||
|
Contents: {},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
aws.DynamoDB = {DocumentClient}
|
||||||
|
aws.S3 = S3
|
||||||
|
aws.config = {update: jest.fn()}
|
||||||
|
|
||||||
|
module.exports = aws
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
const mongodb = {}
|
|
||||||
|
|
||||||
mongodb.MongoClient = function () {
|
|
||||||
this.connect = jest.fn()
|
|
||||||
this.close = jest.fn()
|
|
||||||
this.insertOne = jest.fn()
|
|
||||||
this.find = jest.fn(() => ({ toArray: () => [] }))
|
|
||||||
|
|
||||||
this.collection = jest.fn(() => ({
|
|
||||||
insertOne: this.insertOne,
|
|
||||||
find: this.find,
|
|
||||||
}))
|
|
||||||
|
|
||||||
this.db = () => ({
|
|
||||||
collection: this.collection,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = mongodb
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
module MongoMock {
|
||||||
|
const mongodb: any = {}
|
||||||
|
|
||||||
|
mongodb.MongoClient = function () {
|
||||||
|
this.connect = jest.fn()
|
||||||
|
this.close = jest.fn()
|
||||||
|
this.insertOne = jest.fn()
|
||||||
|
this.find = jest.fn(() => ({toArray: () => []}))
|
||||||
|
|
||||||
|
this.collection = jest.fn(() => ({
|
||||||
|
insertOne: this.insertOne,
|
||||||
|
find: this.find,
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.db = () => ({
|
||||||
|
collection: this.collection,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = mongodb
|
||||||
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
const mssql = {}
|
|
||||||
|
|
||||||
mssql.query = jest.fn(() => ({
|
|
||||||
recordset: [
|
|
||||||
{
|
|
||||||
a: "string",
|
|
||||||
b: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
// mssql.connect = jest.fn(() => ({ recordset: [] }))
|
|
||||||
|
|
||||||
mssql.ConnectionPool = jest.fn(() => ({
|
|
||||||
connect: jest.fn(() => ({
|
|
||||||
request: jest.fn(() => ({
|
|
||||||
query: jest.fn(() => ({})),
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
}))
|
|
||||||
|
|
||||||
module.exports = mssql
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
module MsSqlMock {
|
||||||
|
const mssql: any = {}
|
||||||
|
|
||||||
|
mssql.query = jest.fn(() => ({
|
||||||
|
recordset: [
|
||||||
|
{
|
||||||
|
a: "string",
|
||||||
|
b: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
// mssql.connect = jest.fn(() => ({ recordset: [] }))
|
||||||
|
|
||||||
|
mssql.ConnectionPool = jest.fn(() => ({
|
||||||
|
connect: jest.fn(() => ({
|
||||||
|
request: jest.fn(() => ({
|
||||||
|
query: jest.fn(() => ({})),
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
module.exports = mssql
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
const mysql = {}
|
|
||||||
|
|
||||||
const client = {
|
|
||||||
connect: jest.fn(),
|
|
||||||
query: jest.fn((query, bindings, fn) => {
|
|
||||||
fn(null, [])
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
mysql.createConnection = jest.fn(() => client)
|
|
||||||
|
|
||||||
module.exports = mysql
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
module MySQLMock {
|
||||||
|
const mysql: any = {}
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
connect: jest.fn(),
|
||||||
|
query: jest.fn((query, bindings, fn) => {
|
||||||
|
fn(null, [])
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
mysql.createConnection = jest.fn(() => client)
|
||||||
|
|
||||||
|
module.exports = mysql
|
||||||
|
}
|
|
@ -1,58 +0,0 @@
|
||||||
const fetch = jest.requireActual("node-fetch")
|
|
||||||
|
|
||||||
module.exports = async (url, opts) => {
|
|
||||||
function json(body, status = 200) {
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
headers: {
|
|
||||||
get: () => {
|
|
||||||
return ["application/json"]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
json: async () => {
|
|
||||||
return body
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.includes("/api/admin")) {
|
|
||||||
return json({
|
|
||||||
email: "test@test.com",
|
|
||||||
_id: "us_test@test.com",
|
|
||||||
status: "active",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// mocked data based on url
|
|
||||||
else if (url.includes("api/apps")) {
|
|
||||||
return json({
|
|
||||||
app1: {
|
|
||||||
url: "/app1",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else if (url.includes("test.com")) {
|
|
||||||
return json({
|
|
||||||
body: opts.body,
|
|
||||||
url,
|
|
||||||
method: opts.method,
|
|
||||||
})
|
|
||||||
} else if (url.includes("invalid.com")) {
|
|
||||||
return json(
|
|
||||||
{
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
404
|
|
||||||
)
|
|
||||||
} else if (url.includes("_search")) {
|
|
||||||
return json({
|
|
||||||
rows: [
|
|
||||||
{
|
|
||||||
doc: {
|
|
||||||
_id: "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
bookmark: "test",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return fetch(url, opts)
|
|
||||||
}
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
module FetchMock {
|
||||||
|
const fetch = jest.requireActual("node-fetch")
|
||||||
|
|
||||||
|
module.exports = async (url: any, opts: any) => {
|
||||||
|
function json(body: any, status = 200) {
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
get: () => {
|
||||||
|
return ["application/json"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
json: async () => {
|
||||||
|
return body
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.includes("/api/admin")) {
|
||||||
|
return json({
|
||||||
|
email: "test@test.com",
|
||||||
|
_id: "us_test@test.com",
|
||||||
|
status: "active",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// mocked data based on url
|
||||||
|
else if (url.includes("api/apps")) {
|
||||||
|
return json({
|
||||||
|
app1: {
|
||||||
|
url: "/app1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (url.includes("test.com")) {
|
||||||
|
return json({
|
||||||
|
body: opts.body,
|
||||||
|
url,
|
||||||
|
method: opts.method,
|
||||||
|
})
|
||||||
|
} else if (url.includes("invalid.com")) {
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
invalid: true,
|
||||||
|
},
|
||||||
|
404
|
||||||
|
)
|
||||||
|
} else if (url.includes("_search")) {
|
||||||
|
return json({
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
doc: {
|
||||||
|
_id: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookmark: "test",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return fetch(url, opts)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +0,0 @@
|
||||||
const pg = {}
|
|
||||||
|
|
||||||
const query = jest.fn(() => ({
|
|
||||||
rows: [
|
|
||||||
{
|
|
||||||
a: "string",
|
|
||||||
b: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
// constructor
|
|
||||||
function Client() {}
|
|
||||||
|
|
||||||
Client.prototype.query = query
|
|
||||||
Client.prototype.connect = jest.fn()
|
|
||||||
Client.prototype.release = jest.fn()
|
|
||||||
|
|
||||||
function Pool() {}
|
|
||||||
Pool.prototype.query = query
|
|
||||||
Pool.prototype.connect = jest.fn(() => {
|
|
||||||
return new Client()
|
|
||||||
})
|
|
||||||
|
|
||||||
pg.Client = Client
|
|
||||||
pg.Pool = Pool
|
|
||||||
pg.queryMock = query
|
|
||||||
|
|
||||||
module.exports = pg
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
module PgMock {
|
||||||
|
const pg: any = {}
|
||||||
|
|
||||||
|
const query = jest.fn(() => ({
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
a: "string",
|
||||||
|
b: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
function Client() {
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.prototype.query = query
|
||||||
|
Client.prototype.connect = jest.fn()
|
||||||
|
Client.prototype.release = jest.fn()
|
||||||
|
|
||||||
|
function Pool() {
|
||||||
|
}
|
||||||
|
|
||||||
|
Pool.prototype.query = query
|
||||||
|
Pool.prototype.connect = jest.fn(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
return new Client()
|
||||||
|
})
|
||||||
|
|
||||||
|
pg.Client = Client
|
||||||
|
pg.Pool = Pool
|
||||||
|
pg.queryMock = query
|
||||||
|
|
||||||
|
module.exports = pg
|
||||||
|
}
|
|
@ -3,25 +3,29 @@
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "0.9.66",
|
"version": "0.9.66",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/electron.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Budibase/budibase.git"
|
"url": "https://github.com/Budibase/budibase.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --testPathIgnorePatterns=routes && yarn run test:integration",
|
"build": "rm -rf dist/ && tsc && mv dist/src/* dist/ && rmdir dist/src/ && yarn postbuild",
|
||||||
"test:integration": "jest --coverage --detectOpenHandles",
|
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
|
||||||
|
"test": "jest --coverage --maxWorkers=2",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"build:docker": "docker build . -t app-service",
|
"build:docker": "docker build . -t app-service",
|
||||||
"run:docker": "node src/index",
|
"run:docker": "node dist/index.js",
|
||||||
"dev:stack:up": "node scripts/dev/manage.js up",
|
"dev:stack:up": "node scripts/dev/manage.js up",
|
||||||
"dev:stack:down": "node scripts/dev/manage.js down",
|
"dev:stack:down": "node scripts/dev/manage.js down",
|
||||||
"dev:stack:nuke": "node scripts/dev/manage.js nuke",
|
"dev:stack:nuke": "node scripts/dev/manage.js nuke",
|
||||||
"dev:builder": "yarn run dev:stack:up && nodemon src/index.js",
|
"dev:builder": "yarn run dev:stack:up && ts-node src/index.ts",
|
||||||
|
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
|
||||||
"lint": "eslint --fix src/",
|
"lint": "eslint --fix src/",
|
||||||
|
"lint:fix": "yarn run format && yarn run lint",
|
||||||
"initialise": "node scripts/initialise.js"
|
"initialise": "node scripts/initialise.js"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"preset": "ts-jest",
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"./scripts/jestSetup.js"
|
"./scripts/jestSetup.js"
|
||||||
|
@ -112,14 +116,25 @@
|
||||||
"@babel/preset-env": "^7.14.4",
|
"@babel/preset-env": "^7.14.4",
|
||||||
"@budibase/standard-components": "^0.9.66",
|
"@budibase/standard-components": "^0.9.66",
|
||||||
"@jest/test-sequencer": "^24.8.0",
|
"@jest/test-sequencer": "^24.8.0",
|
||||||
|
"@types/bull": "^3.15.1",
|
||||||
|
"@types/jest": "^26.0.23",
|
||||||
|
"@types/koa": "^2.13.3",
|
||||||
|
"@types/koa-router": "^7.4.2",
|
||||||
|
"@types/node": "^15.12.4",
|
||||||
|
"@typescript-eslint/parser": "^4.28.0",
|
||||||
"babel-jest": "^27.0.2",
|
"babel-jest": "^27.0.2",
|
||||||
|
"copyfiles": "^2.4.1",
|
||||||
"docker-compose": "^0.23.6",
|
"docker-compose": "^0.23.6",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"jest": "^24.8.0",
|
"jest": "^27.0.5",
|
||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.4",
|
||||||
"pouchdb-adapter-memory": "^7.2.1",
|
"pouchdb-adapter-memory": "^7.2.1",
|
||||||
"supertest": "^4.0.2"
|
"prettier": "^2.3.1",
|
||||||
|
"supertest": "^4.0.2",
|
||||||
|
"ts-jest": "^27.0.3",
|
||||||
|
"ts-node": "^10.0.0",
|
||||||
|
"typescript": "^4.3.4"
|
||||||
},
|
},
|
||||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,3 +6,4 @@ env._set("JWT_SECRET", "test-jwtsecret")
|
||||||
env._set("CLIENT_ID", "test-client-id")
|
env._set("CLIENT_ID", "test-client-id")
|
||||||
env._set("BUDIBASE_DIR", tmpdir("budibase-unittests"))
|
env._set("BUDIBASE_DIR", tmpdir("budibase-unittests"))
|
||||||
env._set("LOG_LEVEL", "silent")
|
env._set("LOG_LEVEL", "silent")
|
||||||
|
env._set("PORT", 0)
|
||||||
|
|
|
@ -14,6 +14,7 @@ exports.save = async function (ctx) {
|
||||||
...EMPTY_LAYOUT,
|
...EMPTY_LAYOUT,
|
||||||
...layout,
|
...layout,
|
||||||
}
|
}
|
||||||
|
layout.props._instanceName = layout.name
|
||||||
}
|
}
|
||||||
|
|
||||||
layout._id = layout._id || generateLayoutID()
|
layout._id = layout._id || generateLayoutID()
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
const { clearAllApps, checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
|
||||||
const setup = require("./utilities")
|
|
||||||
const { AppStatus } = require("../../../db/utils")
|
|
||||||
|
|
||||||
jest.mock("../../../utilities/redis", () => ({
|
jest.mock("../../../utilities/redis", () => ({
|
||||||
init: jest.fn(),
|
init: jest.fn(),
|
||||||
getAllLocks: () => {
|
getAllLocks: () => {
|
||||||
|
@ -15,6 +11,10 @@ jest.mock("../../../utilities/redis", () => ({
|
||||||
checkDebounce: jest.fn(),
|
checkDebounce: jest.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const { clearAllApps, checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||||
|
const setup = require("./utilities")
|
||||||
|
const { AppStatus } = require("../../../db/utils")
|
||||||
|
|
||||||
describe("/applications", () => {
|
describe("/applications", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
jest.mock("../../../utilities/fileSystem/utilities")
|
||||||
|
|
||||||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||||
const setup = require("./utilities")
|
const setup = require("./utilities")
|
||||||
|
|
||||||
jest.mock("../../../utilities/fileSystem/utilities")
|
|
||||||
|
|
||||||
describe("/backups", () => {
|
describe("/backups", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
jest.mock("pg")
|
||||||
|
|
||||||
let setup = require("./utilities")
|
let setup = require("./utilities")
|
||||||
let { basicDatasource } = setup.structures
|
let { basicDatasource } = setup.structures
|
||||||
let { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
let { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||||
|
|
||||||
jest.mock("pg")
|
|
||||||
const pg = require("pg")
|
const pg = require("pg")
|
||||||
|
|
||||||
describe("/datasources", () => {
|
describe("/datasources", () => {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
// need to load environment first
|
// need to load environment first
|
||||||
|
import { ExtendableContext } from "koa"
|
||||||
|
|
||||||
const env = require("./environment")
|
const env = require("./environment")
|
||||||
const CouchDB = require("./db")
|
const CouchDB = require("./db")
|
||||||
require("@budibase/auth").init(CouchDB)
|
require("@budibase/auth").init(CouchDB)
|
||||||
|
@ -40,7 +42,7 @@ app.use(
|
||||||
|
|
||||||
if (!env.isTest()) {
|
if (!env.isTest()) {
|
||||||
const bullApp = bullboard.init()
|
const bullApp = bullboard.init()
|
||||||
app.use(async (ctx, next) => {
|
app.use(async (ctx: ExtendableContext, next: () => any) => {
|
||||||
if (ctx.path.startsWith(bullboard.pathPrefix)) {
|
if (ctx.path.startsWith(bullboard.pathPrefix)) {
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.respond = false
|
ctx.respond = false
|
||||||
|
@ -61,9 +63,9 @@ if (env.isProd()) {
|
||||||
env._set("NODE_ENV", "production")
|
env._set("NODE_ENV", "production")
|
||||||
Sentry.init()
|
Sentry.init()
|
||||||
|
|
||||||
app.on("error", (err, ctx) => {
|
app.on("error", (err: any, ctx: ExtendableContext) => {
|
||||||
Sentry.withScope(function (scope) {
|
Sentry.withScope(function (scope: any) {
|
||||||
scope.addEventProcessor(function (event) {
|
scope.addEventProcessor(function (event: any) {
|
||||||
return Sentry.Handlers.parseRequest(event, ctx.request)
|
return Sentry.Handlers.parseRequest(event, ctx.request)
|
||||||
})
|
})
|
||||||
Sentry.captureException(err)
|
Sentry.captureException(err)
|
|
@ -1,18 +1,4 @@
|
||||||
require("../../environment")
|
|
||||||
const automation = require("../index")
|
|
||||||
const usageQuota = require("../../utilities/usageQuota")
|
|
||||||
const thread = require("../thread")
|
|
||||||
const triggers = require("../triggers")
|
|
||||||
const { basicAutomation, basicTable } = require("../../tests/utilities/structures")
|
|
||||||
const { wait } = require("../../utilities")
|
|
||||||
const { makePartial } = require("../../tests/utilities")
|
|
||||||
const { cleanInputValues } = require("../automationUtils")
|
|
||||||
const setup = require("./utilities")
|
|
||||||
|
|
||||||
let workerJob
|
|
||||||
|
|
||||||
jest.mock("../../utilities/usageQuota")
|
jest.mock("../../utilities/usageQuota")
|
||||||
usageQuota.getAPIKey.mockReturnValue({ apiKey: "test" })
|
|
||||||
jest.mock("../thread")
|
jest.mock("../thread")
|
||||||
jest.spyOn(global.console, "error")
|
jest.spyOn(global.console, "error")
|
||||||
jest.mock("worker-farm", () => {
|
jest.mock("worker-farm", () => {
|
||||||
|
@ -30,6 +16,21 @@ jest.mock("worker-farm", () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
require("../../environment")
|
||||||
|
const automation = require("../index")
|
||||||
|
const usageQuota = require("../../utilities/usageQuota")
|
||||||
|
const thread = require("../thread")
|
||||||
|
const triggers = require("../triggers")
|
||||||
|
const { basicAutomation, basicTable } = require("../../tests/utilities/structures")
|
||||||
|
const { wait } = require("../../utilities")
|
||||||
|
const { makePartial } = require("../../tests/utilities")
|
||||||
|
const { cleanInputValues } = require("../automationUtils")
|
||||||
|
const setup = require("./utilities")
|
||||||
|
|
||||||
|
let workerJob
|
||||||
|
|
||||||
|
usageQuota.getAPIKey.mockReturnValue({ apiKey: "test" })
|
||||||
|
|
||||||
describe("Run through some parts of the automations system", () => {
|
describe("Run through some parts of the automations system", () => {
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
const usageQuota = require("../../utilities/usageQuota")
|
|
||||||
const env = require("../../environment")
|
|
||||||
const setup = require("./utilities")
|
|
||||||
|
|
||||||
jest.mock("../../utilities/usageQuota")
|
jest.mock("../../utilities/usageQuota")
|
||||||
|
|
||||||
|
const usageQuota = require("../../utilities/usageQuota")
|
||||||
|
const setup = require("./utilities")
|
||||||
|
|
||||||
describe("test the create row action", () => {
|
describe("test the create row action", () => {
|
||||||
let table, row
|
let table, row
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
const usageQuota = require("../../utilities/usageQuota")
|
|
||||||
const env = require("../../environment")
|
|
||||||
const setup = require("./utilities")
|
|
||||||
|
|
||||||
jest.mock("../../utilities/usageQuota")
|
jest.mock("../../utilities/usageQuota")
|
||||||
|
|
||||||
|
const usageQuota = require("../../utilities/usageQuota")
|
||||||
|
const setup = require("./utilities")
|
||||||
|
|
||||||
describe("test the delete row action", () => {
|
describe("test the delete row action", () => {
|
||||||
let table, row, inputs
|
let table, row, inputs
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
export interface Table {
|
||||||
|
_id: string
|
||||||
|
_rev?: string
|
||||||
|
type?: string
|
||||||
|
views?: {}
|
||||||
|
name?: string
|
||||||
|
primary?: string[]
|
||||||
|
schema: {
|
||||||
|
[key: string]: {
|
||||||
|
// TODO: replace with field types enum when done
|
||||||
|
type: string
|
||||||
|
fieldName?: string
|
||||||
|
name: string
|
||||||
|
constraints?: {
|
||||||
|
type?: string
|
||||||
|
email?: boolean
|
||||||
|
inclusion?: string[]
|
||||||
|
length?: {
|
||||||
|
minimum?: string | number
|
||||||
|
maximum?: string | number
|
||||||
|
}
|
||||||
|
presence?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primaryDisplay?: string
|
||||||
|
sourceId?: string
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ exports.FieldTypes = {
|
||||||
LINK: "link",
|
LINK: "link",
|
||||||
FORMULA: "formula",
|
FORMULA: "formula",
|
||||||
AUTO: "auto",
|
AUTO: "auto",
|
||||||
|
JSON: "json",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.RelationshipTypes = {
|
exports.RelationshipTypes = {
|
||||||
|
|
|
@ -57,6 +57,7 @@ const BASE_LAYOUTS = [
|
||||||
name: "Navigation Layout",
|
name: "Navigation Layout",
|
||||||
props: {
|
props: {
|
||||||
_id: "4f569166-a4f3-47ea-a09e-6d218c75586f",
|
_id: "4f569166-a4f3-47ea-a09e-6d218c75586f",
|
||||||
|
_instanceName: "Navigation Layout",
|
||||||
_component: "@budibase/standard-components/layout",
|
_component: "@budibase/standard-components/layout",
|
||||||
_children: [
|
_children: [
|
||||||
{
|
{
|
||||||
|
@ -102,6 +103,7 @@ const BASE_LAYOUTS = [
|
||||||
name: "Empty Layout",
|
name: "Empty Layout",
|
||||||
props: {
|
props: {
|
||||||
_id: "3723ffa1-f9e0-4c05-8013-98195c788ed6",
|
_id: "3723ffa1-f9e0-4c05-8013-98195c788ed6",
|
||||||
|
_instanceName: "Empty Layout",
|
||||||
_component: "@budibase/standard-components/layout",
|
_component: "@budibase/standard-components/layout",
|
||||||
_children: [
|
_children: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,15 +20,13 @@ exports.createHomeScreen = () => ({
|
||||||
_id: "ef60083f-4a02-4df3-80f3-a0d3d16847e7",
|
_id: "ef60083f-4a02-4df3-80f3-a0d3d16847e7",
|
||||||
_component: "@budibase/standard-components/heading",
|
_component: "@budibase/standard-components/heading",
|
||||||
_styles: {
|
_styles: {
|
||||||
normal: {
|
|
||||||
"text-align": "left",
|
|
||||||
},
|
|
||||||
hover: {},
|
hover: {},
|
||||||
active: {},
|
active: {},
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
text: "Welcome to your Budibase App 👋",
|
text: "Welcome to your Budibase App 👋",
|
||||||
type: "h2",
|
size: "M",
|
||||||
|
align: "left",
|
||||||
_instanceName: "Heading",
|
_instanceName: "Heading",
|
||||||
_children: [],
|
_children: [],
|
||||||
},
|
},
|
||||||
|
@ -38,6 +36,7 @@ exports.createHomeScreen = () => ({
|
||||||
hAlign: "stretch",
|
hAlign: "stretch",
|
||||||
vAlign: "top",
|
vAlign: "top",
|
||||||
size: "grow",
|
size: "grow",
|
||||||
|
gap: "M",
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
route: "/",
|
route: "/",
|
||||||
|
|
|
@ -87,33 +87,27 @@ async function getFullLinkedDocs(appId, links) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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} args.eventType states what type of change which is occurring, means this can be expanded upon in the
|
||||||
* future quite easily (all updates go through one function).
|
* future quite easily (all updates go through one function).
|
||||||
* @param {string} appId The ID of the instance in which the change is occurring.
|
* @param {string} args.appId The ID of the instance in which the change is occurring.
|
||||||
* @param {string} tableId The ID of the of the table which is being changed.
|
* @param {string} args.tableId The ID of the of the table which is being changed.
|
||||||
* @param {object|null} row The row which is changing, e.g. created, updated or deleted.
|
* @param {object|null} args.row The row which is changing, e.g. created, updated or deleted.
|
||||||
* @param {object|null} table If the table has already been retrieved this can be used to reduce database gets.
|
* @param {object|null} args.table If the table has already been retrieved this can be used to reduce database gets.
|
||||||
* @param {object|null} oldTable If the table is being updated then the old table can be provided for differencing.
|
* @param {object|null} args.oldTable If the table is being updated then the old table can be provided for differencing.
|
||||||
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the row for
|
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the row for
|
||||||
* row operations and the table for table operations.
|
* row operations and the table for table operations.
|
||||||
*/
|
*/
|
||||||
exports.updateLinks = async function ({
|
exports.updateLinks = async function (args) {
|
||||||
eventType,
|
const { eventType, appId, row, tableId, table, oldTable } = args
|
||||||
appId,
|
|
||||||
row,
|
|
||||||
tableId,
|
|
||||||
table,
|
|
||||||
oldTable,
|
|
||||||
}) {
|
|
||||||
const baseReturnObj = row == null ? table : row
|
const baseReturnObj = row == null ? table : row
|
||||||
if (appId == null) {
|
if (appId == null) {
|
||||||
throw "Cannot operate without an instance ID."
|
throw "Cannot operate without an instance ID."
|
||||||
}
|
}
|
||||||
// make sure table ID is set
|
// make sure table ID is set
|
||||||
if (tableId == null && table != null) {
|
if (tableId == null && table != null) {
|
||||||
arguments[0].tableId = table._id
|
args.tableId = table._id
|
||||||
}
|
}
|
||||||
let linkController = new LinkController(arguments[0])
|
let linkController = new LinkController(args)
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
!(await linkController.doesTableHaveLinkedFields()) &&
|
!(await linkController.doesTableHaveLinkedFields()) &&
|
||||||
|
|
|
@ -17,24 +17,20 @@ exports.createLinkView = createLinkView
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the linking documents, not the linked documents themselves.
|
* Gets the linking documents, not the linked documents themselves.
|
||||||
* @param {string} appId The instance in which we are searching for linked rows.
|
* @param {string} args.appId The instance in which we are searching for linked rows.
|
||||||
* @param {string} tableId The table which we are searching for linked rows against.
|
* @param {string} args.tableId The table which we are searching for linked rows against.
|
||||||
* @param {string|null} fieldName The name of column/field which is being altered, only looking for
|
* @param {string|null} args.fieldName The name of column/field which is being altered, only looking for
|
||||||
* linking documents that are related to it. If this is not specified then the table level will be assumed.
|
* linking documents that are related to it. If this is not specified then the table level will be assumed.
|
||||||
* @param {string|null} rowId The ID of the row which we want to find linking documents for -
|
* @param {string|null} args.rowId The ID of the row which we want to find linking documents for -
|
||||||
* if this is not specified then it will assume table or field level depending on whether the
|
* if this is not specified then it will assume table or field level depending on whether the
|
||||||
* field name has been specified.
|
* field name has been specified.
|
||||||
* @param {boolean|null} includeDocs whether to include docs in the response call, this is considerably slower so only
|
* @param {boolean|null} args.includeDocs whether to include docs in the response call, this is considerably slower so only
|
||||||
* use this if actually interested in the docs themselves.
|
* use this if actually interested in the docs themselves.
|
||||||
* @returns {Promise<object[]>} This will return an array of the linking documents that were found
|
* @returns {Promise<object[]>} This will return an array of the linking documents that were found
|
||||||
* (if any).
|
* (if any).
|
||||||
*/
|
*/
|
||||||
exports.getLinkDocuments = async function ({
|
exports.getLinkDocuments = async function (args) {
|
||||||
appId,
|
const { appId, tableId, rowId, includeDocs } = args
|
||||||
tableId,
|
|
||||||
rowId,
|
|
||||||
includeDocs,
|
|
||||||
}) {
|
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
let params
|
let params
|
||||||
if (rowId != null) {
|
if (rowId != null) {
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
exports.QUERY_TYPES = {
|
|
||||||
SQL: "sql",
|
|
||||||
JSON: "json",
|
|
||||||
FIELDS: "fields",
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.FIELD_TYPES = {
|
|
||||||
STRING: "string",
|
|
||||||
BOOLEAN: "boolean",
|
|
||||||
NUMBER: "number",
|
|
||||||
PASSWORD: "password",
|
|
||||||
LIST: "list",
|
|
||||||
OBJECT: "object",
|
|
||||||
JSON: "json",
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
const Airtable = require("airtable")
|
|
||||||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
|
||||||
|
|
||||||
const SCHEMA = {
|
|
||||||
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: {
|
|
||||||
apiKey: {
|
|
||||||
type: FIELD_TYPES.PASSWORD,
|
|
||||||
default: "enter api key",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
base: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
default: "mybase",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
create: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
fields: {
|
|
||||||
table: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
read: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
fields: {
|
|
||||||
table: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
view: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
numRecords: {
|
|
||||||
type: FIELD_TYPES.NUMBER,
|
|
||||||
default: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
fields: {
|
|
||||||
id: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
type: QUERY_TYPES.JSON,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
class AirtableIntegration {
|
|
||||||
constructor(config) {
|
|
||||||
this.config = config
|
|
||||||
this.client = new Airtable(config).base(config.base)
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(query) {
|
|
||||||
const { table, json } = query
|
|
||||||
|
|
||||||
try {
|
|
||||||
const records = await this.client(table).create([
|
|
||||||
{
|
|
||||||
fields: json,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
return records
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error writing to airtable", err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async read(query) {
|
|
||||||
try {
|
|
||||||
const records = await this.client(query.table)
|
|
||||||
.select({ maxRecords: query.numRecords || 10, view: query.view })
|
|
||||||
.firstPage()
|
|
||||||
return records.map(({ fields }) => fields)
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error writing to airtable", err)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(query) {
|
|
||||||
const { table, id, json } = query
|
|
||||||
|
|
||||||
try {
|
|
||||||
const records = await this.client(table).update([
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
fields: json,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
return records
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error writing to airtable", err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(query) {
|
|
||||||
try {
|
|
||||||
const records = await this.client(query.table).destroy(query.ids)
|
|
||||||
return records
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error writing to airtable", err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
schema: SCHEMA,
|
|
||||||
integration: AirtableIntegration,
|
|
||||||
}
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
import {
|
||||||
|
Integration,
|
||||||
|
DatasourceFieldTypes,
|
||||||
|
QueryTypes,
|
||||||
|
} from "./base/definitions"
|
||||||
|
|
||||||
|
module AirtableModule {
|
||||||
|
const Airtable = require("airtable")
|
||||||
|
|
||||||
|
interface AirtableConfig {
|
||||||
|
apiKey: string
|
||||||
|
base: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHEMA: Integration = {
|
||||||
|
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: {
|
||||||
|
apiKey: {
|
||||||
|
type: DatasourceFieldTypes.PASSWORD,
|
||||||
|
default: "enter api key",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
base: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
default: "mybase",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
create: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
fields: {
|
||||||
|
table: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
fields: {
|
||||||
|
table: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
numRecords: {
|
||||||
|
type: DatasourceFieldTypes.NUMBER,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
fields: {
|
||||||
|
id: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
type: QueryTypes.JSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class AirtableIntegration {
|
||||||
|
private config: AirtableConfig
|
||||||
|
private client: any
|
||||||
|
|
||||||
|
constructor(config: AirtableConfig) {
|
||||||
|
this.config = config
|
||||||
|
this.client = new Airtable(config).base(config.base)
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(query: { table: any; json: any }) {
|
||||||
|
const { table, json } = query
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.client(table).create([
|
||||||
|
{
|
||||||
|
fields: json,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error writing to airtable", err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(query: { table: any; numRecords: any; view: any }) {
|
||||||
|
try {
|
||||||
|
const records = await this.client(query.table)
|
||||||
|
.select({ maxRecords: query.numRecords || 10, view: query.view })
|
||||||
|
.firstPage()
|
||||||
|
// @ts-ignore
|
||||||
|
return records.map(({ fields }) => fields)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error writing to airtable", err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(query: { table: any; id: any; json: any }) {
|
||||||
|
const { table, id, json } = query
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.client(table).update([
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
fields: json,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error writing to airtable", err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(query: { table: any; ids: any }) {
|
||||||
|
try {
|
||||||
|
return await this.client(query.table).destroy(query.ids)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error writing to airtable", err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
schema: SCHEMA,
|
||||||
|
integration: AirtableIntegration,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
const { Database, aql } = require("arangojs")
|
|
||||||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
|
||||||
|
|
||||||
const SCHEMA = {
|
|
||||||
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: {
|
|
||||||
url: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
default: "http://localhost:8529",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
default: "root",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: FIELD_TYPES.PASSWORD,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
databaseName: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
default: "_system",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
collection: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
read: {
|
|
||||||
type: QUERY_TYPES.SQL,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
type: QUERY_TYPES.JSON,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArangoDBIntegration {
|
|
||||||
constructor(config) {
|
|
||||||
config.auth = {
|
|
||||||
username: config.username,
|
|
||||||
password: config.password,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.config = config
|
|
||||||
this.client = new Database(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
async read(query) {
|
|
||||||
try {
|
|
||||||
const result = await this.client.query(query.sql)
|
|
||||||
return result.all()
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error querying arangodb", err.message)
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
this.client.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(query) {
|
|
||||||
const clc = this.client.collection(this.config.collection)
|
|
||||||
try {
|
|
||||||
const result = await this.client.query(
|
|
||||||
aql`INSERT ${query.json} INTO ${clc} RETURN NEW`
|
|
||||||
)
|
|
||||||
return result.all()
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error querying arangodb", err.message)
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
this.client.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
schema: SCHEMA,
|
|
||||||
integration: ArangoDBIntegration,
|
|
||||||
}
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import {
|
||||||
|
Integration,
|
||||||
|
DatasourceFieldTypes,
|
||||||
|
QueryTypes,
|
||||||
|
} from "./base/definitions"
|
||||||
|
|
||||||
|
module ArangoModule {
|
||||||
|
const { Database, aql } = require("arangojs")
|
||||||
|
|
||||||
|
interface ArangodbConfig {
|
||||||
|
url: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
databaseName: string
|
||||||
|
collection: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHEMA: Integration = {
|
||||||
|
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: {
|
||||||
|
url: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
default: "http://localhost:8529",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
default: "root",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: DatasourceFieldTypes.PASSWORD,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
databaseName: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
default: "_system",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
collection: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
read: {
|
||||||
|
type: QueryTypes.SQL,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
type: QueryTypes.JSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArangoDBIntegration {
|
||||||
|
private config: ArangodbConfig
|
||||||
|
private client: any
|
||||||
|
|
||||||
|
constructor(config: ArangodbConfig) {
|
||||||
|
const newConfig = {
|
||||||
|
auth: {
|
||||||
|
username: config.username,
|
||||||
|
password: config.password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config = config
|
||||||
|
this.client = new Database(newConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(query: { sql: any }) {
|
||||||
|
try {
|
||||||
|
const result = await this.client.query(query.sql)
|
||||||
|
return result.all()
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error querying arangodb", err.message)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
this.client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(query: { json: any }) {
|
||||||
|
const clc = this.client.collection(this.config.collection)
|
||||||
|
try {
|
||||||
|
const result = await this.client.query(
|
||||||
|
aql`INSERT ${query.json} INTO ${clc} RETURN NEW`
|
||||||
|
)
|
||||||
|
return result.all()
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error querying arangodb", err.message)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
this.client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
schema: SCHEMA,
|
||||||
|
integration: ArangoDBIntegration,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
exports.Operation = {
|
|
||||||
CREATE: "CREATE",
|
|
||||||
READ: "READ",
|
|
||||||
UPDATE: "UPDATE",
|
|
||||||
DELETE: "DELETE",
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.SortDirection = {
|
|
||||||
ASCENDING: "ASCENDING",
|
|
||||||
DESCENDING: "DESCENDING",
|
|
||||||
}
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
export enum Operation {
|
||||||
|
CREATE = "CREATE",
|
||||||
|
READ = "READ",
|
||||||
|
UPDATE = "UPDATE",
|
||||||
|
DELETE = "DELETE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SortDirection {
|
||||||
|
ASCENDING = "ASCENDING",
|
||||||
|
DESCENDING = "DESCENDING",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum QueryTypes {
|
||||||
|
SQL = "sql",
|
||||||
|
JSON = "json",
|
||||||
|
FIELDS = "fields",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DatasourceFieldTypes {
|
||||||
|
STRING = "string",
|
||||||
|
BOOLEAN = "boolean",
|
||||||
|
NUMBER = "number",
|
||||||
|
PASSWORD = "password",
|
||||||
|
LIST = "list",
|
||||||
|
OBJECT = "object",
|
||||||
|
JSON = "json",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryDefinition {
|
||||||
|
type: QueryTypes
|
||||||
|
displayName?: string
|
||||||
|
readable?: boolean
|
||||||
|
customisable?: boolean
|
||||||
|
fields?: object
|
||||||
|
urlDisplay?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Integration {
|
||||||
|
docs: string
|
||||||
|
plus?: boolean
|
||||||
|
description: string
|
||||||
|
friendlyName: string
|
||||||
|
datasource: {}
|
||||||
|
query: {
|
||||||
|
[key: string]: QueryDefinition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchFilters {
|
||||||
|
allOr: boolean
|
||||||
|
string?: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
fuzzy?: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
range?: {
|
||||||
|
[key: string]: {
|
||||||
|
high: number | string
|
||||||
|
low: number | string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
equal?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
notEqual?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
empty?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
notEmpty?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryJson {
|
||||||
|
endpoint: {
|
||||||
|
datasourceId: string
|
||||||
|
entityId: string
|
||||||
|
operation: Operation
|
||||||
|
}
|
||||||
|
resource: {
|
||||||
|
fields: string[]
|
||||||
|
}
|
||||||
|
filters?: SearchFilters
|
||||||
|
sort?: {
|
||||||
|
[key: string]: SortDirection
|
||||||
|
}
|
||||||
|
paginate?: {
|
||||||
|
limit: number
|
||||||
|
page: string | number
|
||||||
|
}
|
||||||
|
body?: object
|
||||||
|
extra: {
|
||||||
|
idFilter?: SearchFilters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SqlQuery {
|
||||||
|
sql: string
|
||||||
|
bindings?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryOptions {
|
||||||
|
disableReturning?: boolean
|
||||||
|
}
|
|
@ -1,9 +1,21 @@
|
||||||
const { DataSourceOperation, SortDirection } = require("../../constants")
|
import { Knex, knex } from "knex"
|
||||||
|
|
||||||
const BASE_LIMIT = 5000
|
const BASE_LIMIT = 5000
|
||||||
|
import {
|
||||||
|
QueryJson,
|
||||||
|
SearchFilters,
|
||||||
|
QueryOptions,
|
||||||
|
SortDirection,
|
||||||
|
Operation,
|
||||||
|
} from "./definitions"
|
||||||
|
|
||||||
function addFilters(query, filters) {
|
function addFilters(
|
||||||
function iterate(structure, fn) {
|
query: any,
|
||||||
|
filters: SearchFilters | undefined
|
||||||
|
): Knex.QueryBuilder {
|
||||||
|
function iterate(
|
||||||
|
structure: { [key: string]: any },
|
||||||
|
fn: (key: string, value: any) => void
|
||||||
|
) {
|
||||||
for (let [key, value] of Object.entries(structure)) {
|
for (let [key, value] of Object.entries(structure)) {
|
||||||
fn(key, value)
|
fn(key, value)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +24,7 @@ function addFilters(query, filters) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
// if all or specified in filters, then everything is an or
|
// if all or specified in filters, then everything is an or
|
||||||
const allOr = !!filters.allOr
|
const allOr = filters.allOr
|
||||||
if (filters.string) {
|
if (filters.string) {
|
||||||
iterate(filters.string, (key, value) => {
|
iterate(filters.string, (key, value) => {
|
||||||
const fnc = allOr ? "orWhere" : "where"
|
const fnc = allOr ? "orWhere" : "where"
|
||||||
|
@ -55,7 +67,7 @@ function addFilters(query, filters) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCreate(knex, json, opts) {
|
function buildCreate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
||||||
const { endpoint, body } = json
|
const { endpoint, body } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query = knex(endpoint.entityId)
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
|
@ -66,9 +78,9 @@ function buildCreate(knex, json, opts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRead(knex, json, limit) {
|
function buildRead(knex: Knex, json: QueryJson, limit: number) {
|
||||||
let { endpoint, resource, filters, sort, paginate } = json
|
let { endpoint, resource, filters, sort, paginate } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query: Knex.QueryBuilder = knex(endpoint.entityId)
|
||||||
// select all if not specified
|
// select all if not specified
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
resource = { fields: [] }
|
resource = { fields: [] }
|
||||||
|
@ -90,6 +102,7 @@ function buildRead(knex, json, limit) {
|
||||||
}
|
}
|
||||||
// handle pagination
|
// handle pagination
|
||||||
if (paginate && paginate.page && paginate.limit) {
|
if (paginate && paginate.page && paginate.limit) {
|
||||||
|
// @ts-ignore
|
||||||
const page = paginate.page <= 1 ? 0 : paginate.page - 1
|
const page = paginate.page <= 1 ? 0 : paginate.page - 1
|
||||||
const offset = page * paginate.limit
|
const offset = page * paginate.limit
|
||||||
query = query.offset(offset).limit(paginate.limit)
|
query = query.offset(offset).limit(paginate.limit)
|
||||||
|
@ -101,7 +114,7 @@ function buildRead(knex, json, limit) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUpdate(knex, json, opts) {
|
function buildUpdate(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
||||||
const { endpoint, body, filters } = json
|
const { endpoint, body, filters } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query = knex(endpoint.entityId)
|
||||||
query = addFilters(query, filters)
|
query = addFilters(query, filters)
|
||||||
|
@ -113,7 +126,7 @@ function buildUpdate(knex, json, opts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDelete(knex, json, opts) {
|
function buildDelete(knex: Knex, json: QueryJson, opts: QueryOptions) {
|
||||||
const { endpoint, filters } = json
|
const { endpoint, filters } = json
|
||||||
let query = knex(endpoint.entityId)
|
let query = knex(endpoint.entityId)
|
||||||
query = addFilters(query, filters)
|
query = addFilters(query, filters)
|
||||||
|
@ -126,20 +139,19 @@ function buildDelete(knex, json, opts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SqlQueryBuilder {
|
class SqlQueryBuilder {
|
||||||
|
private readonly sqlClient: string
|
||||||
|
private readonly limit: number
|
||||||
// pass through client to get flavour of SQL
|
// pass through client to get flavour of SQL
|
||||||
constructor(client, limit = BASE_LIMIT) {
|
constructor(client: string, limit: number = BASE_LIMIT) {
|
||||||
this._client = client
|
this.sqlClient = client
|
||||||
this._limit = limit
|
this.limit = limit
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param json the input JSON structure from which an SQL query will be built.
|
* @param json the input JSON structure from which an SQL query will be built.
|
||||||
* @return {string} the operation that was found in the JSON.
|
* @return {string} the operation that was found in the JSON.
|
||||||
*/
|
*/
|
||||||
_operation(json) {
|
_operation(json: QueryJson): Operation {
|
||||||
if (!json || !json.endpoint) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return json.endpoint.operation
|
return json.endpoint.operation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,21 +161,21 @@ class SqlQueryBuilder {
|
||||||
* which for the sake of mySQL stops adding the returning statement to inserts, updates and deletes.
|
* which for the sake of mySQL stops adding the returning statement to inserts, updates and deletes.
|
||||||
* @return {{ sql: string, bindings: object }} the query ready to be passed to the driver.
|
* @return {{ sql: string, bindings: object }} the query ready to be passed to the driver.
|
||||||
*/
|
*/
|
||||||
_query(json, opts = {}) {
|
_query(json: QueryJson, opts: QueryOptions = {}) {
|
||||||
const knex = require("knex")({ client: this._client })
|
const client = knex({ client: this.sqlClient })
|
||||||
let query
|
let query
|
||||||
switch (this._operation(json)) {
|
switch (this._operation(json)) {
|
||||||
case DataSourceOperation.CREATE:
|
case Operation.CREATE:
|
||||||
query = buildCreate(knex, json, opts)
|
query = buildCreate(client, json, opts)
|
||||||
break
|
break
|
||||||
case DataSourceOperation.READ:
|
case Operation.READ:
|
||||||
query = buildRead(knex, json, this._limit, opts)
|
query = buildRead(client, json, this.limit)
|
||||||
break
|
break
|
||||||
case DataSourceOperation.UPDATE:
|
case Operation.UPDATE:
|
||||||
query = buildUpdate(knex, json, opts)
|
query = buildUpdate(client, json, opts)
|
||||||
break
|
break
|
||||||
case DataSourceOperation.DELETE:
|
case Operation.DELETE:
|
||||||
query = buildDelete(knex, json, opts)
|
query = buildDelete(client, json, opts)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw `Operation type is not supported by SQL query builder`
|
throw `Operation type is not supported by SQL query builder`
|
|
@ -1,95 +0,0 @@
|
||||||
const PouchDB = require("pouchdb")
|
|
||||||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
|
||||||
|
|
||||||
const SCHEMA = {
|
|
||||||
docs: "https://docs.couchdb.org/en/stable/",
|
|
||||||
friendlyName: "CouchDB",
|
|
||||||
description:
|
|
||||||
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.",
|
|
||||||
datasource: {
|
|
||||||
url: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
default: "http://localhost:5984",
|
|
||||||
},
|
|
||||||
database: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
create: {
|
|
||||||
type: QUERY_TYPES.JSON,
|
|
||||||
},
|
|
||||||
read: {
|
|
||||||
type: QUERY_TYPES.JSON,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
type: QUERY_TYPES.JSON,
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
fields: {
|
|
||||||
id: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
class CouchDBIntegration {
|
|
||||||
constructor(config) {
|
|
||||||
this.config = config
|
|
||||||
this.client = new PouchDB(`${config.url}/${config.database}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(query) {
|
|
||||||
try {
|
|
||||||
const result = await this.client.post(query.json)
|
|
||||||
return result
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error writing to couchDB", err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async read(query) {
|
|
||||||
try {
|
|
||||||
const result = await this.client.allDocs({
|
|
||||||
include_docs: true,
|
|
||||||
...query.json,
|
|
||||||
})
|
|
||||||
return result.rows.map(row => row.doc)
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error querying couchDB", err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(query) {
|
|
||||||
try {
|
|
||||||
const result = await this.client.put(query.json)
|
|
||||||
return result
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error updating couchDB document", err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(query) {
|
|
||||||
try {
|
|
||||||
const result = await this.client.remove(query.id)
|
|
||||||
return result
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error deleting couchDB document", err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
schema: SCHEMA,
|
|
||||||
integration: CouchDBIntegration,
|
|
||||||
}
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
import {
|
||||||
|
Integration,
|
||||||
|
DatasourceFieldTypes,
|
||||||
|
QueryTypes,
|
||||||
|
} from "./base/definitions"
|
||||||
|
|
||||||
|
module CouchDBModule {
|
||||||
|
const PouchDB = require("pouchdb")
|
||||||
|
|
||||||
|
interface CouchDBConfig {
|
||||||
|
url: string
|
||||||
|
database: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHEMA: Integration = {
|
||||||
|
docs: "https://docs.couchdb.org/en/stable/",
|
||||||
|
friendlyName: "CouchDB",
|
||||||
|
description:
|
||||||
|
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.",
|
||||||
|
datasource: {
|
||||||
|
url: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
default: "http://localhost:5984",
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
create: {
|
||||||
|
type: QueryTypes.JSON,
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: QueryTypes.JSON,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
type: QueryTypes.JSON,
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
fields: {
|
||||||
|
id: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class CouchDBIntegration {
|
||||||
|
private config: CouchDBConfig
|
||||||
|
private client: any
|
||||||
|
|
||||||
|
constructor(config: CouchDBConfig) {
|
||||||
|
this.config = config
|
||||||
|
this.client = new PouchDB(`${config.url}/${config.database}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(query: { json: object }) {
|
||||||
|
try {
|
||||||
|
return this.client.post(query.json)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error writing to couchDB", err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(query: { json: object }) {
|
||||||
|
try {
|
||||||
|
const result = await this.client.allDocs({
|
||||||
|
include_docs: true,
|
||||||
|
...query.json,
|
||||||
|
})
|
||||||
|
return result.rows.map((row: { doc: object }) => row.doc)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error querying couchDB", err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(query: { json: object }) {
|
||||||
|
try {
|
||||||
|
return this.client.put(query.json)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error updating couchDB document", err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(query: { id: string }) {
|
||||||
|
try {
|
||||||
|
return await this.client.remove(query.id)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error deleting couchDB document", err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
schema: SCHEMA,
|
||||||
|
integration: CouchDBIntegration,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,200 +0,0 @@
|
||||||
const AWS = require("aws-sdk")
|
|
||||||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
|
||||||
const { AWS_REGION } = require("../db/dynamoClient")
|
|
||||||
|
|
||||||
const SCHEMA = {
|
|
||||||
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: {
|
|
||||||
region: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
default: "us-east-1",
|
|
||||||
},
|
|
||||||
accessKeyId: {
|
|
||||||
type: FIELD_TYPES.PASSWORD,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
secretAccessKey: {
|
|
||||||
type: FIELD_TYPES.PASSWORD,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
endpoint: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: false,
|
|
||||||
default: "https://dynamodb.us-east-1.amazonaws.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
create: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
fields: {
|
|
||||||
table: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
read: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
readable: true,
|
|
||||||
fields: {
|
|
||||||
table: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
index: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scan: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
readable: true,
|
|
||||||
fields: {
|
|
||||||
table: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
index: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
get: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
readable: true,
|
|
||||||
fields: {
|
|
||||||
table: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
fields: {
|
|
||||||
table: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
fields: {
|
|
||||||
table: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
class DynamoDBIntegration {
|
|
||||||
constructor(config) {
|
|
||||||
this.config = config
|
|
||||||
this.connect()
|
|
||||||
let options = {
|
|
||||||
correctClockSkew: true,
|
|
||||||
}
|
|
||||||
if (config.endpoint) {
|
|
||||||
options.endpoint = config.endpoint
|
|
||||||
}
|
|
||||||
this.client = new AWS.DynamoDB.DocumentClient({
|
|
||||||
correctClockSkew: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
end() {
|
|
||||||
this.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
AWS.config.update(this.config)
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
AWS.config.update({
|
|
||||||
secretAccessKey: undefined,
|
|
||||||
accessKeyId: undefined,
|
|
||||||
region: AWS_REGION,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(query) {
|
|
||||||
const params = {
|
|
||||||
TableName: query.table,
|
|
||||||
...query.json,
|
|
||||||
}
|
|
||||||
return this.client.put(params).promise()
|
|
||||||
}
|
|
||||||
|
|
||||||
async read(query) {
|
|
||||||
const params = {
|
|
||||||
TableName: query.table,
|
|
||||||
...query.json,
|
|
||||||
}
|
|
||||||
if (query.index) {
|
|
||||||
params.IndexName = query.index
|
|
||||||
}
|
|
||||||
const response = await this.client.query(params).promise()
|
|
||||||
if (response.Items) {
|
|
||||||
return response.Items
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
async scan(query) {
|
|
||||||
const params = {
|
|
||||||
TableName: query.table,
|
|
||||||
...query.json,
|
|
||||||
}
|
|
||||||
if (query.index) {
|
|
||||||
params.IndexName = query.index
|
|
||||||
}
|
|
||||||
const response = await this.client.scan(params).promise()
|
|
||||||
if (response.Items) {
|
|
||||||
return response.Items
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(query) {
|
|
||||||
const params = {
|
|
||||||
TableName: query.table,
|
|
||||||
...query.json,
|
|
||||||
}
|
|
||||||
return this.client.get(params).promise()
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(query) {
|
|
||||||
const params = {
|
|
||||||
TableName: query.table,
|
|
||||||
...query.json,
|
|
||||||
}
|
|
||||||
return this.client.update(params).promise()
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(query) {
|
|
||||||
const params = {
|
|
||||||
TableName: query.table,
|
|
||||||
...query.json,
|
|
||||||
}
|
|
||||||
return this.client.delete(params).promise()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
schema: SCHEMA,
|
|
||||||
integration: DynamoDBIntegration,
|
|
||||||
}
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
import {
|
||||||
|
Integration,
|
||||||
|
DatasourceFieldTypes,
|
||||||
|
QueryTypes,
|
||||||
|
} from "./base/definitions"
|
||||||
|
|
||||||
|
module DynamoModule {
|
||||||
|
const AWS = require("aws-sdk")
|
||||||
|
const { AWS_REGION } = require("../db/dynamoClient")
|
||||||
|
|
||||||
|
interface DynamoDBConfig {
|
||||||
|
region: string
|
||||||
|
accessKeyId: string
|
||||||
|
secretAccessKey: string
|
||||||
|
endpoint: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHEMA: Integration = {
|
||||||
|
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: {
|
||||||
|
region: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
default: "us-east-1",
|
||||||
|
},
|
||||||
|
accessKeyId: {
|
||||||
|
type: DatasourceFieldTypes.PASSWORD,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
secretAccessKey: {
|
||||||
|
type: DatasourceFieldTypes.PASSWORD,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
endpoint: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: false,
|
||||||
|
default: "https://dynamodb.us-east-1.amazonaws.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
create: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
fields: {
|
||||||
|
table: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
readable: true,
|
||||||
|
fields: {
|
||||||
|
table: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scan: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
readable: true,
|
||||||
|
fields: {
|
||||||
|
table: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
get: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
readable: true,
|
||||||
|
fields: {
|
||||||
|
table: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
fields: {
|
||||||
|
table: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
fields: {
|
||||||
|
table: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class DynamoDBIntegration {
|
||||||
|
private config: DynamoDBConfig
|
||||||
|
private client: any
|
||||||
|
|
||||||
|
constructor(config: DynamoDBConfig) {
|
||||||
|
this.config = config
|
||||||
|
this.connect()
|
||||||
|
let options = {
|
||||||
|
correctClockSkew: true,
|
||||||
|
endpoint: config.endpoint ? config.endpoint : undefined,
|
||||||
|
}
|
||||||
|
this.client = new AWS.DynamoDB.DocumentClient(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
end() {
|
||||||
|
this.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
AWS.config.update(this.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
AWS.config.update({
|
||||||
|
secretAccessKey: undefined,
|
||||||
|
accessKeyId: undefined,
|
||||||
|
region: AWS_REGION,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(query: { table: string; json: object }) {
|
||||||
|
const params = {
|
||||||
|
TableName: query.table,
|
||||||
|
...query.json,
|
||||||
|
}
|
||||||
|
return this.client.put(params).promise()
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(query: { table: string; json: object; index: null | string }) {
|
||||||
|
const params = {
|
||||||
|
TableName: query.table,
|
||||||
|
IndexName: query.index ? query.index : undefined,
|
||||||
|
...query.json,
|
||||||
|
}
|
||||||
|
if (query.index) {
|
||||||
|
const response = await this.client.query(params).promise()
|
||||||
|
if (response.Items) {
|
||||||
|
return response.Items
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scan(query: { table: string; json: object; index: null | string }) {
|
||||||
|
const params = {
|
||||||
|
TableName: query.table,
|
||||||
|
IndexName: query.index ? query.index : undefined,
|
||||||
|
...query.json,
|
||||||
|
}
|
||||||
|
const response = await this.client.scan(params).promise()
|
||||||
|
if (response.Items) {
|
||||||
|
return response.Items
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(query: { table: string; json: object }) {
|
||||||
|
const params = {
|
||||||
|
TableName: query.table,
|
||||||
|
...query.json,
|
||||||
|
}
|
||||||
|
return this.client.get(params).promise()
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(query: { table: string; json: object }) {
|
||||||
|
const params = {
|
||||||
|
TableName: query.table,
|
||||||
|
...query.json,
|
||||||
|
}
|
||||||
|
return this.client.update(params).promise()
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(query: { table: string; json: object }) {
|
||||||
|
const params = {
|
||||||
|
TableName: query.table,
|
||||||
|
...query.json,
|
||||||
|
}
|
||||||
|
return this.client.delete(params).promise()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
schema: SCHEMA,
|
||||||
|
integration: DynamoDBIntegration,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,139 +0,0 @@
|
||||||
const { Client } = require("@elastic/elasticsearch")
|
|
||||||
const { QUERY_TYPES, FIELD_TYPES } = require("./Integration")
|
|
||||||
|
|
||||||
const SCHEMA = {
|
|
||||||
docs: "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: {
|
|
||||||
url: {
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
default: "http://localhost:9200",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
create: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
fields: {
|
|
||||||
index: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
read: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
fields: {
|
|
||||||
index: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
customisable: true,
|
|
||||||
fields: {
|
|
||||||
id: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
index: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
type: QUERY_TYPES.FIELDS,
|
|
||||||
fields: {
|
|
||||||
index: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: FIELD_TYPES.STRING,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
class ElasticSearchIntegration {
|
|
||||||
constructor(config) {
|
|
||||||
this.config = config
|
|
||||||
this.client = new Client({ node: config.url })
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(query) {
|
|
||||||
const { index, json } = query
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await this.client.index({
|
|
||||||
index,
|
|
||||||
body: json,
|
|
||||||
})
|
|
||||||
return result.body
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error writing to elasticsearch", err)
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
await this.client.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async read(query) {
|
|
||||||
const { index, json } = query
|
|
||||||
try {
|
|
||||||
const result = await this.client.search({
|
|
||||||
index: index,
|
|
||||||
body: json,
|
|
||||||
})
|
|
||||||
return result.body.hits.hits.map(({ _source }) => _source)
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error querying elasticsearch", err)
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
await this.client.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(query) {
|
|
||||||
const { id, index, json } = query
|
|
||||||
try {
|
|
||||||
const result = await this.client.update({
|
|
||||||
id,
|
|
||||||
index,
|
|
||||||
body: json,
|
|
||||||
})
|
|
||||||
return result.body
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error querying elasticsearch", err)
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
await this.client.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(query) {
|
|
||||||
try {
|
|
||||||
const result = await this.client.delete(query)
|
|
||||||
return result.body
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error deleting from elasticsearch", err)
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
await this.client.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
schema: SCHEMA,
|
|
||||||
integration: ElasticSearchIntegration,
|
|
||||||
}
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
import {
|
||||||
|
Integration,
|
||||||
|
DatasourceFieldTypes,
|
||||||
|
QueryTypes,
|
||||||
|
} from "./base/definitions"
|
||||||
|
|
||||||
|
module ElasticsearchModule {
|
||||||
|
const { Client } = require("@elastic/elasticsearch")
|
||||||
|
|
||||||
|
interface ElasticsearchConfig {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHEMA: Integration = {
|
||||||
|
docs: "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: {
|
||||||
|
url: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
default: "http://localhost:9200",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
create: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
fields: {
|
||||||
|
index: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
fields: {
|
||||||
|
index: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
customisable: true,
|
||||||
|
fields: {
|
||||||
|
id: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
type: QueryTypes.FIELDS,
|
||||||
|
fields: {
|
||||||
|
index: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: DatasourceFieldTypes.STRING,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class ElasticSearchIntegration {
|
||||||
|
private config: ElasticsearchConfig
|
||||||
|
private client: any
|
||||||
|
|
||||||
|
constructor(config: ElasticsearchConfig) {
|
||||||
|
this.config = config
|
||||||
|
this.client = new Client({ node: config.url })
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(query: { index: string; json: object }) {
|
||||||
|
const { index, json } = query
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.client.index({
|
||||||
|
index,
|
||||||
|
body: json,
|
||||||
|
})
|
||||||
|
return result.body
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error writing to elasticsearch", err)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
await this.client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(query: { index: string; json: object }) {
|
||||||
|
const { index, json } = query
|
||||||
|
try {
|
||||||
|
const result = await this.client.search({
|
||||||
|
index: index,
|
||||||
|
body: json,
|
||||||
|
})
|
||||||
|
return result.body.hits.hits.map(({ _source }: any) => _source)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error querying elasticsearch", err)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
await this.client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(query: { id: string; index: string; json: object }) {
|
||||||
|
const { id, index, json } = query
|
||||||
|
try {
|
||||||
|
const result = await this.client.update({
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
body: json,
|
||||||
|
})
|
||||||
|
return result.body
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error querying elasticsearch", err)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
await this.client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(query: object) {
|
||||||
|
try {
|
||||||
|
const result = await this.client.delete(query)
|
||||||
|
return result.body
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error deleting from elasticsearch", err)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
await this.client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
schema: SCHEMA,
|
||||||
|
integration: ElasticSearchIntegration,
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue