structuring new backend UI

This commit is contained in:
Martin McKeaveney 2020-06-15 16:41:31 +01:00
parent 8f745bcb68
commit af9038f64f
19 changed files with 580 additions and 36 deletions

View File

@ -39,7 +39,7 @@
},
"dependencies": {
"@beyonk/svelte-notifications": "^2.0.3",
"@budibase/bbui": "^1.1.1",
"@budibase/bbui": "^1.4.1",
"@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0",

View File

@ -153,6 +153,10 @@ export default {
find: "builderStore",
replacement: path.resolve(projectRootDir, "src/builderStore"),
},
{
find: "constants",
replacement: path.resolve(projectRootDir, "src/constants"),
},
],
customResolver,
}),

View File

@ -39,18 +39,6 @@ export const getBackendUiStore = () => {
state.views = views
return state
})
/** TODO: DEMO SOLUTION**/
if (!models || models.length === 0) {
const { open, close } = getContext("simple-modal")
open(
CreateEditModelModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
/** DEMO SOLUTION END **/
},
},
records: {
@ -71,6 +59,10 @@ export const getBackendUiStore = () => {
}),
},
models: {
select: model => store.update(state => {
state.selectedModel = model;
return state;
}),
create: model =>
store.update(state => {
state.models.push(model)
@ -78,7 +70,7 @@ export const getBackendUiStore = () => {
state.selectedModel = model
state.selectedView = `all_${model._id}`
return state
}),
})
},
views: {
select: view =>

View File

@ -20,12 +20,6 @@
$: modelFields = model.schema ? Object.entries(model.schema) : []
$: instanceId = $backendUiStore.selectedDatabase._id
function editField() {}
function deleteField() {}
function onFinishedFieldEdit() {}
async function saveModel() {
const SAVE_MODEL_URL = `/api/${instanceId}/models`
const response = await api.post(SAVE_MODEL_URL, model)
@ -94,7 +88,6 @@
{:else}
<FieldView
field={fieldToEdit}
onFinished={onFinishedFieldEdit}
schema={model.schema}
goBack={() => (showFieldView = false)} />
{/if}

View File

@ -31,14 +31,6 @@
class:selected={$backendUiStore.selectedView === `all_${node._id}`}>
<i class={ICON_MAP[type]} />
<span style="margin-left: 1rem">{node.name}</span>
<!-- <i
class="ri-edit-line hoverable"
on:click={editModel}
/>
<i
class="ri-delete-bin-7-line hoverable"
on:click={deleteModel}
/> -->
</div>
</div>

View File

@ -0,0 +1,47 @@
<script>
export let title
export let icon
export let primary
export let secondary
export let tertiary
</script>
<div on:click class:primary class:secondary class:tertiary>
<i class={icon} />
<span>{title}</span>
</div>
<style>
div {
height: 90px;
border-radius: 3px;
color: var(--ink);
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
span {
font-size: 16px;
}
div:hover {
cursor: pointer;
}
.primary {
background: var(--ink);
color: var(--white);
}
.secondary {
background: var(--secondary);
}
.tertiary {
background: var(--white);
}
</style>

View File

@ -0,0 +1,79 @@
<script>
import * as blockDefinitions from "constants/backend"
import Block from "./Block.svelte"
console.log(blockDefinitions)
const HEADINGS = [
{
title: "Fields",
key: "FIELDS",
},
{
title: "Blocks",
key: "BLOCKS",
},
{
title: "Table",
key: "TABLES",
},
]
let selectedTab = "FIELDS"
</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
primary={true}
secondary={false}
tertiary={false}
title={blockDefinition.name}
icon={blockDefinition.icon} />
{/each}
</div>
</section>
<style>
header {
margin-top: 20px;
margin-bottom: 20px;
}
span {
margin-right: 20px;
padding: 10px;
border-radius: 3px;
color: var(--ink-lighter);
font-size: 14px;
background: var(--light-grey);
}
span:hover {
background: var(--secondary);
cursor: pointer;
}
.selected {
background: var(--secondary);
color: var(--ink);
}
.block-grid {
margin-top: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
}
</style>

View File

@ -0,0 +1,46 @@
<script>
export let icon
export let className
export let title
export let selected
export let indented
</script>
<div class:selected on:click class={className}>
<i class:indented class={icon} />
<span>{title}</span>
</div>
<style>
.indented {
margin-left: 20px;
}
div {
padding: 0 10px 0 10px;
width: 260px;
height: 40px;
border-radius: 3px;
font-weight: bold;
display: flex;
align-items: center;
transition: 0.3s background-color;
color: var(--ink);
font-size: 16px;
}
.selected {
background-color: var(--secondary);
}
div:hover {
background-color: var(--secondary);
cursor: pointer;
}
i {
color: var(--dark-grey);
font-size: 20px;
margin-right: 8px;
}
</style>

View File

@ -0,0 +1,90 @@
<script>
import { getContext } from "svelte"
import { Switcher } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { store, backendUiStore } from "builderStore"
import BlockNavigator from "./BlockNavigator.svelte";
import SchemaManagementDrawer from "../SchemaManagementDrawer.svelte"
import HierarchyRow from "../HierarchyRow.svelte"
import ListItem from "./ListItem.svelte"
import { Button } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
const HEADINGS = [
{
title: "Navigate",
key: "NAVIGATE",
},
{
title: "Add",
key: "ADD",
},
]
let selectedTab = "NAVIGATE"
function selectModel(model) {
backendUiStore.actions.models.select(model)
}
function selectField() {}
</script>
<div class="items-root">
{#if $backendUiStore.selectedDatabase._id}
<div class="hierarchy">
<div class="components-list-container">
<Switcher headings={HEADINGS} bind:value={selectedTab}>
{#if selectedTab === 'NAVIGATE'}
<Button
secondary
wide
on:click={() => $goto(`./database/${$backendUiStore.selectedDatabase._id}/newmodel`)}>
Create New Model
</Button>
<div class="hierarchy-items-container">
{#each $backendUiStore.models as model}
<ListItem
selected={model._id === $backendUiStore.selectedModel._id}
title={model.name}
icon="ri-table-fill"
on:click={() => selectModel(model)} />
{#each Object.keys(model.schema) as field}
<ListItem
indented
icon="ri-layout-column-fill"
title={field}
on:click={() => selectField(model.schema[field])} />
{/each}
{/each}
</div>
{:else if selectedTab === 'ADD'}
<BlockNavigator />
{/if}
</Switcher>
</div>
</div>
{/if}
</div>
<style>
.items-root {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background: var(--white);
padding: 20px;
}
.hierarchy {
display: flex;
flex-direction: column;
}
.hierarchy-items-container {
margin-top: 20px;
flex: 1 1 auto;
}
</style>

View File

@ -0,0 +1,111 @@
<script>
import { getContext, onMount } from "svelte"
import { Button, Switcher } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import HierarchyRow from "./HierarchyRow.svelte"
import NavItem from "./NavItem.svelte"
import getIcon from "components/common/icon"
import api from "builderStore/api"
import {
CreateEditModelModal,
CreateEditViewModal,
} from "components/database/ModelDataTable/modals"
const { open, close } = getContext("simple-modal")
const ITEMS = [
{
title: "Setup",
key: "SETUP",
},
{
title: "Filter",
key: "FILTER",
},
{
title: "Delete",
key: "DELETE",
},
]
let selectedTab = "SETUP"
let modelName = $backendUiStore.selectedModel.name
let edited
$: if (modelName) {
edited = modelName !== $backendUiStore.selectedModel.name
}
async function deleteModel() {
const modelToDelete = $backendUiStore.selectedModel
const DELETE_MODEL_URL = `/api/${instanceId}/models/${modelToDelete._id}/${modelToDelete._rev}`
const response = await api.delete(DELETE_MODEL_URL)
backendUiStore.update(state => {
state.models = state.models.filter(
model => model._id !== modelToDelete._id
)
state.selectedView = {}
return state
})
}
function selectView(view) {
backendUiStore.update(state => {
state.selectedView = view.name
return state
})
}
</script>
<div class="items-root">
<Switcher headings={ITEMS} bind:value={selectedTab}>
{#if selectedTab === 'SETUP'}
<div class="titled-input">
<header>Name</header>
<input type="text" class="budibase__input" bind:value={modelName} />
</div>
<div class="titled-input">
<header>Import Data</header>
<Button wide secondary>Import CSV</Button>
</div>
<Button
disabled={!edited}
attention={edited}
wide
on:click={() => alert('Clicked!')}>
Save
</Button>
{:else if selectedTab === 'DELETE'}
<div class="titled-input">
<header>Danger Zone</header>
<Button error wide on:click={deleteModel}>
Delete
</Button>
</div>
{/if}
</Switcher>
</div>
<style>
.items-root {
padding: 20px;
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background-color: var(--white);
}
.titled-input {
padding: 20px;
background: var(--light-grey);
margin-top: 20px;
}
.titled-input header {
display: block;
font-size: 14px;
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,101 @@
const FIELD_TYPES = ["string", "number", "boolean"]
export const FIELDS = {
PLAIN_TEXT: {
name: "Plain Text",
icon: "ri-text",
definition: {
type: "string",
constraints: {
type: "string",
length: {},
presence: false,
},
},
},
NUMBER: {
name: "Number",
icon: "ri-number-1",
definition: {
type: "number",
constraints: {
type: "number",
presence: false,
numericality: {},
},
},
},
BOOLEAN: {
name: "True/False",
icon: "ri-toggle-line",
definition: {
type: "boolean",
constraints: {
type: "boolean",
presence: false,
},
},
},
OPTIONS: {
name: "Options",
icon: "ri-list-check-2",
definition: {
type: "options",
constraints: {
type: "string",
presence: false,
},
},
},
DATETIME: {
name: "Date/Time",
icon: "ri-calendar-event-fill",
definition: {
type: "date",
constraints: {
type: "date",
datetime: {},
presence: false,
},
},
},
IMAGE: {
name: "Image",
icon: "ri-image-line",
definition: {
type: "image",
constraints: {
type: "string",
presence: false,
},
},
},
FILE: {
name: "Image",
icon: "ri-file-line",
definition: {
type: "file",
constraints: {
type: "string",
presence: false,
},
},
},
DATA_LINK: {
name: "Data Links",
icon: "ri-link",
definition: {
type: "link",
modelId: null,
constraints: {
type: "array",
},
},
},
}
export const BLOCKS = {
}
export const TABLES = {}

View File

@ -3,19 +3,19 @@
import { store, backendUiStore } from "builderStore"
import * as api from "components/database/ModelDataTable/api"
import BackendNav from "components/nav/BackendNav.svelte"
import SchemaManagementDrawer from "components/nav/SchemaManagementDrawer.svelte"
import ModelNavigator from "components/nav/ModelNavigator/ModelNavigator.svelte"
import ModelSetupNav from "components/nav/ModelSetupNav.svelte"
</script>
<div class="root">
<div class="nav">
<BackendNav />
<ModelNavigator />
</div>
<div class="content">
<slot />
</div>
<div class="nav">
<SchemaManagementDrawer />
<ModelSetupNav />
</div>
</div>

View File

@ -0,0 +1,49 @@
<script>
export let title
export let icon
export let primary
export let secondary
export let tertiary
</script>
<!-- TODO: Move to bbui -->
<div on:click class:primary class:secondary class:tertiary>
<i class={icon} />
<span>{title}</span>
</div>
<style>
div {
width: 120px;
height: 90px;
border-radius: 3px;
color: var(--ink);
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
span {
font-size: 16px;
}
div:hover {
cursor: pointer;
}
.primary {
background: var(--ink);
color: var(--white);
}
.secondary {
background: var(--secondary);
}
.tertiary {
background: var(--white);
}
</style>

View File

@ -0,0 +1,40 @@
<script>
import { FIELDS } from "constants/backend";
import Block from "./Block.svelte";
</script>
<section>
<header>
<h2>Create New Table</h2>
<span>Before you can view your table, you need to set it up.</span>
</header>
<div class="block-row">
<h4>Fields</h4>
<div class="blocks">
{#each Object.values(FIELDS) as field}
<Block primary title={field.name} icon={field.icon} />
{/each}
</div>
</div>
<!-- TODO: More block rows -->
</section>
<style>
h2 {
font-size: 20px;
font-weight: bold;
}
.block-row {
margin-top: 40px;
}
.block-row .blocks {
display: grid;
grid-auto-flow: column;
grid-gap: 10px;
}
</style>

View File

@ -110,7 +110,7 @@ describe("/models", () => {
afterEach(() => {
delete testModel._rev
})
});
it("returns a success response when a model is deleted.", async done => {
request

View File

@ -41,7 +41,7 @@ const screenPath = (appPath, pageName, name) =>
module.exports.saveScreen = async (config, appname, pagename, screen) => {
const appPath = appPackageFolder(config, appname)
const compPath = screenPath(appPath, pagename, screen.name)
const compPath = screenPath(appPath, pagename, screen.props._instanceName)
await ensureDir(dirname(compPath))
if (screen._css) {
delete screen._css