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",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
"test:e2e:ci": "lerna run cy:ci"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
"pouchdb-replication-stream": "^1.2.9"
|
||||
"@fortawesome/fontawesome": "^1.1.8"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,7 +50,6 @@ context("Create a automation", () => {
|
|||
|
||||
// Activate Automation
|
||||
cy.get("[data-cy=activate-automation]").click()
|
||||
cy.contains("Add Record").should("be.visible")
|
||||
cy.get(".stop-button.highlighted").should("be.visible")
|
||||
})
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ context("Create a Table", () => {
|
|||
cy.createTable("dog")
|
||||
|
||||
// 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", () => {
|
||||
|
|
|
@ -50,13 +50,11 @@ context("Create a View", () => {
|
|||
|
||||
it("creates a stats calculation view based on age", () => {
|
||||
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")
|
||||
.find("select")
|
||||
.first()
|
||||
.select("Statistics")
|
||||
cy.get(".menu-container")
|
||||
.find("select")
|
||||
.eq(1)
|
||||
.eq(0)
|
||||
.select("age")
|
||||
cy.contains("Save").click()
|
||||
cy.get("thead th div").should($headers => {
|
||||
|
|
|
@ -68,6 +68,7 @@ Cypress.Commands.add("createTable", tableName => {
|
|||
cy.contains("Create New Table").click()
|
||||
cy.get(".menu-container")
|
||||
.get("input")
|
||||
.first()
|
||||
.type(tableName)
|
||||
|
||||
cy.contains("Save").click()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "0.1.23",
|
||||
"version": "0.1.25",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -63,8 +63,8 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.39.0",
|
||||
"@budibase/client": "^0.1.23",
|
||||
"@budibase/bbui": "^1.40.1",
|
||||
"@budibase/client": "^0.1.25",
|
||||
"@budibase/colorpicker": "^1.0.1",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
"@sentry/browser": "5.19.1",
|
||||
|
|
|
@ -37,9 +37,9 @@ export default function({ componentInstanceId, screen, components, models }) {
|
|||
.filter(isInstanceInSharedContext(walkResult))
|
||||
.map(componentInstanceToBindable(walkResult)),
|
||||
|
||||
...walkResult.target._contexts
|
||||
...(walkResult.target?._contexts
|
||||
.map(contextToBindables(models, walkResult))
|
||||
.flat(),
|
||||
.flat() ?? []),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -73,6 +73,10 @@ const componentInstanceToBindable = walkResult => i => {
|
|||
|
||||
const contextToBindables = (models, 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 => ({
|
||||
type: "context",
|
||||
|
@ -80,15 +84,12 @@ const contextToBindables = (models, walkResult) => context => {
|
|||
// how the binding expression persists, and is used in the app at runtime
|
||||
runtimeBinding: `${contextParentPath}data.${key}`,
|
||||
// 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
|
||||
// ... this allows us to bind to Model scheams, or View schemas
|
||||
const model = models.find(m => m._id === context.model.modelId)
|
||||
const schema = context.model.isModel
|
||||
? model.schema
|
||||
: model.views[context.model.name].schema
|
||||
// ... this allows us to bind to Model schemas, or View schemas
|
||||
const schema = isModel ? model.schema : model.views[context.model.name].schema
|
||||
|
||||
return (
|
||||
Object.keys(schema)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Table from "./Table.svelte"
|
||||
|
||||
let data = []
|
||||
let loading = false
|
||||
|
||||
$: title = $backendUiStore.selectedModel.name
|
||||
$: schema = $backendUiStore.selectedModel.schema
|
||||
|
@ -19,14 +20,16 @@
|
|||
// Fetch records for specified model
|
||||
$: {
|
||||
if ($backendUiStore.selectedView?.name?.startsWith("all_")) {
|
||||
loading = true
|
||||
api.fetchDataForView($backendUiStore.selectedView).then(records => {
|
||||
data = records || []
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table {title} {schema} {data} allowEditing={true}>
|
||||
<Table {title} {schema} {data} allowEditing={true} {loading}>
|
||||
<CreateColumnButton />
|
||||
{#if Object.keys(schema).length > 0}
|
||||
<CreateRowButton />
|
||||
|
|
|
@ -2,39 +2,33 @@
|
|||
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
|
||||
import Dropzone from "components/common/Dropzone.svelte"
|
||||
import { capitalise } from "../../../helpers"
|
||||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
||||
|
||||
export let meta
|
||||
export let value = meta.type === "boolean" ? false : ""
|
||||
|
||||
const type = determineInputType(meta)
|
||||
const 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"
|
||||
}
|
||||
$: type = meta.type
|
||||
$: label = capitalise(meta.name)
|
||||
</script>
|
||||
|
||||
{#if type === 'select'}
|
||||
{#if type === 'options'}
|
||||
<Select thin secondary {label} data-cy="{meta.name}-select" bind:value>
|
||||
<option value="">Choose an option</option>
|
||||
{#each meta.constraints.inclusion as opt}
|
||||
<option value={opt}>{opt}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if type === 'date'}
|
||||
{:else if type === 'datetime'}
|
||||
<DatePicker {label} bind:value />
|
||||
{:else if type === 'file'}
|
||||
{:else if type === 'attachment'}
|
||||
<div>
|
||||
<Label extraSmall grey forAttr={'dropzone-label'}>{label}</Label>
|
||||
<Dropzone bind:files={value} />
|
||||
</div>
|
||||
{:else if type === 'checkbox'}
|
||||
{:else if type === 'boolean'}
|
||||
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
||||
{:else if type === 'link'}
|
||||
<LinkedRecordSelector bind:linkedRecords={value} schema={meta} />
|
||||
{:else}
|
||||
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
|
||||
{/if}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { goto, params } from "@sveltech/routify"
|
||||
import { onMount } from "svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
import fsort from "fast-sort"
|
||||
import getOr from "lodash/fp/getOr"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
|
@ -16,6 +17,7 @@
|
|||
import ColumnHeaderPopover from "./popovers/ColumnPopover.svelte"
|
||||
import EditRowPopover from "./popovers/RowPopover.svelte"
|
||||
import CalculationPopover from "./buttons/CalculateButton.svelte"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
|
||||
const ITEMS_PER_PAGE = 10
|
||||
|
||||
|
@ -23,6 +25,7 @@
|
|||
export let data = []
|
||||
export let title
|
||||
export let allowEditing = false
|
||||
export let loading = false
|
||||
|
||||
let currentPage = 0
|
||||
|
||||
|
@ -50,7 +53,14 @@
|
|||
|
||||
<section>
|
||||
<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">
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -123,6 +133,13 @@
|
|||
text-rendering: optimizeLegibility;
|
||||
text-transform: capitalize;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
.title > span {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
table {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
||||
import RecordFieldControl from "../RecordFieldControl.svelte"
|
||||
import * as api from "../api"
|
||||
import { Modal } from "components/common/Modal"
|
||||
|
@ -44,11 +43,7 @@
|
|||
<ErrorsBox {errors} />
|
||||
{#each modelSchema as [key, meta]}
|
||||
<div>
|
||||
{#if meta.type === 'link'}
|
||||
<LinkedRecordSelector bind:linkedRecords={record[key]} schema={meta} />
|
||||
{:else}
|
||||
<RecordFieldControl {meta} bind:value={record[key]} />
|
||||
{/if}
|
||||
<RecordFieldControl {meta} bind:value={record[key]} />
|
||||
</div>
|
||||
{/each}
|
||||
</Modal>
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
)
|
||||
|
||||
function saveView() {
|
||||
if (!view.calculation) view.calculation = "stats"
|
||||
backendUiStore.actions.views.save(view)
|
||||
notifier.success(`View ${view.name} saved.`)
|
||||
onClosed()
|
||||
|
@ -34,14 +35,15 @@
|
|||
<div class="actions">
|
||||
<h5>Calculate</h5>
|
||||
<div class="input-group-row">
|
||||
<p>The</p>
|
||||
<!-- <p>The</p>
|
||||
<Select secondary thin bind:value={view.calculation}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each CALCULATIONS as calculation}
|
||||
<option value={calculation.key}>{calculation.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
<p>of</p>
|
||||
<p>of</p> -->
|
||||
<p>The statistics of</p>
|
||||
<Select secondary thin bind:value={view.field}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each fields as field}
|
||||
|
@ -74,7 +76,7 @@
|
|||
|
||||
.input-group-row {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 1fr 20px 1fr;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--spacing-s);
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
$: modelOptions = $backendUiStore.models.filter(
|
||||
model => model._id !== $backendUiStore.draftModel._id
|
||||
)
|
||||
$: required = !!field?.constraints?.presence
|
||||
|
||||
async function saveColumn() {
|
||||
backendUiStore.update(state => {
|
||||
|
@ -53,6 +54,12 @@
|
|||
field.type = type
|
||||
field.constraints = constraints
|
||||
}
|
||||
|
||||
function onChangeRequired(e) {
|
||||
const req = e.target.checked
|
||||
field.constraints.presence = req ? { allowEmpty: false } : false
|
||||
required = req
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="actions">
|
||||
|
@ -71,8 +78,8 @@
|
|||
|
||||
{#if field.type !== 'link'}
|
||||
<Toggle
|
||||
checked={!field.constraints.presence.allowEmpty}
|
||||
on:change={e => (field.constraints.presence.allowEmpty = !e.target.checked)}
|
||||
checked={required}
|
||||
on:change={onChangeRequired}
|
||||
thin
|
||||
text="Required" />
|
||||
{/if}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Button, Input, Select } from "@budibase/bbui"
|
||||
import { Button, Input, Select, DatePicker } from "@budibase/bbui"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import analytics from "analytics"
|
||||
|
@ -71,11 +71,38 @@
|
|||
|
||||
function isMultipleChoice(field) {
|
||||
return (
|
||||
viewModel.schema[field].constraints &&
|
||||
viewModel.schema[field].constraints.inclusion &&
|
||||
viewModel.schema[field].constraints.inclusion.length
|
||||
(viewModel.schema[field].constraints &&
|
||||
viewModel.schema[field].constraints.inclusion &&
|
||||
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>
|
||||
|
||||
<div class="actions">
|
||||
|
@ -93,7 +120,11 @@
|
|||
{/each}
|
||||
</Select>
|
||||
{/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>
|
||||
{#each fields as field}
|
||||
<option value={field}>{field}</option>
|
||||
|
@ -108,12 +139,25 @@
|
|||
{#if filter.key && isMultipleChoice(filter.key)}
|
||||
<Select secondary thin bind:value={filter.value}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each viewModel.schema[filter.key].constraints.inclusion as option}
|
||||
<option value={option}>{option}</option>
|
||||
{#each fieldOptions(filter.key) as option}
|
||||
<option value={option}>{option.toString()}</option>
|
||||
{/each}
|
||||
</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}
|
||||
<Input thin placeholder="Value" bind:value={filter.value} />
|
||||
<Input
|
||||
thin
|
||||
placeholder={filter.key || fields[0]}
|
||||
bind:value={filter.value} />
|
||||
{/if}
|
||||
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
|
||||
{/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 { backendUiStore } from "builderStore"
|
||||
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"
|
||||
|
||||
let anchor
|
||||
let dropdown
|
||||
let name
|
||||
let dataImport
|
||||
let loading
|
||||
|
||||
async function saveTable() {
|
||||
loading = true
|
||||
const model = await backendUiStore.actions.models.save({
|
||||
name,
|
||||
schema: {},
|
||||
schema: dataImport.schema || {},
|
||||
dataImport,
|
||||
})
|
||||
notifier.success(`Table ${name} created successfully.`)
|
||||
$goto(`./model/${model._id}`)
|
||||
analytics.captureEvent("Table Created", { name })
|
||||
name = ""
|
||||
dropdown.hide()
|
||||
analytics.captureEvent("Table Created", { name })
|
||||
loading = false
|
||||
}
|
||||
|
||||
const onClosed = () => {
|
||||
|
@ -38,9 +45,21 @@
|
|||
thin
|
||||
label="Table Name"
|
||||
bind:value={name} />
|
||||
<div>
|
||||
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
||||
<TableDataImport bind:dataImport />
|
||||
</div>
|
||||
<footer>
|
||||
<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>
|
||||
</div>
|
||||
</Popover>
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
import { onMount } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { Select, Label, Multiselect } from "@budibase/bbui"
|
||||
import { capitalise } from "../../helpers"
|
||||
import MultiSelect from "components/common/MultiSelect.svelte"
|
||||
|
||||
export let schema
|
||||
export let linkedRecords = []
|
||||
|
@ -19,8 +18,7 @@
|
|||
async function fetchRecords(linkedModelId) {
|
||||
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
|
||||
const response = await api.get(FETCH_RECORDS_URL)
|
||||
const result = await response.json()
|
||||
return result
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
function getPrettyName(record) {
|
||||
|
@ -37,15 +35,14 @@
|
|||
</Label>
|
||||
{:else}
|
||||
{#await promise then records}
|
||||
<MultiSelect
|
||||
thin
|
||||
<Multiselect
|
||||
secondary
|
||||
bind:value={linkedRecords}
|
||||
{label}
|
||||
placeholder="Choose an option">
|
||||
placeholder="Choose some options">
|
||||
{#each records as record}
|
||||
<option value={record._id}>{getPrettyName(record)}</option>
|
||||
{/each}
|
||||
</MultiSelect>
|
||||
</Multiselect>
|
||||
{/await}
|
||||
{/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;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
<script>
|
||||
import { Input, Button } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { Input } from "@budibase/bbui"
|
||||
import api from "builderStore/api"
|
||||
import posthog from "posthog-js"
|
||||
import analytics from "analytics"
|
||||
|
||||
let keys = { budibase: "", sendGrid: "" }
|
||||
let keys = { budibase: "" }
|
||||
|
||||
async function updateKey([key, value]) {
|
||||
if (key === "budibase") {
|
||||
|
@ -40,12 +38,6 @@
|
|||
edit
|
||||
value={keys.budibase}
|
||||
label="Budibase API Key" />
|
||||
<Input
|
||||
on:save={e => updateKey(['sendgrid', e.detail])}
|
||||
thin
|
||||
edit
|
||||
value={keys.sendgrid}
|
||||
label="Sendgrid API Key" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -146,7 +146,7 @@
|
|||
})
|
||||
const appJson = await appResp.json()
|
||||
analytics.captureEvent("App Created", {
|
||||
name,
|
||||
name: $createAppStore.values.applicationName,
|
||||
appId: appJson._id,
|
||||
template,
|
||||
})
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
on:input={() => (blurred.api = true)}
|
||||
label="API Key"
|
||||
name="apiKey"
|
||||
placeholder="Enter your API Key"
|
||||
placeholder="Use command-V to paste your API Key"
|
||||
type="password"
|
||||
error={blurred.api && validationErrors.apiKey} />
|
||||
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
label="Password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
type="pasword"
|
||||
type="password"
|
||||
error={blurred.password && validationErrors.password} />
|
||||
<Select label="Access Level" secondary name="accessLevelId">
|
||||
<option value="ADMIN">Admin</option>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
function handleSelected(selected) {
|
||||
dispatch("change", selected)
|
||||
dropdown.hide()
|
||||
dropdownRight.hide()
|
||||
}
|
||||
|
||||
const models = $backendUiStore.models.map(m => ({
|
||||
|
|
|
@ -6,7 +6,7 @@ export const FIELDS = {
|
|||
constraints: {
|
||||
type: "string",
|
||||
length: {},
|
||||
presence: { allowEmpty: true },
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
OPTIONS: {
|
||||
|
@ -25,7 +25,7 @@ export const FIELDS = {
|
|||
type: "number",
|
||||
constraints: {
|
||||
type: "number",
|
||||
presence: { allowEmpty: true },
|
||||
presence: false,
|
||||
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
|
||||
},
|
||||
},
|
||||
|
@ -35,7 +35,7 @@ export const FIELDS = {
|
|||
type: "boolean",
|
||||
constraints: {
|
||||
type: "boolean",
|
||||
presence: { allowEmpty: true },
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
DATETIME: {
|
||||
|
@ -45,7 +45,7 @@ export const FIELDS = {
|
|||
constraints: {
|
||||
type: "string",
|
||||
length: {},
|
||||
presence: { allowEmpty: true },
|
||||
presence: false,
|
||||
datetime: {
|
||||
latest: "",
|
||||
earliest: "",
|
||||
|
@ -58,7 +58,7 @@ export const FIELDS = {
|
|||
type: "attachment",
|
||||
constraints: {
|
||||
type: "array",
|
||||
presence: { allowEmpty: true },
|
||||
presence: false,
|
||||
},
|
||||
},
|
||||
LINK: {
|
||||
|
|
|
@ -709,10 +709,10 @@
|
|||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/bbui@^1.39.0":
|
||||
version "1.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.39.0.tgz#7d3e259a60a0b4602f3d2da3452679d91a591fdd"
|
||||
integrity sha512-9IL5Lw488sdYCa9mjHHdrap11VqW6wQHNcNTL8fFHaWNzummtlaUlVMScs9cunYgsR/L4NCgH0zSFdP0RnrUqw==
|
||||
"@budibase/bbui@^1.40.1":
|
||||
version "1.40.1"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.40.1.tgz#7ebfd52b4da822312d3395447a4f73caa41f8014"
|
||||
integrity sha512-0t5Makyn5jOURKZIQPvd+8G4m6ps4GyfLUkAz5rKJSnAQSAgLiuZ+RihcEReDEJK8tnfW7h2echJTffJduQRRQ==
|
||||
dependencies:
|
||||
sirv-cli "^0.4.6"
|
||||
svelte-flatpickr "^2.4.0"
|
||||
|
@ -5844,10 +5844,6 @@ svelte-portal@^0.1.0:
|
|||
version "0.1.0"
|
||||
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:
|
||||
version "3.25.1"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "budibase",
|
||||
"version": "0.1.23",
|
||||
"version": "0.1.25",
|
||||
"description": "Budibase CLI",
|
||||
"repository": "https://github.com/Budibase/Budibase",
|
||||
"homepage": "https://www.budibase.com",
|
||||
|
@ -17,7 +17,7 @@
|
|||
"author": "Budibase",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/server": "^0.1.23",
|
||||
"@budibase/server": "^0.1.25",
|
||||
"@inquirer/password": "^0.0.6-alpha.0",
|
||||
"chalk": "^2.4.2",
|
||||
"dotenv": "^8.2.0",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "0.1.23",
|
||||
"version": "0.1.25",
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/budibase-client.js",
|
||||
"module": "dist/budibase-client.esm.mjs",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"version": "0.1.23",
|
||||
"version": "0.1.25",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/electron.js",
|
||||
"repository": {
|
||||
|
@ -42,13 +42,14 @@
|
|||
"author": "Michael Shanks",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/client": "^0.1.23",
|
||||
"@budibase/client": "^0.1.25",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@sendgrid/mail": "^7.1.1",
|
||||
"@sentry/node": "^5.19.2",
|
||||
"aws-sdk": "^2.767.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chmodr": "^1.2.0",
|
||||
"csvtojson": "^2.0.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"download": "^8.0.0",
|
||||
"electron-is-dev": "^1.2.0",
|
||||
|
@ -71,6 +72,7 @@
|
|||
"pino-pretty": "^4.0.0",
|
||||
"pouchdb": "^7.2.1",
|
||||
"pouchdb-all-dbs": "^1.0.2",
|
||||
"pouchdb-replication-stream": "^1.2.9",
|
||||
"sharp": "^0.26.0",
|
||||
"squirrelly": "^7.5.0",
|
||||
"tar-fs": "^2.1.0",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const fs = require("fs")
|
||||
const { join } = require("../../utilities/sanitisedPath")
|
||||
const readline = require("readline")
|
||||
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
||||
const ENV_FILE_PATH = "/.env"
|
||||
|
@ -7,7 +8,6 @@ exports.fetch = async function(ctx) {
|
|||
ctx.status = 200
|
||||
ctx.body = {
|
||||
budibase: process.env.BUDIBASE_API_KEY,
|
||||
sendgrid: process.env.SENDGRID_API_KEY,
|
||||
userId: process.env.USERID_API_KEY,
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,9 @@ exports.update = async function(ctx) {
|
|||
async function updateValues([key, value]) {
|
||||
let newContent = ""
|
||||
let keyExists = false
|
||||
let envPath = join(budibaseAppsDir(), ENV_FILE_PATH)
|
||||
const readInterface = readline.createInterface({
|
||||
input: fs.createReadStream(`${budibaseAppsDir()}/${ENV_FILE_PATH}`),
|
||||
input: fs.createReadStream(envPath),
|
||||
output: process.stdout,
|
||||
console: false,
|
||||
})
|
||||
|
@ -48,6 +49,6 @@ async function updateValues([key, value]) {
|
|||
// Add API Key if it doesn't exist in the file at all
|
||||
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 env = require("../../environment")
|
||||
const instanceController = require("./instance")
|
||||
const { resolve, join } = require("path")
|
||||
const { copy, exists, readFile, writeFile } = require("fs-extra")
|
||||
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
||||
const sqrl = require("squirrelly")
|
||||
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||
const fs = require("fs-extra")
|
||||
const { join, resolve } = require("../../utilities/sanitisedPath")
|
||||
const { promisify } = require("util")
|
||||
const chmodr = require("chmodr")
|
||||
const { generateAppID, getAppParams } = require("../../db/utils")
|
||||
|
@ -116,7 +116,7 @@ exports.delete = async function(ctx) {
|
|||
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
|
||||
const app = await db.get(ctx.params.applicationId)
|
||||
const result = await db.remove(app)
|
||||
await fs.rmdir(`${budibaseAppsDir()}/${ctx.params.applicationId}`, {
|
||||
await fs.rmdir(join(budibaseAppsDir(), ctx.params.applicationId), {
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const CouchDB = require("../../db")
|
||||
const ClientDb = require("../../db/clientDb")
|
||||
const { resolve, join } = require("path")
|
||||
const { resolve, join } = require("../../utilities/sanitisedPath")
|
||||
const {
|
||||
budibaseTempDir,
|
||||
budibaseAppsDir,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const fs = require("fs")
|
||||
const { join } = require("../../../utilities/sanitisedPath")
|
||||
const AWS = require("aws-sdk")
|
||||
const fetch = require("node-fetch")
|
||||
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)
|
||||
|
||||
|
@ -116,7 +117,7 @@ exports.uploadAppAssets = async function({
|
|||
|
||||
for (let page of appPages) {
|
||||
// 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({
|
||||
file: {
|
||||
path: filePath,
|
||||
|
|
|
@ -3,6 +3,7 @@ const CouchDB = require("../../db")
|
|||
const client = require("../../db/clientDb")
|
||||
const newid = require("../../db/newid")
|
||||
const { createLinkView } = require("../../db/linkedRecords")
|
||||
const { join } = require("../../utilities/sanitisedPath")
|
||||
const { downloadTemplate } = require("../../utilities/templates")
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
|
@ -39,7 +40,9 @@ exports.create = async function(ctx) {
|
|||
// replicate the template data to the instance DB
|
||||
if (template) {
|
||||
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)
|
||||
if (!ok) {
|
||||
ctx.throw(500, "Error loading database dump from template.")
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
const CouchDB = require("../../db")
|
||||
const linkRecords = require("../../db/linkedRecords")
|
||||
const csvParser = require("../../utilities/csvParser")
|
||||
const {
|
||||
getRecordParams,
|
||||
getModelParams,
|
||||
generateModelID,
|
||||
generateRecordID,
|
||||
} = require("../../db/utils")
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
|
@ -24,11 +26,12 @@ exports.find = async function(ctx) {
|
|||
exports.save = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const { dataImport, ...rest } = ctx.request.body
|
||||
const modelToSave = {
|
||||
type: "model",
|
||||
_id: generateModelID(),
|
||||
views: {},
|
||||
...ctx.request.body,
|
||||
...rest,
|
||||
}
|
||||
|
||||
// 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.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.message = `Model ${ctx.request.body.name} saved successfully.`
|
||||
ctx.body = modelToSave
|
||||
|
@ -116,3 +132,12 @@ exports.destroy = async function(ctx) {
|
|||
ctx.status = 200
|
||||
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 linkRecords = require("../../db/linkedRecords")
|
||||
const { getRecordParams, generateRecordID } = require("../../db/utils")
|
||||
const { cloneDeep } = require("lodash")
|
||||
|
||||
const MODEL_VIEW_BEGINS_WITH = "all_model:"
|
||||
|
||||
|
@ -21,6 +22,7 @@ exports.patch = async function(ctx) {
|
|||
let record = await db.get(ctx.params.id)
|
||||
const model = await db.get(record.modelId)
|
||||
const patchfields = ctx.request.body
|
||||
record = coerceRecordValues(record, model)
|
||||
|
||||
for (let key of Object.keys(patchfields)) {
|
||||
if (!model.schema[key]) continue
|
||||
|
@ -75,6 +77,8 @@ exports.save = async function(ctx) {
|
|||
|
||||
const model = await db.get(record.modelId)
|
||||
|
||||
record = coerceRecordValues(record, model)
|
||||
|
||||
const validateResult = await validate({
|
||||
record,
|
||||
model,
|
||||
|
@ -285,3 +289,50 @@ exports.fetchEnrichedRecord = async function(ctx) {
|
|||
ctx.body = record
|
||||
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 { resolve, join } = require("path")
|
||||
const { resolve, join } = require("../../utilities/sanitisedPath")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const fetch = require("node-fetch")
|
||||
const fs = require("fs-extra")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const CouchDB = require("../../../db")
|
||||
const viewTemplate = require("./viewBuilder")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { join } = require("../../../utilities/sanitisedPath")
|
||||
const os = require("os")
|
||||
const exporters = require("./exporters")
|
||||
const { fetchView } = require("../record")
|
||||
|
@ -99,7 +99,8 @@ const controller = {
|
|||
const exporter = exporters[format]
|
||||
const exportedFile = exporter(headers, ctx.body)
|
||||
const filename = `${view.name}.${format}`
|
||||
fs.writeFileSync(path.join(os.tmpdir(), filename), exportedFile)
|
||||
fs.writeFileSync(join(os.tmpdir(), filename), exportedFile)
|
||||
|
||||
ctx.body = {
|
||||
url: `/api/views/export/download/${filename}`,
|
||||
name: view.name,
|
||||
|
@ -109,7 +110,7 @@ const controller = {
|
|||
const filename = ctx.params.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}")`
|
||||
)
|
||||
} else {
|
||||
const value =
|
||||
typeof filter.value == "string" ? `"${filter.value}"` : filter.value
|
||||
|
||||
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
|
||||
)
|
||||
.post("/api/models", authorized(BUILDER), usage, modelController.save)
|
||||
.post(
|
||||
"/api/models/csv/validate",
|
||||
authorized(BUILDER),
|
||||
modelController.validateCSVSchema
|
||||
)
|
||||
.delete(
|
||||
"/api/models/:modelId/:revId",
|
||||
authorized(BUILDER),
|
||||
|
|
|
@ -49,13 +49,13 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
|||
key: "name",
|
||||
schema: {
|
||||
name: {
|
||||
type: "text",
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "text",
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
|
|
|
@ -180,7 +180,7 @@ describe("/models", () => {
|
|||
key: "name",
|
||||
schema: {
|
||||
name: {
|
||||
type: "text",
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
|
|
|
@ -38,7 +38,7 @@ describe("/records", () => {
|
|||
|
||||
const createRecord = async r =>
|
||||
await request
|
||||
.post(`/api/${model._id}/records`)
|
||||
.post(`/api/${r ? r.modelId : record.modelId}/records`)
|
||||
.send(r || record)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
|
@ -152,6 +152,95 @@ describe("/records", () => {
|
|||
.expect('Content-Type', /json/)
|
||||
.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", () => {
|
||||
|
|
|
@ -69,13 +69,13 @@ describe("/views", () => {
|
|||
filters: [],
|
||||
schema: {
|
||||
name: {
|
||||
type: "text",
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string"
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "text",
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string"
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@ const createUser = require("./steps/createUser")
|
|||
const environment = require("../environment")
|
||||
const download = require("download")
|
||||
const fetch = require("node-fetch")
|
||||
const path = require("path")
|
||||
const { join } = require("../utilities/sanitisedPath")
|
||||
const os = require("os")
|
||||
const fs = require("fs")
|
||||
const Sentry = require("@sentry/node")
|
||||
|
@ -43,7 +43,7 @@ async function downloadPackage(name, version, bundleName) {
|
|||
`${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`,
|
||||
AUTOMATION_DIRECTORY
|
||||
)
|
||||
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
|
||||
return require(join(AUTOMATION_DIRECTORY, bundleName))
|
||||
}
|
||||
|
||||
module.exports.getAction = async function(actionName) {
|
||||
|
@ -57,7 +57,7 @@ module.exports.getAction = async function(actionName) {
|
|||
const pkg = MANIFEST.packages[actionName]
|
||||
const bundleName = buildBundleName(pkg.stepId, pkg.version)
|
||||
try {
|
||||
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
|
||||
return require(join(AUTOMATION_DIRECTORY, bundleName))
|
||||
} catch (err) {
|
||||
return downloadPackage(pkg.stepId, pkg.version, bundleName)
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ module.exports.getAction = async function(actionName) {
|
|||
module.exports.init = async function() {
|
||||
// set defaults
|
||||
if (!AUTOMATION_DIRECTORY) {
|
||||
AUTOMATION_DIRECTORY = path.join(os.homedir(), DEFAULT_DIRECTORY)
|
||||
AUTOMATION_DIRECTORY = join(os.homedir(), DEFAULT_DIRECTORY)
|
||||
}
|
||||
if (!AUTOMATION_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 = {
|
||||
description: "Send an email",
|
||||
tagline: "Send email to {{inputs.to}}",
|
||||
|
@ -13,6 +9,10 @@ module.exports.definition = {
|
|||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
apiKey: {
|
||||
type: "string",
|
||||
title: "SendGrid API key",
|
||||
},
|
||||
to: {
|
||||
type: "string",
|
||||
title: "Send To",
|
||||
|
@ -49,6 +49,8 @@ module.exports.definition = {
|
|||
}
|
||||
|
||||
module.exports.run = async function({ inputs }) {
|
||||
const sgMail = require("@sendgrid/mail")
|
||||
sgMail.setApiKey(inputs.apiKey)
|
||||
const msg = {
|
||||
to: inputs.to,
|
||||
from: inputs.from,
|
||||
|
|
|
@ -2,6 +2,7 @@ const PouchDB = require("pouchdb")
|
|||
const replicationStream = require("pouchdb-replication-stream")
|
||||
const allDbs = require("pouchdb-all-dbs")
|
||||
const { budibaseAppsDir } = require("../utilities/budibaseDir")
|
||||
const { sanitise } = require("../utilities/sanitisedPath")
|
||||
const env = require("../environment")
|
||||
|
||||
const COUCH_DB_URL = env.COUCH_DB_URL || `leveldb://${budibaseAppsDir()}/.data/`
|
||||
|
@ -26,4 +27,10 @@ const Pouch = PouchDB.defaults(POUCH_DB_DEFAULTS)
|
|||
|
||||
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 { join } = require("path")
|
||||
const { join } = require("./utilities/sanitisedPath")
|
||||
const isDev = require("electron-is-dev")
|
||||
const { autoUpdater } = require("electron-updater")
|
||||
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 { app } = require("electron")
|
||||
const fixPath = require("fix-path")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const { join } = require("path")
|
||||
const { join } = require("./sanitisedPath")
|
||||
const { homedir, tmpdir } = require("os")
|
||||
const env = require("../environment")
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ const {
|
|||
readFile,
|
||||
writeJSON,
|
||||
} = require("fs-extra")
|
||||
const { join, resolve } = require("path")
|
||||
const { join, resolve } = require("../sanitisedPath")
|
||||
const sqrl = require("squirrelly")
|
||||
const { convertCssToFiles } = require("./convertCssToFiles")
|
||||
const publicPath = require("./publicPath")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const crypto = require("crypto")
|
||||
const { ensureDir, emptyDir, writeFile } = require("fs-extra")
|
||||
const { join } = require("path")
|
||||
const { join } = require("../sanitisedPath")
|
||||
|
||||
module.exports.convertCssToFiles = async (publicPagePath, pkg) => {
|
||||
const cssDir = join(publicPagePath, "css")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const { readJSON, readdir } = require("fs-extra")
|
||||
const { join } = require("path")
|
||||
const { join } = require("../sanitisedPath")
|
||||
|
||||
module.exports = async appPath => {
|
||||
const pages = {}
|
||||
|
|
|
@ -8,7 +8,8 @@ const {
|
|||
unlink,
|
||||
rmdir,
|
||||
} = require("fs-extra")
|
||||
const { join, dirname, resolve } = require("path")
|
||||
const { join, resolve } = require("../sanitisedPath")
|
||||
const { dirname } = require("path")
|
||||
const env = require("../../environment")
|
||||
|
||||
const buildPage = require("./buildPage")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const { appPackageFolder } = require("../createAppPackage")
|
||||
const { readJSON, readdir, stat } = require("fs-extra")
|
||||
const { join } = require("path")
|
||||
const { join } = require("../sanitisedPath")
|
||||
const { keyBy } = require("lodash/fp")
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const { resolve } = require("path")
|
||||
const { resolve } = require("./sanitisedPath")
|
||||
const { cwd } = require("process")
|
||||
const stream = require("stream")
|
||||
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 { join, resolve } = require("path")
|
||||
const { join, resolve } = require("./sanitisedPath")
|
||||
const Sqrl = require("squirrelly")
|
||||
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 { join } = require("./sanitisedPath")
|
||||
const os = require("os")
|
||||
const fetch = require("node-fetch")
|
||||
const stream = require("stream")
|
||||
|
@ -27,10 +27,10 @@ exports.downloadTemplate = async function(type, name) {
|
|||
await streamPipeline(
|
||||
response.body,
|
||||
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({
|
||||
|
@ -39,15 +39,17 @@ exports.exportTemplateFromApp = async function({
|
|||
instanceId,
|
||||
}) {
|
||||
// Copy frontend files
|
||||
const appToExport = path.join(os.homedir(), ".budibase", appId, "pages")
|
||||
const templatesDir = path.join(os.homedir(), ".budibase", "templates")
|
||||
const appToExport = join(os.homedir(), ".budibase", appId, "pages")
|
||||
const templatesDir = join(os.homedir(), ".budibase", "templates")
|
||||
fs.ensureDirSync(templatesDir)
|
||||
|
||||
const templateOutputPath = path.join(templatesDir, templateName)
|
||||
fs.copySync(appToExport, `${templateOutputPath}/pages`)
|
||||
const templateOutputPath = join(templatesDir, templateName)
|
||||
fs.copySync(appToExport, join(templateOutputPath, "pages"))
|
||||
|
||||
fs.ensureDirSync(path.join(templateOutputPath, "db"))
|
||||
const writeStream = fs.createWriteStream(`${templateOutputPath}/db/dump.txt`)
|
||||
fs.ensureDirSync(join(templateOutputPath, "db"))
|
||||
const writeStream = fs.createWriteStream(
|
||||
join(templateOutputPath, "db", "dump.txt")
|
||||
)
|
||||
|
||||
// perform couch dump
|
||||
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"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/client@^0.1.23":
|
||||
version "0.1.23"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.23.tgz#d72d2b26ff3a2d99f2b6c1b71020b1136880937d"
|
||||
integrity sha512-pZdwdCq5kKLZfZYxasIHBNnqu3BFFrqJLxXMFs0K9ddCVZ0UNons59nn73nFGbeRgNVdWp6yyW71XyMQr8NOEw==
|
||||
"@budibase/client@^0.1.25":
|
||||
version "0.1.25"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.25.tgz#f08c4a614f9018eb0f0faa6d20bb05f7a3215c70"
|
||||
integrity sha512-vZ0cqJwLYcs7MHihFnJO3qOe7qxibnB4Va1+IYNfnPc9kcxy4KvfQxCx/G/DDxP9CXfEvsguy9ymzR3RUAvBHw==
|
||||
dependencies:
|
||||
deep-equal "^2.0.1"
|
||||
mustache "^4.0.1"
|
||||
|
@ -1079,9 +1079,10 @@ bluebird-lst@^1.0.9:
|
|||
dependencies:
|
||||
bluebird "^3.5.5"
|
||||
|
||||
bluebird@^3.5.5:
|
||||
bluebird@^3.5.1, bluebird@^3.5.5:
|
||||
version "3.7.2"
|
||||
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:
|
||||
version "3.0.1"
|
||||
|
@ -1618,6 +1619,15 @@ cssstyle@^1.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.14.1"
|
||||
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"
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.1.6"
|
||||
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"
|
||||
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:
|
||||
version "4.7.0"
|
||||
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"
|
||||
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:
|
||||
version "1.4.0"
|
||||
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"
|
||||
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:
|
||||
version "0.6.2"
|
||||
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"
|
||||
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:
|
||||
version "7.2.1"
|
||||
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:
|
||||
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:
|
||||
version "7.2.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||
dependencies:
|
||||
|
@ -6088,6 +6168,13 @@ split-string@^3.0.1, split-string@^3.0.2:
|
|||
dependencies:
|
||||
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:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/split2/-/split2-3.1.1.tgz#c51f18f3e06a8c4469aaab487687d8d956160bb6"
|
||||
|
@ -6262,6 +6349,13 @@ strip-ansi@^6.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
|
@ -6968,7 +7070,7 @@ xmlbuilder@~9.0.1:
|
|||
version "9.0.7"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@budibase/client": "^0.1.23",
|
||||
"@budibase/client": "^0.1.25",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
"rollup": "^1.11.0",
|
||||
|
@ -31,12 +31,12 @@
|
|||
"keywords": [
|
||||
"svelte"
|
||||
],
|
||||
"version": "0.1.23",
|
||||
"version": "0.1.25",
|
||||
"license": "MIT",
|
||||
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
|
||||
"dependencies": {
|
||||
"@beyonk/svelte-googlemaps": "^2.2.0",
|
||||
"@budibase/bbui": "^1.39.0",
|
||||
"@budibase/bbui": "^1.40.1",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
"britecharts": "^2.16.1",
|
||||
"d3-selection": "^1.4.2",
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
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 LinkedRecordSelector from "./LinkedRecordSelector.svelte"
|
||||
import debounce from "lodash.debounce"
|
||||
import ErrorsBox from "./ErrorsBox.svelte"
|
||||
import { capitalise } from "./helpers"
|
||||
|
||||
export let _bb
|
||||
export let model
|
||||
|
@ -32,16 +42,11 @@
|
|||
let isNew = true
|
||||
let errors = {}
|
||||
|
||||
$: fields = schema ? Object.keys(schema) : []
|
||||
$: if (model && model.length !== 0) {
|
||||
fetchModel()
|
||||
}
|
||||
|
||||
$: fields = schema ? Object.keys(schema) : []
|
||||
|
||||
$: errorMessages = Object.entries(errors).map(
|
||||
([field, message]) => `${field} ${message}`
|
||||
)
|
||||
|
||||
async function fetchModel() {
|
||||
const FETCH_MODEL_URL = `/api/models/${model}`
|
||||
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||
|
@ -82,11 +87,13 @@
|
|||
saved = true
|
||||
setTimeout(() => {
|
||||
saved = false
|
||||
}, 1000)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
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 response = await _bb.api.get(GET_RECORD_URL)
|
||||
const json = await response.json()
|
||||
record = json
|
||||
record = await response.json()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -112,44 +118,52 @@
|
|||
{#if title}
|
||||
<h1>{title}</h1>
|
||||
{/if}
|
||||
{#each errorMessages as error}
|
||||
<p class="error">{error}</p>
|
||||
{/each}
|
||||
<hr />
|
||||
<div class="form-content">
|
||||
<ErrorsBox {errors} />
|
||||
{#each fields as field}
|
||||
<div class="form-item">
|
||||
<Label small forAttr={'form-stacked-text'}>{field}</Label>
|
||||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
|
||||
<select bind:value={record[field]}>
|
||||
{#each schema[field].constraints.inclusion as opt}
|
||||
<option>{opt}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if schema[field].type === 'datetime'}
|
||||
<DatePicker bind:value={record[field]} />
|
||||
{:else if schema[field].type === 'boolean'}
|
||||
<input class="input" type="checkbox" bind:checked={record[field]} />
|
||||
{:else if schema[field].type === 'number'}
|
||||
<input class="input" type="number" bind:value={record[field]} />
|
||||
{:else if schema[field].type === 'string'}
|
||||
<input class="input" type="text" bind:value={record[field]} />
|
||||
{:else if schema[field].type === 'attachment'}
|
||||
{#if schema[field].type === 'options'}
|
||||
<Select
|
||||
secondary
|
||||
label={capitalise(schema[field].name)}
|
||||
bind:value={record[field]}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each schema[field].constraints.inclusion as opt}
|
||||
<option>{opt}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if schema[field].type === 'datetime'}
|
||||
<DatePicker
|
||||
label={capitalise(schema[field].name)}
|
||||
bind:value={record[field]} />
|
||||
{:else if schema[field].type === 'boolean'}
|
||||
<Toggle
|
||||
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]} />
|
||||
{/if}
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
{:else if schema[field].type === 'link'}
|
||||
<LinkedRecordSelector
|
||||
secondary
|
||||
bind:linkedRecords={record[field]}
|
||||
schema={schema[field]} />
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="button-block">
|
||||
<button on:click={save} class:saved>
|
||||
{#if saved}
|
||||
<div in:fade>
|
||||
<span class:saved>Success</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div>{buttonText || 'Submit Form'}</div>
|
||||
{/if}
|
||||
</button>
|
||||
<div class="buttons">
|
||||
<Button primary on:click={save} green={saved}>
|
||||
{#if saved}Success{:else}{buttonText || 'Submit Form'}{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -162,104 +176,14 @@
|
|||
}
|
||||
|
||||
.form-content {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
display: grid;
|
||||
gap: var(--spacing-xl);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
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 {
|
||||
.buttons {
|
||||
display: flex;
|
||||
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>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { cssVars, createClasses } from "./cssVars"
|
||||
import { cssVars } from "./cssVars"
|
||||
import ArrowUp from "./icons/ArrowUp.svelte"
|
||||
import ArrowDown from "./icons/ArrowDown.svelte"
|
||||
import fsort from "fast-sort"
|
||||
|
@ -13,6 +13,7 @@
|
|||
export let stripeColor
|
||||
export let borderColor
|
||||
export let datasource = {}
|
||||
export let _bb
|
||||
|
||||
let data = []
|
||||
let headers = []
|
||||
|
@ -29,11 +30,19 @@
|
|||
|
||||
$: 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 () => {
|
||||
if (!isEmpty(datasource)) {
|
||||
data = await fetchData(datasource)
|
||||
if (data) {
|
||||
headers = Object.keys(data[0]).filter(shouldDisplayField)
|
||||
if (data && data.length) {
|
||||
await fetchModel(data[0].modelId)
|
||||
headers = Object.keys(schema).filter(shouldDisplayField)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -85,11 +94,15 @@
|
|||
{#each sorted as row (row._id)}
|
||||
<tr>
|
||||
{#each headers as header}
|
||||
<!-- Rudimentary solution for attachments on array given this entire table will be replaced by AG Grid -->
|
||||
{#if Array.isArray(row[header])}
|
||||
<AttachmentList files={row[header]} />
|
||||
{:else if row[header]}
|
||||
<td>{row[header]}</td>
|
||||
{#if schema[header]}
|
||||
<!-- Rudimentary solution for attachments on array given this entire table will be replaced by AG Grid -->
|
||||
{#if schema[header].type === 'attachment'}
|
||||
<AttachmentList files={row[header]} />
|
||||
{: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}
|
||||
{/each}
|
||||
</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 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() {
|
||||
const FETCH_RECORDS_URL = `/api/views/all_${model}`
|
||||
const response = await _bb.api.get(FETCH_RECORDS_URL)
|
||||
|
@ -34,6 +40,14 @@
|
|||
}
|
||||
|
||||
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, {
|
||||
hydrate: false,
|
||||
context: record,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { FILE_TYPES } from "./fileTypes"
|
||||
|
||||
export let files
|
||||
export let files = []
|
||||
export let height = "70"
|
||||
export let width = "70"
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,28 @@ export default async function fetchData(datasource) {
|
|||
const { isModel, name } = datasource
|
||||
|
||||
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() {
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
86
yarn.lock
86
yarn.lock
|
@ -913,11 +913,6 @@ argparse@^1.0.7:
|
|||
dependencies:
|
||||
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:
|
||||
version "4.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
|
@ -2805,13 +2790,6 @@ libnpmpublish@^1.1.1:
|
|||
semver "^5.5.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:
|
||||
version "1.1.0"
|
||||
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"
|
||||
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:
|
||||
version "4.3.2"
|
||||
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"
|
||||
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:
|
||||
version "2.6.1"
|
||||
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"
|
||||
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:
|
||||
version "1.1.2"
|
||||
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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
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:
|
||||
extend-shallow "^3.0.0"
|
||||
|
||||
split2@^2.0.0, split2@^2.1.0:
|
||||
split2@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
|
||||
dependencies:
|
||||
|
@ -4396,11 +4321,6 @@ string_decoder@^1.1.1:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.1.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in New Issue