Merge branch 'v3-ui' into feature/automation-branching-ux
This commit is contained in:
commit
bc406e14db
|
@ -1,20 +1,144 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton, Modal } from "@budibase/bbui"
|
import {
|
||||||
import ExportModal from "../modals/ExportModal.svelte"
|
ActionButton,
|
||||||
|
Select,
|
||||||
|
notifications,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import download from "downloadjs"
|
||||||
|
import { API } from "api"
|
||||||
|
import { ROW_EXPORT_FORMATS } from "constants/backend"
|
||||||
|
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||||
|
|
||||||
export let view
|
export let view
|
||||||
export let filters
|
|
||||||
export let sorting
|
export let sorting
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let selectedRows
|
export let selectedRows
|
||||||
export let formats
|
export let formats
|
||||||
|
|
||||||
let modal
|
const FORMATS = [
|
||||||
|
{
|
||||||
|
name: "CSV",
|
||||||
|
key: ROW_EXPORT_FORMATS.CSV,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JSON",
|
||||||
|
key: ROW_EXPORT_FORMATS.JSON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JSON with Schema",
|
||||||
|
key: ROW_EXPORT_FORMATS.JSON_WITH_SCHEMA,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
let popover
|
||||||
|
let exportFormat
|
||||||
|
let loading = false
|
||||||
|
|
||||||
|
$: options = FORMATS.filter(format => {
|
||||||
|
if (formats && !formats.includes(format.key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
$: if (options && !exportFormat) {
|
||||||
|
exportFormat = Array.isArray(options) ? options[0]?.key : []
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPopover = () => {
|
||||||
|
loading = false
|
||||||
|
popover.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadWithBlob(data, filename) {
|
||||||
|
download(new Blob([data], { type: "text/plain" }), filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportAllData = async () => {
|
||||||
|
return await API.exportView({
|
||||||
|
viewName: view,
|
||||||
|
format: exportFormat,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportFilteredData = async () => {
|
||||||
|
let payload = {
|
||||||
|
tableId: view,
|
||||||
|
format: exportFormat,
|
||||||
|
search: {
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (selectedRows?.length) {
|
||||||
|
payload.rows = selectedRows.map(row => row._id)
|
||||||
|
}
|
||||||
|
if (sorting) {
|
||||||
|
payload.search.sort = sorting.sortColumn
|
||||||
|
payload.search.sortOrder = sorting.sortOrder
|
||||||
|
}
|
||||||
|
return await API.exportRows(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportData = async () => {
|
||||||
|
try {
|
||||||
|
loading = true
|
||||||
|
let data
|
||||||
|
if (selectedRows?.length || sorting) {
|
||||||
|
data = await exportFilteredData()
|
||||||
|
} else {
|
||||||
|
data = await exportAllData()
|
||||||
|
}
|
||||||
|
notifications.success("Export successful")
|
||||||
|
downloadWithBlob(data, `export.${exportFormat}`)
|
||||||
|
popover.hide()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notifications.error("Error exporting data")
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton {disabled} icon="DataDownload" quiet on:click={modal.show}>
|
<DetailPopover title="Export data" bind:this={popover}>
|
||||||
|
<svelte:fragment slot="anchor" let:open>
|
||||||
|
<ActionButton
|
||||||
|
icon="DataDownload"
|
||||||
|
quiet
|
||||||
|
on:click={openPopover}
|
||||||
|
{disabled}
|
||||||
|
selected={open}
|
||||||
|
>
|
||||||
Export
|
Export
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
</svelte:fragment>
|
||||||
<ExportModal {view} {filters} {sorting} {selectedRows} {formats} />
|
|
||||||
</Modal>
|
{#if selectedRows?.length}
|
||||||
|
<Body size="S">
|
||||||
|
<span data-testid="exporting-n-rows">
|
||||||
|
<strong>{selectedRows?.length}</strong>
|
||||||
|
{`row${selectedRows?.length > 1 ? "s" : ""} will be exported.`}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
{:else}
|
||||||
|
<Body size="S">
|
||||||
|
<span data-testid="export-all-rows">
|
||||||
|
Exporting <strong>all</strong> rows.
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
<span data-testid="format-select">
|
||||||
|
<Select
|
||||||
|
label="Format"
|
||||||
|
bind:value={exportFormat}
|
||||||
|
{options}
|
||||||
|
placeholder={null}
|
||||||
|
getOptionLabel={x => x.name}
|
||||||
|
getOptionValue={x => x.key}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Button cta disabled={loading} on:click={exportData}>Export</Button>
|
||||||
|
</div>
|
||||||
|
</DetailPopover>
|
||||||
|
|
|
@ -1,17 +1,81 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton, Modal } from "@budibase/bbui"
|
import { ActionButton, Button, Body, notifications } from "@budibase/bbui"
|
||||||
import ImportModal from "../modals/ImportModal.svelte"
|
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||||
|
import ExistingTableDataImport from "components/backend/TableNavigator/ExistingTableDataImport.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
export let tableId
|
export let tableId
|
||||||
export let tableType
|
export let tableType
|
||||||
export let disabled
|
export let disabled
|
||||||
|
|
||||||
let modal
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let popover
|
||||||
|
let rows = []
|
||||||
|
let allValid = false
|
||||||
|
let displayColumn = null
|
||||||
|
let identifierFields = []
|
||||||
|
let loading = false
|
||||||
|
|
||||||
|
const openPopover = () => {
|
||||||
|
rows = []
|
||||||
|
allValid = false
|
||||||
|
displayColumn = null
|
||||||
|
identifierFields = []
|
||||||
|
loading = false
|
||||||
|
popover.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const importData = async () => {
|
||||||
|
try {
|
||||||
|
loading = true
|
||||||
|
await API.importTableData({
|
||||||
|
tableId,
|
||||||
|
rows,
|
||||||
|
identifierFields,
|
||||||
|
})
|
||||||
|
notifications.success("Rows successfully imported")
|
||||||
|
popover.hide()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notifications.error("Unable to import data")
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always refresh rows just to be sure
|
||||||
|
dispatch("importrows")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="DataUpload" quiet on:click={modal.show} {disabled}>
|
<DetailPopover title="Import data" bind:this={popover}>
|
||||||
|
<svelte:fragment slot="anchor" let:open>
|
||||||
|
<ActionButton
|
||||||
|
icon="DataUpload"
|
||||||
|
quiet
|
||||||
|
on:click={openPopover}
|
||||||
|
{disabled}
|
||||||
|
selected={open}
|
||||||
|
>
|
||||||
Import
|
Import
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
</svelte:fragment>
|
||||||
<ImportModal {tableId} {tableType} on:importrows />
|
<Body size="S">
|
||||||
</Modal>
|
Import rows to an existing table from a CSV or JSON file. Only columns from
|
||||||
|
the file which exist in the table will be imported.
|
||||||
|
</Body>
|
||||||
|
<ExistingTableDataImport
|
||||||
|
{tableId}
|
||||||
|
{tableType}
|
||||||
|
bind:rows
|
||||||
|
bind:allValid
|
||||||
|
bind:displayColumn
|
||||||
|
bind:identifierFields
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Button cta disabled={loading || !allValid} on:click={importData}>
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DetailPopover>
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { ActionButton, Drawer, DrawerContent, Button } from "@budibase/bbui"
|
import { ActionButton, Button } from "@budibase/bbui"
|
||||||
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||||
import { getUserBindings } from "dataBinding"
|
import { getUserBindings } from "dataBinding"
|
||||||
import { makePropSafe } from "@budibase/string-templates"
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { search } from "@budibase/frontend-core"
|
import { search } from "@budibase/frontend-core"
|
||||||
import { tables } from "stores/builder"
|
import { tables } from "stores/builder"
|
||||||
|
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let filters
|
export let filters
|
||||||
|
@ -14,7 +15,7 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let drawer
|
let popover
|
||||||
|
|
||||||
$: localFilters = filters
|
$: localFilters = filters
|
||||||
$: schemaFields = search.getFields(
|
$: schemaFields = search.getFields(
|
||||||
|
@ -39,39 +40,27 @@
|
||||||
},
|
},
|
||||||
...getUserBindings(),
|
...getUserBindings(),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const openPopover = () => {
|
||||||
|
localFilters = filters
|
||||||
|
popover.show()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton
|
<DetailPopover bind:this={popover} title="Configure filters" width={800}>
|
||||||
|
<svelte:fragment slot="anchor" let:open>
|
||||||
|
<ActionButton
|
||||||
icon="Filter"
|
icon="Filter"
|
||||||
quiet
|
quiet
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={drawer.show}
|
on:click={openPopover}
|
||||||
selected={filterCount > 0}
|
selected={open || filterCount > 0}
|
||||||
accentColor="#004EA6"
|
accentColor="#004EA6"
|
||||||
>
|
|
||||||
{filterCount ? `Filter: ${filterCount}` : "Filter"}
|
|
||||||
</ActionButton>
|
|
||||||
|
|
||||||
<Drawer
|
|
||||||
bind:this={drawer}
|
|
||||||
title="Filtering"
|
|
||||||
on:drawerHide
|
|
||||||
on:drawerShow={() => {
|
|
||||||
localFilters = filters
|
|
||||||
}}
|
|
||||||
forceModal
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
cta
|
|
||||||
slot="buttons"
|
|
||||||
on:click={() => {
|
|
||||||
dispatch("change", localFilters)
|
|
||||||
drawer.hide()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Save
|
{filterCount ? `Filter: ${filterCount}` : "Filter"}
|
||||||
</Button>
|
</ActionButton>
|
||||||
<DrawerContent slot="body">
|
</svelte:fragment>
|
||||||
|
|
||||||
<FilterBuilder
|
<FilterBuilder
|
||||||
filters={localFilters}
|
filters={localFilters}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
|
@ -79,5 +68,16 @@
|
||||||
on:change={e => (localFilters = e.detail)}
|
on:change={e => (localFilters = e.detail)}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
</DrawerContent>
|
<div>
|
||||||
</Drawer>
|
<Button
|
||||||
|
cta
|
||||||
|
slot="buttons"
|
||||||
|
on:click={() => {
|
||||||
|
dispatch("change", localFilters)
|
||||||
|
popover.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DetailPopover>
|
||||||
|
|
|
@ -210,6 +210,7 @@
|
||||||
anchor={relationshipPanelAnchor}
|
anchor={relationshipPanelAnchor}
|
||||||
align="left"
|
align="left"
|
||||||
>
|
>
|
||||||
|
<div class="nested">
|
||||||
{#if relationshipPanelColumns.length}
|
{#if relationshipPanelColumns.length}
|
||||||
<div class="relationship-header">
|
<div class="relationship-header">
|
||||||
{relationshipFieldName} columns
|
{relationshipFieldName} columns
|
||||||
|
@ -220,6 +221,7 @@
|
||||||
permissions={[FieldPermissions.READONLY, FieldPermissions.HIDDEN]}
|
permissions={[FieldPermissions.READONLY, FieldPermissions.HIDDEN]}
|
||||||
fromRelationshipField={relationshipField}
|
fromRelationshipField={relationshipField}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -230,11 +232,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 12px 12px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
.nested {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
.columns {
|
.columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -262,6 +266,6 @@
|
||||||
}
|
}
|
||||||
.relationship-header {
|
.relationship-header {
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
padding: 12px 12px 0 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,15 +8,15 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ActionButton, Popover } from "@budibase/bbui"
|
import { ActionButton } from "@budibase/bbui"
|
||||||
import ColumnsSettingContent from "./ColumnsSettingContent.svelte"
|
import ColumnsSettingContent from "./ColumnsSettingContent.svelte"
|
||||||
import { isEnabled } from "helpers/featureFlags"
|
import { isEnabled } from "helpers/featureFlags"
|
||||||
import { FeatureFlag } from "@budibase/types"
|
import { FeatureFlag } from "@budibase/types"
|
||||||
|
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||||
|
|
||||||
const { tableColumns, datasource } = getContext("grid")
|
const { tableColumns, datasource } = getContext("grid")
|
||||||
|
|
||||||
let open = false
|
let popover
|
||||||
let anchor
|
|
||||||
|
|
||||||
$: anyRestricted = $tableColumns.filter(
|
$: anyRestricted = $tableColumns.filter(
|
||||||
col => !col.visible || col.readonly
|
col => !col.visible || col.readonly
|
||||||
|
@ -32,24 +32,23 @@
|
||||||
: [FieldPermissions.WRITABLE, FieldPermissions.HIDDEN]
|
: [FieldPermissions.WRITABLE, FieldPermissions.HIDDEN]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<DetailPopover bind:this={popover} title="Column settings">
|
||||||
|
<svelte:fragment slot="anchor" let:open>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="ColumnSettings"
|
icon="ColumnSettings"
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => (open = !open)}
|
on:click={popover?.open}
|
||||||
selected={open || anyRestricted}
|
selected={open || anyRestricted}
|
||||||
disabled={!$tableColumns.length}
|
disabled={!$tableColumns.length}
|
||||||
accentColor="#674D00"
|
accentColor="#674D00"
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</svelte:fragment>
|
||||||
|
|
||||||
<Popover bind:open {anchor} align="left">
|
|
||||||
<ColumnsSettingContent
|
<ColumnsSettingContent
|
||||||
columns={$tableColumns}
|
columns={$tableColumns}
|
||||||
canSetRelationshipSchemas={isEnabled(FeatureFlag.ENRICHED_RELATIONSHIPS)}
|
canSetRelationshipSchemas={isEnabled(FeatureFlag.ENRICHED_RELATIONSHIPS)}
|
||||||
{permissions}
|
{permissions}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</DetailPopover>
|
||||||
|
|
|
@ -9,8 +9,7 @@
|
||||||
$: selectedRowArray = Object.keys($selectedRows).map(id => ({ _id: id }))
|
$: selectedRowArray = Object.keys($selectedRows).map(id => ({ _id: id }))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span data-ignore-click-outside="true">
|
<ExportButton
|
||||||
<ExportButton
|
|
||||||
{disabled}
|
{disabled}
|
||||||
view={$datasource.tableId}
|
view={$datasource.tableId}
|
||||||
filters={$filter}
|
filters={$filter}
|
||||||
|
@ -19,11 +18,4 @@
|
||||||
sortOrder: $sort.order,
|
sortOrder: $sort.order,
|
||||||
}}
|
}}
|
||||||
selectedRows={selectedRowArray}
|
selectedRows={selectedRowArray}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
span {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ActionButton, Popover, Label } from "@budibase/bbui"
|
import { ActionButton, Label } from "@budibase/bbui"
|
||||||
|
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
Constants,
|
Constants,
|
||||||
|
@ -32,8 +33,7 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let open = false
|
let popover
|
||||||
let anchor
|
|
||||||
|
|
||||||
// Column width sizes
|
// Column width sizes
|
||||||
$: allSmall = $columns.every(col => col.width === smallColSize)
|
$: allSmall = $columns.every(col => col.width === smallColSize)
|
||||||
|
@ -66,21 +66,19 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<DetailPopover bind:this={popover} title="Column and row size" width={300}>
|
||||||
|
<svelte:fragment slot="anchor" let:open>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="MoveUpDown"
|
icon="MoveUpDown"
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => (open = !open)}
|
on:click={popover?.open}
|
||||||
selected={open}
|
selected={open}
|
||||||
disabled={!$columns.length}
|
disabled={!$columns.length}
|
||||||
>
|
>
|
||||||
Size
|
Size
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</svelte:fragment>
|
||||||
|
|
||||||
<Popover bind:open {anchor} align="left">
|
|
||||||
<div class="content">
|
|
||||||
<div class="size">
|
<div class="size">
|
||||||
<Label>Row height</Label>
|
<Label>Row height</Label>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
|
@ -113,16 +111,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DetailPopover>
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.content {
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.size {
|
.size {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
import { ActionButton, Select } from "@budibase/bbui"
|
||||||
import { canBeSortColumn } from "@budibase/frontend-core"
|
import { canBeSortColumn } from "@budibase/frontend-core"
|
||||||
|
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||||
|
|
||||||
const { sort, columns } = getContext("grid")
|
const { sort, columns } = getContext("grid")
|
||||||
|
|
||||||
let open = false
|
let popover
|
||||||
let anchor
|
|
||||||
|
|
||||||
$: columnOptions = $columns
|
$: columnOptions = $columns
|
||||||
.filter(col => canBeSortColumn(col.schema))
|
.filter(col => canBeSortColumn(col.schema))
|
||||||
|
@ -45,21 +45,19 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<DetailPopover bind:this={popover} title="Sorting" width={300}>
|
||||||
|
<svelte:fragment slot="anchor" let:open>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="SortOrderDown"
|
icon="SortOrderDown"
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => (open = !open)}
|
on:click={popover?.open}
|
||||||
selected={open}
|
selected={open}
|
||||||
disabled={!columnOptions.length}
|
disabled={!columnOptions.length}
|
||||||
>
|
>
|
||||||
Sort
|
Sort
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</svelte:fragment>
|
||||||
|
|
||||||
<Popover bind:open {anchor} align="left">
|
|
||||||
<div class="content">
|
|
||||||
<Select
|
<Select
|
||||||
placeholder="Default"
|
placeholder="Default"
|
||||||
value={$sort.column}
|
value={$sort.column}
|
||||||
|
@ -78,17 +76,4 @@
|
||||||
label="Order"
|
label="Order"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</DetailPopover>
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
padding: 6px 12px 12px 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.content :global(.spectrum-Picker) {
|
|
||||||
width: 140px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
ActionButton,
|
ActionButton,
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
Select,
|
||||||
Icon,
|
Icon,
|
||||||
Multiselect,
|
Multiselect,
|
||||||
|
Button,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { CalculationType, canGroupBy, isNumeric } from "@budibase/types"
|
import { CalculationType, canGroupBy, isNumeric } from "@budibase/types"
|
||||||
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||||
|
|
||||||
const { definition, datasource, rows } = getContext("grid")
|
const { definition, datasource, rows } = getContext("grid")
|
||||||
const calculationTypeOptions = [
|
const calculationTypeOptions = [
|
||||||
|
@ -35,19 +35,20 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let modal
|
let popover
|
||||||
let calculations = []
|
let calculations = []
|
||||||
let groupBy = []
|
let groupBy = []
|
||||||
let schema = {}
|
let schema = {}
|
||||||
|
let loading = false
|
||||||
|
|
||||||
$: schema = $definition?.schema || {}
|
$: schema = $definition?.schema || {}
|
||||||
$: count = extractCalculations($definition?.schema || {}).length
|
$: count = extractCalculations($definition?.schema || {}).length
|
||||||
$: groupByOptions = getGroupByOptions(schema)
|
$: groupByOptions = getGroupByOptions(schema)
|
||||||
|
|
||||||
const open = () => {
|
const openPopover = () => {
|
||||||
calculations = extractCalculations(schema)
|
calculations = extractCalculations(schema)
|
||||||
groupBy = calculations.length ? extractGroupBy(schema) : []
|
groupBy = calculations.length ? extractGroupBy(schema) : []
|
||||||
modal?.show()
|
popover?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractCalculations = schema => {
|
const extractCalculations = schema => {
|
||||||
|
@ -132,6 +133,7 @@
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
let newSchema = {}
|
let newSchema = {}
|
||||||
|
loading = true
|
||||||
|
|
||||||
// Add calculations
|
// Add calculations
|
||||||
for (let calc of calculations) {
|
for (let calc of calculations) {
|
||||||
|
@ -165,26 +167,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save changes
|
// Save changes
|
||||||
|
try {
|
||||||
await datasource.actions.saveDefinition({
|
await datasource.actions.saveDefinition({
|
||||||
...$definition,
|
...$definition,
|
||||||
primaryDisplay,
|
primaryDisplay,
|
||||||
schema: newSchema,
|
schema: newSchema,
|
||||||
})
|
})
|
||||||
await rows.actions.refreshData()
|
await rows.actions.refreshData()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
popover.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="WebPage" quiet on:click={open}>
|
<DetailPopover bind:this={popover} title="Configure calculations" width={480}>
|
||||||
|
<svelte:fragment slot="anchor" let:open>
|
||||||
|
<ActionButton icon="WebPage" quiet on:click={openPopover} selected={open}>
|
||||||
Configure calculations{count ? `: ${count}` : ""}
|
Configure calculations{count ? `: ${count}` : ""}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<ModalContent
|
|
||||||
title="Calculations"
|
|
||||||
confirmText="Save"
|
|
||||||
size="M"
|
|
||||||
onConfirm={save}
|
|
||||||
>
|
|
||||||
{#if calculations.length}
|
{#if calculations.length}
|
||||||
<div class="calculations">
|
<div class="calculations">
|
||||||
{#each calculations as calc, idx}
|
{#each calculations as calc, idx}
|
||||||
|
@ -233,8 +236,11 @@
|
||||||
quiet
|
quiet
|
||||||
body="Calculations only work with numeric columns and a maximum of 5 calculations can be added at once."
|
body="Calculations only work with numeric columns and a maximum of 5 calculations can be added at once."
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
<div>
|
||||||
|
<Button cta on:click={save} disabled={loading}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</DetailPopover>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.calculations {
|
.calculations {
|
||||||
|
|
|
@ -1,224 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
ModalContent,
|
|
||||||
notifications,
|
|
||||||
Body,
|
|
||||||
Table,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import download from "downloadjs"
|
|
||||||
import { API } from "api"
|
|
||||||
import { QueryUtils } from "@budibase/frontend-core"
|
|
||||||
import { utils } from "@budibase/shared-core"
|
|
||||||
import { ROW_EXPORT_FORMATS } from "constants/backend"
|
|
||||||
|
|
||||||
export let view
|
|
||||||
export let filters
|
|
||||||
export let sorting
|
|
||||||
export let selectedRows = []
|
|
||||||
export let formats
|
|
||||||
|
|
||||||
const FORMATS = [
|
|
||||||
{
|
|
||||||
name: "CSV",
|
|
||||||
key: ROW_EXPORT_FORMATS.CSV,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "JSON",
|
|
||||||
key: ROW_EXPORT_FORMATS.JSON,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "JSON with Schema",
|
|
||||||
key: ROW_EXPORT_FORMATS.JSON_WITH_SCHEMA,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
$: appliedFilters = filters?.filter(filter => !filter.onEmptyFilter)
|
|
||||||
|
|
||||||
$: options = FORMATS.filter(format => {
|
|
||||||
if (formats && !formats.includes(format.key)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
let exportFormat
|
|
||||||
let filterLookup
|
|
||||||
|
|
||||||
$: if (options && !exportFormat) {
|
|
||||||
exportFormat = Array.isArray(options) ? options[0]?.key : []
|
|
||||||
}
|
|
||||||
|
|
||||||
$: query = QueryUtils.buildQuery(appliedFilters)
|
|
||||||
$: exportOpDisplay = buildExportOpDisplay(
|
|
||||||
sorting,
|
|
||||||
filterDisplay,
|
|
||||||
appliedFilters
|
|
||||||
)
|
|
||||||
|
|
||||||
filterLookup = utils.filterValueToLabel()
|
|
||||||
|
|
||||||
const filterDisplay = () => {
|
|
||||||
if (!appliedFilters) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return appliedFilters.map(filter => {
|
|
||||||
let newFieldName = filter.field + ""
|
|
||||||
const parts = newFieldName.split(":")
|
|
||||||
parts.shift()
|
|
||||||
newFieldName = parts.join(":")
|
|
||||||
return {
|
|
||||||
Field: newFieldName,
|
|
||||||
Operation: filterLookup[filter.operator],
|
|
||||||
"Field Value": filter.value || "",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildExportOpDisplay = (sorting, filterDisplay) => {
|
|
||||||
let filterDisplayConfig = filterDisplay()
|
|
||||||
if (sorting?.sortColumn) {
|
|
||||||
filterDisplayConfig = [
|
|
||||||
...filterDisplayConfig,
|
|
||||||
{
|
|
||||||
Field: sorting.sortColumn,
|
|
||||||
Operation: "Order By",
|
|
||||||
"Field Value": sorting.sortOrder,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return filterDisplayConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
const displaySchema = {
|
|
||||||
Field: {
|
|
||||||
type: "string",
|
|
||||||
fieldName: "Field",
|
|
||||||
},
|
|
||||||
Operation: {
|
|
||||||
type: "string",
|
|
||||||
fieldName: "Operation",
|
|
||||||
},
|
|
||||||
"Field Value": {
|
|
||||||
type: "string",
|
|
||||||
fieldName: "Value",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadWithBlob(data, filename) {
|
|
||||||
download(new Blob([data], { type: "text/plain" }), filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exportView() {
|
|
||||||
try {
|
|
||||||
const data = await API.exportView({
|
|
||||||
viewName: view,
|
|
||||||
format: exportFormat,
|
|
||||||
})
|
|
||||||
downloadWithBlob(
|
|
||||||
data,
|
|
||||||
`export.${exportFormat === "csv" ? "csv" : "json"}`
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exportRows() {
|
|
||||||
if (selectedRows?.length) {
|
|
||||||
const data = await API.exportRows({
|
|
||||||
tableId: view,
|
|
||||||
rows: selectedRows.map(row => row._id),
|
|
||||||
format: exportFormat,
|
|
||||||
})
|
|
||||||
downloadWithBlob(data, `export.${exportFormat}`)
|
|
||||||
} else if (appliedFilters || sorting) {
|
|
||||||
let response
|
|
||||||
try {
|
|
||||||
response = await API.exportRows({
|
|
||||||
tableId: view,
|
|
||||||
format: exportFormat,
|
|
||||||
search: {
|
|
||||||
query,
|
|
||||||
sort: sorting?.sortColumn,
|
|
||||||
sortOrder: sorting?.sortOrder,
|
|
||||||
paginate: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to export", e)
|
|
||||||
notifications.error("Export Failed")
|
|
||||||
}
|
|
||||||
if (response) {
|
|
||||||
downloadWithBlob(response, `export.${exportFormat}`)
|
|
||||||
notifications.success("Export Successful")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await exportView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
title="Export Data"
|
|
||||||
confirmText="Export"
|
|
||||||
onConfirm={exportRows}
|
|
||||||
size={appliedFilters?.length || sorting ? "M" : "S"}
|
|
||||||
>
|
|
||||||
{#if selectedRows?.length}
|
|
||||||
<Body size="S">
|
|
||||||
<span data-testid="exporting-n-rows">
|
|
||||||
<strong>{selectedRows?.length}</strong>
|
|
||||||
{`row${selectedRows?.length > 1 ? "s" : ""} will be exported`}
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
{:else if appliedFilters?.length || (sorting?.sortOrder && sorting?.sortColumn)}
|
|
||||||
<Body size="S">
|
|
||||||
{#if !appliedFilters}
|
|
||||||
<span data-testid="exporting-rows">
|
|
||||||
Exporting <strong>all</strong> rows
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span data-testid="filters-applied">Filters applied</span>
|
|
||||||
{/if}
|
|
||||||
</Body>
|
|
||||||
|
|
||||||
<div class="table-wrap" data-testid="export-config-table">
|
|
||||||
<Table
|
|
||||||
schema={displaySchema}
|
|
||||||
data={exportOpDisplay}
|
|
||||||
{appliedFilters}
|
|
||||||
loading={false}
|
|
||||||
rowCount={appliedFilters?.length + 1}
|
|
||||||
disableSorting={true}
|
|
||||||
allowSelectRows={false}
|
|
||||||
allowEditRows={false}
|
|
||||||
allowEditColumns={false}
|
|
||||||
quiet={true}
|
|
||||||
compact={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Body size="S">
|
|
||||||
<span data-testid="export-all-rows">
|
|
||||||
Exporting <strong>all</strong> rows
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
{/if}
|
|
||||||
<span data-testid="format-select">
|
|
||||||
<Select
|
|
||||||
label="Format"
|
|
||||||
bind:value={exportFormat}
|
|
||||||
{options}
|
|
||||||
placeholder={null}
|
|
||||||
getOptionLabel={x => x.name}
|
|
||||||
getOptionValue={x => x.key}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</ModalContent>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.table-wrap :global(.wrapper) {
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,243 +0,0 @@
|
||||||
import { it, expect, describe, vi } from "vitest"
|
|
||||||
import { render, screen } from "@testing-library/svelte"
|
|
||||||
import "@testing-library/jest-dom"
|
|
||||||
|
|
||||||
import ExportModal from "./ExportModal.svelte"
|
|
||||||
import { utils } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
const labelLookup = utils.filterValueToLabel()
|
|
||||||
|
|
||||||
const rowText = filter => {
|
|
||||||
let readableField = filter.field.split(":")[1]
|
|
||||||
let rowLabel = labelLookup[filter.operator]
|
|
||||||
let value = Array.isArray(filter.value)
|
|
||||||
? JSON.stringify(filter.value)
|
|
||||||
: filter.value
|
|
||||||
return `${readableField}${rowLabel}${value}`.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultFilters = [
|
|
||||||
{
|
|
||||||
onEmptyFilter: "all",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
vi.mock("svelte", async () => {
|
|
||||||
return {
|
|
||||||
getContext: () => {
|
|
||||||
return {
|
|
||||||
hide: vi.fn(),
|
|
||||||
cancel: vi.fn(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createEventDispatcher: vi.fn(),
|
|
||||||
onDestroy: vi.fn(),
|
|
||||||
tick: vi.fn(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock("api", async () => {
|
|
||||||
return {
|
|
||||||
API: {
|
|
||||||
exportView: vi.fn(),
|
|
||||||
exportRows: vi.fn(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Export Modal", () => {
|
|
||||||
it("show default messaging with no export config specified", () => {
|
|
||||||
render(ExportModal, {
|
|
||||||
props: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(screen.getByTestId("export-all-rows")).toBeVisible()
|
|
||||||
expect(screen.getByTestId("export-all-rows")).toHaveTextContent(
|
|
||||||
"Exporting all rows"
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(screen.queryByTestId("export-config-table")).toBe(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("indicate that a filter is being applied to the export", () => {
|
|
||||||
const propsCfg = {
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
id: "MOQkMx9p9",
|
|
||||||
field: "1:Cost",
|
|
||||||
operator: "rangeHigh",
|
|
||||||
value: "100",
|
|
||||||
valueType: "Value",
|
|
||||||
type: "number",
|
|
||||||
noValue: false,
|
|
||||||
},
|
|
||||||
...defaultFilters,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
render(ExportModal, {
|
|
||||||
props: propsCfg,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(propsCfg.filters[0].field).toBe("1:Cost")
|
|
||||||
|
|
||||||
expect(screen.getByTestId("filters-applied")).toBeVisible()
|
|
||||||
expect(screen.getByTestId("filters-applied").textContent).toBe(
|
|
||||||
"Filters applied"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ele = screen.queryByTestId("export-config-table")
|
|
||||||
expect(ele).toBeVisible()
|
|
||||||
|
|
||||||
const rows = ele.getElementsByClassName("spectrum-Table-row")
|
|
||||||
|
|
||||||
expect(rows.length).toBe(1)
|
|
||||||
let rowTextContent = rowText(propsCfg.filters[0])
|
|
||||||
|
|
||||||
//"CostLess than or equal to100"
|
|
||||||
expect(rows[0].textContent?.trim()).toEqual(rowTextContent)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Show only selected row messaging if rows are supplied", () => {
|
|
||||||
const propsCfg = {
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
id: "MOQkMx9p9",
|
|
||||||
field: "1:Cost",
|
|
||||||
operator: "rangeHigh",
|
|
||||||
value: "100",
|
|
||||||
valueType: "Value",
|
|
||||||
type: "number",
|
|
||||||
noValue: false,
|
|
||||||
},
|
|
||||||
...defaultFilters,
|
|
||||||
],
|
|
||||||
sorting: {
|
|
||||||
sortColumn: "Cost",
|
|
||||||
sortOrder: "descending",
|
|
||||||
},
|
|
||||||
selectedRows: [
|
|
||||||
{
|
|
||||||
_id: "ro_ta_bb_expenses_57d5f6fe1b6640d8bb22b15f5eae62cd",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "ro_ta_bb_expenses_99ce5760a53a430bab4349cd70335a07",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
render(ExportModal, {
|
|
||||||
props: propsCfg,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(screen.queryByTestId("export-config-table")).toBeNull()
|
|
||||||
expect(screen.queryByTestId("filters-applied")).toBeNull()
|
|
||||||
|
|
||||||
expect(screen.queryByTestId("exporting-n-rows")).toBeVisible()
|
|
||||||
expect(screen.queryByTestId("exporting-n-rows").textContent).toEqual(
|
|
||||||
"2 rows will be exported"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Show only the configured sort when no filters are specified", () => {
|
|
||||||
const propsCfg = {
|
|
||||||
filters: [...defaultFilters],
|
|
||||||
sorting: {
|
|
||||||
sortColumn: "Cost",
|
|
||||||
sortOrder: "descending",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
render(ExportModal, {
|
|
||||||
props: propsCfg,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(screen.queryByTestId("export-config-table")).toBeVisible()
|
|
||||||
const ele = screen.queryByTestId("export-config-table")
|
|
||||||
const rows = ele.getElementsByClassName("spectrum-Table-row")
|
|
||||||
|
|
||||||
expect(rows.length).toBe(1)
|
|
||||||
expect(rows[0].textContent?.trim()).toEqual(
|
|
||||||
`${propsCfg.sorting.sortColumn}Order By${propsCfg.sorting.sortOrder}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Display all currently configured filters and applied sort", () => {
|
|
||||||
const propsCfg = {
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
id: "MOQkMx9p9",
|
|
||||||
field: "1:Cost",
|
|
||||||
operator: "rangeHigh",
|
|
||||||
value: "100",
|
|
||||||
valueType: "Value",
|
|
||||||
type: "number",
|
|
||||||
noValue: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2ot-aB0gE",
|
|
||||||
field: "2:Expense Tags",
|
|
||||||
operator: "contains",
|
|
||||||
value: ["Equipment", "Services"],
|
|
||||||
valueType: "Value",
|
|
||||||
type: "array",
|
|
||||||
noValue: false,
|
|
||||||
},
|
|
||||||
...defaultFilters,
|
|
||||||
],
|
|
||||||
sorting: {
|
|
||||||
sortColumn: "Payment Due",
|
|
||||||
sortOrder: "ascending",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
render(ExportModal, {
|
|
||||||
props: propsCfg,
|
|
||||||
})
|
|
||||||
|
|
||||||
const ele = screen.queryByTestId("export-config-table")
|
|
||||||
expect(ele).toBeVisible()
|
|
||||||
|
|
||||||
const rows = ele.getElementsByClassName("spectrum-Table-row")
|
|
||||||
expect(rows.length).toBe(3)
|
|
||||||
|
|
||||||
let rowTextContent1 = rowText(propsCfg.filters[0])
|
|
||||||
expect(rows[0].textContent?.trim()).toEqual(rowTextContent1)
|
|
||||||
|
|
||||||
let rowTextContent2 = rowText(propsCfg.filters[1])
|
|
||||||
expect(rows[1].textContent?.trim()).toEqual(rowTextContent2)
|
|
||||||
|
|
||||||
expect(rows[2].textContent?.trim()).toEqual(
|
|
||||||
`${propsCfg.sorting.sortColumn}Order By${propsCfg.sorting.sortOrder}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("show only the valid, configured download formats", () => {
|
|
||||||
const propsCfg = {
|
|
||||||
formats: ["badger", "json"],
|
|
||||||
}
|
|
||||||
|
|
||||||
render(ExportModal, {
|
|
||||||
props: propsCfg,
|
|
||||||
})
|
|
||||||
|
|
||||||
let ele = screen.getByTestId("format-select")
|
|
||||||
expect(ele).toBeVisible()
|
|
||||||
|
|
||||||
let formatDisplay = ele.getElementsByTagName("button")[0]
|
|
||||||
|
|
||||||
expect(formatDisplay.textContent.trim()).toBe("JSON")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Load the default format config when no explicit formats are configured", () => {
|
|
||||||
render(ExportModal, {
|
|
||||||
props: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
let ele = screen.getByTestId("format-select")
|
|
||||||
expect(ele).toBeVisible()
|
|
||||||
|
|
||||||
let formatDisplay = ele.getElementsByTagName("button")[0]
|
|
||||||
|
|
||||||
expect(formatDisplay.textContent.trim()).toBe("CSV")
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,61 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
ModalContent,
|
|
||||||
Label,
|
|
||||||
notifications,
|
|
||||||
Body,
|
|
||||||
Layout,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import TableDataImport from "../../TableNavigator/ExistingTableDataImport.svelte"
|
|
||||||
import { API } from "api"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
export let tableId
|
|
||||||
export let tableType
|
|
||||||
|
|
||||||
let rows = []
|
|
||||||
let allValid = false
|
|
||||||
let displayColumn = null
|
|
||||||
let identifierFields = []
|
|
||||||
|
|
||||||
async function importData() {
|
|
||||||
try {
|
|
||||||
await API.importTableData({
|
|
||||||
tableId,
|
|
||||||
rows,
|
|
||||||
identifierFields,
|
|
||||||
})
|
|
||||||
notifications.success("Rows successfully imported")
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Unable to import data")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always refresh rows just to be sure
|
|
||||||
dispatch("importrows")
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
title="Import Data"
|
|
||||||
confirmText="Import"
|
|
||||||
onConfirm={importData}
|
|
||||||
disabled={!allValid}
|
|
||||||
>
|
|
||||||
<Body size="S">
|
|
||||||
Import rows to an existing table from a CSV or JSON file. Only columns from
|
|
||||||
the file which exist in the table will be imported.
|
|
||||||
</Body>
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Label grey extraSmall>CSV or JSON file to import</Label>
|
|
||||||
<TableDataImport
|
|
||||||
{tableId}
|
|
||||||
{tableType}
|
|
||||||
bind:rows
|
|
||||||
bind:allValid
|
|
||||||
bind:displayColumn
|
|
||||||
bind:identifierFields
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
</ModalContent>
|
|
|
@ -4,7 +4,7 @@
|
||||||
BBReferenceFieldSubType,
|
BBReferenceFieldSubType,
|
||||||
SourceName,
|
SourceName,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { Select, Toggle, Multiselect } from "@budibase/bbui"
|
import { Select, Toggle, Multiselect, Label, Layout } from "@budibase/bbui"
|
||||||
import { DB_TYPE_INTERNAL } from "constants/backend"
|
import { DB_TYPE_INTERNAL } from "constants/backend"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { parseFile } from "./utils"
|
import { parseFile } from "./utils"
|
||||||
|
@ -140,7 +140,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dropzone">
|
<Layout gap="S" noPadding>
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Label grey extraSmall>CSV or JSON file to import</Label>
|
||||||
|
<div class="dropzone">
|
||||||
<input
|
<input
|
||||||
disabled={!schema || loading}
|
disabled={!schema || loading}
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
|
@ -159,11 +162,13 @@
|
||||||
Upload
|
Upload
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{#if fileName && Object.keys(validation).length === 0}
|
</Layout>
|
||||||
<p>No valid fields, try another file</p>
|
|
||||||
{:else if rows.length > 0 && !error}
|
{#if fileName && Object.keys(validation).length === 0}
|
||||||
<div class="schema-fields">
|
<div>No valid fields - please try another file.</div>
|
||||||
|
{:else if fileName && rows.length > 0 && !error}
|
||||||
|
<div>
|
||||||
{#each Object.keys(validation) as name}
|
{#each Object.keys(validation) as name}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
|
@ -185,7 +190,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<br />
|
|
||||||
<!-- SQL Server doesn't yet support overwriting rows by existing keys -->
|
<!-- SQL Server doesn't yet support overwriting rows by existing keys -->
|
||||||
{#if datasource?.source !== SourceName.SQL_SERVER}
|
{#if datasource?.source !== SourceName.SQL_SERVER}
|
||||||
<Toggle
|
<Toggle
|
||||||
|
@ -203,21 +207,24 @@
|
||||||
bind:value={identifierFields}
|
bind:value={identifierFields}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Rows will be updated based on the table's primary key.</p>
|
<div>Rows will be updated based on the table's primary key.</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if invalidColumns.length > 0}
|
{#if invalidColumns.length > 0}
|
||||||
<p class="spectrum-FieldLabel spectrum-FieldLabel--sizeM">
|
<Layout noPadding gap="XS">
|
||||||
The following columns are present in the data you wish to import, but do
|
<div>
|
||||||
not match the schema of this table and will be ignored.
|
The following columns are present in the data you wish to import, but
|
||||||
</p>
|
do not match the schema of this table and will be ignored:
|
||||||
<ul class="ignoredList">
|
</div>
|
||||||
|
<div>
|
||||||
{#each invalidColumns as column}
|
{#each invalidColumns as column}
|
||||||
<li>{column}</li>
|
- {column}<br />
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</div>
|
||||||
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dropzone {
|
.dropzone {
|
||||||
|
@ -228,11 +235,9 @@
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -240,7 +245,6 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
color: var(--ink);
|
|
||||||
padding: var(--spacing-m) var(--spacing-l);
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
transition: all 0.2s ease 0s;
|
transition: all 0.2s ease 0s;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -254,20 +258,14 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--grey-2);
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-s);
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
border: var(--border-transparent);
|
border: var(--border-transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploaded {
|
.uploaded {
|
||||||
color: var(--blue);
|
color: var(--spectrum-global-color-blue-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.schema-fields {
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 2fr 1fr auto;
|
grid-template-columns: 2fr 2fr 1fr auto;
|
||||||
|
@ -276,23 +274,14 @@
|
||||||
grid-gap: var(--spacing-m);
|
grid-gap: var(--spacing-m);
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fieldStatusSuccess {
|
.fieldStatusSuccess {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fieldStatusFailure {
|
.fieldStatusFailure {
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ignoredList {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Icon } from "@budibase/bbui"
|
import { Select, Icon, Layout, Label } from "@budibase/bbui"
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import { canBeDisplayColumn } from "@budibase/frontend-core"
|
import { canBeDisplayColumn } from "@budibase/frontend-core"
|
||||||
|
@ -184,7 +184,12 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dropzone">
|
<Layout noPadding gap="S">
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Label grey extraSmall>
|
||||||
|
Create a Table from a CSV or JSON file (Optional)
|
||||||
|
</Label>
|
||||||
|
<div class="dropzone">
|
||||||
<input
|
<input
|
||||||
bind:this={fileInput}
|
bind:this={fileInput}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
@ -202,9 +207,11 @@
|
||||||
Upload
|
Upload
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{#if rawRows.length > 0 && !error}
|
</Layout>
|
||||||
<div class="schema-fields">
|
|
||||||
|
{#if rawRows.length > 0 && !error}
|
||||||
|
<div>
|
||||||
{#each Object.entries(schema) as [name, column]}
|
{#each Object.entries(schema) as [name, column]}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span>{column.name}</span>
|
<span>{column.name}</span>
|
||||||
|
@ -239,15 +246,14 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="display-column">
|
|
||||||
<Select
|
<Select
|
||||||
label="Display Column"
|
label="Display Column"
|
||||||
bind:value={displayColumn}
|
bind:value={displayColumn}
|
||||||
options={displayColumnOptions}
|
options={displayColumnOptions}
|
||||||
sort
|
sort
|
||||||
/>
|
/>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dropzone {
|
.dropzone {
|
||||||
|
@ -269,7 +275,6 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
color: var(--ink);
|
|
||||||
padding: var(--spacing-m) var(--spacing-l);
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
transition: all 0.2s ease 0s;
|
transition: all 0.2s ease 0s;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -283,20 +288,14 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--grey-2);
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-s);
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
border: var(--border-transparent);
|
border: var(--border-transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploaded {
|
.uploaded {
|
||||||
color: var(--blue);
|
color: var(--spectrum-global-color-blue-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.schema-fields {
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 2fr 1fr auto;
|
grid-template-columns: 2fr 2fr 1fr auto;
|
||||||
|
@ -322,8 +321,4 @@
|
||||||
.fieldStatusFailure :global(.spectrum-Icon) {
|
.fieldStatusFailure :global(.spectrum-Icon) {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-column {
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, url } from "@roxi/routify"
|
import { goto, url } from "@roxi/routify"
|
||||||
import { tables, datasources } from "stores/builder"
|
import { tables, datasources } from "stores/builder"
|
||||||
import {
|
import { notifications, Input, ModalContent } from "@budibase/bbui"
|
||||||
notifications,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
ModalContent,
|
|
||||||
Layout,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import TableDataImport from "../TableDataImport.svelte"
|
import TableDataImport from "../TableDataImport.svelte"
|
||||||
import {
|
import {
|
||||||
BUDIBASE_INTERNAL_DB_ID,
|
BUDIBASE_INTERNAL_DB_ID,
|
||||||
|
@ -101,11 +95,6 @@
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
{error}
|
{error}
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Label grey extraSmall
|
|
||||||
>Create a Table from a CSV or JSON file (Optional)</Label
|
|
||||||
>
|
|
||||||
<TableDataImport
|
<TableDataImport
|
||||||
{promptUpload}
|
{promptUpload}
|
||||||
bind:rows
|
bind:rows
|
||||||
|
@ -113,6 +102,4 @@
|
||||||
bind:allValid
|
bind:allValid
|
||||||
bind:displayColumn
|
bind:displayColumn
|
||||||
/>
|
/>
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
{showPopover}
|
{showPopover}
|
||||||
on:open
|
on:open
|
||||||
on:close
|
on:close
|
||||||
|
customZindex={100}
|
||||||
>
|
>
|
||||||
<div class="detail-popover">
|
<div class="detail-popover">
|
||||||
<div class="detail-popover__header">
|
<div class="detail-popover__header">
|
||||||
|
|
|
@ -60,10 +60,10 @@
|
||||||
buttonsCollapsed
|
buttonsCollapsed
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="controls">
|
<svelte:fragment slot="controls">
|
||||||
|
<GridManageAccessButton />
|
||||||
{#if calculation}
|
{#if calculation}
|
||||||
<GridViewCalculationButton />
|
<GridViewCalculationButton />
|
||||||
{/if}
|
{/if}
|
||||||
<GridManageAccessButton />
|
|
||||||
<GridFilterButton />
|
<GridFilterButton />
|
||||||
<GridSortButton />
|
<GridSortButton />
|
||||||
<GridSizeButton />
|
<GridSizeButton />
|
||||||
|
|
|
@ -4298,6 +4298,97 @@ describe.each([
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "can handle logical operator any",
|
||||||
|
insert: [{ string: "bar" }, { string: "foo" }],
|
||||||
|
query: {
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
logicalOperator: UILogicalOperator.ANY,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "string",
|
||||||
|
value: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "string",
|
||||||
|
value: "bar",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
searchOpts: {
|
||||||
|
sort: "string",
|
||||||
|
sortOrder: SortOrder.ASCENDING,
|
||||||
|
},
|
||||||
|
expected: [{ string: "bar" }, { string: "foo" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "can handle logical operator all",
|
||||||
|
insert: [
|
||||||
|
{ string: "bar", number: 1 },
|
||||||
|
{ string: "foo", number: 2 },
|
||||||
|
],
|
||||||
|
query: {
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "string",
|
||||||
|
value: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "number",
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
searchOpts: {
|
||||||
|
sort: "string",
|
||||||
|
sortOrder: SortOrder.ASCENDING,
|
||||||
|
},
|
||||||
|
expected: [{ string: "foo", number: 2 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overrides allOr with logical operators",
|
||||||
|
insert: [
|
||||||
|
{ string: "bar", number: 1 },
|
||||||
|
{ string: "foo", number: 1 },
|
||||||
|
],
|
||||||
|
query: {
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
|
filters: [
|
||||||
|
{ operator: "allOr" },
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "string",
|
||||||
|
value: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "number",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
searchOpts: {
|
||||||
|
sort: "string",
|
||||||
|
sortOrder: SortOrder.ASCENDING,
|
||||||
|
},
|
||||||
|
expected: [{ string: "foo", number: 1 }],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
it.each(testCases)(
|
it.each(testCases)(
|
||||||
|
|
|
@ -488,7 +488,13 @@ export function buildQuery(
|
||||||
if (onEmptyFilter) {
|
if (onEmptyFilter) {
|
||||||
query.onEmptyFilter = onEmptyFilter
|
query.onEmptyFilter = onEmptyFilter
|
||||||
}
|
}
|
||||||
const operator = allOr ? LogicalOperator.OR : LogicalOperator.AND
|
|
||||||
|
// logicalOperator takes precendence over allOr
|
||||||
|
let operator = allOr ? LogicalOperator.OR : LogicalOperator.AND
|
||||||
|
if (group.logicalOperator) {
|
||||||
|
operator = logicalOperatorFromUI(group.logicalOperator)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[operator]: { conditions: filters.map(buildCondition).filter(f => f) },
|
[operator]: { conditions: filters.map(buildCondition).filter(f => f) },
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue