add new models and fields, delete models
This commit is contained in:
parent
bda580bc3f
commit
949052c41c
|
@ -77,7 +77,8 @@
|
|||
|
||||
.budibase__input {
|
||||
height: 35px;
|
||||
width: 220px;
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--grey-dark);
|
||||
text-align: left;
|
||||
|
|
|
@ -25,9 +25,7 @@ export const getBackendUiStore = () => {
|
|||
store.update(state => {
|
||||
state.selectedDatabase = db
|
||||
if (models && models.length > 0) {
|
||||
state.selectedModel = models[0]
|
||||
state.draftModel = models[0]
|
||||
state.selectedView = `all_${models[0]._id}`
|
||||
store.actions.models.select(models[0]);
|
||||
}
|
||||
state.breadcrumbs = [db.name]
|
||||
state.models = models
|
||||
|
@ -56,14 +54,25 @@ export const getBackendUiStore = () => {
|
|||
models: {
|
||||
select: model => store.update(state => {
|
||||
state.selectedModel = model;
|
||||
// TODO: prevent pointing to same obj
|
||||
state.draftModel = cloneDeep(model);
|
||||
state.selectedField = null
|
||||
state.selectedField = ""
|
||||
state.selectedView = `all_${model._id}`
|
||||
return state;
|
||||
}),
|
||||
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 response = await api.post(SAVE_MODEL_URL, model)
|
||||
const response = await api.post(SAVE_MODEL_URL, updatedModel)
|
||||
const savedModel = await response.json()
|
||||
|
||||
store.update(state => {
|
||||
|
@ -76,9 +85,7 @@ export const getBackendUiStore = () => {
|
|||
state.models = state.models
|
||||
}
|
||||
|
||||
state.selectedModel = savedModel
|
||||
state.draftModel = savedModel
|
||||
state.selectedView = `all_${savedModel._id}`
|
||||
store.actions.models.select(savedModel)
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -93,11 +100,11 @@ export const getBackendUiStore = () => {
|
|||
[field.name]: field
|
||||
}
|
||||
|
||||
state.selectedField = field
|
||||
state.selectedField = field.name
|
||||
|
||||
return state
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
views: {
|
||||
select: view =>
|
||||
|
|
|
@ -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>
|
|
@ -68,10 +68,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: paginatedData = data.slice(
|
||||
$: paginatedData = data ? data.slice(
|
||||
currentPage * ITEMS_PER_PAGE,
|
||||
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
|
||||
)
|
||||
) : []
|
||||
|
||||
onMount(() => {
|
||||
if (views.length) {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { store, backendUiStore } from "builderStore"
|
||||
import { compose, map, get, flatten } from "lodash/fp"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
||||
import Select from "components/common/Select.svelte"
|
||||
import RecordFieldControl from "./RecordFieldControl.svelte"
|
||||
import * as api from "../api"
|
||||
|
@ -73,11 +74,15 @@
|
|||
<form on:submit|preventDefault class="uk-form-stacked">
|
||||
{#each modelSchema as [key, meta]}
|
||||
<div class="uk-margin">
|
||||
<RecordFieldControl
|
||||
type={determineInputType(meta)}
|
||||
options={determineOptions(meta)}
|
||||
label={key}
|
||||
bind:value={record[key]} />
|
||||
{#if meta.type === "link"}
|
||||
<LinkedRecordSelector modelId={meta.modelId} />
|
||||
{:else}
|
||||
<RecordFieldControl
|
||||
type={determineInputType(meta)}
|
||||
options={determineOptions(meta)}
|
||||
label={key}
|
||||
bind:value={record[key]} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</form>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
<style>
|
||||
.indented {
|
||||
margin-left: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
div {
|
||||
|
@ -21,11 +21,11 @@
|
|||
width: 260px;
|
||||
height: 40px;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: 0.3s background-color;
|
||||
color: var(--ink);
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
const HEADINGS = [
|
||||
let HEADINGS = [
|
||||
{
|
||||
title: "Navigate",
|
||||
key: "NAVIGATE",
|
||||
|
@ -28,9 +28,9 @@
|
|||
backendUiStore.actions.models.select(model)
|
||||
}
|
||||
|
||||
function selectField(field) {
|
||||
function selectField(fieldName) {
|
||||
backendUiStore.update(state => {
|
||||
state.selectedField = field
|
||||
state.selectedField = fieldName
|
||||
return state
|
||||
});
|
||||
}
|
||||
|
@ -41,7 +41,6 @@
|
|||
state.draftModel = { schema: {} }
|
||||
return state
|
||||
})
|
||||
$goto(`./database/${$backendUiStore.selectedDatabase._id}/newmodel`)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -51,7 +50,7 @@
|
|||
<div class="components-list-container">
|
||||
<Switcher headings={HEADINGS} bind:value={selectedTab}>
|
||||
{#if selectedTab === 'NAVIGATE'}
|
||||
<Button secondary wide on:click={setupForNewModel}>Create New Model</Button>
|
||||
<Button secondary wide on:click={setupForNewModel}>Create New Model</Button>
|
||||
<div class="hierarchy-items-container">
|
||||
{#each $backendUiStore.models as model}
|
||||
<ListItem
|
||||
|
@ -61,10 +60,11 @@
|
|||
on:click={() => selectModel(model)} />
|
||||
{#each Object.keys(model.schema) as field}
|
||||
<ListItem
|
||||
selected={field === $backendUiStore.selectedField}
|
||||
indented
|
||||
icon="ri-layout-column-fill"
|
||||
title={field}
|
||||
on:click={() => selectField({ name: field, ...model.schema[field] })} />
|
||||
on:click={() => selectField(field)} />
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -14,12 +14,11 @@
|
|||
|
||||
const FIELD_TYPES = ["string", "number", "boolean", "link"]
|
||||
|
||||
export let field
|
||||
let field = {}
|
||||
|
||||
$: type = field.type
|
||||
$: constraints = field.constraints
|
||||
$: field = $backendUiStore.draftModel.schema[$backendUiStore.selectedField] || {}
|
||||
$: required =
|
||||
constraints && constraints.presence && !constraints.presence.allowEmpty
|
||||
field.constraints && field.constraints.presence && !constraints.presence.allowEmpty
|
||||
|
||||
const save = () => {
|
||||
backendUiStore.actions.models.save({
|
||||
|
@ -31,24 +30,24 @@
|
|||
|
||||
<form on:submit|preventDefault class="uk-form-stacked">
|
||||
<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} />
|
||||
|
||||
{#if type === 'string'}
|
||||
<NumberBox label="Max Length" bind:value={constraints.length.maximum} />
|
||||
<ValuesList label="Categories" bind:values={constraints.inclusion} />
|
||||
{:else if type === 'datetime'}
|
||||
<DatePicker label="Min Value" bind:value={constraints.datetime.earliest} />
|
||||
<DatePicker label="Max Value" bind:value={constraints.datetime.latest} />
|
||||
{:else if type === 'number'}
|
||||
{#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={constraints.numericality.greaterThanOrEqualTo} />
|
||||
bind:value={field.constraints.numericality.greaterThanOrEqualTo} />
|
||||
<NumberBox
|
||||
label="Max Value"
|
||||
bind:value={constraints.numericality.lessThanOrEqualTo} />
|
||||
{:else if type === 'link'}
|
||||
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
||||
{:else if field.type === 'link'}
|
||||
<select class="budibase__input" bind:value={field.modelId}>
|
||||
<option value={''} />
|
||||
{#each $backendUiStore.models as model}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { Button, Switcher } from "@budibase/bbui"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import FieldView from "./FieldView.svelte";
|
||||
|
@ -27,14 +28,19 @@
|
|||
$: edited = $backendUiStore.draftModel.name !== $backendUiStore.selectedModel.name
|
||||
|
||||
async function deleteModel() {
|
||||
backendUiStore.update(async state => {
|
||||
const modelToDelete = state.selectedModel
|
||||
const DELETE_MODEL_URL = `/api/${state.selectedDatabase._id}/models/${modelToDelete._id}/${modelToDelete._rev}`
|
||||
const response = await api.delete(DELETE_MODEL_URL)
|
||||
const modelToDelete = $backendUiStore.selectedModel
|
||||
if ($backendUiStore.selectedField) {
|
||||
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)
|
||||
backendUiStore.update(state => {
|
||||
state.selectedView = null
|
||||
state.models = state.models.filter(
|
||||
model => model._id !== modelToDelete._id
|
||||
)
|
||||
state.selectedView = {}
|
||||
notifier.danger(`${modelToDelete.name} deleted successfully.`)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
@ -44,6 +50,7 @@
|
|||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
model: $backendUiStore.draftModel
|
||||
})
|
||||
notifier.success("Success! Your changes have been saved. Please continue on with your greatness.");
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -51,7 +58,7 @@
|
|||
<Switcher headings={ITEMS} bind:value={selectedTab}>
|
||||
{#if selectedTab === 'SETUP'}
|
||||
{#if $backendUiStore.selectedField}
|
||||
<FieldView field={$backendUiStore.selectedField} />
|
||||
<FieldView />
|
||||
{:else}
|
||||
<div class="titled-input">
|
||||
<header>Name</header>
|
||||
|
@ -64,7 +71,6 @@
|
|||
<header>Import Data</header>
|
||||
<Button wide secondary>Import CSV</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
attention
|
||||
wide
|
||||
|
@ -82,6 +88,10 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
header {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.items-root {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
|
|
|
@ -79,7 +79,104 @@ export const FIELDS = {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,6 @@
|
|||
}
|
||||
|
||||
html, body {
|
||||
display: grid;
|
||||
font-family: var(--fontnormal);
|
||||
color: var(--secondary80);
|
||||
padding: 0;
|
||||
|
|
|
@ -19,11 +19,17 @@
|
|||
height: 90px;
|
||||
border-radius: 3px;
|
||||
color: var(--ink);
|
||||
padding: 20px;
|
||||
font-weight: 500;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
transition: 0.3s transform;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
span {
|
||||
|
@ -32,6 +38,7 @@
|
|||
|
||||
div:hover {
|
||||
cursor: pointer;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.primary {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import NewModel from "./NewModel.svelte"
|
||||
import ModelDataTable from "components/database/ModelDataTable"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
|
@ -33,7 +34,9 @@
|
|||
</ActionButton>
|
||||
{/if}
|
||||
</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 />
|
||||
{:else}
|
||||
<i style="color: var(--grey-dark)">
|
||||
|
|
|
@ -1,31 +1,70 @@
|
|||
<script>
|
||||
import { FIELDS } from "constants/backend";
|
||||
import { backendUiStore } from "builderStore";
|
||||
import { FIELDS, BLOCKS, MODELS } from "constants/backend";
|
||||
import Block from "./Block.svelte";
|
||||
|
||||
function addNewField(field) {
|
||||
backendUiStore.actions.models.addField(field);
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
{#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}
|
||||
</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>
|
||||
|
||||
<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 {
|
||||
|
@ -35,6 +74,6 @@
|
|||
.block-row .blocks {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 10px;
|
||||
grid-gap: 20px;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue