This commit is contained in:
Gerard Burns 2024-03-27 08:41:27 +00:00
parent d0fd19a0bc
commit ab40e3babd
16 changed files with 363 additions and 51 deletions

View File

@ -9,6 +9,8 @@
export let options = []
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let getOptionsIcon = () => null
export let getOptionsIconToolip = () => null
export let readonly = false
export let autocomplete = false
export let sort = false
@ -80,6 +82,8 @@
<Picker
on:loadMore
{getOptionsIcon}
{getOptionsIconTooltip}
{id}
{disabled}
{readonly}

View File

@ -26,6 +26,7 @@
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let getOptionIcon = () => null
export let getOptionIconTooltip = () => null
export let useOptionIconImage = false
export let getOptionColour = () => null
export let getOptionSubtitle = () => null
@ -202,7 +203,7 @@
>
{#if getOptionIcon(option, idx)}
<span class="option-extra icon">
{#if useOptionIconImage}
{#if useoptioniconimage}
<img
src={getOptionIcon(option, idx)}
alt="icon"

View File

@ -9,8 +9,10 @@ import TableSelect from "./controls/TableSelect.svelte"
import ColorPicker from "./controls/ColorPicker.svelte"
import { IconSelect } from "./controls/IconSelect"
import FieldSelect from "./controls/FieldSelect.svelte"
import ChartFieldSelect from "./controls/ChartFieldSelect.svelte"
import SortableFieldSelect from "./controls/SortableFieldSelect.svelte"
import MultiFieldSelect from "./controls/MultiFieldSelect.svelte"
import ChartMultiFieldSelect from "./controls/ChartMultiFieldSelect.svelte"
import SearchFieldSelect from "./controls/SearchFieldSelect.svelte"
import SchemaSelect from "./controls/SchemaSelect.svelte"
import SectionSelect from "./controls/SectionSelect.svelte"
@ -46,7 +48,9 @@ const componentMap = {
color: ColorPicker,
icon: IconSelect,
field: FieldSelect,
chartfield: ChartFieldSelect,
multifield: MultiFieldSelect,
chartmultifield: ChartMultiFieldSelect,
searchfield: SearchFieldSelect,
options: OptionsEditor,
schema: SchemaSelect,

View File

@ -0,0 +1,53 @@
<script>
import { Select } from "@budibase/bbui"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "dataBinding"
import { selectedScreen } from "stores/builder"
import { createEventDispatcher } from "svelte"
export let componentInstance = {}
export let value = ""
export let placeholder
const dispatch = createEventDispatcher()
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
$: schema = getSchemaForDatasource($selectedScreen, datasource).schema
$: options = Object.keys(schema || {}).filter(key => {
return (
schema[key].type !== "json" &&
schema[key].type !== "array" &&
schema[key].type !== "attachment" &&
schema[key].type !== "barcodeqr" &&
schema[key].type !== "link" &&
schema[key].type !== "bb_reference"
);
});
$: boundValue = getValidValue(value, options)
const getValidValue = (value, options) => {
// Reset value if there aren't any options
if (!Array.isArray(options)) {
return null
}
// Reset value if the value isn't found in the options
if (options.indexOf(value) === -1) {
return null
}
return value
}
const onChange = value => {
boundValue = getValidValue(value.detail, options)
dispatch("change", boundValue)
}
$: {
console.log(options, schema);
}
</script>
<Select {placeholder} value={boundValue} on:change={onChange} {options} />

View File

@ -0,0 +1,48 @@
<script>
import { Multiselect } from "@budibase/bbui"
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
import { selectedScreen } from "stores/builder"
import { createEventDispatcher } from "svelte"
export let componentInstance = {}
export let value = ""
export let placeholder
export let fieldValidator
const dispatch = createEventDispatcher()
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
$: schema = getSchemaForDatasource($selectedScreen, datasource).schema
const getOptions = (schema, fieldValidator) => {
if (fieldValidator != null) {
return Object.keys(schema || {})
}
}
$: options = Object.keys(schema || {}).filter(key => {
return (
schema[key].type !== "json" &&
schema[key].type !== "array" &&
schema[key].type !== "attachment" &&
schema[key].type !== "barcodeqr" &&
schema[key].type !== "link" &&
schema[key].type !== "bb_reference"
);
});
$: boundValue = getValidOptions(value, options)
const getValidOptions = (selectedOptions, allOptions) => {
// Fix the hardcoded default string value
if (!Array.isArray(selectedOptions)) {
selectedOptions = []
}
return selectedOptions.filter(val => allOptions.indexOf(val) !== -1)
}
const setValue = value => {
boundValue = getValidOptions(value.detail, options)
dispatch("change", boundValue)
}
</script>
<Multiselect {placeholder} value={boundValue} on:change={setValue} {options} />

View File

@ -0,0 +1,42 @@
export const unsupported = Symbol("values-validator-unsupported")
export const partialSupport = Symbol("values-validator-partial-support")
export const supported = Symbol("values-validator-supported")
const validatorMap = {
chart: (fieldSchema) => {
if (
fieldSchema.type === "json" ||
fieldSchema.type === "array" ||
fieldSchema.type === "attachment" ||
fieldSchema.type === "barcodeqr" ||
fieldSchema.type === "link" ||
fieldSchema.type === "bb_reference"
) {
return {
support: unsupported,
message: "This field cannot be used as a chart value"
}
}
if (fieldSchema.type === "string") {
return {
support: partialSupport,
message: "This field can be used as a chart value, but non-numeric values will not be parsed correctly"
}
}
if (fieldSchema.type === "number") {
return {
support: supported,
message: "This field can be used for chart values"
}
}
return {
support: partialSupport,
message: "This field can be used as a chart value, but it may not be parsed correctly"
}
}
};
export default validatorMap;

View File

@ -191,6 +191,9 @@
// Number fields
min: setting.min ?? null,
max: setting.max ?? null,
// Field select settings
fieldValidator: setting.fieldValidator,
}}
{bindings}
{componentBindings}

View File

@ -1629,10 +1629,11 @@
"required": true
},
{
"type": "multifield",
"type": "chartmultifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataProvider",
"fieldValidator": "chart",
"required": true
},
{
@ -1787,7 +1788,7 @@
"required": true
},
{
"type": "multifield",
"type": "chartmultifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataProvider",
@ -1940,7 +1941,7 @@
"required": true
},
{
"type": "multifield",
"type": "chartmultifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataProvider",
@ -2105,8 +2106,8 @@
"required": true
},
{
"type": "field",
"label": "Data columns",
"type": "chartfield",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataProvider",
"required": true
@ -2234,7 +2235,7 @@
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Data columns",
"key": "valueColumn",
"dependsOn": "dataProvider",
@ -2363,28 +2364,28 @@
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Open column",
"key": "openColumn",
"dependsOn": "dataProvider",
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Close column",
"key": "closeColumn",
"dependsOn": "dataProvider",
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "High column",
"key": "highColumn",
"dependsOn": "dataProvider",
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Low column",
"key": "lowColumn",
"dependsOn": "dataProvider",
@ -2448,7 +2449,7 @@
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataProvider",
@ -5265,7 +5266,7 @@
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataSource",
@ -5290,7 +5291,7 @@
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataSource",
@ -5315,7 +5316,7 @@
"required": true
},
{
"type": "multifield",
"type": "chartmultifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataSource",
@ -5362,7 +5363,7 @@
},
"settings": [
{
"type": "field",
"type": "chartfield",
"label": "Value column",
"key": "valueColumn",
"dependsOn": "dataSource",
@ -5410,7 +5411,7 @@
"required": true
},
{
"type": "multifield",
"type": "chartmultifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataSource",
@ -5459,7 +5460,7 @@
"required": true
},
{
"type": "multifield",
"type": "chartmultifield",
"label": "Data columns",
"key": "valueColumns",
"dependsOn": "dataSource",
@ -5520,28 +5521,28 @@
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Open column",
"key": "openColumn",
"dependsOn": "dataSource",
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Close column",
"key": "closeColumn",
"dependsOn": "dataSource",
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "High column",
"key": "highColumn",
"dependsOn": "dataSource",
"required": true
},
{
"type": "field",
"type": "chartfield",
"label": "Low column",
"key": "lowColumn",
"dependsOn": "dataSource",

View File

@ -32,11 +32,12 @@
"@spectrum-css/tag": "^3.1.4",
"@spectrum-css/typography": "^3.0.2",
"@spectrum-css/vars": "^3.0.1",
"apexcharts": "^3.22.1",
"apexcharts": "^3.45.2",
"dayjs": "^1.10.8",
"downloadjs": "1.4.7",
"html5-qrcode": "^2.2.1",
"leaflet": "^1.7.1",
"lodash": "^4.17.21",
"sanitize-html": "^2.7.0",
"screenfull": "^6.0.1",
"shortid": "^2.2.15",

View File

@ -283,6 +283,9 @@
const dependsOnKey = setting.dependsOn.setting || setting.dependsOn
const dependsOnValue = setting.dependsOn.value
const realDependentValue = instance[dependsOnKey]
if (dependsOnValue === undefined && realDependentValue) {
return missing
}
if (dependsOnValue == null && realDependentValue == null) {
return false
}

View File

@ -1,25 +1,104 @@
<script>
import { getContext } from "svelte"
import { chart } from "svelte-apexcharts"
import Placeholder from "../Placeholder.svelte"
import ApexCharts from 'apexcharts'
import { Icon } from "@budibase/bbui"
import { cloneDeep } from "lodash";
const { styleable, builderStore } = getContext("sdk")
const component = getContext("component")
export let options
/*
export let invalid = false
const parseValue = (value) => {
// A value like [10, 11, 12] actually would be output by parseInt as `10`, but this behaviour is odd and not something a
// reasonable user would expect.
if (Array.isArray(value)) {
return null;
}
const parsedValue = parseInt(value, 10);
if (Number.isNaN(parsedValue)) {
return null;
}
return parsedValue
}
const parseOptions = (options) => {
const parsedOptions = { series: [], ...cloneDeep(options)}
// Object form of series, used by most charts
if (parsedOptions.series.some(entry => Array.isArray(entry?.data))) {
parsedOptions.series = parsedOptions.series.map(entry => ({ ...entry, data: parseValue})parseValue);
} else {
// Scalar form of series, used by non-axis charts like pie and donut
parsedOptions.series = parsedOptions.series.map(parseValue);
}
return parsedOptions;
}
$: parsedOptions = parseOptions(options);
*/
let chartElement;
let chart;
const updateChart = async (newOptions) => {
try {
await chart?.updateOptions(newOptions)
} catch(e) {
//console.log(e)
}
}
const renderChart = async (newChartElement) => {
try {
await chart?.destroy()
chart = new ApexCharts(newChartElement, options)
await chart.render()
} catch(e) {
//console.log(e)
}
}
const isSeriesValid = (series) => {
return true
}
$: noData = options == null || options?.series?.length === 0
$: hide = noData || !seriesValid
// Call render chart upon changes to hide, as apex charts has issues with rendering upon changes automatically
// if the chart is hidden.
$: renderChart(chartElement, hide)
$: updateChart(options)
$: seriesValid = isSeriesValid(options?.series || [])
</script>
{#if options}
{#key options.customColor}
<div use:chart={options} use:styleable={$component.styles} />
{/key}
{:else if $builderStore.inBuilder}
<div use:styleable={$component.styles}>
<Placeholder />
{#key options?.customColor}
<div class:hide use:styleable={$component.styles} bind:this={chartElement} />
{#if $builderStore.inBuilder && noData }
<div class="component-placeholder" use:styleable={{ ...$component.styles, normal: {}, custom: null, empty: true }}>
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
Add rows to your data source to start using your component
</div>
{:else if $builderStore.inBuilder && !seriesValid}
<div class="component-placeholder" use:styleable={{ ...$component.styles, normal: {}, custom: null, empty: true }}>
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
Your selected data cannot be displayed in this chart
</div>
{/if}
{/key}
<style>
.hide {
display: none;
}
div :global(.apexcharts-legend-series) {
display: flex !important;
text-transform: capitalize;
@ -59,4 +138,25 @@
) {
padding-bottom: 0;
}
.component-placeholder {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
color: var(--spectrum-global-color-gray-600);
font-size: var(--font-size-s);
padding: var(--spacing-xs);
gap: var(--spacing-s);
}
/* Common styles for all error states to use */
.component-placeholder :global(mark) {
background-color: var(--spectrum-global-color-gray-400);
padding: 0 4px;
border-radius: 2px;
}
.component-placeholder :global(.spectrum-Link) {
cursor: pointer;
}
</style>

View File

@ -7,6 +7,9 @@ export class ApexOptionsBuilder {
}
this.options = {
series: [],
noData:{
text: "no data to show"
},
legend: {
show: false,
position: "top",
@ -99,7 +102,8 @@ export class ApexOptionsBuilder {
}
series(series) {
return this.setOption(["series"], series)
const foo = this.setOption(["series"], series)
return foo;
}
horizontal(horizontal) {
@ -161,6 +165,15 @@ export class ApexOptionsBuilder {
)
}
candleStick() {
//this.options.xaxis.convertedCatToNumeric = false;
this.options.xaxis.labels.formatter = (epoch) => {
return (new Date(epoch)).toDateString();
}
return this;
}
clearXFormatter() {
delete this.options.xaxis.labels
return this

View File

@ -1,6 +1,7 @@
<script>
import { ApexOptionsBuilder } from "./ApexOptionsBuilder"
import ApexChart from "./ApexChart.svelte"
import { get } from "lodash";
export let title
export let dataProvider
@ -71,9 +72,8 @@
// Fetch data
const { schema, rows } = dataProvider
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = rows.filter(row => hasAllColumns(row)).slice(0, 100)
const data = rows.slice(0, 100)
if (!schema || !data.length) {
return null
}
@ -105,11 +105,21 @@
}
useDates = labelFieldType === "datetime"
}
const series = valueColumns.map(column => ({
const series = (valueColumns ?? []).map(column => ({
name: column,
data: data.map(row => {
if (!useDates) {
return row[column]
const value = get(row, column);
if (Array.isArray(value)) {
return null;
}
if (Number.isNaN(parseInt(value, 10))) {
return null;
}
return value;
} else {
return [row[labelColumn], row[column]]
}

View File

@ -76,14 +76,16 @@
.animate(animate)
.yUnits(yAxisUnits)
.yTooltip(true)
.xType("datetime")
//.xType("datetime")
.candleStick()
// Add data
//const parseDate = d => d
const parseDate = d => (isNaN(d) ? Date.parse(d).valueOf() : parseInt(d))
const chartData = data.map(row => ({
x: parseDate(row[dateColumn]),
y: [row[openColumn], row[highColumn], row[lowColumn], row[closeColumn]],
}))
const chartData = data.map(row => ([
parseDate(row[dateColumn]),
row[openColumn], row[highColumn], row[lowColumn], row[closeColumn]
]))
builder = builder.series([{ data: chartData }])
// Build chart options

View File

@ -1,6 +1,7 @@
<script>
import { ApexOptionsBuilder } from "./ApexOptionsBuilder"
import ApexChart from "./ApexChart.svelte"
import { get } from "lodash";
// Common props
export let title
@ -80,9 +81,7 @@
// Fetch, filter and sort data
const { schema, rows } = dataProvider
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = rows.filter(row => hasAllColumns(row))
const data = rows
if (!schema || !data.length) {
return null
}
@ -112,11 +111,21 @@
builder = builder.xType(labelFieldType)
useDates = labelFieldType === "datetime"
}
const series = valueColumns.map(column => ({
const series = (valueColumns ?? []).map(column => ({
name: column,
data: data.map(row => {
if (!useDates) {
return row[column]
const value = get(row, column);
if (Array.isArray(value)) {
return null;
}
if (Number.isNaN(parseInt(value, 10))) {
return null;
}
return value;
} else {
return [row[labelColumn], row[column]]
}

View File

@ -6605,6 +6605,11 @@
js-yaml "^3.10.0"
tslib "^2.4.0"
"@yr/monotone-cubic-spline@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz#7272d89f8e4f6fb7a1600c28c378cc18d3b577b9"
integrity sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==
"@zerodevx/svelte-json-view@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@zerodevx/svelte-json-view/-/svelte-json-view-1.0.7.tgz#abf3efa71dedcb3e9d16bc9cc61d5ea98c8d00b1"
@ -6907,7 +6912,7 @@ anymatch@^3.0.3, anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
apexcharts@^3.19.2, apexcharts@^3.22.1:
apexcharts@^3.19.2:
version "3.37.1"
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.37.1.tgz#50443d302fc7fc72aace9c6c4074baae017c6950"
integrity sha512-fmQ5Updeb/LASl+S1+mIxXUFxzY0Fa7gexfCs4o+OPP9f2NEBNjvybOtPrah44N4roK7U5o5Jis906QeEQu0cA==
@ -6919,6 +6924,19 @@ apexcharts@^3.19.2, apexcharts@^3.22.1:
svg.resize.js "^1.4.3"
svg.select.js "^3.0.1"
apexcharts@^3.45.2:
version "3.48.0"
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.48.0.tgz#cf0e18f4551b04a242d81942c965c15547f5f1b6"
integrity sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==
dependencies:
"@yr/monotone-cubic-spline" "^1.0.3"
svg.draggable.js "^2.2.2"
svg.easing.js "^2.0.0"
svg.filter.js "^2.0.2"
svg.pathmorphing.js "^0.1.3"
svg.resize.js "^1.4.3"
svg.select.js "^3.0.1"
apidoc@0.50.4:
version "0.50.4"
resolved "https://registry.yarnpkg.com/apidoc/-/apidoc-0.50.4.tgz#52ff8fb4d067a73faf544455031f44459bd68d75"