forked from tsai/budibase
179 changed files with 2843 additions and 2323 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,22 @@ |
|||
|
|||
context('Screen Tests', () => { |
|||
before(() => { |
|||
cy.visit('localhost:4001/_builder') |
|||
cy.createApp('Conor Cy App', 'Model App Description') |
|||
cy.navigateToFrontend() |
|||
}) |
|||
|
|||
it('Should successful create a screen', () => { |
|||
cy.createScreen("test Screen") |
|||
}) |
|||
|
|||
it('Should rename a screen', () => { |
|||
cy.get(".components-pane").within(() => { |
|||
cy.contains("Settings").click() |
|||
cy.get("input[name=_instanceName]").clear().type("About Us").blur() |
|||
}) |
|||
cy.get('.nav-items-container').within(() => { |
|||
cy.contains("About Us").should('exist') |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,23 @@ |
|||
import { writable } from "svelte/store" |
|||
import { generate } from "shortid" |
|||
|
|||
export const notificationStore = writable({ |
|||
notifications: [], |
|||
}) |
|||
|
|||
export function send(message, type = "default") { |
|||
notificationStore.update(state => { |
|||
state.notifications = [ |
|||
...state.notifications, |
|||
{ id: generate(), type, message }, |
|||
] |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
export const notifier = { |
|||
danger: msg => send(msg, "danger"), |
|||
warning: msg => send(msg, "warning"), |
|||
info: msg => send(msg, "info"), |
|||
success: msg => send(msg, "success"), |
|||
} |
|||
@ -1,77 +0,0 @@ |
|||
<script context="module"> |
|||
import UIKit from "uikit" |
|||
|
|||
export function showAppNotification({ message, status }) { |
|||
UIKit.notification({ |
|||
message: ` |
|||
<div class="message-container"> |
|||
<div class="information-icon">🤯</div> |
|||
<span class="notification-message"> |
|||
${message} |
|||
</span> |
|||
<button class="hoverable refresh-page-button" onclick="window.location.reload()">Refresh Page</button> |
|||
</div> |
|||
`, |
|||
status, |
|||
timeout: 100000, |
|||
}) |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
:global(.information-icon) { |
|||
font-size: 24px; |
|||
margin-right: 8px; |
|||
} |
|||
|
|||
:global(.uk-nofi) { |
|||
display: grid; |
|||
grid-template-columns: 40px 1fr auto; |
|||
grid-gap: 5px; |
|||
align-items: center; |
|||
} |
|||
|
|||
:global(.message-container) { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
:global(.uk-notification) { |
|||
width: 50% !important; |
|||
left: 0 !important; |
|||
right: 0 !important; |
|||
margin-right: auto !important; |
|||
margin-left: auto !important; |
|||
border-radius: 10px; |
|||
} |
|||
|
|||
:global(.uk-notification-message) { |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
:global(.uk-notification-message:hover .uk-notification-close) { |
|||
visibility: hidden; |
|||
} |
|||
|
|||
:global(.uk-notification-message-danger) { |
|||
background: var(--ink-light) !important; |
|||
color: #fff !important; |
|||
font-family: Roboto; |
|||
font-size: 16px !important; |
|||
} |
|||
|
|||
:global(.refresh-page-button) { |
|||
font-size: 14px; |
|||
border-radius: 3px; |
|||
border: none; |
|||
padding: 8px 16px; |
|||
color: var(--ink); |
|||
background: #ffffff; |
|||
margin-left: 20px; |
|||
} |
|||
|
|||
:global(.refresh-page-button):hover { |
|||
background: var(--grey-light); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,56 @@ |
|||
<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: 80px; |
|||
border-radius: 5px; |
|||
color: var(--ink); |
|||
font-weight: 400; |
|||
padding: 15px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
flex-direction: column; |
|||
transition: 0.3s transform; |
|||
background: var(--grey-1); |
|||
} |
|||
|
|||
i { |
|||
font-size: 30px; |
|||
} |
|||
|
|||
span { |
|||
font-size: 14px; |
|||
text-align: center; |
|||
} |
|||
|
|||
div:hover { |
|||
cursor: pointer; |
|||
background: var(--grey-2); |
|||
} |
|||
|
|||
.primary { |
|||
background: var(--ink); |
|||
color: var(--white); |
|||
} |
|||
|
|||
.secondary { |
|||
background: var(--blue-light); |
|||
} |
|||
|
|||
.tertiary { |
|||
background: var(--white); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,101 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
|
|||
export let modelId |
|||
export let linkName |
|||
export let linked = [] |
|||
|
|||
let records = [] |
|||
let model = {} |
|||
|
|||
let linkedRecords = new Set(linked) |
|||
|
|||
$: linked = [...linkedRecords] |
|||
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel._id] |
|||
$: schema = $backendUiStore.selectedModel.schema |
|||
|
|||
async function fetchRecords() { |
|||
const FETCH_RECORDS_URL = `/api/${modelId}/records` |
|||
const response = await api.get(FETCH_RECORDS_URL) |
|||
const modelResponse = await api.get(`/api/models/${modelId}`) |
|||
|
|||
model = await modelResponse.json() |
|||
records = await response.json() |
|||
} |
|||
|
|||
function linkRecord(id) { |
|||
if (linkedRecords.has(id)) { |
|||
linkedRecords.delete(id) |
|||
} else { |
|||
linkedRecords.add(id) |
|||
} |
|||
|
|||
linkedRecords = linkedRecords |
|||
} |
|||
|
|||
onMount(() => { |
|||
fetchRecords() |
|||
}) |
|||
</script> |
|||
|
|||
<section> |
|||
<header> |
|||
<h3>{linkName}</h3> |
|||
</header> |
|||
{#each records as record} |
|||
<div class="linked-record" on:click={() => linkRecord(record._id)}> |
|||
<div class="fields" class:selected={linkedRecords.has(record._id)}> |
|||
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key} |
|||
<div class="field"> |
|||
<span>{model.schema[key].name}</span> |
|||
<p>{record[key]}</p> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
</section> |
|||
|
|||
<style> |
|||
.fields.selected { |
|||
background: var(--light-grey); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
margin-bottom: 12px; |
|||
color: var(--ink); |
|||
} |
|||
|
|||
.fields { |
|||
padding: 15px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr; |
|||
grid-gap: 20px; |
|||
background: var(--white); |
|||
border: 1px solid var(--grey); |
|||
border-radius: 5px; |
|||
transition: 0.5s all; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.fields:hover { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.field span { |
|||
color: var(--ink-lighter); |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.field p { |
|||
color: var(--ink); |
|||
font-size: 14px; |
|||
word-break: break-word; |
|||
font-weight: 500; |
|||
margin-top: 4px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,65 @@ |
|||
<script> |
|||
import { notificationStore } from "builderStore/store/notifications" |
|||
import { onMount, onDestroy } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
|
|||
export let themes = { |
|||
danger: "#E26D69", |
|||
success: "#84C991", |
|||
warning: "#f0ad4e", |
|||
info: "#5bc0de", |
|||
default: "#aaaaaa", |
|||
} |
|||
|
|||
export let timeout = 3000 |
|||
|
|||
$: if ($notificationStore.notifications.length) { |
|||
setTimeout(() => { |
|||
notificationStore.update(state => { |
|||
state.notifications.shift() |
|||
state.notifications = state.notifications |
|||
return state |
|||
}) |
|||
}, timeout) |
|||
} |
|||
</script> |
|||
|
|||
<ul class="notifications"> |
|||
{#each $notificationStore.notifications as notification (notification.id)} |
|||
<li |
|||
class="toast" |
|||
style="background: {themes[notification.type]};" |
|||
transition:fade> |
|||
<div class="content">{notification.message}</div> |
|||
{#if notification.icon} |
|||
<i class={notification.icon} /> |
|||
{/if} |
|||
</li> |
|||
{/each} |
|||
</ul> |
|||
|
|||
<style> |
|||
.notifications { |
|||
width: 40vw; |
|||
list-style: none; |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
padding: 0; |
|||
z-index: 9999; |
|||
} |
|||
|
|||
.toast { |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.content { |
|||
padding: 10px; |
|||
display: block; |
|||
color: var(--white); |
|||
font-weight: 500; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,124 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
import { backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
|
|||
export let ids = [] |
|||
export let field |
|||
|
|||
let records = [] |
|||
let open = false |
|||
let model |
|||
|
|||
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel._id, field.modelId] |
|||
|
|||
async function fetchRecords() { |
|||
const response = await api.post("/api/records/search", { |
|||
keys: ids, |
|||
}) |
|||
const modelResponse = await api.get(`/api/models/${field.modelId}`) |
|||
records = await response.json() |
|||
model = await modelResponse.json() |
|||
} |
|||
|
|||
$: ids && fetchRecords() |
|||
|
|||
function toggleOpen() { |
|||
open = !open |
|||
} |
|||
|
|||
onMount(() => { |
|||
fetchRecords() |
|||
}) |
|||
</script> |
|||
|
|||
<section> |
|||
<a on:click={toggleOpen}>{records.length}</a> |
|||
{#if open} |
|||
<div class="popover" transition:fade> |
|||
<header> |
|||
<h3>{field.name}</h3> |
|||
<i class="ri-close-circle-fill" on:click={toggleOpen} /> |
|||
</header> |
|||
{#each records as record} |
|||
<div class="linked-record"> |
|||
<div class="fields"> |
|||
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key} |
|||
<div class="field"> |
|||
<span>{model.schema[key].name}</span> |
|||
<p>{record[key]}</p> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
</section> |
|||
|
|||
<style> |
|||
section { |
|||
display: relative; |
|||
} |
|||
|
|||
header { |
|||
width: 100%; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
i { |
|||
font-size: 24px; |
|||
color: var(--ink-lighter); |
|||
} |
|||
|
|||
i:hover { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
a { |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.popover { |
|||
width: 500px; |
|||
position: absolute; |
|||
right: 15%; |
|||
padding: 20px; |
|||
background: var(--light-grey); |
|||
border: 1px solid var(--grey); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
margin: 0; |
|||
} |
|||
|
|||
.fields { |
|||
padding: 15px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr; |
|||
grid-gap: 20px; |
|||
background: var(--white); |
|||
border: 1px solid var(--grey); |
|||
border-radius: 5px; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.field span { |
|||
color: var(--ink-lighter); |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.field p { |
|||
color: var(--ink); |
|||
font-size: 14px; |
|||
word-break: break-word; |
|||
font-weight: 500; |
|||
margin-top: 4px; |
|||
} |
|||
</style> |
|||
@ -1,106 +0,0 @@ |
|||
<script> |
|||
import Dropdown from "components/common/Dropdown.svelte" |
|||
import Textbox from "components/common/Textbox.svelte" |
|||
import Button from "components/common/Button.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 { keys, cloneDeep } from "lodash/fp" |
|||
|
|||
const FIELD_TYPES = ["string", "number", "boolean"] |
|||
|
|||
export let field = { |
|||
type: "string", |
|||
constraints: { type: "string", presence: false }, |
|||
} |
|||
export let schema |
|||
export let goBack |
|||
|
|||
let errors = [] |
|||
let draftField = cloneDeep(field) |
|||
|
|||
let type = field.type |
|||
let constraints = field.constraints |
|||
let required = |
|||
field.constraints.presence && !field.constraints.presence.allowEmpty |
|||
|
|||
const save = () => { |
|||
constraints.presence = required ? { allowEmpty: false } : false |
|||
draftField.constraints = constraints |
|||
draftField.type = type |
|||
schema[field.name] = draftField |
|||
goBack() |
|||
} |
|||
|
|||
$: constraints = |
|||
type === "string" |
|||
? { type: "string", length: {}, presence: false } |
|||
: type === "number" |
|||
? { type: "number", presence: false, numericality: {} } |
|||
: type === "boolean" |
|||
? { type: "boolean", presence: false } |
|||
: type === "datetime" |
|||
? { type: "date", datetime: {}, presence: false } |
|||
: type.startsWith("array") |
|||
? { type: "array", presence: false } |
|||
: { type: "string", presence: false } |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<ErrorsBox {errors} /> |
|||
|
|||
<form on:submit|preventDefault class="uk-form-stacked"> |
|||
<Textbox label="Name" bind:text={field.name} /> |
|||
<Dropdown label="Type" bind:selected={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'} |
|||
<NumberBox |
|||
label="Min Value" |
|||
bind:value={constraints.numericality.greaterThanOrEqualTo} /> |
|||
<NumberBox |
|||
label="Max Value" |
|||
bind:value={constraints.numericality.lessThanOrEqualTo} /> |
|||
{/if} |
|||
</form> |
|||
</div> |
|||
<footer> |
|||
<div class="button"> |
|||
<ActionButton secondary on:click={goBack}>Cancel</ActionButton> |
|||
</div> |
|||
<ActionButton primary on:click={save}>Save</ActionButton> |
|||
</footer> |
|||
|
|||
<style> |
|||
.root { |
|||
margin: 40px; |
|||
} |
|||
footer { |
|||
padding: 20px 40px; |
|||
border-radius: 0 0 5px 5px; |
|||
bottom: 0; |
|||
left: 0; |
|||
background: var(--grey-light); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: flex-end; |
|||
} |
|||
|
|||
.button { |
|||
margin-right: 20px; |
|||
} |
|||
</style> |
|||
@ -1,6 +1,5 @@ |
|||
export { default as DeleteRecordModal } from "./DeleteRecord.svelte" |
|||
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte" |
|||
export { default as CreateEditModelModal } from "./CreateEditModel/CreateEditModel.svelte" |
|||
export { default as CreateEditViewModal } from "./CreateEditView.svelte" |
|||
export { default as CreateDatabaseModal } from "./CreateDatabase.svelte" |
|||
export { default as CreateUserModal } from "./CreateUser.svelte" |
|||
|
|||
@ -1,87 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import HierarchyRow from "./HierarchyRow.svelte" |
|||
import DatabasesList from "./DatabasesList.svelte" |
|||
import UsersList from "./UsersList.svelte" |
|||
import NavItem from "./NavItem.svelte" |
|||
import getIcon from "components/common/icon" |
|||
import { |
|||
CreateDatabaseModal, |
|||
CreateUserModal, |
|||
} from "components/database/ModelDataTable/modals" |
|||
const { open, close } = getContext("simple-modal") |
|||
|
|||
const openDatabaseCreator = () => { |
|||
open( |
|||
CreateDatabaseModal, |
|||
{ |
|||
onClosed: close, |
|||
}, |
|||
{ styleContent: { padding: "0" } } |
|||
) |
|||
} |
|||
const openUserCreator = () => { |
|||
open( |
|||
CreateUserModal, |
|||
{ |
|||
onClosed: close, |
|||
}, |
|||
{ styleContent: { padding: "0" } } |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<div class="items-root"> |
|||
<div class="hierarchy" /> |
|||
{#if $backendUiStore.selectedDatabase._id} |
|||
<div class="hierarchy"> |
|||
<div class="components-list-container"> |
|||
<div class="nav-group-header"> |
|||
<div class="hierarchy-title">Users</div> |
|||
<i class="ri-add-line hoverable" on:click={openUserCreator} /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="hierarchy-items-container"> |
|||
<UsersList /> |
|||
</div> |
|||
|
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.items-root { |
|||
display: flex; |
|||
flex-direction: column; |
|||
max-height: 100%; |
|||
height: 100%; |
|||
background: var(--white); |
|||
} |
|||
|
|||
.nav-group-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 20px 20px 10px 20px; |
|||
} |
|||
|
|||
.hierarchy-title { |
|||
align-items: center; |
|||
font-size: 18px; |
|||
font-weight: 700; |
|||
text-rendering: optimizeLegibility; |
|||
color: var(--ink); |
|||
} |
|||
|
|||
.hierarchy { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.hierarchy-items-container { |
|||
flex: 1 1 auto; |
|||
overflow-y: auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,80 @@ |
|||
<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 { |
|||
text-align: center; |
|||
padding: 10px; |
|||
font-weight: 500; |
|||
border-radius: 3px; |
|||
color: var(--ink-lighter); |
|||
font-size: 14px; |
|||
background: var(--light-grey); |
|||
} |
|||
|
|||
span:hover { |
|||
background: var(--blue-light); |
|||
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> |
|||
@ -0,0 +1,112 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { uuid } from "builderStore/uuid" |
|||
import { fade } from "svelte/transition" |
|||
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 |
|||
|
|||
const newModel = { ...model, schema: {} } |
|||
|
|||
// TODO: could be better |
|||
for (let key in model.schema) { |
|||
newModel.schema[uuid()] = model.schema[key] |
|||
} |
|||
|
|||
backendUiStore.actions.models.save({ |
|||
model: { |
|||
...newModel, |
|||
...rest, |
|||
}, |
|||
}) |
|||
} |
|||
</script> |
|||
|
|||
<section transition:fade> |
|||
<header> |
|||
<h2>Create New Model</h2> |
|||
<p>Before you can view your model, 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 model 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 model 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">Models</span> |
|||
<p>Blocks are pre-made fields and help you build your model 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> |
|||
@ -0,0 +1,47 @@ |
|||
<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: 10px; |
|||
} |
|||
|
|||
div { |
|||
padding: 0 10px 0 10px; |
|||
height: 36px; |
|||
border-radius: 5px; |
|||
display: flex; |
|||
align-items: center; |
|||
transition: 0.3s background-color; |
|||
color: var(--ink); |
|||
font-weight: 400; |
|||
font-size: 14px; |
|||
margin-top: 4px; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
.selected { |
|||
background-color: var(--blue-light); |
|||
} |
|||
|
|||
div:hover { |
|||
background-color: var(--blue-light); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
i { |
|||
color: var(--grey-7); |
|||
font-size: 20px; |
|||
margin-right: 8px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,107 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { slide } from "svelte/transition" |
|||
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" |
|||
|
|||
const { open, close } = getContext("simple-modal") |
|||
|
|||
let HEADINGS = [ |
|||
{ |
|||
title: "Navigate", |
|||
key: "NAVIGATE", |
|||
}, |
|||
{ |
|||
title: "Add", |
|||
key: "ADD", |
|||
}, |
|||
] |
|||
|
|||
$: selectedTab = $backendUiStore.tabs.NAVIGATION_PANEL |
|||
|
|||
function selectModel(model, fieldId) { |
|||
backendUiStore.actions.models.select(model) |
|||
|
|||
if (fieldId) { |
|||
backendUiStore.update(state => { |
|||
state.selectedField = fieldId |
|||
return state |
|||
}) |
|||
} |
|||
} |
|||
|
|||
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 Model |
|||
</Button> |
|||
<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 fieldId} |
|||
<ListItem |
|||
selected={model._id === $backendUiStore.selectedModel._id && fieldId === $backendUiStore.selectedField} |
|||
indented |
|||
icon="ri-layout-column-fill" |
|||
title={model.schema[fieldId].name} |
|||
on:click={() => selectModel(model, fieldId)} /> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
{/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,132 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { Button } from "@budibase/bbui" |
|||
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 { keys, cloneDeep } from "lodash/fp" |
|||
|
|||
const FIELD_TYPES = ["string", "number", "boolean", "link"] |
|||
|
|||
let field = {} |
|||
|
|||
$: field = |
|||
$backendUiStore.draftModel.schema[$backendUiStore.selectedField] || {} |
|||
$: required = |
|||
field.constraints && |
|||
field.constraints.presence && |
|||
!constraints.presence.allowEmpty |
|||
|
|||
function attachModelIdToSchema(evt) { |
|||
const { draftModel } = $backendUiStore |
|||
if ($backendUiStore.selectedField !== evt.target.value) { |
|||
delete draftModel.schema[$backendUiStore.selectedField] |
|||
draftModel.schema[evt.target.value] = field |
|||
backendUiStore.update(state => { |
|||
state.selectedField = evt.target.value |
|||
return state |
|||
}) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="info"> |
|||
<div class="field-box"> |
|||
<header>Name</header> |
|||
<input class="budibase__input" type="text" bind:value={field.name} /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="info"> |
|||
<div class="field-box"> |
|||
<header>Type</header> |
|||
<span>{field.type}</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="info"> |
|||
<div class="field"> |
|||
<label>Required</label> |
|||
<input type="checkbox" /> |
|||
</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} |
|||
on:change={attachModelIdToSchema}> |
|||
<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> |
|||
|
|||
<style> |
|||
.info { |
|||
margin-bottom: 16px; |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
label { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.field { |
|||
display: grid; |
|||
align-items: center; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.field-box header { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.field-box span { |
|||
background: var(--grey-2); |
|||
color: var(--grey-6); |
|||
font-weight: 400; |
|||
height: 36px; |
|||
display: grid; |
|||
align-items: center; |
|||
padding-left: 12px; |
|||
text-transform: capitalize; |
|||
border-radius: 5px; |
|||
cursor: not-allowed; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,127 @@ |
|||
<script> |
|||
import { getContext, onMount } from "svelte" |
|||
import { Button, Switcher } from "@budibase/bbui" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import ModelFieldEditor from "./ModelFieldEditor.svelte" |
|||
|
|||
const { open, close } = getContext("simple-modal") |
|||
|
|||
const ITEMS = [ |
|||
{ |
|||
title: "Setup", |
|||
key: "SETUP", |
|||
}, |
|||
{ |
|||
title: "Delete", |
|||
key: "DELETE", |
|||
}, |
|||
] |
|||
|
|||
let edited = false |
|||
|
|||
$: selectedTab = $backendUiStore.tabs.SETUP_PANEL |
|||
|
|||
$: edited = |
|||
$backendUiStore.selectedField || |
|||
($backendUiStore.draftModel && |
|||
$backendUiStore.draftModel.name !== $backendUiStore.selectedModel.name) |
|||
|
|||
async function deleteModel() { |
|||
const model = $backendUiStore.selectedModel |
|||
const field = $backendUiStore.selectedField |
|||
|
|||
if (field) { |
|||
const name = model.schema[field].name |
|||
delete model.schema[field] |
|||
backendUiStore.actions.models.save({ model }) |
|||
notifier.danger(`Field ${name} deleted.`) |
|||
return |
|||
} |
|||
|
|||
const DELETE_MODEL_URL = `/api/models/${model._id}/${model._rev}` |
|||
const response = await api.delete(DELETE_MODEL_URL) |
|||
backendUiStore.update(state => { |
|||
state.selectedView = null |
|||
state.selectedModel = {} |
|||
state.draftModel = {} |
|||
state.models = state.models.filter(({ _id }) => _id !== model._id) |
|||
notifier.danger(`${model.name} deleted successfully.`) |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
async function saveModel() { |
|||
await backendUiStore.actions.models.save({ |
|||
model: $backendUiStore.draftModel, |
|||
}) |
|||
notifier.success( |
|||
"Success! Your changes have been saved. Please continue on with your greatness." |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<div class="items-root"> |
|||
<Switcher headings={ITEMS} bind:value={$backendUiStore.tabs.SETUP_PANEL}> |
|||
{#if selectedTab === 'SETUP'} |
|||
{#if $backendUiStore.selectedField} |
|||
<ModelFieldEditor /> |
|||
{:else if $backendUiStore.draftModel.schema} |
|||
<div class="titled-input"> |
|||
<header>Name</header> |
|||
<input |
|||
type="text" |
|||
class="budibase__input" |
|||
bind:value={$backendUiStore.draftModel.name} /> |
|||
</div> |
|||
<div class="titled-input"> |
|||
<header>Import Data</header> |
|||
<Button wide secondary>Import CSV</Button> |
|||
</div> |
|||
{/if} |
|||
<footer> |
|||
<Button disabled={!edited} green={edited} wide on:click={saveModel}> |
|||
Save |
|||
</Button> |
|||
</footer> |
|||
{:else if selectedTab === 'DELETE'} |
|||
<div class="titled-input"> |
|||
<header>Danger Zone</header> |
|||
<Button red wide on:click={deleteModel}>Delete</Button> |
|||
</div> |
|||
{/if} |
|||
</Switcher> |
|||
</div> |
|||
|
|||
<style> |
|||
header { |
|||
font-weight: 500; |
|||
} |
|||
|
|||
footer { |
|||
width: 100%; |
|||
position: fixed; |
|||
bottom: 20px; |
|||
} |
|||
|
|||
.items-root { |
|||
padding: 20px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
max-height: 100%; |
|||
height: 100%; |
|||
background-color: var(--white); |
|||
} |
|||
|
|||
.titled-input { |
|||
margin-bottom: 16px; |
|||
display: grid; |
|||
} |
|||
|
|||
.titled-input header { |
|||
display: block; |
|||
font-size: 14px; |
|||
margin-bottom: 8px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1 @@ |
|||
export { default as ModelSetupNav } from "./ModelSetupNav.svelte" |
|||
@ -1,143 +0,0 @@ |
|||
<script> |
|||
import { getContext, onMount } from "svelte" |
|||
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") |
|||
|
|||
function editModel() { |
|||
open( |
|||
CreateEditModelModal, |
|||
{ |
|||
model: node, |
|||
onClosed: close, |
|||
}, |
|||
{ styleContent: { padding: "0" } } |
|||
) |
|||
} |
|||
|
|||
function newModel() { |
|||
open( |
|||
CreateEditModelModal, |
|||
{ |
|||
onClosed: close, |
|||
}, |
|||
{ styleContent: { padding: "0" } } |
|||
) |
|||
} |
|||
|
|||
function newView() { |
|||
open( |
|||
CreateEditViewModal, |
|||
{ |
|||
onClosed: close, |
|||
}, |
|||
{ styleContent: { padding: "0" } } |
|||
) |
|||
} |
|||
|
|||
function selectModel(model) { |
|||
backendUiStore.update(state => { |
|||
state.selectedModel = model |
|||
state.selectedView = `all_${model._id}` |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
async function deleteModel(modelToDelete) { |
|||
const DELETE_MODEL_URL = `/api/models/${node._id}/${node._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"> |
|||
<div class="hierarchy"> |
|||
<div class="components-list-container"> |
|||
<div class="nav-group-header"> |
|||
<div class="hierarchy-title">Models</div> |
|||
<div class="uk-inline"> |
|||
<i class="ri-add-line hoverable" on:click={newModel} /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="hierarchy-items-container"> |
|||
{#each $backendUiStore.models as model} |
|||
<HierarchyRow onSelect={selectModel} node={model} type="model" /> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="hierarchy"> |
|||
<div class="components-list-container"> |
|||
<div class="nav-group-header"> |
|||
<div class="hierarchy-title">Views</div> |
|||
<div class="uk-inline"> |
|||
<i class="ri-add-line hoverable" on:click={newView} /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="hierarchy-items-container"> |
|||
{#each $backendUiStore.views as view} |
|||
<HierarchyRow onSelect={selectView} node={view} type="view" /> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.items-root { |
|||
display: flex; |
|||
flex-direction: column; |
|||
max-height: 100%; |
|||
height: 100%; |
|||
background-color: var(--white); |
|||
} |
|||
|
|||
.nav-group-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 20px 20px 10px 20px; |
|||
} |
|||
|
|||
.hierarchy-title { |
|||
align-items: center; |
|||
font-size: 18px; |
|||
font-weight: 700; |
|||
text-rendering: optimizeLegibility; |
|||
color: var(--ink); |
|||
} |
|||
|
|||
.hierarchy { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.hierarchy-items-container { |
|||
flex: 1 1 auto; |
|||
overflow-y: auto; |
|||
} |
|||
</style> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue