mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
58 changed files with 1631 additions and 1245 deletions
@ -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,76 +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(--grey-9) !important; |
|||
color: #fff !important; |
|||
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-1); |
|||
} |
|||
</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: 3px; |
|||
color: var(--ink); |
|||
font-weight: 500; |
|||
padding: 15px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
flex-direction: column; |
|||
transition: 0.3s transform; |
|||
background: var(--light-grey); |
|||
} |
|||
|
|||
i { |
|||
font-size: 30px; |
|||
} |
|||
|
|||
span { |
|||
font-size: 14px; |
|||
text-align: center; |
|||
} |
|||
|
|||
div:hover { |
|||
cursor: pointer; |
|||
transform: scale(1.1); |
|||
} |
|||
|
|||
.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-1); |
|||
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: 600; |
|||
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,81 @@ |
|||
<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-gap: 20px; |
|||
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(--blue-light); |
|||
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,48 @@ |
|||
<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; |
|||
width: 90%; |
|||
height: 40px; |
|||
border-radius: 3px; |
|||
display: flex; |
|||
align-items: center; |
|||
transition: 0.3s background-color; |
|||
color: var(--ink); |
|||
font-weight: 500; |
|||
font-size: 16px; |
|||
margin-top: 4px; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
.selected { |
|||
background-color: var(--blue-light); |
|||
} |
|||
|
|||
div:hover { |
|||
background-color: var(--blue-light); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
i { |
|||
color: var(--dark-grey); |
|||
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 secondary 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,124 @@ |
|||
<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 { |
|||
background: var(--light-grey); |
|||
padding: 12px; |
|||
margin-bottom: 5px; |
|||
border-radius: 3px; |
|||
} |
|||
|
|||
label { |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.field { |
|||
display: grid; |
|||
align-items: center; |
|||
grid-template-columns: 40% 1fr; |
|||
margin-top: 8px; |
|||
} |
|||
|
|||
.field-box header { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.field-box span { |
|||
font-weight: bold; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,128 @@ |
|||
<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} attention={edited} wide on:click={saveModel}> |
|||
Save |
|||
</Button> |
|||
</footer> |
|||
{: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> |
|||
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 { |
|||
padding: 12px; |
|||
background: var(--light-grey); |
|||
margin-bottom: 5px; |
|||
} |
|||
|
|||
.titled-input header { |
|||
display: block; |
|||
font-size: 14px; |
|||
margin-bottom: 16px; |
|||
} |
|||
</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: 600; |
|||
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,180 @@ |
|||
export const FIELDS = { |
|||
PLAIN_TEXT: { |
|||
name: "Plain Text", |
|||
icon: "ri-text", |
|||
type: "string", |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
}, |
|||
}, |
|||
NUMBER: { |
|||
name: "Number", |
|||
icon: "ri-number-1", |
|||
type: "number", |
|||
constraints: { |
|||
type: "number", |
|||
presence: false, |
|||
numericality: {}, |
|||
}, |
|||
}, |
|||
BOOLEAN: { |
|||
name: "True/False", |
|||
icon: "ri-toggle-line", |
|||
type: "boolean", |
|||
constraints: { |
|||
type: "boolean", |
|||
presence: false, |
|||
}, |
|||
}, |
|||
// OPTIONS: {
|
|||
// name: "Options",
|
|||
// icon: "ri-list-check-2",
|
|||
// type: "options",
|
|||
// constraints: {
|
|||
// type: "string",
|
|||
// presence: false,
|
|||
// },
|
|||
// },
|
|||
DATETIME: { |
|||
name: "Date/Time", |
|||
icon: "ri-calendar-event-fill", |
|||
type: "datetime", |
|||
constraints: { |
|||
type: "date", |
|||
datetime: {}, |
|||
presence: false, |
|||
}, |
|||
}, |
|||
// IMAGE: {
|
|||
// name: "File",
|
|||
// icon: "ri-image-line",
|
|||
// type: "file",
|
|||
// constraints: {
|
|||
// type: "string",
|
|||
// presence: false,
|
|||
// },
|
|||
// },
|
|||
// FILE: {
|
|||
// name: "Image",
|
|||
// 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 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,
|
|||
// },
|
|||
// },
|
|||
} |
|||
|
|||
export const MODELS = { |
|||
CONTACTS: { |
|||
icon: "ri-contacts-book-line", |
|||
name: "Contacts", |
|||
schema: { |
|||
Name: BLOCKS.NAME, |
|||
"Phone Number": BLOCKS.PHONE_NUMBER, |
|||
}, |
|||
}, |
|||
RECIPES: { |
|||
icon: "ri-link", |
|||
name: "Recipes", |
|||
schema: { |
|||
Name: BLOCKS.NAME, |
|||
Cuisine: { |
|||
...FIELDS.PLAIN_TEXT, |
|||
name: "Cuisine", |
|||
}, |
|||
}, |
|||
}, |
|||
SPORTS_TEAM: { |
|||
icon: "ri-basketball-line", |
|||
name: "Sports Team", |
|||
schema: { |
|||
Name: BLOCKS.NAME, |
|||
Championships: { |
|||
...FIELDS.NUMBER, |
|||
name: "Championships", |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
@ -1,647 +0,0 @@ |
|||
body, html { |
|||
margin: 0px; |
|||
padding: 0px; |
|||
overflow: hidden; |
|||
background-repeat: repeat; |
|||
background-size: 30px 30px; |
|||
background-color: var(--grey-1); |
|||
height: 100%; |
|||
} |
|||
#navigation { |
|||
height: 71px; |
|||
background-color: #FFF; |
|||
border: 1px solid #E8E8EF; |
|||
width: 100%; |
|||
display: table; |
|||
box-sizing: border-box; |
|||
position: fixed; |
|||
top: 0; |
|||
z-index: 9 |
|||
} |
|||
#back { |
|||
width: 40px; |
|||
height: 40px; |
|||
border-radius: 100px; |
|||
background-color: #F1F4FC; |
|||
text-align: center; |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
margin-top: 12px; |
|||
margin-right: 10px |
|||
} |
|||
#back img { |
|||
margin-top: 13px; |
|||
} |
|||
#names { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
} |
|||
#title { |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
font-size: 16px; |
|||
color: #393C44; |
|||
margin-bottom: 0px; |
|||
} |
|||
#subtitle { |
|||
font-family: Roboto; |
|||
color: #808292; |
|||
font-size: 14px; |
|||
margin-top: 5px; |
|||
} |
|||
#leftside { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
margin-left: 20px; |
|||
} |
|||
#centerswitch { |
|||
position: absolute; |
|||
width: 222px; |
|||
left: 50%; |
|||
margin-left: -111px; |
|||
top: 15px; |
|||
} |
|||
#leftswitch { |
|||
border: 1px solid #E8E8EF; |
|||
background-color: var(--grey-1); |
|||
width: 111px; |
|||
height: 39px; |
|||
line-height: 39px; |
|||
border-radius: 5px 0px 0px 5px; |
|||
font-family: Roboto; |
|||
color: #393C44; |
|||
display: inline-block; |
|||
font-size: 14px; |
|||
text-align: center; |
|||
} |
|||
#rightswitch { |
|||
font-family: Roboto; |
|||
color: #808292; |
|||
border-radius: 0px 5px 5px 0px; |
|||
border: 1px solid #E8E8EF; |
|||
height: 39px; |
|||
width: 102px; |
|||
display: inline-block; |
|||
font-size: 14px; |
|||
line-height: 39px; |
|||
text-align: center; |
|||
margin-left: -5px; |
|||
} |
|||
#discard { |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
color: #A6A6B3; |
|||
width: 95px; |
|||
height: 38px; |
|||
border: 1px solid #E8E8EF; |
|||
border-radius: 5px; |
|||
text-align: center; |
|||
line-height: 38px; |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
transition: all .2s cubic-bezier(.05,.03,.35,1); |
|||
} |
|||
#discard:hover { |
|||
cursor: pointer; |
|||
opacity: .7; |
|||
} |
|||
#publish { |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
color: #FFF; |
|||
background-color: #217CE8; |
|||
border-radius: 5px; |
|||
width: 143px; |
|||
height: 38px; |
|||
margin-left: 10px; |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
text-align: center; |
|||
line-height: 38px; |
|||
margin-right: 20px; |
|||
transition: all .2s cubic-bezier(.05,.03,.35,1); |
|||
} |
|||
#publish:hover { |
|||
cursor: pointer; |
|||
opacity: .7; |
|||
} |
|||
#buttonsright { |
|||
float: right; |
|||
margin-top: 15px; |
|||
} |
|||
#leftcard { |
|||
width: 363px; |
|||
background-color: #FFF; |
|||
border: 1px solid #E8E8EF; |
|||
box-sizing: border-box; |
|||
padding-top: 85px; |
|||
padding-left: 20px; |
|||
height: 100%; |
|||
position: absolute; |
|||
z-index: 2; |
|||
} |
|||
#search input { |
|||
width: 318px; |
|||
height: 40px; |
|||
background-color: #FFF; |
|||
border: 1px solid #E8E8EF; |
|||
box-sizing: border-box; |
|||
box-shadow: 0px 2px 8px rgba(34,34,87,0.05); |
|||
border-radius: 5px; |
|||
text-indent: 35px; |
|||
font-family: Roboto; |
|||
font-size: 16px; |
|||
} |
|||
::-webkit-input-placeholder { /* Edge */ |
|||
color: #C9C9D5; |
|||
} |
|||
|
|||
:-ms-input-placeholder { /* Internet Explorer 10-11 */ |
|||
color: #C9C9D5 |
|||
} |
|||
|
|||
::placeholder { |
|||
color: #C9C9D5; |
|||
} |
|||
#search img { |
|||
position: absolute; |
|||
margin-top: 10px; |
|||
width: 18px; |
|||
margin-left: 12px; |
|||
} |
|||
#header { |
|||
font-size: 20px; |
|||
font-family: Roboto; |
|||
font-weight: bold; |
|||
color: #393C44; |
|||
} |
|||
#subnav { |
|||
border-bottom: 1px solid #E8E8EF; |
|||
width: calc(100% + 20px); |
|||
margin-left: -20px; |
|||
margin-top: 10px; |
|||
} |
|||
.navdisabled { |
|||
transition: all .3s cubic-bezier(.05,.03,.35,1); |
|||
} |
|||
.navdisabled:hover { |
|||
cursor: pointer; |
|||
opacity: .5; |
|||
} |
|||
.navactive { |
|||
color: #393C44!important; |
|||
} |
|||
#triggers { |
|||
margin-left: 20px; |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
text-align: center; |
|||
color: #808292; |
|||
width: calc(88% / 3); |
|||
height: 48px; |
|||
line-height: 48px; |
|||
display: inline-block; |
|||
float: left; |
|||
} |
|||
.navactive:after { |
|||
display: block; |
|||
content: ""; |
|||
width: 100%; |
|||
height: 4px; |
|||
background-color: #217CE8; |
|||
margin-top: -4px; |
|||
} |
|||
#actions { |
|||
display: inline-block; |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
color: #808292; |
|||
font-size: 14px; |
|||
height: 48px; |
|||
line-height: 48px; |
|||
width: calc(88% / 3); |
|||
text-align: center; |
|||
float: left; |
|||
} |
|||
#loggers { |
|||
width: calc(88% / 3); |
|||
display: inline-block; |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
color: #808292; |
|||
font-size: 14px; |
|||
height: 48px; |
|||
line-height: 48px; |
|||
text-align: center; |
|||
} |
|||
#footer { |
|||
position: absolute; |
|||
left: 0; |
|||
padding-left: 20px; |
|||
line-height: 40px; |
|||
bottom: 0; |
|||
width: 362px; |
|||
border: 1px solid #E8E8EF; |
|||
height: 67px; |
|||
box-sizing: border-box; |
|||
background-color: #FFF; |
|||
font-family: Roboto; |
|||
font-size: 14px; |
|||
} |
|||
#footer a { |
|||
text-decoration: none; |
|||
color: #393C44; |
|||
transition: all .2s cubic-bezier(.05,.03,.35,1); |
|||
} |
|||
#footer a:hover { |
|||
opacity: .5; |
|||
} |
|||
#footer span { |
|||
color: #808292; |
|||
} |
|||
#footer p { |
|||
display: inline-block; |
|||
color: #808292; |
|||
} |
|||
#footer img { |
|||
margin-left: 5px; |
|||
margin-right: 5px; |
|||
} |
|||
.blockelem:first-child { |
|||
margin-top: 20px |
|||
} |
|||
.blockelem { |
|||
padding-top: 10px; |
|||
width: 318px; |
|||
border: 1px solid transparent; |
|||
transition-property: box-shadow, height; |
|||
transition-duration: .2s; |
|||
transition-timing-function: cubic-bezier(.05,.03,.35,1); |
|||
border-radius: 5px; |
|||
box-shadow: 0px 0px 30px rgba(22, 33, 74, 0); |
|||
box-sizing: border-box; |
|||
} |
|||
.blockelem:hover { |
|||
box-shadow: 0px 4px 30px rgba(22, 33, 74, 0.08); |
|||
border-radius: 5px; |
|||
background-color: #FFF; |
|||
cursor: pointer; |
|||
} |
|||
.grabme, .blockico { |
|||
display: inline-block; |
|||
} |
|||
|
|||
.grabme { |
|||
margin-top: 10px; |
|||
margin-left: 10px; |
|||
margin-bottom: -14px; |
|||
width: 15px; |
|||
} |
|||
#blocklist { |
|||
height: calc(100% - 220px); |
|||
overflow: auto; |
|||
} |
|||
#proplist { |
|||
height: calc(100% - 305px); |
|||
overflow: auto; |
|||
margin-top: -30px; |
|||
padding-top: 30px; |
|||
} |
|||
.blockin { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
margin-left: 12px; |
|||
} |
|||
.blockico { |
|||
width: 36px; |
|||
height: 36px; |
|||
background-color: #F1F4FC; |
|||
border-radius: 5px; |
|||
text-align: center; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.blockico i { |
|||
font-size: 24px; |
|||
color: var(--grey-4); |
|||
} |
|||
|
|||
.blockico span { |
|||
height: 100%; |
|||
width: 0px; |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
} |
|||
.blockico img { |
|||
vertical-align: middle; |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
display: inline-block; |
|||
} |
|||
.blocktext { |
|||
display: inline-block; |
|||
width: 220px; |
|||
vertical-align: top; |
|||
margin-left: 12px |
|||
} |
|||
.blocktitle { |
|||
margin: 0px!important; |
|||
padding: 0px!important; |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
font-size: 16px; |
|||
color: #393C44; |
|||
} |
|||
.blockdesc { |
|||
margin-top: 5px; |
|||
font-family: Roboto; |
|||
color: #808292; |
|||
font-size: 14px; |
|||
line-height: 21px; |
|||
} |
|||
.blockdisabled { |
|||
background-color: #F0F2F9; |
|||
opacity: .5; |
|||
} |
|||
#closecard { |
|||
position: absolute; |
|||
margin-left: 340px; |
|||
background-color: #FFF; |
|||
border-radius: 0px 5px 5px 0px; |
|||
border-bottom: 1px solid #E8E8EF; |
|||
border-right: 1px solid #E8E8EF; |
|||
border-top: 1px solid #E8E8EF; |
|||
width: 53px; |
|||
height: 53px; |
|||
text-align: center; |
|||
z-index: 10; |
|||
} |
|||
#closecard img { |
|||
margin-top: 15px |
|||
} |
|||
#canvas { |
|||
border: 1px solid green; |
|||
position: absolute; |
|||
width: calc(100% - 361px); |
|||
height: calc(100% - 71px); |
|||
top: 71px; |
|||
left: 361px; |
|||
z-index: 0; |
|||
overflow: auto; |
|||
} |
|||
#propwrap { |
|||
position: absolute; |
|||
right: 0; |
|||
top: 0; |
|||
width: 311px; |
|||
height: 100%; |
|||
padding-left: 20px; |
|||
overflow: hidden; |
|||
z-index: -2; |
|||
} |
|||
#properties { |
|||
position: absolute; |
|||
height: 100%; |
|||
width: 311px; |
|||
background-color: #FFF; |
|||
right: -150px; |
|||
opacity: 0; |
|||
z-index: 2; |
|||
top: 0px; |
|||
box-shadow: -4px 0px 40px rgba(26, 26, 73, 0); |
|||
padding-left: 20px; |
|||
transition: all .25s cubic-bezier(.05,.03,.35,1); |
|||
} |
|||
.itson { |
|||
z-index: 2!important; |
|||
} |
|||
.expanded { |
|||
right: 0!important; |
|||
opacity: 1!important; |
|||
box-shadow: -4px 0px 40px rgba(26, 26, 73, 0.05); |
|||
z-index: 2; |
|||
} |
|||
#header2 { |
|||
font-size: 20px; |
|||
font-family: Roboto; |
|||
font-weight: bold; |
|||
color: #393C44; |
|||
margin-top: 101px; |
|||
} |
|||
#close { |
|||
margin-top: 100px; |
|||
position: absolute; |
|||
right: 20px; |
|||
z-index: 9999; |
|||
transition: all .25s cubic-bezier(.05,.03,.35,1); |
|||
} |
|||
#close:hover { |
|||
cursor: pointer; |
|||
opacity: .7; |
|||
} |
|||
#propswitch { |
|||
border-bottom: 1px solid #E8E8EF; |
|||
width: 331px; |
|||
margin-top: 10px; |
|||
margin-left: -20px; |
|||
margin-bottom: 30px; |
|||
} |
|||
#dataprop { |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
text-align: center; |
|||
color: #393C44; |
|||
width: calc(88% / 3); |
|||
height: 48px; |
|||
line-height: 48px; |
|||
display: inline-block; |
|||
float: left; |
|||
margin-left: 20px; |
|||
} |
|||
#dataprop:after { |
|||
display: block; |
|||
content: ""; |
|||
width: 100%; |
|||
height: 4px; |
|||
background-color: #217CE8; |
|||
margin-top: -4px; |
|||
} |
|||
#alertprop { |
|||
display: inline-block; |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
color: #808292; |
|||
font-size: 14px; |
|||
height: 48px; |
|||
line-height: 48px; |
|||
width: calc(88% / 3); |
|||
text-align: center; |
|||
float: left; |
|||
} |
|||
#logsprop { |
|||
width: calc(88% / 3); |
|||
display: inline-block; |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
color: #808292; |
|||
font-size: 14px; |
|||
height: 48px; |
|||
line-height: 48px; |
|||
text-align: center; |
|||
} |
|||
.inputlabel { |
|||
font-family: Roboto; |
|||
font-size: 14px; |
|||
color: #253134; |
|||
} |
|||
.dropme { |
|||
background-color: #FFF; |
|||
border-radius: 5px; |
|||
border: 1px solid #E8E8EF; |
|||
box-shadow: 0px 2px 8px rgba(34, 34, 87, 0.05); |
|||
font-family: Roboto; |
|||
font-size: 14px; |
|||
color: #253134; |
|||
text-indent: 20px; |
|||
height: 40px; |
|||
line-height: 40px; |
|||
width: 287px; |
|||
margin-bottom: 25px; |
|||
} |
|||
.dropme img { |
|||
margin-top: 17px; |
|||
float: right; |
|||
margin-right: 15px; |
|||
} |
|||
.checkus { |
|||
margin-bottom: 10px; |
|||
} |
|||
.checkus img { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
} |
|||
.checkus p { |
|||
display: inline-block; |
|||
font-family: Roboto; |
|||
font-size: 14px; |
|||
vertical-align: middle; |
|||
margin-left: 10px; |
|||
} |
|||
#divisionthing { |
|||
height: 1px; |
|||
width: 100%; |
|||
background-color: #E8E8EF; |
|||
position: absolute; |
|||
right: 0px; |
|||
bottom: 80; |
|||
} |
|||
#removeblock { |
|||
border-radius: 5px; |
|||
position: absolute; |
|||
bottom: 20px; |
|||
font-family: Roboto; |
|||
font-size: 14px; |
|||
text-align: center; |
|||
width: 287px; |
|||
height: 38px; |
|||
line-height: 38px; |
|||
color: #253134; |
|||
border: 1px solid #E8E8EF; |
|||
transition: all .3s cubic-bezier(.05,.03,.35,1); |
|||
} |
|||
#removeblock:hover { |
|||
cursor: pointer; |
|||
opacity: .5; |
|||
} |
|||
.noselect { |
|||
-webkit-touch-callout: none; /* iOS Safari */ |
|||
-webkit-user-select: none; /* Safari */ |
|||
-khtml-user-select: none; /* Konqueror HTML */ |
|||
-moz-user-select: none; /* Old versions of Firefox */ |
|||
-ms-user-select: none; /* Internet Explorer/Edge */ |
|||
user-select: none; /* Non-prefixed version, currently |
|||
supported by Chrome, Opera and Firefox */ |
|||
} |
|||
.blockyname { |
|||
font-family: Roboto; |
|||
font-weight: 500; |
|||
color: #253134; |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
margin-left: 8px; |
|||
font-size: 16px; |
|||
} |
|||
.blockyleft img { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
} |
|||
.blockyright { |
|||
display: inline-block; |
|||
float: right; |
|||
vertical-align: middle; |
|||
margin-right: 20px; |
|||
margin-top: 10px; |
|||
width: 28px; |
|||
height: 28px; |
|||
border-radius: 5px; |
|||
text-align: center; |
|||
background-color: #FFF; |
|||
transition: all .3s cubic-bezier(.05,.03,.35,1); |
|||
z-index: 10; |
|||
} |
|||
.blockyright:hover { |
|||
background-color: #F1F4FC; |
|||
cursor: pointer; |
|||
} |
|||
.blockyright img { |
|||
margin-top: 12px; |
|||
} |
|||
.blockyleft { |
|||
display: inline-block; |
|||
margin-left: 20px; |
|||
} |
|||
.blockydiv { |
|||
width: 100%; |
|||
height: 1px; |
|||
background-color: #E9E9EF; |
|||
} |
|||
.blockyinfo { |
|||
font-family: Roboto; |
|||
font-size: 14px; |
|||
color: #808292; |
|||
margin-top: 15px; |
|||
text-indent: 20px; |
|||
margin-bottom: 20px; |
|||
} |
|||
.blockyinfo span { |
|||
color: #253134; |
|||
font-weight: 500; |
|||
display: inline-block; |
|||
border-bottom: 1px solid #D3DCEA; |
|||
line-height: 20px; |
|||
text-indent: 0px; |
|||
} |
|||
.block { |
|||
background-color: #FFF; |
|||
margin-top: 0px!important; |
|||
box-shadow: 0px 4px 30px rgba(22, 33, 74, 0.05); |
|||
} |
|||
.selectedblock { |
|||
border: 2px solid #217CE8; |
|||
box-shadow: 0px 4px 30px rgba(22, 33, 74, 0.08); |
|||
} |
|||
|
|||
@media only screen and (max-width: 832px) { |
|||
#centerswitch { |
|||
display: none; |
|||
} |
|||
} |
|||
@media only screen and (max-width: 560px) { |
|||
#names { |
|||
display: none; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
<slot /> |
|||
Loading…
Reference in new issue