merge from master

This commit is contained in:
Michael Shanks 2020-08-11 17:31:14 +01:00
commit 9a940c0df5
38 changed files with 1810 additions and 1944 deletions

View File

@ -1,5 +1,5 @@
{
"version": "0.1.16",
"version": "0.1.17",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -19,7 +19,7 @@
"build": "lerna run build",
"initialise": "lerna run initialise",
"publishdev": "lerna run publishdev",
"publishnpm": "yarn build && lerna publish",
"publishnpm": "yarn build && lerna publish --force-publish",
"clean": "lerna clean",
"dev": "node ./scripts/symlinkDev.js && lerna run --parallel --stream dev:builder",
"test": "lerna run test",

View File

@ -5,7 +5,7 @@ xcontext('Create Components', () => {
cy.visit('localhost:4001/_builder')
// https://on.cypress.io/type
cy.createApp('Model App', 'Model App Description')
cy.createModel('dog', 'name', 'age')
cy.createTable('dog', 'name', 'age')
cy.addRecord('bob', '15')
})

View File

@ -1,22 +0,0 @@
xcontext('Create a Model', () => {
before(() => {
cy.visit('localhost:4001/_builder')
// https://on.cypress.io/type
cy.createApp('Model App', 'Model App Description')
})
// https://on.cypress.io/interacting-with-elements
it('should create a new model', () => {
cy.createModel('dog', 'name', 'age')
// Check if model exists
cy.get('.title').should('have.text', 'dog')
})
it('should add a record', () => {
cy.addRecord('bob', '15')
cy.contains('bob').should('have.text', 'bob')
})
})

View File

@ -0,0 +1,70 @@
context('Create a Table', () => {
before(() => {
cy.visit('localhost:4001/_builder')
cy.createApp('Table App', 'Table App Description')
})
it('should create a new Table', () => {
cy.createTable('dog')
// Check if Table exists
cy.get('.title').should('have.text', 'dog')
})
it('adds a new column to the table', () => {
cy.addColumn('dog', 'name', 'Plain Text')
cy.contains('name').should("be.visible")
})
it('creates a record in the table', () => {
cy.addRecord(["Rover"])
cy.contains('Rover').should("be.visible")
})
it('updates a column on the table', () => {
cy.contains("name").click()
cy.get("[data-cy='edit-column-header']").click()
cy.get("[placeholder=Name]").type("updated")
cy.get("select").select("Plain Text")
cy.contains("Save Column").click()
cy.contains('nameupdated').should('have.text', 'nameupdated ')
})
it('edits a record', () => {
cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=edit-row]").click()
cy.get(".actions input").type("updatedRecord")
cy.contains("Save").click()
cy.contains('updatedRecord').should('have.text', 'updatedRecord')
})
it('deletes a record', () => {
cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=delete-row]").click()
cy.get(".modal-actions").contains("Delete").click()
cy.contains('updatedRecord').should('not.exist')
})
it('deletes a column', () => {
cy.contains("name").click()
cy.get("[data-cy='delete-column-header']").click()
cy.contains('nameupdated').should('not.exist')
})
it('deletes a table', () => {
cy.contains("div", "dog").get(".ri-more-line").click()
cy.get("[data-cy=delete-table]").click()
cy.get(".modal-actions").contains("Delete").click()
cy.contains('dog').should('not.exist')
})
})

View File

@ -9,7 +9,7 @@ xcontext('Create a workflow', () => {
// https://on.cypress.io/interacting-with-elements
it('should create a workflow', () => {
cy.createModel('dog', 'name', 'age')
cy.createTable('dog', 'name', 'age')
cy.contains('workflow').click()
cy.contains('Create New Workflow').click()

View File

@ -57,29 +57,37 @@ Cypress.Commands.add("createApp", name => {
})
})
Cypress.Commands.add("createModel", modelName => {
Cypress.Commands.add("createTable", tableName => {
// Enter model name
cy.contains("Create New Table").click()
cy.get("[data-cy=table-name-input]").type(modelName)
cy.get("[placeholder='Table Name']").type(tableName)
// Add 'name' field
cy.contains("Add").click()
cy.contains("Plain Text").click()
// Add 'age' field
cy.contains("Add").click()
cy.contains("Number").click()
cy.contains("Save").click()
cy.contains(modelName).click()
cy.contains(tableName).should("be.visible")
})
Cypress.Commands.add("addRecord", (firstField, secondField) => {
cy.contains("Create New Record").click()
Cypress.Commands.add("addColumn", (tableName, columnName, type) => {
// Select Table
cy.contains(tableName).click()
cy.contains("Create New Column").click()
cy.get("[data-cy='Plain Text-input']").type(firstField)
cy.get("[data-cy=Number-input]").type(secondField)
cy.get("[placeholder=Name]").type(columnName)
cy.get("select").select(type)
cy.contains("Save Column")
cy.contains("Save").click()
})
Cypress.Commands.add("addRecord", values => {
cy.contains("Create New Row").click()
for (let i = 0; i < values.length; i++) {
cy.get("input")
.eq(i)
.type(values[i])
}
// Save
cy.contains("Save").click()

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.1.16",
"version": "0.1.17",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -58,7 +58,7 @@
},
"dependencies": {
"@budibase/bbui": "^1.23.1",
"@budibase/client": "^0.1.1",
"@budibase/client": "^0.1.17",
"@budibase/colorpicker": "^1.0.1",
"@nx-js/compiler-util": "^2.0.0",
"@sentry/browser": "5.19.1",
@ -68,6 +68,7 @@
"d3-selection": "^1.4.1",
"date-fns": "^1.29.0",
"deepmerge": "^4.2.2",
"fast-sort": "^2.2.0",
"feather-icons": "^4.21.0",
"flatpickr": "^4.5.7",
"lodash": "^4.17.13",
@ -98,6 +99,7 @@
"cypress-terminal-report": "^1.4.1",
"eslint-plugin-cypress": "^2.11.1",
"http-proxy-middleware": "^0.19.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.2.2",
"ncp": "^2.0.0",
"npm-run-all": "^4.1.5",

View File

@ -61,10 +61,9 @@ export const getBackendUiStore = () => {
state.draftModel = cloneDeep(model)
state.selectedField = ""
state.selectedView = `all_${model._id}`
state.tabs.SETUP_PANEL = "SETUP"
return state
}),
save: async ({ model }) => {
save: async model => {
const updatedModel = cloneDeep(model)
// update any renamed schema keys to reflect their names
@ -83,20 +82,35 @@ export const getBackendUiStore = () => {
const savedModel = await response.json()
await store.actions.models.fetch()
store.actions.models.select(savedModel)
return savedModel
},
addField: field => {
delete: async model => {
await api.delete(`/api/models/${model._id}/${model._rev}`)
store.update(state => {
if (!state.draftModel.schema) {
state.draftModel.schema = {}
}
state.models = state.models.filter(
existing => existing._id !== model._id
)
state.selectedModel = state.models[0] || {}
return state
})
},
saveField: ({ originalName, field }) => {
store.update(state => {
// delete the original if renaming
delete state.draftModel.schema[originalName]
state.draftModel.schema = {
...state.draftModel.schema,
[field.name]: cloneDeep(field),
}
state.selectedField = field.name
state.tabs.NAVIGATION_PANEL = "NAVIGATE"
store.actions.models.save(state.draftModel)
return state
})
},
deleteField: field => {
store.update(state => {
delete state.draftModel.schema[field.name]
store.actions.models.save(state.draftModel)
return state
})
},

View File

@ -1,38 +1,19 @@
<script>
import { onMount, getContext } from "svelte"
import { onMount } from "svelte"
import fsort from "fast-sort"
import { store, backendUiStore } from "builderStore"
import { Button } from "@budibase/bbui"
import { Button, Icon } from "@budibase/bbui"
import Select from "components/common/Select.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte"
import * as api from "./api"
const { open, close } = getContext("simple-modal")
const editRecord = async row => {
open(
CreateEditRecordModal,
{
onClosed: close,
record: row,
},
{ styleContent: { padding: "0" } }
)
}
const deleteRecord = async row => {
open(
DeleteRecordModal,
{
onClosed: close,
record: row,
},
{ styleContent: { padding: "0" } }
)
}
const ITEMS_PER_PAGE = 10
// Internal headers we want to hide from the user
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
@ -58,6 +39,8 @@
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
)
: []
$: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
$: headers = Object.keys($backendUiStore.selectedModel.schema).filter(
id => !INTERNAL_HEADERS.includes(id)
@ -85,53 +68,41 @@
<section>
<div class="table-controls">
<h2 class="title">{$backendUiStore.selectedModel.name}</h2>
<Button primary on:click={createNewRecord}>
<span class="button-inner">Create New Record</span>
</Button>
<div class="popovers">
<ColumnPopover />
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
<RowPopover />
{/if}
</div>
</div>
<table class="uk-table">
<thead>
<tr>
<th>Edit</th>
<th class="edit-header">
<div>Edit</div>
</th>
{#each headers as header}
<th>{$backendUiStore.selectedModel.schema[header].name}</th>
<th>
<ColumnHeaderPopover
field={$backendUiStore.selectedModel.schema[header]} />
</th>
{/each}
</tr>
</thead>
<tbody>
{#if paginatedData.length === 0}
{#if sorted.length === 0}
<div class="no-data">No Data.</div>
{/if}
{#each paginatedData as row}
<tr class="hoverable">
{#each sorted as row}
<tr>
<td>
<div class="uk-inline">
<i class="ri-more-line" />
<div uk-dropdown="mode: click">
<ul class="uk-nav uk-dropdown-nav">
<li
on:click={() => {
editRecord(row)
}}>
<i class="ri-edit-line" />
<div class="label">Edit</div>
</li>
<li
on:click={() => {
deleteRecord(row)
}}>
<i class="ri-delete-bin-2-line" />
<div class="label">Delete</div>
</li>
</ul>
</div>
</div>
<EditRowPopover {row} />
</td>
{#each headers as header}
<td>
<td class="hoverable">
{#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} />
{:else}{row[header]}{/if}
{:else}{row[header] || ''}{/if}
</td>
{/each}
</tr>
@ -164,7 +135,8 @@
}
thead {
background: var(--blue-light);
height: 40px;
background: var(--grey-3);
border: 1px solid var(--grey-4);
}
@ -174,6 +146,28 @@
font-weight: 500;
font-size: 14px;
text-rendering: optimizeLegibility;
transition: 0.5s all;
vertical-align: middle;
}
.edit-header {
width: 100px;
cursor: default;
}
.edit-header:hover {
color: var(--ink);
}
th:hover {
color: var(--blue);
cursor: pointer;
}
td {
max-width: 200px;
text-overflow: ellipsis;
border: 1px solid var(--grey-4);
}
tbody tr {
@ -188,47 +182,14 @@
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-top: 10px;
width: 100%;
}
.ri-more-line:hover,
.uk-dropdown-nav li:hover {
cursor: pointer;
.popovers {
display: flex;
}
.no-data {
padding: 20px;
}
.button-inner {
display: flex;
align-items: center;
}
li {
display: flex;
align-items: center;
border-radius: 5px;
}
i {
color: var(--grey-7);
margin-right: 8px;
font-size: 20px;
}
.label {
color: var(--grey-7);
font-size: 14px;
font-family: inter;
font-weight: 400;
margin: 12px 0px;
}
.label:hover {
color: var(--ink);
cursor: pointer;
padding: 14px;
}
</style>

View File

@ -1,40 +0,0 @@
<script>
import { store } from "builderStore"
import Modal from "components/common/Modal.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import * as api from "../api"
export let onClosed
let databaseName
async function createDatabase() {
const response = await api.createDatabase($store.appId, databaseName)
store.createDatabaseForApp(response)
onClosed()
}
</script>
<div>
<section>
Database Name
<input class="uk-input" type="text" bind:value={databaseName} />
</section>
<footer>
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
<ActionButton disabled={!databaseName} on:click={createDatabase}>
Save
</ActionButton>
</footer>
</div>
<style>
section {
padding: 30px;
}
footer {
padding: 20px;
background: var(--grey-1);
border-radius: 0.5rem;
}
</style>

View File

@ -0,0 +1,140 @@
<script>
import { onMount } from "svelte"
import { Input, TextArea, Button, Select } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import { FIELDS } from "constants/backend"
import { notifier } from "builderStore/store/notifications"
import Dropdown from "components/common/Dropdown.svelte"
import Textbox from "components/common/Textbox.svelte"
import ButtonGroup from "components/common/ButtonGroup.svelte"
import NumberBox from "components/common/NumberBox.svelte"
import ValuesList from "components/common/ValuesList.svelte"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import Checkbox from "components/common/Checkbox.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import DatePicker from "components/common/DatePicker.svelte"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import * as api from "../api"
export let onClosed
export let field = {}
let originalName = field.name
$: required =
field.constraints &&
field.constraints.presence &&
!field.constraints.presence.allowEmpty
$: if (field.type) {
field.constraints = FIELDS[field.type.toUpperCase()].constraints
}
async function saveColumn() {
backendUiStore.update(state => {
backendUiStore.actions.models.saveField({
originalName,
field,
})
return state
})
onClosed()
}
</script>
<div class="actions">
<Input placeholder="Name" thin bind:value={field.name} />
<Select secondary thin bind:value={field.type}>
{#each Object.values(FIELDS) as field}
<option value={field.type}>{field.name}</option>
{/each}
</Select>
<div class="info">
<div class="field">
<label>Required</label>
<input
type="checkbox"
bind:checked={required}
on:change={() => (field.constraints.presence.allowEmpty = required)} />
</div>
{#if field.type === 'string'}
<NumberBox
label="Max Length"
bind:value={field.constraints.length.maximum} />
<ValuesList
label="Categories"
bind:values={field.constraints.inclusion} />
{:else if field.type === 'datetime'}
<DatePicker
label="Min Value"
bind:value={field.constraints.datetime.earliest} />
<DatePicker
label="Max Value"
bind:value={field.constraints.datetime.latest} />
{:else if field.type === 'number'}
<NumberBox
label="Min Value"
bind:value={field.constraints.numericality.greaterThanOrEqualTo} />
<NumberBox
label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'}
<div class="field">
<label>Link</label>
<select class="budibase__input" bind:value={field.modelId}>
<option value={''} />
{#each $backendUiStore.models as model}
{#if model._id !== $backendUiStore.draftModel._id}
<option value={model._id}>{model.name}</option>
{/if}
{/each}
</select>
</div>
{/if}
</div>
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={saveColumn}>Save Column</Button>
</div>
</footer>
<style>
.actions {
padding: var(--spacing-l) var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
}
footer {
padding: 20px 30px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.field {
margin-top: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View File

@ -1,165 +0,0 @@
<script>
import { tick } from "svelte"
import Textbox from "components/common/Textbox.svelte"
import Button from "components/common/Button.svelte"
import Select from "components/common/Select.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import getIcon from "components/common/icon"
import FieldView from "./FieldView.svelte"
import api from "builderStore/api"
import { store, backendUiStore } from "builderStore"
import { pipe } from "components/common/core"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let model = { schema: {} }
export let onClosed
let showFieldView = false
let fieldToEdit
$: modelFields = model.schema ? Object.entries(model.schema) : []
function editField() {}
function deleteField() {}
function onFinishedFieldEdit() {}
async function saveModel() {
const SAVE_MODEL_URL = `/api/models`
const response = await api.post(SAVE_MODEL_URL, model)
const newModel = await response.json()
backendUiStore.actions.models.create(newModel)
onClosed()
}
</script>
<div class="heading">
{#if !showFieldView}
<i class="ri-list-settings-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit Table</h3>
{:else}
<i class="ri-file-list-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit Field</h3>
{/if}
</div>
{#if !showFieldView}
<div class="padding">
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
<div class="textbox">
<Textbox label="Name" bind:text={model.name} />
</div>
<div class="table-controls">
<span class="label">Fields</span>
<div
data-cy="add-new-model-field"
class="hoverable new-field"
on:click={() => (showFieldView = true)}>
Add new field
</div>
</div>
<table class="uk-table fields-table budibase__table">
<thead>
<tr>
<th>Edit</th>
<th>Name</th>
<th>Type</th>
<th />
</tr>
</thead>
<tbody>
{#each modelFields as [key, meta]}
<tr>
<td>
<i class="ri-more-line" on:click={() => editField(meta)} />
</td>
<td>
<div>{key}</div>
</td>
<td>{meta.type}</td>
<td>
<i
class="ri-delete-bin-6-line hoverable"
on:click={() => deleteField(meta)} />
</td>
</tr>
{/each}
</tbody>
</table>
<footer>
<ActionButton color="secondary" on:click={saveModel}>Save</ActionButton>
</footer>
</div>
{:else}
<FieldView
field={fieldToEdit}
onFinished={onFinishedFieldEdit}
schema={model.schema}
goBack={() => (showFieldView = false)} />
{/if}
<style>
.padding {
padding-top: 40px;
}
.label {
font-size: 14px;
font-weight: 500;
}
.textbox {
margin: 0px 40px 0px 40px;
font-size: 14px;
font-weight: 500;
}
.new-field {
font-size: 16px;
font-weight: bold;
color: var(--blue);
}
.fields-table {
margin: 8px 40px 0px 40px;
border-collapse: collapse;
width: 88%;
}
tbody > tr:hover {
background-color: var(--grey-1);
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0px 40px;
}
.ri-more-line:hover {
cursor: pointer;
}
.heading {
padding: 40px 40px 0 40px;
display: flex;
align-items: center;
}
h3 {
margin: 0 0 0 10px;
color: var(--ink);
}
footer {
background-color: var(--grey-1);
margin-top: 40px;
padding: 20px 40px 20px 40px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -20,10 +20,6 @@
? Object.entries($backendUiStore.selectedModel.schema)
: []
function closed() {
onClosed()
}
const isSelect = meta =>
meta.type === "string" &&
meta.constraints &&
@ -68,10 +64,6 @@
</script>
<div class="actions">
<header>
<i class="ri-file-user-fill" />
<h4>Create / Edit Record</h4>
</header>
<ErrorsBox {errors} />
<form on:submit|preventDefault class="uk-form-stacked">
{#each modelSchema as [key, meta]}
@ -97,42 +89,13 @@
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button blue on:click={saveRecord}>Save</Button>
<Button primary on:click={saveRecord}>Save</Button>
</div>
</footer>
<style>
header {
margin-bottom: 40px;
display: grid;
grid-gap: 20px;
grid-template-columns: 40px 1fr;
align-items: center;
}
i {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--blue-light);
color: var(--grey-7);
font-size: 20px;
border-radius: 5px;
}
h4 {
display: inline-block;
font-size: 24px;
font-weight: 600;
font-family: sans-serif;
color: var(--ink);
margin: 0;
}
.actions {
padding: 30px;
padding: var(--spacing-l) var(--spacing-xl);
}
footer {

View File

@ -0,0 +1,64 @@
<script>
import ActionButton from "components/common/ActionButton.svelte"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore } from "builderStore"
import * as api from "../api"
export let table
export let onClosed
function deleteTable() {
backendUiStore.actions.models.delete(table)
}
</script>
<section>
<div class="content">
<heading>
<i class="ri-information-line alert" />
<h4 class="budibase__title--4">Delete Table</h4>
</heading>
<p>
Are you sure you want to delete this table? All of your data will be
permanently removed. This action cannot be undone.
</p>
</div>
<div class="modal-actions">
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton
alert
on:click={async () => {
await backendUiStore.actions.models.delete(table)
notifier.danger('Table deleted')
onClosed()
}}>
Delete
</ActionButton>
</div>
</section>
<style>
.alert {
color: rgba(255, 0, 31, 1);
background: var(--grey-1);
padding: 5px;
}
.modal-actions {
padding: 10px;
background: var(--grey-1);
border-top: 1px solid #ccc;
}
heading {
display: flex;
align-items: center;
}
.content {
padding: 30px;
}
h4 {
margin: 0 0 0 10px;
}
</style>

View File

@ -1,18 +1,13 @@
<script>
import { Input, Select } from "@budibase/bbui"
export let type = "text"
export let value = ""
export let label
export let errors = []
export let options = []
let checked = type === "checkbox" ? value : false
const determineClassName = type => {
if (type === "checkbox") return "uk-checkbox"
if (type === "select") return "uk-select"
return "uk-input"
}
const handleInput = event => {
if (event.target.type === "checkbox") {
value = event.target.checked
@ -28,40 +23,21 @@
}
</script>
<label>{label}</label>
{#if type === 'select'}
<select
data-cy="{label}-select"
class={determineClassName(type)}
bind:value
class:uk-form-danger={errors.length > 0}>
<Select thin secondary data-cy="{label}-select" bind:value>
<option />
{#each options as opt}
<option value={opt}>{opt}</option>
{/each}
</select>
</Select>
{:else}
<input
<Input
thin
placeholder={label}
data-cy="{label}-input"
class={determineClassName(type)}
class:uk-form-danger={errors.length > 0}
{checked}
{type}
{value}
on:input={handleInput}
on:change={handleInput} />
{/if}
<style>
label {
display: block;
font-size: 18px;
font-weight: 500;
margin-bottom: 12px;
}
input {
color: var(--dark-grey);
}
</style>

View File

@ -1,5 +1,4 @@
export { default as DeleteRecordModal } from "./DeleteRecord.svelte"
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte"
export { default as CreateEditViewModal } from "./CreateEditView.svelte"
export { default as CreateDatabaseModal } from "./CreateDatabase.svelte"
export { default as CreateUserModal } from "./CreateUser.svelte"

View File

@ -0,0 +1,31 @@
<script>
import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import { ModelSetupNav } from "components/nav/ModelSetupNav"
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte"
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
let anchor
let dropdown
let fieldName
</script>
<div bind:this={anchor}>
<Button text small on:click={dropdown.show}>
<Icon name="addcolumn" />
Create New Column
</Button>
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<h5>Create Column</h5>
<CreateEditColumn onClosed={dropdown.hide} />
</DropdownMenu>
<style>
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
</style>

View File

@ -0,0 +1,107 @@
<script>
import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import { ModelSetupNav } from "components/nav/ModelSetupNav"
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte"
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
export let field
let anchor
let dropdown
let editing
$: sortColumn = $backendUiStore.sort && $backendUiStore.sort.column
$: sortDirection = $backendUiStore.sort && $backendUiStore.sort.direction
function showEditor() {
editing = true
}
function hideEditor() {
dropdown.hide()
editing = false
}
function deleteField() {
backendUiStore.actions.models.deleteField(field)
}
function sort(direction, column) {
backendUiStore.update(state => {
state.sort = { direction, column }
return state
})
hideEditor()
}
</script>
<div bind:this={anchor} on:click={dropdown.show}>
{field.name}
<Icon name="arrowdown" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing}
<h5>Edit Column</h5>
<CreateEditColumn onClosed={hideEditor} {field} />
{:else}
<ul>
<li data-cy="edit-column-header" on:click={showEditor}>
<Icon name="edit" />
Edit
</li>
<li data-cy="delete-column-header" on:click={deleteField}>
<Icon name="delete" />
Delete
</li>
{#if sortDirection === 'desc' || sortColumn !== field.name}
<li on:click={() => sort('asc', field.name)}>
<Icon name="sortascending" />
Sort A - Z
</li>
{/if}
{#if sortDirection === 'asc' || sortColumn !== field.name}
<li on:click={() => sort('desc', field.name)}>
<Icon name="sortdescending" />
Sort Z - A
</li>
{/if}
</ul>
{/if}
</DropdownMenu>
<style>
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
ul {
list-style: none;
padding-left: 0;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
</style>

View File

@ -0,0 +1,97 @@
<script>
import { getContext } from "svelte"
import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
import DeleteRecordModal from "../modals/DeleteRecord.svelte"
const { open, close } = getContext("simple-modal")
export let row
let anchor
let dropdown
let editing
function showEditor() {
editing = true
}
function hideEditor() {
dropdown.hide()
editing = false
close()
}
const deleteRow = () => {
open(
DeleteRecordModal,
{
onClosed: hideEditor,
record: row,
},
{ styleContent: { padding: "0" } }
)
}
</script>
<div bind:this={anchor} on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing}
<h5>Edit Row</h5>
<CreateEditRecord onClosed={hideEditor} record={row} />
{:else}
<ul>
<li data-cy="edit-row" on:click={showEditor}>
<Icon name="edit" />
Edit
</li>
<li data-cy="delete-row" on:click={deleteRow}>
<Icon name="delete" />
Delete
</li>
</ul>
{/if}
</DropdownMenu>
<style>
.ri-more-line:hover {
cursor: pointer;
}
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
ul {
list-style: none;
padding-left: 0;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
</style>

View File

@ -0,0 +1,26 @@
<script>
import { DropdownMenu, Button, Icon } from "@budibase/bbui"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
let anchor
let dropdown
</script>
<div bind:this={anchor}>
<Button text small on:click={dropdown.show}>
<Icon name="addrow" />
Create New Row
</Button>
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<h5>Add New Row</h5>
<CreateEditRecord onClosed={dropdown.hide} />
</DropdownMenu>
<style>
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
</style>

View File

@ -1,91 +0,0 @@
<script>
import * as blockDefinitions from "constants/backend"
import { backendUiStore } from "builderStore"
import Block from "components/common/Block.svelte"
const HEADINGS = [
{
title: "Fields",
key: "FIELDS",
},
{
title: "Blocks",
key: "BLOCKS",
},
]
let selectedTab = "FIELDS"
function addField(blockDefinition) {
backendUiStore.actions.models.addField(blockDefinition)
backendUiStore.actions.models.fetch()
}
</script>
<section>
<header>
{#each HEADINGS as tab}
<span
class:selected={selectedTab === tab.key}
on:click={() => (selectedTab = tab.key)}>
{tab.title}
</span>
{/each}
</header>
<div class="block-grid">
{#each Object.values(blockDefinitions[selectedTab]) as blockDefinition}
<Block
on:click={() => addField(blockDefinition)}
title={blockDefinition.name}
icon={blockDefinition.icon} />
{/each}
</div>
</section>
<style>
header {
margin-top: 20px;
margin-bottom: 20px;
display: grid;
grid-template-columns: repeat(3, 1fr);
}
span {
cursor: pointer;
display: grid;
justify-content: center;
align-content: center;
padding: 0px 16px;
height: 36px;
text-align: center;
background: #ffffff;
color: var(--grey-7);
border-radius: 5px;
font-family: inter;
font-size: 14px;
font-weight: 400;
transition: all 0.3s;
text-rendering: optimizeLegibility;
border: none !important;
transition: 0.2s;
outline: none;
}
span:hover {
color: var(--ink);
cursor: pointer;
}
.selected {
background: var(--grey-3);
color: var(--ink);
}
.block-grid {
margin-top: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
}
</style>

View File

@ -0,0 +1,77 @@
<script>
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
export let table
let anchor
let dropdown
let name
async function saveTable() {
const model = await backendUiStore.actions.models.save({
name,
schema: {},
})
notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`)
dropdown.hide()
}
</script>
<div bind:this={anchor}>
<Button primary wide on:click={dropdown.show}>Create New Table</Button>
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<div class="container">
<h5>Create Table</h5>
<Input
data-cy="table-name-input"
placeholder="Table Name"
thin
bind:value={name} />
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={saveTable}>Save</Button>
</div>
</footer>
</DropdownMenu>
<style>
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
.container {
padding: var(--spacing-l);
margin: 0;
}
footer {
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View File

@ -0,0 +1,137 @@
<script>
import { getContext } from "svelte"
import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import DeleteTableModal from "components/database/ModelDataTable/modals/DeleteTable.svelte"
const { open, close } = getContext("simple-modal")
export let table
let anchor
let dropdown
let editing
function showEditor() {
editing = true
}
function hideEditor() {
dropdown.hide()
editing = false
close()
}
const deleteTable = () => {
open(
DeleteTableModal,
{
onClosed: close,
table,
},
{ styleContent: { padding: "0" } }
)
}
function save() {
backendUiStore.actions.models.save(table)
hideEditor()
}
</script>
<div bind:this={anchor} on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing}
<h5>Edit Table</h5>
<div class="container">
<Input placeholder="Table Name" thin bind:value={table.name} />
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={hideEditor}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={save}>Save</Button>
</div>
</footer>
{:else}
<ul>
<li on:click={showEditor}>
<Icon name="edit" />
Edit
</li>
<li data-cy="delete-table" on:click={deleteTable}>
<Icon name="delete" />
Delete
</li>
</ul>
{/if}
</DropdownMenu>
<style>
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
.container {
padding: var(--spacing-xl);
}
ul {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
list-style: none;
padding-left: 0;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
footer {
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-1 {
grid-column-start: 1;
display: grid;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

View File

@ -1,107 +0,0 @@
<script>
import { backendUiStore } from "builderStore"
import { uuid } from "builderStore/uuid"
import { fade } from "svelte/transition"
import { notifier } from "builderStore/store/notifications"
import { FIELDS, BLOCKS, MODELS } from "constants/backend"
import Block from "components/common/Block.svelte"
function addNewField(field) {
backendUiStore.actions.models.addField(field)
}
function createModel(model) {
const { schema, ...rest } = $backendUiStore.selectedModel
backendUiStore.actions.models.save({
model: {
...model,
...rest,
},
})
notifier.success(`${model.name} table created.`)
}
</script>
<section transition:fade>
<header>
<h2>Create New Table</h2>
<p>Before you can view your table, you need to set it up.</p>
</header>
<div class="block-row">
<span class="block-row-title">Fields</span>
<p>Blocks are pre-made fields and help you build your table quicker.</p>
<div class="blocks">
{#each Object.values(FIELDS) as field}
<Block
primary
title={field.name}
icon={field.icon}
on:click={() => addNewField(field)} />
{/each}
</div>
</div>
<div class="block-row">
<span class="block-row-title">Blocks</span>
<p>Blocks are pre-made fields and help you build your table quicker.</p>
<div class="blocks">
{#each Object.values(BLOCKS) as field}
<Block
secondary
title={field.name}
icon={field.icon}
on:click={() => addNewField(field)} />
{/each}
</div>
</div>
<div class="block-row">
<span class="block-row-title">Tables</span>
<p>Blocks are pre-made fields and help you build your table quicker.</p>
<div class="blocks">
{#each Object.values(MODELS) as model}
<Block
tertiary
title={model.name}
icon={model.icon}
on:click={() => createModel(model)} />
{/each}
</div>
</div>
</section>
<style>
section {
height: 100vh;
}
h2 {
font-size: 20px;
font-weight: bold;
margin: 0;
}
.block-row-title {
font-weight: 500;
font-size: 16px;
}
p {
margin-top: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.block-row {
margin-top: 40px;
}
.block-row .blocks {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 110px;
grid-gap: 20px;
}
</style>

View File

@ -9,6 +9,7 @@
<div class:selected on:click class={className}>
<i class:indented class={icon} />
<span>{title}</span>
<slot />
</div>
<style>
@ -20,7 +21,8 @@
padding: 0 10px 0 10px;
height: 36px;
border-radius: 5px;
display: flex;
display: grid;
grid-template-columns: 30px 1fr 20px;
align-items: center;
transition: 0.3s background-color;
color: var(--ink);

View File

@ -4,15 +4,20 @@
import { Switcher } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { store, backendUiStore } from "builderStore"
import BlockNavigator from "./BlockNavigator.svelte"
import ListItem from "./ListItem.svelte"
import { Button } from "@budibase/bbui"
import CreateTablePopover from "./CreateTable.svelte"
import EditTablePopover from "./EditTable.svelte"
const { open, close } = getContext("simple-modal")
let HEADINGS = [
{
title: "Navigate",
title: "Tables",
key: "TABLES",
},
{
title: "Tables",
key: "NAVIGATE",
},
{
@ -33,53 +38,25 @@
})
}
}
function setupForNewModel() {
backendUiStore.update(state => {
state.selectedModel = {}
state.draftModel = { schema: {} }
state.tabs.SETUP_PANEL = "SETUP"
return state
})
}
</script>
<div class="items-root">
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
<div class="hierarchy">
<div class="components-list-container">
<Switcher
headings={HEADINGS}
bind:value={$backendUiStore.tabs.NAVIGATION_PANEL}>
{#if selectedTab === 'NAVIGATE'}
<Button purple wide on:click={setupForNewModel}>
Create New Table
</Button>
<h3>Tables</h3>
<CreateTablePopover />
<div class="hierarchy-items-container">
{#each $backendUiStore.models as model}
<ListItem
selected={!$backendUiStore.selectedField && model._id === $backendUiStore.selectedModel._id}
title={model.name}
icon="ri-table-fill"
on:click={() => selectModel(model)} />
{#if model._id === $backendUiStore.selectedModel._id}
<div in:slide>
{#each Object.keys(model.schema) as fieldName}
<ListItem
selected={model._id === $backendUiStore.selectedModel._id && fieldName === $backendUiStore.selectedField}
indented
icon="ri-layout-column-line"
title={model.schema[fieldName].name}
on:click={() => selectModel(model, fieldName)} />
on:click={() => selectModel(model)}>
<EditTablePopover table={model} />
</ListItem>
{/each}
</div>
{/if}
{/each}
</div>
{:else if selectedTab === 'ADD'}
<BlockNavigator />
{/if}
</Switcher>
</div>
</div>
{/if}

View File

@ -35,7 +35,7 @@
if (field) {
const name = model.schema[field].name
delete model.schema[field]
backendUiStore.actions.models.save({ model })
backendUiStore.actions.models.save(model)
notifier.danger(`Field ${name} deleted.`)
return
}
@ -81,9 +81,7 @@
return
}
await backendUiStore.actions.models.save({
model: $backendUiStore.draftModel,
})
await backendUiStore.actions.models.save($backendUiStore.draftModel)
notifier.success(
"Success! Your changes have been saved. Please continue on with your greatness."
)

View File

@ -1,5 +1,5 @@
export const FIELDS = {
PLAIN_TEXT: {
STRING: {
name: "Plain Text",
icon: "ri-text",
type: "string",
@ -65,15 +65,15 @@ export const FIELDS = {
// presence: { allowEmpty: true },
// },
// },
LINKED_FIELDS: {
name: "Linked Fields",
icon: "ri-link",
type: "link",
modelId: null,
constraints: {
type: "array",
},
},
// LINKED_FIELDS: {
// name: "Linked Fields",
// icon: "ri-link",
// type: "link",
// modelId: null,
// constraints: {
// type: "array",
// },
// },
}
export const BLOCKS = {
@ -195,36 +195,3 @@ export const BLOCKS = {
// },
// },
}
export const MODELS = {
CONTACTS: {
icon: "ri-contacts-book-line",
name: "Contacts",
schema: {
Name: BLOCKS.NAME,
"Phone Number": BLOCKS.PHONE_NUMBER,
},
},
RECIPES: {
icon: "ri-link",
name: "Recipes",
schema: {
Name: BLOCKS.NAME,
Cuisine: {
...FIELDS.PLAIN_TEXT,
name: "Cuisine",
},
},
},
SPORTS_TEAM: {
icon: "ri-basketball-line",
name: "Sports Team",
schema: {
Name: BLOCKS.NAME,
Championships: {
...FIELDS.NUMBER,
name: "Championships",
},
},
},
}

View File

@ -14,16 +14,13 @@
<div class="content">
<slot />
</div>
<div class="nav">
<ModelSetupNav />
</div>
</div>
<style>
.root {
height: 100%;
display: grid;
grid-template-columns: 300px minmax(0, 1fr) 300px;
grid-template-columns: 300px minmax(0, 1fr);
background: var(--grey-1);
line-height: 1;
}

View File

@ -1,7 +1,6 @@
<script>
import { getContext } from "svelte"
import { Button } from "@budibase/bbui"
import EmptyModel from "components/nav/ModelNavigator/EmptyModel.svelte"
import ModelDataTable from "components/database/ModelDataTable"
import { backendUiStore } from "builderStore"
import ActionButton from "components/common/ActionButton.svelte"
@ -11,16 +10,6 @@
const { open, close } = getContext("simple-modal")
$: selectedModel = $backendUiStore.selectedModel
const createNewRecord = () => {
open(
CreateEditRecordModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
</script>
{#if $backendUiStore.selectedDatabase._id && selectedModel.name}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "budibase",
"version": "0.1.16",
"version": "0.1.17",
"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.16",
"@budibase/server": "^0.1.17",
"@inquirer/password": "^0.0.6-alpha.0",
"chalk": "^2.4.2",
"dotenv": "^8.2.0",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.1.1",
"version": "0.1.17",
"license": "MPL-2.0",
"main": "dist/budibase-client.js",
"module": "dist/budibase-client.esm.mjs",

View File

@ -12,8 +12,8 @@
"dev:builder": "rollup -cw"
},
"devDependencies": {
"@budibase/client": "^0.1.1",
"@budibase/standard-components": "^0.1.14",
"@budibase/client": "^0.1.17",
"@budibase/standard-components": "^0.1.17",
"@material/button": "^4.0.0",
"@material/checkbox": "^4.0.0",
"@material/data-table": "4.0.0",
@ -50,7 +50,7 @@
"keywords": [
"svelte"
],
"version": "0.1.16",
"version": "0.1.17",
"license": "MIT",
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/server",
"version": "0.1.16",
"version": "0.1.17",
"description": "Budibase Web Server",
"main": "src/electron.js",
"repository": {
@ -42,7 +42,7 @@
"author": "Michael Shanks",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/client": "^0.1.1",
"@budibase/client": "^0.1.17",
"@koa/router": "^8.0.0",
"@sendgrid/mail": "^7.1.1",
"@sentry/node": "^5.19.2",

View File

@ -12,7 +12,7 @@
"dev:builder": "rollup -cw"
},
"devDependencies": {
"@budibase/client": "^0.1.1",
"@budibase/client": "^0.1.17",
"@nx-js/compiler-util": "^2.0.0",
"@rollup/plugin-commonjs": "^11.1.0",
"bcryptjs": "2.4.3",
@ -33,7 +33,7 @@
"keywords": [
"svelte"
],
"version": "0.1.16",
"version": "0.1.17",
"license": "MIT",
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
"dependencies": {