Merge branch 'master' into chore/lint_imports
This commit is contained in:
commit
16af6ce352
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.13.11",
|
||||
"version": "2.13.12",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
} from "@budibase/bbui"
|
||||
import download from "downloadjs"
|
||||
import { API } from "api"
|
||||
import { Constants, LuceneUtils } from "@budibase/frontend-core"
|
||||
import { LuceneUtils } from "@budibase/frontend-core"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { ROW_EXPORT_FORMATS } from "constants/backend"
|
||||
|
||||
export let view
|
||||
|
@ -32,6 +33,8 @@
|
|||
},
|
||||
]
|
||||
|
||||
$: appliedFilters = filters?.filter(filter => !filter.onEmptyFilter)
|
||||
|
||||
$: options = FORMATS.filter(format => {
|
||||
if (formats && !formats.includes(format.key)) {
|
||||
return false
|
||||
|
@ -46,23 +49,20 @@
|
|||
exportFormat = Array.isArray(options) ? options[0]?.key : []
|
||||
}
|
||||
|
||||
$: luceneFilter = LuceneUtils.buildLuceneQuery(filters)
|
||||
$: exportOpDisplay = buildExportOpDisplay(sorting, filterDisplay, filters)
|
||||
$: luceneFilter = LuceneUtils.buildLuceneQuery(appliedFilters)
|
||||
$: exportOpDisplay = buildExportOpDisplay(
|
||||
sorting,
|
||||
filterDisplay,
|
||||
appliedFilters
|
||||
)
|
||||
|
||||
const buildFilterLookup = () => {
|
||||
return Object.keys(Constants.OperatorOptions).reduce((acc, key) => {
|
||||
const op = Constants.OperatorOptions[key]
|
||||
acc[op.value] = op.label
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
filterLookup = buildFilterLookup()
|
||||
filterLookup = utils.filterValueToLabel()
|
||||
|
||||
const filterDisplay = () => {
|
||||
if (!filters) {
|
||||
if (!appliedFilters) {
|
||||
return []
|
||||
}
|
||||
return filters.map(filter => {
|
||||
return appliedFilters.map(filter => {
|
||||
let newFieldName = filter.field + ""
|
||||
const parts = newFieldName.split(":")
|
||||
parts.shift()
|
||||
|
@ -77,7 +77,7 @@
|
|||
|
||||
const buildExportOpDisplay = (sorting, filterDisplay) => {
|
||||
let filterDisplayConfig = filterDisplay()
|
||||
if (sorting) {
|
||||
if (sorting?.sortColumn) {
|
||||
filterDisplayConfig = [
|
||||
...filterDisplayConfig,
|
||||
{
|
||||
|
@ -132,7 +132,7 @@
|
|||
format: exportFormat,
|
||||
})
|
||||
downloadWithBlob(data, `export.${exportFormat}`)
|
||||
} else if (filters || sorting) {
|
||||
} else if (appliedFilters || sorting) {
|
||||
let response
|
||||
try {
|
||||
response = await API.exportRows({
|
||||
|
@ -163,29 +163,33 @@
|
|||
title="Export Data"
|
||||
confirmText="Export"
|
||||
onConfirm={exportRows}
|
||||
size={filters?.length || sorting ? "M" : "S"}
|
||||
size={appliedFilters?.length || sorting ? "M" : "S"}
|
||||
>
|
||||
{#if selectedRows?.length}
|
||||
<Body size="S">
|
||||
<strong>{selectedRows?.length}</strong>
|
||||
{`row${selectedRows?.length > 1 ? "s" : ""} will be exported`}
|
||||
<span data-testid="exporting-n-rows">
|
||||
<strong>{selectedRows?.length}</strong>
|
||||
{`row${selectedRows?.length > 1 ? "s" : ""} will be exported`}
|
||||
</span>
|
||||
</Body>
|
||||
{:else if filters || (sorting?.sortOrder && sorting?.sortColumn)}
|
||||
{:else if appliedFilters?.length || (sorting?.sortOrder && sorting?.sortColumn)}
|
||||
<Body size="S">
|
||||
{#if !filters}
|
||||
Exporting <strong>all</strong> rows
|
||||
{#if !appliedFilters}
|
||||
<span data-testid="exporting-rows">
|
||||
Exporting <strong>all</strong> rows
|
||||
</span>
|
||||
{:else}
|
||||
Filters applied
|
||||
<span data-testid="filters-applied">Filters applied</span>
|
||||
{/if}
|
||||
</Body>
|
||||
|
||||
<div class="table-wrap">
|
||||
<div class="table-wrap" data-testid="export-config-table">
|
||||
<Table
|
||||
schema={displaySchema}
|
||||
data={exportOpDisplay}
|
||||
{filters}
|
||||
{appliedFilters}
|
||||
loading={false}
|
||||
rowCount={filters?.length + 1}
|
||||
rowCount={appliedFilters?.length + 1}
|
||||
disableSorting={true}
|
||||
allowSelectRows={false}
|
||||
allowEditRows={false}
|
||||
|
@ -196,18 +200,21 @@
|
|||
</div>
|
||||
{:else}
|
||||
<Body size="S">
|
||||
Exporting <strong>all</strong> rows
|
||||
<span data-testid="export-all-rows">
|
||||
Exporting <strong>all</strong> rows
|
||||
</span>
|
||||
</Body>
|
||||
{/if}
|
||||
|
||||
<Select
|
||||
label="Format"
|
||||
bind:value={exportFormat}
|
||||
{options}
|
||||
placeholder={null}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x.key}
|
||||
/>
|
||||
<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>
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
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(),
|
||||
}
|
||||
})
|
||||
|
||||
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(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,3 +1,5 @@
|
|||
import * as Constants from "./constants"
|
||||
|
||||
export function unreachable(
|
||||
value: never,
|
||||
message = `No such case in exhaustive switch: ${value}`
|
||||
|
@ -43,3 +45,15 @@ export async function parallelForeach<T>(
|
|||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
export function filterValueToLabel() {
|
||||
return Object.keys(Constants.OperatorOptions).reduce(
|
||||
(acc: { [key: string]: string }, key: string) => {
|
||||
const ops: { [key: string]: any } = Constants.OperatorOptions
|
||||
const op: { [key: string]: string } = ops[key]
|
||||
acc[op["value"]] = op.label
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue