mirror of https://github.com/Budibase/budibase.git
250 changed files with 3508 additions and 10685 deletions
@ -0,0 +1,85 @@ |
|||
context('Create a View', () => { |
|||
before(() => { |
|||
cy.visit('localhost:4001/_builder') |
|||
cy.createApp('View App', 'View App Description') |
|||
cy.createTable('data') |
|||
cy.addColumn('data', 'group', 'Plain Text') |
|||
cy.addColumn('data', 'age', 'Number') |
|||
cy.addColumn('data', 'rating', 'Number') |
|||
|
|||
cy.addRecord(["Students", 25, 1]) |
|||
cy.addRecord(["Students", 20, 3]) |
|||
cy.addRecord(["Students", 18, 6]) |
|||
cy.addRecord(["Students", 25, 2]) |
|||
cy.addRecord(["Teachers", 49, 5]) |
|||
cy.addRecord(["Teachers", 36, 3]) |
|||
}) |
|||
|
|||
|
|||
it('creates a stats view based on age', () => { |
|||
cy.contains("Create New View").click() |
|||
cy.get("[placeholder='View Name']").type("Test View") |
|||
cy.contains("Save View").click() |
|||
cy.get("thead th").should(($headers) => { |
|||
expect($headers).to.have.length(7) |
|||
const headers = $headers.map((i, header) => Cypress.$(header).text()) |
|||
expect(headers.get()).to.deep.eq([ |
|||
"group", |
|||
"sum", |
|||
"min", |
|||
"max", |
|||
"sumsqr", |
|||
"count", |
|||
"avg", |
|||
]) |
|||
}) |
|||
cy.get("tbody td").should(($values) => { |
|||
const values = $values.map((i, value) => Cypress.$(value).text()) |
|||
expect(values.get()).to.deep.eq([ |
|||
"null", |
|||
"173", |
|||
"18", |
|||
"49", |
|||
"5671", |
|||
"6", |
|||
"28.833333333333332" |
|||
]) |
|||
}) |
|||
}) |
|||
|
|||
it('groups the stats view by group', () => { |
|||
cy.contains("Group By").click() |
|||
cy.get("select").select("group") |
|||
cy.contains("Save").click() |
|||
cy.contains("Students").should("be.visible") |
|||
cy.contains("Teachers").should("be.visible") |
|||
|
|||
cy.get("tbody tr").first().find("td").should(($values) => { |
|||
const values = $values.map((i, value) => Cypress.$(value).text()) |
|||
expect(values.get()).to.deep.eq([ |
|||
"Students", |
|||
"88", |
|||
"18", |
|||
"25", |
|||
"1974", |
|||
"4", |
|||
"22" |
|||
]) |
|||
}) |
|||
}) |
|||
|
|||
it('renames a view', () => { |
|||
cy.contains("[data-cy=model-nav-item]", "Test View").find(".ri-more-line").click() |
|||
cy.contains("Edit").click() |
|||
cy.get("[placeholder='View Name']").type(" Updated") |
|||
cy.contains("Save").click() |
|||
cy.contains("Test View Updated").should("be.visible") |
|||
}) |
|||
|
|||
it('deletes a view', () => { |
|||
cy.contains("[data-cy=model-nav-item]", "Test View Updated").find(".ri-more-line").click() |
|||
cy.contains("Delete").click() |
|||
cy.get(".content").contains("button", "Delete").click() |
|||
cy.contains("TestView Updated").should("not.be.visible") |
|||
}) |
|||
}) |
|||
@ -1,59 +0,0 @@ |
|||
<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: 24px; |
|||
color: var(--grey-7); |
|||
} |
|||
|
|||
span { |
|||
font-size: 14px; |
|||
text-align: center; |
|||
margin-top: 8px; |
|||
line-height: 1.25; |
|||
} |
|||
|
|||
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> |
|||
@ -1,20 +0,0 @@ |
|||
<script> |
|||
import { JavaScriptIcon } from "../common/Icons" |
|||
// todo: use https://ace.c9.io |
|||
export let text = "" |
|||
</script> |
|||
|
|||
<textarea class="uk-textarea" bind:value={text} /> |
|||
|
|||
<style> |
|||
textarea { |
|||
padding: 10px; |
|||
margin-top: 5px; |
|||
margin-bottom: 10px; |
|||
background: var(--grey-7); |
|||
color: var(--white); |
|||
font-family: "Courier New", Courier, monospace; |
|||
height: 200px; |
|||
border-radius: 5px; |
|||
} |
|||
</style> |
|||
@ -1,13 +0,0 @@ |
|||
<script> |
|||
export let name = "" |
|||
</script> |
|||
|
|||
<div> |
|||
<h4>Coming Sometime: {name}</h4> |
|||
</div> |
|||
|
|||
<style> |
|||
h4 { |
|||
margin-top: 20px; |
|||
} |
|||
</style> |
|||
@ -1,45 +0,0 @@ |
|||
<script> |
|||
import { createEventDispatcher } from "svelte" |
|||
import Select from "../common/Select.svelte" |
|||
|
|||
export let selected |
|||
export let label |
|||
export let options |
|||
export let valueMember |
|||
export let textMember |
|||
export let multiple = false |
|||
export let width = "medium" |
|||
export let size = "small" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
</script> |
|||
|
|||
<div class="uk-margin"> |
|||
<label class="uk-form-label">{label}</label> |
|||
<div class="uk-form-controls"> |
|||
{#if multiple} |
|||
<Select |
|||
class="uk-select uk-form-width-{width} uk-form-{size}" |
|||
multiple |
|||
bind:value={selected} |
|||
on:change> |
|||
{#each options as option} |
|||
<option value={!valueMember ? option : valueMember(option)}> |
|||
{!textMember ? option : textMember(option)} |
|||
</option> |
|||
{/each} |
|||
</Select> |
|||
{:else} |
|||
<Select |
|||
class="uk-select uk-form-width-{width} uk-form-{size}" |
|||
bind:value={selected} |
|||
on:change> |
|||
{#each options as option} |
|||
<option value={!valueMember ? option : valueMember(option)}> |
|||
{!textMember ? option : textMember(option)} |
|||
</option> |
|||
{/each} |
|||
</Select> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
@ -1,62 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { buildStyle } from "../../helpers.js" |
|||
export let value = "" |
|||
export let name = "" |
|||
export let textAlign = "left" |
|||
export let width = "160px" |
|||
export let placeholder = "" |
|||
export let suffix = "" |
|||
export let onChange = val => {} |
|||
|
|||
let centerPlaceholder = textAlign === "center" |
|||
|
|||
let style = buildStyle({ width, textAlign }) |
|||
|
|||
function handleChange(val) { |
|||
value = val |
|||
let _value = value !== "auto" ? value + suffix : value |
|||
onChange(_value) |
|||
} |
|||
|
|||
$: displayValue = |
|||
suffix && value && value.endsWith(suffix) |
|||
? value.replace(new RegExp(`${suffix}$`), "") |
|||
: value || "" |
|||
</script> |
|||
|
|||
<input |
|||
{name} |
|||
class:centerPlaceholder |
|||
type="text" |
|||
value={displayValue} |
|||
{placeholder} |
|||
{style} |
|||
on:change={e => handleChange(e.target.value)} /> |
|||
|
|||
<style> |
|||
input { |
|||
/* width: 32px; */ |
|||
height: 36px; |
|||
font-size: 14px; |
|||
font-weight: 400; |
|||
margin: 0px 0px 0px 2px; |
|||
color: var(--ink); |
|||
padding: 0px 8px; |
|||
font-family: inter; |
|||
width: 164px; |
|||
box-sizing: border-box; |
|||
background-color: var(--grey-2); |
|||
border-radius: 4px; |
|||
border: 1px solid var(--grey-2); |
|||
outline: none; |
|||
} |
|||
|
|||
input::placeholder { |
|||
text-align: left; |
|||
} |
|||
|
|||
.centerPlaceholder::placeholder { |
|||
text-align: center; |
|||
} |
|||
</style> |
|||
@ -1,50 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import Input from "../Input.svelte" |
|||
|
|||
export let meta = [] |
|||
export let label = "" |
|||
export let value = ["0", "0", "0", "0"] |
|||
export let suffix = "" |
|||
|
|||
export let onChange = () => {} |
|||
|
|||
function handleChange(val, idx) { |
|||
value.splice(idx, 1, val !== "auto" && suffix ? val + suffix : val) |
|||
|
|||
value = value |
|||
let _value = value.map(v => |
|||
suffix && !v.endsWith(suffix) && v !== "auto" ? v + suffix : v |
|||
) |
|||
onChange(_value) |
|||
} |
|||
|
|||
$: displayValues = |
|||
value && suffix |
|||
? value.map(v => v.replace(new RegExp(`${suffix}$`), "")) |
|||
: value || [] |
|||
</script> |
|||
|
|||
<div class="input-container"> |
|||
<div class="label">{label}</div> |
|||
<div class="inputs-group"> |
|||
{#each meta as m, i} |
|||
<Input |
|||
width="37px" |
|||
textAlign="center" |
|||
placeholder={m.placeholder || ''} |
|||
value={!displayValues || displayValues[i] === '0' ? '' : displayValues[i]} |
|||
onChange={value => handleChange(value || 0, i)} /> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.label { |
|||
flex: 0; |
|||
} |
|||
|
|||
.inputs-group { |
|||
flex: 1; |
|||
} |
|||
</style> |
|||
@ -1,23 +0,0 @@ |
|||
<button on:click> |
|||
<slot>+</slot> |
|||
</button> |
|||
|
|||
<style> |
|||
button { |
|||
cursor: pointer; |
|||
outline: none; |
|||
border: none; |
|||
border-radius: 5px; |
|||
min-width: 1.8rem; |
|||
min-height: 1.8rem; |
|||
padding-bottom: 10px; |
|||
|
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
|
|||
font-size: 1.2rem; |
|||
font-weight: 600; |
|||
color: var(--ink); |
|||
} |
|||
</style> |
|||
@ -1,65 +0,0 @@ |
|||
<script> |
|||
import getIcon from "./icon" |
|||
|
|||
export let icon |
|||
export let value |
|||
</script> |
|||
|
|||
<div class="select-container"> |
|||
{#if icon} |
|||
<i class={icon} /> |
|||
{/if} |
|||
<select class:adjusted={icon} on:change bind:value> |
|||
<slot /> |
|||
</select> |
|||
<span class="arrow"> |
|||
{@html getIcon('chevron-down', '24')} |
|||
</span> |
|||
</div> |
|||
|
|||
<style> |
|||
.select-container { |
|||
font-size: 14px; |
|||
position: relative; |
|||
border: var(--grey-4) 1px solid; |
|||
} |
|||
|
|||
.adjusted { |
|||
padding-left: 30px; |
|||
} |
|||
|
|||
i { |
|||
position: absolute; |
|||
left: 10px; |
|||
top: 10px; |
|||
} |
|||
|
|||
select { |
|||
height: 40px; |
|||
display: block; |
|||
font-family: sans-serif; |
|||
font-weight: 400; |
|||
font-size: 14px; |
|||
color: var(--ink); |
|||
padding: 0 40px 0px 20px; |
|||
width: 100%; |
|||
max-width: 100%; |
|||
box-sizing: border-box; |
|||
margin: 0; |
|||
-moz-appearance: none; |
|||
-webkit-appearance: none; |
|||
appearance: none; |
|||
background: var(--white); |
|||
} |
|||
|
|||
.arrow { |
|||
position: absolute; |
|||
right: 10px; |
|||
bottom: 0; |
|||
margin: auto; |
|||
width: 30px; |
|||
height: 30px; |
|||
pointer-events: none; |
|||
color: var(--ink); |
|||
} |
|||
</style> |
|||
@ -1,33 +0,0 @@ |
|||
<script> |
|||
export let text = "" |
|||
export let label = "" |
|||
export let width = "medium" |
|||
export let size = "small" |
|||
export let margin = true |
|||
export let infoText = "" |
|||
export let hasError = false |
|||
export let disabled = false |
|||
</script> |
|||
|
|||
<div class:uk-margin={margin}> |
|||
<label class="uk-form-label">{label}</label> |
|||
<div class="uk-form-controls"> |
|||
<input |
|||
data-cy={label} |
|||
class="budibase__input" |
|||
class:uk-form-danger={hasError} |
|||
on:change |
|||
bind:value={text} |
|||
{disabled} /> |
|||
</div> |
|||
{#if infoText} |
|||
<div class="info-text">{infoText}</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.info-text { |
|||
font-size: 0.7rem; |
|||
color: var(--secondary50); |
|||
} |
|||
</style> |
|||
@ -1,16 +0,0 @@ |
|||
<script> |
|||
import RecordCard from "./RecordCard.svelte" |
|||
|
|||
export let record = {} |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<div class="title">{record.name}</div> |
|||
<div class="inner"> |
|||
<div class="node-path">{record.nodeKey()}</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
</style> |
|||
@ -0,0 +1,144 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import fsort from "fast-sort" |
|||
import getOr from "lodash/fp/getOr" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import { Button, Icon } from "@budibase/bbui" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import LinkedRecord from "./LinkedRecord.svelte" |
|||
import TablePagination from "./TablePagination.svelte" |
|||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals" |
|||
import RowPopover from "./popovers/Row.svelte" |
|||
import ColumnPopover from "./popovers/Column.svelte" |
|||
import ViewPopover from "./popovers/View.svelte" |
|||
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte" |
|||
import EditRowPopover from "./popovers/EditRow.svelte" |
|||
import CalculationPopover from "./popovers/Calculate.svelte" |
|||
|
|||
export let columns = [] |
|||
export let data = [] |
|||
export let title |
|||
|
|||
const ITEMS_PER_PAGE = 10 |
|||
|
|||
let currentPage = 0 |
|||
|
|||
$: paginatedData = |
|||
data && data.length |
|||
? data.slice( |
|||
currentPage * ITEMS_PER_PAGE, |
|||
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE |
|||
) |
|||
: [] |
|||
$: sort = $backendUiStore.sort |
|||
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data |
|||
</script> |
|||
|
|||
<section> |
|||
<div class="table-controls"> |
|||
<h2 class="title">{title}</h2> |
|||
<div class="popovers"> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
<table class="uk-table"> |
|||
<thead> |
|||
<tr> |
|||
{#each columns as header} |
|||
<th>{header.name}</th> |
|||
{/each} |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
{#if paginatedData.length === 0} |
|||
<div class="no-data">No Data.</div> |
|||
{/if} |
|||
{#each paginatedData as row} |
|||
<tr> |
|||
{#each columns as header} |
|||
<td>{getOr(row.default || '', header.key, row)}</td> |
|||
{/each} |
|||
</tr> |
|||
{/each} |
|||
</tbody> |
|||
</table> |
|||
<TablePagination |
|||
{data} |
|||
bind:currentPage |
|||
pageItemCount={data.length} |
|||
{ITEMS_PER_PAGE} /> |
|||
</section> |
|||
|
|||
<style> |
|||
section { |
|||
margin-bottom: 20px; |
|||
} |
|||
.title { |
|||
font-size: 24px; |
|||
font-weight: 600; |
|||
text-rendering: optimizeLegibility; |
|||
text-transform: capitalize; |
|||
} |
|||
|
|||
table { |
|||
border: 1px solid var(--grey-4); |
|||
background: #fff; |
|||
border-radius: 3px; |
|||
border-collapse: collapse; |
|||
} |
|||
|
|||
thead { |
|||
height: 40px; |
|||
background: var(--grey-3); |
|||
border: 1px solid var(--grey-4); |
|||
} |
|||
|
|||
thead th { |
|||
color: var(--ink); |
|||
text-transform: capitalize; |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
text-rendering: optimizeLegibility; |
|||
transition: 0.5s all; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
th:hover { |
|||
color: var(--blue); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
td { |
|||
max-width: 200px; |
|||
text-overflow: ellipsis; |
|||
border: 1px solid var(--grey-4); |
|||
} |
|||
|
|||
tbody tr { |
|||
border-bottom: 1px solid var(--grey-4); |
|||
transition: 0.3s background-color; |
|||
color: var(--ink); |
|||
font-size: 12px; |
|||
} |
|||
|
|||
tbody tr:hover { |
|||
background: var(--grey-1); |
|||
} |
|||
|
|||
.table-controls { |
|||
width: 100%; |
|||
} |
|||
|
|||
.popovers { |
|||
display: flex; |
|||
} |
|||
|
|||
:global(.popovers > div) { |
|||
margin-right: var(--spacing-m); |
|||
} |
|||
|
|||
.no-data { |
|||
padding: 14px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,74 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import fsort from "fast-sort" |
|||
import getOr from "lodash/fp/getOr" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import { Button, Icon } from "@budibase/bbui" |
|||
import Table from "./Table.svelte" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import LinkedRecord from "./LinkedRecord.svelte" |
|||
import TablePagination from "./TablePagination.svelte" |
|||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals" |
|||
import RowPopover from "./popovers/Row.svelte" |
|||
import ColumnPopover from "./popovers/Column.svelte" |
|||
import ViewPopover from "./popovers/View.svelte" |
|||
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte" |
|||
import EditRowPopover from "./popovers/EditRow.svelte" |
|||
import CalculationPopover from "./popovers/Calculate.svelte" |
|||
import GroupByPopover from "./popovers/GroupBy.svelte" |
|||
|
|||
let COLUMNS = [ |
|||
{ |
|||
name: "group", |
|||
key: "key", |
|||
default: "All Records", |
|||
}, |
|||
{ |
|||
name: "sum", |
|||
key: "value.sum", |
|||
}, |
|||
{ |
|||
name: "min", |
|||
key: "value.min", |
|||
}, |
|||
{ |
|||
name: "max", |
|||
key: "value.max", |
|||
}, |
|||
{ |
|||
name: "sumsqr", |
|||
key: "value.sumsqr", |
|||
}, |
|||
{ |
|||
name: "count", |
|||
key: "value.count", |
|||
}, |
|||
{ |
|||
name: "avg", |
|||
key: "value.avg", |
|||
}, |
|||
] |
|||
|
|||
export let view = {} |
|||
|
|||
let data = [] |
|||
|
|||
$: ({ name, groupBy } = view) |
|||
$: !name.startsWith("all_") && fetchViewData(name, groupBy) |
|||
|
|||
async function fetchViewData(name, groupBy) { |
|||
let QUERY_VIEW_URL = `/api/views/${name}?stats=true` |
|||
if (groupBy) { |
|||
QUERY_VIEW_URL += `&group=${groupBy}` |
|||
} |
|||
|
|||
const response = await api.get(QUERY_VIEW_URL) |
|||
data = await response.json() |
|||
} |
|||
</script> |
|||
|
|||
<Table title={decodeURI(view.name)} columns={COLUMNS} {data}> |
|||
<CalculationPopover {view} /> |
|||
<GroupByPopover {view} /> |
|||
</Table> |
|||
@ -0,0 +1,62 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import * as api from "../api" |
|||
|
|||
export let viewName |
|||
export let onClosed |
|||
</script> |
|||
|
|||
<section> |
|||
<div class="content"> |
|||
<header> |
|||
<i class="ri-information-line alert" /> |
|||
<h4 class="budibase__title--4">Delete View</h4> |
|||
</header> |
|||
<p> |
|||
Are you sure you want to delete this view? All of your data will be |
|||
permanently removed. This action cannot be undone. |
|||
</p> |
|||
</div> |
|||
<div class="modal-actions"> |
|||
<ActionButton on:click={onClosed}>Cancel</ActionButton> |
|||
<ActionButton |
|||
alert |
|||
on:click={async () => { |
|||
await backendUiStore.actions.views.delete(viewName) |
|||
notifier.danger(`View ${viewName} deleted.`) |
|||
$goto(`./backend`) |
|||
onClosed() |
|||
}}> |
|||
Delete |
|||
</ActionButton> |
|||
</div> |
|||
</section> |
|||
|
|||
<style> |
|||
.alert { |
|||
color: rgba(255, 0, 31, 1); |
|||
background: var(--grey-1); |
|||
padding: 5px; |
|||
} |
|||
|
|||
.modal-actions { |
|||
padding: 10px; |
|||
background: var(--grey-1); |
|||
border-top: 1px solid #ccc; |
|||
} |
|||
|
|||
header { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.content { |
|||
padding: 30px; |
|||
} |
|||
|
|||
h4 { |
|||
margin: 0 0 0 10px; |
|||
} |
|||
</style> |
|||
@ -1,4 +1,2 @@ |
|||
export { default as DeleteRecordModal } from "./DeleteRecord.svelte" |
|||
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte" |
|||
export { default as CreateEditViewModal } from "./CreateEditView.svelte" |
|||
export { default as CreateUserModal } from "./CreateUser.svelte" |
|||
@ -0,0 +1,95 @@ |
|||
<script> |
|||
import { |
|||
Popover, |
|||
TextButton, |
|||
Button, |
|||
Icon, |
|||
Input, |
|||
Select, |
|||
} from "@budibase/bbui" |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
|||
|
|||
const CALCULATIONS = [ |
|||
{ |
|||
name: "Statistics", |
|||
key: "stats", |
|||
}, |
|||
] |
|||
|
|||
export let view = {} |
|||
|
|||
let anchor |
|||
let dropdown |
|||
|
|||
$: viewModel = $backendUiStore.models.find( |
|||
({ _id }) => _id === $backendUiStore.selectedView.modelId |
|||
) |
|||
$: fields = |
|||
viewModel && |
|||
Object.keys(viewModel.schema).filter( |
|||
field => viewModel.schema[field].type === "number" |
|||
) |
|||
|
|||
function saveView() { |
|||
backendUiStore.actions.views.save(view) |
|||
notifier.success(`View ${view.name} saved.`) |
|||
dropdown.hide() |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<TextButton text small on:click={dropdown.show} active={!!view.field}> |
|||
<Icon name="calculate" /> |
|||
Calculate |
|||
</TextButton> |
|||
</div> |
|||
<Popover bind:this={dropdown} {anchor} align="left"> |
|||
<h5>Calculate</h5> |
|||
<div class="input-group-row"> |
|||
<p>The</p> |
|||
<Select secondary thin bind:value={view.calculation}> |
|||
{#each CALCULATIONS as calculation} |
|||
<option value={calculation.key}>{calculation.name}</option> |
|||
{/each} |
|||
</Select> |
|||
<p>of</p> |
|||
<Select secondary thin bind:value={view.field}> |
|||
{#each fields as field} |
|||
<option value={field}>{field}</option> |
|||
{/each} |
|||
</Select> |
|||
</div> |
|||
<div class="button-group"> |
|||
<Button secondary on:click={dropdown.hide}>Cancel</Button> |
|||
<Button primary on:click={saveView}>Save</Button> |
|||
</div> |
|||
</Popover> |
|||
|
|||
<style> |
|||
h5 { |
|||
margin-bottom: var(--spacing-l); |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.button-group { |
|||
margin-top: var(--spacing-l); |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: var(--spacing-s); |
|||
} |
|||
|
|||
.input-group-row { |
|||
display: grid; |
|||
grid-template-columns: 50px 1fr 20px 1fr; |
|||
gap: var(--spacing-s); |
|||
margin-bottom: var(--spacing-l); |
|||
align-items: center; |
|||
} |
|||
|
|||
p { |
|||
margin: 0; |
|||
font-size: var(--font-size-xs); |
|||
} |
|||
</style> |
|||
@ -1,9 +1,13 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" |
|||
import { |
|||
DropdownMenu, |
|||
TextButton as Button, |
|||
Icon, |
|||
Input, |
|||
Select, |
|||
} from "@budibase/bbui" |
|||
import { FIELDS } from "constants/backend" |
|||
import { ModelSetupNav } from "components/nav/ModelSetupNav" |
|||
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte" |
|||
import CreateEditColumn from "../modals/CreateEditColumn.svelte" |
|||
|
|||
let anchor |
|||
@ -0,0 +1,86 @@ |
|||
<script> |
|||
import { |
|||
Popover, |
|||
TextButton, |
|||
Button, |
|||
Icon, |
|||
Input, |
|||
Select, |
|||
} from "@budibase/bbui" |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
|||
|
|||
const CALCULATIONS = [ |
|||
{ |
|||
name: "Statistics", |
|||
key: "stats", |
|||
}, |
|||
] |
|||
|
|||
export let view = {} |
|||
|
|||
let anchor |
|||
let dropdown |
|||
|
|||
$: viewModel = $backendUiStore.models.find( |
|||
({ _id }) => _id === $backendUiStore.selectedView.modelId |
|||
) |
|||
$: fields = viewModel && Object.keys(viewModel.schema) |
|||
|
|||
function saveView() { |
|||
backendUiStore.actions.views.save(view) |
|||
notifier.success(`View ${view.name} saved.`) |
|||
dropdown.hide() |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<TextButton text small active={!!view.groupBy} on:click={dropdown.show}> |
|||
<Icon name="group" /> |
|||
Group By |
|||
</TextButton> |
|||
</div> |
|||
<Popover bind:this={dropdown} {anchor} align="left"> |
|||
<h5>Group By</h5> |
|||
<div class="input-group-row"> |
|||
<p>Group By</p> |
|||
<Select secondary thin bind:value={view.groupBy}> |
|||
<option value={false} /> |
|||
{#each fields as field} |
|||
<option value={field}>{field}</option> |
|||
{/each} |
|||
</Select> |
|||
</div> |
|||
<div class="button-group"> |
|||
<Button secondary on:click={dropdown.hide}>Cancel</Button> |
|||
<Button primary on:click={saveView}>Save</Button> |
|||
</div> |
|||
</Popover> |
|||
|
|||
<style> |
|||
h5 { |
|||
margin-bottom: var(--spacing-l); |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.button-group { |
|||
margin-top: var(--spacing-l); |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: var(--spacing-s); |
|||
} |
|||
|
|||
.input-group-row { |
|||
display: grid; |
|||
grid-template-columns: 50px 1fr 20px 1fr; |
|||
gap: var(--spacing-s); |
|||
margin-bottom: var(--spacing-l); |
|||
align-items: center; |
|||
} |
|||
|
|||
p { |
|||
margin: 0; |
|||
font-size: var(--font-size-xs); |
|||
} |
|||
</style> |
|||
@ -1,5 +1,5 @@ |
|||
<script> |
|||
import { DropdownMenu, Button, Icon } from "@budibase/bbui" |
|||
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" |
|||
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
|||
|
|||
let anchor |
|||
@ -0,0 +1,84 @@ |
|||
<script> |
|||
import { |
|||
Popover, |
|||
TextButton, |
|||
Button, |
|||
Icon, |
|||
Input, |
|||
Select, |
|||
} from "@budibase/bbui" |
|||
import { goto } from "@sveltech/routify" |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
|||
|
|||
let anchor |
|||
let dropdown |
|||
|
|||
let name |
|||
let field |
|||
|
|||
$: fields = Object.keys($backendUiStore.selectedModel.schema).filter(key => { |
|||
return $backendUiStore.selectedModel.schema[key].type === "number" |
|||
}) |
|||
$: views = $backendUiStore.models.flatMap(model => |
|||
Object.keys(model.views || {}) |
|||
) |
|||
|
|||
function saveView() { |
|||
if (views.includes(name)) { |
|||
notifier.danger(`View exists with name ${name}.`) |
|||
return |
|||
} |
|||
backendUiStore.actions.views.save({ |
|||
name, |
|||
modelId: $backendUiStore.selectedModel._id, |
|||
field, |
|||
}) |
|||
notifier.success(`View ${name} created`) |
|||
dropdown.hide() |
|||
$goto(`../../../view/${name}`) |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<TextButton text small on:click={dropdown.show}> |
|||
<Icon name="view" /> |
|||
Create New View |
|||
</TextButton> |
|||
</div> |
|||
<Popover bind:this={dropdown} {anchor} align="left"> |
|||
<h5>Create View</h5> |
|||
<div class="input-group-column"> |
|||
<Input placeholder="View Name" thin bind:value={name} /> |
|||
<Select thin secondary bind:value={field}> |
|||
{#each fields as field} |
|||
<option value={field}>{field}</option> |
|||
{/each} |
|||
</Select> |
|||
</div> |
|||
<div class="button-group"> |
|||
<Button secondary on:click={dropdown.hide}>Cancel</Button> |
|||
<Button primary on:click={saveView}>Save View</Button> |
|||
</div> |
|||
</Popover> |
|||
|
|||
<style> |
|||
h5 { |
|||
margin-bottom: var(--spacing-l); |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.button-group { |
|||
margin-top: var(--spacing-l); |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: var(--spacing-s); |
|||
} |
|||
|
|||
.input-group-column { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: var(--spacing-s); |
|||
} |
|||
</style> |
|||
@ -1,127 +0,0 @@ |
|||
<script> |
|||
import Textbox from "components/common/Textbox.svelte" |
|||
import CodeArea from "components/common/CodeArea.svelte" |
|||
import Button from "components/common/Button.svelte" |
|||
import Dropdown from "components/common/Dropdown.svelte" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import { filter, some, map, compose } from "lodash/fp" |
|||
import ErrorsBox from "components/common/ErrorsBox.svelte" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import api from "builderStore/api" |
|||
|
|||
const SNIPPET_EDITORS = { |
|||
MAP: "Map", |
|||
FILTER: "Filter", |
|||
REDUCE: "Reduce", |
|||
} |
|||
|
|||
const COUCHDB_FUNCTION = `function(doc) { |
|||
|
|||
}` |
|||
|
|||
export let onClosed |
|||
export let view = {} |
|||
|
|||
let currentSnippetEditor = SNIPPET_EDITORS.MAP |
|||
|
|||
$: instanceId = $backendUiStore.selectedDatabase._id |
|||
|
|||
function deleteView() {} |
|||
|
|||
async function saveView() { |
|||
const SAVE_VIEW_URL = `/api/views` |
|||
const response = await api.post(SAVE_VIEW_URL, view) |
|||
backendUiStore.update(state => { |
|||
state.views = [...state.views, response.view] |
|||
return state |
|||
}) |
|||
onClosed() |
|||
} |
|||
</script> |
|||
|
|||
<div class="header"> |
|||
<i class="ri-eye-line button--toggled" /> |
|||
<h3 class="budibase__title--3">Create / Edit View</h3> |
|||
</div> |
|||
<form on:submit|preventDefault class="uk-form-stacked root"> |
|||
{#if $store.errors && $store.errors.length > 0} |
|||
<ErrorsBox errors={$store.errors} /> |
|||
{/if} |
|||
<div class="main"> |
|||
<div class="uk-grid-small" uk-grid> |
|||
<div class="uk-width-1-2@s"> |
|||
<Textbox bind:text={view.name} label="Name" /> |
|||
</div> |
|||
</div> |
|||
<div class="code-snippets"> |
|||
{#each Object.values(SNIPPET_EDITORS) as snippetType} |
|||
<span |
|||
class="snippet-selector__heading hoverable" |
|||
class:highlighted={currentSnippetEditor === snippetType} |
|||
on:click={() => (currentSnippetEditor = snippetType)}> |
|||
{snippetType} |
|||
</span> |
|||
{/each} |
|||
{#if currentSnippetEditor === SNIPPET_EDITORS.MAP} |
|||
<CodeArea bind:text={view.map} label="Map" /> |
|||
{:else if currentSnippetEditor === SNIPPET_EDITORS.FILTER} |
|||
<CodeArea bind:text={view.filter} label="Filter" /> |
|||
{:else if currentSnippetEditor === SNIPPET_EDITORS.REDUCE} |
|||
<CodeArea bind:text={view.reduce} label="Reduce" /> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
<div class="buttons"> |
|||
<div class="button"> |
|||
<ActionButton secondary on:click={deleteView}>Delete</ActionButton> |
|||
</div> |
|||
<ActionButton color="secondary" on:click={saveView}>Save</ActionButton> |
|||
</div> |
|||
</form> |
|||
|
|||
<style> |
|||
.root { |
|||
height: 100%; |
|||
} |
|||
|
|||
.highlighted { |
|||
opacity: 1; |
|||
} |
|||
|
|||
h3 { |
|||
margin: 0 0 0 10px; |
|||
color: var(--ink); |
|||
} |
|||
|
|||
.snippet-selector__heading { |
|||
margin-right: 20px; |
|||
font-size: 14px; |
|||
color: var(--grey-5); |
|||
} |
|||
|
|||
.header { |
|||
padding: 20px 40px 0 40px; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.main { |
|||
margin: 20px 40px 0px 40px; |
|||
} |
|||
|
|||
.code-snippets { |
|||
margin: 20px 0px 20px 0px; |
|||
} |
|||
|
|||
.buttons { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
background-color: var(--grey-1); |
|||
margin: 0 40px; |
|||
padding: 20px 0; |
|||
} |
|||
|
|||
.button { |
|||
margin-right: 20px; |
|||
} |
|||
</style> |
|||
@ -1,94 +0,0 @@ |
|||
<script> |
|||
import { store, backendUiStore } from "builderStore" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import * as api from "../api" |
|||
|
|||
export let onClosed |
|||
|
|||
let username |
|||
let password |
|||
let accessLevelId |
|||
|
|||
$: valid = username && password && accessLevelId |
|||
$: appId = $store.appId |
|||
|
|||
async function createUser() { |
|||
const user = { name: username, username, password, accessLevelId } |
|||
const response = await api.createUser(user) |
|||
backendUiStore.actions.users.create(response) |
|||
onClosed() |
|||
} |
|||
</script> |
|||
|
|||
<form on:submit|preventDefault class="uk-form-stacked"> |
|||
<div class="main"> |
|||
<div class="heading"> |
|||
<i class="ri-list-settings-line button--toggled" /> |
|||
<div class="title">Create User</div> |
|||
</div> |
|||
<div class="uk-margin"> |
|||
<label class="uk-form-label" for="form-stacked-text">Username</label> |
|||
<input |
|||
data-cy="username" |
|||
class="uk-input" |
|||
type="text" |
|||
bind:value={username} /> |
|||
</div> |
|||
<div class="uk-margin"> |
|||
<label class="uk-form-label" for="form-stacked-text">Password</label> |
|||
<input |
|||
data-cy="password" |
|||
class="uk-input" |
|||
type="password" |
|||
bind:value={password} /> |
|||
</div> |
|||
<div class="uk-margin"> |
|||
<label class="uk-form-label" for="form-stacked-text">Access Level</label> |
|||
<select |
|||
data-cy="accessLevel" |
|||
class="uk-select" |
|||
bind:value={accessLevelId}> |
|||
<option value="" /> |
|||
<option value="POWER_USER">Power User</option> |
|||
<option value="ADMIN">Admin</option> |
|||
</select> |
|||
</div> |
|||
</div> |
|||
<footer> |
|||
<div class="button"> |
|||
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton> |
|||
</div> |
|||
<ActionButton disabled={!valid} on:click={createUser}>Save</ActionButton> |
|||
</footer> |
|||
</form> |
|||
|
|||
<style> |
|||
.main { |
|||
padding: 40px 40px 20px 40px; |
|||
} |
|||
|
|||
.title { |
|||
font-size: 24px; |
|||
font-weight: 600; |
|||
color: var(--ink); |
|||
margin-left: 12px; |
|||
} |
|||
|
|||
.heading { |
|||
display: flex; |
|||
align-items: baseline; |
|||
} |
|||
|
|||
footer { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: flex-end; |
|||
padding: 20px; |
|||
background: var(--grey-1); |
|||
border-radius: 0 0 5px 5px; |
|||
} |
|||
|
|||
.button { |
|||
margin-right: 20px; |
|||
} |
|||
</style> |
|||
@ -1,95 +0,0 @@ |
|||
<script> |
|||
import { tick, onMount } from "svelte" |
|||
import { goto } from "@sveltech/routify" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import { CheckIcon } from "../common/Icons" |
|||
|
|||
$: instances = $store.appInstances |
|||
|
|||
async function selectDatabase(database) { |
|||
backendUiStore.actions.database.select(database) |
|||
} |
|||
|
|||
async function deleteDatabase(database) { |
|||
const DELETE_DATABASE_URL = `/api/instances/${database.name}` |
|||
const response = await api.delete(DELETE_DATABASE_URL) |
|||
store.update(state => { |
|||
state.appInstances = state.appInstances.filter( |
|||
db => db._id !== database._id |
|||
) |
|||
return state |
|||
}) |
|||
} |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<ul> |
|||
{#each $store.appInstances as database} |
|||
<li> |
|||
<span class="icon"> |
|||
{#if database._id === $backendUiStore.selectedDatabase._id} |
|||
<CheckIcon /> |
|||
{/if} |
|||
</span> |
|||
<button |
|||
class:active={database._id === $backendUiStore.selectedDatabase._id} |
|||
on:click={() => { |
|||
$goto(`./database/${database._id}`), selectDatabase(database) |
|||
}}> |
|||
{database.name} |
|||
</button> |
|||
<i |
|||
class="ri-delete-bin-7-line hoverable alignment" |
|||
on:click={() => deleteDatabase(database)} /> |
|||
</li> |
|||
{/each} |
|||
</ul> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
font-size: 13px; |
|||
color: var(--ink); |
|||
position: relative; |
|||
padding-left: 20px; |
|||
} |
|||
|
|||
ul { |
|||
margin: 0; |
|||
padding: 0; |
|||
list-style: none; |
|||
} |
|||
|
|||
.alignment { |
|||
margin-left: auto; |
|||
padding-right: 20px; |
|||
} |
|||
|
|||
li { |
|||
margin: 0px 0px 10px 0px; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
button { |
|||
margin: 0 0 0 6px; |
|||
padding: 0; |
|||
border: none; |
|||
font-size: 13px; |
|||
outline: none; |
|||
cursor: pointer; |
|||
background: rgba(0, 0, 0, 0); |
|||
text-rendering: optimizeLegibility; |
|||
} |
|||
|
|||
.active { |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.icon { |
|||
display: inline-block; |
|||
width: 14px; |
|||
color: #333; |
|||
} |
|||
</style> |
|||
@ -1,45 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import { cloneDeep } from "lodash/fp" |
|||
import getIcon from "../common/icon" |
|||
import { CreateEditViewModal } from "components/database/ModelDataTable/modals" |
|||
import api from "builderStore/api" |
|||
|
|||
const { open, close } = getContext("simple-modal") |
|||
|
|||
export let node |
|||
export let type |
|||
export let onSelect |
|||
|
|||
let navActive = "" |
|||
|
|||
const ICON_MAP = { |
|||
index: "ri-eye-line", |
|||
model: "ri-list-settings-line", |
|||
} |
|||
</script> |
|||
|
|||
<div> |
|||
<div |
|||
on:click={() => onSelect(node)} |
|||
class="budibase__nav-item hierarchy-item" |
|||
class:capitalized={type === 'model'} |
|||
class:selected={$backendUiStore.selectedView === `all_${node._id}`}> |
|||
<i class={ICON_MAP[type]} /> |
|||
<span style="margin-left: 1rem">{node.name}</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.hierarchy-item { |
|||
font-size: 13px; |
|||
font-weight: 400; |
|||
margin-bottom: 10px; |
|||
padding-left: 20px; |
|||
} |
|||
|
|||
.capitalized { |
|||
text-transform: capitalize; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,143 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" |
|||
import { FIELDS } from "constants/backend" |
|||
import DeleteViewModal from "components/database/DataTable/modals/DeleteView.svelte" |
|||
|
|||
const { open, close } = getContext("simple-modal") |
|||
|
|||
export let view |
|||
|
|||
let anchor |
|||
let dropdown |
|||
|
|||
let editing |
|||
let originalName = view.name |
|||
|
|||
function showEditor() { |
|||
editing = true |
|||
} |
|||
|
|||
function hideEditor() { |
|||
dropdown.hide() |
|||
editing = false |
|||
close() |
|||
} |
|||
|
|||
const deleteView = () => { |
|||
open( |
|||
DeleteViewModal, |
|||
{ |
|||
onClosed: close, |
|||
viewName: view.name, |
|||
}, |
|||
{ styleContent: { padding: "0" } } |
|||
) |
|||
} |
|||
|
|||
function save() { |
|||
backendUiStore.actions.views.save({ |
|||
originalName, |
|||
...view, |
|||
}) |
|||
notifier.success("Renamed View Successfully.") |
|||
hideEditor() |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor} on:click={dropdown.show}> |
|||
<i class="ri-more-line" /> |
|||
</div> |
|||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|||
{#if editing} |
|||
<h5>Edit View</h5> |
|||
<div class="container"> |
|||
<Input placeholder="View Name" thin bind:value={view.name} /> |
|||
</div> |
|||
<footer> |
|||
<div class="button-margin-3"> |
|||
<Button secondary on:click={hideEditor}>Cancel</Button> |
|||
</div> |
|||
<div class="button-margin-4"> |
|||
<Button primary on:click={save}>Save</Button> |
|||
</div> |
|||
</footer> |
|||
{:else} |
|||
<ul> |
|||
<li on:click={showEditor}> |
|||
<Icon name="edit" /> |
|||
Edit |
|||
</li> |
|||
<li data-cy="delete-view" on:click={deleteView}> |
|||
<Icon name="delete" /> |
|||
Delete |
|||
</li> |
|||
</ul> |
|||
{/if} |
|||
</DropdownMenu> |
|||
|
|||
<style> |
|||
h5 { |
|||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|||
margin: 0; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.container { |
|||
padding: var(--spacing-xl); |
|||
} |
|||
|
|||
ul { |
|||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|||
list-style: none; |
|||
padding-left: 0; |
|||
margin: 0; |
|||
padding: var(--spacing-s) 0; |
|||
} |
|||
|
|||
li { |
|||
display: flex; |
|||
font-family: var(--font-sans); |
|||
font-size: var(--font-size-xs); |
|||
color: var(--ink); |
|||
padding: var(--spacing-s) var(--spacing-m); |
|||
margin: auto 0px; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
li:hover { |
|||
background-color: var(--grey-2); |
|||
} |
|||
|
|||
li:active { |
|||
color: var(--blue); |
|||
} |
|||
|
|||
footer { |
|||
padding: 20px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr 1fr; |
|||
gap: 20px; |
|||
background: var(--grey-1); |
|||
border-bottom-left-radius: 0.5rem; |
|||
border-bottom-left-radius: 0.5rem; |
|||
} |
|||
|
|||
.button-margin-1 { |
|||
grid-column-start: 1; |
|||
display: grid; |
|||
} |
|||
|
|||
.button-margin-3 { |
|||
grid-column-start: 3; |
|||
display: grid; |
|||
} |
|||
|
|||
.button-margin-4 { |
|||
grid-column-start: 4; |
|||
display: grid; |
|||
} |
|||
</style> |
|||
@ -1,120 +0,0 @@ |
|||
<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 && |
|||
!field.constraints.presence.allowEmpty |
|||
</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" |
|||
bind:checked={required} |
|||
on:change={() => (field.constraints.presence.allowEmpty = required)} /> |
|||
</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}> |
|||
<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> |
|||
@ -1,156 +0,0 @@ |
|||
<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 |
|||
}) |
|||
} |
|||
|
|||
function validate() { |
|||
let errors = [] |
|||
for (let field of Object.values($backendUiStore.draftModel.schema)) { |
|||
const restrictedFieldNames = ["type", "modelId"] |
|||
if (field.name.startsWith("_")) { |
|||
errors.push(`field '${field.name}' - name cannot begin with '_''`) |
|||
} else if (restrictedFieldNames.includes(field.name)) { |
|||
errors.push( |
|||
`field '${field.name}' - is a restricted name, please rename` |
|||
) |
|||
} else if (!field.name || !field.name.trim()) { |
|||
errors.push("field name cannot be blank") |
|||
} |
|||
} |
|||
|
|||
if (!$backendUiStore.draftModel.name) { |
|||
errors.push("Table name cannot be blank") |
|||
} |
|||
|
|||
return errors |
|||
} |
|||
|
|||
async function saveModel() { |
|||
const errors = validate() |
|||
if (errors.length > 0) { |
|||
notifier.danger(errors.join("/n")) |
|||
return |
|||
} |
|||
|
|||
await backendUiStore.actions.models.save($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 |
|||
data-cy="table-name-input" |
|||
type="text" |
|||
class="budibase__input" |
|||
bind:value={$backendUiStore.draftModel.name} /> |
|||
</div> |
|||
<!-- dont have this capability yet.. |
|||
<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: 260px; |
|||
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> |
|||
@ -1 +0,0 @@ |
|||
export { default as ModelSetupNav } from "./ModelSetupNav.svelte" |
|||
@ -1,82 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import getIcon from "../common/icon" |
|||
import { CheckIcon } from "../common/Icons" |
|||
|
|||
const getPage = (s, name) => { |
|||
const props = s.pages[name] |
|||
return { name, props } |
|||
} |
|||
|
|||
$: currentAppInfo = { |
|||
name: $store.name, |
|||
} |
|||
|
|||
async function fetchUsers() { |
|||
const FETCH_USERS_URL = `/api/users` |
|||
const response = await api.get(FETCH_USERS_URL) |
|||
const users = await response.json() |
|||
backendUiStore.update(state => { |
|||
state.users = users |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
onMount(fetchUsers) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<ul> |
|||
{#each $backendUiStore.users as user} |
|||
<li> |
|||
<i class="ri-user-4-line" /> |
|||
<button class:active={user.username === $store.currentUserName}> |
|||
{user.name} |
|||
</button> |
|||
</li> |
|||
{/each} |
|||
</ul> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
padding-bottom: 10px; |
|||
font-size: 0.9rem; |
|||
color: var(--secondary50); |
|||
font-weight: bold; |
|||
position: relative; |
|||
padding-left: 1.8rem; |
|||
} |
|||
|
|||
ul { |
|||
margin: 0; |
|||
padding: 0; |
|||
list-style: none; |
|||
} |
|||
|
|||
li { |
|||
margin: 0.5rem 0; |
|||
} |
|||
|
|||
button { |
|||
margin: 0 0 0 6px; |
|||
padding: 0; |
|||
border: none; |
|||
font-size: 13px; |
|||
outline: none; |
|||
cursor: pointer; |
|||
background: rgba(0, 0, 0, 0); |
|||
} |
|||
|
|||
.active { |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.icon { |
|||
display: inline-block; |
|||
width: 14px; |
|||
color: #333; |
|||
} |
|||
</style> |
|||
@ -1,111 +0,0 @@ |
|||
<script> |
|||
import { store } from "builderStore" |
|||
import UIkit from "uikit" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import ButtonGroup from "components/common/ButtonGroup.svelte" |
|||
import CodeMirror from "codemirror" |
|||
import "codemirror/mode/javascript/javascript.js" |
|||
|
|||
export let onCodeChanged |
|||
export let code |
|||
|
|||
export const show = () => { |
|||
UIkit.modal(codeModal).show() |
|||
} |
|||
|
|||
let codeModal |
|||
let editor |
|||
let cmInstance |
|||
|
|||
$: currentCode = code |
|||
$: originalCode = code |
|||
$: { |
|||
if (editor) { |
|||
if (!cmInstance) { |
|||
cmInstance = CodeMirror.fromTextArea(editor, { |
|||
mode: "javascript", |
|||
lineNumbers: false, |
|||
lineWrapping: true, |
|||
smartIndent: true, |
|||
matchBrackets: true, |
|||
readOnly: false, |
|||
}) |
|||
cmInstance.on("change", () => (currentCode = cmInstance.getValue())) |
|||
} |
|||
cmInstance.focus() |
|||
cmInstance.setValue(code || "") |
|||
} |
|||
} |
|||
|
|||
const cancel = () => { |
|||
UIkit.modal(codeModal).hide() |
|||
currentCode = originalCode |
|||
} |
|||
|
|||
const save = () => { |
|||
originalCode = currentCode |
|||
onCodeChanged(currentCode) |
|||
UIkit.modal(codeModal).hide() |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={codeModal} uk-modal> |
|||
<div class="uk-modal-dialog" uk-overflow-auto> |
|||
|
|||
<div class="uk-modal-header"> |
|||
<h3>Code</h3> |
|||
</div> |
|||
|
|||
<div class="uk-modal-body uk-form-horizontal"> |
|||
|
|||
<p> |
|||
Use the code box below to control how this component is displayed, with |
|||
javascript. |
|||
</p> |
|||
|
|||
<div> |
|||
<div class="editor-code-surround"> |
|||
function(render, context, state, route) {'{'} |
|||
</div> |
|||
<div class="editor"> |
|||
<textarea bind:this={editor} /> |
|||
</div> |
|||
<div class="editor-code-surround">{'}'}</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="uk-modal-footer"> |
|||
<ButtonGroup> |
|||
<ActionButton primary on:click={save}>Save</ActionButton> |
|||
<ActionButton alert on:click={cancel}>Close</ActionButton> |
|||
</ButtonGroup> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
h3 { |
|||
text-transform: uppercase; |
|||
font-size: 13px; |
|||
font-weight: 700; |
|||
color: #8997ab; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
p { |
|||
font-size: 13px; |
|||
color: #333; |
|||
margin-top: 0; |
|||
} |
|||
|
|||
.editor { |
|||
border-style: dotted; |
|||
border-width: 1px; |
|||
border-color: gainsboro; |
|||
padding: 10px 30px; |
|||
} |
|||
|
|||
.editor-code-surround { |
|||
font-family: "Courier New", Courier, monospace; |
|||
} |
|||
</style> |
|||
@ -1,320 +0,0 @@ |
|||
<script> |
|||
import { onMount, createEventDispatcher } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
import Swatch from "./Swatch.svelte" |
|||
import CheckedBackground from "./CheckedBackground.svelte" |
|||
import { buildStyle } from "./helpers.js" |
|||
import { |
|||
getColorFormat, |
|||
convertToHSVA, |
|||
convertHsvaToFormat, |
|||
} from "./utils.js" |
|||
import Slider from "./Slider.svelte" |
|||
import Palette from "./Palette.svelte" |
|||
import ButtonGroup from "./ButtonGroup.svelte" |
|||
import Input from "./Input.svelte" |
|||
import Portal from "./Portal.svelte" |
|||
|
|||
export let value = "#3ec1d3ff" |
|||
export let open = false |
|||
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console |
|||
export let disableSwatches = false |
|||
export let format = "hexa" |
|||
export let style = "" |
|||
export let pickerHeight = 0 |
|||
export let pickerWidth = 0 |
|||
|
|||
let colorPicker = null |
|||
let adder = null |
|||
|
|||
let h = null |
|||
let s = null |
|||
let v = null |
|||
let a = null |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
onMount(() => { |
|||
if (!swatches.length > 0) { |
|||
//Don't use locally stored recent colors if swatches have been passed as props |
|||
getRecentColors() |
|||
} |
|||
|
|||
if (colorPicker) { |
|||
colorPicker.focus() |
|||
} |
|||
|
|||
if (format) { |
|||
convertAndSetHSVA() |
|||
} |
|||
}) |
|||
|
|||
function getRecentColors() { |
|||
let colorStore = localStorage.getItem("cp:recent-colors") |
|||
if (colorStore) { |
|||
swatches = JSON.parse(colorStore) |
|||
} |
|||
} |
|||
|
|||
function handleEscape(e) { |
|||
if (open && e.key === "Escape") { |
|||
open = false |
|||
} |
|||
} |
|||
|
|||
function setRecentColor(color) { |
|||
if (swatches.length === 12) { |
|||
swatches.splice(0, 1) |
|||
} |
|||
if (!swatches.includes(color)) { |
|||
swatches = [...swatches, color] |
|||
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches)) |
|||
} |
|||
} |
|||
|
|||
function convertAndSetHSVA() { |
|||
let hsva = convertToHSVA(value, format) |
|||
setHSVA(hsva) |
|||
} |
|||
|
|||
function setHSVA([hue, sat, val, alpha]) { |
|||
h = hue |
|||
s = sat |
|||
v = val |
|||
a = alpha |
|||
} |
|||
|
|||
//fired by choosing a color from the palette |
|||
function setSaturationAndValue({ detail }) { |
|||
s = detail.s |
|||
v = detail.v |
|||
value = convertHsvaToFormat([h, s, v, a], format) |
|||
dispatchValue() |
|||
} |
|||
|
|||
function setHue({ color, isDrag }) { |
|||
h = color |
|||
value = convertHsvaToFormat([h, s, v, a], format) |
|||
if (!isDrag) { |
|||
dispatchValue() |
|||
} |
|||
} |
|||
|
|||
function setAlpha({ color, isDrag }) { |
|||
a = color === "1.00" ? "1" : color |
|||
value = convertHsvaToFormat([h, s, v, a], format) |
|||
if (!isDrag) { |
|||
dispatchValue() |
|||
} |
|||
} |
|||
|
|||
function dispatchValue() { |
|||
dispatch("change", value) |
|||
} |
|||
|
|||
function changeFormatAndConvert(f) { |
|||
format = f |
|||
value = convertHsvaToFormat([h, s, v, a], format) |
|||
} |
|||
|
|||
function handleColorInput(text) { |
|||
let format = getColorFormat(text) |
|||
if (format) { |
|||
value = text |
|||
convertAndSetHSVA() |
|||
} |
|||
} |
|||
|
|||
function dispatchInputChange() { |
|||
if (format) { |
|||
dispatchValue() |
|||
} |
|||
} |
|||
|
|||
function addSwatch() { |
|||
if (format) { |
|||
dispatch("addswatch", value) |
|||
setRecentColor(value) |
|||
} |
|||
} |
|||
|
|||
function removeSwatch(idx) { |
|||
let removedSwatch = swatches.splice(idx, 1) |
|||
swatches = swatches |
|||
dispatch("removeswatch", removedSwatch) |
|||
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches)) |
|||
} |
|||
|
|||
function applySwatch(color) { |
|||
if (value !== color) { |
|||
format = getColorFormat(color) |
|||
if (format) { |
|||
value = color |
|||
convertAndSetHSVA() |
|||
dispatchValue() |
|||
} |
|||
} |
|||
} |
|||
|
|||
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : "" |
|||
$: selectedColorStyle = buildStyle({ background: value, border }) |
|||
$: shrink = swatches.length > 0 |
|||
</script> |
|||
|
|||
<Portal> |
|||
<div |
|||
class="colorpicker-container" |
|||
transition:fade |
|||
bind:this={colorPicker} |
|||
{style} |
|||
tabindex="0" |
|||
on:keydown={handleEscape} |
|||
bind:clientHeight={pickerHeight} |
|||
bind:clientWidth={pickerWidth}> |
|||
|
|||
<div class="palette-panel"> |
|||
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} /> |
|||
</div> |
|||
|
|||
<div class="control-panel"> |
|||
<div class="alpha-hue-panel"> |
|||
<div> |
|||
<CheckedBackground borderRadius="50%" backgroundSize="8px"> |
|||
<div class="selected-color" style={selectedColorStyle} /> |
|||
</CheckedBackground> |
|||
</div> |
|||
<div> |
|||
<Slider |
|||
type="hue" |
|||
value={h} |
|||
on:change={hue => setHue(hue.detail)} |
|||
on:dragend={dispatchValue} /> |
|||
|
|||
<CheckedBackground borderRadius="10px" backgroundSize="7px"> |
|||
<Slider |
|||
type="alpha" |
|||
value={a} |
|||
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)} |
|||
on:dragend={dispatchValue} /> |
|||
</CheckedBackground> |
|||
|
|||
</div> |
|||
</div> |
|||
|
|||
{#if !disableSwatches} |
|||
<div transition:fade class="swatch-panel"> |
|||
{#if swatches.length > 0} |
|||
{#each swatches as color, idx} |
|||
<Swatch |
|||
{color} |
|||
on:click={() => applySwatch(color)} |
|||
on:removeswatch={() => removeSwatch(idx)} /> |
|||
{/each} |
|||
{/if} |
|||
{#if swatches.length !== 12} |
|||
<div |
|||
bind:this={adder} |
|||
transition:fade |
|||
class="adder" |
|||
on:click={addSwatch} |
|||
class:shrink> |
|||
<span>+</span> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
{/if} |
|||
|
|||
<div class="format-input-panel"> |
|||
<ButtonGroup {format} onclick={changeFormatAndConvert} /> |
|||
<Input |
|||
{value} |
|||
on:input={event => handleColorInput(event.target.value)} |
|||
on:change={dispatchInputChange} /> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
</Portal> |
|||
|
|||
<style> |
|||
.colorpicker-container { |
|||
position: absolute; |
|||
outline: none; |
|||
z-index: 3; |
|||
display: flex; |
|||
font-size: 11px; |
|||
font-weight: 400; |
|||
flex-direction: column; |
|||
margin: 5px 0px; |
|||
height: auto; |
|||
width: 220px; |
|||
background: #ffffff; |
|||
border-radius: 2px; |
|||
box-shadow: 0 0.15em 1.5em 0 rgba(0, 0, 0, 0.1), |
|||
0 0 1em 0 rgba(0, 0, 0, 0.03); |
|||
} |
|||
|
|||
.palette-panel { |
|||
flex: 1; |
|||
} |
|||
|
|||
.control-panel { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
padding: 8px; |
|||
background: white; |
|||
border: 1px solid #d2d2d2; |
|||
color: #777373; |
|||
} |
|||
|
|||
.alpha-hue-panel { |
|||
display: grid; |
|||
grid-template-columns: 25px 1fr; |
|||
grid-gap: 15px; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.selected-color { |
|||
width: 30px; |
|||
height: 30px; |
|||
border-radius: 50%; |
|||
} |
|||
|
|||
.swatch-panel { |
|||
flex: 0 0 15px; |
|||
display: flex; |
|||
flex-flow: row wrap; |
|||
justify-content: flex-start; |
|||
padding: 0 5px; |
|||
max-height: 56px; |
|||
} |
|||
|
|||
.adder { |
|||
flex: 1; |
|||
height: 20px; |
|||
display: flex; |
|||
transition: flex 0.5s; |
|||
justify-content: center; |
|||
align-items: center; |
|||
background: #f1f3f4; |
|||
cursor: pointer; |
|||
border: 1px solid #d4d4d4; |
|||
border-radius: 8px; |
|||
margin-left: 5px; |
|||
margin-top: 3px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.shrink { |
|||
flex: 0 0 20px; |
|||
} |
|||
|
|||
.format-input-panel { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
padding-top: 3px; |
|||
} |
|||
</style> |
|||
@ -1,157 +0,0 @@ |
|||
<script> |
|||
import Colorpicker from "./Colorpicker.svelte" |
|||
import CheckedBackground from "./CheckedBackground.svelte" |
|||
import { createEventDispatcher, beforeUpdate } from "svelte" |
|||
|
|||
import { buildStyle } from "./helpers.js" |
|||
import { fade } from "svelte/transition" |
|||
import { getColorFormat } from "./utils.js" |
|||
|
|||
export let value = "#3ec1d3ff" |
|||
export let swatches = [] |
|||
export let disableSwatches = false |
|||
export let open = false |
|||
export let width = "25px" |
|||
export let height = "25px" |
|||
|
|||
let format = "hexa" |
|||
let dimensions = { top: 0, bottom: 0, right: 0, left: 0 } |
|||
let positionSide = "top" |
|||
let colorPreview = null |
|||
|
|||
let previewHeight = null |
|||
let previewWidth = null |
|||
let pickerWidth = 0 |
|||
let pickerHeight = 0 |
|||
|
|||
let errorMsg = null |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
beforeUpdate(() => { |
|||
format = getColorFormat(value) |
|||
if (!format) { |
|||
errorMsg = `Colorpicker - ${value} is an unknown color format. Please use a hex, rgb or hsl value` |
|||
console.error(errorMsg) |
|||
} else { |
|||
errorMsg = null |
|||
} |
|||
}) |
|||
|
|||
function openColorpicker(event) { |
|||
if (colorPreview) { |
|||
open = true |
|||
} |
|||
} |
|||
|
|||
function onColorChange(color) { |
|||
value = color.detail |
|||
dispatch("change", color.detail) |
|||
} |
|||
|
|||
$: if (open && colorPreview) { |
|||
const { |
|||
top: spaceAbove, |
|||
width, |
|||
bottom, |
|||
right, |
|||
left, |
|||
} = colorPreview.getBoundingClientRect() |
|||
|
|||
const spaceBelow = window.innerHeight - bottom |
|||
const previewCenter = previewWidth / 2 |
|||
|
|||
let y, x |
|||
|
|||
if (spaceAbove > spaceBelow) { |
|||
positionSide = "bottom" |
|||
y = window.innerHeight - spaceAbove |
|||
} else { |
|||
positionSide = "top" |
|||
y = bottom |
|||
} |
|||
|
|||
x = left + previewCenter - pickerWidth / 2 |
|||
|
|||
dimensions = { [positionSide]: y.toFixed(1), left: x.toFixed(1) } |
|||
} |
|||
|
|||
$: previewStyle = buildStyle({ width, height, background: value }) |
|||
$: errorPreviewStyle = buildStyle({ width, height }) |
|||
$: pickerStyle = buildStyle({ |
|||
[positionSide]: `${dimensions[positionSide]}px`, |
|||
left: `${dimensions.left}px`, |
|||
}) |
|||
</script> |
|||
|
|||
<div class="color-preview-container"> |
|||
{#if !errorMsg} |
|||
<CheckedBackground borderRadius="3px" backgroundSize="8px"> |
|||
<div |
|||
bind:this={colorPreview} |
|||
bind:clientHeight={previewHeight} |
|||
bind:clientWidth={previewWidth} |
|||
class="color-preview" |
|||
style={previewStyle} |
|||
on:click={openColorpicker} /> |
|||
</CheckedBackground> |
|||
|
|||
{#if open} |
|||
<Colorpicker |
|||
style={pickerStyle} |
|||
on:change={onColorChange} |
|||
on:addswatch |
|||
on:removeswatch |
|||
bind:format |
|||
bind:value |
|||
bind:pickerHeight |
|||
bind:pickerWidth |
|||
bind:open |
|||
{swatches} |
|||
{disableSwatches} /> |
|||
<div on:click|self={() => (open = false)} class="overlay" /> |
|||
{/if} |
|||
{:else} |
|||
<div class="color-preview preview-error" style={errorPreviewStyle}> |
|||
<span>×</span> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.color-preview-container { |
|||
display: flex; |
|||
flex-flow: row nowrap; |
|||
height: fit-content; |
|||
} |
|||
|
|||
.color-preview { |
|||
cursor: pointer; |
|||
border-radius: 3px; |
|||
border: 1px solid #dedada; |
|||
} |
|||
|
|||
.preview-error { |
|||
background: #cccccc; |
|||
color: #808080; |
|||
text-align: center; |
|||
font-size: 18px; |
|||
cursor: not-allowed; |
|||
} |
|||
|
|||
/* .picker-container { |
|||
position: absolute; |
|||
z-index: 3; |
|||
width: fit-content; |
|||
height: fit-content; |
|||
} */ |
|||
|
|||
.overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
z-index: 2; |
|||
} |
|||
</style> |
|||
@ -1,37 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
|
|||
export let target = document.body |
|||
|
|||
let targetEl |
|||
let portal |
|||
let componentInstance |
|||
|
|||
onMount(() => { |
|||
if (typeof target === "string") { |
|||
targetEl = document.querySelector(target) |
|||
// Force exit |
|||
if (targetEl === null) { |
|||
return () => {} |
|||
} |
|||
} else if (target instanceof HTMLElement) { |
|||
targetEl = target |
|||
} else { |
|||
throw new TypeError( |
|||
`Unknown target type: ${typeof target}. Allowed types: String (CSS selector), HTMLElement.` |
|||
) |
|||
} |
|||
|
|||
portal = document.createElement("div") |
|||
targetEl.appendChild(portal) |
|||
portal.appendChild(componentInstance) |
|||
|
|||
return () => { |
|||
targetEl.removeChild(portal) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<div bind:this={componentInstance}> |
|||
<slot /> |
|||
</div> |
|||
@ -1,59 +0,0 @@ |
|||
<script> |
|||
import { searchAllComponents } from "./pagesParsing/searchComponents" |
|||
import { store } from "../builderStore" |
|||
|
|||
export let onComponentChosen = () => {} |
|||
|
|||
let phrase = "" |
|||
|
|||
components = $store.components |
|||
|
|||
$: filteredComponents = !phrase ? [] : searchAllComponents(components, phrase) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<form on:submit|preventDefault class="uk-search uk-search-large"> |
|||
<span uk-search-icon /> |
|||
<input |
|||
class="uk-search-input" |
|||
type="search" |
|||
placeholder="Based on component..." |
|||
bind:value={phrase} /> |
|||
</form> |
|||
|
|||
<div> |
|||
{#each filteredComponents as component} |
|||
<div class="component" on:click={() => onComponentChosen(component)}> |
|||
<div class="title">{component.name}</div> |
|||
<div class="description">{component.description}</div> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
.component { |
|||
padding: 5px; |
|||
border-style: solid; |
|||
border-width: 0 0 1px 0; |
|||
border-color: var(--grey-1); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.component:hover { |
|||
background-color: var(--primary10); |
|||
} |
|||
|
|||
.component > .title { |
|||
font-size: 13pt; |
|||
color: var(--ink); |
|||
} |
|||
|
|||
.component > .description { |
|||
font-size: 10pt; |
|||
color: var(--blue); |
|||
font-style: italic; |
|||
} |
|||
</style> |
|||
@ -1,90 +0,0 @@ |
|||
<script> |
|||
import PropsView from "./PropsView.svelte" |
|||
import { store } from "../builderStore" |
|||
import Textbox from "../common/Textbox.svelte" |
|||
import Button from "../common/Button.svelte" |
|||
import { LayoutIcon, PaintIcon, TerminalIcon } from "../common/Icons/" |
|||
|
|||
import { cloneDeep, join, split, last } from "lodash/fp" |
|||
import { assign } from "lodash" |
|||
|
|||
$: component = $store.currentPreviewItem |
|||
$: componentInfo = $store.currentComponentInfo |
|||
$: components = $store.components |
|||
|
|||
const updateComponent = doChange => doChange(cloneDeep(component)) |
|||
|
|||
const onPropsChanged = newProps => { |
|||
updateComponent(newComponent => assign(newComponent.props, newProps)) |
|||
} |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<ul> |
|||
<li> |
|||
<button> |
|||
<PaintIcon /> |
|||
</button> |
|||
</li> |
|||
<li> |
|||
<button> |
|||
<LayoutIcon /> |
|||
</button> |
|||
</li> |
|||
<li> |
|||
<button> |
|||
<TerminalIcon /> |
|||
</button> |
|||
</li> |
|||
</ul> |
|||
|
|||
<div class="component-props-container"> |
|||
<PropsView {componentInfo} {onPropsChanged} /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.title > div:nth-child(1) { |
|||
grid-column-start: name; |
|||
color: var(--ink); |
|||
} |
|||
|
|||
.title > div:nth-child(2) { |
|||
grid-column-start: actions; |
|||
} |
|||
|
|||
.component-props-container { |
|||
flex: 1 1 auto; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
ul { |
|||
list-style: none; |
|||
display: flex; |
|||
padding: 0; |
|||
} |
|||
|
|||
li { |
|||
margin-right: 20px; |
|||
background: none; |
|||
border-radius: 5px; |
|||
width: 45px; |
|||
height: 45px; |
|||
} |
|||
|
|||
li button { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: none; |
|||
border: none; |
|||
border-radius: 5px; |
|||
padding: 13px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,158 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { |
|||
keys, |
|||
map, |
|||
some, |
|||
includes, |
|||
cloneDeep, |
|||
isEqual, |
|||
sortBy, |
|||
filter, |
|||
difference, |
|||
} from "lodash/fp" |
|||
import { pipe } from "components/common/core" |
|||
import Checkbox from "components/common/Checkbox.svelte" |
|||
import IconButton from "components/common/IconButton.svelte" |
|||
import EventEditorModal from "./EventEditorModal.svelte" |
|||
|
|||
import { PencilIcon } from "components/common/Icons" |
|||
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers" |
|||
|
|||
export const EVENT_TYPE = "event" |
|||
|
|||
export let component |
|||
|
|||
let events = [] |
|||
let selectedEvent = null |
|||
|
|||
$: { |
|||
events = Object.keys(component) |
|||
// TODO: use real events |
|||
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName)) |
|||
.map(propName => ({ |
|||
name: propName, |
|||
handlers: component[propName] || [], |
|||
})) |
|||
} |
|||
|
|||
// Handle create app modal |
|||
const { open, close } = getContext("simple-modal") |
|||
|
|||
const openModal = event => { |
|||
selectedEvent = event |
|||
open( |
|||
EventEditorModal, |
|||
{ |
|||
eventOptions: events, |
|||
event: selectedEvent, |
|||
onClose: () => { |
|||
close() |
|||
selectedEvent = null |
|||
}, |
|||
}, |
|||
{ |
|||
closeButton: false, |
|||
closeOnEsc: false, |
|||
styleContent: { padding: 0 }, |
|||
closeOnOuterClick: true, |
|||
} |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<button class="newevent" on:click={() => openModal()}> |
|||
<i class="icon ri-add-circle-fill" /> |
|||
Create New Event |
|||
</button> |
|||
|
|||
<div class="root"> |
|||
<form on:submit|preventDefault class="uk-form-stacked form-root"> |
|||
{#each events as event, index} |
|||
{#if event.handlers.length > 0} |
|||
<div |
|||
class:selected={selectedEvent && selectedEvent.index === index} |
|||
class="handler-container budibase__nav-item" |
|||
on:click={() => openModal({ ...event, index })}> |
|||
<span class="event-name">{event.name}</span> |
|||
<span class="edit-text">EDIT</span> |
|||
</div> |
|||
{/if} |
|||
{/each} |
|||
</form> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
font-size: 10pt; |
|||
width: 100%; |
|||
} |
|||
|
|||
.newevent { |
|||
cursor: pointer; |
|||
border: 1px solid var(--grey-4); |
|||
border-radius: 3px; |
|||
width: 100%; |
|||
padding: 8px 16px; |
|||
margin: 0px 0px 12px 0px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
background: white; |
|||
color: var(--ink); |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
transition: all 2ms; |
|||
} |
|||
|
|||
.newevent:hover { |
|||
background: var(--grey-1); |
|||
} |
|||
|
|||
.icon { |
|||
color: var(--ink); |
|||
font-size: 16px; |
|||
margin-right: 4px; |
|||
} |
|||
|
|||
.form-root { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
header { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.handler-container { |
|||
display: grid; |
|||
grid-template-columns: repeat(2, 1fr); |
|||
border: 2px solid #f9f9f9; |
|||
height: 80px; |
|||
width: 100%; |
|||
} |
|||
|
|||
.event-name { |
|||
margin-top: 5px; |
|||
font-weight: bold; |
|||
font-size: 16px; |
|||
color: rgba(22, 48, 87, 0.6); |
|||
align-self: end; |
|||
} |
|||
|
|||
.edit-text { |
|||
font-family: Arial, Helvetica, sans-serif; |
|||
font-weight: bold; |
|||
align-self: end; |
|||
justify-self: end; |
|||
font-size: 10px; |
|||
color: rgba(35, 65, 105, 0.4); |
|||
} |
|||
|
|||
.selected { |
|||
color: var(--blue); |
|||
background: var(--grey-1) !important; |
|||
} |
|||
</style> |
|||
@ -1,170 +0,0 @@ |
|||
<script> |
|||
import InputGroup from "../common/Inputs/InputGroup.svelte" |
|||
import LayoutTemplateControls from "./LayoutTemplateControls.svelte" |
|||
|
|||
export let onStyleChanged = () => {} |
|||
export let component |
|||
|
|||
const tbrl = [ |
|||
{ placeholder: "T" }, |
|||
{ placeholder: "R" }, |
|||
{ placeholder: "B" }, |
|||
{ placeholder: "L" }, |
|||
] |
|||
|
|||
const se = [{ placeholder: "START" }, { placeholder: "END" }] |
|||
|
|||
const single = [{ placeholder: "" }] |
|||
|
|||
$: layout = { |
|||
...component._styles.position, |
|||
...component._styles.layout, |
|||
} |
|||
|
|||
$: layouts = { |
|||
templaterows: ["Grid Rows", single], |
|||
templatecolumns: ["Grid Columns", single], |
|||
} |
|||
|
|||
$: display = { |
|||
direction: ["Direction", single], |
|||
align: ["Align", single], |
|||
justify: ["Justify", single], |
|||
} |
|||
|
|||
$: positions = { |
|||
column: ["Column", se], |
|||
row: ["Row", se], |
|||
} |
|||
|
|||
$: spacing = { |
|||
margin: ["Margin", tbrl, "small"], |
|||
padding: ["Padding", tbrl, "small"], |
|||
} |
|||
|
|||
$: size = { |
|||
height: ["Height", single], |
|||
width: ["Width", single], |
|||
} |
|||
|
|||
$: zindex = { |
|||
zindex: ["Z-Index", single], |
|||
} |
|||
|
|||
const newValue = n => Array(n).fill("") |
|||
</script> |
|||
|
|||
<h3>Layout</h3> |
|||
<div class="layout-pos"> |
|||
{#each Object.entries(display) as [key, [name, meta, size]] (component._id + key)} |
|||
<div class="grid"> |
|||
<h5>{name}:</h5> |
|||
<LayoutTemplateControls |
|||
onStyleChanged={_value => onStyleChanged('layout', key, _value)} |
|||
values={layout[key] || newValue(meta.length)} |
|||
propertyName={name} |
|||
{meta} |
|||
{size} |
|||
type="text" /> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<!-- <h4>Positioning</h4> |
|||
<div class="layout-pos"> |
|||
{#each Object.entries(positions) as [key, [name, meta, size]] (component._id + key)} |
|||
<div class="grid"> |
|||
<h5>{name}:</h5> |
|||
<InputGroup |
|||
onStyleChanged={_value => onStyleChanged('position', key, _value)} |
|||
values={layout[key] || newValue(meta.length)} |
|||
{meta} |
|||
{size} /> |
|||
</div> |
|||
{/each} |
|||
</div> --> |
|||
|
|||
<h3>Spacing</h3> |
|||
<div class="layout-spacing"> |
|||
{#each Object.entries(spacing) as [key, [name, meta, size]] (component._id + key)} |
|||
<div class="grid"> |
|||
<h5>{name}:</h5> |
|||
<InputGroup |
|||
onStyleChanged={_value => onStyleChanged('position', key, _value)} |
|||
values={layout[key] || newValue(meta.length)} |
|||
{meta} |
|||
{size} |
|||
type="text" /> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<h3>Size</h3> |
|||
<div class="layout-layer"> |
|||
{#each Object.entries(size) as [key, [name, meta, size]] (component._id + key)} |
|||
<div class="grid"> |
|||
<h5>{name}:</h5> |
|||
<InputGroup |
|||
onStyleChanged={_value => onStyleChanged('position', key, _value)} |
|||
values={layout[key] || newValue(meta.length)} |
|||
type="text" |
|||
{meta} |
|||
{size} /> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<h3>Order</h3> |
|||
<div class="layout-layer"> |
|||
{#each Object.entries(zindex) as [key, [name, meta, size]] (component._id + key)} |
|||
<div class="grid"> |
|||
<h5>{name}:</h5> |
|||
<InputGroup |
|||
onStyleChanged={_value => onStyleChanged('position', key, _value)} |
|||
values={layout[key] || newValue(meta.length)} |
|||
{meta} |
|||
{size} /> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
h3 { |
|||
text-transform: uppercase; |
|||
font-size: 13px; |
|||
font-weight: 600; |
|||
color: var(--ink); |
|||
opacity: 0.6; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
h4 { |
|||
text-transform: uppercase; |
|||
font-size: 10px; |
|||
font-weight: 600; |
|||
color: var(--ink); |
|||
opacity: 0.4; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
h5 { |
|||
font-size: 13px; |
|||
font-weight: 400; |
|||
color: var(--ink); |
|||
opacity: 0.8; |
|||
padding-top: 13px; |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
div > div { |
|||
display: grid; |
|||
grid-template-rows: 1fr; |
|||
grid-gap: 10px; |
|||
height: 40px; |
|||
margin-bottom: 15px; |
|||
} |
|||
|
|||
.grid { |
|||
grid-template-columns: 70px 2fr; |
|||
} |
|||
</style> |
|||
@ -1,16 +1,13 @@ |
|||
<script> |
|||
import { Select } from "@budibase/bbui" |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
export let value |
|||
</script> |
|||
|
|||
<div class="uk-margin block-field"> |
|||
<div class="uk-form-controls"> |
|||
<select class="budibase__input" on:change {value}> |
|||
<option value="" /> |
|||
{#each $backendUiStore.models as model} |
|||
<option value={model._id}>{model.name}</option> |
|||
{/each} |
|||
</select> |
|||
</div> |
|||
</div> |
|||
<Select thin secondary wide on:change {value}> |
|||
<option value="" /> |
|||
{#each $backendUiStore.models as model} |
|||
<option value={model._id}>{model.name}</option> |
|||
{/each} |
|||
</Select> |
|||
|
|||
@ -1,33 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
export let value = "" |
|||
export let onChange = value => {} |
|||
export let options = [] |
|||
export let initialValue = "" |
|||
export let styleBindingProperty = "" |
|||
|
|||
const handleStyleBind = value => |
|||
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {} |
|||
|
|||
$: isOptionsObject = options.every(o => typeof o === "object") |
|||
|
|||
onMount(() => { |
|||
if (!value && !!initialValue) { |
|||
value = initialValue |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<select {value} on:change={ev => onChange(ev.target.value)}> |
|||
{#if isOptionsObject} |
|||
{#each options as { value, label }} |
|||
<option {...handleStyleBind(value || label)} value={value || label}> |
|||
{label} |
|||
</option> |
|||
{/each} |
|||
{:else} |
|||
{#each options as value} |
|||
<option {...handleStyleBind(value)} {value}>{value}</option> |
|||
{/each} |
|||
{/if} |
|||
</select> |
|||
@ -1,65 +0,0 @@ |
|||
<script> |
|||
import Textbox from "components/common/Textbox.svelte" |
|||
import Dropdown from "components/common/Dropdown.svelte" |
|||
import Button from "components/common/Button.svelte" |
|||
import { store } from "builderStore" |
|||
import { isRootComponent } from "./pagesParsing/searchComponents" |
|||
import { pipe } from "components/common/core" |
|||
import { filter, find, concat } from "lodash/fp" |
|||
|
|||
const notSelectedComponent = { name: "(none selected)" } |
|||
|
|||
$: page = $store.pages[$store.currentPageName] |
|||
$: title = page.index.title |
|||
$: components = pipe($store.components, [ |
|||
filter(store => !isRootComponent($store)), |
|||
concat([notSelectedComponent]), |
|||
]) |
|||
$: entryComponent = components[page.appBody] || notSelectedComponent |
|||
|
|||
const save = () => { |
|||
if (!title || !entryComponent || entryComponent === notSeletedComponent) |
|||
return |
|||
const page = { |
|||
index: { |
|||
title, |
|||
}, |
|||
appBody: entryComponent.name, |
|||
} |
|||
store.savePage(page) |
|||
} |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<h3>{$store.currentPageName}</h3> |
|||
|
|||
<form on:submit|preventDefault class="uk-form-horizontal"> |
|||
<Textbox bind:text={title} label="Title" hasError={!title} /> |
|||
<div class="help-text"> |
|||
The title of your page, displayed in the bowser tab |
|||
</div> |
|||
<Dropdown |
|||
label="App Entry Component" |
|||
options={components} |
|||
bind:selected={entryComponent} |
|||
textMember={v => v.name} /> |
|||
|
|||
<div class="help-text"> |
|||
The component that will be loaded into the body of the page |
|||
</div> |
|||
<div style="margin-top: 20px" /> |
|||
<Button on:click={save}>Save</Button> |
|||
</form> |
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
padding: 15px; |
|||
} |
|||
.help-text { |
|||
color: var(--grey-2); |
|||
font-size: 10pt; |
|||
} |
|||
</style> |
|||
@ -1,46 +0,0 @@ |
|||
<script> |
|||
import { store } from "builderStore" |
|||
import Checkbox from "../common/Checkbox.svelte" |
|||
import Textbox from "../common/Textbox.svelte" |
|||
import Dropdown from "../common/Dropdown.svelte" |
|||
import StateBindingControl from "./StateBindingControl.svelte" |
|||
|
|||
export let index |
|||
export let prop_name |
|||
export let prop_value |
|||
export let prop_definition = {} |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
{#if prop_definition.type !== 'event'} |
|||
<h5>{prop_name}</h5> |
|||
<StateBindingControl |
|||
value={prop_value} |
|||
type={prop_definition.type || prop_definition} |
|||
options={prop_definition.options} |
|||
styleBindingProperty={prop_definition.styleBindingProperty} |
|||
onChanged={v => store.setComponentProp(prop_name, v)} /> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
height: 40px; |
|||
margin-bottom: 15px; |
|||
display: grid; |
|||
grid-template-rows: 1fr; |
|||
grid-template-columns: 70px 1fr; |
|||
grid-gap: 10px; |
|||
align-items: baseline; |
|||
} |
|||
|
|||
h5 { |
|||
word-wrap: break-word; |
|||
font-size: 13px; |
|||
font-weight: 400; |
|||
color: var(--ink); |
|||
opacity: 0.8; |
|||
padding-top: 13px; |
|||
margin-bottom: 0; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,11 @@ |
|||
<script> |
|||
/* |
|||
This file exists because of how we pass this control to the |
|||
properties panel - via a JS reference, not using svelte tags |
|||
... checkout the use of Input in propertyCategories to see what i mean |
|||
*/ |
|||
import { Input } from "@budibase/bbui" |
|||
export let name, value, placeholder, type |
|||
</script> |
|||
|
|||
<Input {name} {value} {placeholder} {type} thin on:change /> |
|||
@ -1,54 +0,0 @@ |
|||
<script> |
|||
import { some, includes, filter } from "lodash/fp" |
|||
import Textbox from "../common/Textbox.svelte" |
|||
import Dropdown from "../common/Dropdown.svelte" |
|||
import PropControl from "./PropControl.svelte" |
|||
import IconButton from "../common/IconButton.svelte" |
|||
|
|||
export let component |
|||
export let components |
|||
|
|||
let errors = [] |
|||
const props_to_ignore = ["_component", "_children", "_styles", "_code", "_id"] |
|||
|
|||
$: componentDef = components[component._component] |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<form on:submit|preventDefault class="uk-form-stacked form-root"> |
|||
{#if componentDef} |
|||
{#each Object.entries(componentDef.props) as [prop_name, prop_def], index} |
|||
{#if prop_def !== 'event'} |
|||
<div class="prop-container"> |
|||
<PropControl |
|||
{prop_name} |
|||
prop_value={component[prop_name]} |
|||
prop_definition={prop_def} |
|||
{index} |
|||
disabled={false} /> |
|||
|
|||
</div> |
|||
{/if} |
|||
{/each} |
|||
{/if} |
|||
</form> |
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
font-size: 10pt; |
|||
width: 100%; |
|||
} |
|||
|
|||
.form-root { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.prop-container { |
|||
flex: 1 1 auto; |
|||
min-width: 250px; |
|||
} |
|||
</style> |
|||
@ -1,63 +0,0 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import IconButton from "../common/IconButton.svelte" |
|||
import Input from "../common/Input.svelte" |
|||
|
|||
export let value = "" |
|||
export let onChanged = () => {} |
|||
export let type = "" |
|||
export let options = [] |
|||
export let styleBindingProperty = "" |
|||
|
|||
$: bindOptionToStyle = !!styleBindingProperty |
|||
</script> |
|||
|
|||
<div class="unbound-container"> |
|||
{#if type === 'bool'} |
|||
<div> |
|||
<IconButton |
|||
icon={value == true ? 'check-square' : 'square'} |
|||
size="19" |
|||
on:click={() => onChanged(!value)} /> |
|||
</div> |
|||
{:else if type === 'models'} |
|||
<select |
|||
class="uk-select uk-form-small" |
|||
bind:value |
|||
on:change={() => { |
|||
onChanged(value) |
|||
}}> |
|||
{#each $backendUiStore.models || [] as option} |
|||
<option value={option}>{option.name}</option> |
|||
{/each} |
|||
</select> |
|||
{:else if type === 'options' || type === 'models'} |
|||
<select |
|||
class="uk-select uk-form-small" |
|||
{value} |
|||
on:change={ev => onChanged(ev.target.value)}> |
|||
{#each options || [] as option} |
|||
{#if bindOptionToStyle} |
|||
<option style={`${styleBindingProperty}: ${option};`} value={option}> |
|||
{option} |
|||
</option> |
|||
{:else} |
|||
<option value={option}>{option}</option> |
|||
{/if} |
|||
{/each} |
|||
</select> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.unbound-container { |
|||
display: flex; |
|||
} |
|||
|
|||
.bound-header > div:nth-child(1) { |
|||
flex: 1 0 auto; |
|||
width: 30px; |
|||
color: var(--secondary50); |
|||
padding-left: 5px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,22 @@ |
|||
<script> |
|||
import { params } from "@sveltech/routify" |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
if ($params.selectedView) { |
|||
let view |
|||
const viewName = decodeURI($params.selectedView) |
|||
for (let model of $backendUiStore.models) { |
|||
if (model.views && model.views[viewName]) { |
|||
view = model.views[viewName] |
|||
} |
|||
} |
|||
if (view) { |
|||
backendUiStore.actions.views.select({ |
|||
name: viewName, |
|||
...view, |
|||
}) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<slot /> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue