Merge branch 'master' of github.com:Budibase/budibase into update-aliasing

This commit is contained in:
Andrew Kingston 2024-12-30 15:59:02 +00:00
commit b91f915e40
No known key found for this signature in database
87 changed files with 2036 additions and 1215 deletions

View File

@ -10,7 +10,7 @@
}, },
"dependencies": { "dependencies": {
"bulma": "^0.9.3", "bulma": "^0.9.3",
"next": "14.2.10", "next": "14.2.15",
"node-fetch": "^3.2.10", "node-fetch": "^3.2.10",
"sass": "^1.52.3", "sass": "^1.52.3",
"react": "17.0.2", "react": "17.0.2",

View File

@ -46,10 +46,10 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@next/env@14.2.10": "@next/env@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.10.tgz#1d3178340028ced2d679f84140877db4f420333c" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.15.tgz#06d984e37e670d93ddd6790af1844aeb935f332f"
integrity sha512-dZIu93Bf5LUtluBXIv4woQw2cZVZ2DJTjax5/5DOs3lzEOeKLy7GxRSr4caK9/SCPdaW6bCgpye6+n4Dh9oJPw== integrity sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==
"@next/eslint-plugin-next@12.1.0": "@next/eslint-plugin-next@12.1.0":
version "12.1.0" version "12.1.0"
@ -58,50 +58,50 @@
dependencies: dependencies:
glob "7.1.7" glob "7.1.7"
"@next/swc-darwin-arm64@14.2.10": "@next/swc-darwin-arm64@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.10.tgz#49d10ca4086fbd59ee68e204f75d7136eda2aa80" resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz#6386d585f39a1c490c60b72b1f76612ba4434347"
integrity sha512-V3z10NV+cvMAfxQUMhKgfQnPbjw+Ew3cnr64b0lr8MDiBJs3eLnM6RpGC46nhfMZsiXgQngCJKWGTC/yDcgrDQ== integrity sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==
"@next/swc-darwin-x64@14.2.10": "@next/swc-darwin-x64@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.10.tgz#0ebeae3afb8eac433882b79543295ab83624a1a8" resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz#b7baeedc6a28f7545ad2bc55adbab25f7b45cb89"
integrity sha512-Y0TC+FXbFUQ2MQgimJ/7Ina2mXIKhE7F+GUe1SgnzRmwFY3hX2z8nyVCxE82I2RicspdkZnSWMn4oTjIKz4uzA== integrity sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==
"@next/swc-linux-arm64-gnu@14.2.10": "@next/swc-linux-arm64-gnu@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.10.tgz#7e602916d2fb55a3c532f74bed926a0137c16f20" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz#fa13c59d3222f70fb4cb3544ac750db2c6e34d02"
integrity sha512-ZfQ7yOy5zyskSj9rFpa0Yd7gkrBnJTkYVSya95hX3zeBG9E55Z6OTNPn1j2BTFWvOVVj65C3T+qsjOyVI9DQpA== integrity sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==
"@next/swc-linux-arm64-musl@14.2.10": "@next/swc-linux-arm64-musl@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.10.tgz#6b143f628ccee490b527562e934f8de578d4be47" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz#30e45b71831d9a6d6d18d7ac7d611a8d646a17f9"
integrity sha512-n2i5o3y2jpBfXFRxDREr342BGIQCJbdAUi/K4q6Env3aSx8erM9VuKXHw5KNROK9ejFSPf0LhoSkU/ZiNdacpQ== integrity sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==
"@next/swc-linux-x64-gnu@14.2.10": "@next/swc-linux-x64-gnu@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.10.tgz#086f2f16a0678890a1eb46518c4dda381b046082" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz#5065db17fc86f935ad117483f21f812dc1b39254"
integrity sha512-GXvajAWh2woTT0GKEDlkVhFNxhJS/XdDmrVHrPOA83pLzlGPQnixqxD8u3bBB9oATBKB//5e4vpACnx5Vaxdqg== integrity sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==
"@next/swc-linux-x64-musl@14.2.10": "@next/swc-linux-x64-musl@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.10.tgz#1befef10ed8dbcc5047b5d637a25ae3c30a0bfc3" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz#3c4a4568d8be7373a820f7576cf33388b5dab47e"
integrity sha512-opFFN5B0SnO+HTz4Wq4HaylXGFV+iHrVxd3YvREUX9K+xfc4ePbRrxqOuPOFjtSuiVouwe6uLeDtabjEIbkmDA== integrity sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==
"@next/swc-win32-arm64-msvc@14.2.10": "@next/swc-win32-arm64-msvc@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.10.tgz#731f52c3ae3c56a26cf21d474b11ae1529531209" resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz#fb812cc4ca0042868e32a6a021da91943bb08b98"
integrity sha512-9NUzZuR8WiXTvv+EiU/MXdcQ1XUvFixbLIMNQiVHuzs7ZIFrJDLJDaOF1KaqttoTujpcxljM/RNAOmw1GhPPQQ== integrity sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==
"@next/swc-win32-ia32-msvc@14.2.10": "@next/swc-win32-ia32-msvc@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.10.tgz#32723ef7f04e25be12af357cc72ddfdd42fd1041" resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz#ec26e6169354f8ced240c1427be7fd485c5df898"
integrity sha512-fr3aEbSd1GeW3YUMBkWAu4hcdjZ6g4NBl1uku4gAn661tcxd1bHs1THWYzdsbTRLcCKLjrDZlNp6j2HTfrw+Bg== integrity sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==
"@next/swc-win32-x64-msvc@14.2.10": "@next/swc-win32-x64-msvc@14.2.15":
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.10.tgz#ee1d036cb5ec871816f96baee7991035bb242455" resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz#18d68697002b282006771f8d92d79ade9efd35c4"
integrity sha512-UjeVoRGKNL2zfbcQ6fscmgjBAS/inHBh63mjIlfPg/NG8Yn2ztqylXt5qilYb6hoHIwaU2ogHknHWWmahJjgZQ== integrity sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
@ -1253,12 +1253,12 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
next@14.2.10: next@14.2.15:
version "14.2.10" version "14.2.15"
resolved "https://registry.yarnpkg.com/next/-/next-14.2.10.tgz#331981a4fecb1ae8af1817d4db98fc9687ee1cb6" resolved "https://registry.yarnpkg.com/next/-/next-14.2.15.tgz#348e5603e22649775d19c785c09a89c9acb5189a"
integrity sha512-sDDExXnh33cY3RkS9JuFEKaS4HmlWmDKP1VJioucCG6z5KuA008DPsDZOzi8UfqEk3Ii+2NCQSJrfbEWtZZfww== integrity sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==
dependencies: dependencies:
"@next/env" "14.2.10" "@next/env" "14.2.15"
"@swc/helpers" "0.5.5" "@swc/helpers" "0.5.5"
busboy "1.6.0" busboy "1.6.0"
caniuse-lite "^1.0.30001579" caniuse-lite "^1.0.30001579"
@ -1266,15 +1266,15 @@ next@14.2.10:
postcss "8.4.31" postcss "8.4.31"
styled-jsx "5.1.1" styled-jsx "5.1.1"
optionalDependencies: optionalDependencies:
"@next/swc-darwin-arm64" "14.2.10" "@next/swc-darwin-arm64" "14.2.15"
"@next/swc-darwin-x64" "14.2.10" "@next/swc-darwin-x64" "14.2.15"
"@next/swc-linux-arm64-gnu" "14.2.10" "@next/swc-linux-arm64-gnu" "14.2.15"
"@next/swc-linux-arm64-musl" "14.2.10" "@next/swc-linux-arm64-musl" "14.2.15"
"@next/swc-linux-x64-gnu" "14.2.10" "@next/swc-linux-x64-gnu" "14.2.15"
"@next/swc-linux-x64-musl" "14.2.10" "@next/swc-linux-x64-musl" "14.2.15"
"@next/swc-win32-arm64-msvc" "14.2.10" "@next/swc-win32-arm64-msvc" "14.2.15"
"@next/swc-win32-ia32-msvc" "14.2.10" "@next/swc-win32-ia32-msvc" "14.2.15"
"@next/swc-win32-x64-msvc" "14.2.10" "@next/swc-win32-x64-msvc" "14.2.15"
node-domexception@^1.0.0: node-domexception@^1.0.0:
version "1.0.0" version "1.0.0"

View File

@ -51,6 +51,7 @@
} }
input.hide-arrows { input.hide-arrows {
-moz-appearance: textfield; -moz-appearance: textfield;
appearance: textfield;
} }
input[type="time"]::-webkit-calendar-picker-indicator { input[type="time"]::-webkit-calendar-picker-indicator {
display: none; display: none;

View File

@ -39,6 +39,7 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
-webkit-appearance: none; -webkit-appearance: none;
appearance: none;
background: transparent; background: transparent;
} }
input::-webkit-slider-thumb { input::-webkit-slider-thumb {

View File

@ -124,8 +124,6 @@
.spectrum-Tabs-selectionIndicator.emphasized { .spectrum-Tabs-selectionIndicator.emphasized {
background-color: var(--spectrum-global-color-blue-400); background-color: var(--spectrum-global-color-blue-400);
} }
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
}
.noHorizPadding { .noHorizPadding {
padding: 0; padding: 0;
} }

View File

@ -134,6 +134,7 @@
.spectrum-Tooltip-label { .spectrum-Tooltip-label {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
font-size: 12px; font-size: 12px;

View File

@ -94,6 +94,7 @@
"@sveltejs/vite-plugin-svelte": "1.4.0", "@sveltejs/vite-plugin-svelte": "1.4.0",
"@testing-library/jest-dom": "6.4.2", "@testing-library/jest-dom": "6.4.2",
"@testing-library/svelte": "^4.1.0", "@testing-library/svelte": "^4.1.0",
"@types/shortid": "^2.2.0",
"babel-jest": "^29.6.2", "babel-jest": "^29.6.2",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "29.7.0", "jest": "29.7.0",

View File

@ -22,7 +22,7 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: blockRefs = $selectedAutomation?.blockRefs || {} $: blockRefs = $selectedAutomation?.blockRefs || {}
$: stepNames = automation?.definition.stepNames $: stepNames = automation?.definition.stepNames || {}
$: allSteps = automation?.definition.steps || [] $: allSteps = automation?.definition.steps || []
$: automationName = itemName || stepNames?.[block.id] || block?.name || "" $: automationName = itemName || stepNames?.[block.id] || block?.name || ""
$: automationNameError = getAutomationNameError(automationName) $: automationNameError = getAutomationNameError(automationName)
@ -64,7 +64,7 @@
const getAutomationNameError = name => { const getAutomationNameError = name => {
const duplicateError = const duplicateError =
"This name already exists, please enter a unique name" "This name already exists, please enter a unique name"
if (stepNames && editing) { if (editing) {
for (const [key, value] of Object.entries(stepNames)) { for (const [key, value] of Object.entries(stepNames)) {
if (name !== block.name && name === value && key !== block.id) { if (name !== block.name && name === value && key !== block.id) {
return duplicateError return duplicateError

View File

@ -33,7 +33,7 @@
...datasource, ...datasource,
name, name,
} }
await datasources.update({ await datasources.save({
datasource: updatedDatasource, datasource: updatedDatasource,
integration: integrationForDatasource(get(integrations), datasource), integration: integrationForDatasource(get(integrations), datasource),
}) })

View File

@ -12,6 +12,7 @@
export let name export let name
export let config export let config
export let showModal = () => {} export let showModal = () => {}
export let placeholder
const selectComponent = type => { const selectComponent = type => {
if (type === "object") { if (type === "object") {
@ -40,6 +41,7 @@
{name} {name}
{config} {config}
{showModal} {showModal}
{placeholder}
on:blur on:blur
on:change on:change
/> />

View File

@ -5,11 +5,12 @@
export let name export let name
export let value export let value
export let error export let error
export let placeholder
</script> </script>
<div class="form-row"> <div class="form-row">
<Label>{name}</Label> <Label>{name}</Label>
<TextArea on:blur on:change {type} {value} {error} /> <TextArea on:blur on:change {type} {value} {error} {placeholder} />
</div> </div>
<style> <style>

View File

@ -6,6 +6,7 @@
export let value export let value
export let error export let error
export let config export let config
export let placeholder
</script> </script>
<div class="form-row"> <div class="form-row">
@ -17,6 +18,7 @@
{type} {type}
value={value || undefined} value={value || undefined}
{error} {error}
{placeholder}
/> />
</div> </div>

View File

@ -6,6 +6,7 @@
export let name export let name
export let value export let value
export let error export let error
export let placeholder
export let showModal = () => {} export let showModal = () => {}
async function handleUpgradePanel() { async function handleUpgradePanel() {
@ -22,6 +23,7 @@
type={type === "port" ? "string" : type} type={type === "port" ? "string" : type}
{value} {value}
{error} {error}
{placeholder}
variables={$environment.variables} variables={$environment.variables}
environmentVariablesEnabled={$licensing.environmentVariablesEnabled} environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
{showModal} {showModal}

View File

@ -85,7 +85,7 @@
/> />
{/if} {/if}
{#each $configStore.validatedConfig as { type, key, value, error, name, hidden, config }} {#each $configStore.validatedConfig as { type, key, value, error, name, hidden, config, placeholder }}
{#if hidden === undefined || !eval(processStringSync(hidden, $configStore.config))} {#if hidden === undefined || !eval(processStringSync(hidden, $configStore.config))}
<ConfigInput <ConfigInput
{type} {type}
@ -93,6 +93,7 @@
{error} {error}
{name} {name}
{config} {config}
{placeholder}
showModal={() => showModal={() =>
showModal(newValue => configStore.updateFieldValue(key, newValue))} showModal(newValue => configStore.updateFieldValue(key, newValue))}
on:blur={() => configStore.markFieldActive(key)} on:blur={() => configStore.markFieldActive(key)}

View File

@ -114,6 +114,7 @@ export const createValidatedConfigStore = (integration, config) => {
value: getValue(), value: getValue(),
error: $errorsStore[key], error: $errorsStore[key],
name: capitalise(properties.display || key), name: capitalise(properties.display || key),
placeholder: properties.placeholder,
type: properties.type, type: properties.type,
hidden: properties.hidden, hidden: properties.hidden,
config: properties.config, config: properties.config,

View File

@ -41,7 +41,7 @@
get(integrations), get(integrations),
datasource datasource
) )
await datasources.update({ datasource, integration }) await datasources.save({ datasource, integration })
await afterSave({ datasource, action }) await afterSave({ datasource, action })
} catch (err) { } catch (err) {

View File

@ -176,7 +176,7 @@
notifications.success(`Request saved successfully`) notifications.success(`Request saved successfully`)
if (dynamicVariables) { if (dynamicVariables) {
datasource.config.dynamicVariables = rebuildVariables(saveId) datasource.config.dynamicVariables = rebuildVariables(saveId)
datasource = await datasources.update({ datasource = await datasources.save({
integration: integrationInfo, integration: integrationInfo,
datasource, datasource,
}) })

View File

@ -1507,7 +1507,12 @@ export const updateReferencesInObject = ({
// Migrate references // Migrate references
// Switch all bindings to reference their ids // Switch all bindings to reference their ids
export const migrateReferencesInObject = ({ obj, label = "steps", steps }) => { export const migrateReferencesInObject = ({
obj,
label = "steps",
steps,
originalIndex,
}) => {
const stepIndexRegex = new RegExp(`{{\\s*${label}\\.(\\d+)\\.`, "g") const stepIndexRegex = new RegExp(`{{\\s*${label}\\.(\\d+)\\.`, "g")
const updateActionStep = (str, index, replaceWith) => const updateActionStep = (str, index, replaceWith) =>
str.replace(`{{ ${label}.${index}.`, `{{ ${label}.${replaceWith}.`) str.replace(`{{ ${label}.${index}.`, `{{ ${label}.${replaceWith}.`)
@ -1528,6 +1533,7 @@ export const migrateReferencesInObject = ({ obj, label = "steps", steps }) => {
migrateReferencesInObject({ migrateReferencesInObject({
obj: obj[key], obj: obj[key],
steps, steps,
originalIndex,
}) })
} }
} }

View File

@ -368,20 +368,22 @@
const payload = [ const payload = [
{ {
email: newUserEmail, email: newUserEmail,
builder: { userInfo: {
global: creationRoleType === Constants.BudibaseRoles.Admin, builder: {
creator: creationRoleType === Constants.BudibaseRoles.Creator, global: creationRoleType === Constants.BudibaseRoles.Admin,
creator: creationRoleType === Constants.BudibaseRoles.Creator,
},
admin: { global: creationRoleType === Constants.BudibaseRoles.Admin },
}, },
admin: { global: creationRoleType === Constants.BudibaseRoles.Admin },
}, },
] ]
const notCreatingAdmin = creationRoleType !== Constants.BudibaseRoles.Admin const notCreatingAdmin = creationRoleType !== Constants.BudibaseRoles.Admin
const isCreator = creationAccessType === Constants.Roles.CREATOR const isCreator = creationAccessType === Constants.Roles.CREATOR
if (notCreatingAdmin && isCreator) { if (notCreatingAdmin && isCreator) {
payload[0].builder.apps = [prodAppId] payload[0].userInfo.builder.apps = [prodAppId]
} else if (notCreatingAdmin && !isCreator) { } else if (notCreatingAdmin && !isCreator) {
payload[0].apps = { [prodAppId]: creationAccessType } payload[0].userInfo.apps = { [prodAppId]: creationAccessType }
} }
let userInviteResponse let userInviteResponse

View File

@ -13,7 +13,7 @@
async function saveDatasource({ config, name }) { async function saveDatasource({ config, name }) {
try { try {
await datasources.update({ await datasources.save({
integration, integration,
datasource: { ...datasource, config, name }, datasource: { ...datasource, config, name },
}) })

View File

@ -16,7 +16,7 @@
get(integrations), get(integrations),
updatedDatasource updatedDatasource
) )
await datasources.update({ datasource: updatedDatasource, integration }) await datasources.save({ datasource: updatedDatasource, integration })
notifications.success( notifications.success(
`Datasource ${updatedDatasource.name} updated successfully` `Datasource ${updatedDatasource.name} updated successfully`
) )

View File

@ -1,40 +1,22 @@
import { writable, Writable } from "svelte/store" import { writable, Writable, Readable } from "svelte/store"
interface BudiStoreOpts { interface BudiStoreOpts {
debug?: boolean debug?: boolean
} }
export default class BudiStore<T> implements Writable<T> { export class BudiStore<T> {
store: Writable<T> store: Writable<T>
subscribe: Writable<T>["subscribe"] subscribe: Writable<T>["subscribe"]
update: Writable<T>["update"] update: Writable<T>["update"]
set: Writable<T>["set"] set: Writable<T>["set"]
constructor(init: T, opts?: BudiStoreOpts) { constructor(init: T, opts?: BudiStoreOpts) {
const store = writable<T>(init) this.store = writable<T>(init)
/**
* Internal Svelte store
*/
this.store = store
/**
* Exposes the svelte subscribe fn to allow $ notation access
* @example
* $navigation.selectedScreenId
*/
this.subscribe = this.store.subscribe this.subscribe = this.store.subscribe
/**
* Exposes the svelte update fn.
* *Store modification should be kept to a minimum
*/
this.update = this.store.update this.update = this.store.update
this.set = this.store.set this.set = this.store.set
/** // Optional debug mode to output the store updates to console
* Optional debug mode to output the store updates to console
*/
if (opts?.debug) { if (opts?.debug) {
this.subscribe(state => { this.subscribe(state => {
console.warn(`${this.constructor.name}`, state) console.warn(`${this.constructor.name}`, state)
@ -42,3 +24,18 @@ export default class BudiStore<T> implements Writable<T> {
} }
} }
} }
export class DerivedBudiStore<T, DerivedT extends T> extends BudiStore<T> {
derivedStore: Readable<DerivedT>
subscribe: Readable<DerivedT>["subscribe"]
constructor(
init: T,
makeDerivedStore: (store: Writable<T>) => Readable<DerivedT>,
opts?: BudiStoreOpts
) {
super(init, opts)
this.derivedStore = makeDerivedStore(this.store)
this.subscribe = this.derivedStore.subscribe
}
}

View File

@ -1,7 +1,54 @@
import { API } from "@/api" import { API } from "@/api"
import BudiStore from "../BudiStore" import { BudiStore } from "../BudiStore"
import {
App,
AppFeatures,
AppIcon,
AutomationSettings,
Plugin,
} from "@budibase/types"
export const INITIAL_APP_META_STATE = { interface ClientFeatures {
spectrumThemes: boolean
intelligentLoading: boolean
deviceAwareness: boolean
state: boolean
rowSelection: boolean
customThemes: boolean
devicePreview: boolean
messagePassing: boolean
continueIfAction: boolean
showNotificationAction: boolean
sidePanel: boolean
}
interface TypeSupportPresets {
[key: string]: any
}
interface AppMetaState {
appId: string
name: string
url: string
libraries: string[]
clientFeatures: ClientFeatures
typeSupportPresets: TypeSupportPresets
features: AppFeatures
clientLibPath: string
hasLock: boolean
appInstance: { _id: string } | null
initialised: boolean
hasAppPackage: boolean
usedPlugins: Plugin[] | null
automations: AutomationSettings
routes: { [key: string]: any }
version?: string
revertableVersion?: string
upgradableVersion?: string
icon?: AppIcon
}
export const INITIAL_APP_META_STATE: AppMetaState = {
appId: "", appId: "",
name: "", name: "",
url: "", url: "",
@ -34,23 +81,27 @@ export const INITIAL_APP_META_STATE = {
routes: {}, routes: {},
} }
export class AppMetaStore extends BudiStore { export class AppMetaStore extends BudiStore<AppMetaState> {
constructor() { constructor() {
super(INITIAL_APP_META_STATE) super(INITIAL_APP_META_STATE)
} }
reset() { reset(): void {
this.store.set({ ...INITIAL_APP_META_STATE }) this.store.set({ ...INITIAL_APP_META_STATE })
} }
syncAppPackage(pkg) { syncAppPackage(pkg: {
application: App
clientLibPath: string
hasLock: boolean
}): void {
const { application: app, clientLibPath, hasLock } = pkg const { application: app, clientLibPath, hasLock } = pkg
this.update(state => ({ this.update(state => ({
...state, ...state,
name: app.name, name: app.name,
appId: app.appId, appId: app.appId,
url: app.url, url: app.url || "",
hasLock, hasLock,
clientLibPath, clientLibPath,
libraries: app.componentLibraries, libraries: app.componentLibraries,
@ -58,8 +109,8 @@ export class AppMetaStore extends BudiStore {
appInstance: app.instance, appInstance: app.instance,
revertableVersion: app.revertableVersion, revertableVersion: app.revertableVersion,
upgradableVersion: app.upgradableVersion, upgradableVersion: app.upgradableVersion,
usedPlugins: app.usedPlugins, usedPlugins: app.usedPlugins || null,
icon: app.icon || {}, icon: app.icon,
features: { features: {
...INITIAL_APP_META_STATE.features, ...INITIAL_APP_META_STATE.features,
...app.features, ...app.features,
@ -70,7 +121,7 @@ export class AppMetaStore extends BudiStore {
})) }))
} }
syncClientFeatures(features) { syncClientFeatures(features: Partial<ClientFeatures>): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
clientFeatures: { clientFeatures: {
@ -80,14 +131,14 @@ export class AppMetaStore extends BudiStore {
})) }))
} }
syncClientTypeSupportPresets(typeSupportPresets) { syncClientTypeSupportPresets(typeSupportPresets: TypeSupportPresets): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
typeSupportPresets, typeSupportPresets,
})) }))
} }
async syncAppRoutes() { async syncAppRoutes(): Promise<void> {
const resp = await API.fetchAppRoutes() const resp = await API.fetchAppRoutes()
this.update(state => ({ this.update(state => ({
...state, ...state,
@ -96,7 +147,7 @@ export class AppMetaStore extends BudiStore {
} }
// Returned from socket // Returned from socket
syncMetadata(metadata) { syncMetadata(metadata: { name: string; url: string; icon?: AppIcon }): void {
const { name, url, icon } = metadata const { name, url, icon } = metadata
this.update(state => ({ this.update(state => ({
...state, ...state,

View File

@ -1,10 +1,28 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { createBuilderWebsocket } from "./websocket.js" import { createBuilderWebsocket } from "./websocket.js"
import { Socket } from "socket.io-client"
import { BuilderSocketEvent } from "@budibase/shared-core" import { BuilderSocketEvent } from "@budibase/shared-core"
import BudiStore from "../BudiStore.js" import { BudiStore } from "../BudiStore.js"
import { TOUR_KEYS } from "@/components/portal/onboarding/tours.js" import { TOUR_KEYS } from "@/components/portal/onboarding/tours.js"
import { App } from "@budibase/types"
export const INITIAL_BUILDER_STATE = { interface BuilderState {
previousTopNavPath: Record<string, string>
highlightedSetting: {
key: string
type: "info" | string
} | null
propertyFocus: string | null
builderSidePanel: boolean
onboarding: boolean
tourNodes: Record<string, HTMLElement> | null
tourKey: string | null
tourStepKey: string | null
hoveredComponentId: string | null
websocket?: Socket
}
export const INITIAL_BUILDER_STATE: BuilderState = {
previousTopNavPath: {}, previousTopNavPath: {},
highlightedSetting: null, highlightedSetting: null,
propertyFocus: null, propertyFocus: null,
@ -16,7 +34,9 @@ export const INITIAL_BUILDER_STATE = {
hoveredComponentId: null, hoveredComponentId: null,
} }
export class BuilderStore extends BudiStore { export class BuilderStore extends BudiStore<BuilderState> {
websocket?: Socket
constructor() { constructor() {
super({ ...INITIAL_BUILDER_STATE }) super({ ...INITIAL_BUILDER_STATE })
@ -32,11 +52,9 @@ export class BuilderStore extends BudiStore {
this.registerTourNode = this.registerTourNode.bind(this) this.registerTourNode = this.registerTourNode.bind(this)
this.destroyTourNode = this.destroyTourNode.bind(this) this.destroyTourNode = this.destroyTourNode.bind(this)
this.startBuilderOnboarding = this.startBuilderOnboarding.bind(this) this.startBuilderOnboarding = this.startBuilderOnboarding.bind(this)
this.websocket
} }
init(app) { init(app: App): void {
if (!app?.appId) { if (!app?.appId) {
console.error("BuilderStore: No appId supplied for websocket") console.error("BuilderStore: No appId supplied for websocket")
return return
@ -46,45 +64,46 @@ export class BuilderStore extends BudiStore {
} }
} }
refresh() { refresh(): void {
this.store.set(this.store.get()) const currentState = get(this.store)
this.store.set(currentState)
} }
reset() { reset(): void {
this.store.set({ ...INITIAL_BUILDER_STATE }) this.store.set({ ...INITIAL_BUILDER_STATE })
this.websocket?.disconnect() this.websocket?.disconnect()
this.websocket = null this.websocket = undefined
} }
highlightSetting(key, type) { highlightSetting(key?: string, type?: string): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
highlightedSetting: key ? { key, type: type || "info" } : null, highlightedSetting: key ? { key, type: type || "info" } : null,
})) }))
} }
propertyFocus(key) { propertyFocus(key: string | null): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
propertyFocus: key, propertyFocus: key,
})) }))
} }
showBuilderSidePanel() { showBuilderSidePanel(): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
builderSidePanel: true, builderSidePanel: true,
})) }))
} }
hideBuilderSidePanel() { hideBuilderSidePanel(): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
builderSidePanel: false, builderSidePanel: false,
})) }))
} }
setPreviousTopNavPath(route, url) { setPreviousTopNavPath(route: string, url: string): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
previousTopNavPath: { previousTopNavPath: {
@ -94,13 +113,13 @@ export class BuilderStore extends BudiStore {
})) }))
} }
selectResource(id) { selectResource(id: string): void {
this.websocket.emit(BuilderSocketEvent.SelectResource, { this.websocket?.emit(BuilderSocketEvent.SelectResource, {
resourceId: id, resourceId: id,
}) })
} }
registerTourNode(tourStepKey, node) { registerTourNode(tourStepKey: string, node: HTMLElement): void {
this.update(state => { this.update(state => {
const update = { const update = {
...state, ...state,
@ -113,7 +132,7 @@ export class BuilderStore extends BudiStore {
}) })
} }
destroyTourNode(tourStepKey) { destroyTourNode(tourStepKey: string): void {
const store = get(this.store) const store = get(this.store)
if (store.tourNodes?.[tourStepKey]) { if (store.tourNodes?.[tourStepKey]) {
const nodes = { ...store.tourNodes } const nodes = { ...store.tourNodes }
@ -125,7 +144,7 @@ export class BuilderStore extends BudiStore {
} }
} }
startBuilderOnboarding() { startBuilderOnboarding(): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
onboarding: true, onboarding: true,
@ -133,19 +152,19 @@ export class BuilderStore extends BudiStore {
})) }))
} }
endBuilderOnboarding() { endBuilderOnboarding(): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
onboarding: false, onboarding: false,
})) }))
} }
setTour(tourKey) { setTour(tourKey?: string | null): void {
this.update(state => ({ this.update(state => ({
...state, ...state,
tourStepKey: null, tourStepKey: null,
tourNodes: null, tourNodes: null,
tourKey: tourKey, tourKey: tourKey || null,
})) }))
} }
} }

View File

@ -28,7 +28,7 @@ import {
DB_TYPE_INTERNAL, DB_TYPE_INTERNAL,
DB_TYPE_EXTERNAL, DB_TYPE_EXTERNAL,
} from "@/constants/backend" } from "@/constants/backend"
import BudiStore from "../BudiStore" import { BudiStore } from "../BudiStore"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import { FieldType } from "@budibase/types" import { FieldType } from "@budibase/types"
import { utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"

View File

@ -1,28 +0,0 @@
import { writable } from "svelte/store"
export const INITIAL_CONTEXT_MENU_STATE = {
id: null,
items: [],
position: { x: 0, y: 0 },
visible: false,
}
export function createViewsStore() {
const store = writable({ ...INITIAL_CONTEXT_MENU_STATE })
const open = (id, items, position) => {
store.set({ id, items, position, visible: true })
}
const close = () => {
store.set({ ...INITIAL_CONTEXT_MENU_STATE })
}
return {
subscribe: store.subscribe,
open,
close,
}
}
export const contextMenuStore = createViewsStore()

View File

@ -0,0 +1,46 @@
import { writable } from "svelte/store"
interface Position {
x: number
y: number
}
interface MenuItem {
label: string
icon?: string
action: () => void
}
interface ContextMenuState {
id: string | null
items: MenuItem[]
position: Position
visible: boolean
}
export const INITIAL_CONTEXT_MENU_STATE: ContextMenuState = {
id: null,
items: [],
position: { x: 0, y: 0 },
visible: false,
}
export function createViewsStore() {
const store = writable<ContextMenuState>({ ...INITIAL_CONTEXT_MENU_STATE })
const open = (id: string, items: MenuItem[], position: Position): void => {
store.set({ id, items, position, visible: true })
}
const close = (): void => {
store.set({ ...INITIAL_CONTEXT_MENU_STATE })
}
return {
subscribe: store.subscribe,
open,
close,
}
}
export const contextMenuStore = createViewsStore()

View File

@ -1,4 +1,4 @@
import { writable, derived, get } from "svelte/store" import { derived, get, Writable } from "svelte/store"
import { import {
IntegrationTypes, IntegrationTypes,
DEFAULT_BB_DATASOURCE_ID, DEFAULT_BB_DATASOURCE_ID,
@ -16,11 +16,7 @@ import {
SourceName, SourceName,
} from "@budibase/types" } from "@budibase/types"
import { TableNames } from "@/constants" import { TableNames } from "@/constants"
import { DerivedBudiStore } from "@/stores/BudiStore"
// when building the internal DS - seems to represent it slightly differently to the backend typing of a DS
interface InternalDatasource extends Omit<Datasource, "entities"> {
entities: Table[]
}
class TableImportError extends Error { class TableImportError extends Error {
errors: Record<string, string> errors: Record<string, string>
@ -40,102 +36,138 @@ class TableImportError extends Error {
} }
} }
interface DatasourceStore { // when building the internal DS - seems to represent it slightly differently to the backend typing of a DS
list: Datasource[] interface InternalDatasource extends Omit<Datasource, "entities"> {
entities: Table[]
}
interface BuilderDatasourceStore {
rawList: Datasource[]
selectedDatasourceId: null | string selectedDatasourceId: null | string
} }
export function createDatasourcesStore() { interface DerivedDatasourceStore extends BuilderDatasourceStore {
const store = writable<DatasourceStore>({ list: (Datasource | InternalDatasource)[]
list: [], selected?: Datasource | InternalDatasource
selectedDatasourceId: null, hasDefaultData: boolean
}) hasData: boolean
}
const derivedStore = derived([store, tables], ([$store, $tables]) => { export class DatasourceStore extends DerivedBudiStore<
// Set the internal datasource entities from the table list, which we're BuilderDatasourceStore,
// able to keep updated unlike the egress generated definition of the DerivedDatasourceStore
// internal datasource > {
let internalDS: Datasource | InternalDatasource | undefined = constructor() {
$store.list?.find(ds => ds._id === BUDIBASE_INTERNAL_DB_ID) const makeDerivedStore = (store: Writable<BuilderDatasourceStore>) => {
let otherDS = $store.list?.filter(ds => ds._id !== BUDIBASE_INTERNAL_DB_ID) return derived([store, tables], ([$store, $tables]) => {
if (internalDS) { // Set the internal datasource entities from the table list, which we're
const tables: Table[] = $tables.list?.filter((table: Table) => { // able to keep updated unlike the egress generated definition of the
return ( // internal datasource
table.sourceId === BUDIBASE_INTERNAL_DB_ID && let internalDS: Datasource | InternalDatasource | undefined =
table._id !== TableNames.USERS $store.rawList?.find(ds => ds._id === BUDIBASE_INTERNAL_DB_ID)
let otherDS = $store.rawList?.filter(
ds => ds._id !== BUDIBASE_INTERNAL_DB_ID
) )
if (internalDS) {
const tables: Table[] = $tables.list?.filter((table: Table) => {
return (
table.sourceId === BUDIBASE_INTERNAL_DB_ID &&
table._id !== TableNames.USERS
)
})
internalDS = {
...internalDS,
entities: tables,
}
}
// Build up enriched DS list
// Only add the internal DS if we have at least one non-users table
let list: (InternalDatasource | Datasource)[] = []
if (internalDS?.entities?.length) {
list.push(internalDS)
}
list = list.concat(otherDS || [])
return {
...$store,
list,
selected: list?.find(ds => ds._id === $store.selectedDatasourceId),
hasDefaultData: list?.some(ds => ds._id === DEFAULT_BB_DATASOURCE_ID),
hasData: list?.length > 0,
}
}) })
internalDS = {
...internalDS,
entities: tables,
}
} }
// Build up enriched DS list super(
// Only add the internal DS if we have at least one non-users table {
let list: (InternalDatasource | Datasource)[] = [] rawList: [],
if (internalDS?.entities?.length) { selectedDatasourceId: null,
list.push(internalDS) },
} makeDerivedStore
list = list.concat(otherDS || []) )
return { this.fetch = this.fetch.bind(this)
...$store, this.init = this.fetch.bind(this)
list, this.select = this.select.bind(this)
selected: list?.find(ds => ds._id === $store.selectedDatasourceId), this.updateSchema = this.updateSchema.bind(this)
hasDefaultData: list?.some(ds => ds._id === DEFAULT_BB_DATASOURCE_ID), this.create = this.create.bind(this)
hasData: list?.length > 0, this.delete = this.deleteDatasource.bind(this)
} this.save = this.save.bind(this)
}) this.replaceDatasource = this.replaceDatasource.bind(this)
this.getTableNames = this.getTableNames.bind(this)
}
const fetch = async () => { async fetch() {
const datasources = await API.getDatasources() const datasources = await API.getDatasources()
store.update(state => ({ this.store.update(state => ({
...state, ...state,
list: datasources, rawList: datasources,
})) }))
} }
const select = (id: string) => { async init() {
store.update(state => ({ return this.fetch()
}
select(id: string) {
this.store.update(state => ({
...state, ...state,
selectedDatasourceId: id, selectedDatasourceId: id,
})) }))
} }
const updateDatasource = ( private updateDatasourceInStore(
response: { datasource: Datasource; errors?: Record<string, string> }, response: { datasource: Datasource; errors?: Record<string, string> },
{ ignoreErrors }: { ignoreErrors?: boolean } = {} { ignoreErrors }: { ignoreErrors?: boolean } = {}
) => { ) {
const { datasource, errors } = response const { datasource, errors } = response
if (!ignoreErrors && errors && Object.keys(errors).length > 0) { if (!ignoreErrors && errors && Object.keys(errors).length > 0) {
throw new TableImportError(errors) throw new TableImportError(errors)
} }
replaceDatasource(datasource._id!, datasource) this.replaceDatasource(datasource._id!, datasource)
select(datasource._id!) this.select(datasource._id!)
return datasource return datasource
} }
const updateSchema = async ( async updateSchema(datasource: Datasource, tablesFilter: string[]) {
datasource: Datasource,
tablesFilter: string[]
) => {
const response = await API.buildDatasourceSchema( const response = await API.buildDatasourceSchema(
datasource?._id!, datasource?._id!,
tablesFilter tablesFilter
) )
updateDatasource(response) this.updateDatasourceInStore(response)
} }
const sourceCount = (source: string) => { sourceCount(source: string) {
return get(store).list.filter(datasource => datasource.source === source) return get(this.store).rawList.filter(
.length datasource => datasource.source === source
).length
} }
const checkDatasourceValidity = async ( async checkDatasourceValidity(
integration: Integration, integration: Integration,
datasource: Datasource datasource: Datasource
): Promise<{ valid: boolean; error?: string }> => { ): Promise<{ valid: boolean; error?: string }> {
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) { if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
const { connected, error } = await API.validateDatasource(datasource) const { connected, error } = await API.validateDatasource(datasource)
if (connected) { if (connected) {
@ -147,14 +179,14 @@ export function createDatasourcesStore() {
return { valid: true } return { valid: true }
} }
const create = async ({ async create({
integration, integration,
config, config,
}: { }: {
integration: UIIntegration integration: UIIntegration
config: Record<string, any> config: Record<string, any>
}) => { }) {
const count = sourceCount(integration.name) const count = this.sourceCount(integration.name)
const nameModifier = count === 0 ? "" : ` ${count + 1}` const nameModifier = count === 0 ? "" : ` ${count + 1}`
const datasource: Datasource = { const datasource: Datasource = {
@ -166,7 +198,7 @@ export function createDatasourcesStore() {
isSQL: integration.isSQL, isSQL: integration.isSQL,
} }
const { valid, error } = await checkDatasourceValidity( const { valid, error } = await this.checkDatasourceValidity(
integration, integration,
datasource datasource
) )
@ -179,43 +211,47 @@ export function createDatasourcesStore() {
fetchSchema: integration.plus, fetchSchema: integration.plus,
}) })
return updateDatasource(response, { ignoreErrors: true }) return this.updateDatasourceInStore(response, { ignoreErrors: true })
} }
const update = async ({ async save({
integration, integration,
datasource, datasource,
}: { }: {
integration: Integration integration: Integration
datasource: Datasource datasource: Datasource
}) => { }) {
if (await checkDatasourceValidity(integration, datasource)) { if (!(await this.checkDatasourceValidity(integration, datasource)).valid) {
throw new Error("Unable to connect") throw new Error("Unable to connect")
} }
const response = await API.updateDatasource(datasource) const response = await API.updateDatasource(datasource)
return updateDatasource(response) return this.updateDatasourceInStore(response)
} }
const deleteDatasource = async (datasource: Datasource) => { async deleteDatasource(datasource: Datasource) {
if (!datasource?._id || !datasource?._rev) { if (!datasource?._id || !datasource?._rev) {
return return
} }
await API.deleteDatasource(datasource._id, datasource._rev) await API.deleteDatasource(datasource._id, datasource._rev)
replaceDatasource(datasource._id) this.replaceDatasource(datasource._id)
} }
const replaceDatasource = (datasourceId: string, datasource?: Datasource) => { async delete(datasource: Datasource) {
return this.deleteDatasource(datasource)
}
replaceDatasource(datasourceId: string, datasource?: Datasource) {
if (!datasourceId) { if (!datasourceId) {
return return
} }
// Handle deletion // Handle deletion
if (!datasource) { if (!datasource) {
store.update(state => ({ this.store.update(state => ({
...state, ...state,
list: state.list.filter(x => x._id !== datasourceId), rawList: state.rawList.filter(x => x._id !== datasourceId),
})) }))
tables.removeDatasourceTables(datasourceId) tables.removeDatasourceTables(datasourceId)
queries.removeDatasourceQueries(datasourceId) queries.removeDatasourceQueries(datasourceId)
@ -223,11 +259,13 @@ export function createDatasourcesStore() {
} }
// Add new datasource // Add new datasource
const index = get(store).list.findIndex(x => x._id === datasource._id) const index = get(this.store).rawList.findIndex(
x => x._id === datasource._id
)
if (index === -1) { if (index === -1) {
store.update(state => ({ this.store.update(state => ({
...state, ...state,
list: [...state.list, datasource], rawList: [...state.rawList, datasource],
})) }))
// If this is a new datasource then we should refresh the tables list, // If this is a new datasource then we should refresh the tables list,
@ -237,30 +275,17 @@ export function createDatasourcesStore() {
// Update existing datasource // Update existing datasource
else if (datasource) { else if (datasource) {
store.update(state => { this.store.update(state => {
state.list[index] = datasource state.rawList[index] = datasource
return state return state
}) })
} }
} }
const getTableNames = async (datasource: Datasource) => { async getTableNames(datasource: Datasource) {
const info = await API.fetchInfoForDatasource(datasource) const info = await API.fetchInfoForDatasource(datasource)
return info.tableNames || [] return info.tableNames || []
} }
return {
subscribe: derivedStore.subscribe,
fetch,
init: fetch,
select,
updateSchema,
create,
update,
delete: deleteDatasource,
replaceDatasource,
getTableNames,
}
} }
export const datasources = createDatasourcesStore() export const datasources = new DatasourceStore()

View File

@ -1,11 +1,12 @@
import { writable } from "svelte/store" import { writable, type Writable } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { DeploymentProgressResponse } from "@budibase/types"
export const createDeploymentStore = () => { export const createDeploymentStore = () => {
let store = writable([]) let store: Writable<DeploymentProgressResponse[]> = writable([])
const load = async () => { const load = async (): Promise<void> => {
try { try {
store.set(await API.getAppDeployments()) store.set(await API.getAppDeployments())
} catch (err) { } catch (err) {

View File

@ -1,6 +1,6 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { previewStore } from "@/stores/builder" import { previewStore } from "@/stores/builder"
import BudiStore from "../BudiStore" import { BudiStore } from "../BudiStore"
export const INITIAL_HOVER_STATE = { export const INITIAL_HOVER_STATE = {
componentId: null, componentId: null,

View File

@ -1,6 +1,6 @@
import { derived, get } from "svelte/store" import { derived, get } from "svelte/store"
import { componentStore } from "@/stores/builder" import { componentStore } from "@/stores/builder"
import BudiStore from "../BudiStore" import { BudiStore } from "../BudiStore"
import { API } from "@/api" import { API } from "@/api"
export const INITIAL_LAYOUT_STATE = { export const INITIAL_LAYOUT_STATE = {

View File

@ -1,7 +1,7 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { appStore } from "@/stores/builder" import { appStore } from "@/stores/builder"
import BudiStore from "../BudiStore" import { BudiStore } from "../BudiStore"
export const INITIAL_NAVIGATION_STATE = { export const INITIAL_NAVIGATION_STATE = {
navigation: "Top", navigation: "Top",

View File

@ -1,5 +1,5 @@
import { get, derived } from "svelte/store" import { get, derived } from "svelte/store"
import BudiStore from "@/stores/BudiStore" import { BudiStore } from "@/stores/BudiStore"
import { tables } from "./tables" import { tables } from "./tables"
import { viewsV2 } from "./viewsV2" import { viewsV2 } from "./viewsV2"
import { automationStore } from "./automations" import { automationStore } from "./automations"

View File

@ -12,7 +12,7 @@ import {
} from "@/stores/builder" } from "@/stores/builder"
import { createHistoryStore } from "@/stores/builder/history" import { createHistoryStore } from "@/stores/builder/history"
import { API } from "@/api" import { API } from "@/api"
import BudiStore from "../BudiStore" import { BudiStore } from "../BudiStore"
export const INITIAL_SCREENS_STATE = { export const INITIAL_SCREENS_STATE = {
screens: [], screens: [],

View File

@ -2,6 +2,7 @@ import { integrations } from "./integrations"
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { DatasourceTypes } from "@/constants/backend" import { DatasourceTypes } from "@/constants/backend"
import { UIIntegration, Integration } from "@budibase/types" import { UIIntegration, Integration } from "@budibase/types"
import { BudiStore } from "@/stores/BudiStore"
const getIntegrationOrder = (type: string | undefined) => { const getIntegrationOrder = (type: string | undefined) => {
// if type is not known, sort to end // if type is not known, sort to end
@ -17,29 +18,35 @@ const getIntegrationOrder = (type: string | undefined) => {
return type.charCodeAt(0) + 4 return type.charCodeAt(0) + 4
} }
export const createSortedIntegrationsStore = () => { export class SortedIntegrationStore extends BudiStore<UIIntegration[]> {
return derived<typeof integrations, UIIntegration[]>( constructor() {
integrations, super([])
$integrations => {
const entries: [string, Integration][] = Object.entries($integrations)
const integrationsAsArray = entries.map(([name, integration]) => ({
name,
...integration,
}))
return integrationsAsArray.sort((integrationA, integrationB) => { const derivedStore = derived<typeof integrations, UIIntegration[]>(
const integrationASortOrder = getIntegrationOrder(integrationA.type) integrations,
const integrationBSortOrder = getIntegrationOrder(integrationB.type) $integrations => {
if (integrationASortOrder === integrationBSortOrder) { const entries: [string, Integration][] = Object.entries($integrations)
return integrationA.friendlyName.localeCompare( const integrationsAsArray = entries.map(([name, integration]) => ({
integrationB.friendlyName name,
) ...integration,
} }))
return integrationASortOrder < integrationBSortOrder ? -1 : 1 return integrationsAsArray.sort((integrationA, integrationB) => {
}) const integrationASortOrder = getIntegrationOrder(integrationA.type)
} const integrationBSortOrder = getIntegrationOrder(integrationB.type)
) if (integrationASortOrder === integrationBSortOrder) {
return integrationA.friendlyName.localeCompare(
integrationB.friendlyName
)
}
return integrationASortOrder < integrationBSortOrder ? -1 : 1
})
}
)
this.subscribe = derivedStore.subscribe
}
} }
export const sortedIntegrations = createSortedIntegrationsStore() export const sortedIntegrations = new SortedIntegrationStore()

View File

@ -1,30 +1,64 @@
import { FieldType } from "@budibase/types" import {
FieldSchema,
FieldType,
SaveTableRequest,
Table,
} from "@budibase/types"
import { SWITCHABLE_TYPES } from "@budibase/shared-core" import { SWITCHABLE_TYPES } from "@budibase/shared-core"
import { get, writable, derived } from "svelte/store" import { get, derived, Writable } from "svelte/store"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { API } from "@/api" import { API } from "@/api"
import { DerivedBudiStore } from "@/stores/BudiStore"
export function createTablesStore() { interface BuilderTableStore {
const store = writable({ list: Table[]
list: [], selectedTableId?: string
selectedTableId: null, }
})
const derivedStore = derived(store, $store => ({
...$store,
selected: $store.list?.find(table => table._id === $store.selectedTableId),
}))
const fetch = async () => { interface DerivedTableStore extends BuilderTableStore {
selected?: Table
}
export class TableStore extends DerivedBudiStore<
BuilderTableStore,
DerivedTableStore
> {
constructor() {
const makeDerivedStore = (store: Writable<BuilderTableStore>) => {
return derived(store, $store => ({
...$store,
selected: $store.list?.find(
table => table._id === $store.selectedTableId
),
}))
}
super(
{
list: [],
selectedTableId: undefined,
},
makeDerivedStore
)
this.select = this.select.bind(this)
}
async init() {
return this.fetch()
}
async fetch() {
const tables = await API.getTables() const tables = await API.getTables()
store.update(state => ({ this.store.update(state => ({
...state, ...state,
list: tables, list: tables,
})) }))
} }
const singleFetch = async tableId => { private async singleFetch(tableId: string) {
const table = await API.getTable(tableId) const table = await API.getTable(tableId)
store.update(state => { this.store.update(state => {
const list = [] const list = []
// update the list, keep order accurate // update the list, keep order accurate
for (let tbl of state.list) { for (let tbl of state.list) {
@ -39,16 +73,16 @@ export function createTablesStore() {
}) })
} }
const select = tableId => { select(tableId: string | undefined) {
store.update(state => ({ this.store.update(state => ({
...state, ...state,
selectedTableId: tableId, selectedTableId: tableId,
})) }))
} }
const save = async table => { async save(table: Table) {
const updatedTable = cloneDeep(table) const updatedTable: SaveTableRequest = cloneDeep(table)
const oldTable = get(store).list.filter(t => t._id === table._id)[0] const oldTable = get(this.store).list.filter(t => t._id === table._id)[0]
const fieldNames = [] const fieldNames = []
// Update any renamed schema keys to reflect their names // Update any renamed schema keys to reflect their names
@ -79,8 +113,8 @@ export function createTablesStore() {
} }
const savedTable = await API.saveTable(updatedTable) const savedTable = await API.saveTable(updatedTable)
replaceTable(savedTable._id, savedTable) this.replaceTable(savedTable._id, savedTable)
select(savedTable._id) this.select(savedTable._id)
// make sure tables up to date (related) // make sure tables up to date (related)
let newTableIds = [] let newTableIds = []
for (let column of Object.values(updatedTable?.schema || {})) { for (let column of Object.values(updatedTable?.schema || {})) {
@ -99,28 +133,30 @@ export function createTablesStore() {
const tableIdsToFetch = [...new Set([...newTableIds, ...oldTableIds])] const tableIdsToFetch = [...new Set([...newTableIds, ...oldTableIds])]
// too many tables to fetch, just get all // too many tables to fetch, just get all
if (tableIdsToFetch.length > 3) { if (tableIdsToFetch.length > 3) {
await fetch() await this.fetch()
} else { } else {
await Promise.all(tableIdsToFetch.map(id => singleFetch(id))) await Promise.all(tableIdsToFetch.map(id => this.singleFetch(id)))
} }
return savedTable return savedTable
} }
const deleteTable = async table => { async delete(table: { _id: string; _rev: string }) {
if (!table?._id) { await API.deleteTable(table._id, table._rev)
return this.replaceTable(table._id, null)
}
await API.deleteTable(table._id, table._rev || "rev")
replaceTable(table._id, null)
} }
const saveField = async ({ async saveField({
originalName, originalName,
field, field,
primaryDisplay = false, primaryDisplay = false,
indexes, indexes,
}) => { }: {
let draft = cloneDeep(get(derivedStore).selected) originalName: string
field: FieldSchema
primaryDisplay: boolean
indexes: Record<string, any>
}) {
const draft: SaveTableRequest = cloneDeep(get(this.derivedStore).selected!)
// delete the original if renaming // delete the original if renaming
// need to handle if the column had no name, empty string // need to handle if the column had no name, empty string
@ -139,7 +175,7 @@ export function createTablesStore() {
const fields = Object.keys(draft.schema) const fields = Object.keys(draft.schema)
// pick another display column randomly if unselecting // pick another display column randomly if unselecting
draft.primaryDisplay = fields.filter( draft.primaryDisplay = fields.filter(
name => name !== originalName || name !== field name => name !== originalName || name !== field.name
)[0] )[0]
} }
if (indexes) { if (indexes) {
@ -150,24 +186,24 @@ export function createTablesStore() {
[field.name]: cloneDeep(field), [field.name]: cloneDeep(field),
} }
await save(draft) await this.save(draft)
} }
const deleteField = async field => { async deleteField(field: { name: string | number }) {
let draft = cloneDeep(get(derivedStore).selected) let draft = cloneDeep(get(this.derivedStore).selected!)
delete draft.schema[field.name] delete draft.schema[field.name]
await save(draft) await this.save(draft)
} }
// Handles external updates of tables // Handles external updates of tables
const replaceTable = (tableId, table) => { replaceTable(tableId: string | undefined, table: Table | null) {
if (!tableId) { if (!tableId) {
return return
} }
// Handle deletion // Handle deletion
if (!table) { if (!table) {
store.update(state => ({ this.store.update(state => ({
...state, ...state,
list: state.list.filter(x => x._id !== tableId), list: state.list.filter(x => x._id !== tableId),
})) }))
@ -175,9 +211,9 @@ export function createTablesStore() {
} }
// Add new table // Add new table
const index = get(store).list.findIndex(x => x._id === table._id) const index = get(this.store).list.findIndex(x => x._id === table._id)
if (index === -1) { if (index === -1) {
store.update(state => ({ this.store.update(state => ({
...state, ...state,
list: [...state.list, table], list: [...state.list, table],
})) }))
@ -188,7 +224,7 @@ export function createTablesStore() {
// This function has to merge state as there discrepancies with the table // This function has to merge state as there discrepancies with the table
// API endpoints. The table list endpoint and get table endpoint use the // API endpoints. The table list endpoint and get table endpoint use the
// "type" property to mean different things. // "type" property to mean different things.
store.update(state => { this.store.update(state => {
state.list[index] = { state.list[index] = {
...table, ...table,
type: state.list[index].type, type: state.list[index].type,
@ -198,26 +234,12 @@ export function createTablesStore() {
} }
} }
const removeDatasourceTables = datasourceId => { removeDatasourceTables(datasourceId: string) {
store.update(state => ({ this.store.update(state => ({
...state, ...state,
list: state.list.filter(table => table.sourceId !== datasourceId), list: state.list.filter(table => table.sourceId !== datasourceId),
})) }))
} }
return {
...store,
subscribe: derivedStore.subscribe,
fetch,
init: fetch,
select,
save,
delete: deleteTable,
saveField,
deleteField,
replaceTable,
removeDatasourceTables,
}
} }
export const tables = createTablesStore() export const tables = new TableStore()

View File

@ -65,7 +65,7 @@ describe("Builder store", () => {
ctx.test.builderStore.reset() ctx.test.builderStore.reset()
expect(disconnected).toBe(true) expect(disconnected).toBe(true)
expect(ctx.test.store).toStrictEqual(INITIAL_BUILDER_STATE) expect(ctx.test.store).toStrictEqual(INITIAL_BUILDER_STATE)
expect(ctx.test.builderStore.websocket).toBeNull() expect(ctx.test.builderStore.websocket).toBeUndefined()
}) })
it("Attempt to emit a resource select event to the websocket on select", ctx => { it("Attempt to emit a resource select event to the websocket on select", ctx => {

View File

@ -1,12 +1,13 @@
import { it, expect, describe, beforeEach, vi } from "vitest" import { it, expect, describe, beforeEach, vi } from "vitest"
import { createSortedIntegrationsStore } from "@/stores/builder/sortedIntegrations" import { SortedIntegrationStore } from "@/stores/builder/sortedIntegrations"
import { DatasourceTypes } from "@/constants/backend" import { DatasourceTypes } from "@/constants/backend"
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { integrations } from "@/stores/builder/integrations" import { integrations } from "@/stores/builder/integrations"
vi.mock("svelte/store", () => ({ vi.mock("svelte/store", () => ({
derived: vi.fn(), derived: vi.fn(() => ({
subscribe: vi.fn(),
})),
writable: vi.fn(() => ({ writable: vi.fn(() => ({
subscribe: vi.fn(), subscribe: vi.fn(),
})), })),
@ -14,6 +15,8 @@ vi.mock("svelte/store", () => ({
vi.mock("@/stores/builder/integrations", () => ({ integrations: vi.fn() })) vi.mock("@/stores/builder/integrations", () => ({ integrations: vi.fn() }))
const mockedDerived = vi.mocked(derived)
const inputA = { const inputA = {
nonRelationalA: { nonRelationalA: {
friendlyName: "non-relational A", friendlyName: "non-relational A",
@ -104,25 +107,28 @@ const expectedOutput = [
] ]
describe("sorted integrations store", () => { describe("sorted integrations store", () => {
beforeEach(ctx => { interface LocalContext {
returnedStore: SortedIntegrationStore
derivedCallback: any
}
beforeEach<LocalContext>(ctx => {
vi.clearAllMocks() vi.clearAllMocks()
ctx.returnedStore = createSortedIntegrationsStore() ctx.returnedStore = new SortedIntegrationStore()
ctx.derivedCallback = mockedDerived.mock.calls[0]?.[1]
ctx.derivedCallback = derived.mock.calls[0][1]
}) })
it("calls derived with the correct parameters", () => { it("calls derived with the correct parameters", () => {
expect(derived).toHaveBeenCalledTimes(1) expect(mockedDerived).toHaveBeenCalledTimes(1)
expect(derived).toHaveBeenCalledWith(integrations, expect.toBeFunc()) expect(mockedDerived).toHaveBeenCalledWith(
integrations,
expect.any(Function)
)
}) })
describe("derived callback", () => { describe("derived callback", () => {
it("When no integrations are loaded", ctx => { it<LocalContext>("When integrations are present", ctx => {
expect(ctx.derivedCallback({})).toEqual([])
})
it("When integrations are present", ctx => {
expect(ctx.derivedCallback(inputA)).toEqual(expectedOutput) expect(ctx.derivedCallback(inputA)).toEqual(expectedOutput)
expect(ctx.derivedCallback(inputB)).toEqual(expectedOutput) expect(ctx.derivedCallback(inputB)).toEqual(expectedOutput)
}) })

View File

@ -1,102 +0,0 @@
import { writable, derived, get } from "svelte/store"
import { tables } from "./tables"
import { API } from "@/api"
export function createViewsV2Store() {
const store = writable({
selectedViewId: null,
})
const derivedStore = derived([store, tables], ([$store, $tables]) => {
let list = []
$tables.list?.forEach(table => {
const views = Object.values(table?.views || {}).filter(view => {
return view.version === 2
})
list = list.concat(views)
})
return {
...$store,
list,
selected: list.find(view => view.id === $store.selectedViewId),
}
})
const select = id => {
store.update(state => ({
...state,
selectedViewId: id,
}))
}
const deleteView = async view => {
await API.viewV2.delete(view.id)
replaceView(view.id, null)
}
const create = async view => {
const savedViewResponse = await API.viewV2.create(view)
const savedView = savedViewResponse.data
replaceView(savedView.id, savedView)
return savedView
}
const save = async view => {
const res = await API.viewV2.update(view)
const savedView = res?.data
replaceView(view.id, savedView)
}
// Handles external updates of tables
const replaceView = (viewId, view) => {
if (!viewId) {
return
}
const existingView = get(derivedStore).list.find(view => view.id === viewId)
const tableIndex = get(tables).list.findIndex(table => {
return table._id === view?.tableId || table._id === existingView?.tableId
})
if (tableIndex === -1) {
return
}
// Handle deletion
if (!view) {
tables.update(state => {
delete state.list[tableIndex].views[existingView.name]
return state
})
return
}
// Add new view
if (!existingView) {
tables.update(state => {
state.list[tableIndex].views[view.name] = view
return state
})
}
// Update existing view
else {
tables.update(state => {
// Remove old view
delete state.list[tableIndex].views[existingView.name]
// Add new view
state.list[tableIndex].views[view.name] = view
return state
})
}
}
return {
subscribe: derivedStore.subscribe,
select,
delete: deleteView,
create,
save,
replaceView,
}
}
export const viewsV2 = createViewsV2Store()

View File

@ -0,0 +1,122 @@
import { derived, get, Writable } from "svelte/store"
import { tables } from "./tables"
import { API } from "@/api"
import { DerivedBudiStore } from "@/stores/BudiStore"
import { CreateViewRequest, UpdateViewRequest, ViewV2 } from "@budibase/types"
import { helpers } from "@budibase/shared-core"
interface BuilderViewV2Store {
selectedViewId: string | null
}
interface DerivedViewV2Store extends BuilderViewV2Store {
list: ViewV2[]
selected?: ViewV2
}
export class ViewV2Store extends DerivedBudiStore<
BuilderViewV2Store,
DerivedViewV2Store
> {
constructor() {
const makeDerivedStore = (store: Writable<BuilderViewV2Store>) => {
return derived(
[store, tables],
([$store, $tables]): DerivedViewV2Store => {
let list: ViewV2[] = []
$tables.list?.forEach(table => {
const views = Object.values(table?.views || {}).filter(
helpers.views.isV2
)
list = list.concat(views)
})
return {
...$store,
list,
selected: list.find(view => view.id === $store.selectedViewId),
}
}
)
}
super(
{
selectedViewId: null,
},
makeDerivedStore
)
this.select = this.select.bind(this)
}
select(id: string) {
this.store.update(state => ({
...state,
selectedViewId: id,
}))
}
async delete(view: { id: string }) {
await API.viewV2.delete(view.id)
this.replaceView(view.id, null)
}
async create(view: CreateViewRequest) {
const savedViewResponse = await API.viewV2.create(view)
const savedView = savedViewResponse.data
this.replaceView(savedView.id, savedView)
return savedView
}
async save(view: UpdateViewRequest) {
const res = await API.viewV2.update(view)
const savedView = res?.data
this.replaceView(view.id, savedView)
}
// Handles external updates of tables
replaceView(viewId: string, view: ViewV2 | null) {
const existingView = get(this.derivedStore).list.find(
view => view.id === viewId
)
const tableIndex = get(tables).list.findIndex(table => {
return table._id === view?.tableId || table._id === existingView?.tableId
})
if (tableIndex === -1) {
return
}
// Handle deletion
if (!view && existingView) {
tables.update(state => {
delete state.list[tableIndex].views![existingView.name]
return state
})
return
}
// Add new view
else if (!existingView && view) {
tables.update(state => {
state.list[tableIndex].views ??= {}
state.list[tableIndex].views[view.name] = view
return state
})
}
// Update existing view
else if (existingView && view) {
tables.update(state => {
// Remove old view
state.list[tableIndex].views ??= {}
delete state.list[tableIndex].views[existingView.name]
// Add new view
state.list[tableIndex].views[view.name] = view
return state
})
}
}
}
export const viewsV2 = new ViewV2Store()

View File

@ -1,6 +1,5 @@
import { it, expect, describe, beforeEach, vi } from "vitest" import { it, expect, describe, beforeEach, vi } from "vitest"
import { createAdminStore } from "./admin" import { createAdminStore } from "./admin"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { auth } from "@/stores/portal" import { auth } from "@/stores/portal"

View File

@ -2,7 +2,7 @@ import { derived } from "svelte/store"
import { AppStatus } from "@/constants" import { AppStatus } from "@/constants"
import { API } from "@/api" import { API } from "@/api"
import { auth } from "./auth" import { auth } from "./auth"
import BudiStore from "../BudiStore" import { BudiStore } from "../BudiStore"
import { App, UpdateAppRequest } from "@budibase/types" import { App, UpdateAppRequest } from "@budibase/types"
interface AppIdentifierMetadata { interface AppIdentifierMetadata {

View File

@ -1,7 +1,7 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { licensing } from "./licensing" import { licensing } from "./licensing"
import BudiStore from "../BudiStore" import { BudiStore } from "../BudiStore"
import { import {
DownloadAuditLogsRequest, DownloadAuditLogsRequest,
SearchAuditLogsRequest, SearchAuditLogsRequest,

View File

@ -2,7 +2,7 @@ import { get } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { admin } from "@/stores/portal" import { admin } from "@/stores/portal"
import analytics from "@/analytics" import analytics from "@/analytics"
import BudiStore from "@/stores/BudiStore" import { BudiStore } from "@/stores/BudiStore"
import { import {
isSSOUser, isSSOUser,
SetInitInfoRequest, SetInitInfoRequest,

View File

@ -22,6 +22,7 @@ export const createLicensingStore = () => {
backupsEnabled: false, backupsEnabled: false,
brandingEnabled: false, brandingEnabled: false,
scimEnabled: false, scimEnabled: false,
environmentVariablesEnabled: false,
budibaseAIEnabled: false, budibaseAIEnabled: false,
customAIConfigsEnabled: false, customAIConfigsEnabled: false,
auditLogsEnabled: false, auditLogsEnabled: false,

View File

@ -1,6 +1,7 @@
{ {
"extends": "./tsconfig.build.json", "extends": "./tsconfig.build.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@budibase/*": [ "@budibase/*": [

View File

@ -5,6 +5,9 @@
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "./src/index.ts", "svelte": "./src/index.ts",
"scripts": {
"check:types": "yarn svelte-check"
},
"dependencies": { "dependencies": {
"@budibase/bbui": "*", "@budibase/bbui": "*",
"@budibase/shared-core": "*", "@budibase/shared-core": "*",
@ -13,5 +16,8 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"shortid": "2.2.15", "shortid": "2.2.15",
"socket.io-client": "^4.7.5" "socket.io-client": "^4.7.5"
},
"devDependencies": {
"svelte-check": "^4.1.0"
} }
} }

View File

@ -73,6 +73,7 @@
.value { .value {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: var(--content-lines); -webkit-line-clamp: var(--content-lines);
line-clamp: var(--content-lines);
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
line-height: 20px; line-height: 20px;

View File

@ -93,6 +93,7 @@
.value { .value {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: var(--content-lines); -webkit-line-clamp: var(--content-lines);
line-clamp: var(--content-lines);
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
line-height: 20px; line-height: 20px;

View File

@ -74,12 +74,14 @@
.value { .value {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: var(--content-lines); -webkit-line-clamp: var(--content-lines);
line-clamp: var(--content-lines);
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
line-height: 20px; line-height: 20px;
} }
.number .value { .number .value {
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
line-clamp: 1;
} }
input { input {
flex: 1 1 auto; flex: 1 1 auto;
@ -110,5 +112,6 @@
} }
input[type="number"] { input[type="number"] {
-moz-appearance: textfield; -moz-appearance: textfield;
appearance: textfield;
} }
</style> </style>

View File

@ -1,8 +1,25 @@
import { derived, get, writable } from "svelte/store" import { derived, get, Readable, Writable, writable } from "svelte/store"
import { DefaultColumnWidth, GutterWidth } from "../lib/constants" import { DefaultColumnWidth, GutterWidth } from "../lib/constants"
import { UIColumn } from "@budibase/types"
import { Store as StoreContext } from "."
export const createStores = () => { interface ColumnStore {
const columns = writable([]) columns: Writable<UIColumn[]>
}
interface DerivedColumnStore {
tableColumns: Readable<UIColumn[]>
displayColumn: Readable<UIColumn | undefined>
columnLookupMap: Readable<Record<string, UIColumn>>
visibleColumns: Readable<UIColumn[]>
scrollableColumns: Readable<UIColumn[]>
hasNonAutoColumn: Readable<boolean>
}
export type Store = ColumnStore & DerivedColumnStore
export const createStores = (): ColumnStore => {
const columns = writable<UIColumn[]>([])
// Enrich columns with metadata about their display position // Enrich columns with metadata about their display position
const enrichedColumns = derived(columns, $columns => { const enrichedColumns = derived(columns, $columns => {
@ -16,7 +33,7 @@ export const createStores = () => {
} }
if (col.visible) { if (col.visible) {
idx++ idx++
offset += col.width offset += col.width ?? 0
} }
return enriched return enriched
}) })
@ -30,12 +47,12 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const deriveStores = (context: StoreContext): DerivedColumnStore => {
const { columns } = context const { columns } = context
// Derive a lookup map for all columns by name // Derive a lookup map for all columns by name
const columnLookupMap = derived(columns, $columns => { const columnLookupMap = derived(columns, $columns => {
let map = {} let map: Record<string, UIColumn> = {}
$columns.forEach(column => { $columns.forEach(column => {
map[column.name] = column map[column.name] = column
}) })
@ -78,11 +95,11 @@ export const deriveStores = context => {
} }
} }
export const createActions = context => { export const createActions = (context: StoreContext) => {
const { columns, datasource } = context const { columns, datasource } = context
// Updates the width of all columns // Updates the width of all columns
const changeAllColumnWidths = async width => { const changeAllColumnWidths = async (width: number) => {
const $columns = get(columns) const $columns = get(columns)
$columns.forEach(column => { $columns.forEach(column => {
const { related } = column const { related } = column
@ -101,7 +118,7 @@ export const createActions = context => {
} }
// Checks if a column is readonly // Checks if a column is readonly
const isReadonly = column => { const isReadonly = (column: UIColumn) => {
if (!column?.schema) { if (!column?.schema) {
return false return false
} }
@ -125,11 +142,11 @@ export const createActions = context => {
} }
} }
export const initialise = context => { export const initialise = (context: StoreContext) => {
const { definition, columns, displayColumn, enrichedSchema } = context const { definition, columns, displayColumn, enrichedSchema } = context
// Merge new schema fields with existing schema in order to preserve widths // Merge new schema fields with existing schema in order to preserve widths
const processColumns = $enrichedSchema => { const processColumns = ($enrichedSchema: any) => {
if (!$enrichedSchema) { if (!$enrichedSchema) {
columns.set([]) columns.set([])
return return
@ -139,7 +156,7 @@ export const initialise = context => {
const $displayColumn = get(displayColumn) const $displayColumn = get(displayColumn)
// Find primary display // Find primary display
let primaryDisplay let primaryDisplay: string
const candidatePD = $definition.primaryDisplay || $displayColumn?.name const candidatePD = $definition.primaryDisplay || $displayColumn?.name
if (candidatePD && $enrichedSchema[candidatePD]) { if (candidatePD && $enrichedSchema[candidatePD]) {
primaryDisplay = candidatePD primaryDisplay = candidatePD
@ -151,7 +168,8 @@ export const initialise = context => {
.map(field => { .map(field => {
const fieldSchema = $enrichedSchema[field] const fieldSchema = $enrichedSchema[field]
const oldColumn = $columns?.find(col => col.name === field) const oldColumn = $columns?.find(col => col.name === field)
const column = { const column: UIColumn = {
type: fieldSchema.type,
name: field, name: field,
label: fieldSchema.displayName || field, label: fieldSchema.displayName || field,
schema: fieldSchema, schema: fieldSchema,

View File

@ -1,10 +1,54 @@
import { derived, get } from "svelte/store" import { derived, get, Readable, Writable } from "svelte/store"
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch" import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
import { enrichSchemaWithRelColumns, memo } from "../../../utils" import { enrichSchemaWithRelColumns, memo } from "../../../utils"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { ViewV2Type } from "@budibase/types" import {
Row,
SaveRowRequest,
SaveTableRequest,
UIDatasource,
UIFieldMutation,
UIFieldSchema,
UpdateViewRequest,
ViewV2Type,
} from "@budibase/types"
import { Store as StoreContext } from "."
import { DatasourceActions } from "./datasources"
export const createStores = () => { interface DatasourceStore {
definition: Writable<UIDatasource>
schemaMutations: Writable<Record<string, UIFieldMutation>>
subSchemaMutations: Writable<Record<string, Record<string, UIFieldMutation>>>
}
interface DerivedDatasourceStore {
schema: Readable<Record<string, UIFieldSchema> | null>
enrichedSchema: Readable<Record<string, UIFieldSchema> | null>
hasBudibaseIdentifiers: Readable<boolean>
}
interface ActionDatasourceStore {
datasource: DatasourceStore["definition"] & {
actions: DatasourceActions & {
refreshDefinition: () => Promise<void>
changePrimaryDisplay: (column: string) => Promise<void>
addSchemaMutation: (field: string, mutation: UIFieldMutation) => void
addSubSchemaMutation: (
field: string,
fromField: string,
mutation: UIFieldMutation
) => void
saveSchemaMutations: () => Promise<void>
resetSchemaMutations: () => void
}
}
}
export type Store = DatasourceStore &
DerivedDatasourceStore &
ActionDatasourceStore
export const createStores = (): DatasourceStore => {
const definition = memo(null) const definition = memo(null)
const schemaMutations = memo({}) const schemaMutations = memo({})
const subSchemaMutations = memo({}) const subSchemaMutations = memo({})
@ -16,7 +60,7 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
const { const {
API, API,
definition, definition,
@ -27,7 +71,7 @@ export const deriveStores = context => {
} = context } = context
const schema = derived(definition, $definition => { const schema = derived(definition, $definition => {
let schema = getDatasourceSchema({ let schema: Record<string, UIFieldSchema> = getDatasourceSchema({
API, API,
datasource: get(datasource), datasource: get(datasource),
definition: $definition, definition: $definition,
@ -40,7 +84,7 @@ export const deriveStores = context => {
// Certain datasources like queries use primitives. // Certain datasources like queries use primitives.
Object.keys(schema || {}).forEach(key => { Object.keys(schema || {}).forEach(key => {
if (typeof schema[key] !== "object") { if (typeof schema[key] !== "object") {
schema[key] = { type: schema[key] } schema[key] = { name: key, type: schema[key] }
} }
}) })
@ -58,19 +102,18 @@ export const deriveStores = context => {
const schemaWithRelatedColumns = enrichSchemaWithRelColumns($schema) const schemaWithRelatedColumns = enrichSchemaWithRelColumns($schema)
const enrichedSchema = {} const enrichedSchema: Record<string, UIFieldSchema> = {}
Object.keys(schemaWithRelatedColumns).forEach(field => { Object.keys(schemaWithRelatedColumns || {}).forEach(field => {
enrichedSchema[field] = { enrichedSchema[field] = {
...schemaWithRelatedColumns[field], ...schemaWithRelatedColumns?.[field],
...$schemaOverrides?.[field], ...$schemaOverrides?.[field],
...$schemaMutations[field], ...$schemaMutations[field],
} }
if ($subSchemaMutations[field]) { if ($subSchemaMutations[field]) {
enrichedSchema[field].columns ??= {} enrichedSchema[field].columns ??= {}
for (const [fieldName, mutation] of Object.entries( for (const fieldName of Object.keys($subSchemaMutations[field])) {
$subSchemaMutations[field] const mutation = $subSchemaMutations[field][fieldName]
)) {
enrichedSchema[field].columns[fieldName] = { enrichedSchema[field].columns[fieldName] = {
...enrichedSchema[field].columns[fieldName], ...enrichedSchema[field].columns[fieldName],
...mutation, ...mutation,
@ -87,7 +130,7 @@ export const deriveStores = context => {
([$datasource, $definition]) => { ([$datasource, $definition]) => {
let type = $datasource?.type let type = $datasource?.type
if (type === "provider") { if (type === "provider") {
type = $datasource.value?.datasource?.type type = ($datasource as any).value?.datasource?.type
} }
// Handle calculation views // Handle calculation views
if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) { if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) {
@ -104,7 +147,7 @@ export const deriveStores = context => {
} }
} }
export const createActions = context => { export const createActions = (context: StoreContext): ActionDatasourceStore => {
const { const {
API, API,
datasource, datasource,
@ -147,21 +190,23 @@ export const createActions = context => {
} }
// Saves the datasource definition // Saves the datasource definition
const saveDefinition = async newDefinition => { const saveDefinition = async (
newDefinition: SaveTableRequest | UpdateViewRequest
) => {
// Update local state // Update local state
const originalDefinition = get(definition) const originalDefinition = get(definition)
definition.set(newDefinition) definition.set(newDefinition as UIDatasource)
// Update server // Update server
if (get(config).canSaveSchema) { if (get(config).canSaveSchema) {
try { try {
await getAPI()?.actions.saveDefinition(newDefinition) await getAPI()?.actions.saveDefinition(newDefinition as never)
// Broadcast change so external state can be updated, as this change // Broadcast change so external state can be updated, as this change
// will not be received by the builder websocket because we caused it // will not be received by the builder websocket because we caused it
// ourselves // ourselves
dispatch("updatedatasource", newDefinition) dispatch("updatedatasource", newDefinition)
} catch (error) { } catch (error: any) {
const msg = error?.message || error || "Unknown error" const msg = error?.message || error || "Unknown error"
get(notifications).error(`Error saving schema: ${msg}`) get(notifications).error(`Error saving schema: ${msg}`)
@ -172,7 +217,7 @@ export const createActions = context => {
} }
// Updates the datasources primary display column // Updates the datasources primary display column
const changePrimaryDisplay = async column => { const changePrimaryDisplay = async (column: string) => {
let newDefinition = cloneDeep(get(definition)) let newDefinition = cloneDeep(get(definition))
// Update primary display // Update primary display
@ -183,12 +228,14 @@ export const createActions = context => {
newDefinition.schema[column].constraints = {} newDefinition.schema[column].constraints = {}
} }
newDefinition.schema[column].constraints.presence = { allowEmpty: false } newDefinition.schema[column].constraints.presence = { allowEmpty: false }
delete newDefinition.schema[column].default if ("default" in newDefinition.schema[column]) {
return await saveDefinition(newDefinition) delete newDefinition.schema[column].default
}
return await saveDefinition(newDefinition as any)
} }
// Adds a schema mutation for a single field // Adds a schema mutation for a single field
const addSchemaMutation = (field, mutation) => { const addSchemaMutation = (field: string, mutation: UIFieldMutation) => {
if (!field || !mutation) { if (!field || !mutation) {
return return
} }
@ -204,7 +251,11 @@ export const createActions = context => {
} }
// Adds a nested schema mutation for a single field // Adds a nested schema mutation for a single field
const addSubSchemaMutation = (field, fromField, mutation) => { const addSubSchemaMutation = (
field: string,
fromField: string,
mutation: UIFieldMutation
) => {
if (!field || !fromField || !mutation) { if (!field || !fromField || !mutation) {
return return
} }
@ -231,8 +282,8 @@ export const createActions = context => {
const $definition = get(definition) const $definition = get(definition)
const $schemaMutations = get(schemaMutations) const $schemaMutations = get(schemaMutations)
const $subSchemaMutations = get(subSchemaMutations) const $subSchemaMutations = get(subSchemaMutations)
const $schema = get(schema) const $schema = get(schema) || {}
let newSchema = {} let newSchema: Record<string, UIFieldSchema> = {}
// Build new updated datasource schema // Build new updated datasource schema
Object.keys($schema).forEach(column => { Object.keys($schema).forEach(column => {
@ -242,9 +293,8 @@ export const createActions = context => {
} }
if ($subSchemaMutations[column]) { if ($subSchemaMutations[column]) {
newSchema[column].columns ??= {} newSchema[column].columns ??= {}
for (const [fieldName, mutation] of Object.entries( for (const fieldName of Object.keys($subSchemaMutations[column])) {
$subSchemaMutations[column] const mutation = $subSchemaMutations[column][fieldName]
)) {
newSchema[column].columns[fieldName] = { newSchema[column].columns[fieldName] = {
...newSchema[column].columns[fieldName], ...newSchema[column].columns[fieldName],
...mutation, ...mutation,
@ -257,7 +307,7 @@ export const createActions = context => {
await saveDefinition({ await saveDefinition({
...$definition, ...$definition,
schema: newSchema, schema: newSchema,
}) } as any)
resetSchemaMutations() resetSchemaMutations()
} }
@ -267,32 +317,32 @@ export const createActions = context => {
} }
// Adds a row to the datasource // Adds a row to the datasource
const addRow = async row => { const addRow = async (row: SaveRowRequest) => {
return await getAPI()?.actions.addRow(row) return await getAPI()?.actions.addRow(row)
} }
// Updates an existing row in the datasource // Updates an existing row in the datasource
const updateRow = async row => { const updateRow = async (row: SaveRowRequest) => {
return await getAPI()?.actions.updateRow(row) return await getAPI()?.actions.updateRow(row)
} }
// Deletes rows from the datasource // Deletes rows from the datasource
const deleteRows = async rows => { const deleteRows = async (rows: Row[]) => {
return await getAPI()?.actions.deleteRows(rows) return await getAPI()?.actions.deleteRows(rows)
} }
// Gets a single row from a datasource // Gets a single row from a datasource
const getRow = async id => { const getRow = async (id: string) => {
return await getAPI()?.actions.getRow(id) return await getAPI()?.actions.getRow(id)
} }
// Checks if a certain datasource config is valid // Checks if a certain datasource config is valid
const isDatasourceValid = datasource => { const isDatasourceValid = (datasource: UIDatasource) => {
return getAPI()?.actions.isDatasourceValid(datasource) return getAPI()?.actions.isDatasourceValid(datasource)
} }
// Checks if this datasource can use a specific column by name // Checks if this datasource can use a specific column by name
const canUseColumn = name => { const canUseColumn = (name: string) => {
return getAPI()?.actions.canUseColumn(name) return getAPI()?.actions.canUseColumn(name)
} }

View File

@ -0,0 +1,31 @@
import {
Row,
SaveRowRequest,
SaveTableRequest,
UIDatasource,
UpdateViewRequest,
} from "@budibase/types"
interface DatasourceBaseActions<
TSaveDefinitionRequest = UpdateViewRequest | SaveTableRequest
> {
saveDefinition: (newDefinition: TSaveDefinitionRequest) => Promise<void>
addRow: (row: SaveRowRequest) => Promise<Row | void>
updateRow: (row: SaveRowRequest) => Promise<Row | void>
deleteRows: (rows: Row[]) => Promise<void>
getRow: (id: string) => Promise<Row | void>
isDatasourceValid: (datasource: UIDatasource) => boolean | void
canUseColumn: (name: string) => boolean | void
}
export interface DatasourceTableActions
extends DatasourceBaseActions<SaveTableRequest> {}
export interface DatasourceViewActions
extends DatasourceBaseActions<UpdateViewRequest> {}
export interface DatasourceNonPlusActions
extends DatasourceBaseActions<never> {}
export type DatasourceActions =
| DatasourceTableActions & DatasourceViewActions & DatasourceNonPlusActions

View File

@ -1,7 +1,17 @@
import { SortOrder } from "@budibase/types" import { SortOrder, UIDatasource } from "@budibase/types"
import { get } from "svelte/store" import { get } from "svelte/store"
import { Store as StoreContext } from ".."
import { DatasourceNonPlusActions } from "."
export const createActions = context => { interface NonPlusActions {
nonPlus: {
actions: DatasourceNonPlusActions
}
}
export type Store = NonPlusActions
export const createActions = (context: StoreContext): NonPlusActions => {
const { columns, table, viewV2 } = context const { columns, table, viewV2 } = context
const saveDefinition = async () => { const saveDefinition = async () => {
@ -20,7 +30,7 @@ export const createActions = context => {
throw "This datasource does not support fetching individual rows" throw "This datasource does not support fetching individual rows"
} }
const isDatasourceValid = datasource => { const isDatasourceValid = (datasource: UIDatasource) => {
// There are many different types and shapes of datasource, so we only // There are many different types and shapes of datasource, so we only
// check that we aren't null // check that we aren't null
return ( return (
@ -30,7 +40,7 @@ export const createActions = context => {
) )
} }
const canUseColumn = name => { const canUseColumn = (name: string) => {
return get(columns).some(col => col.name === name) return get(columns).some(col => col.name === name)
} }
@ -50,11 +60,11 @@ export const createActions = context => {
} }
// Small util to compare datasource definitions // Small util to compare datasource definitions
const isSameDatasource = (a, b) => { const isSameDatasource = (a: any, b: any) => {
return JSON.stringify(a) === JSON.stringify(b) return JSON.stringify(a) === JSON.stringify(b)
} }
export const initialise = context => { export const initialise = (context: StoreContext) => {
const { const {
datasource, datasource,
sort, sort,
@ -69,7 +79,7 @@ export const initialise = context => {
} = context } = context
// Keep a list of subscriptions so that we can clear them when the datasource // Keep a list of subscriptions so that we can clear them when the datasource
// config changes // config changes
let unsubscribers = [] let unsubscribers: any[] = []
// Observe datasource changes and apply logic for view V2 datasources // Observe datasource changes and apply logic for view V2 datasources
datasource.subscribe($datasource => { datasource.subscribe($datasource => {

View File

@ -1,16 +1,32 @@
import { SortOrder } from "@budibase/types" import {
Row,
SaveRowRequest,
SaveTableRequest,
SortOrder,
UIDatasource,
} from "@budibase/types"
import { get } from "svelte/store" import { get } from "svelte/store"
import { Store as StoreContext } from ".."
import { DatasourceTableActions } from "."
const SuppressErrors = true const SuppressErrors = true
export const createActions = context => { interface TableActions {
table: {
actions: DatasourceTableActions
}
}
export type Store = TableActions
export const createActions = (context: StoreContext): TableActions => {
const { API, datasource, columns } = context const { API, datasource, columns } = context
const saveDefinition = async newDefinition => { const saveDefinition = async (newDefinition: SaveTableRequest) => {
await API.saveTable(newDefinition) await API.saveTable(newDefinition)
} }
const saveRow = async row => { const saveRow = async (row: SaveRowRequest) => {
row = { row = {
...row, ...row,
tableId: get(datasource)?.tableId, tableId: get(datasource)?.tableId,
@ -18,15 +34,15 @@ export const createActions = context => {
return await API.saveRow(row, SuppressErrors) return await API.saveRow(row, SuppressErrors)
} }
const deleteRows = async rows => { const deleteRows = async (rows: Row[]) => {
await API.deleteRows(get(datasource).tableId, rows) await API.deleteRows(get(datasource).tableId, rows)
} }
const isDatasourceValid = datasource => { const isDatasourceValid = (datasource: UIDatasource) => {
return datasource?.type === "table" && datasource?.tableId return datasource?.type === "table" && !!datasource?.tableId
} }
const getRow = async id => { const getRow = async (id: any) => {
const res = await API.searchTable(get(datasource).tableId, { const res = await API.searchTable(get(datasource).tableId, {
limit: 1, limit: 1,
query: { query: {
@ -39,7 +55,7 @@ export const createActions = context => {
return res?.rows?.[0] return res?.rows?.[0]
} }
const canUseColumn = name => { const canUseColumn = (name: string) => {
return get(columns).some(col => col.name === name) return get(columns).some(col => col.name === name)
} }
@ -58,7 +74,7 @@ export const createActions = context => {
} }
} }
export const initialise = context => { export const initialise = (context: StoreContext) => {
const { const {
datasource, datasource,
fetch, fetch,
@ -74,7 +90,7 @@ export const initialise = context => {
// Keep a list of subscriptions so that we can clear them when the datasource // Keep a list of subscriptions so that we can clear them when the datasource
// config changes // config changes
let unsubscribers = [] let unsubscribers: any[] = []
// Observe datasource changes and apply logic for table datasources // Observe datasource changes and apply logic for table datasources
datasource.subscribe($datasource => { datasource.subscribe($datasource => {

View File

@ -1,16 +1,33 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { SortOrder } from "@budibase/types" import {
Row,
SaveRowRequest,
SortOrder,
UIDatasource,
UIView,
UpdateViewRequest,
} from "@budibase/types"
import { Store as StoreContext } from ".."
import { DatasourceViewActions } from "."
const SuppressErrors = true const SuppressErrors = true
export const createActions = context => { interface ViewActions {
viewV2: {
actions: DatasourceViewActions
}
}
export type Store = ViewActions
export const createActions = (context: StoreContext): ViewActions => {
const { API, datasource, columns } = context const { API, datasource, columns } = context
const saveDefinition = async newDefinition => { const saveDefinition = async (newDefinition: UpdateViewRequest) => {
await API.viewV2.update(newDefinition) await API.viewV2.update(newDefinition)
} }
const saveRow = async row => { const saveRow = async (row: SaveRowRequest) => {
const $datasource = get(datasource) const $datasource = get(datasource)
row = { row = {
...row, ...row,
@ -23,11 +40,11 @@ export const createActions = context => {
} }
} }
const deleteRows = async rows => { const deleteRows = async (rows: Row[]) => {
await API.deleteRows(get(datasource).id, rows) await API.deleteRows(get(datasource).id, rows)
} }
const getRow = async id => { const getRow = async (id: string) => {
const res = await API.viewV2.fetch(get(datasource).id, { const res = await API.viewV2.fetch(get(datasource).id, {
limit: 1, limit: 1,
query: { query: {
@ -40,13 +57,13 @@ export const createActions = context => {
return res?.rows?.[0] return res?.rows?.[0]
} }
const isDatasourceValid = datasource => { const isDatasourceValid = (datasource: UIDatasource) => {
return ( return (
datasource?.type === "viewV2" && datasource?.id && datasource?.tableId datasource?.type === "viewV2" && !!datasource?.id && !!datasource?.tableId
) )
} }
const canUseColumn = name => { const canUseColumn = (name: string) => {
return get(columns).some(col => col.name === name && col.visible) return get(columns).some(col => col.name === name && col.visible)
} }
@ -65,7 +82,7 @@ export const createActions = context => {
} }
} }
export const initialise = context => { export const initialise = (context: StoreContext) => {
const { const {
definition, definition,
datasource, datasource,
@ -85,7 +102,7 @@ export const initialise = context => {
// Keep a list of subscriptions so that we can clear them when the datasource // Keep a list of subscriptions so that we can clear them when the datasource
// config changes // config changes
let unsubscribers = [] let unsubscribers: any[] = []
// Observe datasource changes and apply logic for view V2 datasources // Observe datasource changes and apply logic for view V2 datasources
datasource.subscribe($datasource => { datasource.subscribe($datasource => {
@ -131,7 +148,7 @@ export const initialise = context => {
unsubscribers.push( unsubscribers.push(
sort.subscribe(async $sort => { sort.subscribe(async $sort => {
// Ensure we're updating the correct view // Ensure we're updating the correct view
const $view = get(definition) const $view = get(definition) as UIView
if ($view?.id !== $datasource.id) { if ($view?.id !== $datasource.id) {
return return
} }
@ -182,7 +199,7 @@ export const initialise = context => {
await datasource.actions.saveDefinition({ await datasource.actions.saveDefinition({
...$view, ...$view,
queryUI: $filter, queryUI: $filter,
}) } as never as UpdateViewRequest)
// Refresh data since view definition changed // Refresh data since view definition changed
await rows.actions.refreshData() await rows.actions.refreshData()

View File

@ -1,73 +0,0 @@
import * as Bounds from "./bounds"
import * as Columns from "./columns"
import * as Menu from "./menu"
import * as Pagination from "./pagination"
import * as Reorder from "./reorder"
import * as Resize from "./resize"
import * as Rows from "./rows"
import * as Scroll from "./scroll"
import * as UI from "./ui"
import * as Users from "./users"
import * as Validation from "./validation"
import * as Viewport from "./viewport"
import * as Clipboard from "./clipboard"
import * as Config from "./config"
import * as Sort from "./sort"
import * as Filter from "./filter"
import * as Notifications from "./notifications"
import * as Datasource from "./datasource"
import * as Table from "./datasources/table"
import * as ViewV2 from "./datasources/viewV2"
import * as NonPlus from "./datasources/nonPlus"
import * as Cache from "./cache"
import * as Conditions from "./conditions"
const DependencyOrderedStores = [
Sort,
Filter,
Bounds,
Table,
ViewV2,
NonPlus,
Datasource,
Columns,
Scroll,
Validation,
Rows,
Conditions,
UI,
Resize,
Viewport,
Reorder,
Users,
Menu,
Pagination,
Config,
Clipboard,
Notifications,
Cache,
]
export const attachStores = context => {
// Atomic store creation
for (let store of DependencyOrderedStores) {
context = { ...context, ...store.createStores?.(context) }
}
// Derived store creation
for (let store of DependencyOrderedStores) {
context = { ...context, ...store.deriveStores?.(context) }
}
// Action creation
for (let store of DependencyOrderedStores) {
context = { ...context, ...store.createActions?.(context) }
}
// Initialise any store logic
for (let store of DependencyOrderedStores) {
store.initialise?.(context)
}
return context
}

View File

@ -0,0 +1,121 @@
import { Writable } from "svelte/store"
import type { APIClient } from "../../../api/types"
import * as Bounds from "./bounds"
import * as Columns from "./columns"
import * as Menu from "./menu"
import * as Pagination from "./pagination"
import * as Reorder from "./reorder"
import * as Resize from "./resize"
import * as Rows from "./rows"
import * as Scroll from "./scroll"
import * as UI from "./ui"
import * as Users from "./users"
import * as Validation from "./validation"
import * as Viewport from "./viewport"
import * as Clipboard from "./clipboard"
import * as Config from "./config"
import * as Sort from "./sort"
import * as Filter from "./filter"
import * as Notifications from "./notifications"
import * as Datasource from "./datasource"
import * as Table from "./datasources/table"
import * as ViewV2 from "./datasources/viewV2"
import * as NonPlus from "./datasources/nonPlus"
import * as Cache from "./cache"
import * as Conditions from "./conditions"
const DependencyOrderedStores = [
Sort,
Filter,
Bounds,
Table,
ViewV2,
NonPlus,
Datasource,
Columns,
Scroll,
Validation,
Rows,
Conditions,
UI,
Resize,
Viewport,
Reorder,
Users,
Menu,
Pagination,
Config as any,
Clipboard,
Notifications,
Cache,
]
export interface BaseStore {
API: APIClient
}
export type Store = BaseStore &
Columns.Store &
Table.Store &
ViewV2.Store &
NonPlus.Store &
Datasource.Store &
Validation.Store &
Users.Store &
Menu.Store & {
// TODO while typing the rest of stores
fetch: Writable<any>
filter: Writable<any>
inlineFilters: Writable<any>
allFilters: Writable<any>
sort: Writable<any>
initialFilter: Writable<any>
initialSortColumn: Writable<any>
initialSortOrder: Writable<any>
rows: Writable<any> & { actions: any }
subscribe: any
config: Writable<any>
dispatch: (event: string, data: any) => any
notifications: Writable<any>
schemaOverrides: Writable<any>
focusedCellId: Writable<any>
previousFocusedRowId: Writable<string>
gridID: string
selectedRows: Writable<any>
selectedRowCount: Writable<any>
selectedCellMap: Writable<any>
selectedCellCount: Writable<any>
}
export const attachStores = (context: Store): Store => {
// Atomic store creation
for (let store of DependencyOrderedStores) {
if ("createStores" in store) {
context = { ...context, ...store.createStores?.(context) }
}
}
// Derived store creation
for (let store of DependencyOrderedStores) {
if ("deriveStores" in store) {
context = { ...context, ...store.deriveStores?.(context) }
}
}
// Action creation
for (let store of DependencyOrderedStores) {
if ("createActions" in store) {
context = { ...context, ...store.createActions?.(context) }
}
}
// Initialise any store logic
for (let store of DependencyOrderedStores) {
if ("initialise" in store) {
store.initialise?.(context)
}
}
return context as Store
}

View File

@ -1,8 +1,24 @@
import { writable, get } from "svelte/store" import { writable, get, Writable } from "svelte/store"
import { Store as StoreContext } from "."
import { parseCellID } from "../lib/utils" import { parseCellID } from "../lib/utils"
interface MenuStoreData {
left: number
top: number
visible: boolean
multiRowMode: boolean
multiCellMode: boolean
}
interface MenuStore {
menu: Writable<MenuStoreData>
}
export type Store = MenuStore
export const createStores = () => { export const createStores = () => {
const menu = writable({ const menu = writable<MenuStoreData>({
left: 0, left: 0,
top: 0, top: 0,
visible: false, visible: false,
@ -14,7 +30,7 @@ export const createStores = () => {
} }
} }
export const createActions = context => { export const createActions = (context: StoreContext) => {
const { const {
menu, menu,
focusedCellId, focusedCellId,
@ -25,7 +41,7 @@ export const createActions = context => {
selectedCellCount, selectedCellCount,
} = context } = context
const open = (cellId, e) => { const open = (cellId: string, e: MouseEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@ -37,7 +53,7 @@ export const createActions = context => {
} }
// Compute bounds of cell relative to outer data node // Compute bounds of cell relative to outer data node
const targetBounds = e.target.getBoundingClientRect() const targetBounds = (e.target as HTMLElement).getBoundingClientRect()
const dataBounds = dataNode.getBoundingClientRect() const dataBounds = dataNode.getBoundingClientRect()
// Check if there are multiple rows selected, and if this is one of them // Check if there are multiple rows selected, and if this is one of them

View File

@ -1,11 +1,38 @@
import { writable, get, derived } from "svelte/store" import { writable, get, derived, Writable, Readable } from "svelte/store"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
import { Store as StoreContext } from "."
import { UIUser } from "@budibase/types"
export const createStores = () => { interface UIEnrichedUser extends UIUser {
const users = writable([]) color: string
label: string
}
interface UsersStore {
users: Writable<UIUser[]>
}
interface DerivedUsersStore {
userCellMap: Readable<Record<string, UIUser>>
}
interface ActionUserStore {
users: UsersStore["users"] &
Readable<UIEnrichedUser[]> & {
actions: {
updateUser: (user: UIUser) => void
removeUser: (sessionId: string) => void
}
}
}
export type Store = DerivedUsersStore & ActionUserStore
export const createStores = (): UsersStore => {
const users = writable<UIUser[]>([])
const enrichedUsers = derived(users, $users => { const enrichedUsers = derived(users, $users => {
return $users.map(user => ({ return $users.map<UIEnrichedUser>(user => ({
...user, ...user,
color: helpers.getUserColor(user), color: helpers.getUserColor(user),
label: helpers.getUserLabel(user), label: helpers.getUserLabel(user),
@ -20,7 +47,7 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const deriveStores = (context: StoreContext): DerivedUsersStore => {
const { users, focusedCellId } = context const { users, focusedCellId } = context
// Generate a lookup map of cell ID to the user that has it selected, to make // Generate a lookup map of cell ID to the user that has it selected, to make
@ -28,7 +55,7 @@ export const deriveStores = context => {
const userCellMap = derived( const userCellMap = derived(
[users, focusedCellId], [users, focusedCellId],
([$users, $focusedCellId]) => { ([$users, $focusedCellId]) => {
let map = {} let map: Record<string, UIUser> = {}
$users.forEach(user => { $users.forEach(user => {
const cellId = user.gridMetadata?.focusedCellId const cellId = user.gridMetadata?.focusedCellId
if (cellId && cellId !== $focusedCellId) { if (cellId && cellId !== $focusedCellId) {
@ -44,10 +71,10 @@ export const deriveStores = context => {
} }
} }
export const createActions = context => { export const createActions = (context: StoreContext): ActionUserStore => {
const { users } = context const { users } = context
const updateUser = user => { const updateUser = (user: UIUser) => {
const $users = get(users) const $users = get(users)
if (!$users.some(x => x.sessionId === user.sessionId)) { if (!$users.some(x => x.sessionId === user.sessionId)) {
users.set([...$users, user]) users.set([...$users, user])
@ -60,7 +87,7 @@ export const createActions = context => {
} }
} }
const removeUser = sessionId => { const removeUser = (sessionId: string) => {
users.update(state => { users.update(state => {
return state.filter(x => x.sessionId !== sessionId) return state.filter(x => x.sessionId !== sessionId)
}) })

View File

@ -1,10 +1,21 @@
import { writable, get, derived } from "svelte/store" import { writable, get, derived, Writable, Readable } from "svelte/store"
import { Store as StoreContext } from "."
import { parseCellID } from "../lib/utils" import { parseCellID } from "../lib/utils"
interface ValidationStore {
validation: Writable<Record<string, string>>
}
interface DerivedValidationStore {
validationRowLookupMap: Readable<Record<string, string[]>>
}
export type Store = ValidationStore & DerivedValidationStore
// Normally we would break out actions into the explicit "createActions" // Normally we would break out actions into the explicit "createActions"
// function, but for validation all these actions are pure so can go into // function, but for validation all these actions are pure so can go into
// "createStores" instead to make dependency ordering simpler // "createStores" instead to make dependency ordering simpler
export const createStores = () => { export const createStores = (): ValidationStore => {
const validation = writable({}) const validation = writable({})
return { return {
@ -12,12 +23,12 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const deriveStores = (context: StoreContext): DerivedValidationStore => {
const { validation } = context const { validation } = context
// Derive which rows have errors so that we can use that info later // Derive which rows have errors so that we can use that info later
const validationRowLookupMap = derived(validation, $validation => { const validationRowLookupMap = derived(validation, $validation => {
let map = {} const map: Record<string, string[]> = {}
Object.entries($validation).forEach(([key, error]) => { Object.entries($validation).forEach(([key, error]) => {
// Extract row ID from all errored cell IDs // Extract row ID from all errored cell IDs
if (error) { if (error) {
@ -36,10 +47,10 @@ export const deriveStores = context => {
} }
} }
export const createActions = context => { export const createActions = (context: StoreContext) => {
const { validation, focusedCellId, validationRowLookupMap } = context const { validation, focusedCellId, validationRowLookupMap } = context
const setError = (cellId, error) => { const setError = (cellId: string | undefined, error: string) => {
if (!cellId) { if (!cellId) {
return return
} }
@ -49,11 +60,11 @@ export const createActions = context => {
})) }))
} }
const rowHasErrors = rowId => { const rowHasErrors = (rowId: string) => {
return get(validationRowLookupMap)[rowId]?.length > 0 return get(validationRowLookupMap)[rowId]?.length > 0
} }
const focusFirstRowError = rowId => { const focusFirstRowError = (rowId: string) => {
const errorCells = get(validationRowLookupMap)[rowId] const errorCells = get(validationRowLookupMap)[rowId]
const cellId = errorCells?.[0] const cellId = errorCells?.[0]
if (cellId) { if (cellId) {
@ -73,7 +84,7 @@ export const createActions = context => {
} }
} }
export const initialise = context => { export const initialise = (context: StoreContext) => {
const { validation, previousFocusedRowId, validationRowLookupMap } = context const { validation, previousFocusedRowId, validationRowLookupMap } = context
// Remove validation errors when changing rows // Remove validation errors when changing rows

View File

@ -1,103 +0,0 @@
import { FieldType, RelationshipType } from "@budibase/types"
import { Helpers } from "@budibase/bbui"
const columnTypeManyTypeOverrides = {
[FieldType.DATETIME]: FieldType.STRING,
[FieldType.BOOLEAN]: FieldType.STRING,
[FieldType.SIGNATURE_SINGLE]: FieldType.ATTACHMENTS,
}
const columnTypeManyParser = {
[FieldType.DATETIME]: (value, field) => {
function parseDate(value) {
const { timeOnly, dateOnly, ignoreTimezones } = field || {}
const enableTime = !dateOnly
const parsedValue = Helpers.parseDate(value, {
timeOnly,
enableTime,
ignoreTimezones,
})
const parsed = Helpers.getDateDisplayValue(parsedValue, {
enableTime,
timeOnly,
})
return parsed
}
return value.map(v => parseDate(v))
},
[FieldType.BOOLEAN]: value => value.map(v => !!v),
[FieldType.BB_REFERENCE_SINGLE]: value => [
...new Map(value.map(i => [i._id, i])).values(),
],
[FieldType.BB_REFERENCE]: value => [
...new Map(value.map(i => [i._id, i])).values(),
],
[FieldType.ARRAY]: value => Array.from(new Set(value)),
}
export function enrichSchemaWithRelColumns(schema) {
if (!schema) {
return
}
const result = Object.keys(schema).reduce((result, fieldName) => {
const field = schema[fieldName]
result[fieldName] = field
if (field.visible !== false && field.columns) {
const fromSingle =
field?.relationshipType === RelationshipType.ONE_TO_MANY
for (const relColumn of Object.keys(field.columns)) {
const relField = field.columns[relColumn]
if (!relField.visible) {
continue
}
const name = `${field.name}.${relColumn}`
result[name] = {
...relField,
name,
related: { field: fieldName, subField: relColumn },
cellRenderType:
(!fromSingle && columnTypeManyTypeOverrides[relField.type]) ||
relField.type,
}
}
}
return result
}, {})
return result
}
export function getRelatedTableValues(row, field, fromField) {
const fromSingle =
fromField?.relationshipType === RelationshipType.ONE_TO_MANY
let result = ""
if (fromSingle) {
result = row[field.related.field]?.[0]?.[field.related.subField]
} else {
const parser = columnTypeManyParser[field.type] || (value => value)
const value = row[field.related.field]
?.flatMap(r => r[field.related.subField])
?.filter(i => i !== undefined && i !== null)
result = parser(value || [], field)
if (
[
FieldType.STRING,
FieldType.NUMBER,
FieldType.BIGINT,
FieldType.BOOLEAN,
FieldType.DATETIME,
FieldType.LONGFORM,
FieldType.BARCODEQR,
].includes(field.type)
) {
result = result?.join(", ")
}
}
return result
}

View File

@ -0,0 +1,129 @@
import { Helpers } from "@budibase/bbui"
import {
FieldType,
isRelationshipField,
RelationshipType,
Row,
UIFieldSchema,
} from "@budibase/types"
const columnTypeManyTypeOverrides: Partial<Record<FieldType, FieldType>> = {
[FieldType.DATETIME]: FieldType.STRING,
[FieldType.BOOLEAN]: FieldType.STRING,
[FieldType.SIGNATURE_SINGLE]: FieldType.ATTACHMENTS,
}
const columnTypeManyParser = {
[FieldType.DATETIME]: (
value: any[],
field: {
timeOnly?: boolean
dateOnly?: boolean
}
) => {
function parseDate(value: any) {
const { timeOnly, dateOnly } = field || {}
const enableTime = !dateOnly
const parsedValue = Helpers.parseDate(value, { enableTime })
const parsed = Helpers.getDateDisplayValue(parsedValue, {
enableTime,
timeOnly,
})
return parsed
}
return value.map(v => parseDate(v))
},
[FieldType.BOOLEAN]: (value: any[]) => value.map(v => !!v),
[FieldType.BB_REFERENCE_SINGLE]: (value: any[]) => [
...new Map(value.map(i => [i._id, i])).values(),
],
[FieldType.BB_REFERENCE]: (value: any[]) => [
...new Map(value.map(i => [i._id, i])).values(),
],
[FieldType.ARRAY]: (value: any[]) => Array.from(new Set(value)),
}
export function enrichSchemaWithRelColumns(
schema: Record<string, UIFieldSchema>
): Record<string, UIFieldSchema> | undefined {
if (!schema) {
return
}
const result = Object.keys(schema).reduce<Record<string, UIFieldSchema>>(
(result, fieldName) => {
const field = schema[fieldName]
result[fieldName] = field
if (
field.visible !== false &&
isRelationshipField(field) &&
field.columns
) {
const fromSingle =
field?.relationshipType === RelationshipType.ONE_TO_MANY
for (const relColumn of Object.keys(field.columns)) {
const relField = field.columns[relColumn]
if (!relField.visible) {
continue
}
const name = `${field.name}.${relColumn}`
result[name] = {
...relField,
type: relField.type as any, // TODO
name,
related: { field: fieldName, subField: relColumn },
cellRenderType:
(!fromSingle && columnTypeManyTypeOverrides[relField.type]) ||
relField.type,
}
}
}
return result
},
{}
)
return result
}
export function getRelatedTableValues(
row: Row,
field: UIFieldSchema & { related: { field: string; subField: string } },
fromField: UIFieldSchema
) {
const fromSingle =
isRelationshipField(fromField) &&
fromField?.relationshipType === RelationshipType.ONE_TO_MANY
let result = ""
if (fromSingle) {
result = row[field.related.field]?.[0]?.[field.related.subField]
} else {
const parser =
columnTypeManyParser[field.type as keyof typeof columnTypeManyParser] ||
((value: any) => value)
const value = row[field.related.field]
?.flatMap((r: Row) => r[field.related.subField])
?.filter((i: any) => i !== undefined && i !== null)
const parsed = parser(value || [], field as any)
result = parsed as any
if (
[
FieldType.STRING,
FieldType.NUMBER,
FieldType.BIGINT,
FieldType.BOOLEAN,
FieldType.DATETIME,
FieldType.LONGFORM,
FieldType.BARCODEQR,
].includes(field.type)
) {
result = parsed?.join(", ")
}
}
return result
}

View File

@ -1,13 +1,12 @@
{ {
"extends": "../../tsconfig.build.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "preserve",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"outDir": "./dist",
"skipLibCheck": true, "skipLibCheck": true,
"paths": { "allowJs": true
"@budibase/types": ["../types/src"],
"@budibase/shared-core": ["../shared-core/src"],
"@budibase/bbui": ["../bbui/src"]
}
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]

@ -1 +1 @@
Subproject commit 7fc699463b3957eb050351b983edef0d25a531ae Subproject commit ae786121d923449b0ad5fcbd123d0a9fec28f65e

View File

@ -37,6 +37,7 @@ import { jsonFromCsvString } from "../../../utilities/csv"
import { builderSocket } from "../../../websockets" import { builderSocket } from "../../../websockets"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { import {
canBeDisplayColumn,
helpers, helpers,
PROTECTED_EXTERNAL_COLUMNS, PROTECTED_EXTERNAL_COLUMNS,
PROTECTED_INTERNAL_COLUMNS, PROTECTED_INTERNAL_COLUMNS,
@ -67,6 +68,27 @@ function checkDefaultFields(table: Table) {
} }
} }
async function guardTable(table: Table, isCreate: boolean) {
checkDefaultFields(table)
if (
table.primaryDisplay &&
!canBeDisplayColumn(table.schema[table.primaryDisplay]?.type)
) {
// Prevent throwing errors from existing badly configured tables. Only throw for new tables or if this setting is being updated
if (
isCreate ||
(await sdk.tables.getTable(table._id!)).primaryDisplay !==
table.primaryDisplay
) {
throw new HTTPError(
`Column "${table.primaryDisplay}" cannot be used as a display type.`,
400
)
}
}
}
// covers both internal and external // covers both internal and external
export async function fetch(ctx: UserCtx<void, FetchTablesResponse>) { export async function fetch(ctx: UserCtx<void, FetchTablesResponse>) {
const internal = await sdk.tables.getAllInternalTables() const internal = await sdk.tables.getAllInternalTables()
@ -111,7 +133,7 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
const isCreate = !table._id const isCreate = !table._id
checkDefaultFields(table) await guardTable(table, isCreate)
let savedTable: Table let savedTable: Table
if (isCreate) { if (isCreate) {

View File

@ -3399,7 +3399,7 @@ if (descriptions.length) {
type: FieldType.LINK, type: FieldType.LINK,
relationshipType: RelationshipType.MANY_TO_ONE, relationshipType: RelationshipType.MANY_TO_ONE,
tableId: toRelateTableId, tableId: toRelateTableId,
fieldName: "link", fieldName: "main",
}, },
}) })
@ -3408,7 +3408,7 @@ if (descriptions.length) {
) )
await config.api.table.save({ await config.api.table.save({
...toRelateTable, ...toRelateTable,
primaryDisplay: "link", primaryDisplay: "name",
}) })
const relatedRows = await Promise.all([ const relatedRows = await Promise.all([
config.api.row.save(toRelateTable._id!, { config.api.row.save(toRelateTable._id!, {

View File

@ -185,6 +185,62 @@ if (descriptions.length) {
) )
} }
) )
it("can set primary display", async () => {
const columnName = generator.word()
const table = await config.api.table.save(
tableForDatasource(datasource, {
primaryDisplay: columnName,
schema: {
[columnName]: {
name: columnName,
type: FieldType.STRING,
},
},
})
)
expect(table.primaryDisplay).toEqual(columnName)
const res = await config.api.table.get(table._id!)
expect(res.primaryDisplay).toEqual(columnName)
})
it("cannot use unexisting columns as primary display", async () => {
const columnName = generator.word()
await config.api.table.save(
tableForDatasource(datasource, {
primaryDisplay: columnName,
}),
{
status: 400,
body: {
message: `Column "${columnName}" cannot be used as a display type.`,
},
}
)
})
it("cannot use invalid column types as display name", async () => {
const columnName = generator.word()
await config.api.table.save(
tableForDatasource(datasource, {
primaryDisplay: columnName,
schema: {
[columnName]: {
name: columnName,
type: FieldType.BOOLEAN,
},
},
}),
{
status: 400,
body: {
message: `Column "${columnName}" cannot be used as a display type.`,
},
}
)
})
}) })
describe("permissions", () => { describe("permissions", () => {
@ -603,6 +659,49 @@ if (descriptions.length) {
} }
expect(response).toEqual(expectedResponse) expect(response).toEqual(expectedResponse)
}) })
it("cannot use unexisting columns as primary display", async () => {
const table = await config.api.table.save(
tableForDatasource(datasource)
)
const columnName = generator.word()
const tableRequest = {
...table,
primaryDisplay: columnName,
}
await config.api.table.save(tableRequest, {
status: 400,
body: {
message: `Column "${columnName}" cannot be used as a display type.`,
},
})
})
it("cannot use invalid column types as display name", async () => {
const table = await config.api.table.save(
tableForDatasource(datasource)
)
const columnName = generator.word()
const tableRequest: SaveTableRequest = {
...table,
primaryDisplay: columnName,
schema: {
...table.schema,
[columnName]: {
name: columnName,
type: FieldType.BOOLEAN,
},
},
}
await config.api.table.save(tableRequest, {
status: 400,
body: {
message: `Column "${columnName}" cannot be used as a display type.`,
},
})
})
}) })
describe("import", () => { describe("import", () => {

View File

@ -12,6 +12,7 @@ interface FirebaseConfig {
email: string email: string
privateKey: string privateKey: string
projectId: string projectId: string
databaseId?: string
} }
const SCHEMA: Integration = { const SCHEMA: Integration = {
@ -30,12 +31,21 @@ const SCHEMA: Integration = {
}, },
privateKey: { privateKey: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
display: "Private Key",
required: true, required: true,
}, },
projectId: { projectId: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
display: "Project ID",
required: true, required: true,
}, },
databaseId: {
type: DatasourceFieldType.STRING,
display: "Database ID",
required: false,
default: "(default)",
placeholder: "(default)",
},
}, },
query: { query: {
create: { create: {
@ -97,6 +107,7 @@ class FirebaseIntegration implements IntegrationBase {
this.config = config this.config = config
this.client = new Firestore({ this.client = new Firestore({
projectId: config.projectId, projectId: config.projectId,
databaseId: config.databaseId || "(default)",
credentials: { credentials: {
client_email: config.email, client_email: config.email,
private_key: config.privateKey?.replace(/\\n/g, "\n"), private_key: config.privateKey?.replace(/\\n/g, "\n"),

View File

@ -324,8 +324,8 @@ export async function update(
return pickApi(tableId).update(tableId, view) return pickApi(tableId).update(tableId, view)
} }
export function isV2(view: View | ViewV2): view is ViewV2 { export function isV2(view: View | ViewV2) {
return (view as ViewV2).version === 2 return helpers.views.isV2(view)
} }
export async function remove(viewId: string): Promise<ViewV2> { export async function remove(viewId: string): Promise<ViewV2> {

View File

@ -456,7 +456,7 @@ export function filterAutomation(appId: string, tableId?: string): Automation {
icon: "Icon", icon: "Icon",
id: "a", id: "a",
type: AutomationStepType.TRIGGER, type: AutomationStepType.TRIGGER,
event: "row:save", event: AutomationEventType.ROW_SAVE,
stepId: AutomationTriggerStepId.ROW_SAVED, stepId: AutomationTriggerStepId.ROW_SAVED,
inputs: { inputs: {
tableId: tableId!, tableId: tableId!,
@ -498,7 +498,7 @@ export function updateRowAutomationWithFilters(
icon: "Icon", icon: "Icon",
id: "a", id: "a",
type: AutomationStepType.TRIGGER, type: AutomationStepType.TRIGGER,
event: "row:update", event: AutomationEventType.ROW_UPDATE,
stepId: AutomationTriggerStepId.ROW_UPDATED, stepId: AutomationTriggerStepId.ROW_UPDATED,
inputs: { tableId }, inputs: { tableId },
schema: TRIGGER_DEFINITIONS.ROW_UPDATED.schema, schema: TRIGGER_DEFINITIONS.ROW_UPDATED.schema,
@ -513,7 +513,7 @@ export function basicAutomationResults(
return { return {
automationId, automationId,
status: AutomationStatus.SUCCESS, status: AutomationStatus.SUCCESS,
trigger: "trigger", trigger: "trigger" as any,
steps: [ steps: [
{ {
stepId: AutomationActionStepId.SERVER_LOG, stepId: AutomationActionStepId.SERVER_LOG,

View File

@ -1,5 +1,6 @@
import { import {
BasicViewFieldMetadata, BasicViewFieldMetadata,
View,
ViewCalculationFieldMetadata, ViewCalculationFieldMetadata,
ViewFieldMetadata, ViewFieldMetadata,
ViewV2, ViewV2,
@ -43,3 +44,7 @@ export function basicFields(view: UnsavedViewV2, opts?: { visible?: boolean }) {
return !isCalculationField(field) && (!visible || isVisible(field)) return !isCalculationField(field) && (!visible || isVisible(field))
}) })
} }
export function isV2(view: View | ViewV2): view is ViewV2 {
return (view as ViewV2).version === 2
}

View File

@ -12,8 +12,8 @@ const allowDisplayColumnByType: Record<FieldType, boolean> = {
[FieldType.AUTO]: true, [FieldType.AUTO]: true,
[FieldType.INTERNAL]: true, [FieldType.INTERNAL]: true,
[FieldType.BARCODEQR]: true, [FieldType.BARCODEQR]: true,
[FieldType.BIGINT]: true, [FieldType.BIGINT]: true,
[FieldType.BOOLEAN]: false, [FieldType.BOOLEAN]: false,
[FieldType.ARRAY]: false, [FieldType.ARRAY]: false,
[FieldType.ATTACHMENTS]: false, [FieldType.ATTACHMENTS]: false,

View File

@ -148,6 +148,7 @@ export interface Automation extends Document {
interface BaseIOStructure { interface BaseIOStructure {
type?: AutomationIOType type?: AutomationIOType
subtype?: AutomationIOType
customType?: AutomationCustomIOType customType?: AutomationCustomIOType
title?: string title?: string
description?: string description?: string
@ -192,7 +193,7 @@ export enum AutomationStoppedReason {
export interface AutomationResults { export interface AutomationResults {
automationId?: string automationId?: string
status?: AutomationStatus status?: AutomationStatus
trigger?: any trigger?: AutomationTrigger
steps: { steps: {
stepId: AutomationTriggerStepId | AutomationActionStepId stepId: AutomationTriggerStepId | AutomationActionStepId
inputs: { inputs: {

View File

@ -6,6 +6,7 @@ import {
AutomationFeature, AutomationFeature,
InputOutputBlock, InputOutputBlock,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType,
} from "./automation" } from "./automation"
import { import {
CollectStepInputs, CollectStepInputs,
@ -142,6 +143,7 @@ export type ActionImplementations<T extends Hosting> = {
export interface AutomationStepSchemaBase { export interface AutomationStepSchemaBase {
name: string name: string
stepTitle?: string stepTitle?: string
event?: AutomationEventType
tagline: string tagline: string
icon: string icon: string
description: string description: string
@ -344,7 +346,7 @@ export interface AutomationTriggerSchema<
> extends AutomationStepSchemaBase { > extends AutomationStepSchemaBase {
id: string id: string
type: AutomationStepType.TRIGGER type: AutomationStepType.TRIGGER
event?: string event?: AutomationEventType
cronJobId?: string cronJobId?: string
stepId: TTrigger stepId: TTrigger
inputs: AutomationTriggerInputs<TTrigger> & Record<string, any> // The record union to be removed once the types are fixed inputs: AutomationTriggerInputs<TTrigger> & Record<string, any> // The record union to be removed once the types are fixed

View File

@ -119,6 +119,7 @@ interface DatasourceBasicFieldConfig {
default?: any default?: any
deprecated?: boolean deprecated?: boolean
hidden?: string hidden?: string
placeholder?: string
} }
interface DatasourceSelectFieldConfig extends DatasourceBasicFieldConfig { interface DatasourceSelectFieldConfig extends DatasourceBasicFieldConfig {

View File

@ -0,0 +1,12 @@
export interface BranchPath {
stepIdx: number
branchIdx: number
branchStepId: string
id: string
}
export interface BlockDefinitions {
TRIGGER: Record<string, any>
CREATABLE_TRIGGER: Record<string, any>
ACTION: Record<string, any>
}

View File

@ -0,0 +1,19 @@
import { CalculationType, FieldSchema, FieldType } from "@budibase/types"
export type UIColumn = FieldSchema & {
label: string
readonly: boolean
conditions: any
related?: {
field: string
subField: string
}
primaryDisplay?: boolean
schema?: {
disabled: boolean
type: FieldType
readonly: boolean
autocolumn: boolean
}
calculationType: CalculationType
}

View File

@ -0,0 +1,11 @@
import { UITable, UIView } from "@budibase/types"
export type UIDatasource = (UITable | UIView) & {
type: string
}
export interface UIFieldMutation {
visible?: boolean
readonly?: boolean
width?: number
}

View File

@ -0,0 +1,5 @@
export * from "./columns"
export * from "./datasource"
export * from "./table"
export * from "./view"
export * from "./user"

View File

@ -0,0 +1,34 @@
import {
BasicViewFieldMetadata,
FieldSchema,
FieldType,
RelationSchemaField,
SortOrder,
Table,
UISearchFilter,
} from "@budibase/types"
export interface UITable extends Omit<Table, "type"> {
name: string
id: string
type: string
tableId: string
primaryDisplay?: string
sort?: {
field: string
order: SortOrder
}
queryUI: UISearchFilter
schema: Record<string, UIFieldSchema>
}
export type UIFieldSchema = FieldSchema &
BasicViewFieldMetadata & {
related?: { field: string; subField: string }
columns?: Record<string, UIRelationSchemaField>
cellRenderType?: string
}
interface UIRelationSchemaField extends RelationSchemaField {
type: FieldType
}

View File

@ -0,0 +1,6 @@
import { User } from "@budibase/types"
export interface UIUser extends User {
sessionId: string
gridMetadata?: { focusedCellId?: string }
}

View File

@ -0,0 +1,6 @@
import { ViewV2 } from "@budibase/types"
import { UIFieldSchema } from "./table"
export interface UIView extends ViewV2 {
schema: Record<string, UIFieldSchema>
}

View File

@ -1 +1,3 @@
export * from "./integration" export * from "./integration"
export * from "./automations"
export * from "./grid"

View File

@ -5946,6 +5946,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/shortid@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@types/shortid/-/shortid-2.2.0.tgz#905990fc4275f77e60ab0cd9f791b91a3d4bff04"
integrity sha512-jBG2FgBxcaSf0h662YloTGA32M8UtNbnTPekUr/eCmWXq0JWQXgNEQ/P5Gf05Cv66QZtE1Ttr83I1AJBPdzCBg==
"@types/ssh2-streams@*": "@types/ssh2-streams@*":
version "0.1.12" version "0.1.12"
resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz#e68795ba2bf01c76b93f9c9809e1f42f0eaaec5f" resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz#e68795ba2bf01c76b93f9c9809e1f42f0eaaec5f"
@ -18639,16 +18644,7 @@ string-length@^4.0.1:
char-regex "^1.0.2" char-regex "^1.0.2"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -18740,7 +18736,7 @@ stringify-object@^3.2.1:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -18754,13 +18750,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies: dependencies:
ansi-regex "^4.1.0" ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1: strip-ansi@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@ -20508,7 +20497,7 @@ worker-farm@1.7.0:
dependencies: dependencies:
errno "~0.1.7" errno "~0.1.7"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -20526,15 +20515,6 @@ wrap-ansi@^5.1.0:
string-width "^3.0.0" string-width "^3.0.0"
strip-ansi "^5.0.0" strip-ansi "^5.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"