forked from tsai/budibase
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 DeleteRecordModal } from "./DeleteRecord.svelte" |
||||
export { default as CreateEditRecordModal } from "./CreateEditRecord.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> |
<script> |
||||
import { backendUiStore } from "builderStore" |
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 { FIELDS } from "constants/backend" |
||||
import { ModelSetupNav } from "components/nav/ModelSetupNav" |
|
||||
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte" |
|
||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte" |
import CreateEditColumn from "../modals/CreateEditColumn.svelte" |
||||
|
|
||||
let anchor |
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> |
<script> |
||||
import { DropdownMenu, Button, Icon } from "@budibase/bbui" |
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" |
||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
||||
|
|
||||
let anchor |
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> |
<script> |
||||
|
import { Select } from "@budibase/bbui" |
||||
import { backendUiStore } from "builderStore" |
import { backendUiStore } from "builderStore" |
||||
|
|
||||
export let value |
export let value |
||||
</script> |
</script> |
||||
|
|
||||
<div class="uk-margin block-field"> |
<Select thin secondary wide on:change {value}> |
||||
<div class="uk-form-controls"> |
<option value="" /> |
||||
<select class="budibase__input" on:change {value}> |
{#each $backendUiStore.models as model} |
||||
<option value="" /> |
<option value={model._id}>{model.name}</option> |
||||
{#each $backendUiStore.models as model} |
{/each} |
||||
<option value={model._id}>{model.name}</option> |
</Select> |
||||
{/each} |
|
||||
</select> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|||||
@ -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