Merge pull request #15838 from Budibase/pdf-specific-components

PDF table component
This commit is contained in:
Andrew Kingston 2025-04-01 20:29:57 +01:00 committed by GitHub
commit 9d7ba4edd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 457 additions and 50 deletions

View File

@ -211,9 +211,12 @@ const localeDateFormat = new Intl.DateTimeFormat()
// Formats a dayjs date according to schema flags
export const getDateDisplayValue = (
value: dayjs.Dayjs | null,
value: dayjs.Dayjs | string | null,
{ enableTime = true, timeOnly = false } = {}
): string => {
if (typeof value === "string") {
value = dayjs(value)
}
if (!value?.isValid()) {
return ""
}

View File

@ -22,6 +22,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
import TopLevelColumnEditor from "./controls/ColumnEditor/TopLevelColumnEditor.svelte"
import GridColumnEditor from "./controls/GridColumnConfiguration/GridColumnConfiguration.svelte"
import BarButtonList from "./controls/BarButtonList.svelte"
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
@ -62,7 +63,10 @@ const componentMap = {
stepConfiguration: FormStepConfiguration,
formStepControls: FormStepControls,
columns: ColumnEditor,
// "Basic" actually includes nested JSON and relationship fields
"columns/basic": BasicColumnEditor,
// "Top level" is only the top level schema fields
"columns/toplevel": TopLevelColumnEditor,
"columns/grid": GridColumnEditor,
tableConditions: TableConditionEditor,
"field/sortable": SortableFieldSelect,

View File

@ -145,7 +145,7 @@
<div class="column">
<div class="wide">
<Body size="S">
By default, all columns will automatically be shown.
The default column configuration will automatically be shown.
<br />
You can manually control which columns are included by adding them
below.

View File

@ -10,10 +10,18 @@
} from "@/dataBinding"
import { selectedScreen, tables } from "@/stores/builder"
export let componentInstance
const getSearchableFields = (schema, tableList) => {
return search.getFields(tableList, Object.values(schema || {}), {
allowLinks: true,
})
}
export let componentInstance = undefined
export let value = []
export let allowCellEditing = true
export let allowReorder = true
export let getSchemaFields = getSearchableFields
export let placeholder = "All columns"
const dispatch = createEventDispatcher()
@ -28,13 +36,7 @@
: enrichedSchemaFields?.map(field => field.name)
$: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue)
$: enrichedSchemaFields = search.getFields(
$tables.list,
Object.values(schema || {}),
{
allowLinks: true,
}
)
$: enrichedSchemaFields = getSchemaFields(schema, $tables.list)
$: {
value = (value || []).filter(
@ -44,7 +46,7 @@
const getText = value => {
if (!value?.length) {
return "All columns"
return placeholder
}
let text = `${value.length} column`
if (value.length !== 1) {

View File

@ -0,0 +1,15 @@
<script lang="ts">
import ColumnEditor from "./ColumnEditor.svelte"
import type { TableSchema } from "@budibase/types"
const getTopLevelSchemaFields = (schema: TableSchema) => {
return Object.values(schema).filter(fieldSchema => !fieldSchema.nestedJSON)
}
</script>
<ColumnEditor
{...$$props}
on:change
allowCellEditing={false}
getSchemaFields={getTopLevelSchemaFields}
/>

View File

@ -26,7 +26,7 @@ import {
getJsHelperList,
} from "@budibase/string-templates"
import { TableNames } from "./constants"
import { JSONUtils, Constants } from "@budibase/frontend-core"
import { JSONUtils, Constants, SchemaUtils } from "@budibase/frontend-core"
import ActionDefinitions from "@/components/design/settings/controls/ButtonActionEditor/manifest.json"
import { environment, licensing } from "@/stores/portal"
import { convertOldFieldFormat } from "@/components/design/settings/controls/FieldConfiguration/utils"
@ -1026,25 +1026,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
// Check for any JSON fields so we can add any top level properties
if (schema) {
let jsonAdditions = {}
Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") {
const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema(
fieldSchema,
{
squashObjects: true,
}
)
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,
nestedJSON: true,
}
})
}
})
schema = { ...schema, ...jsonAdditions }
schema = SchemaUtils.addNestedJSONSchemaFields(schema)
}
// Determine if we should add ID and rev to the schema

View File

@ -23,6 +23,7 @@
"dataprovider",
"repeater",
"gridblock",
"pdftable",
"spreadsheet",
"dynamicfilter",
"daterangepicker"

View File

@ -8017,6 +8017,32 @@
"key": "text",
"wide": true
},
{
"type": "select",
"label": "Size",
"key": "size",
"defaultValue": "14px",
"showInBar": true,
"placeholder": "Default",
"options": [
{
"label": "Small",
"value": "12px"
},
{
"label": "Medium",
"value": "14px"
},
{
"label": "Large",
"value": "18px"
},
{
"label": "Extra large",
"value": "24px"
}
]
},
{
"type": "select",
"label": "Alignment",
@ -8088,5 +8114,71 @@
"defaultValue": "Download PDF"
}
]
},
"pdftable": {
"name": "PDF Table",
"icon": "Table",
"styles": ["size"],
"size": {
"width": 600,
"height": 304
},
"grid": {
"hAlign": "stretch",
"vAlign": "stretch"
},
"settings": [
{
"type": "dataSource",
"label": "Data",
"key": "datasource",
"required": true
},
{
"type": "filter",
"label": "Filtering",
"key": "filter",
"resetOn": "datasource",
"dependsOn": {
"setting": "datasource.type",
"value": "custom",
"invert": true
}
},
{
"type": "field/sortable",
"label": "Sort column",
"key": "sortColumn",
"placeholder": "Default",
"resetOn": "datasource",
"dependsOn": {
"setting": "datasource.type",
"value": "custom",
"invert": true
}
},
{
"type": "select",
"label": "Sort order",
"key": "sortOrder",
"resetOn": "datasource",
"options": ["Ascending", "Descending"],
"defaultValue": "Ascending",
"dependsOn": "sortColumn"
},
{
"type": "columns/toplevel",
"label": "Columns",
"key": "columns",
"resetOn": "datasource",
"placeholder": "First 3 columns"
},
{
"type": "number",
"label": "Limit",
"key": "limit",
"defaultValue": 10
}
]
}
}

View File

@ -5,12 +5,13 @@
export let text: any = ""
export let color: string | undefined = undefined
export let align: "left" | "center" | "right" | "justify" = "left"
export let size: string | undefined = "14px"
const component = getContext("component")
const { styleable } = getContext("sdk")
// Add in certain settings to styles
$: styles = enrichStyles($component.styles, color, align)
$: styles = enrichStyles($component.styles, color, align, size)
// Ensure we're always passing in a string value to the markdown editor
$: safeText = stringify(text)
@ -18,10 +19,12 @@
const enrichStyles = (
styles: any,
colorStyle: typeof color,
alignStyle: typeof align
alignStyle: typeof align,
size: string | undefined
) => {
let additions: Record<string, string> = {
"text-align": alignStyle,
"font-size": size || "14px",
}
if (colorStyle) {
additions.color = colorStyle

View File

@ -36,11 +36,11 @@ export { default as sidepanel } from "./SidePanel.svelte"
export { default as modal } from "./Modal.svelte"
export { default as gridblock } from "./GridBlock.svelte"
export { default as textv2 } from "./Text.svelte"
export { default as pdf } from "./pdf/PDF.svelte"
export * from "./charts"
export * from "./forms"
export * from "./blocks"
export * from "./dynamic-filter"
export * from "./pdf"
// Deprecated component left for compatibility in old apps
export * from "./deprecated/table"

View File

@ -0,0 +1,143 @@
<script lang="ts">
import type {
DataFetchDatasource,
FieldSchema,
GroupUserDatasource,
SortOrder,
TableSchema,
UISearchFilter,
UserDatasource,
} from "@budibase/types"
import { fetchData, QueryUtils, stringifyRow } from "@budibase/frontend-core"
import { getContext } from "svelte"
type ProviderDatasource = Exclude<
DataFetchDatasource,
UserDatasource | GroupUserDatasource
>
type ChosenColumns = Array<{ name: string; displayName?: string }> | undefined
type Schema = { [key: string]: FieldSchema & { displayName: string } }
export let datasource: ProviderDatasource
export let filter: UISearchFilter | undefined = undefined
export let sortColumn: string | undefined = undefined
export let sortOrder: SortOrder | undefined = undefined
export let columns: ChosenColumns = undefined
export let limit: number = 20
const component = getContext("component")
const { styleable, API } = getContext("sdk")
$: query = QueryUtils.buildQuery(filter)
$: fetch = createFetch(datasource)
$: fetch.update({
query,
sortColumn,
sortOrder,
limit,
})
$: schema = sanitizeSchema($fetch.schema, columns)
$: columnCount = Object.keys(schema).length
$: rowCount = $fetch.rows?.length || 0
$: stringifiedRows = ($fetch?.rows || []).map(row =>
stringifyRow(row, schema)
)
const createFetch = (datasource: ProviderDatasource) => {
return fetchData({
API,
datasource,
options: {
query,
sortColumn,
sortOrder,
limit,
paginate: false,
},
})
}
const sanitizeSchema = (
schema: TableSchema | null,
columns: ChosenColumns
): Schema => {
if (!schema) {
return {}
}
let sanitized: Schema = {}
// Clean out hidden fields and ensure we have
Object.entries(schema).forEach(([field, fieldSchema]) => {
if (fieldSchema.visible !== false) {
sanitized[field] = {
...fieldSchema,
displayName: field,
}
}
})
// Clean out unselected columns.
// Default to first 3 columns if none specified, as we are width contrained.
if (!columns?.length) {
columns = Object.values(sanitized).slice(0, 3)
}
let pruned: Schema = {}
for (let col of columns) {
if (sanitized[col.name]) {
pruned[col.name] = {
...sanitized[col.name],
displayName: col.displayName || sanitized[col.name].displayName,
}
}
}
sanitized = pruned
return sanitized
}
</script>
<div class="vars" style="--cols:{columnCount}; --rows:{rowCount};">
<div class="table" class:valid={!!schema} use:styleable={$component.styles}>
{#if schema}
{#each Object.keys(schema) as col}
<div class="cell header">{schema[col].displayName}</div>
{/each}
{#each stringifiedRows as row}
{#each Object.keys(schema) as col}
<div class="cell">{row[col]}</div>
{/each}
{/each}
{/if}
</div>
</div>
<style>
.vars {
display: contents;
--border-color: var(--spectrum-global-color-gray-300);
}
.table {
display: grid;
grid-template-columns: repeat(var(--cols), minmax(40px, auto));
grid-template-rows: repeat(var(--rows), max-content);
overflow: hidden;
background: var(--spectrum-global-color-gray-50);
}
.table.valid {
border-left: 1px solid var(--border-color);
border-top: 1px solid var(--border-color);
}
.cell {
border-right: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-xs) var(--spacing-s);
overflow: hidden;
word-break: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.cell.header {
font-weight: 600;
}
</style>

View File

@ -0,0 +1,2 @@
export { default as pdf } from "./PDF.svelte"
export { default as pdftable } from "./PDFTable.svelte"

View File

@ -1,8 +1,5 @@
<script context="module">
const NumberFormatter = Intl.NumberFormat()
</script>
<script>
import { formatNumber } from "@budibase/frontend-core"
import TextCell from "./TextCell.svelte"
export let api
@ -13,18 +10,6 @@
const newValue = isNaN(float) ? null : float
onChange(newValue)
}
const formatNumber = value => {
const type = typeof value
if (type !== "string" && type !== "number") {
return ""
}
if (type === "string" && !value.trim().length) {
return ""
}
const res = NumberFormatter.format(value)
return res === "NaN" ? value : res
}
</script>
<TextCell

View File

@ -0,0 +1,149 @@
import {
BBReferenceFieldSubType,
FieldSchema,
FieldType,
Row,
TableSchema,
} from "@budibase/types"
import { Helpers } from "@budibase/bbui"
// Singleton formatter to save us creating one every time
const NumberFormatter = Intl.NumberFormat()
export type StringifiedRow = { [key: string]: string }
// Formats a number according to the locale
export const formatNumber = (value: any): string => {
const type = typeof value
if (type !== "string" && type !== "number") {
return ""
}
if (type === "string" && !value.trim().length) {
return ""
}
const res = NumberFormatter.format(value)
return res === "NaN" ? stringifyValue(value) : res
}
// Attempts to stringify any type of value
const stringifyValue = (value: any): string => {
if (value == null) {
return ""
}
if (typeof value === "string") {
return value
}
if (typeof value.toString === "function") {
return stringifyValue(value.toString())
}
try {
return JSON.stringify(value)
} catch (e) {
return ""
}
}
const stringifyField = (value: any, schema: FieldSchema): string => {
switch (schema.type) {
// Auto should not exist as it should always be typed by its underlying
// real type, like date or user
case FieldType.AUTO:
return ""
// Just state whether signatures exist or not
case FieldType.SIGNATURE_SINGLE:
return value ? "Yes" : "No"
// Extract attachment names
case FieldType.ATTACHMENT_SINGLE:
case FieldType.ATTACHMENTS: {
if (!value) {
return ""
}
const arrayValue = Array.isArray(value) ? value : [value]
return arrayValue
.map(x => x.name)
.filter(x => !!x)
.join(", ")
}
// Extract primary displays from relationships
case FieldType.LINK: {
if (!value) {
return ""
}
const arrayValue = Array.isArray(value) ? value : [value]
return arrayValue
.map(x => x.primaryDisplay)
.filter(x => !!x)
.join(", ")
}
// Stringify JSON blobs
case FieldType.JSON:
return value ? JSON.stringify(value) : ""
// User is the only BB reference subtype right now
case FieldType.BB_REFERENCE:
case FieldType.BB_REFERENCE_SINGLE: {
if (
schema.subtype !== BBReferenceFieldSubType.USERS &&
schema.subtype !== BBReferenceFieldSubType.USER
) {
return ""
}
if (!value) {
return ""
}
const arrayVal = Array.isArray(value) ? value : [value]
return arrayVal?.map((user: any) => user.primaryDisplay).join(", ") || ""
}
// Join arrays with commas
case FieldType.ARRAY:
return value?.join(", ") || ""
// Just capitalise booleans
case FieldType.BOOLEAN:
return Helpers.capitalise(value?.toString() || "false")
// Format dates into something readable
case FieldType.DATETIME: {
return Helpers.getDateDisplayValue(value, {
enableTime: !schema.dateOnly,
timeOnly: schema.timeOnly,
})
}
// Format numbers using a locale string
case FieldType.NUMBER:
return formatNumber(value)
// Simple string types
case FieldType.STRING:
case FieldType.LONGFORM:
case FieldType.BIGINT:
case FieldType.OPTIONS:
case FieldType.AI:
case FieldType.BARCODEQR:
return value || ""
// Fallback for unknown types or future column types that we forget to add
case FieldType.FORMULA:
default:
return stringifyValue(value)
}
}
// Stringifies every property of a row, ensuring they are all human-readable
// strings for display
export const stringifyRow = (row: Row, schema: TableSchema): StringifiedRow => {
let stringified: StringifiedRow = {}
Object.entries(schema).forEach(([field, fieldSchema]) => {
stringified[field] = stringifyField(
Helpers.deepGet(row, field),
fieldSchema
)
})
return stringified
}

View File

@ -15,3 +15,4 @@ export * from "./relatedColumns"
export * from "./table"
export * from "./components"
export * from "./validation"
export * from "./formatting"

View File

@ -1,5 +1,6 @@
import { helpers } from "@budibase/shared-core"
import { TypeIconMap } from "../constants"
import { convertJSONSchemaToTableSchema } from "./json"
export const getColumnIcon = column => {
// For some reason we have remix icons saved under this property sometimes,
@ -24,3 +25,25 @@ export const getColumnIcon = column => {
return result || "Text"
}
export const addNestedJSONSchemaFields = schema => {
if (!schema) {
return schema
}
let jsonAdditions = {}
Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
squashObjects: true,
})
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,
nestedJSON: true,
}
})
}
})
return { ...schema, ...jsonAdditions }
}

View File

@ -207,6 +207,8 @@ export interface BaseFieldSchema extends UIFieldMetadata {
autocolumn?: boolean
autoReason?: AutoReason.FOREIGN_KEY
subtype?: never
// added when enriching nested JSON fields into schema
nestedJSON?: boolean
}
interface OtherFieldMetadata extends BaseFieldSchema {