Merge branch 'linked-records' of github.com:Budibase/budibase into api-usage-tracking
This commit is contained in:
commit
106badc9c6
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.23",
|
"version": "0.1.25",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
"test:e2e:ci": "lerna run cy:ci"
|
"test:e2e:ci": "lerna run cy:ci"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome": "^1.1.8",
|
"@fortawesome/fontawesome": "^1.1.8"
|
||||||
"pouchdb-replication-stream": "^1.2.9"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -50,7 +50,6 @@ context("Create a automation", () => {
|
||||||
|
|
||||||
// Activate Automation
|
// Activate Automation
|
||||||
cy.get("[data-cy=activate-automation]").click()
|
cy.get("[data-cy=activate-automation]").click()
|
||||||
cy.contains("Add Record").should("be.visible")
|
|
||||||
cy.get(".stop-button.highlighted").should("be.visible")
|
cy.get(".stop-button.highlighted").should("be.visible")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ context("Create a Table", () => {
|
||||||
cy.createTable("dog")
|
cy.createTable("dog")
|
||||||
|
|
||||||
// Check if Table exists
|
// Check if Table exists
|
||||||
cy.get(".title").should("have.text", "dog")
|
cy.get(".title span").should("have.text", "dog")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("adds a new column to the table", () => {
|
it("adds a new column to the table", () => {
|
||||||
|
|
|
@ -50,13 +50,11 @@ context("Create a View", () => {
|
||||||
|
|
||||||
it("creates a stats calculation view based on age", () => {
|
it("creates a stats calculation view based on age", () => {
|
||||||
cy.contains("Calculate").click()
|
cy.contains("Calculate").click()
|
||||||
|
// we may reinstate this - have commented this dropdown for now as there is only one option
|
||||||
|
//cy.get(".menu-container").find("select").first().select("Statistics")
|
||||||
cy.get(".menu-container")
|
cy.get(".menu-container")
|
||||||
.find("select")
|
.find("select")
|
||||||
.first()
|
.eq(0)
|
||||||
.select("Statistics")
|
|
||||||
cy.get(".menu-container")
|
|
||||||
.find("select")
|
|
||||||
.eq(1)
|
|
||||||
.select("age")
|
.select("age")
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
cy.get("thead th div").should($headers => {
|
cy.get("thead th div").should($headers => {
|
||||||
|
|
|
@ -68,6 +68,7 @@ Cypress.Commands.add("createTable", tableName => {
|
||||||
cy.contains("Create New Table").click()
|
cy.contains("Create New Table").click()
|
||||||
cy.get(".menu-container")
|
cy.get(".menu-container")
|
||||||
.get("input")
|
.get("input")
|
||||||
|
.first()
|
||||||
.type(tableName)
|
.type(tableName)
|
||||||
|
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "0.1.23",
|
"version": "0.1.25",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -63,8 +63,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.39.0",
|
"@budibase/bbui": "^1.40.1",
|
||||||
"@budibase/client": "^0.1.23",
|
"@budibase/client": "^0.1.25",
|
||||||
"@budibase/colorpicker": "^1.0.1",
|
"@budibase/colorpicker": "^1.0.1",
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
|
|
|
@ -37,9 +37,9 @@ export default function({ componentInstanceId, screen, components, models }) {
|
||||||
.filter(isInstanceInSharedContext(walkResult))
|
.filter(isInstanceInSharedContext(walkResult))
|
||||||
.map(componentInstanceToBindable(walkResult)),
|
.map(componentInstanceToBindable(walkResult)),
|
||||||
|
|
||||||
...walkResult.target._contexts
|
...(walkResult.target?._contexts
|
||||||
.map(contextToBindables(models, walkResult))
|
.map(contextToBindables(models, walkResult))
|
||||||
.flat(),
|
.flat() ?? []),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +73,10 @@ const componentInstanceToBindable = walkResult => i => {
|
||||||
|
|
||||||
const contextToBindables = (models, walkResult) => context => {
|
const contextToBindables = (models, walkResult) => context => {
|
||||||
const contextParentPath = getParentPath(walkResult, context)
|
const contextParentPath = getParentPath(walkResult, context)
|
||||||
|
const isModel = context.model?.isModel || typeof context.model === "string"
|
||||||
|
const modelId =
|
||||||
|
typeof context.model === "string" ? context.model : context.model.modelId
|
||||||
|
const model = models.find(model => model._id === modelId)
|
||||||
|
|
||||||
const newBindable = key => ({
|
const newBindable = key => ({
|
||||||
type: "context",
|
type: "context",
|
||||||
|
@ -80,15 +84,12 @@ const contextToBindables = (models, walkResult) => context => {
|
||||||
// how the binding expression persists, and is used in the app at runtime
|
// how the binding expression persists, and is used in the app at runtime
|
||||||
runtimeBinding: `${contextParentPath}data.${key}`,
|
runtimeBinding: `${contextParentPath}data.${key}`,
|
||||||
// how the binding exressions looks to the user of the builder
|
// how the binding exressions looks to the user of the builder
|
||||||
readableBinding: `${context.instance._instanceName}.${context.model.label}.${key}`,
|
readableBinding: `${context.instance._instanceName}.${model.name}.${key}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
// see ModelViewSelect.svelte for the format of context.model
|
// see ModelViewSelect.svelte for the format of context.model
|
||||||
// ... this allows us to bind to Model scheams, or View schemas
|
// ... this allows us to bind to Model schemas, or View schemas
|
||||||
const model = models.find(m => m._id === context.model.modelId)
|
const schema = isModel ? model.schema : model.views[context.model.name].schema
|
||||||
const schema = context.model.isModel
|
|
||||||
? model.schema
|
|
||||||
: model.views[context.model.name].schema
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Object.keys(schema)
|
Object.keys(schema)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
|
|
||||||
let data = []
|
let data = []
|
||||||
|
let loading = false
|
||||||
|
|
||||||
$: title = $backendUiStore.selectedModel.name
|
$: title = $backendUiStore.selectedModel.name
|
||||||
$: schema = $backendUiStore.selectedModel.schema
|
$: schema = $backendUiStore.selectedModel.schema
|
||||||
|
@ -19,14 +20,16 @@
|
||||||
// Fetch records for specified model
|
// Fetch records for specified model
|
||||||
$: {
|
$: {
|
||||||
if ($backendUiStore.selectedView?.name?.startsWith("all_")) {
|
if ($backendUiStore.selectedView?.name?.startsWith("all_")) {
|
||||||
|
loading = true
|
||||||
api.fetchDataForView($backendUiStore.selectedView).then(records => {
|
api.fetchDataForView($backendUiStore.selectedView).then(records => {
|
||||||
data = records || []
|
data = records || []
|
||||||
|
loading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Table {title} {schema} {data} allowEditing={true}>
|
<Table {title} {schema} {data} allowEditing={true} {loading}>
|
||||||
<CreateColumnButton />
|
<CreateColumnButton />
|
||||||
{#if Object.keys(schema).length > 0}
|
{#if Object.keys(schema).length > 0}
|
||||||
<CreateRowButton />
|
<CreateRowButton />
|
||||||
|
|
|
@ -2,39 +2,33 @@
|
||||||
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
|
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
|
||||||
import Dropzone from "components/common/Dropzone.svelte"
|
import Dropzone from "components/common/Dropzone.svelte"
|
||||||
import { capitalise } from "../../../helpers"
|
import { capitalise } from "../../../helpers"
|
||||||
|
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
||||||
|
|
||||||
export let meta
|
export let meta
|
||||||
export let value = meta.type === "boolean" ? false : ""
|
export let value = meta.type === "boolean" ? false : ""
|
||||||
|
|
||||||
const type = determineInputType(meta)
|
$: type = meta.type
|
||||||
const label = capitalise(meta.name)
|
$: label = capitalise(meta.name)
|
||||||
|
|
||||||
function determineInputType(meta) {
|
|
||||||
if (meta.type === "datetime") return "date"
|
|
||||||
if (meta.type === "number") return "number"
|
|
||||||
if (meta.type === "boolean") return "checkbox"
|
|
||||||
if (meta.type === "attachment") return "file"
|
|
||||||
if (meta.type === "options") return "select"
|
|
||||||
return "text"
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === 'select'}
|
{#if type === 'options'}
|
||||||
<Select thin secondary {label} data-cy="{meta.name}-select" bind:value>
|
<Select thin secondary {label} data-cy="{meta.name}-select" bind:value>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each meta.constraints.inclusion as opt}
|
{#each meta.constraints.inclusion as opt}
|
||||||
<option value={opt}>{opt}</option>
|
<option value={opt}>{opt}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{:else if type === 'date'}
|
{:else if type === 'datetime'}
|
||||||
<DatePicker {label} bind:value />
|
<DatePicker {label} bind:value />
|
||||||
{:else if type === 'file'}
|
{:else if type === 'attachment'}
|
||||||
<div>
|
<div>
|
||||||
<Label extraSmall grey forAttr={'dropzone-label'}>{label}</Label>
|
<Label extraSmall grey forAttr={'dropzone-label'}>{label}</Label>
|
||||||
<Dropzone bind:files={value} />
|
<Dropzone bind:files={value} />
|
||||||
</div>
|
</div>
|
||||||
{:else if type === 'checkbox'}
|
{:else if type === 'boolean'}
|
||||||
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
||||||
|
{:else if type === 'link'}
|
||||||
|
<LinkedRecordSelector bind:linkedRecords={value} schema={meta} />
|
||||||
{:else}
|
{:else}
|
||||||
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
|
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, params } from "@sveltech/routify"
|
import { goto, params } from "@sveltech/routify"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
import fsort from "fast-sort"
|
import fsort from "fast-sort"
|
||||||
import getOr from "lodash/fp/getOr"
|
import getOr from "lodash/fp/getOr"
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
import ColumnHeaderPopover from "./popovers/ColumnPopover.svelte"
|
import ColumnHeaderPopover from "./popovers/ColumnPopover.svelte"
|
||||||
import EditRowPopover from "./popovers/RowPopover.svelte"
|
import EditRowPopover from "./popovers/RowPopover.svelte"
|
||||||
import CalculationPopover from "./buttons/CalculateButton.svelte"
|
import CalculationPopover from "./buttons/CalculateButton.svelte"
|
||||||
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 10
|
const ITEMS_PER_PAGE = 10
|
||||||
|
|
||||||
|
@ -23,6 +25,7 @@
|
||||||
export let data = []
|
export let data = []
|
||||||
export let title
|
export let title
|
||||||
export let allowEditing = false
|
export let allowEditing = false
|
||||||
|
export let loading = false
|
||||||
|
|
||||||
let currentPage = 0
|
let currentPage = 0
|
||||||
|
|
||||||
|
@ -50,7 +53,14 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="table-controls">
|
<div class="table-controls">
|
||||||
<h2 class="title">{title}</h2>
|
<h2 class="title">
|
||||||
|
<span>{title}</span>
|
||||||
|
{#if loading}
|
||||||
|
<div transition:fade>
|
||||||
|
<Spinner size="10" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</h2>
|
||||||
<div class="popovers">
|
<div class="popovers">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -123,6 +133,13 @@
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.title > span {
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
|
||||||
import RecordFieldControl from "../RecordFieldControl.svelte"
|
import RecordFieldControl from "../RecordFieldControl.svelte"
|
||||||
import * as api from "../api"
|
import * as api from "../api"
|
||||||
import { Modal } from "components/common/Modal"
|
import { Modal } from "components/common/Modal"
|
||||||
|
@ -44,11 +43,7 @@
|
||||||
<ErrorsBox {errors} />
|
<ErrorsBox {errors} />
|
||||||
{#each modelSchema as [key, meta]}
|
{#each modelSchema as [key, meta]}
|
||||||
<div>
|
<div>
|
||||||
{#if meta.type === 'link'}
|
<RecordFieldControl {meta} bind:value={record[key]} />
|
||||||
<LinkedRecordSelector bind:linkedRecords={record[key]} schema={meta} />
|
|
||||||
{:else}
|
|
||||||
<RecordFieldControl {meta} bind:value={record[key]} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
function saveView() {
|
function saveView() {
|
||||||
|
if (!view.calculation) view.calculation = "stats"
|
||||||
backendUiStore.actions.views.save(view)
|
backendUiStore.actions.views.save(view)
|
||||||
notifier.success(`View ${view.name} saved.`)
|
notifier.success(`View ${view.name} saved.`)
|
||||||
onClosed()
|
onClosed()
|
||||||
|
@ -34,14 +35,15 @@
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<h5>Calculate</h5>
|
<h5>Calculate</h5>
|
||||||
<div class="input-group-row">
|
<div class="input-group-row">
|
||||||
<p>The</p>
|
<!-- <p>The</p>
|
||||||
<Select secondary thin bind:value={view.calculation}>
|
<Select secondary thin bind:value={view.calculation}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each CALCULATIONS as calculation}
|
{#each CALCULATIONS as calculation}
|
||||||
<option value={calculation.key}>{calculation.name}</option>
|
<option value={calculation.key}>{calculation.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
<p>of</p>
|
<p>of</p> -->
|
||||||
|
<p>The statistics of</p>
|
||||||
<Select secondary thin bind:value={view.field}>
|
<Select secondary thin bind:value={view.field}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
|
@ -74,7 +76,7 @@
|
||||||
|
|
||||||
.input-group-row {
|
.input-group-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 30px 1fr 20px 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
$: modelOptions = $backendUiStore.models.filter(
|
$: modelOptions = $backendUiStore.models.filter(
|
||||||
model => model._id !== $backendUiStore.draftModel._id
|
model => model._id !== $backendUiStore.draftModel._id
|
||||||
)
|
)
|
||||||
|
$: required = !!field?.constraints?.presence
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
backendUiStore.update(state => {
|
backendUiStore.update(state => {
|
||||||
|
@ -53,6 +54,12 @@
|
||||||
field.type = type
|
field.type = type
|
||||||
field.constraints = constraints
|
field.constraints = constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChangeRequired(e) {
|
||||||
|
const req = e.target.checked
|
||||||
|
field.constraints.presence = req ? { allowEmpty: false } : false
|
||||||
|
required = req
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
@ -71,8 +78,8 @@
|
||||||
|
|
||||||
{#if field.type !== 'link'}
|
{#if field.type !== 'link'}
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={!field.constraints.presence.allowEmpty}
|
checked={required}
|
||||||
on:change={e => (field.constraints.presence.allowEmpty = !e.target.checked)}
|
on:change={onChangeRequired}
|
||||||
thin
|
thin
|
||||||
text="Required" />
|
text="Required" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Input, Select } from "@budibase/bbui"
|
import { Button, Input, Select, DatePicker } from "@budibase/bbui"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
@ -71,11 +71,38 @@
|
||||||
|
|
||||||
function isMultipleChoice(field) {
|
function isMultipleChoice(field) {
|
||||||
return (
|
return (
|
||||||
viewModel.schema[field].constraints &&
|
(viewModel.schema[field].constraints &&
|
||||||
viewModel.schema[field].constraints.inclusion &&
|
viewModel.schema[field].constraints.inclusion &&
|
||||||
viewModel.schema[field].constraints.inclusion.length
|
viewModel.schema[field].constraints.inclusion.length) ||
|
||||||
|
viewModel.schema[field].type === "boolean"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldOptions(field) {
|
||||||
|
return viewModel.schema[field].type === "string"
|
||||||
|
? viewModel.schema[field].constraints.inclusion
|
||||||
|
: [true, false]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDate(field) {
|
||||||
|
return viewModel.schema[field].type === "datetime"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNumber(field) {
|
||||||
|
return viewModel.schema[field].type === "number"
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldChanged = filter => ev => {
|
||||||
|
// reset if type changed
|
||||||
|
if (
|
||||||
|
filter.key &&
|
||||||
|
ev.target.value &&
|
||||||
|
viewModel.schema[filter.key].type !==
|
||||||
|
viewModel.schema[ev.target.value].type
|
||||||
|
) {
|
||||||
|
filter.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
@ -93,7 +120,11 @@
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{/if}
|
{/if}
|
||||||
<Select secondary thin bind:value={filter.key}>
|
<Select
|
||||||
|
secondary
|
||||||
|
thin
|
||||||
|
bind:value={filter.key}
|
||||||
|
on:change={fieldChanged(filter)}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<option value={field}>{field}</option>
|
<option value={field}>{field}</option>
|
||||||
|
@ -108,12 +139,25 @@
|
||||||
{#if filter.key && isMultipleChoice(filter.key)}
|
{#if filter.key && isMultipleChoice(filter.key)}
|
||||||
<Select secondary thin bind:value={filter.value}>
|
<Select secondary thin bind:value={filter.value}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each viewModel.schema[filter.key].constraints.inclusion as option}
|
{#each fieldOptions(filter.key) as option}
|
||||||
<option value={option}>{option}</option>
|
<option value={option}>{option.toString()}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
|
{:else if filter.key && isDate(filter.key)}
|
||||||
|
<DatePicker
|
||||||
|
bind:value={filter.value}
|
||||||
|
placeholder={filter.key || fields[0]} />
|
||||||
|
{:else if filter.key && isNumber(filter.key)}
|
||||||
|
<Input
|
||||||
|
thin
|
||||||
|
bind:value={filter.value}
|
||||||
|
placeholder={filter.key || fields[0]}
|
||||||
|
type="number" />
|
||||||
{:else}
|
{:else}
|
||||||
<Input thin placeholder="Value" bind:value={filter.value} />
|
<Input
|
||||||
|
thin
|
||||||
|
placeholder={filter.key || fields[0]}
|
||||||
|
bind:value={filter.value} />
|
||||||
{/if}
|
{/if}
|
||||||
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
|
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
<script>
|
||||||
|
import { Heading, Body, Button, Select } from "@budibase/bbui"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
import { FIELDS } from "constants/backend"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
const BYTES_IN_KB = 1000
|
||||||
|
const BYTES_IN_MB = 1000000
|
||||||
|
const FILE_SIZE_LIMIT = BYTES_IN_MB * 1
|
||||||
|
|
||||||
|
export let files = []
|
||||||
|
export let dataImport = {
|
||||||
|
valid: true,
|
||||||
|
schema: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
let parseResult
|
||||||
|
|
||||||
|
$: schema = parseResult && parseResult.schema
|
||||||
|
$: valid =
|
||||||
|
!schema || Object.keys(schema).every(column => schema[column].success)
|
||||||
|
$: dataImport = {
|
||||||
|
valid,
|
||||||
|
schema: buildModelSchema(schema),
|
||||||
|
path: files[0] && files[0].path,
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModelSchema(schema) {
|
||||||
|
const modelSchema = {}
|
||||||
|
for (let key in schema) {
|
||||||
|
const type = schema[key].type
|
||||||
|
|
||||||
|
if (type === "omit") continue
|
||||||
|
|
||||||
|
modelSchema[key] = {
|
||||||
|
name: key,
|
||||||
|
type,
|
||||||
|
constraints: FIELDS[type.toUpperCase()].constraints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modelSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateCSV() {
|
||||||
|
const response = await api.post("/api/models/csv/validate", {
|
||||||
|
file: files[0],
|
||||||
|
schema: schema || {},
|
||||||
|
})
|
||||||
|
|
||||||
|
parseResult = await response.json()
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
notifier.danger("CSV Invalid, please try another CSV file")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFile(evt) {
|
||||||
|
const fileArray = Array.from(evt.target.files)
|
||||||
|
const filesToProcess = fileArray.map(({ name, path, size }) => ({
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
size,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (filesToProcess.some(file => file.size >= FILE_SIZE_LIMIT)) {
|
||||||
|
notifier.danger(
|
||||||
|
`Files cannot exceed ${FILE_SIZE_LIMIT /
|
||||||
|
BYTES_IN_MB}MB. Please try again with smaller files.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files = filesToProcess
|
||||||
|
|
||||||
|
await validateCSV()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function omitColumn(columnName) {
|
||||||
|
schema[columnName].type = "omit"
|
||||||
|
await validateCSV()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTypeChange = column => evt => {
|
||||||
|
schema[column].type = evt.target.value
|
||||||
|
validateCSV()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dropzone">
|
||||||
|
<input id="file-upload" accept=".csv" type="file" on:change={handleFile} />
|
||||||
|
<label for="file-upload" class:uploaded={files[0]}>
|
||||||
|
{#if files[0]}{files[0].name}{:else}Upload{/if}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="schema-fields">
|
||||||
|
{#if schema}
|
||||||
|
{#each Object.keys(schema).filter(key => schema[key].type !== 'omit') as columnName}
|
||||||
|
<div class="field">
|
||||||
|
<span>{columnName}</span>
|
||||||
|
<Select
|
||||||
|
secondary
|
||||||
|
thin
|
||||||
|
bind:value={schema[columnName].type}
|
||||||
|
on:change={handleTypeChange(columnName)}>
|
||||||
|
<option value={'string'}>Text</option>
|
||||||
|
<option value={'number'}>Number</option>
|
||||||
|
<option value={'datetime'}>Date</option>
|
||||||
|
</Select>
|
||||||
|
<span class="field-status" class:error={!schema[columnName].success}>
|
||||||
|
{schema[columnName].success ? 'Success' : 'Failure'}
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
class="omit-button ri-close-circle-fill"
|
||||||
|
on:click={() => omitColumn(columnName)} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dropzone {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-status {
|
||||||
|
color: var(--green);
|
||||||
|
justify-self: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
|
transition: all 0.2s ease 0s;
|
||||||
|
display: inline-flex;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
min-width: auto;
|
||||||
|
outline: none;
|
||||||
|
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
line-height: normal;
|
||||||
|
border: var(--border-transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.omit-button {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: var(--grey-7);
|
||||||
|
cursor: pointer;
|
||||||
|
justify-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,23 +2,30 @@
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { Popover, Button, Icon, Input, Select } from "@budibase/bbui"
|
import { Popover, Button, Icon, Input, Select, Label } from "@budibase/bbui"
|
||||||
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
import TableDataImport from "../TableDataImport.svelte"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
let dropdown
|
let dropdown
|
||||||
let name
|
let name
|
||||||
|
let dataImport
|
||||||
|
let loading
|
||||||
|
|
||||||
async function saveTable() {
|
async function saveTable() {
|
||||||
|
loading = true
|
||||||
const model = await backendUiStore.actions.models.save({
|
const model = await backendUiStore.actions.models.save({
|
||||||
name,
|
name,
|
||||||
schema: {},
|
schema: dataImport.schema || {},
|
||||||
|
dataImport,
|
||||||
})
|
})
|
||||||
notifier.success(`Table ${name} created successfully.`)
|
notifier.success(`Table ${name} created successfully.`)
|
||||||
$goto(`./model/${model._id}`)
|
$goto(`./model/${model._id}`)
|
||||||
|
analytics.captureEvent("Table Created", { name })
|
||||||
name = ""
|
name = ""
|
||||||
dropdown.hide()
|
dropdown.hide()
|
||||||
analytics.captureEvent("Table Created", { name })
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClosed = () => {
|
const onClosed = () => {
|
||||||
|
@ -38,9 +45,21 @@
|
||||||
thin
|
thin
|
||||||
label="Table Name"
|
label="Table Name"
|
||||||
bind:value={name} />
|
bind:value={name} />
|
||||||
|
<div>
|
||||||
|
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
||||||
|
<TableDataImport bind:dataImport />
|
||||||
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
<Button primary on:click={saveTable}>Save</Button>
|
<Button
|
||||||
|
disabled={!name || (dataImport && !dataImport.valid)}
|
||||||
|
primary
|
||||||
|
on:click={saveTable}>
|
||||||
|
<span style={`margin-right: ${loading ? '10px' : 0};`}>Save</span>
|
||||||
|
{#if loading}
|
||||||
|
<Spinner size="10" />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { Select, Label } from "@budibase/bbui"
|
import { Select, Label, Multiselect } from "@budibase/bbui"
|
||||||
import { capitalise } from "../../helpers"
|
import { capitalise } from "../../helpers"
|
||||||
import MultiSelect from "components/common/MultiSelect.svelte"
|
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let linkedRecords = []
|
export let linkedRecords = []
|
||||||
|
@ -19,8 +18,7 @@
|
||||||
async function fetchRecords(linkedModelId) {
|
async function fetchRecords(linkedModelId) {
|
||||||
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
|
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
|
||||||
const response = await api.get(FETCH_RECORDS_URL)
|
const response = await api.get(FETCH_RECORDS_URL)
|
||||||
const result = await response.json()
|
return await response.json()
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrettyName(record) {
|
function getPrettyName(record) {
|
||||||
|
@ -37,15 +35,14 @@
|
||||||
</Label>
|
</Label>
|
||||||
{:else}
|
{:else}
|
||||||
{#await promise then records}
|
{#await promise then records}
|
||||||
<MultiSelect
|
<Multiselect
|
||||||
thin
|
|
||||||
secondary
|
secondary
|
||||||
bind:value={linkedRecords}
|
bind:value={linkedRecords}
|
||||||
{label}
|
{label}
|
||||||
placeholder="Choose an option">
|
placeholder="Choose some options">
|
||||||
{#each records as record}
|
{#each records as record}
|
||||||
<option value={record._id}>{getPrettyName(record)}</option>
|
<option value={record._id}>{getPrettyName(record)}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</MultiSelect>
|
</Multiselect>
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,278 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { fly } from "svelte/transition"
|
|
||||||
import { Label } from "@budibase/bbui"
|
|
||||||
const xPath =
|
|
||||||
"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
|
|
||||||
|
|
||||||
export let value = []
|
|
||||||
export let label
|
|
||||||
|
|
||||||
let placeholder = "Type to search"
|
|
||||||
let options = []
|
|
||||||
let optionsVisible = false
|
|
||||||
let selected = {}
|
|
||||||
let first = true
|
|
||||||
let slot
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const domOptions = Array.from(slot.querySelectorAll("option"))
|
|
||||||
options = domOptions.map(option => ({
|
|
||||||
value: option.value,
|
|
||||||
name: option.textContent,
|
|
||||||
}))
|
|
||||||
if (value) {
|
|
||||||
options.forEach(option => {
|
|
||||||
if (value.includes(option.value)) {
|
|
||||||
selected[option.value] = option
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
first = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Keep value up to date with selected options
|
|
||||||
$: {
|
|
||||||
if (!first) {
|
|
||||||
value = Object.values(selected).map(option => option.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function add(token) {
|
|
||||||
selected[token.value] = token
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(value) {
|
|
||||||
const { [value]: val, ...rest } = selected
|
|
||||||
selected = rest
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeAll() {
|
|
||||||
selected = []
|
|
||||||
}
|
|
||||||
|
|
||||||
function showOptions(show) {
|
|
||||||
optionsVisible = show
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
showOptions(!optionsVisible)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOptionMousedown(e) {
|
|
||||||
const value = e.target.dataset.value
|
|
||||||
if (value == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (selected[value]) {
|
|
||||||
remove(value)
|
|
||||||
} else {
|
|
||||||
add(options.filter(option => option.value === value)[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{#if label}
|
|
||||||
<Label extraSmall grey>{label}</Label>
|
|
||||||
{/if}
|
|
||||||
<div class="multiselect">
|
|
||||||
<div class="tokens-wrapper">
|
|
||||||
<div
|
|
||||||
class="tokens"
|
|
||||||
class:optionsVisible
|
|
||||||
on:click|self={handleClick}
|
|
||||||
class:empty={!value || !value.length}>
|
|
||||||
{#each Object.values(selected) as option}
|
|
||||||
<div class="token" data-id={option.value} on:click|self={handleClick}>
|
|
||||||
<span>{option.name}</span>
|
|
||||||
<div
|
|
||||||
class="token-remove"
|
|
||||||
title="Remove {option.name}"
|
|
||||||
on:click={() => remove(option.value)}>
|
|
||||||
<svg
|
|
||||||
class="icon-clear"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24">
|
|
||||||
<path d={xPath} />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{#if !value || !value.length} {/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<select bind:this={slot} type="multiple" class="hidden">
|
|
||||||
<slot />
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{#if optionsVisible}
|
|
||||||
<div class="options-overlay" on:click|self={() => showOptions(false)} />
|
|
||||||
<ul
|
|
||||||
class="options"
|
|
||||||
transition:fly={{ duration: 200, y: 5 }}
|
|
||||||
on:mousedown|preventDefault={handleOptionMousedown}>
|
|
||||||
{#each options as option}
|
|
||||||
<li class:selected={selected[option.value]} data-value={option.value}>
|
|
||||||
{option.name}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
{#if !options.length}
|
|
||||||
<li class="no-results">No results</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.multiselect {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.multiselect:hover {
|
|
||||||
border-bottom-color: hsl(0, 0%, 50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tokens-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
flex: 0 1 auto;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tokens {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
width: 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
background-color: var(--grey-2);
|
|
||||||
border-radius: var(--border-radius-m);
|
|
||||||
padding: 0 var(--spacing-m) calc(var(--spacing-m) - var(--spacing-xs))
|
|
||||||
calc(var(--spacing-m) / 2);
|
|
||||||
border: var(--border-transparent);
|
|
||||||
}
|
|
||||||
.tokens:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.tokens::after {
|
|
||||||
background: none repeat scroll 0 0 transparent;
|
|
||||||
bottom: -1px;
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
height: 2px;
|
|
||||||
left: 50%;
|
|
||||||
position: absolute;
|
|
||||||
transition: width 0.3s ease 0s, left 0.3s ease 0s;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
.tokens.optionsVisible {
|
|
||||||
border: var(--border-blue);
|
|
||||||
}
|
|
||||||
.tokens.empty {
|
|
||||||
padding: var(--spacing-m);
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.tokens::after {
|
|
||||||
width: 100%;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.token {
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
align-items: center;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: var(--border-radius-l);
|
|
||||||
display: flex;
|
|
||||||
margin: calc(var(--spacing-m) - var(--spacing-xs)) 0 0
|
|
||||||
calc(var(--spacing-m) / 2);
|
|
||||||
max-height: 1.3rem;
|
|
||||||
padding: var(--spacing-xs) var(--spacing-s);
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.token span {
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.token-remove {
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--grey-4);
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--white);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
height: 1rem;
|
|
||||||
width: 1rem;
|
|
||||||
margin: calc(-1 * var(--spacing-xs)) 0 calc(-1 * var(--spacing-xs))
|
|
||||||
var(--spacing-xs);
|
|
||||||
}
|
|
||||||
.token-remove:hover {
|
|
||||||
background-color: var(--grey-5);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-clear path {
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options {
|
|
||||||
z-index: 2;
|
|
||||||
left: 0;
|
|
||||||
list-style: none;
|
|
||||||
margin-block-end: 0;
|
|
||||||
margin-block-start: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-inline-start: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + 1px);
|
|
||||||
width: calc(100% - 4px);
|
|
||||||
border: var(--border-dark);
|
|
||||||
border-radius: var(--border-radius-m);
|
|
||||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
margin-top: var(--spacing-xs);
|
|
||||||
padding: var(--spacing-s) 0;
|
|
||||||
background-color: white;
|
|
||||||
max-height: 200px;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
background-color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
}
|
|
||||||
li.selected {
|
|
||||||
background-color: var(--blue);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
li:not(.selected):hover {
|
|
||||||
background-color: var(--grey-1);
|
|
||||||
}
|
|
||||||
li.no-results:hover {
|
|
||||||
background-color: white;
|
|
||||||
cursor: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -51,6 +51,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Button } from "@budibase/bbui"
|
import { Input } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import posthog from "posthog-js"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
let keys = { budibase: "", sendGrid: "" }
|
let keys = { budibase: "" }
|
||||||
|
|
||||||
async function updateKey([key, value]) {
|
async function updateKey([key, value]) {
|
||||||
if (key === "budibase") {
|
if (key === "budibase") {
|
||||||
|
@ -40,12 +38,6 @@
|
||||||
edit
|
edit
|
||||||
value={keys.budibase}
|
value={keys.budibase}
|
||||||
label="Budibase API Key" />
|
label="Budibase API Key" />
|
||||||
<Input
|
|
||||||
on:save={e => updateKey(['sendgrid', e.detail])}
|
|
||||||
thin
|
|
||||||
edit
|
|
||||||
value={keys.sendgrid}
|
|
||||||
label="Sendgrid API Key" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -146,7 +146,7 @@
|
||||||
})
|
})
|
||||||
const appJson = await appResp.json()
|
const appJson = await appResp.json()
|
||||||
analytics.captureEvent("App Created", {
|
analytics.captureEvent("App Created", {
|
||||||
name,
|
name: $createAppStore.values.applicationName,
|
||||||
appId: appJson._id,
|
appId: appJson._id,
|
||||||
template,
|
template,
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
on:input={() => (blurred.api = true)}
|
on:input={() => (blurred.api = true)}
|
||||||
label="API Key"
|
label="API Key"
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
placeholder="Enter your API Key"
|
placeholder="Use command-V to paste your API Key"
|
||||||
type="password"
|
type="password"
|
||||||
error={blurred.api && validationErrors.apiKey} />
|
error={blurred.api && validationErrors.apiKey} />
|
||||||
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>
|
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
type="pasword"
|
type="password"
|
||||||
error={blurred.password && validationErrors.password} />
|
error={blurred.password && validationErrors.password} />
|
||||||
<Select label="Access Level" secondary name="accessLevelId">
|
<Select label="Access Level" secondary name="accessLevelId">
|
||||||
<option value="ADMIN">Admin</option>
|
<option value="ADMIN">Admin</option>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
function handleSelected(selected) {
|
function handleSelected(selected) {
|
||||||
dispatch("change", selected)
|
dispatch("change", selected)
|
||||||
dropdown.hide()
|
dropdownRight.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const models = $backendUiStore.models.map(m => ({
|
const models = $backendUiStore.models.map(m => ({
|
||||||
|
|
|
@ -6,7 +6,7 @@ export const FIELDS = {
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
length: {},
|
length: {},
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OPTIONS: {
|
OPTIONS: {
|
||||||
|
@ -25,7 +25,7 @@ export const FIELDS = {
|
||||||
type: "number",
|
type: "number",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "number",
|
type: "number",
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
|
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -35,7 +35,7 @@ export const FIELDS = {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DATETIME: {
|
DATETIME: {
|
||||||
|
@ -45,7 +45,7 @@ export const FIELDS = {
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
length: {},
|
length: {},
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
datetime: {
|
datetime: {
|
||||||
latest: "",
|
latest: "",
|
||||||
earliest: "",
|
earliest: "",
|
||||||
|
@ -58,7 +58,7 @@ export const FIELDS = {
|
||||||
type: "attachment",
|
type: "attachment",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array",
|
type: "array",
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
LINK: {
|
LINK: {
|
||||||
|
|
|
@ -709,10 +709,10 @@
|
||||||
lodash "^4.17.13"
|
lodash "^4.17.13"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@budibase/bbui@^1.39.0":
|
"@budibase/bbui@^1.40.1":
|
||||||
version "1.39.0"
|
version "1.40.1"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.39.0.tgz#7d3e259a60a0b4602f3d2da3452679d91a591fdd"
|
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.40.1.tgz#7ebfd52b4da822312d3395447a4f73caa41f8014"
|
||||||
integrity sha512-9IL5Lw488sdYCa9mjHHdrap11VqW6wQHNcNTL8fFHaWNzummtlaUlVMScs9cunYgsR/L4NCgH0zSFdP0RnrUqw==
|
integrity sha512-0t5Makyn5jOURKZIQPvd+8G4m6ps4GyfLUkAz5rKJSnAQSAgLiuZ+RihcEReDEJK8tnfW7h2echJTffJduQRRQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
sirv-cli "^0.4.6"
|
sirv-cli "^0.4.6"
|
||||||
svelte-flatpickr "^2.4.0"
|
svelte-flatpickr "^2.4.0"
|
||||||
|
@ -5844,10 +5844,6 @@ svelte-portal@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742"
|
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742"
|
||||||
|
|
||||||
svelte-simple-modal@^0.4.2:
|
|
||||||
version "0.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/svelte-simple-modal/-/svelte-simple-modal-0.4.2.tgz#2cfe26ec8c0760b89813d65dfee836399620d6b2"
|
|
||||||
|
|
||||||
svelte@^3.24.1:
|
svelte@^3.24.1:
|
||||||
version "3.25.1"
|
version "3.25.1"
|
||||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4"
|
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "budibase",
|
"name": "budibase",
|
||||||
"version": "0.1.23",
|
"version": "0.1.25",
|
||||||
"description": "Budibase CLI",
|
"description": "Budibase CLI",
|
||||||
"repository": "https://github.com/Budibase/Budibase",
|
"repository": "https://github.com/Budibase/Budibase",
|
||||||
"homepage": "https://www.budibase.com",
|
"homepage": "https://www.budibase.com",
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/server": "^0.1.23",
|
"@budibase/server": "^0.1.25",
|
||||||
"@inquirer/password": "^0.0.6-alpha.0",
|
"@inquirer/password": "^0.0.6-alpha.0",
|
||||||
"chalk": "^2.4.2",
|
"chalk": "^2.4.2",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "0.1.23",
|
"version": "0.1.25",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
"module": "dist/budibase-client.esm.mjs",
|
"module": "dist/budibase-client.esm.mjs",
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"version": "0.1.23",
|
"version": "0.1.25",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/electron.js",
|
"main": "src/electron.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -42,13 +42,14 @@
|
||||||
"author": "Michael Shanks",
|
"author": "Michael Shanks",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/client": "^0.1.23",
|
"@budibase/client": "^0.1.25",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"@sendgrid/mail": "^7.1.1",
|
"@sendgrid/mail": "^7.1.1",
|
||||||
"@sentry/node": "^5.19.2",
|
"@sentry/node": "^5.19.2",
|
||||||
"aws-sdk": "^2.767.0",
|
"aws-sdk": "^2.767.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chmodr": "^1.2.0",
|
"chmodr": "^1.2.0",
|
||||||
|
"csvtojson": "^2.0.10",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"download": "^8.0.0",
|
"download": "^8.0.0",
|
||||||
"electron-is-dev": "^1.2.0",
|
"electron-is-dev": "^1.2.0",
|
||||||
|
@ -71,6 +72,7 @@
|
||||||
"pino-pretty": "^4.0.0",
|
"pino-pretty": "^4.0.0",
|
||||||
"pouchdb": "^7.2.1",
|
"pouchdb": "^7.2.1",
|
||||||
"pouchdb-all-dbs": "^1.0.2",
|
"pouchdb-all-dbs": "^1.0.2",
|
||||||
|
"pouchdb-replication-stream": "^1.2.9",
|
||||||
"sharp": "^0.26.0",
|
"sharp": "^0.26.0",
|
||||||
"squirrelly": "^7.5.0",
|
"squirrelly": "^7.5.0",
|
||||||
"tar-fs": "^2.1.0",
|
"tar-fs": "^2.1.0",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
|
const { join } = require("../../utilities/sanitisedPath")
|
||||||
const readline = require("readline")
|
const readline = require("readline")
|
||||||
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
||||||
const ENV_FILE_PATH = "/.env"
|
const ENV_FILE_PATH = "/.env"
|
||||||
|
@ -7,7 +8,6 @@ exports.fetch = async function(ctx) {
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
budibase: process.env.BUDIBASE_API_KEY,
|
budibase: process.env.BUDIBASE_API_KEY,
|
||||||
sendgrid: process.env.SENDGRID_API_KEY,
|
|
||||||
userId: process.env.USERID_API_KEY,
|
userId: process.env.USERID_API_KEY,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,9 @@ exports.update = async function(ctx) {
|
||||||
async function updateValues([key, value]) {
|
async function updateValues([key, value]) {
|
||||||
let newContent = ""
|
let newContent = ""
|
||||||
let keyExists = false
|
let keyExists = false
|
||||||
|
let envPath = join(budibaseAppsDir(), ENV_FILE_PATH)
|
||||||
const readInterface = readline.createInterface({
|
const readInterface = readline.createInterface({
|
||||||
input: fs.createReadStream(`${budibaseAppsDir()}/${ENV_FILE_PATH}`),
|
input: fs.createReadStream(envPath),
|
||||||
output: process.stdout,
|
output: process.stdout,
|
||||||
console: false,
|
console: false,
|
||||||
})
|
})
|
||||||
|
@ -48,6 +49,6 @@ async function updateValues([key, value]) {
|
||||||
// Add API Key if it doesn't exist in the file at all
|
// Add API Key if it doesn't exist in the file at all
|
||||||
newContent = `${newContent}\n${key}=${value}`
|
newContent = `${newContent}\n${key}=${value}`
|
||||||
}
|
}
|
||||||
fs.writeFileSync(`${budibaseAppsDir()}/${ENV_FILE_PATH}`, newContent)
|
fs.writeFileSync(envPath, newContent)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@ const ClientDb = require("../../db/clientDb")
|
||||||
const { getPackageForBuilder, buildPage } = require("../../utilities/builder")
|
const { getPackageForBuilder, buildPage } = require("../../utilities/builder")
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const instanceController = require("./instance")
|
const instanceController = require("./instance")
|
||||||
const { resolve, join } = require("path")
|
|
||||||
const { copy, exists, readFile, writeFile } = require("fs-extra")
|
const { copy, exists, readFile, writeFile } = require("fs-extra")
|
||||||
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
||||||
const sqrl = require("squirrelly")
|
const sqrl = require("squirrelly")
|
||||||
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||||
const fs = require("fs-extra")
|
const fs = require("fs-extra")
|
||||||
|
const { join, resolve } = require("../../utilities/sanitisedPath")
|
||||||
const { promisify } = require("util")
|
const { promisify } = require("util")
|
||||||
const chmodr = require("chmodr")
|
const chmodr = require("chmodr")
|
||||||
const { generateAppID, getAppParams } = require("../../db/utils")
|
const { generateAppID, getAppParams } = require("../../db/utils")
|
||||||
|
@ -116,7 +116,7 @@ exports.delete = async function(ctx) {
|
||||||
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
|
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
|
||||||
const app = await db.get(ctx.params.applicationId)
|
const app = await db.get(ctx.params.applicationId)
|
||||||
const result = await db.remove(app)
|
const result = await db.remove(app)
|
||||||
await fs.rmdir(`${budibaseAppsDir()}/${ctx.params.applicationId}`, {
|
await fs.rmdir(join(budibaseAppsDir(), ctx.params.applicationId), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const ClientDb = require("../../db/clientDb")
|
const ClientDb = require("../../db/clientDb")
|
||||||
const { resolve, join } = require("path")
|
const { resolve, join } = require("../../utilities/sanitisedPath")
|
||||||
const {
|
const {
|
||||||
budibaseTempDir,
|
budibaseTempDir,
|
||||||
budibaseAppsDir,
|
budibaseAppsDir,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
|
const { join } = require("../../../utilities/sanitisedPath")
|
||||||
const AWS = require("aws-sdk")
|
const AWS = require("aws-sdk")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
|
||||||
|
@ -108,7 +109,7 @@ exports.uploadAppAssets = async function({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const appAssetsPath = `${budibaseAppsDir()}/${appId}/public`
|
const appAssetsPath = join(budibaseAppsDir(), appId, "public")
|
||||||
|
|
||||||
const appPages = fs.readdirSync(appAssetsPath)
|
const appPages = fs.readdirSync(appAssetsPath)
|
||||||
|
|
||||||
|
@ -116,7 +117,7 @@ exports.uploadAppAssets = async function({
|
||||||
|
|
||||||
for (let page of appPages) {
|
for (let page of appPages) {
|
||||||
// Upload HTML, CSS and JS for each page of the web app
|
// Upload HTML, CSS and JS for each page of the web app
|
||||||
walkDir(`${appAssetsPath}/${page}`, function(filePath) {
|
walkDir(join(appAssetsPath, page), function(filePath) {
|
||||||
const appAssetUpload = prepareUploadForS3({
|
const appAssetUpload = prepareUploadForS3({
|
||||||
file: {
|
file: {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
|
|
|
@ -3,6 +3,7 @@ const CouchDB = require("../../db")
|
||||||
const client = require("../../db/clientDb")
|
const client = require("../../db/clientDb")
|
||||||
const newid = require("../../db/newid")
|
const newid = require("../../db/newid")
|
||||||
const { createLinkView } = require("../../db/linkedRecords")
|
const { createLinkView } = require("../../db/linkedRecords")
|
||||||
|
const { join } = require("../../utilities/sanitisedPath")
|
||||||
const { downloadTemplate } = require("../../utilities/templates")
|
const { downloadTemplate } = require("../../utilities/templates")
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
|
@ -39,7 +40,9 @@ exports.create = async function(ctx) {
|
||||||
// replicate the template data to the instance DB
|
// replicate the template data to the instance DB
|
||||||
if (template) {
|
if (template) {
|
||||||
const templatePath = await downloadTemplate(...template.key.split("/"))
|
const templatePath = await downloadTemplate(...template.key.split("/"))
|
||||||
const dbDumpReadStream = fs.createReadStream(`${templatePath}/db/dump.txt`)
|
const dbDumpReadStream = fs.createReadStream(
|
||||||
|
join(templatePath, "db", "dump.txt")
|
||||||
|
)
|
||||||
const { ok } = await db.load(dbDumpReadStream)
|
const { ok } = await db.load(dbDumpReadStream)
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
ctx.throw(500, "Error loading database dump from template.")
|
ctx.throw(500, "Error loading database dump from template.")
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const linkRecords = require("../../db/linkedRecords")
|
const linkRecords = require("../../db/linkedRecords")
|
||||||
|
const csvParser = require("../../utilities/csvParser")
|
||||||
const {
|
const {
|
||||||
getRecordParams,
|
getRecordParams,
|
||||||
getModelParams,
|
getModelParams,
|
||||||
generateModelID,
|
generateModelID,
|
||||||
|
generateRecordID,
|
||||||
} = require("../../db/utils")
|
} = require("../../db/utils")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
|
@ -24,11 +26,12 @@ exports.find = async function(ctx) {
|
||||||
exports.save = async function(ctx) {
|
exports.save = async function(ctx) {
|
||||||
const instanceId = ctx.user.instanceId
|
const instanceId = ctx.user.instanceId
|
||||||
const db = new CouchDB(instanceId)
|
const db = new CouchDB(instanceId)
|
||||||
|
const { dataImport, ...rest } = ctx.request.body
|
||||||
const modelToSave = {
|
const modelToSave = {
|
||||||
type: "model",
|
type: "model",
|
||||||
_id: generateModelID(),
|
_id: generateModelID(),
|
||||||
views: {},
|
views: {},
|
||||||
...ctx.request.body,
|
...rest,
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the model obj had an _id then it will have been retrieved
|
// if the model obj had an _id then it will have been retrieved
|
||||||
|
@ -81,6 +84,19 @@ exports.save = async function(ctx) {
|
||||||
|
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
ctx.eventEmitter.emitModel(`model:save`, instanceId, modelToSave)
|
ctx.eventEmitter.emitModel(`model:save`, instanceId, modelToSave)
|
||||||
|
|
||||||
|
if (dataImport && dataImport.path) {
|
||||||
|
// Populate the table with records imported from CSV in a bulk update
|
||||||
|
const data = await csvParser.transform(dataImport)
|
||||||
|
|
||||||
|
for (let row of data) {
|
||||||
|
row._id = generateRecordID(modelToSave._id)
|
||||||
|
row.modelId = modelToSave._id
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.bulkDocs(data)
|
||||||
|
}
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.message = `Model ${ctx.request.body.name} saved successfully.`
|
ctx.message = `Model ${ctx.request.body.name} saved successfully.`
|
||||||
ctx.body = modelToSave
|
ctx.body = modelToSave
|
||||||
|
@ -116,3 +132,12 @@ exports.destroy = async function(ctx) {
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.message = `Model ${ctx.params.modelId} deleted.`
|
ctx.message = `Model ${ctx.params.modelId} deleted.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.validateCSVSchema = async function(ctx) {
|
||||||
|
const { file, schema = {} } = ctx.request.body
|
||||||
|
const result = await csvParser.parse(file.path, schema)
|
||||||
|
ctx.body = {
|
||||||
|
schema: result,
|
||||||
|
path: file.path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ const CouchDB = require("../../db")
|
||||||
const validateJs = require("validate.js")
|
const validateJs = require("validate.js")
|
||||||
const linkRecords = require("../../db/linkedRecords")
|
const linkRecords = require("../../db/linkedRecords")
|
||||||
const { getRecordParams, generateRecordID } = require("../../db/utils")
|
const { getRecordParams, generateRecordID } = require("../../db/utils")
|
||||||
|
const { cloneDeep } = require("lodash")
|
||||||
|
|
||||||
const MODEL_VIEW_BEGINS_WITH = "all_model:"
|
const MODEL_VIEW_BEGINS_WITH = "all_model:"
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ exports.patch = async function(ctx) {
|
||||||
let record = await db.get(ctx.params.id)
|
let record = await db.get(ctx.params.id)
|
||||||
const model = await db.get(record.modelId)
|
const model = await db.get(record.modelId)
|
||||||
const patchfields = ctx.request.body
|
const patchfields = ctx.request.body
|
||||||
|
record = coerceRecordValues(record, model)
|
||||||
|
|
||||||
for (let key of Object.keys(patchfields)) {
|
for (let key of Object.keys(patchfields)) {
|
||||||
if (!model.schema[key]) continue
|
if (!model.schema[key]) continue
|
||||||
|
@ -75,6 +77,8 @@ exports.save = async function(ctx) {
|
||||||
|
|
||||||
const model = await db.get(record.modelId)
|
const model = await db.get(record.modelId)
|
||||||
|
|
||||||
|
record = coerceRecordValues(record, model)
|
||||||
|
|
||||||
const validateResult = await validate({
|
const validateResult = await validate({
|
||||||
record,
|
record,
|
||||||
model,
|
model,
|
||||||
|
@ -285,3 +289,50 @@ exports.fetchEnrichedRecord = async function(ctx) {
|
||||||
ctx.body = record
|
ctx.body = record
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coerceRecordValues(rec, model) {
|
||||||
|
const record = cloneDeep(rec)
|
||||||
|
for (let [key, value] of Object.entries(record)) {
|
||||||
|
const field = model.schema[key]
|
||||||
|
if (!field) continue
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
|
||||||
|
record[key] = TYPE_TRANSFORM_MAP[field.type][value]
|
||||||
|
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
|
||||||
|
record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_TRANSFORM_MAP = {
|
||||||
|
string: {
|
||||||
|
"": "",
|
||||||
|
[null]: "",
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
"": null,
|
||||||
|
[null]: null,
|
||||||
|
[undefined]: undefined,
|
||||||
|
parse: n => parseFloat(n),
|
||||||
|
},
|
||||||
|
datetime: {
|
||||||
|
"": null,
|
||||||
|
[undefined]: undefined,
|
||||||
|
[null]: null,
|
||||||
|
},
|
||||||
|
attachment: {
|
||||||
|
"": [],
|
||||||
|
[null]: [],
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
|
boolean: {
|
||||||
|
"": null,
|
||||||
|
[null]: null,
|
||||||
|
[undefined]: undefined,
|
||||||
|
true: true,
|
||||||
|
false: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const send = require("koa-send")
|
const send = require("koa-send")
|
||||||
const { resolve, join } = require("path")
|
const { resolve, join } = require("../../utilities/sanitisedPath")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const fs = require("fs-extra")
|
const fs = require("fs-extra")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const CouchDB = require("../../../db")
|
const CouchDB = require("../../../db")
|
||||||
const viewTemplate = require("./viewBuilder")
|
const viewTemplate = require("./viewBuilder")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const path = require("path")
|
const { join } = require("../../../utilities/sanitisedPath")
|
||||||
const os = require("os")
|
const os = require("os")
|
||||||
const exporters = require("./exporters")
|
const exporters = require("./exporters")
|
||||||
const { fetchView } = require("../record")
|
const { fetchView } = require("../record")
|
||||||
|
@ -99,7 +99,8 @@ const controller = {
|
||||||
const exporter = exporters[format]
|
const exporter = exporters[format]
|
||||||
const exportedFile = exporter(headers, ctx.body)
|
const exportedFile = exporter(headers, ctx.body)
|
||||||
const filename = `${view.name}.${format}`
|
const filename = `${view.name}.${format}`
|
||||||
fs.writeFileSync(path.join(os.tmpdir(), filename), exportedFile)
|
fs.writeFileSync(join(os.tmpdir(), filename), exportedFile)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
url: `/api/views/export/download/${filename}`,
|
url: `/api/views/export/download/${filename}`,
|
||||||
name: view.name,
|
name: view.name,
|
||||||
|
@ -109,7 +110,7 @@ const controller = {
|
||||||
const filename = ctx.params.fileName
|
const filename = ctx.params.fileName
|
||||||
|
|
||||||
ctx.attachment(filename)
|
ctx.attachment(filename)
|
||||||
ctx.body = fs.createReadStream(path.join(os.tmpdir(), filename))
|
ctx.body = fs.createReadStream(join(os.tmpdir(), filename))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,8 +61,11 @@ function parseFilterExpression(filters) {
|
||||||
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
|
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
const value =
|
||||||
|
typeof filter.value == "string" ? `"${filter.value}"` : filter.value
|
||||||
|
|
||||||
expression.push(
|
expression.push(
|
||||||
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} "${filter.value}"`
|
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} ${value}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,11 @@ router
|
||||||
modelController.find
|
modelController.find
|
||||||
)
|
)
|
||||||
.post("/api/models", authorized(BUILDER), usage, modelController.save)
|
.post("/api/models", authorized(BUILDER), usage, modelController.save)
|
||||||
|
.post(
|
||||||
|
"/api/models/csv/validate",
|
||||||
|
authorized(BUILDER),
|
||||||
|
modelController.validateCSVSchema
|
||||||
|
)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/models/:modelId/:revId",
|
"/api/models/:modelId/:revId",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
|
|
|
@ -49,13 +49,13 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
||||||
key: "name",
|
key: "name",
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
|
|
@ -180,7 +180,7 @@ describe("/models", () => {
|
||||||
key: "name",
|
key: "name",
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe("/records", () => {
|
||||||
|
|
||||||
const createRecord = async r =>
|
const createRecord = async r =>
|
||||||
await request
|
await request
|
||||||
.post(`/api/${model._id}/records`)
|
.post(`/api/${r ? r.modelId : record.modelId}/records`)
|
||||||
.send(r || record)
|
.send(r || record)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
|
@ -152,6 +152,95 @@ describe("/records", () => {
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(404)
|
.expect(404)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("record values are coerced", async () => {
|
||||||
|
const str = {type:"string", constraints: { type: "string", presence: false }}
|
||||||
|
const attachment = {type:"attachment", constraints: { type: "array", presence: false }}
|
||||||
|
const bool = {type:"boolean", constraints: { type: "boolean", presence: false }}
|
||||||
|
const number = {type:"number", constraints: { type: "number", presence: false }}
|
||||||
|
const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }}
|
||||||
|
|
||||||
|
model = await createModel(request, app._id, instance._id, {
|
||||||
|
name: "TestModel2",
|
||||||
|
type: "model",
|
||||||
|
key: "name",
|
||||||
|
schema: {
|
||||||
|
name: str,
|
||||||
|
stringUndefined: str,
|
||||||
|
stringNull: str,
|
||||||
|
stringString: str,
|
||||||
|
numberEmptyString: number,
|
||||||
|
numberNull: number,
|
||||||
|
numberUndefined: number,
|
||||||
|
numberString: number,
|
||||||
|
datetimeEmptyString: datetime,
|
||||||
|
datetimeNull: datetime,
|
||||||
|
datetimeUndefined: datetime,
|
||||||
|
datetimeString: datetime,
|
||||||
|
datetimeDate: datetime,
|
||||||
|
boolNull: bool,
|
||||||
|
boolEmpty: bool,
|
||||||
|
boolUndefined: bool,
|
||||||
|
boolString: bool,
|
||||||
|
boolBool: bool,
|
||||||
|
attachmentNull : attachment,
|
||||||
|
attachmentUndefined : attachment,
|
||||||
|
attachmentEmpty : attachment,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
record = {
|
||||||
|
name: "Test Record",
|
||||||
|
stringUndefined: undefined,
|
||||||
|
stringNull: null,
|
||||||
|
stringString: "i am a string",
|
||||||
|
numberEmptyString: "",
|
||||||
|
numberNull: null,
|
||||||
|
numberUndefined: undefined,
|
||||||
|
numberString: "123",
|
||||||
|
numberNumber: 123,
|
||||||
|
datetimeEmptyString: "",
|
||||||
|
datetimeNull: null,
|
||||||
|
datetimeUndefined: undefined,
|
||||||
|
datetimeString: "1984-04-20T00:00:00.000Z",
|
||||||
|
datetimeDate: new Date("1984-04-20"),
|
||||||
|
boolNull: null,
|
||||||
|
boolEmpty: "",
|
||||||
|
boolUndefined: undefined,
|
||||||
|
boolString: "true",
|
||||||
|
boolBool: true,
|
||||||
|
modelId: model._id,
|
||||||
|
attachmentNull : null,
|
||||||
|
attachmentUndefined : undefined,
|
||||||
|
attachmentEmpty : "",
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = (await createRecord(record)).body._id
|
||||||
|
|
||||||
|
const saved = (await loadRecord(id)).body
|
||||||
|
|
||||||
|
expect(saved.stringUndefined).toBe(undefined)
|
||||||
|
expect(saved.stringNull).toBe("")
|
||||||
|
expect(saved.stringString).toBe("i am a string")
|
||||||
|
expect(saved.numberEmptyString).toBe(null)
|
||||||
|
expect(saved.numberNull).toBe(null)
|
||||||
|
expect(saved.numberUndefined).toBe(undefined)
|
||||||
|
expect(saved.numberString).toBe(123)
|
||||||
|
expect(saved.numberNumber).toBe(123)
|
||||||
|
expect(saved.datetimeEmptyString).toBe(null)
|
||||||
|
expect(saved.datetimeNull).toBe(null)
|
||||||
|
expect(saved.datetimeUndefined).toBe(undefined)
|
||||||
|
expect(saved.datetimeString).toBe(new Date(record.datetimeString).toISOString())
|
||||||
|
expect(saved.datetimeDate).toBe(record.datetimeDate.toISOString())
|
||||||
|
expect(saved.boolNull).toBe(null)
|
||||||
|
expect(saved.boolEmpty).toBe(null)
|
||||||
|
expect(saved.boolUndefined).toBe(undefined)
|
||||||
|
expect(saved.boolString).toBe(true)
|
||||||
|
expect(saved.boolBool).toBe(true)
|
||||||
|
expect(saved.attachmentNull).toEqual([])
|
||||||
|
expect(saved.attachmentUndefined).toBe(undefined)
|
||||||
|
expect(saved.attachmentEmpty).toEqual([])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("patch", () => {
|
describe("patch", () => {
|
||||||
|
|
|
@ -69,13 +69,13 @@ describe("/views", () => {
|
||||||
filters: [],
|
filters: [],
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string"
|
type: "string"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string"
|
type: "string"
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@ const createUser = require("./steps/createUser")
|
||||||
const environment = require("../environment")
|
const environment = require("../environment")
|
||||||
const download = require("download")
|
const download = require("download")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const path = require("path")
|
const { join } = require("../utilities/sanitisedPath")
|
||||||
const os = require("os")
|
const os = require("os")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const Sentry = require("@sentry/node")
|
const Sentry = require("@sentry/node")
|
||||||
|
@ -43,7 +43,7 @@ async function downloadPackage(name, version, bundleName) {
|
||||||
`${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`,
|
`${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`,
|
||||||
AUTOMATION_DIRECTORY
|
AUTOMATION_DIRECTORY
|
||||||
)
|
)
|
||||||
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
|
return require(join(AUTOMATION_DIRECTORY, bundleName))
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.getAction = async function(actionName) {
|
module.exports.getAction = async function(actionName) {
|
||||||
|
@ -57,7 +57,7 @@ module.exports.getAction = async function(actionName) {
|
||||||
const pkg = MANIFEST.packages[actionName]
|
const pkg = MANIFEST.packages[actionName]
|
||||||
const bundleName = buildBundleName(pkg.stepId, pkg.version)
|
const bundleName = buildBundleName(pkg.stepId, pkg.version)
|
||||||
try {
|
try {
|
||||||
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
|
return require(join(AUTOMATION_DIRECTORY, bundleName))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return downloadPackage(pkg.stepId, pkg.version, bundleName)
|
return downloadPackage(pkg.stepId, pkg.version, bundleName)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ module.exports.getAction = async function(actionName) {
|
||||||
module.exports.init = async function() {
|
module.exports.init = async function() {
|
||||||
// set defaults
|
// set defaults
|
||||||
if (!AUTOMATION_DIRECTORY) {
|
if (!AUTOMATION_DIRECTORY) {
|
||||||
AUTOMATION_DIRECTORY = path.join(os.homedir(), DEFAULT_DIRECTORY)
|
AUTOMATION_DIRECTORY = join(os.homedir(), DEFAULT_DIRECTORY)
|
||||||
}
|
}
|
||||||
if (!AUTOMATION_BUCKET) {
|
if (!AUTOMATION_BUCKET) {
|
||||||
AUTOMATION_BUCKET = DEFAULT_BUCKET
|
AUTOMATION_BUCKET = DEFAULT_BUCKET
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
const environment = require("../../environment")
|
|
||||||
const sgMail = require("@sendgrid/mail")
|
|
||||||
sgMail.setApiKey(environment.SENDGRID_API_KEY)
|
|
||||||
|
|
||||||
module.exports.definition = {
|
module.exports.definition = {
|
||||||
description: "Send an email",
|
description: "Send an email",
|
||||||
tagline: "Send email to {{inputs.to}}",
|
tagline: "Send email to {{inputs.to}}",
|
||||||
|
@ -13,6 +9,10 @@ module.exports.definition = {
|
||||||
schema: {
|
schema: {
|
||||||
inputs: {
|
inputs: {
|
||||||
properties: {
|
properties: {
|
||||||
|
apiKey: {
|
||||||
|
type: "string",
|
||||||
|
title: "SendGrid API key",
|
||||||
|
},
|
||||||
to: {
|
to: {
|
||||||
type: "string",
|
type: "string",
|
||||||
title: "Send To",
|
title: "Send To",
|
||||||
|
@ -49,6 +49,8 @@ module.exports.definition = {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.run = async function({ inputs }) {
|
module.exports.run = async function({ inputs }) {
|
||||||
|
const sgMail = require("@sendgrid/mail")
|
||||||
|
sgMail.setApiKey(inputs.apiKey)
|
||||||
const msg = {
|
const msg = {
|
||||||
to: inputs.to,
|
to: inputs.to,
|
||||||
from: inputs.from,
|
from: inputs.from,
|
||||||
|
|
|
@ -2,6 +2,7 @@ const PouchDB = require("pouchdb")
|
||||||
const replicationStream = require("pouchdb-replication-stream")
|
const replicationStream = require("pouchdb-replication-stream")
|
||||||
const allDbs = require("pouchdb-all-dbs")
|
const allDbs = require("pouchdb-all-dbs")
|
||||||
const { budibaseAppsDir } = require("../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../utilities/budibaseDir")
|
||||||
|
const { sanitise } = require("../utilities/sanitisedPath")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const COUCH_DB_URL = env.COUCH_DB_URL || `leveldb://${budibaseAppsDir()}/.data/`
|
const COUCH_DB_URL = env.COUCH_DB_URL || `leveldb://${budibaseAppsDir()}/.data/`
|
||||||
|
@ -26,4 +27,10 @@ const Pouch = PouchDB.defaults(POUCH_DB_DEFAULTS)
|
||||||
|
|
||||||
allDbs(Pouch)
|
allDbs(Pouch)
|
||||||
|
|
||||||
module.exports = Pouch
|
function PouchWrapper(instance) {
|
||||||
|
Pouch.apply(this, [sanitise(instance)])
|
||||||
|
}
|
||||||
|
|
||||||
|
PouchWrapper.prototype = Object.create(Pouch.prototype)
|
||||||
|
|
||||||
|
module.exports = PouchWrapper
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { app, BrowserWindow, shell, dialog } = require("electron")
|
const { app, BrowserWindow, shell, dialog } = require("electron")
|
||||||
const { join } = require("path")
|
const { join } = require("./utilities/sanitisedPath")
|
||||||
const isDev = require("electron-is-dev")
|
const isDev = require("electron-is-dev")
|
||||||
const { autoUpdater } = require("electron-updater")
|
const { autoUpdater } = require("electron-updater")
|
||||||
const unhandled = require("electron-unhandled")
|
const unhandled = require("electron-unhandled")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { resolve, join } = require("path")
|
const { resolve, join } = require("./utilities/sanitisedPath")
|
||||||
const { homedir } = require("os")
|
const { homedir } = require("os")
|
||||||
const { app } = require("electron")
|
const { app } = require("electron")
|
||||||
const fixPath = require("fix-path")
|
const fixPath = require("fix-path")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { join } = require("path")
|
const { join } = require("./sanitisedPath")
|
||||||
const { homedir, tmpdir } = require("os")
|
const { homedir, tmpdir } = require("os")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ const {
|
||||||
readFile,
|
readFile,
|
||||||
writeJSON,
|
writeJSON,
|
||||||
} = require("fs-extra")
|
} = require("fs-extra")
|
||||||
const { join, resolve } = require("path")
|
const { join, resolve } = require("../sanitisedPath")
|
||||||
const sqrl = require("squirrelly")
|
const sqrl = require("squirrelly")
|
||||||
const { convertCssToFiles } = require("./convertCssToFiles")
|
const { convertCssToFiles } = require("./convertCssToFiles")
|
||||||
const publicPath = require("./publicPath")
|
const publicPath = require("./publicPath")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const crypto = require("crypto")
|
const crypto = require("crypto")
|
||||||
const { ensureDir, emptyDir, writeFile } = require("fs-extra")
|
const { ensureDir, emptyDir, writeFile } = require("fs-extra")
|
||||||
const { join } = require("path")
|
const { join } = require("../sanitisedPath")
|
||||||
|
|
||||||
module.exports.convertCssToFiles = async (publicPagePath, pkg) => {
|
module.exports.convertCssToFiles = async (publicPagePath, pkg) => {
|
||||||
const cssDir = join(publicPagePath, "css")
|
const cssDir = join(publicPagePath, "css")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { readJSON, readdir } = require("fs-extra")
|
const { readJSON, readdir } = require("fs-extra")
|
||||||
const { join } = require("path")
|
const { join } = require("../sanitisedPath")
|
||||||
|
|
||||||
module.exports = async appPath => {
|
module.exports = async appPath => {
|
||||||
const pages = {}
|
const pages = {}
|
||||||
|
|
|
@ -8,7 +8,8 @@ const {
|
||||||
unlink,
|
unlink,
|
||||||
rmdir,
|
rmdir,
|
||||||
} = require("fs-extra")
|
} = require("fs-extra")
|
||||||
const { join, dirname, resolve } = require("path")
|
const { join, resolve } = require("../sanitisedPath")
|
||||||
|
const { dirname } = require("path")
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
|
|
||||||
const buildPage = require("./buildPage")
|
const buildPage = require("./buildPage")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const { appPackageFolder } = require("../createAppPackage")
|
const { appPackageFolder } = require("../createAppPackage")
|
||||||
const { readJSON, readdir, stat } = require("fs-extra")
|
const { readJSON, readdir, stat } = require("fs-extra")
|
||||||
const { join } = require("path")
|
const { join } = require("../sanitisedPath")
|
||||||
const { keyBy } = require("lodash/fp")
|
const { keyBy } = require("lodash/fp")
|
||||||
|
|
||||||
module.exports = async (config, appname, pagename) => {
|
module.exports = async (config, appname, pagename) => {
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
const { join } = require("path")
|
const { join } = require("../sanitisedPath")
|
||||||
|
|
||||||
module.exports = (appPath, pageName) => join(appPath, "public", pageName)
|
module.exports = (appPath, pageName) => join(appPath, "public", pageName)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { resolve } = require("path")
|
const { resolve } = require("./sanitisedPath")
|
||||||
const { cwd } = require("process")
|
const { cwd } = require("process")
|
||||||
const stream = require("stream")
|
const stream = require("stream")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
const csv = require("csvtojson")
|
||||||
|
|
||||||
|
const VALIDATORS = {
|
||||||
|
string: () => true,
|
||||||
|
number: attribute => !isNaN(Number(attribute)),
|
||||||
|
datetime: attribute => !isNaN(new Date(attribute).getTime()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const PARSERS = {
|
||||||
|
datetime: attribute => new Date(attribute).toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse(path, parsers) {
|
||||||
|
const result = csv().fromFile(path)
|
||||||
|
|
||||||
|
const schema = {}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
result.on("header", headers => {
|
||||||
|
for (let header of headers) {
|
||||||
|
schema[header] = {
|
||||||
|
type: parsers[header] ? parsers[header].type : "string",
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
result.fromFile(path).subscribe(row => {
|
||||||
|
// For each CSV row parse all the columns that need parsed
|
||||||
|
for (let key in parsers) {
|
||||||
|
if (!schema[key] || schema[key].success) {
|
||||||
|
// get the validator for the column type
|
||||||
|
const validator = VALIDATORS[parsers[key].type]
|
||||||
|
|
||||||
|
try {
|
||||||
|
// allow null/undefined values
|
||||||
|
schema[key].success = !row[key] || validator(row[key])
|
||||||
|
} catch (err) {
|
||||||
|
schema[key].success = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
result.on("done", error => {
|
||||||
|
if (error) {
|
||||||
|
console.error(error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(schema)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transform({ schema, path }) {
|
||||||
|
const colParser = {}
|
||||||
|
|
||||||
|
for (let key in schema) {
|
||||||
|
colParser[key] = PARSERS[schema[key].type] || schema[key].type
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = await csv({ colParser }).fromFile(path)
|
||||||
|
return json
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error transforming CSV to JSON for data import`, err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parse,
|
||||||
|
transform,
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
const { exists, readFile, writeFile, ensureDir } = require("fs-extra")
|
const { exists, readFile, writeFile, ensureDir } = require("fs-extra")
|
||||||
const { join, resolve } = require("path")
|
const { join, resolve } = require("./sanitisedPath")
|
||||||
const Sqrl = require("squirrelly")
|
const Sqrl = require("squirrelly")
|
||||||
const uuid = require("uuid")
|
const uuid = require("uuid")
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const regex = new RegExp(/:(?![\\/])/g)
|
||||||
|
|
||||||
|
function sanitiseArgs(args) {
|
||||||
|
let sanitised = []
|
||||||
|
for (let arg of args) {
|
||||||
|
sanitised.push(arg.replace(regex, ""))
|
||||||
|
}
|
||||||
|
return sanitised
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exactly the same as path.join but creates a sanitised path.
|
||||||
|
* @param args Any number of string arguments to add to a path
|
||||||
|
* @returns {string} The final path ready to use
|
||||||
|
*/
|
||||||
|
exports.join = function(...args) {
|
||||||
|
return path.join(...sanitiseArgs(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exactly the same as path.resolve but creates a sanitised path.
|
||||||
|
* @param args Any number of string arguments to add to a path
|
||||||
|
* @returns {string} The final path ready to use
|
||||||
|
*/
|
||||||
|
exports.resolve = function(...args) {
|
||||||
|
return path.resolve(...sanitiseArgs(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitise a single string
|
||||||
|
* @param string input string to sanitise
|
||||||
|
* @returns {string} the final sanitised string
|
||||||
|
*/
|
||||||
|
exports.sanitise = function(string) {
|
||||||
|
return sanitiseArgs([string])[0]
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require("path")
|
|
||||||
const fs = require("fs-extra")
|
const fs = require("fs-extra")
|
||||||
|
const { join } = require("./sanitisedPath")
|
||||||
const os = require("os")
|
const os = require("os")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const stream = require("stream")
|
const stream = require("stream")
|
||||||
|
@ -27,10 +27,10 @@ exports.downloadTemplate = async function(type, name) {
|
||||||
await streamPipeline(
|
await streamPipeline(
|
||||||
response.body,
|
response.body,
|
||||||
zlib.Unzip(),
|
zlib.Unzip(),
|
||||||
tar.extract(path.join(budibaseAppsDir(), "templates", type))
|
tar.extract(join(budibaseAppsDir(), "templates", type))
|
||||||
)
|
)
|
||||||
|
|
||||||
return path.join(budibaseAppsDir(), "templates", type, name)
|
return join(budibaseAppsDir(), "templates", type, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.exportTemplateFromApp = async function({
|
exports.exportTemplateFromApp = async function({
|
||||||
|
@ -39,15 +39,17 @@ exports.exportTemplateFromApp = async function({
|
||||||
instanceId,
|
instanceId,
|
||||||
}) {
|
}) {
|
||||||
// Copy frontend files
|
// Copy frontend files
|
||||||
const appToExport = path.join(os.homedir(), ".budibase", appId, "pages")
|
const appToExport = join(os.homedir(), ".budibase", appId, "pages")
|
||||||
const templatesDir = path.join(os.homedir(), ".budibase", "templates")
|
const templatesDir = join(os.homedir(), ".budibase", "templates")
|
||||||
fs.ensureDirSync(templatesDir)
|
fs.ensureDirSync(templatesDir)
|
||||||
|
|
||||||
const templateOutputPath = path.join(templatesDir, templateName)
|
const templateOutputPath = join(templatesDir, templateName)
|
||||||
fs.copySync(appToExport, `${templateOutputPath}/pages`)
|
fs.copySync(appToExport, join(templateOutputPath, "pages"))
|
||||||
|
|
||||||
fs.ensureDirSync(path.join(templateOutputPath, "db"))
|
fs.ensureDirSync(join(templateOutputPath, "db"))
|
||||||
const writeStream = fs.createWriteStream(`${templateOutputPath}/db/dump.txt`)
|
const writeStream = fs.createWriteStream(
|
||||||
|
join(templateOutputPath, "db", "dump.txt")
|
||||||
|
)
|
||||||
|
|
||||||
// perform couch dump
|
// perform couch dump
|
||||||
const instanceDb = new CouchDB(instanceId)
|
const instanceDb = new CouchDB(instanceId)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`CSV Parser transformation transforms a CSV file into JSON 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"Address": "5 Sesame Street",
|
||||||
|
"Age": 4324,
|
||||||
|
"Name": "Bert",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"Address": "1 World Trade Center",
|
||||||
|
"Age": 34,
|
||||||
|
"Name": "Ernie",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"Address": "44 Second Avenue",
|
||||||
|
"Age": 23423,
|
||||||
|
"Name": "Big Bird",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
|
@ -0,0 +1,108 @@
|
||||||
|
const csvParser = require("../csvParser");
|
||||||
|
|
||||||
|
const CSV_PATH = __dirname + "/test.csv";
|
||||||
|
|
||||||
|
const SCHEMAS = {
|
||||||
|
VALID: {
|
||||||
|
Age: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
INVALID: {
|
||||||
|
Address: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Age: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IGNORE: {
|
||||||
|
Address: {
|
||||||
|
type: "omit",
|
||||||
|
},
|
||||||
|
Age: {
|
||||||
|
type: "omit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BROKEN: {
|
||||||
|
Address: {
|
||||||
|
type: "datetime",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("CSV Parser", () => {
|
||||||
|
describe("parsing", () => {
|
||||||
|
it("returns status and types for a valid CSV transformation", async () => {
|
||||||
|
expect(
|
||||||
|
await csvParser.parse(CSV_PATH, SCHEMAS.VALID)
|
||||||
|
).toEqual({
|
||||||
|
Address: {
|
||||||
|
success: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
Age: {
|
||||||
|
success: true,
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
success: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns status and types for an invalid CSV transformation", async () => {
|
||||||
|
expect(
|
||||||
|
await csvParser.parse(CSV_PATH, SCHEMAS.INVALID)
|
||||||
|
).toEqual({
|
||||||
|
Address: {
|
||||||
|
success: false,
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Age: {
|
||||||
|
success: true,
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
success: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("transformation", () => {
|
||||||
|
it("transforms a CSV file into JSON", async () => {
|
||||||
|
expect(
|
||||||
|
await csvParser.transform({
|
||||||
|
schema: SCHEMAS.VALID,
|
||||||
|
path: CSV_PATH,
|
||||||
|
})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("transforms a CSV file into JSON ignoring certain fields", async () => {
|
||||||
|
expect(
|
||||||
|
await csvParser.transform({
|
||||||
|
schema: SCHEMAS.IGNORE,
|
||||||
|
path: CSV_PATH,
|
||||||
|
})
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
Name: "Bert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Ernie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Big Bird"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error on invalid schema", async () => {
|
||||||
|
await expect(csvParser.transform({ schema: SCHEMAS.BROKEN, path: CSV_PATH })).rejects.toThrow()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
"Name","Age","Address"
|
||||||
|
"Bert","4324","5 Sesame Street"
|
||||||
|
"Ernie","34","1 World Trade Center"
|
||||||
|
"Big Bird","23423","44 Second Avenue"
|
|
|
@ -172,10 +172,10 @@
|
||||||
lodash "^4.17.13"
|
lodash "^4.17.13"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@budibase/client@^0.1.23":
|
"@budibase/client@^0.1.25":
|
||||||
version "0.1.23"
|
version "0.1.25"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.23.tgz#d72d2b26ff3a2d99f2b6c1b71020b1136880937d"
|
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.25.tgz#f08c4a614f9018eb0f0faa6d20bb05f7a3215c70"
|
||||||
integrity sha512-pZdwdCq5kKLZfZYxasIHBNnqu3BFFrqJLxXMFs0K9ddCVZ0UNons59nn73nFGbeRgNVdWp6yyW71XyMQr8NOEw==
|
integrity sha512-vZ0cqJwLYcs7MHihFnJO3qOe7qxibnB4Va1+IYNfnPc9kcxy4KvfQxCx/G/DDxP9CXfEvsguy9ymzR3RUAvBHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-equal "^2.0.1"
|
deep-equal "^2.0.1"
|
||||||
mustache "^4.0.1"
|
mustache "^4.0.1"
|
||||||
|
@ -1079,9 +1079,10 @@ bluebird-lst@^1.0.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
bluebird "^3.5.5"
|
bluebird "^3.5.5"
|
||||||
|
|
||||||
bluebird@^3.5.5:
|
bluebird@^3.5.1, bluebird@^3.5.5:
|
||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
boolean@^3.0.0, boolean@^3.0.1:
|
boolean@^3.0.0, boolean@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
|
@ -1618,6 +1619,15 @@ cssstyle@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cssom "0.3.x"
|
cssom "0.3.x"
|
||||||
|
|
||||||
|
csvtojson@^2.0.10:
|
||||||
|
version "2.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/csvtojson/-/csvtojson-2.0.10.tgz#11e7242cc630da54efce7958a45f443210357574"
|
||||||
|
integrity sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ==
|
||||||
|
dependencies:
|
||||||
|
bluebird "^3.5.1"
|
||||||
|
lodash "^4.17.3"
|
||||||
|
strip-bom "^2.0.0"
|
||||||
|
|
||||||
dashdash@^1.12.0:
|
dashdash@^1.12.0:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||||
|
@ -3455,6 +3465,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||||
|
|
||||||
|
is-utf8@^0.2.0:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
||||||
|
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
|
||||||
|
|
||||||
is-weakmap@^2.0.1:
|
is-weakmap@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
|
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
|
||||||
|
@ -4347,6 +4362,13 @@ lie@3.0.4:
|
||||||
inline-process-browser "^1.0.0"
|
inline-process-browser "^1.0.0"
|
||||||
unreachable-branch-transform "^0.3.0"
|
unreachable-branch-transform "^0.3.0"
|
||||||
|
|
||||||
|
lie@3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||||
|
integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
|
||||||
|
dependencies:
|
||||||
|
immediate "~3.0.5"
|
||||||
|
|
||||||
lines-and-columns@^1.1.6:
|
lines-and-columns@^1.1.6:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||||
|
@ -4409,6 +4431,11 @@ lodash.once@^4.0.0:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||||
|
|
||||||
|
lodash.pick@^4.0.0:
|
||||||
|
version "4.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
|
||||||
|
integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
|
||||||
|
|
||||||
lodash.sortby@^4.7.0:
|
lodash.sortby@^4.7.0:
|
||||||
version "4.7.0"
|
version "4.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
|
@ -4417,6 +4444,11 @@ lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
|
||||||
version "4.17.19"
|
version "4.17.19"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||||
|
|
||||||
|
lodash@^4.17.3:
|
||||||
|
version "4.17.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||||
|
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||||
|
|
||||||
loose-envify@^1.0.0:
|
loose-envify@^1.0.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
@ -4671,6 +4703,16 @@ natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
|
|
||||||
|
ndjson@^1.4.3:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ndjson/-/ndjson-1.5.0.tgz#ae603b36b134bcec347b452422b0bf98d5832ec8"
|
||||||
|
integrity sha1-rmA7NrE0vOw0e0UkIrC/mNWDLsg=
|
||||||
|
dependencies:
|
||||||
|
json-stringify-safe "^5.0.1"
|
||||||
|
minimist "^1.2.0"
|
||||||
|
split2 "^2.1.0"
|
||||||
|
through2 "^2.0.3"
|
||||||
|
|
||||||
negotiator@0.6.2:
|
negotiator@0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||||
|
@ -5164,6 +5206,14 @@ posix-character-classes@^0.1.0:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
||||||
|
|
||||||
|
pouch-stream@^0.4.0:
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouch-stream/-/pouch-stream-0.4.1.tgz#0c6d8475c9307677627991a2f079b301c3b89bdd"
|
||||||
|
integrity sha1-DG2EdckwdndieZGi8HmzAcO4m90=
|
||||||
|
dependencies:
|
||||||
|
inherits "^2.0.1"
|
||||||
|
readable-stream "^1.0.27-1"
|
||||||
|
|
||||||
pouchdb-adapter-leveldb-core@7.2.1:
|
pouchdb-adapter-leveldb-core@7.2.1:
|
||||||
version "7.2.1"
|
version "7.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-adapter-leveldb-core/-/pouchdb-adapter-leveldb-core-7.2.1.tgz#71bf2a05755689e2b05e78e796003a18ebf65a69"
|
resolved "https://registry.yarnpkg.com/pouchdb-adapter-leveldb-core/-/pouchdb-adapter-leveldb-core-7.2.1.tgz#71bf2a05755689e2b05e78e796003a18ebf65a69"
|
||||||
|
@ -5251,6 +5301,26 @@ pouchdb-promise@5.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
lie "3.0.4"
|
lie "3.0.4"
|
||||||
|
|
||||||
|
pouchdb-promise@^6.0.4:
|
||||||
|
version "6.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-promise/-/pouchdb-promise-6.4.3.tgz#74516f4acf74957b54debd0fb2c0e5b5a68ca7b3"
|
||||||
|
integrity sha512-ruJaSFXwzsxRHQfwNHjQfsj58LBOY1RzGzde4PM5CWINZwFjCQAhZwfMrch2o/0oZT6d+Xtt0HTWhq35p3b0qw==
|
||||||
|
dependencies:
|
||||||
|
lie "3.1.1"
|
||||||
|
|
||||||
|
pouchdb-replication-stream@^1.2.9:
|
||||||
|
version "1.2.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/pouchdb-replication-stream/-/pouchdb-replication-stream-1.2.9.tgz#aa4fa5d8f52df4825392f18e07c7e11acffc650a"
|
||||||
|
integrity sha1-qk+l2PUt9IJTkvGOB8fhGs/8ZQo=
|
||||||
|
dependencies:
|
||||||
|
argsarray "0.0.1"
|
||||||
|
inherits "^2.0.3"
|
||||||
|
lodash.pick "^4.0.0"
|
||||||
|
ndjson "^1.4.3"
|
||||||
|
pouch-stream "^0.4.0"
|
||||||
|
pouchdb-promise "^6.0.4"
|
||||||
|
through2 "^2.0.0"
|
||||||
|
|
||||||
pouchdb-utils@7.2.1:
|
pouchdb-utils@7.2.1:
|
||||||
version "7.2.1"
|
version "7.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-7.2.1.tgz#5dec1c53c8ecba717e5762311e9a1def2d4ebf9c"
|
resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-7.2.1.tgz#5dec1c53c8ecba717e5762311e9a1def2d4ebf9c"
|
||||||
|
@ -5511,7 +5581,17 @@ readable-stream@1.0.33:
|
||||||
isarray "0.0.1"
|
isarray "0.0.1"
|
||||||
string_decoder "~0.10.x"
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5:
|
readable-stream@^1.0.27-1:
|
||||||
|
version "1.1.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||||
|
integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
|
||||||
|
dependencies:
|
||||||
|
core-util-is "~1.0.0"
|
||||||
|
inherits "~2.0.1"
|
||||||
|
isarray "0.0.1"
|
||||||
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
|
readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6:
|
||||||
version "2.3.7"
|
version "2.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6088,6 +6168,13 @@ split-string@^3.0.1, split-string@^3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
extend-shallow "^3.0.0"
|
extend-shallow "^3.0.0"
|
||||||
|
|
||||||
|
split2@^2.1.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
|
||||||
|
integrity sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==
|
||||||
|
dependencies:
|
||||||
|
through2 "^2.0.2"
|
||||||
|
|
||||||
split2@^3.1.1:
|
split2@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/split2/-/split2-3.1.1.tgz#c51f18f3e06a8c4469aaab487687d8d956160bb6"
|
resolved "https://registry.yarnpkg.com/split2/-/split2-3.1.1.tgz#c51f18f3e06a8c4469aaab487687d8d956160bb6"
|
||||||
|
@ -6262,6 +6349,13 @@ strip-ansi@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^5.0.0"
|
ansi-regex "^5.0.0"
|
||||||
|
|
||||||
|
strip-bom@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
|
||||||
|
integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
|
||||||
|
dependencies:
|
||||||
|
is-utf8 "^0.2.0"
|
||||||
|
|
||||||
strip-bom@^3.0.0:
|
strip-bom@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||||
|
@ -6445,6 +6539,14 @@ through2@^0.6.2, through2@^0.6.5:
|
||||||
readable-stream ">=1.0.33-1 <1.1.0-0"
|
readable-stream ">=1.0.33-1 <1.1.0-0"
|
||||||
xtend ">=4.0.0 <4.1.0-0"
|
xtend ">=4.0.0 <4.1.0-0"
|
||||||
|
|
||||||
|
through2@^2.0.0, through2@^2.0.2, through2@^2.0.3:
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||||
|
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
|
||||||
|
dependencies:
|
||||||
|
readable-stream "~2.3.6"
|
||||||
|
xtend "~4.0.1"
|
||||||
|
|
||||||
through@^2.3.6, through@^2.3.8, through@~2.3.4:
|
through@^2.3.6, through@^2.3.8, through@~2.3.4:
|
||||||
version "2.3.8"
|
version "2.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||||
|
@ -6968,7 +7070,7 @@ xmlbuilder@~9.0.1:
|
||||||
version "9.0.7"
|
version "9.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
|
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
|
||||||
|
|
||||||
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.0:
|
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@budibase/client": "^0.1.23",
|
"@budibase/client": "^0.1.25",
|
||||||
"@rollup/plugin-commonjs": "^11.1.0",
|
"@rollup/plugin-commonjs": "^11.1.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"rollup": "^1.11.0",
|
"rollup": "^1.11.0",
|
||||||
|
@ -31,12 +31,12 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte"
|
"svelte"
|
||||||
],
|
],
|
||||||
"version": "0.1.23",
|
"version": "0.1.25",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
|
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@beyonk/svelte-googlemaps": "^2.2.0",
|
"@beyonk/svelte-googlemaps": "^2.2.0",
|
||||||
"@budibase/bbui": "^1.39.0",
|
"@budibase/bbui": "^1.40.1",
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"britecharts": "^2.16.1",
|
"britecharts": "^2.16.1",
|
||||||
"d3-selection": "^1.4.2",
|
"d3-selection": "^1.4.2",
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { Label, DatePicker } from "@budibase/bbui"
|
import {
|
||||||
|
Label,
|
||||||
|
DatePicker,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Button,
|
||||||
|
Toggle,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import Dropzone from "./attachments/Dropzone.svelte"
|
import Dropzone from "./attachments/Dropzone.svelte"
|
||||||
|
import LinkedRecordSelector from "./LinkedRecordSelector.svelte"
|
||||||
import debounce from "lodash.debounce"
|
import debounce from "lodash.debounce"
|
||||||
|
import ErrorsBox from "./ErrorsBox.svelte"
|
||||||
|
import { capitalise } from "./helpers"
|
||||||
|
|
||||||
export let _bb
|
export let _bb
|
||||||
export let model
|
export let model
|
||||||
|
@ -32,16 +42,11 @@
|
||||||
let isNew = true
|
let isNew = true
|
||||||
let errors = {}
|
let errors = {}
|
||||||
|
|
||||||
|
$: fields = schema ? Object.keys(schema) : []
|
||||||
$: if (model && model.length !== 0) {
|
$: if (model && model.length !== 0) {
|
||||||
fetchModel()
|
fetchModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
$: fields = schema ? Object.keys(schema) : []
|
|
||||||
|
|
||||||
$: errorMessages = Object.entries(errors).map(
|
|
||||||
([field, message]) => `${field} ${message}`
|
|
||||||
)
|
|
||||||
|
|
||||||
async function fetchModel() {
|
async function fetchModel() {
|
||||||
const FETCH_MODEL_URL = `/api/models/${model}`
|
const FETCH_MODEL_URL = `/api/models/${model}`
|
||||||
const response = await _bb.api.get(FETCH_MODEL_URL)
|
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||||
|
@ -82,11 +87,13 @@
|
||||||
saved = true
|
saved = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
saved = false
|
saved = false
|
||||||
}, 1000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 400) {
|
if (response.status === 400) {
|
||||||
errors = json.errors
|
errors = Object.keys(json.errors)
|
||||||
|
.map(k => ({ dataPath: k, message: json.errors[k] }))
|
||||||
|
.flat()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -103,8 +110,7 @@
|
||||||
|
|
||||||
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
|
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
|
||||||
const response = await _bb.api.get(GET_RECORD_URL)
|
const response = await _bb.api.get(GET_RECORD_URL)
|
||||||
const json = await response.json()
|
record = await response.json()
|
||||||
record = json
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -112,44 +118,52 @@
|
||||||
{#if title}
|
{#if title}
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
{/if}
|
{/if}
|
||||||
{#each errorMessages as error}
|
|
||||||
<p class="error">{error}</p>
|
|
||||||
{/each}
|
|
||||||
<hr />
|
|
||||||
<div class="form-content">
|
<div class="form-content">
|
||||||
|
<ErrorsBox {errors} />
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<div class="form-item">
|
{#if schema[field].type === 'options'}
|
||||||
<Label small forAttr={'form-stacked-text'}>{field}</Label>
|
<Select
|
||||||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
|
secondary
|
||||||
<select bind:value={record[field]}>
|
label={capitalise(schema[field].name)}
|
||||||
{#each schema[field].constraints.inclusion as opt}
|
bind:value={record[field]}>
|
||||||
<option>{opt}</option>
|
<option value="">Choose an option</option>
|
||||||
{/each}
|
{#each schema[field].constraints.inclusion as opt}
|
||||||
</select>
|
<option>{opt}</option>
|
||||||
{:else if schema[field].type === 'datetime'}
|
{/each}
|
||||||
<DatePicker bind:value={record[field]} />
|
</Select>
|
||||||
{:else if schema[field].type === 'boolean'}
|
{:else if schema[field].type === 'datetime'}
|
||||||
<input class="input" type="checkbox" bind:checked={record[field]} />
|
<DatePicker
|
||||||
{:else if schema[field].type === 'number'}
|
label={capitalise(schema[field].name)}
|
||||||
<input class="input" type="number" bind:value={record[field]} />
|
bind:value={record[field]} />
|
||||||
{:else if schema[field].type === 'string'}
|
{:else if schema[field].type === 'boolean'}
|
||||||
<input class="input" type="text" bind:value={record[field]} />
|
<Toggle
|
||||||
{:else if schema[field].type === 'attachment'}
|
text={capitalise(schema[field].name)}
|
||||||
|
bind:checked={record[field]} />
|
||||||
|
{:else if schema[field].type === 'number'}
|
||||||
|
<Input
|
||||||
|
label={capitalise(schema[field].name)}
|
||||||
|
type="number"
|
||||||
|
bind:value={record[field]} />
|
||||||
|
{:else if schema[field].type === 'string'}
|
||||||
|
<Input
|
||||||
|
label={capitalise(schema[field].name)}
|
||||||
|
bind:value={record[field]} />
|
||||||
|
{:else if schema[field].type === 'attachment'}
|
||||||
|
<div>
|
||||||
|
<Label extraSmall grey>{schema[field].name}</Label>
|
||||||
<Dropzone bind:files={record[field]} />
|
<Dropzone bind:files={record[field]} />
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{:else if schema[field].type === 'link'}
|
||||||
<hr />
|
<LinkedRecordSelector
|
||||||
|
secondary
|
||||||
|
bind:linkedRecords={record[field]}
|
||||||
|
schema={schema[field]} />
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<div class="button-block">
|
<div class="buttons">
|
||||||
<button on:click={save} class:saved>
|
<Button primary on:click={save} green={saved}>
|
||||||
{#if saved}
|
{#if saved}Success{:else}{buttonText || 'Submit Form'}{/if}
|
||||||
<div in:fade>
|
</Button>
|
||||||
<span class:saved>Success</span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div>{buttonText || 'Submit Form'}</div>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -162,104 +176,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-content {
|
.form-content {
|
||||||
margin-bottom: 20px;
|
margin-bottom: var(--spacing-xl);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.buttons {
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid #e6e6e6;
|
|
||||||
padding: 1rem;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 1px solid var(--grey-1);
|
|
||||||
margin: 20px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr:nth-last-child(2) {
|
|
||||||
border: 1px solid #fff;
|
|
||||||
margin: 20px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-block {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 0.4em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: white;
|
|
||||||
background-color: #393c44;
|
|
||||||
outline: none;
|
|
||||||
width: 300px;
|
|
||||||
height: 40px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease 0s;
|
|
||||||
overflow: hidden;
|
|
||||||
outline: none;
|
|
||||||
user-select: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.saved {
|
|
||||||
background-color: #84c991;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
||||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
vertical-align: bottom;
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
*overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
select::-ms-expand {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
display: inline-block;
|
|
||||||
cursor: pointer;
|
|
||||||
align-items: baseline;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 1em 1em;
|
|
||||||
border: 1px solid #eaeaea;
|
|
||||||
border-radius: 5px;
|
|
||||||
font: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
-ms-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
|
|
||||||
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
|
||||||
background-position: right 17px top 1.5em, right 10px top 1.5em;
|
|
||||||
background-size: 7px 7px, 7px 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: red;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { cssVars, createClasses } from "./cssVars"
|
import { cssVars } from "./cssVars"
|
||||||
import ArrowUp from "./icons/ArrowUp.svelte"
|
import ArrowUp from "./icons/ArrowUp.svelte"
|
||||||
import ArrowDown from "./icons/ArrowDown.svelte"
|
import ArrowDown from "./icons/ArrowDown.svelte"
|
||||||
import fsort from "fast-sort"
|
import fsort from "fast-sort"
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
export let stripeColor
|
export let stripeColor
|
||||||
export let borderColor
|
export let borderColor
|
||||||
export let datasource = {}
|
export let datasource = {}
|
||||||
|
export let _bb
|
||||||
|
|
||||||
let data = []
|
let data = []
|
||||||
let headers = []
|
let headers = []
|
||||||
|
@ -29,11 +30,19 @@
|
||||||
|
|
||||||
$: sorted = sort.direction ? fsort(data)[sort.direction](sort.column) : data
|
$: sorted = sort.direction ? fsort(data)[sort.direction](sort.column) : data
|
||||||
|
|
||||||
|
async function fetchModel(modelId) {
|
||||||
|
const FETCH_MODEL_URL = `/api/models/${modelId}`
|
||||||
|
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||||
|
const model = await response.json()
|
||||||
|
schema = model.schema
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!isEmpty(datasource)) {
|
if (!isEmpty(datasource)) {
|
||||||
data = await fetchData(datasource)
|
data = await fetchData(datasource)
|
||||||
if (data) {
|
if (data && data.length) {
|
||||||
headers = Object.keys(data[0]).filter(shouldDisplayField)
|
await fetchModel(data[0].modelId)
|
||||||
|
headers = Object.keys(schema).filter(shouldDisplayField)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -85,11 +94,15 @@
|
||||||
{#each sorted as row (row._id)}
|
{#each sorted as row (row._id)}
|
||||||
<tr>
|
<tr>
|
||||||
{#each headers as header}
|
{#each headers as header}
|
||||||
<!-- Rudimentary solution for attachments on array given this entire table will be replaced by AG Grid -->
|
{#if schema[header]}
|
||||||
{#if Array.isArray(row[header])}
|
<!-- Rudimentary solution for attachments on array given this entire table will be replaced by AG Grid -->
|
||||||
<AttachmentList files={row[header]} />
|
{#if schema[header].type === 'attachment'}
|
||||||
{:else if row[header]}
|
<AttachmentList files={row[header]} />
|
||||||
<td>{row[header]}</td>
|
{:else if schema[header].type === 'link'}
|
||||||
|
<td>{row[header] ? row[header].length : 0} related row(s)</td>
|
||||||
|
{:else if row[header]}
|
||||||
|
<td>{row[header]}</td>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
export let errors = []
|
||||||
|
|
||||||
|
$: hasErrors = errors.length > 0
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if hasErrors}
|
||||||
|
<div class="container">
|
||||||
|
{#each errors as error}
|
||||||
|
<div class="error">{error.dataPath} {error.message}</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
border-radius: var(--border-radius-m);
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
background-color: var(--red-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--red-dark);
|
||||||
|
}
|
||||||
|
.error:first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,65 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { Select, Label, Multiselect } from "@budibase/bbui"
|
||||||
|
import api from "./api"
|
||||||
|
import { capitalise } from "./helpers"
|
||||||
|
|
||||||
|
export let schema = {}
|
||||||
|
export let linkedRecords = []
|
||||||
|
export let showLabel = true
|
||||||
|
export let secondary
|
||||||
|
|
||||||
|
let linkedModel
|
||||||
|
|
||||||
|
$: label = capitalise(schema.name)
|
||||||
|
$: linkedModelId = schema.modelId
|
||||||
|
$: recordsPromise = fetchRecords(linkedModelId)
|
||||||
|
$: fetchModel(linkedModelId)
|
||||||
|
|
||||||
|
async function fetchModel() {
|
||||||
|
if (linkedModelId == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const FETCH_MODEL_URL = `/api/models/${linkedModelId}`
|
||||||
|
const response = await api.get(FETCH_MODEL_URL)
|
||||||
|
linkedModel = await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRecords(linkedModelId) {
|
||||||
|
if (linkedModelId == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
|
||||||
|
const response = await api.get(FETCH_RECORDS_URL)
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrettyName(record) {
|
||||||
|
return record[linkedModel?.primaryDisplay || "_id"]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if linkedModel != null}
|
||||||
|
{#if linkedModel.primaryDisplay == null}
|
||||||
|
{#if showLabel}
|
||||||
|
<Label extraSmall grey>{label}</Label>
|
||||||
|
{/if}
|
||||||
|
<Label small black>
|
||||||
|
Please choose a primary display column for the
|
||||||
|
<b>{linkedModel.name}</b>
|
||||||
|
table.
|
||||||
|
</Label>
|
||||||
|
{:else}
|
||||||
|
{#await recordsPromise then records}
|
||||||
|
<Multiselect
|
||||||
|
{secondary}
|
||||||
|
bind:value={linkedRecords}
|
||||||
|
label={showLabel ? label : null}
|
||||||
|
placeholder="Choose some options">
|
||||||
|
{#each records as record}
|
||||||
|
<option value={record._id}>{getPrettyName(record)}</option>
|
||||||
|
{/each}
|
||||||
|
</Multiselect>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
|
@ -8,6 +8,12 @@
|
||||||
let store = _bb.store
|
let store = _bb.store
|
||||||
let target
|
let target
|
||||||
|
|
||||||
|
async function fetchModel(id) {
|
||||||
|
const FETCH_MODEL_URL = `/api/models/${id}`
|
||||||
|
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchFirstRecord() {
|
async function fetchFirstRecord() {
|
||||||
const FETCH_RECORDS_URL = `/api/views/all_${model}`
|
const FETCH_RECORDS_URL = `/api/views/all_${model}`
|
||||||
const response = await _bb.api.get(FETCH_RECORDS_URL)
|
const response = await _bb.api.get(FETCH_RECORDS_URL)
|
||||||
|
@ -34,6 +40,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (record) {
|
if (record) {
|
||||||
|
// Fetch model schema so we can check for linked records
|
||||||
|
const model = await fetchModel(record.modelId)
|
||||||
|
for (let key of Object.keys(model.schema)) {
|
||||||
|
if (model.schema[key].type === "link") {
|
||||||
|
record[key] = Array.isArray(record[key]) ? record[key].length : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_bb.attachChildren(target, {
|
_bb.attachChildren(target, {
|
||||||
hydrate: false,
|
hydrate: false,
|
||||||
context: record,
|
context: record,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { FILE_TYPES } from "./fileTypes"
|
import { FILE_TYPES } from "./fileTypes"
|
||||||
|
|
||||||
export let files
|
export let files = []
|
||||||
export let height = "70"
|
export let height = "70"
|
||||||
export let width = "70"
|
export let width = "70"
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,7 +4,28 @@ export default async function fetchData(datasource) {
|
||||||
const { isModel, name } = datasource
|
const { isModel, name } = datasource
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
return isModel ? await fetchModelData() : await fetchViewData()
|
const records = isModel ? await fetchModelData() : await fetchViewData()
|
||||||
|
|
||||||
|
// Fetch model schema so we can check for linked records
|
||||||
|
if (records && records.length) {
|
||||||
|
const model = await fetchModel(records[0].modelId)
|
||||||
|
const keys = Object.keys(model.schema)
|
||||||
|
records.forEach(record => {
|
||||||
|
for (let key of keys) {
|
||||||
|
if (model.schema[key].type === "link") {
|
||||||
|
record[key] = Array.isArray(record[key]) ? record[key].length : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchModel(id) {
|
||||||
|
const FETCH_MODEL_URL = `/api/models/${id}`
|
||||||
|
const response = await api.get(FETCH_MODEL_URL)
|
||||||
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchModelData() {
|
async function fetchModelData() {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
|
@ -1,3 +1,9 @@
|
||||||
|
# Budibase is in Beta
|
||||||
|
|
||||||
|
Budibase is currently beta software. Until our official launch, we cannot ensure backwards compatibility for your budibase applications between versions. Issues may arise when trying to edit apps created with old versions of the budibase builder.
|
||||||
|
|
||||||
|
If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md#troubleshooting) to clear down your environment.
|
||||||
|
|
||||||
|
|
||||||
# What is Budibase?
|
# What is Budibase?
|
||||||
|
|
||||||
|
|
86
yarn.lock
86
yarn.lock
|
@ -913,11 +913,6 @@ argparse@^1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
sprintf-js "~1.0.2"
|
sprintf-js "~1.0.2"
|
||||||
|
|
||||||
argsarray@0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb"
|
|
||||||
integrity sha1-bnIHtOzbObCviDA/pa4ivajfYcs=
|
|
||||||
|
|
||||||
arr-diff@^4.0.0:
|
arr-diff@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
|
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
|
||||||
|
@ -2336,11 +2331,6 @@ ignore@^4.0.6:
|
||||||
version "4.0.6"
|
version "4.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||||
|
|
||||||
immediate@~3.0.5:
|
|
||||||
version "3.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
|
||||||
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
|
|
||||||
|
|
||||||
import-fresh@^2.0.0:
|
import-fresh@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
|
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
|
||||||
|
@ -2387,7 +2377,7 @@ inflight@^1.0.4:
|
||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
|
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
|
||||||
|
@ -2638,11 +2628,6 @@ is-windows@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||||
|
|
||||||
isarray@0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
|
||||||
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
|
|
||||||
|
|
||||||
isarray@1.0.0, isarray@~1.0.0:
|
isarray@1.0.0, isarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||||
|
@ -2805,13 +2790,6 @@ libnpmpublish@^1.1.1:
|
||||||
semver "^5.5.1"
|
semver "^5.5.1"
|
||||||
ssri "^6.0.1"
|
ssri "^6.0.1"
|
||||||
|
|
||||||
lie@3.1.1:
|
|
||||||
version "3.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
|
||||||
integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
|
|
||||||
dependencies:
|
|
||||||
immediate "~3.0.5"
|
|
||||||
|
|
||||||
load-json-file@^1.0.0:
|
load-json-file@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
|
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
|
||||||
|
@ -2861,11 +2839,6 @@ lodash.ismatch@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
|
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
|
||||||
|
|
||||||
lodash.pick@^4.0.0:
|
|
||||||
version "4.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
|
|
||||||
integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
|
|
||||||
|
|
||||||
lodash.set@^4.3.2:
|
lodash.set@^4.3.2:
|
||||||
version "4.3.2"
|
version "4.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
|
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
|
||||||
|
@ -3204,16 +3177,6 @@ natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
|
|
||||||
ndjson@^1.4.3:
|
|
||||||
version "1.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/ndjson/-/ndjson-1.5.0.tgz#ae603b36b134bcec347b452422b0bf98d5832ec8"
|
|
||||||
integrity sha1-rmA7NrE0vOw0e0UkIrC/mNWDLsg=
|
|
||||||
dependencies:
|
|
||||||
json-stringify-safe "^5.0.1"
|
|
||||||
minimist "^1.2.0"
|
|
||||||
split2 "^2.1.0"
|
|
||||||
through2 "^2.0.3"
|
|
||||||
|
|
||||||
neo-async@^2.6.0:
|
neo-async@^2.6.0:
|
||||||
version "2.6.1"
|
version "2.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||||
|
@ -3717,34 +3680,6 @@ posix-character-classes@^0.1.0:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
||||||
|
|
||||||
pouch-stream@^0.4.0:
|
|
||||||
version "0.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/pouch-stream/-/pouch-stream-0.4.1.tgz#0c6d8475c9307677627991a2f079b301c3b89bdd"
|
|
||||||
integrity sha1-DG2EdckwdndieZGi8HmzAcO4m90=
|
|
||||||
dependencies:
|
|
||||||
inherits "^2.0.1"
|
|
||||||
readable-stream "^1.0.27-1"
|
|
||||||
|
|
||||||
pouchdb-promise@^6.0.4:
|
|
||||||
version "6.4.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-promise/-/pouchdb-promise-6.4.3.tgz#74516f4acf74957b54debd0fb2c0e5b5a68ca7b3"
|
|
||||||
integrity sha512-ruJaSFXwzsxRHQfwNHjQfsj58LBOY1RzGzde4PM5CWINZwFjCQAhZwfMrch2o/0oZT6d+Xtt0HTWhq35p3b0qw==
|
|
||||||
dependencies:
|
|
||||||
lie "3.1.1"
|
|
||||||
|
|
||||||
pouchdb-replication-stream@^1.2.9:
|
|
||||||
version "1.2.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb-replication-stream/-/pouchdb-replication-stream-1.2.9.tgz#aa4fa5d8f52df4825392f18e07c7e11acffc650a"
|
|
||||||
integrity sha1-qk+l2PUt9IJTkvGOB8fhGs/8ZQo=
|
|
||||||
dependencies:
|
|
||||||
argsarray "0.0.1"
|
|
||||||
inherits "^2.0.3"
|
|
||||||
lodash.pick "^4.0.0"
|
|
||||||
ndjson "^1.4.3"
|
|
||||||
pouch-stream "^0.4.0"
|
|
||||||
pouchdb-promise "^6.0.4"
|
|
||||||
through2 "^2.0.0"
|
|
||||||
|
|
||||||
prelude-ls@~1.1.2:
|
prelude-ls@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||||
|
@ -3927,16 +3862,6 @@ read@1, read@~1.0.1:
|
||||||
string_decoder "^1.1.1"
|
string_decoder "^1.1.1"
|
||||||
util-deprecate "^1.0.1"
|
util-deprecate "^1.0.1"
|
||||||
|
|
||||||
readable-stream@^1.0.27-1:
|
|
||||||
version "1.1.14"
|
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
|
||||||
integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
|
|
||||||
dependencies:
|
|
||||||
core-util-is "~1.0.0"
|
|
||||||
inherits "~2.0.1"
|
|
||||||
isarray "0.0.1"
|
|
||||||
string_decoder "~0.10.x"
|
|
||||||
|
|
||||||
readdir-scoped-modules@^1.0.0:
|
readdir-scoped-modules@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
|
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
|
||||||
|
@ -4291,7 +4216,7 @@ split-string@^3.0.1, split-string@^3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
extend-shallow "^3.0.0"
|
extend-shallow "^3.0.0"
|
||||||
|
|
||||||
split2@^2.0.0, split2@^2.1.0:
|
split2@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
|
resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4396,11 +4321,6 @@ string_decoder@^1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.2.0"
|
safe-buffer "~5.2.0"
|
||||||
|
|
||||||
string_decoder@~0.10.x:
|
|
||||||
version "0.10.31"
|
|
||||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
|
||||||
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
|
|
||||||
|
|
||||||
string_decoder@~1.1.1:
|
string_decoder@~1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||||
|
@ -4521,7 +4441,7 @@ text-table@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||||
|
|
||||||
through2@^2.0.0, through2@^2.0.2, through2@^2.0.3:
|
through2@^2.0.0, through2@^2.0.2:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in New Issue