structuring new backend UI
This commit is contained in:
parent
8f745bcb68
commit
af9038f64f
|
@ -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",
|
||||
|
|
|
@ -153,6 +153,10 @@ export default {
|
|||
find: "builderStore",
|
||||
replacement: path.resolve(projectRootDir, "src/builderStore"),
|
||||
},
|
||||
{
|
||||
find: "constants",
|
||||
replacement: path.resolve(projectRootDir, "src/constants"),
|
||||
},
|
||||
],
|
||||
customResolver,
|
||||
}),
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 = {}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -110,7 +110,7 @@ describe("/models", () => {
|
|||
|
||||
afterEach(() => {
|
||||
delete testModel._rev
|
||||
})
|
||||
});
|
||||
|
||||
it("returns a success response when a model is deleted.", async done => {
|
||||
request
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue