add new models and fields, delete models

This commit is contained in:
Martin McKeaveney 2020-06-17 16:51:10 +01:00
parent 374a6b4bc6
commit 5f8d6bbfe4
14 changed files with 295 additions and 60 deletions

View File

@ -77,7 +77,8 @@
.budibase__input { .budibase__input {
height: 35px; height: 35px;
width: 220px; width: 100%;
max-width: 220px;
border-radius: 3px; border-radius: 3px;
border: 1px solid var(--grey-dark); border: 1px solid var(--grey-dark);
text-align: left; text-align: left;

View File

@ -25,9 +25,7 @@ export const getBackendUiStore = () => {
store.update(state => { store.update(state => {
state.selectedDatabase = db state.selectedDatabase = db
if (models && models.length > 0) { if (models && models.length > 0) {
state.selectedModel = models[0] store.actions.models.select(models[0]);
state.draftModel = models[0]
state.selectedView = `all_${models[0]._id}`
} }
state.breadcrumbs = [db.name] state.breadcrumbs = [db.name]
state.models = models state.models = models
@ -56,14 +54,25 @@ export const getBackendUiStore = () => {
models: { models: {
select: model => store.update(state => { select: model => store.update(state => {
state.selectedModel = model; state.selectedModel = model;
// TODO: prevent pointing to same obj
state.draftModel = cloneDeep(model); state.draftModel = cloneDeep(model);
state.selectedField = null state.selectedField = ""
state.selectedView = `all_${model._id}`
return state; return state;
}), }),
save: async ({ instanceId, model }) => { save: async ({ instanceId, model }) => {
const updatedModel = cloneDeep(model);
// TODO: refactor
for (let key in updatedModel.schema) {
const field = updatedModel.schema[key]
if (field.name && field.name !== key) {
updatedModel.schema[field.name] = field
delete updatedModel.schema[key];
}
}
const SAVE_MODEL_URL = `/api/${instanceId}/models` const SAVE_MODEL_URL = `/api/${instanceId}/models`
const response = await api.post(SAVE_MODEL_URL, model) const response = await api.post(SAVE_MODEL_URL, updatedModel)
const savedModel = await response.json() const savedModel = await response.json()
store.update(state => { store.update(state => {
@ -76,9 +85,7 @@ export const getBackendUiStore = () => {
state.models = state.models state.models = state.models
} }
state.selectedModel = savedModel store.actions.models.select(savedModel)
state.draftModel = savedModel
state.selectedView = `all_${savedModel._id}`
return state return state
}) })
}, },
@ -93,11 +100,11 @@ export const getBackendUiStore = () => {
[field.name]: field [field.name]: field
} }
state.selectedField = field state.selectedField = field.name
return state return state
}); });
} },
}, },
views: { views: {
select: view => select: view =>

View File

@ -0,0 +1,68 @@
<script>
import { onMount } from "svelte";
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
export let modelId
export let linkedRecords
let records = []
async function fetchRecords() {
const FETCH_RECORDS_URL = `/api/${$backendUiStore.selectedDatabase._id}/${modelId}/records`
const response = await api.get(FETCH_RECORDS_URL)
records = await response.json()
}
onMount(() => {
fetchRecords()
})
function linkRecord(record) {
linkedRecords.push(record);
}
</script>
<section>
{#each records as record}
<div class="linked-record" on:click={linkRecord}>
<h3>{record.name}</h3>
<div class="fields">
{#each Object.keys(record) as key}
<div class="field">
<span>{key}</span>
<p>{record[key]}</p>
</div>
{/each}
</div>
</div>
{/each}
</section>
<style>
section {
background: var(--grey);
padding: 20px;
}
.fields {
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
background: var(--white);
border: 1px solid var(--grey);
border-radius: 5px;
}
.field span {
color: var(--ink-lighter);
font-size: 12px;
}
.field p {
color: var(--ink);
font-size: 14px;
word-break: break-word;
}
</style>

View File

@ -68,10 +68,10 @@
} }
} }
$: paginatedData = data.slice( $: paginatedData = data ? data.slice(
currentPage * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
) ) : []
onMount(() => { onMount(() => {
if (views.length) { if (views.length) {

View File

@ -3,6 +3,7 @@
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import { compose, map, get, flatten } from "lodash/fp" import { compose, map, get, flatten } from "lodash/fp"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import Select from "components/common/Select.svelte" import Select from "components/common/Select.svelte"
import RecordFieldControl from "./RecordFieldControl.svelte" import RecordFieldControl from "./RecordFieldControl.svelte"
import * as api from "../api" import * as api from "../api"
@ -73,11 +74,15 @@
<form on:submit|preventDefault class="uk-form-stacked"> <form on:submit|preventDefault class="uk-form-stacked">
{#each modelSchema as [key, meta]} {#each modelSchema as [key, meta]}
<div class="uk-margin"> <div class="uk-margin">
{#if meta.type === "link"}
<LinkedRecordSelector modelId={meta.modelId} />
{:else}
<RecordFieldControl <RecordFieldControl
type={determineInputType(meta)} type={determineInputType(meta)}
options={determineOptions(meta)} options={determineOptions(meta)}
label={key} label={key}
bind:value={record[key]} /> bind:value={record[key]} />
{/if}
</div> </div>
{/each} {/each}
</form> </form>

View File

@ -13,7 +13,7 @@
<style> <style>
.indented { .indented {
margin-left: 20px; margin-left: 10px;
} }
div { div {
@ -21,11 +21,11 @@
width: 260px; width: 260px;
height: 40px; height: 40px;
border-radius: 3px; border-radius: 3px;
font-weight: bold;
display: flex; display: flex;
align-items: center; align-items: center;
transition: 0.3s background-color; transition: 0.3s background-color;
color: var(--ink); color: var(--ink);
font-weight: 500;
font-size: 16px; font-size: 16px;
} }

View File

@ -11,7 +11,7 @@
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
const HEADINGS = [ let HEADINGS = [
{ {
title: "Navigate", title: "Navigate",
key: "NAVIGATE", key: "NAVIGATE",
@ -28,9 +28,9 @@
backendUiStore.actions.models.select(model) backendUiStore.actions.models.select(model)
} }
function selectField(field) { function selectField(fieldName) {
backendUiStore.update(state => { backendUiStore.update(state => {
state.selectedField = field state.selectedField = fieldName
return state return state
}); });
} }
@ -41,7 +41,6 @@
state.draftModel = { schema: {} } state.draftModel = { schema: {} }
return state return state
}) })
$goto(`./database/${$backendUiStore.selectedDatabase._id}/newmodel`)
} }
</script> </script>
@ -61,10 +60,11 @@
on:click={() => selectModel(model)} /> on:click={() => selectModel(model)} />
{#each Object.keys(model.schema) as field} {#each Object.keys(model.schema) as field}
<ListItem <ListItem
selected={field === $backendUiStore.selectedField}
indented indented
icon="ri-layout-column-fill" icon="ri-layout-column-fill"
title={field} title={field}
on:click={() => selectField({ name: field, ...model.schema[field] })} /> on:click={() => selectField(field)} />
{/each} {/each}
{/each} {/each}
</div> </div>

View File

@ -14,12 +14,11 @@
const FIELD_TYPES = ["string", "number", "boolean", "link"] const FIELD_TYPES = ["string", "number", "boolean", "link"]
export let field let field = {}
$: type = field.type $: field = $backendUiStore.draftModel.schema[$backendUiStore.selectedField] || {}
$: constraints = field.constraints
$: required = $: required =
constraints && constraints.presence && !constraints.presence.allowEmpty field.constraints && field.constraints.presence && !constraints.presence.allowEmpty
const save = () => { const save = () => {
backendUiStore.actions.models.save({ backendUiStore.actions.models.save({
@ -31,24 +30,24 @@
<form on:submit|preventDefault class="uk-form-stacked"> <form on:submit|preventDefault class="uk-form-stacked">
<Textbox label="Name" bind:text={field.name} /> <Textbox label="Name" bind:text={field.name} />
<Dropdown label="Type" bind:selected={type} options={FIELD_TYPES} /> <Dropdown label="Type" bind:selected={field.type} options={FIELD_TYPES} />
<Checkbox label="Required" bind:checked={required} /> <Checkbox label="Required" bind:checked={required} />
{#if type === 'string'} {#if field.type === 'string'}
<NumberBox label="Max Length" bind:value={constraints.length.maximum} /> <NumberBox label="Max Length" bind:value={field.constraints.length.maximum} />
<ValuesList label="Categories" bind:values={constraints.inclusion} /> <ValuesList label="Categories" bind:values={field.constraints.inclusion} />
{:else if type === 'datetime'} {:else if field.type === 'datetime'}
<DatePicker label="Min Value" bind:value={constraints.datetime.earliest} /> <DatePicker label="Min Value" bind:value={field.constraints.datetime.earliest} />
<DatePicker label="Max Value" bind:value={constraints.datetime.latest} /> <DatePicker label="Max Value" bind:value={field.constraints.datetime.latest} />
{:else if type === 'number'} {:else if field.type === 'number'}
<NumberBox <NumberBox
label="Min Value" label="Min Value"
bind:value={constraints.numericality.greaterThanOrEqualTo} /> bind:value={field.constraints.numericality.greaterThanOrEqualTo} />
<NumberBox <NumberBox
label="Max Value" label="Max Value"
bind:value={constraints.numericality.lessThanOrEqualTo} /> bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if type === 'link'} {:else if field.type === 'link'}
<select class="budibase__input" bind:value={field.modelId}> <select class="budibase__input" bind:value={field.modelId}>
<option value={''} /> <option value={''} />
{#each $backendUiStore.models as model} {#each $backendUiStore.models as model}

View File

@ -1,6 +1,7 @@
<script> <script>
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { Button, Switcher } from "@budibase/bbui" import { Button, Switcher } from "@budibase/bbui"
import { notifier } from "@beyonk/svelte-notifications"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import api from "builderStore/api" import api from "builderStore/api"
import FieldView from "./FieldView.svelte"; import FieldView from "./FieldView.svelte";
@ -27,14 +28,19 @@
$: edited = $backendUiStore.draftModel.name !== $backendUiStore.selectedModel.name $: edited = $backendUiStore.draftModel.name !== $backendUiStore.selectedModel.name
async function deleteModel() { async function deleteModel() {
backendUiStore.update(async state => { const modelToDelete = $backendUiStore.selectedModel
const modelToDelete = state.selectedModel if ($backendUiStore.selectedField) {
const DELETE_MODEL_URL = `/api/${state.selectedDatabase._id}/models/${modelToDelete._id}/${modelToDelete._rev}` delete modelToDelete[$backendUiStore.selectedField]
}
const DELETE_MODEL_URL = `/api/${$backendUiStore.selectedDatabase._id}/models/${modelToDelete._id}/${modelToDelete._rev}`
const response = await api.delete(DELETE_MODEL_URL) const response = await api.delete(DELETE_MODEL_URL)
backendUiStore.update(state => {
state.selectedView = null
state.models = state.models.filter( state.models = state.models.filter(
model => model._id !== modelToDelete._id model => model._id !== modelToDelete._id
) )
state.selectedView = {} notifier.danger(`${modelToDelete.name} deleted successfully.`)
return state return state
}) })
} }
@ -44,6 +50,7 @@
instanceId: $backendUiStore.selectedDatabase._id, instanceId: $backendUiStore.selectedDatabase._id,
model: $backendUiStore.draftModel model: $backendUiStore.draftModel
}) })
notifier.success("Success! Your changes have been saved. Please continue on with your greatness.");
} }
</script> </script>
@ -51,7 +58,7 @@
<Switcher headings={ITEMS} bind:value={selectedTab}> <Switcher headings={ITEMS} bind:value={selectedTab}>
{#if selectedTab === 'SETUP'} {#if selectedTab === 'SETUP'}
{#if $backendUiStore.selectedField} {#if $backendUiStore.selectedField}
<FieldView field={$backendUiStore.selectedField} /> <FieldView />
{:else} {:else}
<div class="titled-input"> <div class="titled-input">
<header>Name</header> <header>Name</header>
@ -64,7 +71,6 @@
<header>Import Data</header> <header>Import Data</header>
<Button wide secondary>Import CSV</Button> <Button wide secondary>Import CSV</Button>
</div> </div>
<Button <Button
attention attention
wide wide
@ -82,6 +88,10 @@
</div> </div>
<style> <style>
header {
font-weight: 500;
}
.items-root { .items-root {
padding: 20px; padding: 20px;
display: flex; display: flex;

View File

@ -79,7 +79,104 @@ export const FIELDS = {
} }
export const BLOCKS = { export const BLOCKS = {
NAME: {
name: "Name",
icon: "ri-text",
type: "string",
constraints: {
type: "string",
length: {},
presence: false,
},
},
PHONE_NUMBER: {
name: "Phone Number",
icon: "ri-number-1",
type: "number",
constraints: {
type: "number",
presence: false,
numericality: {},
},
},
ACTIVE: {
name: "Active",
icon: "ri-toggle-line",
type: "boolean",
constraints: {
type: "boolean",
presence: false,
},
},
PRIORITY: {
name: "Options",
icon: "ri-list-check-2",
type: "options",
constraints: {
type: "string",
presence: false,
inclusion: [
"low",
"medium",
"high"
]
},
},
END_DATE: {
name: "End Date",
icon: "ri-calendar-event-fill",
type: "datetime",
constraints: {
type: "date",
datetime: {},
presence: false,
},
},
AVATAR: {
name: "Avatar",
icon: "ri-image-line",
type: "image",
constraints: {
type: "string",
presence: false,
},
},
PDF: {
name: "PDF",
icon: "ri-file-line",
type: "file",
constraints: {
type: "string",
presence: false,
},
},
DATA_LINK: {
name: "Data Links",
icon: "ri-link",
type: "link",
modelId: null,
constraints: {
type: "array",
}
},
} }
export const TABLES = {} // TODO: Needs more thought, need to come up with the constraints etc for each one
export const MODELS = {
CONTACTS: {
icon: "ri-link",
name: "Contacts",
schema: {
Name: BLOCKS.NAME,
"Phone Number": BLOCKS.PHONE_NUMBER
}
},
RECIPES: {
icon: "ri-link",
name: "Recipes",
schema: {
Name: BLOCKS.NAME,
"Phone Number": BLOCKS.PHONE_NUMBER
}
}
}

View File

@ -92,7 +92,6 @@
} }
html, body { html, body {
display: grid;
font-family: var(--fontnormal); font-family: var(--fontnormal);
color: var(--secondary80); color: var(--secondary80);
padding: 0; padding: 0;

View File

@ -19,11 +19,17 @@
height: 90px; height: 90px;
border-radius: 3px; border-radius: 3px;
color: var(--ink); color: var(--ink);
padding: 20px; font-weight: 500;
padding: 15px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
transition: 0.3s transform;
}
i {
font-size: 30px;
} }
span { span {
@ -32,6 +38,7 @@
div:hover { div:hover {
cursor: pointer; cursor: pointer;
transform: scale(1.1);
} }
.primary { .primary {

View File

@ -1,5 +1,6 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import NewModel from "./NewModel.svelte"
import ModelDataTable from "components/database/ModelDataTable" import ModelDataTable from "components/database/ModelDataTable"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
@ -33,7 +34,9 @@
</ActionButton> </ActionButton>
{/if} {/if}
</div> </div>
{#if $backendUiStore.selectedDatabase._id && $backendUiStore.selectedModel.name} {#if $backendUiStore.selectedModel.schema && Object.keys($backendUiStore.selectedModel.schema).length === 0}
<NewModel />
{:else if $backendUiStore.selectedDatabase._id && $backendUiStore.selectedModel.name}
<ModelDataTable /> <ModelDataTable />
{:else} {:else}
<i style="color: var(--grey-dark)"> <i style="color: var(--grey-dark)">

View File

@ -1,31 +1,70 @@
<script> <script>
import { FIELDS } from "constants/backend"; import { backendUiStore } from "builderStore";
import { FIELDS, BLOCKS, MODELS } from "constants/backend";
import Block from "./Block.svelte"; import Block from "./Block.svelte";
function addNewField(field) {
backendUiStore.actions.models.addField(field);
}
</script> </script>
<section> <section>
<header> <header>
<h2>Create New Table</h2> <h2>Create New Table</h2>
<span>Before you can view your table, you need to set it up.</span> <p>Before you can view your table, you need to set it up.</p>
</header> </header>
<div class="block-row"> <div class="block-row">
<h4>Fields</h4> <span class="block-row-title">Fields</span>
<p>Blocks are pre-made fields and help you build your table quicker.</p>
<div class="blocks"> <div class="blocks">
{#each Object.values(FIELDS) as field} {#each Object.values(FIELDS) as field}
<Block primary title={field.name} icon={field.icon} /> <Block primary title={field.name} icon={field.icon} on:click={() => addNewField(field)} />
{/each} {/each}
</div> </div>
</div> </div>
<!-- TODO: More block rows --> <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} />
{/each}
</div>
</div>
<div class="block-row">
<span class="block-row-title">Models</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} />
{/each}
</div>
</div>
</section> </section>
<style> <style>
section {
height: 100vh;
}
h2 { h2 {
font-size: 20px; font-size: 20px;
font-weight: bold; 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 { .block-row {
@ -35,6 +74,6 @@
.block-row .blocks { .block-row .blocks {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
grid-gap: 10px; grid-gap: 20px;
} }
</style> </style>