Browse Source

Handle nulls / empty in views and tables

master
Rory Powell 5 years ago
parent
commit
0fb4613b5a
  1. 20
      packages/bbui/src/Table/CellRenderer.svelte
  2. 9
      packages/bbui/src/Table/StringRenderer.svelte
  3. 62
      packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte
  4. 6
      packages/server/src/api/controllers/row/ExternalRequest.ts
  5. 20
      packages/server/src/api/controllers/view/viewBuilder.js
  6. 8
      packages/server/src/integrations/base/sql.ts
  7. 5
      packages/server/src/integrations/utils.ts

20
packages/bbui/src/Table/CellRenderer.svelte

@ -28,9 +28,27 @@
$: type = schema?.type ?? "string" $: type = schema?.type ?? "string"
$: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer $: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
/**
* Don't use falsy here as we want to:
* - include empty arrays
* - exclude 0 and booleans
*
* If updated, the corresponding view expression should be updated in 'server/viewBuilder.js'
*/
const isNotSet = value => {
return (
value === undefined ||
value === null ||
value === "" ||
(Array.isArray(value) && value.length === 0)
)
}
</script> </script>
{#if renderer && (customRenderer || (value != null && value !== ""))} {#if !customRenderer && isNotSet(value)}
<svelte:component this={StringRenderer} value={"Not Set"} secondary={true} />
{:else if renderer}
<svelte:component this={renderer} {row} {schema} {value} on:clickrelationship> <svelte:component this={renderer} {row} {schema} {value} on:clickrelationship>
<slot /> <slot />
</svelte:component> </svelte:component>

9
packages/bbui/src/Table/StringRenderer.svelte

@ -1,8 +1,11 @@
<script> <script>
export let value export let value
export let secondary
</script> </script>
<div>{typeof value === "object" ? JSON.stringify(value) : value}</div> <div class={secondary ? "secondary" : ""}>
{typeof value === "object" ? JSON.stringify(value) : value}
</div>
<style> <style>
div { div {
@ -10,4 +13,8 @@
text-overflow: ellipsis; text-overflow: ellipsis;
width: 150px; width: 150px;
} }
.secondary {
font-style: italic;
color: var(--grey-5);
}
</style> </style>

62
packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte

@ -42,6 +42,14 @@
name: "Contains", name: "Contains",
key: "CONTAINS", key: "CONTAINS",
}, },
{
name: "Is Set",
key: "SET",
},
{
name: "Is Not Set",
key: "NOT_SET",
},
] ]
const CONJUNCTIONS = [ const CONJUNCTIONS = [
@ -116,6 +124,10 @@
const getOptionLabel = x => x.name const getOptionLabel = x => x.name
const getOptionValue = x => x.key const getOptionValue = x => x.key
const showValue = filter => {
return !(filter.condition === "SET" || filter.condition === "NOT_SET")
}
</script> </script>
<ModalContent title="Filter" confirmText="Save" onConfirm={saveView} size="L"> <ModalContent title="Filter" confirmText="Save" onConfirm={saveView} size="L">
@ -144,30 +156,36 @@
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
/> />
{#if filter.key && isMultipleChoice(filter.key)} {#if showValue(filter)}
<Select {#if filter.key && isMultipleChoice(filter.key)}
bind:value={filter.value} <Select
options={fieldOptions(filter.key)} bind:value={filter.value}
getOptionLabel={x => x.toString()} options={fieldOptions(filter.key)}
/> getOptionLabel={x => x.toString()}
{:else if filter.key && isDate(filter.key)} />
<DatePicker {:else if filter.key && isDate(filter.key)}
bind:value={filter.value} <DatePicker
placeholder={filter.key || fields[0]} bind:value={filter.value}
/> placeholder={filter.key || fields[0]}
{:else if filter.key && isNumber(filter.key)} />
<Input {:else if filter.key && isNumber(filter.key)}
bind:value={filter.value} <Input
placeholder={filter.key || fields[0]} bind:value={filter.value}
type="number" placeholder={filter.key || fields[0]}
/> type="number"
/>
{:else}
<Input
placeholder={filter.key || fields[0]}
bind:value={filter.value}
/>
{/if}
<Icon hoverable name="Close" on:click={() => removeFilter(idx)} />
{:else} {:else}
<Input <Icon hoverable name="Close" on:click={() => removeFilter(idx)} />
placeholder={filter.key || fields[0]} <!-- empty div to preserve spacing -->
bind:value={filter.value} <div />
/>
{/if} {/if}
<Icon hoverable name="Close" on:click={() => removeFilter(idx)} />
{/each} {/each}
</div> </div>
{:else} {:else}

6
packages/server/src/api/controllers/row/ExternalRequest.ts

@ -37,7 +37,11 @@ interface RunConfig {
module External { module External {
const { makeExternalQuery } = require("./utils") const { makeExternalQuery } = require("./utils")
const { DataSourceOperation, FieldTypes, RelationshipTypes } = require("../../../constants") const {
DataSourceOperation,
FieldTypes,
RelationshipTypes,
} = require("../../../constants")
const { breakExternalTableId, isSQL } = require("../../../integrations/utils") const { breakExternalTableId, isSQL } = require("../../../integrations/utils")
const { processObjectSync } = require("@budibase/string-templates") const { processObjectSync } = require("@budibase/string-templates")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")

20
packages/server/src/api/controllers/view/viewBuilder.js

@ -10,6 +10,22 @@ const TOKEN_MAP = {
OR: "||", OR: "||",
} }
/**
* Don't use falsy here as we want to:
* - include empty arrays
* - exclude 0 and booleans
*
* If updated, the corresponding rendering condition should be updated in 'bbui/CellRenderer.svelte'
*/
const isNotSetExpression = key => {
return `(
doc["${key}"] === undefined ||
doc["${key}"] === null ||
doc["${key}"] === "" ||
(Array.isArray(doc["${key}"]) && doc["${key}"].length === 0)
)`
}
const GROUP_PROPERTY = { const GROUP_PROPERTY = {
group: { group: {
type: "string", type: "string",
@ -72,6 +88,10 @@ function parseFilterExpression(filters) {
expression.push( expression.push(
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")` `doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
) )
} else if (filter.condition === "NOT_SET") {
expression.push(isNotSetExpression(filter.key))
} else if (filter.condition === "SET") {
expression.push(`!${isNotSetExpression(filter.key)}`)
} else { } else {
const value = const value =
typeof filter.value == "string" ? `"${filter.value}"` : filter.value typeof filter.value == "string" ? `"${filter.value}"` : filter.value

8
packages/server/src/integrations/base/sql.ts

@ -201,7 +201,13 @@ function buildRead(knex: Knex, json: QueryJson, limit: number): KnexQuery {
[tableName]: query, [tableName]: query,
}).select(selectStatement) }).select(selectStatement)
// handle joins // handle joins
return addRelationships(knex, preQuery, selectStatement, tableName, relationships) return addRelationships(
knex,
preQuery,
selectStatement,
tableName,
relationships
)
} }
function buildUpdate( function buildUpdate(

5
packages/server/src/integrations/utils.ts

@ -34,7 +34,10 @@ export function generateRowIdField(keyProps: any[] = []) {
} }
export function isRowId(field: any) { export function isRowId(field: any) {
return Array.isArray(field) || (typeof field === "string" && field.match(ROW_ID_REGEX) != null) return (
Array.isArray(field) ||
(typeof field === "string" && field.match(ROW_ID_REGEX) != null)
)
} }
export function convertRowId(field: any) { export function convertRowId(field: any) {

Loading…
Cancel
Save