Merge branch 'master' into fix/grid-null-dereference-error
This commit is contained in:
commit
0fccca6d76
21 changed files with 263 additions and 1274 deletions
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.25.0",
|
||||
"version": "2.26.1",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -73,7 +73,6 @@
|
|||
"chance": "1.1.8",
|
||||
"ioredis-mock": "8.9.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-node": "29.7.0",
|
||||
"jest-serial-runner": "1.2.1",
|
||||
"pino-pretty": "10.0.0",
|
||||
"pouchdb-adapter-memory": "7.2.2",
|
||||
|
|
|
@ -93,7 +93,6 @@
|
|||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"ncp": "^2.0.0",
|
||||
"svelte-jester": "^1.3.2",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-static-copy": "^0.17.0",
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
faQuestionCircle,
|
||||
faCircleCheck,
|
||||
faGear,
|
||||
faRectangleList,
|
||||
} from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||
|
||||
|
@ -37,6 +38,7 @@
|
|||
faFileArrowUp,
|
||||
faChevronLeft,
|
||||
faCircleInfo,
|
||||
faRectangleList,
|
||||
|
||||
// -- Required for easyMDE use in the builder.
|
||||
faBold,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
import { licensing } from "stores/portal"
|
||||
import { isPremiumOrAbove } from "helpers/planTitle"
|
||||
import { ChangelogURL } from "constants"
|
||||
|
||||
$: premiumOrAboveLicense = isPremiumOrAbove($licensing?.license?.plan?.type)
|
||||
|
||||
|
@ -30,6 +31,13 @@
|
|||
<Body size="S">Help docs</Body>
|
||||
</a>
|
||||
<div class="divider" />
|
||||
<a target="_blank" href={ChangelogURL}>
|
||||
<div class="icon">
|
||||
<FontAwesomeIcon name="fa-solid fa-rectangle-list" />
|
||||
</div>
|
||||
<Body size="S">Changelog</Body>
|
||||
</a>
|
||||
<div class="divider" />
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/Budibase/budibase/discussions"
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
Body,
|
||||
Button,
|
||||
StatusLight,
|
||||
Link,
|
||||
} from "@budibase/bbui"
|
||||
import { appStore, initialise } from "stores/builder"
|
||||
import { API } from "api"
|
||||
import RevertModalVersionSelect from "./RevertModalVersionSelect.svelte"
|
||||
import { ChangelogURL } from "constants"
|
||||
|
||||
export function show() {
|
||||
updateModal.show()
|
||||
|
@ -106,6 +108,10 @@
|
|||
latest version available.
|
||||
</Body>
|
||||
{/if}
|
||||
<Body size="S">
|
||||
Find the changelog for the latest release
|
||||
<Link href={ChangelogURL} target="_blank">here</Link>
|
||||
</Body>
|
||||
{#if revertAvailable}
|
||||
<Body size="S">
|
||||
You can revert this app to version
|
||||
|
|
|
@ -49,17 +49,20 @@
|
|||
},
|
||||
]
|
||||
|
||||
$: tables = findAllMatchingComponents($selectedScreen?.props, component =>
|
||||
component._component.endsWith("table")
|
||||
)
|
||||
$: tableBlocks = findAllMatchingComponents(
|
||||
$: components = findAllMatchingComponents(
|
||||
$selectedScreen?.props,
|
||||
component => component._component.endsWith("tableblock")
|
||||
component => {
|
||||
const type = component._component
|
||||
return (
|
||||
type.endsWith("/table") ||
|
||||
type.endsWith("/tableblock") ||
|
||||
type.endsWith("/gridblock")
|
||||
)
|
||||
}
|
||||
)
|
||||
$: components = tables.concat(tableBlocks)
|
||||
$: componentOptions = components.map(table => ({
|
||||
label: table._instanceName,
|
||||
value: table._component.includes("tableblock")
|
||||
value: table._component.endsWith("/tableblock")
|
||||
? `${table._id}-table`
|
||||
: table._id,
|
||||
}))
|
||||
|
@ -69,6 +72,7 @@
|
|||
$: selectedTable = components.find(
|
||||
component => component._id === selectedTableId
|
||||
)
|
||||
$: parameters.rows = `{{ literal [${parameters.tableComponentId}].[selectedRows] }}`
|
||||
|
||||
onMount(() => {
|
||||
if (!parameters.type) {
|
||||
|
|
|
@ -70,3 +70,5 @@ export const PlanModel = {
|
|||
PER_USER: "perUser",
|
||||
DAY_PASS: "dayPass",
|
||||
}
|
||||
|
||||
export const ChangelogURL = "https://docs.budibase.com/changelog"
|
||||
|
|
|
@ -166,10 +166,16 @@ const automationActions = store => ({
|
|||
await store.actions.save(newAutomation)
|
||||
},
|
||||
test: async (automation, testData) => {
|
||||
const result = await API.testAutomation({
|
||||
automationId: automation?._id,
|
||||
testData,
|
||||
})
|
||||
let result
|
||||
try {
|
||||
result = await API.testAutomation({
|
||||
automationId: automation?._id,
|
||||
testData,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = err.message || err.status || JSON.stringify(err)
|
||||
throw `Automation test failed - ${message}`
|
||||
}
|
||||
if (!result?.trigger && !result?.steps?.length) {
|
||||
if (result?.err?.code === "usage_limit_exceeded") {
|
||||
throw "You have exceeded your automation quota"
|
||||
|
|
|
@ -7017,10 +7017,22 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "schema",
|
||||
"scope": "local"
|
||||
},
|
||||
"context": [
|
||||
{
|
||||
"type": "schema",
|
||||
"scope": "local"
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"values": [
|
||||
{
|
||||
"label": "Selected rows",
|
||||
"key": "selectedRows",
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": ["RefreshDatasource"]
|
||||
},
|
||||
"bbreferencefield": {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
// NOTE: this is not a block - it's just named as such to avoid confusing users,
|
||||
// because it functions similarly to one
|
||||
import { getContext } from "svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { get, derived, readable } from "svelte/store"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
|
||||
// table is actually any datasource, but called table for legacy compatibility
|
||||
|
@ -19,7 +19,6 @@
|
|||
export let columns = null
|
||||
export let onRowClick = null
|
||||
export let buttons = null
|
||||
export let repeat = null
|
||||
|
||||
const context = getContext("context")
|
||||
const component = getContext("component")
|
||||
|
@ -36,17 +35,18 @@
|
|||
} = getContext("sdk")
|
||||
|
||||
let grid
|
||||
let gridContext
|
||||
|
||||
$: columnWhitelist = parsedColumns
|
||||
?.filter(col => col.active)
|
||||
?.map(col => col.field)
|
||||
$: parsedColumns = getParsedColumns(columns)
|
||||
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
|
||||
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
||||
$: enrichedButtons = enrichButtons(buttons)
|
||||
$: parsedColumns = getParsedColumns(columns)
|
||||
$: selectedRows = deriveSelectedRows(gridContext)
|
||||
$: data = { selectedRows: $selectedRows }
|
||||
$: actions = [
|
||||
{
|
||||
type: ActionTypes.RefreshDatasource,
|
||||
callback: () => grid?.getContext()?.rows.actions.refreshData(),
|
||||
callback: () => gridContext?.rows.actions.refreshData(),
|
||||
metadata: { dataSource: table },
|
||||
},
|
||||
]
|
||||
|
@ -68,12 +68,14 @@
|
|||
|
||||
// Parses columns to fix older formats
|
||||
const getParsedColumns = columns => {
|
||||
if (!columns?.length) {
|
||||
return []
|
||||
}
|
||||
// If the first element has an active key all elements should be in the new format
|
||||
if (columns?.length && columns[0]?.active !== undefined) {
|
||||
if (columns[0].active !== undefined) {
|
||||
return columns
|
||||
}
|
||||
|
||||
return columns?.map(column => ({
|
||||
return columns.map(column => ({
|
||||
label: column.displayName || column.name,
|
||||
field: column.name,
|
||||
active: true,
|
||||
|
@ -82,7 +84,7 @@
|
|||
|
||||
const getSchemaOverrides = columns => {
|
||||
let overrides = {}
|
||||
columns?.forEach(column => {
|
||||
columns.forEach(column => {
|
||||
overrides[column.field] = {
|
||||
displayName: column.label,
|
||||
}
|
||||
|
@ -109,6 +111,23 @@
|
|||
}))
|
||||
}
|
||||
|
||||
const deriveSelectedRows = gridContext => {
|
||||
if (!gridContext) {
|
||||
return readable([])
|
||||
}
|
||||
return derived(
|
||||
[gridContext.selectedRows, gridContext.rowLookupMap, gridContext.rows],
|
||||
([$selectedRows, $rowLookupMap, $rows]) => {
|
||||
return Object.entries($selectedRows || {})
|
||||
.filter(([_, selected]) => selected)
|
||||
.map(([rowId]) => {
|
||||
const idx = $rowLookupMap[rowId]
|
||||
return gridContext.rows.actions.cleanRow($rows[idx])
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getSanitisedStyles = styles => {
|
||||
return {
|
||||
...styles,
|
||||
|
@ -118,40 +137,44 @@
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
gridContext = grid.getContext()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div use:styleable={styles} class:in-builder={$builderStore.inBuilder}>
|
||||
<span style="--height:{height};">
|
||||
<Provider {actions}>
|
||||
<Grid
|
||||
bind:this={grid}
|
||||
datasource={table}
|
||||
{API}
|
||||
{stripeRows}
|
||||
{quiet}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
{initialSortOrder}
|
||||
{fixedRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
{repeat}
|
||||
canAddRows={allowAddRows}
|
||||
canEditRows={allowEditRows}
|
||||
canDeleteRows={allowDeleteRows}
|
||||
canEditColumns={false}
|
||||
canExpandRows={false}
|
||||
canSaveSchema={false}
|
||||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
buttons={enrichedButtons}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
/>
|
||||
</Provider>
|
||||
<Grid
|
||||
bind:this={grid}
|
||||
datasource={table}
|
||||
{API}
|
||||
{stripeRows}
|
||||
{quiet}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
{initialSortOrder}
|
||||
{fixedRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
canAddRows={allowAddRows}
|
||||
canEditRows={allowEditRows}
|
||||
canDeleteRows={allowDeleteRows}
|
||||
canEditColumns={false}
|
||||
canExpandRows={false}
|
||||
canSaveSchema={false}
|
||||
canSelectRows={true}
|
||||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
buttons={enrichedButtons}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Provider {data} {actions} />
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
|
|
|
@ -15,7 +15,7 @@ const createRowSelectionStore = () => {
|
|||
const componentId = Object.keys(selection).find(
|
||||
componentId => componentId === tableComponentId
|
||||
)
|
||||
return selection[componentId] || {}
|
||||
return selection[componentId]
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -333,31 +333,59 @@ const s3UploadHandler = async action => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For new configs, "rows" is defined and enriched to be the array of rows to
|
||||
* export. For old configs it will be undefined and we need to use the legacy
|
||||
* row selection store in combination with the tableComponentId parameter.
|
||||
*/
|
||||
const exportDataHandler = async action => {
|
||||
let selection = rowSelectionStore.actions.getSelection(
|
||||
action.parameters.tableComponentId
|
||||
)
|
||||
if (selection.selectedRows && selection.selectedRows.length > 0) {
|
||||
let { tableComponentId, rows, type, columns, delimiter, customHeaders } =
|
||||
action.parameters
|
||||
let tableId
|
||||
|
||||
// Handle legacy configs using the row selection store
|
||||
if (!rows?.length) {
|
||||
const selection = rowSelectionStore.actions.getSelection(tableComponentId)
|
||||
if (selection?.selectedRows?.length) {
|
||||
rows = selection.selectedRows
|
||||
tableId = selection.tableId
|
||||
}
|
||||
}
|
||||
|
||||
// Get table ID from first row if needed
|
||||
if (!tableId) {
|
||||
tableId = rows?.[0]?.tableId
|
||||
}
|
||||
|
||||
// Handle no rows selected
|
||||
if (!rows?.length) {
|
||||
notificationStore.actions.error("Please select at least one row")
|
||||
}
|
||||
// Handle case where we're not using a DS+
|
||||
else if (!tableId) {
|
||||
notificationStore.actions.error(
|
||||
"You can only export data from table datasources"
|
||||
)
|
||||
}
|
||||
// Happy path when we have both rows and table ID
|
||||
else {
|
||||
try {
|
||||
// Flatten rows if required
|
||||
if (typeof rows[0] !== "string") {
|
||||
rows = rows.map(row => row._id)
|
||||
}
|
||||
const data = await API.exportRows({
|
||||
tableId: selection.tableId,
|
||||
rows: selection.selectedRows,
|
||||
format: action.parameters.type,
|
||||
columns: action.parameters.columns?.map(
|
||||
column => column.name || column
|
||||
),
|
||||
delimiter: action.parameters.delimiter,
|
||||
customHeaders: action.parameters.customHeaders,
|
||||
tableId,
|
||||
rows,
|
||||
format: type,
|
||||
columns: columns?.map(column => column.name || column),
|
||||
delimiter,
|
||||
customHeaders,
|
||||
})
|
||||
download(
|
||||
new Blob([data], { type: "text/plain" }),
|
||||
`${selection.tableId}.${action.parameters.type}`
|
||||
)
|
||||
download(new Blob([data], { type: "text/plain" }), `${tableId}.${type}`)
|
||||
} catch (error) {
|
||||
notificationStore.actions.error("There was an error exporting the data")
|
||||
}
|
||||
} else {
|
||||
notificationStore.actions.error("Please select at least one row")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
const { config, dispatch, selectedRows } = getContext("grid")
|
||||
const svelteDispatch = createEventDispatcher()
|
||||
|
||||
$: selectionEnabled = $config.canSelectRows || $config.canDeleteRows
|
||||
|
||||
const select = e => {
|
||||
e.stopPropagation()
|
||||
svelteDispatch("select")
|
||||
|
@ -52,7 +54,7 @@
|
|||
<div
|
||||
on:click={select}
|
||||
class="checkbox"
|
||||
class:visible={$config.canDeleteRows &&
|
||||
class:visible={selectionEnabled &&
|
||||
(disableNumber || rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
<Checkbox value={rowSelected} {disabled} />
|
||||
|
@ -60,7 +62,7 @@
|
|||
{#if !disableNumber}
|
||||
<div
|
||||
class="number"
|
||||
class:visible={!$config.canDeleteRows ||
|
||||
class:visible={!selectionEnabled ||
|
||||
!(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
{row.__idx + 1}
|
||||
|
@ -117,19 +119,11 @@
|
|||
.expand {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.expand {
|
||||
.expand:not(.visible),
|
||||
.expand:not(.visible) :global(*) {
|
||||
opacity: 0;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.expand :global(.spectrum-Icon) {
|
||||
pointer-events: none;
|
||||
}
|
||||
.expand.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
.expand.visible :global(.spectrum-Icon) {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.delete:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
export let canDeleteRows = true
|
||||
export let canEditColumns = true
|
||||
export let canSaveSchema = true
|
||||
export let canSelectRows = false
|
||||
export let stripeRows = false
|
||||
export let quiet = false
|
||||
export let collaboration = true
|
||||
|
@ -94,6 +95,7 @@
|
|||
canDeleteRows,
|
||||
canEditColumns,
|
||||
canSaveSchema,
|
||||
canSelectRows,
|
||||
stripeRows,
|
||||
quiet,
|
||||
collaboration,
|
||||
|
|
|
@ -110,12 +110,11 @@ export const deriveStores = context => {
|
|||
}
|
||||
|
||||
export const createActions = context => {
|
||||
const { focusedCellId, selectedRows, hoveredRowId } = context
|
||||
const { focusedCellId, hoveredRowId } = context
|
||||
|
||||
// Callback when leaving the grid, deselecting all focussed or selected items
|
||||
const blur = () => {
|
||||
focusedCellId.set(null)
|
||||
selectedRows.set({})
|
||||
hoveredRowId.set(null)
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit ff397e5454ad3361b25efdf14746c36dcbd3f409
|
||||
Subproject commit d3c3077011a8e20ed3c48dcd6301caca4120b6ac
|
|
@ -101,7 +101,6 @@
|
|||
"mysql2": "3.9.7",
|
||||
"node-fetch": "2.6.7",
|
||||
"object-sizeof": "2.6.1",
|
||||
"open": "8.4.0",
|
||||
"openai": "^3.2.1",
|
||||
"openapi-types": "9.3.1",
|
||||
"pg": "8.10.0",
|
||||
|
@ -113,12 +112,8 @@
|
|||
"server-destroy": "1.0.1",
|
||||
"snowflake-promise": "^4.5.0",
|
||||
"socket.io": "4.6.1",
|
||||
"sqlite3": "5.1.6",
|
||||
"swagger-parser": "10.0.3",
|
||||
"tar": "6.1.15",
|
||||
"to-json-schema": "0.2.5",
|
||||
"undici": "^6.0.1",
|
||||
"undici-types": "^6.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"validate.js": "0.13.1",
|
||||
"worker-farm": "1.7.0",
|
||||
|
@ -144,16 +139,13 @@
|
|||
"@types/supertest": "2.0.14",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/uuid": "8.3.4",
|
||||
"apidoc": "0.50.4",
|
||||
"copyfiles": "2.4.1",
|
||||
"docker-compose": "0.23.17",
|
||||
"jest": "29.7.0",
|
||||
"jest-openapi": "0.14.2",
|
||||
"jest-runner": "29.7.0",
|
||||
"nock": "13.5.4",
|
||||
"nodemon": "2.0.15",
|
||||
"openapi-typescript": "5.2.0",
|
||||
"path-to-regexp": "6.2.0",
|
||||
"rimraf": "3.0.2",
|
||||
"supertest": "6.3.3",
|
||||
"swagger-jsdoc": "6.1.0",
|
||||
|
|
|
@ -279,8 +279,7 @@ export async function trigger(ctx: UserCtx) {
|
|||
{
|
||||
fields: ctx.request.body.fields,
|
||||
timeout:
|
||||
ctx.request.body.timeout * 1000 ||
|
||||
env.getDefaults().AUTOMATION_SYNC_TIMEOUT,
|
||||
ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT,
|
||||
},
|
||||
{ getResponses: true }
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ function parseIntSafe(number?: string) {
|
|||
|
||||
const DEFAULTS = {
|
||||
QUERY_THREAD_TIMEOUT: 15000,
|
||||
AUTOMATION_THREAD_TIMEOUT: 12000,
|
||||
AUTOMATION_THREAD_TIMEOUT: 15000,
|
||||
AUTOMATION_SYNC_TIMEOUT: 120000,
|
||||
AUTOMATION_MAX_ITERATIONS: 200,
|
||||
JS_PER_EXECUTION_TIME_LIMIT_MS: 1500,
|
||||
|
@ -34,6 +34,10 @@ const DEFAULTS = {
|
|||
const QUERY_THREAD_TIMEOUT =
|
||||
parseIntSafe(process.env.QUERY_THREAD_TIMEOUT) ||
|
||||
DEFAULTS.QUERY_THREAD_TIMEOUT
|
||||
const DEFAULT_AUTOMATION_TIMEOUT =
|
||||
QUERY_THREAD_TIMEOUT > DEFAULTS.AUTOMATION_THREAD_TIMEOUT
|
||||
? QUERY_THREAD_TIMEOUT
|
||||
: DEFAULTS.AUTOMATION_THREAD_TIMEOUT
|
||||
const environment = {
|
||||
// features
|
||||
APP_FEATURES: process.env.APP_FEATURES,
|
||||
|
@ -75,9 +79,7 @@ const environment = {
|
|||
QUERY_THREAD_TIMEOUT: QUERY_THREAD_TIMEOUT,
|
||||
AUTOMATION_THREAD_TIMEOUT:
|
||||
parseIntSafe(process.env.AUTOMATION_THREAD_TIMEOUT) ||
|
||||
DEFAULTS.AUTOMATION_THREAD_TIMEOUT > QUERY_THREAD_TIMEOUT
|
||||
? DEFAULTS.AUTOMATION_THREAD_TIMEOUT
|
||||
: QUERY_THREAD_TIMEOUT,
|
||||
DEFAULT_AUTOMATION_TIMEOUT,
|
||||
BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
|
||||
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
|
||||
PLUGINS_DIR: process.env.PLUGINS_DIR || DEFAULTS.PLUGINS_DIR,
|
||||
|
|
Loading…
Reference in a new issue