Merge branch 'linked-records' of github.com:Budibase/budibase into linked-records
This commit is contained in:
commit
0db876ad53
|
@ -79,7 +79,6 @@
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-loading-spinners": "^0.1.1",
|
"svelte-loading-spinners": "^0.1.1",
|
||||||
"svelte-portal": "^0.1.0",
|
"svelte-portal": "^0.1.0",
|
||||||
"svelte-simple-modal": "^0.4.2",
|
|
||||||
"yup": "^0.29.2"
|
"yup": "^0.29.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,32 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
import Modal from "svelte-simple-modal"
|
import { onMount } from "svelte"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { automationStore } from "builderStore"
|
||||||
import { onMount, getContext } from "svelte"
|
|
||||||
import { backendUiStore, automationStore } from "builderStore"
|
|
||||||
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
||||||
import { Button } from "@budibase/bbui"
|
import { Button } from "@budibase/bbui"
|
||||||
|
import { Modal } from "components/common/Modal"
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
let modal
|
||||||
|
|
||||||
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
|
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
|
||||||
|
|
||||||
function newAutomation() {
|
|
||||||
open(
|
|
||||||
CreateAutomationModal,
|
|
||||||
{
|
|
||||||
onClosed: close,
|
|
||||||
},
|
|
||||||
{ styleContent: { padding: "0" } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
automationStore.actions.fetch()
|
automationStore.actions.fetch()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<Button primary wide on:click={newAutomation}>Create New Automation</Button>
|
<Button primary wide on:click={() => modal.show()}>
|
||||||
|
Create New Automation
|
||||||
|
</Button>
|
||||||
<ul>
|
<ul>
|
||||||
{#each $automationStore.automations as automation}
|
{#each $automationStore.automations as automation}
|
||||||
<li
|
<li
|
||||||
|
@ -39,6 +30,9 @@
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateAutomationModal />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore, automationStore } from "builderStore"
|
import { store, backendUiStore, automationStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
|
||||||
import { Input } from "@budibase/bbui"
|
import { Input } from "@budibase/bbui"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
import { ModalTitle, ModalFooter } from "components/common/Modal"
|
||||||
export let onClosed
|
|
||||||
|
|
||||||
let name
|
let name
|
||||||
|
|
||||||
|
@ -13,71 +11,38 @@
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||||
$: appId = $store.appId
|
$: appId = $store.appId
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(resolve, ms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function createAutomation() {
|
async function createAutomation() {
|
||||||
await automationStore.actions.create({
|
await automationStore.actions.create({
|
||||||
name,
|
name,
|
||||||
instanceId,
|
instanceId,
|
||||||
})
|
})
|
||||||
onClosed()
|
|
||||||
notifier.success(`Automation ${name} created.`)
|
notifier.success(`Automation ${name} created.`)
|
||||||
analytics.captureEvent("Automation Created", { name })
|
analytics.captureEvent("Automation Created", { name })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<ModalTitle>Create Automation</ModalTitle>
|
||||||
<header>
|
<Input bind:value={name} label="Name" />
|
||||||
<i class="ri-stackshare-line" />
|
<ModalFooter
|
||||||
<h3>Create Automation</h3>
|
confirmText="Create"
|
||||||
</header>
|
onConfirm={createAutomation}
|
||||||
<div class="content">
|
disabled={!valid}>
|
||||||
<Input bind:value={name} label="Name" />
|
<a
|
||||||
</div>
|
target="_blank"
|
||||||
<footer>
|
href="https://docs.budibase.com/automate/introduction-to-automate">
|
||||||
<a href="https://docs.budibase.com">
|
<i class="ri-information-line" />
|
||||||
<i class="ri-information-line" />
|
<span>Learn about automations</span>
|
||||||
<span>Learn about automations</span>
|
</a>
|
||||||
</a>
|
</ModalFooter>
|
||||||
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
|
|
||||||
<ActionButton disabled={!valid} on:click={createAutomation}>
|
|
||||||
Save
|
|
||||||
</ActionButton>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
a {
|
||||||
padding: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
header h3 {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
color: var(--ink);
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
header i {
|
|
||||||
margin-right: var(--spacing-m);
|
|
||||||
font-size: 28px;
|
|
||||||
color: var(--grey-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: var(--spacing-xl) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
display: grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-gap: var(--spacing-m);
|
|
||||||
grid-auto-columns: 3fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
footer a {
|
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -85,10 +50,10 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
footer a span {
|
a span {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
footer i {
|
i {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-m);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
$: allowDeleteBlock =
|
$: allowDeleteBlock =
|
||||||
$automationStore.selectedBlock?.type !== "TRIGGER" ||
|
$automationStore.selectedBlock?.type !== "TRIGGER" ||
|
||||||
!automation?.definition?.steps?.length
|
!automation?.definition?.steps?.length
|
||||||
|
$: name = automation?.name ?? ""
|
||||||
|
|
||||||
function deleteAutomationBlock() {
|
function deleteAutomationBlock() {
|
||||||
automationStore.actions.deleteAutomationBlock(
|
automationStore.actions.deleteAutomationBlock(
|
||||||
|
@ -101,7 +102,7 @@
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
title="Confirm Delete"
|
title="Confirm Delete"
|
||||||
body={`Are you sure you wish to delete the automation '${automation.name}'?`}
|
body={`Are you sure you wish to delete the automation '${name}'?`}
|
||||||
okText="Delete Automation"
|
okText="Delete Automation"
|
||||||
onOk={deleteAutomation} />
|
onOk={deleteAutomation} />
|
||||||
|
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { fade } from "svelte/transition"
|
|
||||||
import { backendUiStore } from "builderStore"
|
|
||||||
import api from "builderStore/api"
|
|
||||||
|
|
||||||
export let ids = []
|
|
||||||
export let field
|
|
||||||
|
|
||||||
let records = []
|
|
||||||
let open = false
|
|
||||||
let model
|
|
||||||
|
|
||||||
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name]
|
|
||||||
|
|
||||||
async function fetchRecords() {
|
|
||||||
const response = await api.post("/api/records/search", {
|
|
||||||
keys: ids,
|
|
||||||
})
|
|
||||||
const modelResponse = await api.get(`/api/models/${field.modelId}`)
|
|
||||||
records = await response.json()
|
|
||||||
model = await modelResponse.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
$: ids && fetchRecords()
|
|
||||||
|
|
||||||
function toggleOpen() {
|
|
||||||
open = !open
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
fetchRecords()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<a on:click={toggleOpen}>{records.length}</a>
|
|
||||||
{#if open}
|
|
||||||
<div class="popover" transition:fade>
|
|
||||||
<header>
|
|
||||||
<h3>{field.name}</h3>
|
|
||||||
<i class="ri-close-circle-fill" on:click={toggleOpen} />
|
|
||||||
</header>
|
|
||||||
{#each records as record}
|
|
||||||
<div class="linked-record">
|
|
||||||
<div class="fields">
|
|
||||||
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
|
|
||||||
<div class="field">
|
|
||||||
<span>{model.schema[key].name}</span>
|
|
||||||
<p>{record[key]}</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
section {
|
|
||||||
display: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--ink-lighter);
|
|
||||||
}
|
|
||||||
|
|
||||||
i:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover {
|
|
||||||
width: 500px;
|
|
||||||
position: absolute;
|
|
||||||
right: 15%;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--grey-1);
|
|
||||||
border: 1px solid var(--grey);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fields {
|
|
||||||
padding: 15px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
grid-gap: 20px;
|
|
||||||
background: var(--white);
|
|
||||||
border: 1px solid var(--grey);
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field span {
|
|
||||||
color: var(--ink-lighter);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field p {
|
|
||||||
color: var(--ink);
|
|
||||||
font-size: 14px;
|
|
||||||
word-break: break-word;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import CreateRowButton from "./buttons/CreateRowButton.svelte"
|
||||||
import ColumnPopover from "./popovers/Column.svelte"
|
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
|
||||||
import ViewPopover from "./popovers/View.svelte"
|
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||||
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
import * as api from "./api"
|
import * as api from "./api"
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
|
|
||||||
|
@ -10,6 +11,10 @@
|
||||||
|
|
||||||
$: title = $backendUiStore.selectedModel.name
|
$: title = $backendUiStore.selectedModel.name
|
||||||
$: schema = $backendUiStore.selectedModel.schema
|
$: schema = $backendUiStore.selectedModel.schema
|
||||||
|
$: modelView = {
|
||||||
|
schema,
|
||||||
|
name: $backendUiStore.selectedView.name,
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch records for specified model
|
// Fetch records for specified model
|
||||||
$: {
|
$: {
|
||||||
|
@ -22,9 +27,10 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Table {title} {schema} {data} allowEditing={true}>
|
<Table {title} {schema} {data} allowEditing={true}>
|
||||||
<ColumnPopover />
|
<CreateColumnButton />
|
||||||
{#if Object.keys(schema).length > 0}
|
{#if Object.keys(schema).length > 0}
|
||||||
<RowPopover />
|
<CreateRowButton />
|
||||||
<ViewPopover />
|
<CreateViewButton />
|
||||||
|
<ExportButton view={modelView} />
|
||||||
{/if}
|
{/if}
|
||||||
</Table>
|
</Table>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
|
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
|
||||||
import Dropzone from "components/common/Dropzone.svelte"
|
import Dropzone from "components/common/Dropzone.svelte"
|
||||||
import { capitalise } from "../../../../helpers"
|
import { capitalise } from "../../../helpers"
|
||||||
|
|
||||||
export let meta
|
export let meta
|
||||||
export let value = meta.type === "boolean" ? false : ""
|
export let value = meta.type === "boolean" ? false : ""
|
|
@ -9,13 +9,13 @@
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import AttachmentList from "./AttachmentList.svelte"
|
import AttachmentList from "./AttachmentList.svelte"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import TablePagination from "./TablePagination.svelte"
|
||||||
import CreateEditRecordModal from "./popovers/CreateEditRecord.svelte"
|
import CreateEditRecordModal from "./modals/CreateEditRecordModal.svelte"
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import RowPopover from "./buttons/CreateRowButton.svelte"
|
||||||
import ColumnPopover from "./popovers/Column.svelte"
|
import ColumnPopover from "./buttons/CreateColumnButton.svelte"
|
||||||
import ViewPopover from "./popovers/View.svelte"
|
import ViewPopover from "./buttons/CreateViewButton.svelte"
|
||||||
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
|
import ColumnHeaderPopover from "./popovers/ColumnPopover.svelte"
|
||||||
import EditRowPopover from "./popovers/EditRow.svelte"
|
import EditRowPopover from "./popovers/RowPopover.svelte"
|
||||||
import CalculationPopover from "./popovers/Calculate.svelte"
|
import CalculationPopover from "./buttons/CalculateButton.svelte"
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 10
|
const ITEMS_PER_PAGE = 10
|
||||||
|
|
||||||
|
@ -32,9 +32,9 @@
|
||||||
$: paginatedData =
|
$: paginatedData =
|
||||||
sorted && sorted.length
|
sorted && sorted.length
|
||||||
? sorted.slice(
|
? sorted.slice(
|
||||||
currentPage * ITEMS_PER_PAGE,
|
currentPage * ITEMS_PER_PAGE,
|
||||||
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE,
|
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
|
||||||
)
|
)
|
||||||
: []
|
: []
|
||||||
$: modelId = data?.length ? data[0].modelId : null
|
$: modelId = data?.length ? data[0].modelId : null
|
||||||
|
|
||||||
|
@ -42,7 +42,9 @@
|
||||||
if (!record?.[fieldName]?.length) {
|
if (!record?.[fieldName]?.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$goto(`/${$params.application}/backend/model/${modelId}/relationship/${record._id}/${fieldName}`)
|
$goto(
|
||||||
|
`/${$params.application}/backend/model/${modelId}/relationship/${record._id}/${fieldName}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -50,68 +52,68 @@
|
||||||
<div class="table-controls">
|
<div class="table-controls">
|
||||||
<h2 class="title">{title}</h2>
|
<h2 class="title">{title}</h2>
|
||||||
<div class="popovers">
|
<div class="popovers">
|
||||||
<slot/>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="bb-table">
|
<table class="bb-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
|
||||||
{#if allowEditing}
|
|
||||||
<th class="edit-header">
|
|
||||||
<div>Edit</div>
|
|
||||||
</th>
|
|
||||||
{/if}
|
|
||||||
{#each columns as header}
|
|
||||||
<th>
|
|
||||||
{#if allowEditing}
|
|
||||||
<ColumnHeaderPopover field={schema[header]}/>
|
|
||||||
{:else}
|
|
||||||
<div class="header">{header}</div>
|
|
||||||
{/if}
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#if paginatedData.length === 0}
|
|
||||||
{#if allowEditing}
|
|
||||||
<td class="no-border">No data.</td>
|
|
||||||
{/if}
|
|
||||||
{#each columns as header, idx}
|
|
||||||
<td class="no-border">
|
|
||||||
{#if idx === 0}No data.{/if}
|
|
||||||
</td>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{#each paginatedData as row}
|
|
||||||
<tr>
|
<tr>
|
||||||
{#if allowEditing}
|
{#if allowEditing}
|
||||||
<td>
|
<th class="edit-header">
|
||||||
<EditRowPopover {row}/>
|
<div>Edit</div>
|
||||||
</td>
|
</th>
|
||||||
{/if}
|
{/if}
|
||||||
{#each columns as header}
|
{#each columns as header}
|
||||||
<td>
|
<th>
|
||||||
{#if schema[header].type === 'link'}
|
{#if allowEditing}
|
||||||
<div
|
<ColumnHeaderPopover field={schema[header]} />
|
||||||
class:link={row[header] && row[header].length}
|
{:else}
|
||||||
on:click={() => selectRelationship(row, header)}>
|
<div class="header">{header}</div>
|
||||||
{row[header] ? row[header].length : 0} linked row(s)
|
{/if}
|
||||||
</div>
|
</th>
|
||||||
{:else if schema[header].type === 'attachment'}
|
|
||||||
<AttachmentList files={row[header] || []}/>
|
|
||||||
{:else}{getOr('', header, row)}{/if}
|
|
||||||
</td>
|
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#if paginatedData.length === 0}
|
||||||
|
{#if allowEditing}
|
||||||
|
<td class="no-border">No data.</td>
|
||||||
|
{/if}
|
||||||
|
{#each columns as header, idx}
|
||||||
|
<td class="no-border">
|
||||||
|
{#if idx === 0 && !allowEditing}No data.{/if}
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#each paginatedData as row}
|
||||||
|
<tr>
|
||||||
|
{#if allowEditing}
|
||||||
|
<td>
|
||||||
|
<EditRowPopover {row} />
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
{#each columns as header}
|
||||||
|
<td>
|
||||||
|
{#if schema[header].type === 'link'}
|
||||||
|
<div
|
||||||
|
class:link={row[header] && row[header].length}
|
||||||
|
on:click={() => selectRelationship(row, header)}>
|
||||||
|
{row[header] ? row[header].length : 0} related row(s)
|
||||||
|
</div>
|
||||||
|
{:else if schema[header].type === 'attachment'}
|
||||||
|
<AttachmentList files={row[header] || []} />
|
||||||
|
{:else}{getOr('', header, row)}{/if}
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<TablePagination
|
<TablePagination
|
||||||
{data}
|
{data}
|
||||||
bind:currentPage
|
bind:currentPage
|
||||||
pageItemCount={paginatedData.length}
|
pageItemCount={paginatedData.length}
|
||||||
{ITEMS_PER_PAGE}/>
|
{ITEMS_PER_PAGE} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import CalculationPopover from "./popovers/Calculate.svelte"
|
import CalculateButton from "./buttons/CalculateButton.svelte"
|
||||||
import GroupByPopover from "./popovers/GroupBy.svelte"
|
import GroupByButton from "./buttons/GroupByButton.svelte"
|
||||||
import FilterPopover from "./popovers/Filter.svelte"
|
import FilterButton from "./buttons/FilterButton.svelte"
|
||||||
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
|
|
||||||
export let view = {}
|
export let view = {}
|
||||||
|
|
||||||
|
@ -34,9 +35,10 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Table title={decodeURI(name)} schema={view.schema} {data}>
|
<Table title={decodeURI(name)} schema={view.schema} {data}>
|
||||||
<FilterPopover {view} />
|
<FilterButton {view} />
|
||||||
<CalculationPopover {view} />
|
<CalculateButton {view} />
|
||||||
{#if view.calculation}
|
{#if view.calculation}
|
||||||
<GroupByPopover {view} />
|
<GroupByButton {view} />
|
||||||
{/if}
|
{/if}
|
||||||
|
<ExportButton {view} />
|
||||||
</Table>
|
</Table>
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script>
|
||||||
|
import { Popover, TextButton, Icon } from "@budibase/bbui"
|
||||||
|
import CalculatePopover from "../popovers/CalculatePopover.svelte"
|
||||||
|
|
||||||
|
export let view = {}
|
||||||
|
|
||||||
|
let anchor
|
||||||
|
let dropdown
|
||||||
|
</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">
|
||||||
|
<CalculatePopover {view} onClosed={dropdown.hide} />
|
||||||
|
</Popover>
|
|
@ -1,14 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { backendUiStore } from "builderStore"
|
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
|
||||||
import {
|
import CreateEditColumnPopover from "../popovers/CreateEditColumnPopover.svelte"
|
||||||
DropdownMenu,
|
|
||||||
TextButton as Button,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { FIELDS } from "constants/backend"
|
|
||||||
import CreateEditColumn from "./CreateEditColumn.svelte"
|
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
let dropdown
|
let dropdown
|
||||||
|
@ -23,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
||||||
<h5>Create Column</h5>
|
<h5>Create Column</h5>
|
||||||
<CreateEditColumn onClosed={dropdown.hide} />
|
<CreateEditColumnPopover onClosed={dropdown.hide} />
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<style>
|
<style>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script>
|
||||||
|
import { TextButton as Button, Icon } from "@budibase/bbui"
|
||||||
|
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
|
||||||
|
import { Modal } from "components/common/Modal"
|
||||||
|
|
||||||
|
let modal
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button text small on:click={modal.show}>
|
||||||
|
<Icon name="addrow" />
|
||||||
|
Create New Row
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateEditRecordModal />
|
||||||
|
</Modal>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script>
|
||||||
|
import { Popover, TextButton, Icon } from "@budibase/bbui"
|
||||||
|
import CreateViewPopover from "../popovers/CreateViewPopover.svelte"
|
||||||
|
|
||||||
|
let anchor
|
||||||
|
let dropdown
|
||||||
|
</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">
|
||||||
|
<CreateViewPopover onClosed={dropdown.hide} />
|
||||||
|
</Popover>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script>
|
||||||
|
import { TextButton, Icon, Popover } from "@budibase/bbui"
|
||||||
|
import api from "builderStore/api"
|
||||||
|
import ExportPopover from "../popovers/ExportPopover.svelte"
|
||||||
|
|
||||||
|
export let view
|
||||||
|
|
||||||
|
let anchor
|
||||||
|
let dropdown
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={anchor}>
|
||||||
|
<TextButton text small on:click={dropdown.show}>
|
||||||
|
<Icon name="download" />
|
||||||
|
Export
|
||||||
|
</TextButton>
|
||||||
|
</div>
|
||||||
|
<Popover bind:this={dropdown} {anchor} align="left">
|
||||||
|
<ExportPopover {view} onClosed={dropdown.hide} />
|
||||||
|
</Popover>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script>
|
||||||
|
import { Popover, TextButton, Icon } from "@budibase/bbui"
|
||||||
|
import FilterPopover from "../popovers/FilterPopover.svelte"
|
||||||
|
|
||||||
|
export let view = {}
|
||||||
|
|
||||||
|
let anchor
|
||||||
|
let dropdown
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={anchor}>
|
||||||
|
<TextButton
|
||||||
|
text
|
||||||
|
small
|
||||||
|
on:click={dropdown.show}
|
||||||
|
active={view.filters && view.filters.length}>
|
||||||
|
<Icon name="filter" />
|
||||||
|
Filter
|
||||||
|
</TextButton>
|
||||||
|
</div>
|
||||||
|
<Popover bind:this={dropdown} {anchor} align="left">
|
||||||
|
<FilterPopover {view} onClosed={dropdown.hide} />
|
||||||
|
</Popover>
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script>
|
||||||
|
import { Popover, TextButton, Icon } from "@budibase/bbui"
|
||||||
|
import GroupByPopover from "../popovers/GroupByPopover.svelte"
|
||||||
|
|
||||||
|
export let view = {}
|
||||||
|
|
||||||
|
let anchor
|
||||||
|
let dropdown
|
||||||
|
</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">
|
||||||
|
<GroupByPopover {view} onClosed={dropdown.hide} />
|
||||||
|
</Popover>
|
|
@ -1 +0,0 @@
|
||||||
export { default } from "./ModelDataTable.svelte"
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script>
|
||||||
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
||||||
|
import RecordFieldControl from "../RecordFieldControl.svelte"
|
||||||
|
import * as api from "../api"
|
||||||
|
import { ModalTitle, ModalFooter } from "components/common/Modal"
|
||||||
|
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||||
|
|
||||||
|
export let record = {}
|
||||||
|
export let visible = false
|
||||||
|
|
||||||
|
let modal
|
||||||
|
let errors = []
|
||||||
|
|
||||||
|
$: creating = record?._id == null
|
||||||
|
$: model = record.modelId
|
||||||
|
? $backendUiStore.models.find(model => model._id === record?.modelId)
|
||||||
|
: $backendUiStore.selectedModel
|
||||||
|
$: modelSchema = Object.entries(model?.schema ?? {})
|
||||||
|
|
||||||
|
async function saveRecord() {
|
||||||
|
const recordResponse = await api.saveRecord(
|
||||||
|
{ ...record, modelId: model._id },
|
||||||
|
model._id
|
||||||
|
)
|
||||||
|
if (recordResponse.errors) {
|
||||||
|
errors = Object.keys(recordResponse.errors)
|
||||||
|
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
|
||||||
|
.flat()
|
||||||
|
// Prevent modal closing if there were errors
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
notifier.success("Record saved successfully.")
|
||||||
|
backendUiStore.actions.records.save(recordResponse)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalTitle>{creating ? 'Create Row' : 'Edit Row'}</ModalTitle>
|
||||||
|
<ErrorsBox {errors} />
|
||||||
|
{#each modelSchema as [key, meta]}
|
||||||
|
<div>
|
||||||
|
{#if meta.type === 'link'}
|
||||||
|
<LinkedRecordSelector bind:linkedRecords={record[key]} schema={meta} />
|
||||||
|
{:else}
|
||||||
|
<RecordFieldControl {meta} bind:value={record[key]} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<ModalFooter confirmText={creating ? 'Add' : 'Save'} onConfirm={saveRecord} />
|
|
@ -1,104 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
TextButton,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { backendUiStore } from "builderStore"
|
|
||||||
import { notifier } from "builderStore/store/notifications"
|
|
||||||
import CreateEditRecord from "./CreateEditRecord.svelte"
|
|
||||||
import analytics from "analytics"
|
|
||||||
|
|
||||||
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.`)
|
|
||||||
analytics.captureEvent("Added View Calculate", { field: view.field })
|
|
||||||
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">
|
|
||||||
<div class="actions">
|
|
||||||
<h5>Calculate</h5>
|
|
||||||
<div class="input-group-row">
|
|
||||||
<p>The</p>
|
|
||||||
<Select secondary thin bind:value={view.calculation}>
|
|
||||||
<option value="">Choose an option</option>
|
|
||||||
{#each CALCULATIONS as calculation}
|
|
||||||
<option value={calculation.key}>{calculation.name}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
<p>of</p>
|
|
||||||
<Select secondary thin bind:value={view.field}>
|
|
||||||
<option value="">Choose an option</option>
|
|
||||||
{#each fields as field}
|
|
||||||
<option value={field}>{field}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<Button secondary on:click={dropdown.hide}>Cancel</Button>
|
|
||||||
<Button primary on:click={saveView}>Save</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.actions {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 30px 1fr 20px 1fr;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Input, Select } from "@budibase/bbui"
|
||||||
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
import analytics from "analytics"
|
||||||
|
|
||||||
|
const CALCULATIONS = [
|
||||||
|
{
|
||||||
|
name: "Statistics",
|
||||||
|
key: "stats",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export let view = {}
|
||||||
|
export let onClosed
|
||||||
|
|
||||||
|
$: 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.`)
|
||||||
|
onClosed()
|
||||||
|
analytics.captureEvent("Added View Calculate", { field: view.field })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<h5>Calculate</h5>
|
||||||
|
<div class="input-group-row">
|
||||||
|
<p>The</p>
|
||||||
|
<Select secondary thin bind:value={view.calculation}>
|
||||||
|
<option value="">Choose an option</option>
|
||||||
|
{#each CALCULATIONS as calculation}
|
||||||
|
<option value={calculation.key}>{calculation.name}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
<p>of</p>
|
||||||
|
<Select secondary thin bind:value={view.field}>
|
||||||
|
<option value="">Choose an option</option>
|
||||||
|
{#each fields as field}
|
||||||
|
<option value={field}>{field}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
|
<Button primary on:click={saveView}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.actions {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px 1fr 20px 1fr;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,7 +2,7 @@
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
import CreateEditColumnModal from "./CreateEditColumn.svelte"
|
import CreateEditColumnPopover from "./CreateEditColumnPopover.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { notifier } from "../../../../builderStore/store/notifications"
|
import { notifier } from "../../../../builderStore/store/notifications"
|
||||||
|
|
||||||
|
@ -26,6 +26,11 @@
|
||||||
editing = false
|
editing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showDelete() {
|
||||||
|
dropdown.hide()
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
function deleteColumn() {
|
function deleteColumn() {
|
||||||
if (field.name === $backendUiStore.selectedModel.primaryDisplay) {
|
if (field.name === $backendUiStore.selectedModel.primaryDisplay) {
|
||||||
notifier.danger("You cannot delete the primary display column")
|
notifier.danger("You cannot delete the primary display column")
|
||||||
|
@ -52,7 +57,7 @@
|
||||||
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
||||||
{#if editing}
|
{#if editing}
|
||||||
<h5>Edit Column</h5>
|
<h5>Edit Column</h5>
|
||||||
<CreateEditColumnModal onClosed={hideEditor} {field} />
|
<CreateEditColumnPopover onClosed={hideEditor} {field} />
|
||||||
{:else}
|
{:else}
|
||||||
<ul>
|
<ul>
|
||||||
{#if type !== 'link'}
|
{#if type !== 'link'}
|
||||||
|
@ -61,9 +66,7 @@
|
||||||
Edit
|
Edit
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
<li
|
<li data-cy="delete-column-header" on:click={showDelete}>
|
||||||
data-cy="delete-column-header"
|
|
||||||
on:click={() => confirmDeleteDialog.show()}>
|
|
||||||
<Icon name="delete" />
|
<Icon name="delete" />
|
||||||
Delete
|
Delete
|
||||||
</li>
|
</li>
|
|
@ -1,81 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount, tick } from "svelte"
|
|
||||||
import { store, backendUiStore } from "builderStore"
|
|
||||||
import { notifier } from "builderStore/store/notifications"
|
|
||||||
import { compose, map, get, flatten } from "lodash/fp"
|
|
||||||
import { Input, TextArea, Button } from "@budibase/bbui"
|
|
||||||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
|
||||||
import RecordFieldControl from "./RecordFieldControl.svelte"
|
|
||||||
import * as api from "../api"
|
|
||||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
|
||||||
|
|
||||||
export let record = {}
|
|
||||||
export let onClosed
|
|
||||||
|
|
||||||
let errors = []
|
|
||||||
$: model = record.modelId
|
|
||||||
? $backendUiStore.models.find(model => model._id === record?.modelId)
|
|
||||||
: $backendUiStore.selectedModel
|
|
||||||
$: modelSchema = Object.entries(model?.schema ?? {})
|
|
||||||
|
|
||||||
async function saveRecord() {
|
|
||||||
const recordResponse = await api.saveRecord(
|
|
||||||
{
|
|
||||||
...record,
|
|
||||||
modelId: model._id,
|
|
||||||
},
|
|
||||||
model._id
|
|
||||||
)
|
|
||||||
if (recordResponse.errors) {
|
|
||||||
errors = Object.keys(recordResponse.errors)
|
|
||||||
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
|
|
||||||
.flat()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onClosed()
|
|
||||||
notifier.success("Record saved successfully.")
|
|
||||||
backendUiStore.actions.records.save(recordResponse)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<ErrorsBox {errors} />
|
|
||||||
<form on:submit|preventDefault>
|
|
||||||
{#each modelSchema as [key, meta]}
|
|
||||||
<div>
|
|
||||||
{#if meta.type === 'link'}
|
|
||||||
<LinkedRecordSelector
|
|
||||||
bind:linkedRecords={record[key]}
|
|
||||||
schema={meta} />
|
|
||||||
{:else}
|
|
||||||
<RecordFieldControl {meta} bind:value={record[key]} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</form>
|
|
||||||
<footer>
|
|
||||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
|
||||||
<Button primary on:click={saveRecord}>Save</Button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.actions {
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
min-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,20 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Button, Input, Select } from "@budibase/bbui"
|
||||||
Popover,
|
|
||||||
TextButton,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "./CreateEditRecord.svelte"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
let anchor
|
export let onClosed
|
||||||
let dropdown
|
|
||||||
|
|
||||||
let name
|
let name
|
||||||
let field
|
let field
|
||||||
|
@ -37,28 +28,20 @@
|
||||||
field,
|
field,
|
||||||
})
|
})
|
||||||
notifier.success(`View ${name} created`)
|
notifier.success(`View ${name} created`)
|
||||||
dropdown.hide()
|
onClosed()
|
||||||
analytics.captureEvent("View Created", { name })
|
analytics.captureEvent("View Created", { name })
|
||||||
$goto(`../../../view/${name}`)
|
$goto(`../../../view/${name}`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div class="actions">
|
||||||
<TextButton text small on:click={dropdown.show}>
|
<h5>Create View</h5>
|
||||||
<Icon name="view" />
|
<Input label="View Name" thin bind:value={name} />
|
||||||
Create New View
|
<div class="footer">
|
||||||
</TextButton>
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
</div>
|
<Button primary on:click={saveView}>Save View</Button>
|
||||||
<Popover bind:this={dropdown} {anchor} align="left">
|
|
||||||
<div class="actions">
|
|
||||||
<h5>Create View</h5>
|
|
||||||
<Input label="View Name" thin bind:value={name} />
|
|
||||||
<div class="footer">
|
|
||||||
<Button secondary on:click={dropdown.hide}>Cancel</Button>
|
|
||||||
<Button primary on:click={saveView}>Save View</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h5 {
|
h5 {
|
|
@ -0,0 +1,61 @@
|
||||||
|
<script>
|
||||||
|
import api from "builderStore/api"
|
||||||
|
import { Button, Select } from "@budibase/bbui"
|
||||||
|
|
||||||
|
const FORMATS = [
|
||||||
|
{
|
||||||
|
name: "CSV",
|
||||||
|
key: "csv",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JSON",
|
||||||
|
key: "json",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export let view
|
||||||
|
export let onClosed
|
||||||
|
|
||||||
|
let exportFormat = FORMATS[0].key
|
||||||
|
|
||||||
|
async function exportView() {
|
||||||
|
const response = await api.post(
|
||||||
|
`/api/views/export?format=${exportFormat}`,
|
||||||
|
view
|
||||||
|
)
|
||||||
|
const downloadInfo = await response.json()
|
||||||
|
onClosed()
|
||||||
|
window.location = downloadInfo.url
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="popover">
|
||||||
|
<h5>Export Data</h5>
|
||||||
|
<Select label="Format" secondary thin bind:value={exportFormat}>
|
||||||
|
{#each FORMATS as format}
|
||||||
|
<option value={format.key}>{format.name}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
<div class="footer">
|
||||||
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
|
<Button primary on:click={exportView}>Export</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popover {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,15 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Button, Input, Select } from "@budibase/bbui"
|
||||||
Popover,
|
|
||||||
TextButton,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "./CreateEditRecord.svelte"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
const CONDITIONS = [
|
const CONDITIONS = [
|
||||||
|
@ -51,9 +43,7 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
export let view = {}
|
export let view = {}
|
||||||
|
export let onClosed
|
||||||
let anchor
|
|
||||||
let dropdown
|
|
||||||
|
|
||||||
$: viewModel = $backendUiStore.models.find(
|
$: viewModel = $backendUiStore.models.find(
|
||||||
({ _id }) => _id === $backendUiStore.selectedView.modelId
|
({ _id }) => _id === $backendUiStore.selectedView.modelId
|
||||||
|
@ -63,7 +53,7 @@
|
||||||
function saveView() {
|
function saveView() {
|
||||||
backendUiStore.actions.views.save(view)
|
backendUiStore.actions.views.save(view)
|
||||||
notifier.success(`View ${view.name} saved.`)
|
notifier.success(`View ${view.name} saved.`)
|
||||||
dropdown.hide()
|
onClosed()
|
||||||
analytics.captureEvent("Added View Filter", {
|
analytics.captureEvent("Added View Filter", {
|
||||||
filters: JSON.stringify(view.filters),
|
filters: JSON.stringify(view.filters),
|
||||||
})
|
})
|
||||||
|
@ -88,67 +78,55 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div class="actions">
|
||||||
<TextButton
|
<h5>Filter</h5>
|
||||||
text
|
{#if view.filters.length}
|
||||||
small
|
<div class="input-group-row">
|
||||||
on:click={dropdown.show}
|
{#each view.filters as filter, idx}
|
||||||
active={view.filters && view.filters.length}>
|
{#if idx === 0}
|
||||||
<Icon name="filter" />
|
<p>Where</p>
|
||||||
Filter
|
{:else}
|
||||||
</TextButton>
|
<Select secondary thin bind:value={filter.conjunction}>
|
||||||
</div>
|
|
||||||
<Popover bind:this={dropdown} {anchor} align="left">
|
|
||||||
<div class="actions">
|
|
||||||
<h5>Filter</h5>
|
|
||||||
{#if view.filters.length}
|
|
||||||
<div class="input-group-row">
|
|
||||||
{#each view.filters as filter, idx}
|
|
||||||
{#if idx === 0}
|
|
||||||
<p>Where</p>
|
|
||||||
{:else}
|
|
||||||
<Select secondary thin bind:value={filter.conjunction}>
|
|
||||||
<option value="">Choose an option</option>
|
|
||||||
{#each CONJUNCTIONS as conjunction}
|
|
||||||
<option value={conjunction.key}>{conjunction.name}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
{/if}
|
|
||||||
<Select secondary thin bind:value={filter.key}>
|
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each fields as field}
|
{#each CONJUNCTIONS as conjunction}
|
||||||
<option value={field}>{field}</option>
|
<option value={conjunction.key}>{conjunction.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
<Select secondary thin bind:value={filter.condition}>
|
{/if}
|
||||||
|
<Select secondary thin bind:value={filter.key}>
|
||||||
|
<option value="">Choose an option</option>
|
||||||
|
{#each fields as field}
|
||||||
|
<option value={field}>{field}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
<Select secondary thin bind:value={filter.condition}>
|
||||||
|
<option value="">Choose an option</option>
|
||||||
|
{#each CONDITIONS as condition}
|
||||||
|
<option value={condition.key}>{condition.name}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
{#if filter.key && isMultipleChoice(filter.key)}
|
||||||
|
<Select secondary thin bind:value={filter.value}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each CONDITIONS as condition}
|
{#each viewModel.schema[filter.key].constraints.inclusion as option}
|
||||||
<option value={condition.key}>{condition.name}</option>
|
<option value={option}>{option}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{#if filter.key && isMultipleChoice(filter.key)}
|
{:else}
|
||||||
<Select secondary thin bind:value={filter.value}>
|
<Input thin placeholder="Value" bind:value={filter.value} />
|
||||||
<option value="">Choose an option</option>
|
{/if}
|
||||||
{#each viewModel.schema[filter.key].constraints.inclusion as option}
|
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
|
||||||
<option value={option}>{option}</option>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</Select>
|
{/if}
|
||||||
{:else}
|
<div class="footer">
|
||||||
<Input thin placeholder="Value" bind:value={filter.value} />
|
<Button text on:click={addFilter}>Add Filter</Button>
|
||||||
{/if}
|
<div class="buttons">
|
||||||
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
{/each}
|
<Button primary on:click={saveView}>Save</Button>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="footer">
|
|
||||||
<Button text on:click={addFilter}>Add Filter</Button>
|
|
||||||
<div class="buttons">
|
|
||||||
<Button secondary on:click={dropdown.hide}>Cancel</Button>
|
|
||||||
<Button primary on:click={saveView}>Save</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.actions {
|
.actions {
|
|
@ -1,91 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
TextButton,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { backendUiStore } from "builderStore"
|
|
||||||
import { notifier } from "builderStore/store/notifications"
|
|
||||||
import CreateEditRecord from "./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
|
|
||||||
</TextButton>
|
|
||||||
</div>
|
|
||||||
<Popover bind:this={dropdown} {anchor} align="left">
|
|
||||||
<div class="actions">
|
|
||||||
<h5>Group</h5>
|
|
||||||
<div class="input-group-row">
|
|
||||||
<p>By</p>
|
|
||||||
<Select secondary thin bind:value={view.groupBy}>
|
|
||||||
<option value="">Choose an option</option>
|
|
||||||
{#each fields as field}
|
|
||||||
<option value={field}>{field}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<Button secondary on:click={dropdown.hide}>Cancel</Button>
|
|
||||||
<Button primary on:click={saveView}>Save</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.actions {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 20px 1fr;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Input, Select } from "@budibase/bbui"
|
||||||
|
import { backendUiStore } from "builderStore"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
|
||||||
|
export let view = {}
|
||||||
|
export let onClosed
|
||||||
|
|
||||||
|
$: 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.`)
|
||||||
|
onClosed()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<h5>Group</h5>
|
||||||
|
<div class="input-group-row">
|
||||||
|
<p>By</p>
|
||||||
|
<Select secondary thin bind:value={view.groupBy}>
|
||||||
|
<option value="">Choose an option</option>
|
||||||
|
{#each fields as field}
|
||||||
|
<option value={field}>{field}</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
|
<Button primary on:click={saveView}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.actions {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20px 1fr;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,27 +0,0 @@
|
||||||
<script>
|
|
||||||
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
|
|
||||||
import CreateEditRecord from "./CreateEditRecord.svelte"
|
|
||||||
import { Modal } from "components/common/Modal"
|
|
||||||
|
|
||||||
let anchor
|
|
||||||
let dropdown
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
|
||||||
<Button text small on:click={dropdown.show}>
|
|
||||||
<Icon name="addrow" />
|
|
||||||
Create New Row
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Modal bind:this={dropdown}>
|
|
||||||
<h5>Add New Row</h5>
|
|
||||||
<CreateEditRecord onClosed={dropdown.hide} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
h5 {
|
|
||||||
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,41 +1,33 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import {
|
import { DropdownMenu, Icon } from "@budibase/bbui"
|
||||||
DropdownMenu,
|
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Heading,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { FIELDS } from "constants/backend"
|
|
||||||
import CreateEditRecordModal from "./CreateEditRecord.svelte"
|
|
||||||
import * as api from "../api"
|
import * as api from "../api"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { Modal } from "components/common/Modal"
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
let dropdown
|
let dropdown
|
||||||
let editing
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
let modal
|
||||||
|
|
||||||
function showEditor() {
|
function showModal() {
|
||||||
editing = true
|
dropdown.hide()
|
||||||
|
modal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideEditor() {
|
function showDelete() {
|
||||||
dropdown.hide()
|
dropdown.hide()
|
||||||
editing = false
|
confirmDeleteDialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteRow() {
|
async function deleteRow() {
|
||||||
await api.deleteRecord(row)
|
await api.deleteRecord(row)
|
||||||
notifier.success("Record deleted")
|
notifier.success("Record deleted")
|
||||||
backendUiStore.actions.records.delete(row)
|
backendUiStore.actions.records.delete(row)
|
||||||
hideEditor()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -43,21 +35,16 @@
|
||||||
<i class="ri-more-line" />
|
<i class="ri-more-line" />
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
||||||
{#if editing}
|
<ul>
|
||||||
<h5>Edit Row</h5>
|
<li data-cy="edit-row" on:click={showModal}>
|
||||||
<CreateEditRecordModal onClosed={hideEditor} record={row} />
|
<Icon name="edit" />
|
||||||
{:else}
|
<span>Edit</span>
|
||||||
<ul>
|
</li>
|
||||||
<li data-cy="edit-row" on:click={showEditor}>
|
<li data-cy="delete-row" on:click={showDelete}>
|
||||||
<Icon name="edit" />
|
<Icon name="delete" />
|
||||||
<span>Edit</span>
|
<span>Delete</span>
|
||||||
</li>
|
</li>
|
||||||
<li data-cy="delete-row" on:click={() => confirmDeleteDialog.show()}>
|
</ul>
|
||||||
<Icon name="delete" />
|
|
||||||
<span>Delete</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
|
@ -65,6 +52,9 @@
|
||||||
okText="Delete Row"
|
okText="Delete Row"
|
||||||
onOk={deleteRow}
|
onOk={deleteRow}
|
||||||
title="Confirm Delete" />
|
title="Confirm Delete" />
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateEditRecordModal record={row} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.ri-more-line:hover {
|
.ri-more-line:hover {
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
||||||
|
@ -24,6 +23,11 @@
|
||||||
editing = false
|
editing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showModal() {
|
||||||
|
hideEditor()
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteTable() {
|
async function deleteTable() {
|
||||||
await backendUiStore.actions.models.delete(table)
|
await backendUiStore.actions.models.delete(table)
|
||||||
notifier.success("Table deleted")
|
notifier.success("Table deleted")
|
||||||
|
@ -66,7 +70,7 @@
|
||||||
<Icon name="edit" />
|
<Icon name="edit" />
|
||||||
Edit
|
Edit
|
||||||
</li>
|
</li>
|
||||||
<li data-cy="delete-table" on:click={() => confirmDeleteDialog.show()}>
|
<li data-cy="delete-table" on:click={showModal}>
|
||||||
<Icon name="delete" />
|
<Icon name="delete" />
|
||||||
Delete
|
Delete
|
||||||
</li>
|
</li>
|
||||||
|
@ -78,7 +82,6 @@
|
||||||
body={`Are you sure you wish to delete the table '${table.name}'? Your data will be deleted and this action cannot be undone.`}
|
body={`Are you sure you wish to delete the table '${table.name}'? Your data will be deleted and this action cannot be undone.`}
|
||||||
okText="Delete Table"
|
okText="Delete Table"
|
||||||
onOk={deleteTable}
|
onOk={deleteTable}
|
||||||
onCancel={hideEditor}
|
|
||||||
title="Confirm Delete" />
|
title="Confirm Delete" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
||||||
|
@ -24,6 +23,11 @@
|
||||||
editing = false
|
editing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showDelete() {
|
||||||
|
dropdown.hide()
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
await backendUiStore.actions.views.save({
|
await backendUiStore.actions.views.save({
|
||||||
originalName,
|
originalName,
|
||||||
|
@ -61,7 +65,7 @@
|
||||||
<Icon name="edit" />
|
<Icon name="edit" />
|
||||||
Edit
|
Edit
|
||||||
</li>
|
</li>
|
||||||
<li data-cy="delete-view" on:click={() => confirmDeleteDialog.show()}>
|
<li data-cy="delete-view" on:click={showDelete}>
|
||||||
<Icon name="delete" />
|
<Icon name="delete" />
|
||||||
Delete
|
Delete
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,19 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { slide } from "svelte/transition"
|
|
||||||
import { Switcher } from "@budibase/bbui"
|
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import ListItem from "./ListItem.svelte"
|
import ListItem from "./ListItem.svelte"
|
||||||
import { Button } from "@budibase/bbui"
|
|
||||||
import CreateTablePopover from "./CreateTable.svelte"
|
import CreateTablePopover from "./CreateTable.svelte"
|
||||||
import EditTablePopover from "./EditTable.svelte"
|
import EditTablePopover from "./EditTable.svelte"
|
||||||
import EditViewPopover from "./EditView.svelte"
|
import EditViewPopover from "./EditView.svelte"
|
||||||
import { Heading } from "@budibase/bbui"
|
import { Heading } from "@budibase/bbui"
|
||||||
import { Spacer } from "@budibase/bbui"
|
import { Spacer } from "@budibase/bbui"
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
|
||||||
|
|
||||||
$: selectedView =
|
$: selectedView =
|
||||||
$backendUiStore.selectedView && $backendUiStore.selectedView.name
|
$backendUiStore.selectedView && $backendUiStore.selectedView.name
|
||||||
|
|
||||||
|
|
|
@ -1,64 +1,31 @@
|
||||||
<script>
|
<script>
|
||||||
import { Modal, Button, Heading, Spacer } from "@budibase/bbui"
|
import { Modal, ModalTitle, ModalFooter } from "components/common/Modal"
|
||||||
|
|
||||||
export let title = ""
|
export let title = ""
|
||||||
export let body = ""
|
export let body = ""
|
||||||
export let okText = "OK"
|
export let okText = "Confirm"
|
||||||
export let cancelText = "Cancel"
|
export let cancelText = "Cancel"
|
||||||
export let onOk = () => {}
|
export let onOk = () => {}
|
||||||
export let onCancel = () => {}
|
export let onCancel = () => {}
|
||||||
|
|
||||||
|
let modal
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
theModal.show()
|
modal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hide = () => {
|
export const hide = () => {
|
||||||
theModal.hide()
|
modal.hide()
|
||||||
}
|
|
||||||
|
|
||||||
let theModal
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
hide()
|
|
||||||
onCancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
const ok = () => {
|
|
||||||
const result = onOk()
|
|
||||||
// allow caller to return false, to cancel the "ok"
|
|
||||||
if (result === false) return
|
|
||||||
hide()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal id={title} bind:this={theModal}>
|
<Modal id={title} bind:this={modal} on:hide={onCancel}>
|
||||||
<h2>{title}</h2>
|
<ModalTitle>{title}</ModalTitle>
|
||||||
<Spacer extraLarge />
|
<div class="body">{body}</div>
|
||||||
<div class="content">
|
<ModalFooter confirmText={okText} {cancelText} onConfirm={onOk} red />
|
||||||
<slot class="rows">{body}</slot>
|
|
||||||
</div>
|
|
||||||
<Spacer extraLarge />
|
|
||||||
<div class="modal-footer">
|
|
||||||
<Button red wide on:click={ok}>{okText}</Button>
|
|
||||||
<Button secondary wide on:click={cancel}>{cancelText}</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h2 {
|
.body {
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
white-space: normal;
|
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,9 +5,25 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if hasErrors}
|
{#if hasErrors}
|
||||||
<div class="bb__alert bb__alert--danger">
|
<div class="container bb__alert bb__alert--danger">
|
||||||
{#each errors as error}
|
{#each errors as error}
|
||||||
<div>{error.dataPath} {error.message}</div>
|
<div class="error">{error.dataPath} {error.message}</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
border-radius: var(--border-radius-m);
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.error:first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
|
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
|
||||||
const response = await api.get(FETCH_RECORDS_URL)
|
const response = await api.get(FETCH_RECORDS_URL)
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
console.log(result)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
/**
|
||||||
|
* Confirmation is handled as a callback rather than an event to allow
|
||||||
|
* handling the result - meaning a parent can prevent the modal closing.
|
||||||
|
*
|
||||||
|
* A show/hide API is exposed as part of the modal and also via context for
|
||||||
|
* children inside the modal.
|
||||||
|
* "show" and "hide" events are emitted as visibility changes.
|
||||||
|
*
|
||||||
|
* Modals are rendered at the top of the DOM tree.
|
||||||
|
*/
|
||||||
|
import { createEventDispatcher, setContext } from "svelte"
|
||||||
import { fade, fly } from "svelte/transition"
|
import { fade, fly } from "svelte/transition"
|
||||||
import { portal } from "./portal"
|
import Portal from "svelte-portal"
|
||||||
|
import { ContextKey } from "./context"
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let visible = false
|
export let wide = false
|
||||||
export let cancelText = "Cancel"
|
export let padded = true
|
||||||
export let confirmText = "Confirm"
|
|
||||||
export let showCancelButton = true
|
let visible
|
||||||
export let showConfirmButton = true
|
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
console.log("show")
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -26,10 +35,12 @@
|
||||||
visible = false
|
visible = false
|
||||||
dispatch("hide")
|
dispatch("hide")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContext(ContextKey, { show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div class="portal-wrapper" use:portal={'#modal-container'}>
|
<Portal target="#modal-container">
|
||||||
<div
|
<div
|
||||||
class="overlay"
|
class="overlay"
|
||||||
on:click|self={hide}
|
on:click|self={hide}
|
||||||
|
@ -37,15 +48,16 @@
|
||||||
<div
|
<div
|
||||||
class="scroll-wrapper"
|
class="scroll-wrapper"
|
||||||
on:click|self={hide}
|
on:click|self={hide}
|
||||||
transition:fly={{ y: 100 }}>
|
transition:fly={{ y: 50 }}>
|
||||||
<div class="content-wrapper" on:click|self={hide}>
|
<div class="content-wrapper" on:click|self={hide}>
|
||||||
<div class="content">
|
<div class="content" class:wide class:padded>
|
||||||
<slot />
|
<slot />
|
||||||
|
<i class="ri-close-line" on:click={hide} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Portal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -66,7 +78,6 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
z-index: 999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-wrapper {
|
.scroll-wrapper {
|
||||||
|
@ -98,5 +109,24 @@
|
||||||
flex: 0 0 400px;
|
flex: 0 0 400px;
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
border-radius: var(--border-radius-m);
|
border-radius: var(--border-radius-m);
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.content.wide {
|
||||||
|
flex: 0 0 600px;
|
||||||
|
}
|
||||||
|
.content.padded {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--spacing-xl);
|
||||||
|
right: var(--spacing-xl);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
}
|
||||||
|
i:hover {
|
||||||
|
color: var(--grey-6);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,5 +5,6 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 999;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { Button } from "@budibase/bbui"
|
||||||
|
import { ContextKey } from "./context"
|
||||||
|
|
||||||
|
export let cancelText = "Cancel"
|
||||||
|
export let confirmText = "Confirm"
|
||||||
|
export let showCancelButton = true
|
||||||
|
export let showConfirmButton = true
|
||||||
|
export let onConfirm
|
||||||
|
|
||||||
|
const modalContext = getContext(ContextKey)
|
||||||
|
let loading = false
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modalContext.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirm() {
|
||||||
|
loading = true
|
||||||
|
if (!onConfirm || (await onConfirm()) !== false) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
{#if showCancelButton}
|
||||||
|
<Button secondary on:click={hide}>{cancelText}</Button>
|
||||||
|
{/if}
|
||||||
|
{#if showConfirmButton}
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
{...$$restProps}
|
||||||
|
disabled={$$props.disabled || loading}
|
||||||
|
on:click={confirm}>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<h5>
|
||||||
|
<slot />
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1 @@
|
||||||
|
export const ContextKey = "budibase-modal"
|
|
@ -1,2 +1,5 @@
|
||||||
export { default as Modal } from "./Modal.svelte"
|
export { default as Modal } from "./Modal.svelte"
|
||||||
export { default as ModalContainer } from "./ModalContainer.svelte"
|
export { default as ModalContainer } from "./ModalContainer.svelte"
|
||||||
|
export { default as ModalTitle } from "./ModalTitle.svelte"
|
||||||
|
export { default as ModalFooter } from "./ModalFooter.svelte"
|
||||||
|
export { ContextKey } from "./context"
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
export function portal(node, targetNodeOrSelector) {
|
|
||||||
const targetNode =
|
|
||||||
typeof targetNodeOrSelector == "string"
|
|
||||||
? document.querySelector(targetNodeOrSelector)
|
|
||||||
: targetNodeOrSelector
|
|
||||||
const portalChildren = [...node.children]
|
|
||||||
targetNode.append(...portalChildren)
|
|
||||||
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
for (const portalChild of portalChildren) portalChild.remove()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,83 +6,68 @@
|
||||||
"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
|
"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
|
||||||
|
|
||||||
export let value = []
|
export let value = []
|
||||||
export let readonly = false
|
|
||||||
export let label
|
export let label
|
||||||
|
|
||||||
let placeholder = "Type to search"
|
let placeholder = "Type to search"
|
||||||
let input
|
|
||||||
let inputValue
|
|
||||||
let options = []
|
let options = []
|
||||||
let activeOption
|
|
||||||
let optionsVisible = false
|
let optionsVisible = false
|
||||||
let selected = {}
|
let selected = {}
|
||||||
let first = true
|
let first = true
|
||||||
let slot
|
let slot
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
slot.querySelectorAll("option").forEach(o => {
|
const domOptions = Array.from(slot.querySelectorAll("option"))
|
||||||
o.selected && !value.includes(o.value) && (value = [...value, o.value])
|
options = domOptions.map(option => ({
|
||||||
options = [...options, { value: o.value, name: o.textContent }]
|
value: option.value,
|
||||||
})
|
name: option.textContent,
|
||||||
value &&
|
}))
|
||||||
(selected = options.reduce(
|
if (value) {
|
||||||
(obj, op) =>
|
options.forEach(option => {
|
||||||
value.includes(op.value) ? { ...obj, [op.value]: op } : obj,
|
if (value.includes(option.value)) {
|
||||||
{}
|
selected[option.value] = option
|
||||||
))
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
first = false
|
first = false
|
||||||
})
|
})
|
||||||
|
|
||||||
$: if (!first) value = Object.values(selected).map(o => o.value)
|
// Keep value up to date with selected options
|
||||||
$: filtered = options.filter(o =>
|
$: {
|
||||||
inputValue ? o.name.toLowerCase().includes(inputValue.toLowerCase()) : o
|
if (!first) {
|
||||||
)
|
value = Object.values(selected).map(option => option.value)
|
||||||
$: if (
|
}
|
||||||
(activeOption && !filtered.includes(activeOption)) ||
|
}
|
||||||
(!activeOption && inputValue)
|
|
||||||
)
|
|
||||||
activeOption = filtered[0]
|
|
||||||
|
|
||||||
function add(token) {
|
function add(token) {
|
||||||
if (!readonly) selected[token.value] = token
|
selected[token.value] = token
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(value) {
|
function remove(value) {
|
||||||
if (!readonly) {
|
const { [value]: val, ...rest } = selected
|
||||||
const { [value]: val, ...rest } = selected
|
selected = rest
|
||||||
selected = rest
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAll() {
|
function removeAll() {
|
||||||
selected = []
|
selected = []
|
||||||
inputValue = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOptions(show) {
|
function showOptions(show) {
|
||||||
optionsVisible = show
|
optionsVisible = show
|
||||||
if (!show) {
|
|
||||||
activeOption = undefined
|
|
||||||
} else {
|
|
||||||
input.focus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBlur() {
|
function handleClick() {
|
||||||
showOptions(false)
|
showOptions(!optionsVisible)
|
||||||
}
|
|
||||||
|
|
||||||
function handleFocus() {
|
|
||||||
showOptions(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOptionMousedown(e) {
|
function handleOptionMousedown(e) {
|
||||||
const value = e.target.dataset.value
|
const value = e.target.dataset.value
|
||||||
|
if (value == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (selected[value]) {
|
if (selected[value]) {
|
||||||
remove(value)
|
remove(value)
|
||||||
} else {
|
} else {
|
||||||
add(options.filter(option => option.value === value)[0])
|
add(options.filter(option => option.value === value)[0])
|
||||||
input.focus()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -91,76 +76,52 @@
|
||||||
{#if label}
|
{#if label}
|
||||||
<Label extraSmall grey>{label}</Label>
|
<Label extraSmall grey>{label}</Label>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="multiselect" class:readonly>
|
<div class="multiselect">
|
||||||
<div class="tokens-wrapper">
|
<div class="tokens-wrapper">
|
||||||
<div class="tokens" class:showOptions>
|
<div
|
||||||
|
class="tokens"
|
||||||
|
class:optionsVisible
|
||||||
|
on:click|self={handleClick}
|
||||||
|
class:empty={!value || !value.length}>
|
||||||
{#each Object.values(selected) as option}
|
{#each Object.values(selected) as option}
|
||||||
<div class="token" data-id={option.value}>
|
<div class="token" data-id={option.value} on:click|self={handleClick}>
|
||||||
<span>{option.name}</span>
|
<span>{option.name}</span>
|
||||||
{#if !readonly}
|
<div
|
||||||
<div
|
class="token-remove"
|
||||||
class="token-remove"
|
title="Remove {option.name}"
|
||||||
title="Remove {option.name}"
|
on:click={() => remove(option.value)}>
|
||||||
on:click={() => remove(option.value)}>
|
<svg
|
||||||
<svg
|
class="icon-clear"
|
||||||
class="icon-clear"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
width="18"
|
||||||
width="18"
|
height="18"
|
||||||
height="18"
|
viewBox="0 0 24 24">
|
||||||
viewBox="0 0 24 24">
|
<path d={xPath} />
|
||||||
<path d={xPath} />
|
</svg>
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if !value || !value.length} {/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
|
||||||
{#if !readonly}
|
|
||||||
<input
|
|
||||||
autocomplete="off"
|
|
||||||
bind:value={inputValue}
|
|
||||||
bind:this={input}
|
|
||||||
on:blur={handleBlur}
|
|
||||||
on:focus={handleFocus}
|
|
||||||
{placeholder} />
|
|
||||||
<div
|
|
||||||
class="remove-all"
|
|
||||||
title="Remove All"
|
|
||||||
class:hidden={!Object.keys(selected).length}
|
|
||||||
on:click={removeAll}>
|
|
||||||
<svg
|
|
||||||
class="icon-clear"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24">
|
|
||||||
<path d={xPath} />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<select bind:this={slot} type="multiple" class="hidden">
|
<select bind:this={slot} type="multiple" class="hidden">
|
||||||
<slot />
|
<slot />
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{#if optionsVisible}
|
{#if optionsVisible}
|
||||||
|
<div class="options-overlay" on:click|self={() => showOptions(false)} />
|
||||||
<ul
|
<ul
|
||||||
class="options"
|
class="options"
|
||||||
transition:fly={{ duration: 200, y: 5 }}
|
transition:fly={{ duration: 200, y: 5 }}
|
||||||
on:mousedown|preventDefault={handleOptionMousedown}>
|
on:mousedown|preventDefault={handleOptionMousedown}>
|
||||||
{#each filtered as option}
|
{#each options as option}
|
||||||
<li
|
<li class:selected={selected[option.value]} data-value={option.value}>
|
||||||
class:selected={selected[option.value]}
|
|
||||||
class:active={activeOption === option}
|
|
||||||
data-value={option.value}>
|
|
||||||
{option.name}
|
{option.name}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
{#if !filtered.length && inputValue.length}
|
{#if !options.length}
|
||||||
<li>No results</li>
|
<li class="no-results">No results</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -175,7 +136,7 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.multiselect:not(.readonly):hover {
|
.multiselect:hover {
|
||||||
border-bottom-color: hsl(0, 0%, 50%);
|
border-bottom-color: hsl(0, 0%, 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +146,7 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tokens {
|
.tokens {
|
||||||
|
@ -194,6 +156,14 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 0;
|
width: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
border-radius: var(--border-radius-m);
|
||||||
|
padding: 0 var(--spacing-m) calc(var(--spacing-m) - var(--spacing-xs))
|
||||||
|
calc(var(--spacing-m) / 2);
|
||||||
|
border: var(--border-transparent);
|
||||||
|
}
|
||||||
|
.tokens:hover {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.tokens::after {
|
.tokens::after {
|
||||||
background: none repeat scroll 0 0 transparent;
|
background: none repeat scroll 0 0 transparent;
|
||||||
|
@ -206,74 +176,72 @@
|
||||||
transition: width 0.3s ease 0s, left 0.3s ease 0s;
|
transition: width 0.3s ease 0s, left 0.3s ease 0s;
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
.tokens.showOptions::after {
|
.tokens.optionsVisible {
|
||||||
|
border: var(--border-blue);
|
||||||
|
}
|
||||||
|
.tokens.empty {
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.tokens::after {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
.token {
|
.token {
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--grey-3);
|
background-color: white;
|
||||||
border-radius: var(--border-radius-l);
|
border-radius: var(--border-radius-l);
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0.25rem 0.5rem 0.25rem 0;
|
margin: calc(var(--spacing-m) - var(--spacing-xs)) 0 0
|
||||||
|
calc(var(--spacing-m) / 2);
|
||||||
max-height: 1.3rem;
|
max-height: 1.3rem;
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-xs) var(--spacing-s);
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.token:hover {
|
.token span {
|
||||||
background-color: var(--grey-4);
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.readonly .token {
|
.token-remove {
|
||||||
color: hsl(0, 0%, 40%);
|
|
||||||
}
|
|
||||||
.token-remove,
|
|
||||||
.remove-all {
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--grey-5);
|
background-color: var(--grey-4);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 1.25rem;
|
height: 1rem;
|
||||||
margin-left: 0.25rem;
|
width: 1rem;
|
||||||
min-width: 1.25rem;
|
margin: calc(-1 * var(--spacing-xs)) 0 calc(-1 * var(--spacing-xs))
|
||||||
|
var(--spacing-xs);
|
||||||
}
|
}
|
||||||
.token-remove:hover,
|
.token-remove:hover {
|
||||||
.remove-all:hover {
|
background-color: var(--grey-5);
|
||||||
background-color: var(--grey-6);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.actions > * {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
.actions > input {
|
|
||||||
border: none;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
line-height: 1.5rem;
|
|
||||||
outline: none;
|
|
||||||
background-color: transparent;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-clear path {
|
.icon-clear path {
|
||||||
fill: white;
|
fill: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.options-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
|
z-index: 2;
|
||||||
left: 0;
|
left: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
max-height: 70vh;
|
overflow-y: auto;
|
||||||
overflow: auto;
|
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 1px);
|
top: calc(100% + 1px);
|
||||||
|
@ -283,8 +251,8 @@
|
||||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.15);
|
||||||
margin-top: var(--spacing-xs);
|
margin-top: var(--spacing-xs);
|
||||||
padding: var(--spacing-s) 0;
|
padding: var(--spacing-s) 0;
|
||||||
z-index: 1;
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
max-height: 200px;
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
@ -299,6 +267,10 @@
|
||||||
li:not(.selected):hover {
|
li:not(.selected):hover {
|
||||||
background-color: var(--grey-1);
|
background-color: var(--grey-1);
|
||||||
}
|
}
|
||||||
|
li.no-results:hover {
|
||||||
|
background-color: white;
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { notificationStore } from "builderStore/store/notifications"
|
import { notificationStore } from "builderStore/store/notifications"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { fade } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
|
|
||||||
export let themes = {
|
export let themes = {
|
||||||
danger: "#E26D69",
|
danger: "#E26D69",
|
||||||
|
@ -24,36 +24,41 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="notifications">
|
<div class="notifications">
|
||||||
{#each $notificationStore.notifications as notification (notification.id)}
|
{#each $notificationStore.notifications as notification (notification.id)}
|
||||||
<li
|
<div
|
||||||
class="toast"
|
class="toast"
|
||||||
style="background: {themes[notification.type]};"
|
style="background: {themes[notification.type]};"
|
||||||
transition:fade>
|
transition:fly={{ y: -30 }}>
|
||||||
<div class="content">{notification.message}</div>
|
<div class="content">{notification.message}</div>
|
||||||
{#if notification.icon}
|
{#if notification.icon}
|
||||||
<i class={notification.icon} />
|
<i class={notification.icon} />
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.notifications {
|
.notifications {
|
||||||
width: 40vw;
|
|
||||||
list-style: none;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 10px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin-left: auto;
|
margin: 0 auto;
|
||||||
margin-right: auto;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
|
flex: 0 0 auto;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
/* The toasts now support being auto sized, so this static width could be removed */
|
||||||
|
width: 40vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|
|
@ -1,196 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import fsort from "fast-sort"
|
|
||||||
import getOr from "lodash/fp/getOr"
|
|
||||||
import { store, backendUiStore } from "builderStore"
|
|
||||||
import { Button, Icon } from "@budibase/bbui"
|
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
|
||||||
import LinkedRecord from "./LinkedRecord.svelte"
|
|
||||||
import AttachmentList from "./AttachmentList.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 ExportPopover from "./popovers/Export.svelte"
|
|
||||||
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
|
|
||||||
import EditRowPopover from "./popovers/EditRow.svelte"
|
|
||||||
import * as api from "./api"
|
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 10
|
|
||||||
// Internal headers we want to hide from the user
|
|
||||||
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
|
|
||||||
|
|
||||||
let modalOpen = false
|
|
||||||
let data = []
|
|
||||||
let headers = []
|
|
||||||
let currentPage = 0
|
|
||||||
let search
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (
|
|
||||||
$backendUiStore.selectedView &&
|
|
||||||
$backendUiStore.selectedView.name.startsWith("all_")
|
|
||||||
) {
|
|
||||||
api.fetchDataForView($backendUiStore.selectedView).then(records => {
|
|
||||||
data = records || []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: sort = $backendUiStore.sort
|
|
||||||
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
|
|
||||||
$: paginatedData = sorted
|
|
||||||
? sorted.slice(
|
|
||||||
currentPage * ITEMS_PER_PAGE,
|
|
||||||
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
|
|
||||||
)
|
|
||||||
: []
|
|
||||||
|
|
||||||
$: headers = Object.keys($backendUiStore.selectedModel.schema)
|
|
||||||
.sort()
|
|
||||||
.filter(id => !INTERNAL_HEADERS.includes(id))
|
|
||||||
|
|
||||||
$: schema = $backendUiStore.selectedModel.schema
|
|
||||||
$: modelView = {
|
|
||||||
schema: $backendUiStore.selectedModel.schema,
|
|
||||||
name: $backendUiStore.selectedView.name,
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div class="table-controls">
|
|
||||||
<h2 class="title">{$backendUiStore.selectedModel.name}</h2>
|
|
||||||
<div class="popovers">
|
|
||||||
<ColumnPopover />
|
|
||||||
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
|
|
||||||
<RowPopover />
|
|
||||||
<ViewPopover />
|
|
||||||
<ExportPopover view={modelView} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table class="bb-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="edit-header">
|
|
||||||
<div>Edit</div>
|
|
||||||
</th>
|
|
||||||
{#each headers as header}
|
|
||||||
<th>
|
|
||||||
<ColumnHeaderPopover
|
|
||||||
field={$backendUiStore.selectedModel.schema[header]} />
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#if paginatedData.length === 0}
|
|
||||||
<div class="no-data">No Data.</div>
|
|
||||||
{/if}
|
|
||||||
{#each paginatedData as row}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<EditRowPopover {row} />
|
|
||||||
</td>
|
|
||||||
{#each headers as header}
|
|
||||||
<td>
|
|
||||||
{#if schema[header].type === 'link'}
|
|
||||||
<LinkedRecord field={schema[header]} ids={row[header]} />
|
|
||||||
{:else if schema[header].type === 'attachment'}
|
|
||||||
<AttachmentList files={row[header] || []} />
|
|
||||||
{:else}{getOr('', header, row)}{/if}
|
|
||||||
</td>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<TablePagination
|
|
||||||
{data}
|
|
||||||
bind:currentPage
|
|
||||||
pageItemCount={paginatedData.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-header {
|
|
||||||
width: 100px;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-header:hover {
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
th:hover {
|
|
||||||
color: var(--blue);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
max-width: 200px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
border: 1px solid var(--grey-4);
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: pre;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-data {
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,56 +0,0 @@
|
||||||
<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"
|
|
||||||
import FilterPopover from "./popovers/Filter.svelte"
|
|
||||||
import ExportPopover from "./popovers/Export.svelte"
|
|
||||||
|
|
||||||
export let view = {}
|
|
||||||
|
|
||||||
let data = []
|
|
||||||
|
|
||||||
$: name = view.name
|
|
||||||
$: filters = view.filters
|
|
||||||
$: field = view.field
|
|
||||||
$: groupBy = view.groupBy
|
|
||||||
$: !name.startsWith("all_") && filters && fetchViewData(name, field, groupBy)
|
|
||||||
|
|
||||||
async function fetchViewData(name, field, groupBy) {
|
|
||||||
const params = new URLSearchParams()
|
|
||||||
|
|
||||||
if (field) {
|
|
||||||
params.set("field", field)
|
|
||||||
params.set("stats", true)
|
|
||||||
}
|
|
||||||
if (groupBy) params.set("group", groupBy)
|
|
||||||
|
|
||||||
let QUERY_VIEW_URL = `/api/views/${name}?${params}`
|
|
||||||
|
|
||||||
const response = await api.get(QUERY_VIEW_URL)
|
|
||||||
data = await response.json()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Table title={decodeURI(name)} schema={view.schema} {data}>
|
|
||||||
<FilterPopover {view} />
|
|
||||||
<CalculationPopover {view} />
|
|
||||||
{#if view.calculation}
|
|
||||||
<GroupByPopover {view} />
|
|
||||||
{/if}
|
|
||||||
<ExportPopover {view} />
|
|
||||||
</Table>
|
|
|
@ -1,71 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
TextButton,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Popover,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { backendUiStore } from "builderStore"
|
|
||||||
import { notifier } from "builderStore/store/notifications"
|
|
||||||
import api from "builderStore/api"
|
|
||||||
|
|
||||||
const FORMATS = [
|
|
||||||
{
|
|
||||||
name: "CSV",
|
|
||||||
key: "csv",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "JSON",
|
|
||||||
key: "json",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export let view
|
|
||||||
|
|
||||||
let anchor
|
|
||||||
let dropdown
|
|
||||||
let exportFormat
|
|
||||||
|
|
||||||
async function exportView() {
|
|
||||||
const response = await api.post(
|
|
||||||
`/api/views/export?format=${exportFormat}`,
|
|
||||||
view
|
|
||||||
)
|
|
||||||
const downloadInfo = await response.json()
|
|
||||||
window.location = downloadInfo.url
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
|
||||||
<TextButton text small on:click={dropdown.show}>
|
|
||||||
<Icon name="download" />
|
|
||||||
Export
|
|
||||||
</TextButton>
|
|
||||||
</div>
|
|
||||||
<Popover bind:this={dropdown} {anchor} align="left">
|
|
||||||
<h5>Export Format</h5>
|
|
||||||
<Select secondary thin bind:value={exportFormat}>
|
|
||||||
<option value={''}>Select an option</option>
|
|
||||||
{#each FORMATS as format}
|
|
||||||
<option value={format.key}>{format.name}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
<div class="button-group">
|
|
||||||
<Button secondary on:click={dropdown.hide}>Cancel</Button>
|
|
||||||
<Button primary on:click={exportView}>Export</Button>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
h5 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
margin-top: var(--spacing-l);
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,31 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import Modal from "./Modal.svelte"
|
import SettingsModal from "./SettingsModal.svelte"
|
||||||
import { SettingsIcon } from "components/common/Icons/"
|
import { SettingsIcon } from "components/common/Icons/"
|
||||||
import { getContext } from "svelte"
|
import { Modal } from "components/common/Modal"
|
||||||
import { isActive, goto, layout } from "@sveltech/routify"
|
|
||||||
|
|
||||||
// Handle create app modal
|
let modal
|
||||||
const { open, close } = getContext("simple-modal")
|
|
||||||
|
|
||||||
const showSettingsModal = () => {
|
|
||||||
open(
|
|
||||||
Modal,
|
|
||||||
{
|
|
||||||
name: "Placeholder App Name",
|
|
||||||
description: "This is a hardcoded description that needs to change",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
closeOnEsc: true,
|
|
||||||
styleContent: { padding: 0 },
|
|
||||||
closeOnOuterClick: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="topnavitemright settings" on:click={showSettingsModal}>
|
<span class="topnavitemright settings" on:click={modal.show}>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</span>
|
</span>
|
||||||
|
<Modal bind:this={modal} wide>
|
||||||
|
<SettingsModal />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
span:first-letter {
|
span:first-letter {
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { General, Users, DangerZone, APIKeys } from "./tabs"
|
import { General, Users, DangerZone, APIKeys } from "./tabs"
|
||||||
|
import { Switcher } from "@budibase/bbui"
|
||||||
|
import { ModalTitle } from "components/common/Modal"
|
||||||
|
|
||||||
import { Input, TextArea, Button, Switcher } from "@budibase/bbui"
|
|
||||||
import { SettingsIcon, CloseIcon } from "components/common/Icons/"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { post } from "builderStore/api"
|
|
||||||
|
|
||||||
export let name = ""
|
|
||||||
export let description = ""
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
title: "General",
|
title: "General",
|
||||||
|
@ -30,15 +25,16 @@
|
||||||
component: DangerZone,
|
component: DangerZone,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let value = "GENERAL"
|
let value = "GENERAL"
|
||||||
|
|
||||||
$: selectedTab = tabs.find(tab => tab.key === value).component
|
$: selectedTab = tabs.find(tab => tab.key === value).component
|
||||||
|
|
||||||
|
function hide() {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="heading">
|
<ModalTitle>Settings</ModalTitle>
|
||||||
<i class="ri-settings-2-fill" />
|
|
||||||
<h3>Settings</h3>
|
|
||||||
</div>
|
|
||||||
<Switcher headings={tabs} bind:value>
|
<Switcher headings={tabs} bind:value>
|
||||||
<svelte:component this={selectedTab} />
|
<svelte:component this={selectedTab} />
|
||||||
</Switcher>
|
</Switcher>
|
||||||
|
@ -46,7 +42,6 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
padding: 30px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-xl);
|
grid-gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
@ -54,21 +49,7 @@
|
||||||
/* Fix margin defined in BBUI as L rather than XL */
|
/* Fix margin defined in BBUI as L rather than XL */
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
.container :global(textarea) {
|
||||||
.heading {
|
min-height: 60px;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.heading h3 {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
color: var(--ink);
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.heading i {
|
|
||||||
margin-right: var(--spacing-m);
|
|
||||||
font-size: 28px;
|
|
||||||
color: var(--grey-6);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -2,6 +2,7 @@
|
||||||
import { params, goto } from "@sveltech/routify"
|
import { params, goto } from "@sveltech/routify"
|
||||||
import { Input, TextArea, Button, Body } from "@budibase/bbui"
|
import { Input, TextArea, Button, Body } from "@budibase/bbui"
|
||||||
import { del } from "builderStore/api"
|
import { del } from "builderStore/api"
|
||||||
|
import { ModalFooter } from "components/common/Modal"
|
||||||
|
|
||||||
let value = ""
|
let value = ""
|
||||||
let loading = false
|
let loading = false
|
||||||
|
@ -9,16 +10,9 @@
|
||||||
async function deleteApp() {
|
async function deleteApp() {
|
||||||
loading = true
|
loading = true
|
||||||
const id = $params.application
|
const id = $params.application
|
||||||
const res = await del(`/api/${id}`)
|
await del(`/api/${id}`)
|
||||||
const json = await res.json()
|
|
||||||
|
|
||||||
loading = false
|
loading = false
|
||||||
if (res.ok) {
|
$goto("/")
|
||||||
$goto("/")
|
|
||||||
return json
|
|
||||||
} else {
|
|
||||||
throw new Error(json)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -35,13 +29,12 @@
|
||||||
thin
|
thin
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
placeholder="" />
|
placeholder="" />
|
||||||
<Button
|
<ModalFooter
|
||||||
disabled={value !== 'DELETE' || loading}
|
disabled={value !== 'DELETE' || loading}
|
||||||
red
|
red
|
||||||
wide
|
showCancelButton={false}
|
||||||
on:click={deleteApp}>
|
confirmText="Delete Entire App"
|
||||||
Delete Entire Web App
|
onConfirm={deleteApp} />
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -53,7 +46,4 @@
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.background :global(button) {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,12 +11,10 @@
|
||||||
import { Input, TextArea, Button } from "@budibase/bbui"
|
import { Input, TextArea, Button } from "@budibase/bbui"
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
|
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { post } from "builderStore/api"
|
import { post } from "builderStore/api"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
|
||||||
//Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
|
//Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
|
||||||
const createAppStore = writable({ currentStep: 0, values: {} })
|
const createAppStore = writable({ currentStep: 0, values: {} })
|
||||||
|
|
||||||
|
@ -169,7 +167,7 @@
|
||||||
}
|
}
|
||||||
const userResp = await api.post(`/api/users`, user)
|
const userResp = await api.post(`/api/users`, user)
|
||||||
const json = await userResp.json()
|
const json = await userResp.json()
|
||||||
$goto(`./${appJson._id}`)
|
$goto(`/${appJson._id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
@ -194,10 +192,6 @@
|
||||||
|
|
||||||
let onChange = () => {}
|
let onChange = () => {}
|
||||||
|
|
||||||
function _onCancel() {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _onOkay() {
|
async function _onOkay() {
|
||||||
await createNewApp()
|
await createNewApp()
|
||||||
}
|
}
|
||||||
|
@ -249,9 +243,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="close-button" on:click={_onCancel}>
|
|
||||||
<CloseIcon />
|
|
||||||
</div>
|
|
||||||
<img src="/_builder/assets/bb-logo.svg" alt="budibase icon" />
|
<img src="/_builder/assets/bb-logo.svg" alt="budibase icon" />
|
||||||
{#if submitting}
|
{#if submitting}
|
||||||
<div in:fade class="spinner-container">
|
<div in:fade class="spinner-container">
|
||||||
|
@ -276,16 +267,6 @@
|
||||||
align-content: center;
|
align-content: center;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
.close-button {
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
}
|
|
||||||
.close-button :global(svg) {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
.heading {
|
.heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { MoreIcon } from "components/common/Icons"
|
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { getComponentDefinition } from "builderStore/storeUtils"
|
import { getComponentDefinition } from "builderStore/storeUtils"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
@ -109,9 +108,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor} on:click|stopPropagation={() => {}}>
|
<div bind:this={anchor} on:click|stopPropagation={() => {}}>
|
||||||
<button on:click={dropdown.show}>
|
<div class="icon" on:click={dropdown.show}>
|
||||||
<MoreIcon />
|
<i class="ri-more-line" />
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
class="menu"
|
class="menu"
|
||||||
|
@ -122,27 +121,27 @@
|
||||||
align="left">
|
align="left">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="item" on:click={() => confirmDeleteDialog.show()}>
|
<li class="item" on:click={() => confirmDeleteDialog.show()}>
|
||||||
<i class="icon ri-delete-bin-2-line" />
|
<i class="ri-delete-bin-2-line" />
|
||||||
Delete
|
Delete
|
||||||
</li>
|
</li>
|
||||||
<li class="item" on:click={moveUpComponent}>
|
<li class="item" on:click={moveUpComponent}>
|
||||||
<i class="icon ri-arrow-up-line" />
|
<i class="ri-arrow-up-line" />
|
||||||
Move up
|
Move up
|
||||||
</li>
|
</li>
|
||||||
<li class="item" on:click={moveDownComponent}>
|
<li class="item" on:click={moveDownComponent}>
|
||||||
<i class="icon ri-arrow-down-line" />
|
<i class="ri-arrow-down-line" />
|
||||||
Move down
|
Move down
|
||||||
</li>
|
</li>
|
||||||
<li class="item" on:click={copyComponent}>
|
<li class="item" on:click={copyComponent}>
|
||||||
<i class="icon ri-repeat-one-line" />
|
<i class="ri-repeat-one-line" />
|
||||||
Duplicate
|
Duplicate
|
||||||
</li>
|
</li>
|
||||||
<li class="item" on:click={() => storeComponentForCopy(true)}>
|
<li class="item" on:click={() => storeComponentForCopy(true)}>
|
||||||
<i class="icon ri-scissors-cut-line" />
|
<i class="ri-scissors-cut-line" />
|
||||||
Cut
|
Cut
|
||||||
</li>
|
</li>
|
||||||
<li class="item" on:click={() => storeComponentForCopy(false)}>
|
<li class="item" on:click={() => storeComponentForCopy(false)}>
|
||||||
<i class="icon ri-file-copy-line" />
|
<i class="ri-file-copy-line" />
|
||||||
Copy
|
Copy
|
||||||
</li>
|
</li>
|
||||||
<hr class="hr-style" />
|
<hr class="hr-style" />
|
||||||
|
@ -150,21 +149,21 @@
|
||||||
class="item"
|
class="item"
|
||||||
class:disabled={noPaste}
|
class:disabled={noPaste}
|
||||||
on:click={() => pasteComponent('above')}>
|
on:click={() => pasteComponent('above')}>
|
||||||
<i class="icon ri-insert-row-top" />
|
<i class="ri-insert-row-top" />
|
||||||
Paste above
|
Paste above
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class="item"
|
class="item"
|
||||||
class:disabled={noPaste}
|
class:disabled={noPaste}
|
||||||
on:click={() => pasteComponent('below')}>
|
on:click={() => pasteComponent('below')}>
|
||||||
<i class="icon ri-insert-row-bottom" />
|
<i class="ri-insert-row-bottom" />
|
||||||
Paste below
|
Paste below
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class="item"
|
class="item"
|
||||||
class:disabled={noPaste || noChildrenAllowed}
|
class:disabled={noPaste || noChildrenAllowed}
|
||||||
on:click={() => pasteComponent('inside')}>
|
on:click={() => pasteComponent('inside')}>
|
||||||
<i class="icon ri-insert-column-right" />
|
<i class="ri-insert-column-right" />
|
||||||
Paste inside
|
Paste inside
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -181,7 +180,7 @@
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: var(--spacing-xs) 0;
|
margin: var(--spacing-s) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
@ -190,44 +189,29 @@
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
margin: auto 0px;
|
margin: auto 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
li:not(.disabled):hover {
|
||||||
button {
|
|
||||||
border-style: none;
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--ink);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li:hover {
|
|
||||||
background-color: var(--grey-2);
|
background-color: var(--grey-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
li:active {
|
li:active {
|
||||||
color: var(--blue);
|
color: var(--blue);
|
||||||
}
|
}
|
||||||
|
li i {
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
|
li.disabled {
|
||||||
.disabled {
|
|
||||||
color: var(--grey-4);
|
color: var(--grey-4);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.hr-style {
|
.hr-style {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
color: var(--grey-4);
|
color: var(--grey-4);
|
||||||
|
|
|
@ -190,9 +190,9 @@
|
||||||
.item {
|
.item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto auto auto;
|
grid-template-columns: 1fr auto auto auto;
|
||||||
padding: 0px 5px 0px 15px;
|
padding: 0 var(--spacing-m);
|
||||||
margin: auto 0px;
|
margin: 0;
|
||||||
border-radius: 5px;
|
border-radius: var(--border-radius-m);
|
||||||
height: 36px;
|
height: 36px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -205,9 +205,6 @@
|
||||||
.actions {
|
.actions {
|
||||||
display: none;
|
display: none;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
padding: 0 5px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-style: none;
|
border-style: none;
|
||||||
background: rgba(0, 0, 0, 0);
|
background: rgba(0, 0, 0, 0);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -1,29 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { keys, map, includes, filter } from "lodash/fp"
|
||||||
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 EventEditorModal from "./EventEditorModal.svelte"
|
import EventEditorModal from "./EventEditorModal.svelte"
|
||||||
|
import { Modal } from "components/common/Modal"
|
||||||
import { PencilIcon } from "components/common/Icons"
|
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
|
||||||
|
|
||||||
export const EVENT_TYPE = "event"
|
export const EVENT_TYPE = "event"
|
||||||
|
|
||||||
export let component
|
export let component
|
||||||
|
|
||||||
let events = []
|
let events = []
|
||||||
let selectedEvent = null
|
let selectedEvent = null
|
||||||
|
let modal
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
events = Object.keys(component)
|
events = Object.keys(component)
|
||||||
|
@ -35,28 +20,9 @@
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle create app modal
|
|
||||||
const { open, close } = getContext("simple-modal")
|
|
||||||
|
|
||||||
const openModal = event => {
|
const openModal = event => {
|
||||||
selectedEvent = event
|
selectedEvent = event
|
||||||
open(
|
modal.show()
|
||||||
EventEditorModal,
|
|
||||||
{
|
|
||||||
eventOptions: events,
|
|
||||||
event: selectedEvent,
|
|
||||||
onClose: () => {
|
|
||||||
close()
|
|
||||||
selectedEvent = null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
closeButton: false,
|
|
||||||
closeOnEsc: false,
|
|
||||||
styleContent: { padding: 0 },
|
|
||||||
closeOnOuterClick: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -81,6 +47,13 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<EventEditorModal
|
||||||
|
eventOptions={events}
|
||||||
|
event={selectedEvent}
|
||||||
|
on:hide={() => (selectedEvent = null)} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.root {
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
} from "builderStore/replaceBindings"
|
} from "builderStore/replaceBindings"
|
||||||
import { DropdownMenu } from "@budibase/bbui"
|
import { DropdownMenu } from "@budibase/bbui"
|
||||||
import BindingDropdown from "components/userInterface/BindingDropdown.svelte"
|
import BindingDropdown from "components/userInterface/BindingDropdown.svelte"
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let label = ""
|
export let label = ""
|
||||||
export let componentInstance = {}
|
export let componentInstance = {}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { MoreIcon } from "components/common/Icons"
|
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
@ -10,7 +9,7 @@
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let dropdown
|
let dropdown
|
||||||
let buttonForDropdown
|
let anchor
|
||||||
|
|
||||||
const hideDropdown = () => {
|
const hideDropdown = () => {
|
||||||
dropdown.hide()
|
dropdown.hide()
|
||||||
|
@ -34,14 +33,17 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root boundary" on:click|stopPropagation={() => {}}>
|
<div
|
||||||
<button on:click={() => dropdown.show()} bind:this={buttonForDropdown}>
|
bind:this={anchor}
|
||||||
<MoreIcon />
|
class="root boundary"
|
||||||
</button>
|
on:click|stopPropagation={() => {}}>
|
||||||
<DropdownMenu bind:this={dropdown} anchor={buttonForDropdown}>
|
<div class="icon" on:click={() => dropdown.show()}>
|
||||||
<ul class="menu" on:click={hideDropdown}>
|
<i class="ri-more-line" />
|
||||||
<li class="item" on:click={() => confirmDeleteDialog.show()}>
|
</div>
|
||||||
<i class="icon ri-delete-bin-2-line" />
|
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
||||||
|
<ul on:click={hideDropdown}>
|
||||||
|
<li on:click={() => confirmDeleteDialog.show()}>
|
||||||
|
<i class="ri-delete-bin-2-line" />
|
||||||
Delete
|
Delete
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -71,40 +73,40 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
ul {
|
||||||
z-index: 100000;
|
z-index: 100000;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: var(--spacing-xs) 0;
|
margin: var(--spacing-s) 0;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu li {
|
li {
|
||||||
border-style: none;
|
|
||||||
background-color: transparent;
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 4px 16px;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
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 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
li:not(.disabled):hover {
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
}
|
||||||
|
li:active {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
li i {
|
||||||
|
margin-right: 8px;
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
|
li.disabled {
|
||||||
.icon {
|
color: var(--grey-4);
|
||||||
margin-right: 8px;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu li:not(.disabled) {
|
.icon i {
|
||||||
cursor: pointer;
|
font-size: 16px;
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu li:not(.disabled):hover {
|
|
||||||
color: var(--ink);
|
|
||||||
background-color: var(--grey-1);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import Modal from "svelte-simple-modal"
|
|
||||||
import { store, automationStore, backendUiStore } from "builderStore"
|
import { store, automationStore, backendUiStore } from "builderStore"
|
||||||
import SettingsLink from "components/settings/Link.svelte"
|
import SettingsLink from "components/settings/Link.svelte"
|
||||||
import { get } from "builderStore/api"
|
import { get } from "builderStore/api"
|
||||||
|
import { isActive, goto, layout } from "@sveltech/routify"
|
||||||
import { fade } from "svelte/transition"
|
import { PreviewIcon } from "components/common/Icons/"
|
||||||
import { isActive, goto, layout, url } from "@sveltech/routify"
|
|
||||||
|
|
||||||
import { SettingsIcon, PreviewIcon } from "components/common/Icons/"
|
|
||||||
|
|
||||||
// Get Package and set store
|
// Get Package and set store
|
||||||
export let application
|
export let application
|
||||||
|
@ -47,50 +43,46 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<div class="root">
|
||||||
<div class="root">
|
<div class="top-nav">
|
||||||
|
<div class="topleftnav">
|
||||||
|
<button class="home-logo">
|
||||||
|
<img
|
||||||
|
src="/_builder/assets/bb-logo.svg"
|
||||||
|
alt="budibase icon"
|
||||||
|
on:click={() => $goto(`/`)} />
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="top-nav">
|
<!-- This gets all indexable subroutes and sticks them in the top nav. -->
|
||||||
<div class="topleftnav">
|
{#each $layout.children as { path, title }}
|
||||||
<button class="home-logo">
|
|
||||||
<img
|
|
||||||
src="/_builder/assets/bb-logo.svg"
|
|
||||||
alt="budibase icon"
|
|
||||||
on:click={() => $goto(`/`)} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- This gets all indexable subroutes and sticks them in the top nav. -->
|
|
||||||
{#each $layout.children as { path, title }}
|
|
||||||
<span
|
|
||||||
class:active={$isActive(path)}
|
|
||||||
class="topnavitem"
|
|
||||||
on:click={topItemNavigate(path)}>
|
|
||||||
{title}
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<div class="toprightnav">
|
|
||||||
<SettingsLink />
|
|
||||||
<span
|
<span
|
||||||
class:active={false}
|
class:active={$isActive(path)}
|
||||||
class="topnavitemright"
|
class="topnavitem"
|
||||||
on:click={() => window.open(`/${application}`)}>
|
on:click={topItemNavigate(path)}>
|
||||||
<PreviewIcon />
|
{title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="toprightnav">
|
||||||
|
<SettingsLink />
|
||||||
|
<span
|
||||||
|
class:active={false}
|
||||||
|
class="topnavitemright"
|
||||||
|
on:click={() => window.open(`/${application}`)}>
|
||||||
|
<PreviewIcon />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#await promise}
|
|
||||||
<!-- This should probably be some kind of loading state? -->
|
|
||||||
<div />
|
|
||||||
{:then}
|
|
||||||
<slot />
|
|
||||||
{:catch error}
|
|
||||||
<p>Something went wrong: {error.message}</p>
|
|
||||||
{/await}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
|
{#await promise}
|
||||||
|
<!-- This should probably be some kind of loading state? -->
|
||||||
|
<div />
|
||||||
|
{:then _}
|
||||||
|
<slot />
|
||||||
|
{:catch error}
|
||||||
|
<p>Something went wrong: {error.message}</p>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.root {
|
||||||
|
@ -138,7 +130,7 @@
|
||||||
margin: 0px 00px 0px 20px;
|
margin: 0px 00px 0px 20px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-m);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -183,10 +175,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-logo:hover {
|
|
||||||
color: var(--hovercolor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-logo:active {
|
.home-logo:active {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
import * as api from "components/backend/DataTable/api"
|
import * as api from "components/backend/DataTable/api"
|
||||||
import ModelNavigator from "components/backend/ModelNavigator/ModelNavigator.svelte"
|
import ModelNavigator from "components/backend/ModelNavigator/ModelNavigator.svelte"
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import ModelDataTable from "components/backend/DataTable/ModelDataTable.svelte"
|
||||||
import { Button } from "@budibase/bbui"
|
|
||||||
import ModelDataTable from "components/backend/DataTable"
|
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
|
||||||
import * as api from "components/backend/DataTable/api"
|
|
||||||
import CreateEditRecordModal from "components/backend/DataTable/popovers/CreateEditRecord.svelte"
|
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
|
||||||
|
|
||||||
$: selectedModel = $backendUiStore.selectedModel
|
$: selectedModel = $backendUiStore.selectedModel
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { Button } from "@budibase/bbui"
|
|
||||||
import ViewDataTable from "components/backend/DataTable/ViewDataTable"
|
import ViewDataTable from "components/backend/DataTable/ViewDataTable"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
|
||||||
import * as api from "components/backend/DataTable/api"
|
|
||||||
import CreateEditRecord from "components/backend/DataTable/popovers/CreateEditRecord.svelte"
|
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
|
||||||
|
|
||||||
$: selectedView = $backendUiStore.selectedView
|
$: selectedView = $backendUiStore.selectedView
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import Modal from "svelte-simple-modal"
|
|
||||||
import { Home as Link } from "@budibase/bbui"
|
import { Home as Link } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
AppsIcon,
|
AppsIcon,
|
||||||
|
@ -7,43 +6,41 @@
|
||||||
DocumentationIcon,
|
DocumentationIcon,
|
||||||
CommunityIcon,
|
CommunityIcon,
|
||||||
BugIcon,
|
BugIcon,
|
||||||
} from "components/common/Icons/"
|
} from "components/common/Icons"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<div class="root">
|
||||||
<div class="root">
|
<div class="ui-nav">
|
||||||
<div class="ui-nav">
|
<div class="home-logo">
|
||||||
<div class="home-logo">
|
<img src="/_builder/assets/budibase-logo.svg" alt="Budibase icon" />
|
||||||
<img src="/_builder/assets/budibase-logo.svg" alt="Budibase icon" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-section">
|
|
||||||
<Link icon={AppsIcon} title="Apps" href="/" active />
|
|
||||||
<Link
|
|
||||||
icon={HostingIcon}
|
|
||||||
title="Hosting"
|
|
||||||
href="https://portal.budi.live/" />
|
|
||||||
<Link
|
|
||||||
icon={DocumentationIcon}
|
|
||||||
title="Documentation"
|
|
||||||
href="https://docs.budibase.com/" />
|
|
||||||
<Link
|
|
||||||
icon={CommunityIcon}
|
|
||||||
title="Community"
|
|
||||||
href="https://forum.budibase.com/" />
|
|
||||||
|
|
||||||
<Link
|
|
||||||
icon={BugIcon}
|
|
||||||
title="Raise an issue"
|
|
||||||
href="https://github.com/Budibase/budibase" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main">
|
<div class="nav-section">
|
||||||
<slot />
|
<Link icon={AppsIcon} title="Apps" href="/" active />
|
||||||
|
<Link
|
||||||
|
icon={HostingIcon}
|
||||||
|
title="Hosting"
|
||||||
|
href="https://portal.budi.live/" />
|
||||||
|
<Link
|
||||||
|
icon={DocumentationIcon}
|
||||||
|
title="Documentation"
|
||||||
|
href="https://docs.budibase.com/" />
|
||||||
|
<Link
|
||||||
|
icon={CommunityIcon}
|
||||||
|
title="Community"
|
||||||
|
href="https://forum.budibase.com/" />
|
||||||
|
|
||||||
|
<Link
|
||||||
|
icon={BugIcon}
|
||||||
|
title="Raise an issue"
|
||||||
|
href="https://github.com/Budibase/budibase" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
|
<div class="main">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root {
|
.root {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import AppList from "components/start/AppList.svelte"
|
import AppList from "components/start/AppList.svelte"
|
||||||
|
@ -10,8 +9,11 @@
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import { Button, Heading } from "@budibase/bbui"
|
import { Button, Heading } from "@budibase/bbui"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
import { Modal } from "components/common/Modal"
|
||||||
|
|
||||||
let promise = getApps()
|
let promise = getApps()
|
||||||
|
let hasKey
|
||||||
|
let modal
|
||||||
|
|
||||||
async function getApps() {
|
async function getApps() {
|
||||||
const res = await get("/api/applications")
|
const res = await get("/api/applications")
|
||||||
|
@ -24,8 +26,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasKey
|
|
||||||
|
|
||||||
async function fetchKeys() {
|
async function fetchKeys() {
|
||||||
const response = await api.get(`/api/keys/`)
|
const response = await api.get(`/api/keys/`)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
@ -40,37 +40,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!keys.budibase) {
|
if (!keys.budibase) {
|
||||||
showCreateAppModal()
|
modal.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle create app modal
|
|
||||||
const { open } = getContext("simple-modal")
|
|
||||||
|
|
||||||
const showCreateAppModal = () => {
|
|
||||||
open(
|
|
||||||
CreateAppModal,
|
|
||||||
{
|
|
||||||
hasKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
closeButton: false,
|
|
||||||
closeOnEsc: false,
|
|
||||||
closeOnOuterClick: false,
|
|
||||||
styleContent: { padding: 0 },
|
|
||||||
closeOnOuterClick: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfKeysAndApps()
|
checkIfKeysAndApps()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<Heading medium black>Welcome to the Budibase Beta</Heading>
|
<Heading medium black>Welcome to the Budibase Beta</Heading>
|
||||||
<Button primary black on:click={showCreateAppModal}>
|
<Button primary black on:click={modal.show}>Create New Web App</Button>
|
||||||
Create New Web App
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="banner">
|
<div class="banner">
|
||||||
|
@ -80,6 +59,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:this={modal} wide padded={false}>
|
||||||
|
<CreateAppModal {hasKey} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{#await promise}
|
{#await promise}
|
||||||
<div class="spinner-container">
|
<div class="spinner-container">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
|
|
@ -18,8 +18,8 @@ validateJs.extend(validateJs.validators.datetime, {
|
||||||
exports.patch = async function(ctx) {
|
exports.patch = async function(ctx) {
|
||||||
const instanceId = ctx.user.instanceId
|
const instanceId = ctx.user.instanceId
|
||||||
const db = new CouchDB(instanceId)
|
const db = new CouchDB(instanceId)
|
||||||
const model = await db.get(record.modelId)
|
|
||||||
let record = await db.get(ctx.params.id)
|
let record = await db.get(ctx.params.id)
|
||||||
|
const model = await db.get(record.modelId)
|
||||||
const patchfields = ctx.request.body
|
const patchfields = ctx.request.body
|
||||||
|
|
||||||
for (let key of Object.keys(patchfields)) {
|
for (let key of Object.keys(patchfields)) {
|
||||||
|
@ -159,7 +159,7 @@ exports.fetchModelRecords = async function(ctx) {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
ctx.body = response.rows.map(row => row.doc)
|
ctx.body = response.rows.map(row => row.doc)
|
||||||
ctx.body = await linkRecords.attachLinkInfo(
|
ctx.body = await linkRecords.attachLinkInfo(
|
||||||
instanceId,
|
instanceId,
|
||||||
response.rows.map(row => row.doc)
|
response.rows.map(row => row.doc)
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ const fs = require("fs")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
const os = require("os")
|
const os = require("os")
|
||||||
const exporters = require("./exporters")
|
const exporters = require("./exporters")
|
||||||
|
const { fetchView } = require("../record")
|
||||||
|
|
||||||
const controller = {
|
const controller = {
|
||||||
fetch: async ctx => {
|
fetch: async ctx => {
|
||||||
|
@ -12,6 +13,10 @@ const controller = {
|
||||||
const response = []
|
const response = []
|
||||||
|
|
||||||
for (let name of Object.keys(designDoc.views)) {
|
for (let name of Object.keys(designDoc.views)) {
|
||||||
|
// Only return custom views
|
||||||
|
if (name === "by_link") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
response.push({
|
response.push({
|
||||||
name,
|
name,
|
||||||
...designDoc.views[name],
|
...designDoc.views[name],
|
||||||
|
@ -77,36 +82,24 @@ const controller = {
|
||||||
ctx.message = `View ${ctx.params.viewName} saved successfully.`
|
ctx.message = `View ${ctx.params.viewName} saved successfully.`
|
||||||
},
|
},
|
||||||
exportView: async ctx => {
|
exportView: async ctx => {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
|
||||||
const view = ctx.request.body
|
const view = ctx.request.body
|
||||||
const format = ctx.query.format
|
const format = ctx.query.format
|
||||||
|
|
||||||
// fetch records for the view
|
// Fetch view records
|
||||||
const response = await db.query(`database/${view.name}`, {
|
ctx.params.viewName = view.name
|
||||||
include_docs: !view.calculation,
|
ctx.query.group = view.groupBy
|
||||||
group: view.groupBy,
|
if (view.field) {
|
||||||
})
|
ctx.query.stats = true
|
||||||
|
ctx.query.field = view.field
|
||||||
if (view.calculation === "stats") {
|
|
||||||
response.rows = response.rows.map(row => ({
|
|
||||||
group: row.key,
|
|
||||||
field: view.field,
|
|
||||||
...row.value,
|
|
||||||
avg: row.value.sum / row.value.count,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
response.rows = response.rows.map(row => row.doc)
|
|
||||||
}
|
}
|
||||||
|
await fetchView(ctx)
|
||||||
|
|
||||||
|
// Export part
|
||||||
let headers = Object.keys(view.schema)
|
let headers = Object.keys(view.schema)
|
||||||
|
|
||||||
const exporter = exporters[format]
|
const exporter = exporters[format]
|
||||||
const exportedFile = exporter(headers, response.rows)
|
const exportedFile = exporter(headers, ctx.body)
|
||||||
|
|
||||||
const filename = `${view.name}.${format}`
|
const filename = `${view.name}.${format}`
|
||||||
|
|
||||||
fs.writeFileSync(path.join(os.tmpdir(), filename), exportedFile)
|
fs.writeFileSync(path.join(os.tmpdir(), filename), exportedFile)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
url: `/api/views/export/download/${filename}`,
|
url: `/api/views/export/download/${filename}`,
|
||||||
name: view.name,
|
name: view.name,
|
||||||
|
|
Loading…
Reference in New Issue