diff --git a/web-frontend/modules/core/components/auth/TOTPLogin.vue b/web-frontend/modules/core/components/auth/TOTPLogin.vue
index c05a02a74a..9875ec0686 100644
--- a/web-frontend/modules/core/components/auth/TOTPLogin.vue
+++ b/web-frontend/modules/core/components/auth/TOTPLogin.vue
@@ -15,6 +15,10 @@
{{ $t('totpLogin.backupCodesDescription') }}
+
+ {{ errorTitle }}
+ {{ errorDescription }}
+
-
+
+
+
diff --git a/web-frontend/modules/core/components/settings/twoFactorAuth/AuthCodeInput.vue b/web-frontend/modules/core/components/settings/twoFactorAuth/AuthCodeInput.vue
index a0bfcf54bb..833c363d3a 100644
--- a/web-frontend/modules/core/components/settings/twoFactorAuth/AuthCodeInput.vue
+++ b/web-frontend/modules/core/components/settings/twoFactorAuth/AuthCodeInput.vue
@@ -152,6 +152,9 @@ export default {
return this.code.length === 6
},
},
+ mounted() {
+ this.reset()
+ },
methods: {
reset() {
this.values.number1 = ''
@@ -160,9 +163,7 @@ export default {
this.values.number4 = ''
this.values.number5 = ''
this.values.number6 = ''
- this.$nextTick(() => {
- this.$refs.input1.focus()
- })
+ this.$refs.input1.focus()
},
sanitizeInput(value) {
const sanitized = value.replace(/\D/g, '').slice(0, 1)
diff --git a/web-frontend/modules/core/components/settings/twoFactorAuth/EnableWithQRCode.vue b/web-frontend/modules/core/components/settings/twoFactorAuth/EnableWithQRCode.vue
index a9deab89b5..69c338a93c 100644
--- a/web-frontend/modules/core/components/settings/twoFactorAuth/EnableWithQRCode.vue
+++ b/web-frontend/modules/core/components/settings/twoFactorAuth/EnableWithQRCode.vue
@@ -8,6 +8,14 @@
{{ $t('enableWithQRCode.scanQRCodeDescription') }}
+
+ {{ $t('enableWithQRCode.clickToCopy') }}
+
{{ $t('enableWithQRCode.enterCodeDescription') }}
+
+ {{ errorTitle }}
+ {{ errorDescription }}
+
import AuthCodeInput from '@baserow/modules/core/components/settings/twoFactorAuth/AuthCodeInput'
import TwoFactorAuthService from '@baserow/modules/core/services/twoFactorAuth'
+import { copyToClipboard } from '@baserow/modules/database/utils/clipboard'
export default {
name: 'EnableWithQRCode',
@@ -49,8 +62,20 @@ export default {
loading: false,
checkCodeLoading: false,
qr_code: null,
+ provisioning_url: null,
+ errorTitle: null,
+ errorDescription: null,
}
},
+ computed: {
+ secret() {
+ if (this.provisioning_url) {
+ const url = new URL(this.provisioning_url)
+ return url.searchParams.get('secret')
+ }
+ return ''
+ },
+ },
mounted() {
this.configureTOTP()
},
@@ -62,6 +87,7 @@ export default {
'totp'
)
this.qr_code = data.provisioning_qr_code
+ this.provisioning_url = data.provisioning_url
} catch (error) {
const title = this.$t('enableWithQRCode.provisioningFailed')
this.$store.dispatch('toast/error', { title })
@@ -70,6 +96,8 @@ export default {
}
},
async checkCode(code) {
+ this.errorTitle = null
+ this.errorDescription = null
this.checkCodeLoading = true
try {
const params = { code }
@@ -84,9 +112,20 @@ export default {
this.checkCodeLoading = false
this.$refs.authCodeInput.reset()
const title = this.$t('enableWithQRCode.verificationFailed')
- this.$store.dispatch('toast/error', { title })
+ const description = this.$t(
+ 'enableWithQRCode.verificationFailedDescription'
+ )
+ this.errorTitle = title
+ this.errorDescription = description
}
},
+ copy() {
+ copyToClipboard(this.secret)
+ this.$store.dispatch('toast/success', {
+ title: this.$t('enableWithQRCode.secretCopiedTitle'),
+ message: this.$t('enableWithQRCode.secretCopiedMessage'),
+ })
+ },
},
}
diff --git a/web-frontend/modules/core/locales/en.json b/web-frontend/modules/core/locales/en.json
index b1379119c4..88a02ec29a 100644
--- a/web-frontend/modules/core/locales/en.json
+++ b/web-frontend/modules/core/locales/en.json
@@ -115,7 +115,11 @@
"totpDescription": "Enter the code from your authenticator app.",
"verify": "Verify",
"useBackupCode": "Use backup code",
- "verificationFailed": "Verification failed"
+ "verificationFailed": "Verification failed",
+ "verificationFailedDescription": "The entered code is not correct.",
+ "loginExpired": "Login expired",
+ "loginExpiredDescription": "Please provide your password again.",
+ "rateLimit": "Too many attempts."
},
"settingsModal": {
"title": "My settings"
@@ -1075,12 +1079,20 @@
"notAllowedTitle": "2FA not enabled",
"notAllowedDescription": "Adding 2FA is only possible to password-based accounts."
},
+ "nodeHelpTooltip": {
+ "exampleLabel": "Example",
+ "result": "Result: {result}"
+ },
"enableWithQRCode": {
"scanQRCode": "Scan QR code",
"scanQRCodeDescription": "Scan the code with an app like Google Authenticator, Authy or Microsoft Authenticator.",
+ "clickToCopy": "Alternatively, click here to copy the code.",
+ "secretCopiedTitle": "Secret copied",
+ "secretCopiedMessage": "TOTP secret copied to clipboard.",
"enterCode": "Enter the code shown",
"enterCodeDescription": "Enter a 6-digit code shown by the app to confirm that you have set it up correctly.",
"verificationFailed": "Verification failed",
+ "verificationFailedDescription": "The entered code is not valid.",
"provisioningFailed": "Provisioning failed",
"checkSuccess": "Successfully enabled two-factor authentication"
}
diff --git a/web-frontend/modules/core/runtimeFormulaTypes.js b/web-frontend/modules/core/runtimeFormulaTypes.js
index d203a89519..6da86934e8 100644
--- a/web-frontend/modules/core/runtimeFormulaTypes.js
+++ b/web-frontend/modules/core/runtimeFormulaTypes.js
@@ -239,8 +239,11 @@ export class RuntimeConcat extends RuntimeFormulaFunction {
getExamples() {
return [
- "concat('Hello,', ' World!') = 'Hello, world!'",
- "concat(get('data_source.1.0.field_1'), ' bar') = 'foo bar'",
+ { formula: "concat('Hello,', ' World!')", result: '"Hello, world!"' },
+ {
+ formula: "concat(get('data_source.1.0.field_1'), ' bar')",
+ result: '"foo bar"',
+ },
]
}
}
@@ -326,7 +329,12 @@ export class RuntimeGet extends RuntimeFormulaFunction {
}
getExamples() {
- return ["get('previous_node.1.body')"]
+ return [
+ {
+ formula: "get('previous_node.1.body')",
+ result: "'Hello world'",
+ },
+ ]
}
}
@@ -364,7 +372,16 @@ export class RuntimeAdd extends RuntimeFormulaFunction {
}
getExamples() {
- return ['2 + 3 = 5', '1 + 2 + 3 = 6']
+ return [
+ {
+ formula: '2 + 3',
+ result: '5',
+ },
+ {
+ formula: '1 + 2 + 3',
+ result: '6',
+ },
+ ]
}
}
@@ -402,7 +419,16 @@ export class RuntimeMinus extends RuntimeFormulaFunction {
}
getExamples() {
- return ['3 - 2 = 1', '5 - 2 - 1 = 2']
+ return [
+ {
+ formula: '3 - 2',
+ result: '1',
+ },
+ {
+ formula: '5 - 2 - 1',
+ result: '2',
+ },
+ ]
}
}
@@ -440,7 +466,16 @@ export class RuntimeMultiply extends RuntimeFormulaFunction {
}
getExamples() {
- return ['2 * 3 = 6', '2 * 3 * 3 = 18']
+ return [
+ {
+ formula: '2 * 3',
+ result: '6',
+ },
+ {
+ formula: '2 * 3 * 3',
+ result: '18',
+ },
+ ]
}
}
@@ -478,7 +513,16 @@ export class RuntimeDivide extends RuntimeFormulaFunction {
}
getExamples() {
- return ['6 / 2 = 3', '15 / 2 / 2 = 3.75']
+ return [
+ {
+ formula: '6 / 2',
+ result: '3',
+ },
+ {
+ formula: '15 / 2 / 2',
+ result: '3.75',
+ },
+ ]
}
}
@@ -516,7 +560,20 @@ export class RuntimeEqual extends RuntimeFormulaFunction {
}
getExamples() {
- return ['2 = 3 = false', '"foo" = "bar" = false', '"foo" = "foo" = true']
+ return [
+ {
+ formula: '2 = 3',
+ result: 'false',
+ },
+ {
+ formula: '"foo" = "bar"',
+ result: 'false',
+ },
+ {
+ formula: '"foo" = "foo"',
+ result: 'true',
+ },
+ ]
}
}
@@ -554,7 +611,20 @@ export class RuntimeNotEqual extends RuntimeFormulaFunction {
}
getExamples() {
- return ['2 != 3 = true', '"foo" != "foo" = false', '"foo" != "bar" = true']
+ return [
+ {
+ formula: '2 != 3',
+ result: 'true',
+ },
+ {
+ formula: '"foo" != "foo"',
+ result: 'false',
+ },
+ {
+ formula: '"foo" != "bar"',
+ result: 'true',
+ },
+ ]
}
}
@@ -592,7 +662,20 @@ export class RuntimeGreaterThan extends RuntimeFormulaFunction {
}
getExamples() {
- return ['5 > 4 = true', '"a" > "b" = false', '"Ambarella" > "fig" = false']
+ return [
+ {
+ formula: '5 > 4',
+ result: 'true',
+ },
+ {
+ formula: '"a" > "b"',
+ result: 'false',
+ },
+ {
+ formula: '"Ambarella" > "fig"',
+ result: 'false',
+ },
+ ]
}
}
@@ -630,7 +713,20 @@ export class RuntimeLessThan extends RuntimeFormulaFunction {
}
getExamples() {
- return ['2 < 3 = true', '"b" < "a" = false', '"Ambarella" < "fig" = true']
+ return [
+ {
+ formula: '2 < 3',
+ result: 'true',
+ },
+ {
+ formula: '"b" < "a"',
+ result: 'false',
+ },
+ {
+ formula: '"Ambarella" < "fig"',
+ result: 'true',
+ },
+ ]
}
}
@@ -669,9 +765,18 @@ export class RuntimeGreaterThanOrEqual extends RuntimeFormulaFunction {
getExamples() {
return [
- '3 >= 2 = false',
- '"b" >= "a" = true',
- '"Ambarella" >= "fig" = false',
+ {
+ formula: '3 >= 2',
+ result: 'false',
+ },
+ {
+ formula: '"b" >= "a"',
+ result: 'true',
+ },
+ {
+ formula: '"Ambarella" >= "fig"',
+ result: 'false',
+ },
]
}
}
@@ -711,9 +816,18 @@ export class RuntimeLessThanOrEqual extends RuntimeFormulaFunction {
getExamples() {
return [
- '3 <= 3 = true',
- '"a" <= "b" = false',
- '"fig" <= "Ambarella" = false',
+ {
+ formula: '3 <= 3',
+ result: 'true',
+ },
+ {
+ formula: '"a" <= "b"',
+ result: 'false',
+ },
+ {
+ formula: '"fig" <= "Ambarella"',
+ result: 'false',
+ },
]
}
}
@@ -745,7 +859,12 @@ export class RuntimeUpper extends RuntimeFormulaFunction {
}
getExamples() {
- return ["upper('Hello, World!') = 'HELLO, WORLD!'"]
+ return [
+ {
+ formula: "upper('Hello, World!')",
+ result: "'HELLO, WORLD!'",
+ },
+ ]
}
}
@@ -776,7 +895,12 @@ export class RuntimeLower extends RuntimeFormulaFunction {
}
getExamples() {
- return ["lower('Hello, World!') = 'hello, world!'"]
+ return [
+ {
+ formula: "lower('Hello, World!')",
+ result: "'hello, world!'",
+ },
+ ]
}
}
@@ -813,7 +937,12 @@ export class RuntimeCapitalize extends RuntimeFormulaFunction {
}
getExamples() {
- return ["capitalize('hello, world!') = 'Hello, world!'"]
+ return [
+ {
+ formula: "capitalize('hello, world!')",
+ result: "'Hello, world!'",
+ },
+ ]
}
}
@@ -858,7 +987,12 @@ export class RuntimeRound extends RuntimeFormulaFunction {
}
getExamples() {
- return ["round('12.345', 2) = '12.35'"]
+ return [
+ {
+ formula: "round('12.345', 2)",
+ result: '12.35',
+ },
+ ]
}
}
@@ -889,7 +1023,16 @@ export class RuntimeIsEven extends RuntimeFormulaFunction {
}
getExamples() {
- return ['is_even(12) = true', 'is_even(13) = false']
+ return [
+ {
+ formula: 'is_even(12)',
+ result: 'true',
+ },
+ {
+ formula: 'is_even(13)',
+ result: 'false',
+ },
+ ]
}
}
@@ -920,7 +1063,16 @@ export class RuntimeIsOdd extends RuntimeFormulaFunction {
}
getExamples() {
- return ['is_odd(11) = true', 'is_odd(12) = false']
+ return [
+ {
+ formula: 'is_odd(11)',
+ result: 'true',
+ },
+ {
+ formula: 'is_odd(12)',
+ result: 'false',
+ },
+ ]
}
}
@@ -962,9 +1114,18 @@ export class RuntimeDateTimeFormat extends RuntimeFormulaFunction {
getExamples() {
return [
- "datetime_format(now(), 'YYYY-MM-DD') = '2025-11-03'",
- "datetime_format(now(), 'YYYY-MM-DD', 'Europe/Amsterdam') = '2025-11-03'",
- "datetime_format(now(), 'DD/MM/YYYY HH:mm:ss', 'UTC') = '03/11/2025 12:12:09'",
+ {
+ formula: "datetime_format(now(), 'YYYY-MM-DD')",
+ result: "'2025-11-03'",
+ },
+ {
+ formula: "datetime_format(now(), 'YYYY-MM-DD', 'Europe/Amsterdam')",
+ result: "'2025-11-03'",
+ },
+ {
+ formula: "datetime_format(now(), 'DD/MM/YYYY HH:mm:ss', 'UTC')",
+ result: "'03/11/2025 12:12:09'",
+ },
]
}
}
@@ -996,7 +1157,12 @@ export class RuntimeDay extends RuntimeFormulaFunction {
}
getExamples() {
- return ["day('2025-10-16 11:05:38') = '16'"]
+ return [
+ {
+ formula: "day('2025-10-16 11:05:38')",
+ result: '16',
+ },
+ ]
}
}
@@ -1028,7 +1194,12 @@ export class RuntimeMonth extends RuntimeFormulaFunction {
getExamples() {
// Month is 0 indexed
- return ["month('2025-10-16 11:05:38') = '9'"]
+ return [
+ {
+ formula: "month('2025-10-16 11:05:38')",
+ result: '9',
+ },
+ ]
}
}
@@ -1059,7 +1230,12 @@ export class RuntimeYear extends RuntimeFormulaFunction {
}
getExamples() {
- return ["year('2025-10-16 11:05:38') = '2025'"]
+ return [
+ {
+ formula: "year('2025-10-16 11:05:38')",
+ result: '2025',
+ },
+ ]
}
}
@@ -1090,7 +1266,12 @@ export class RuntimeHour extends RuntimeFormulaFunction {
}
getExamples() {
- return ["hour('2025-10-16 11:05:38') = '11'"]
+ return [
+ {
+ formula: "hour('2025-10-16 11:05:38')",
+ result: '11',
+ },
+ ]
}
}
@@ -1121,7 +1302,12 @@ export class RuntimeMinute extends RuntimeFormulaFunction {
}
getExamples() {
- return ["minute('2025-10-16T11:05:38') = '5'"]
+ return [
+ {
+ formula: "minute('2025-10-16T11:05:38')",
+ result: '5',
+ },
+ ]
}
}
@@ -1152,7 +1338,12 @@ export class RuntimeSecond extends RuntimeFormulaFunction {
}
getExamples() {
- return ["second('2025-10-16 11:05:38') = '38'"]
+ return [
+ {
+ formula: "second('2025-10-16 11:05:38')",
+ result: '38',
+ },
+ ]
}
}
@@ -1178,7 +1369,12 @@ export class RuntimeNow extends RuntimeFormulaFunction {
}
getExamples() {
- return ["now() = '2025-10-16 11:05:38'"]
+ return [
+ {
+ formula: 'now()',
+ result: "'2025-10-16 11:05:38'",
+ },
+ ]
}
}
@@ -1209,7 +1405,12 @@ export class RuntimeToday extends RuntimeFormulaFunction {
}
getExamples() {
- return ["today() = '2025-10-16'"]
+ return [
+ {
+ formula: 'today()',
+ result: "'2025-10-16'",
+ },
+ ]
}
}
@@ -1243,7 +1444,12 @@ export class RuntimeGetProperty extends RuntimeFormulaFunction {
}
getExamples() {
- return ["get_property('{\"cherry\": \"red\"}', 'cherry') = 'red'"]
+ return [
+ {
+ formula: 'get_property(\'{"cherry": "red"}\', \'cherry\')',
+ result: "'red'",
+ },
+ ]
}
}
@@ -1279,7 +1485,12 @@ export class RuntimeRandomInt extends RuntimeFormulaFunction {
}
getExamples() {
- return ['random_int(10, 20) = 17']
+ return [
+ {
+ formula: 'random_int(10, 20)',
+ result: '17',
+ },
+ ]
}
}
@@ -1313,7 +1524,12 @@ export class RuntimeRandomFloat extends RuntimeFormulaFunction {
}
getExamples() {
- return ['random_float(10, 20) = 18.410550297490616']
+ return [
+ {
+ formula: 'random_float(10, 20)',
+ result: '18.410550297490616',
+ },
+ ]
}
}
@@ -1344,7 +1560,12 @@ export class RuntimeRandomBool extends RuntimeFormulaFunction {
}
getExamples() {
- return ['random_bool() = true']
+ return [
+ {
+ formula: 'random_bool()',
+ result: 'true',
+ },
+ ]
}
}
@@ -1375,7 +1596,12 @@ export class RuntimeGenerateUUID extends RuntimeFormulaFunction {
}
getExamples() {
- return ["generate_uuid() = '9b772ad6-08bc-4d19-958d-7f1c21a4f4ef'"]
+ return [
+ {
+ formula: 'generate_uuid()',
+ result: "'9b772ad6-08bc-4d19-958d-7f1c21a4f4ef'",
+ },
+ ]
}
}
@@ -1411,8 +1637,15 @@ export class RuntimeIf extends RuntimeFormulaFunction {
getExamples() {
return [
- 'if(true, true, false) = true',
- "if(random_bool(), 'Random bool is true', 'Random bool is false') = 'Random bool is false'",
+ {
+ formula: 'if(true, true, false)',
+ result: 'true',
+ },
+ {
+ formula:
+ "if(random_bool(), 'Random bool is true', 'Random bool is false')",
+ result: "'Random bool is false'",
+ },
]
}
}
@@ -1447,7 +1680,16 @@ export class RuntimeAnd extends RuntimeFormulaFunction {
}
getExamples() {
- return ['true && true = true', 'true && true && false = false']
+ return [
+ {
+ formula: 'true && true',
+ result: 'true',
+ },
+ {
+ formula: 'true && true && false',
+ result: 'false',
+ },
+ ]
}
}
@@ -1482,9 +1724,18 @@ export class RuntimeOr extends RuntimeFormulaFunction {
getExamples() {
return [
- 'true || true = true',
- 'true || true || false = true',
- 'false || false = false',
+ {
+ formula: 'true || true',
+ result: 'true',
+ },
+ {
+ formula: 'true || true || false',
+ result: 'true',
+ },
+ {
+ formula: 'false || false',
+ result: 'false',
+ },
]
}
}
diff --git a/web-frontend/modules/database/store/view/bufferedRows.js b/web-frontend/modules/database/store/view/bufferedRows.js
index 0d175cb32a..faf66c9bb8 100644
--- a/web-frontend/modules/database/store/view/bufferedRows.js
+++ b/web-frontend/modules/database/store/view/bufferedRows.js
@@ -799,6 +799,7 @@ export default ({ service, customPopulateRow, fieldOptions }) => {
const updateRowsData = [
Object.assign({ id: row.id }, updateRequestValues),
]
+ commit('SET_ROW_FETCHING', { row, value: true })
const { data } = await RowService(this.$client).batchUpdate(
table.id,
updateRowsData
@@ -844,6 +845,8 @@ export default ({ service, customPopulateRow, fieldOptions }) => {
values: { ...oldValues },
})
throw error
+ } finally {
+ commit('SET_ROW_FETCHING', { row, value: false })
}
},
/**
@@ -1251,7 +1254,7 @@ export default ({ service, customPopulateRow, fieldOptions }) => {
return state.fetching
},
getRow: (state) => (id) => {
- return state.rows.find((row) => row.id === id)
+ return state.rows.find((row) => row?.id === id)
},
getRows(state) {
return state.rows
diff --git a/web-frontend/modules/database/viewTypes.js b/web-frontend/modules/database/viewTypes.js
index 2db6ce93fb..8039665cff 100644
--- a/web-frontend/modules/database/viewTypes.js
+++ b/web-frontend/modules/database/viewTypes.js
@@ -583,6 +583,11 @@ export class GridViewType extends ViewType {
if (
this.app.$featureFlagIsEnabled(FF_DATE_DEPENDENCY) &&
!isPublic &&
+ this.app.$hasPermission(
+ 'database.table.field_rules.read_field_rules',
+ view.table,
+ database.workspace.id
+ ) &&
!store.getters['fieldRules/hasRules']({ tableId: view.table_id })
) {
await store.dispatch('fieldRules/fetchInitial', {
@@ -892,6 +897,21 @@ export const BaseBufferedRowViewTypeMixin = (Base) =>
adhocSorting,
}
)
+
+ if (
+ this.app.$featureFlagIsEnabled(FF_DATE_DEPENDENCY) &&
+ !isPublic &&
+ this.app.$hasPermission(
+ 'database.table.field_rules.read_field_rules',
+ view.table,
+ database.workspace.id
+ ) &&
+ !store.getters['fieldRules/hasRules']({ tableId: view.table_id })
+ ) {
+ await store.dispatch('fieldRules/fetchInitial', {
+ tableId: view.table_id,
+ })
+ }
}
async refresh(
diff --git a/web-frontend/modules/integrations/localBaserow/serviceTypes.js b/web-frontend/modules/integrations/localBaserow/serviceTypes.js
index 703a6217a0..7f3de16be6 100644
--- a/web-frontend/modules/integrations/localBaserow/serviceTypes.js
+++ b/web-frontend/modules/integrations/localBaserow/serviceTypes.js
@@ -89,7 +89,7 @@ export class LocalBaserowTableServiceType extends ServiceType {
const [field, ...rest] = path
let humanName = field
- if (schema && field && field.startsWith('field_')) {
+ if (schema && typeof field === 'string' && field.startsWith('field_')) {
if (this.returnsList) {
if (schema.items?.properties?.[field]?.title) {
humanName = schema.items.properties[field].title
diff --git a/web-frontend/test/unit/builder/elementTypes.spec.js b/web-frontend/test/unit/builder/elementTypes.spec.js
index cd0cc945cb..27c7299ef6 100644
--- a/web-frontend/test/unit/builder/elementTypes.spec.js
+++ b/web-frontend/test/unit/builder/elementTypes.spec.js
@@ -8,7 +8,14 @@ import {
RatingElementType,
} from '@baserow/modules/builder/elementTypes'
import { TestApp } from '@baserow/test/helpers/testApp'
-
+import {
+ VISIBILITY_NOT_LOGGED,
+ VISIBILITY_LOGGED_IN,
+ ROLE_TYPE_ALLOW_EXCEPT,
+ ROLE_TYPE_DISALLOW_EXCEPT,
+ VISIBILITY_ALL,
+ ROLE_TYPE_ALLOW_ALL,
+} from '@baserow/modules/builder/constants'
import {
IFRAME_SOURCE_TYPES,
IMAGE_SOURCE_TYPES,
@@ -334,7 +341,162 @@ describe('elementTypes tests', () => {
)
})
})
+ describe('elementType isVisible', () => {
+ test('HeadingElementType isVisible', () => {
+ const elementType = testApp.getRegistry().get('element', 'heading')
+
+ const element = {
+ value: { formula: "'Heading'", mode: 'simple' },
+ roles: [],
+ role_type: ROLE_TYPE_ALLOW_ALL,
+ visibility: VISIBILITY_ALL,
+ visibility_condition: { formula: 'true', mode: 'simple' },
+ }
+
+ const applicationContextNotLogged = {
+ builder: { userSourceUser: { authenticated: false } },
+ }
+ const applicationContextLoggedAdmin = {
+ builder: {
+ userSourceUser: {
+ authenticated: true,
+ user: {
+ email: 'fake@email.com',
+ id: 42,
+ username: 'Fake',
+ role: 'admin',
+ user_source_uid: '',
+ },
+ },
+ },
+ }
+ const applicationContextLoggedUser = {
+ builder: {
+ userSourceUser: {
+ authenticated: true,
+ user: {
+ email: 'fake@email.com',
+ id: 42,
+ username: 'Fake',
+ role: 'user',
+ user_source_uid: '',
+ },
+ },
+ },
+ }
+
+ // Nothing should hide it
+ expect(
+ elementType.isVisible({
+ element,
+ applicationContext: applicationContextNotLogged,
+ })
+ ).toBeTruthy()
+
+ let elementTest = {
+ ...element,
+ visibility_condition: {
+ ...element.visibility_condition,
+ formula: 'false',
+ },
+ }
+ // The visibility formula resolves to false
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextNotLogged,
+ })
+ ).toBeFalsy()
+
+ elementTest = {
+ ...element,
+ visibility: VISIBILITY_NOT_LOGGED,
+ }
+ // Not logged only
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextNotLogged,
+ })
+ ).toBeTruthy()
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextLoggedUser,
+ })
+ ).toBeFalsy()
+
+ elementTest = {
+ ...element,
+ visibility: VISIBILITY_LOGGED_IN,
+ }
+ // Logged only
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextNotLogged,
+ })
+ ).toBeFalsy()
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextLoggedUser,
+ })
+ ).toBeTruthy()
+ elementTest = {
+ ...element,
+ visibility: VISIBILITY_LOGGED_IN,
+ role_type: ROLE_TYPE_DISALLOW_EXCEPT,
+ roles: ['admin'],
+ }
+ // Logged admin only
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextNotLogged,
+ })
+ ).toBeFalsy()
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextLoggedUser,
+ })
+ ).toBeFalsy()
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextLoggedAdmin,
+ })
+ ).toBeTruthy()
+
+ elementTest = {
+ ...element,
+ visibility: VISIBILITY_LOGGED_IN,
+ role_type: ROLE_TYPE_ALLOW_EXCEPT,
+ roles: ['admin'],
+ }
+ // Logged except admin
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextNotLogged,
+ })
+ ).toBeFalsy()
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextLoggedUser,
+ })
+ ).toBeTruthy()
+ expect(
+ elementType.isVisible({
+ element: elementTest,
+ applicationContext: applicationContextLoggedAdmin,
+ })
+ ).toBeFalsy()
+ })
+ })
describe('elementType form validation tests', () => {
test('RatingInputElementType | required | no value', () => {
const elementType = new RatingInputElementType()
@@ -738,15 +900,21 @@ describe('elementTypes tests', () => {
// Heading with missing value is invalid
expect(
- elementType.isInError({ page: {}, element: { value: { formula: '' } } })
+ elementType.isInError(
+ { value: { formula: '' } },
+ { page: {}, element: { value: { formula: '' } } }
+ )
).toBe(true)
// Heading with value is valid
expect(
- elementType.isInError({
- page: {},
- element: { value: { formula: "'Foo Heading'" } },
- })
+ elementType.isInError(
+ { value: { formula: "'Foo Heading'" } },
+ {
+ page: {},
+ element: { value: { formula: "'Foo Heading'" } },
+ }
+ )
).toBe(false)
})
})
@@ -757,15 +925,21 @@ describe('elementTypes tests', () => {
// Text with missing value is invalid
expect(
- elementType.isInError({ page: {}, element: { value: { formula: '' } } })
+ elementType.isInError(
+ { value: { formula: '' } },
+ { page: {}, element: { value: { formula: '' } } }
+ )
).toBe(true)
// Text with value is valid
expect(
- elementType.isInError({
- page: {},
- element: { value: { formula: "'Foo Text'" } },
- })
+ elementType.isInError(
+ { value: { formula: "'Foo Text'" } },
+ {
+ page: {},
+ element: { value: { formula: "'Foo Text'" } },
+ }
+ )
).toBe(false)
})
})
@@ -776,7 +950,10 @@ describe('elementTypes tests', () => {
// Link with missing text is invalid
expect(
- elementType.isInError({ element: { value: { formula: '' } } })
+ elementType.isInError(
+ { value: { formula: '' } },
+ { element: { value: { formula: '' } } }
+ )
).toBe(true)
// When navigation_type is 'page' the navigate_to_page_id must be set
@@ -785,13 +962,15 @@ describe('elementTypes tests', () => {
navigate_to_page_id: '',
value: { formula: "'Foo Link'" },
}
- expect(elementType.isInError({ page: {}, element })).toBe(true)
+ expect(elementType.isInError(element, { page: {}, element })).toBe(true)
// Otherwise it is valid
const page = { id: 10, shared: false, order: 1 }
const builder = { pages: [page] }
element.navigate_to_page_id = 10
- expect(elementType.isInError({ page, builder, element })).toBe(false)
+ expect(elementType.isInError(element, { page, builder, element })).toBe(
+ false
+ )
// When navigation_type is 'custom' the navigate_to_url must be set
element = {
@@ -799,12 +978,12 @@ describe('elementTypes tests', () => {
navigate_to_url: { formula: '' },
value: { formula: "'Test'" },
}
- expect(elementType.isInError({ page, element })).toBe(true)
+ expect(elementType.isInError(element, { page, element })).toBe(true)
// Otherwise it is valid
element.navigate_to_url = { formula: 'http://localhost' }
element.value = { formula: "'Foo Link'" }
- expect(elementType.isInError({ page, element })).toBe(false)
+ expect(elementType.isInError(element, { page, element })).toBe(false)
})
})
@@ -814,20 +993,20 @@ describe('elementTypes tests', () => {
// Image with image_source_type of 'upload' must have an image_file url
const element = { image_source_type: IMAGE_SOURCE_TYPES.UPLOAD }
- expect(elementType.isInError({ element })).toBe(true)
+ expect(elementType.isInError(element, { element })).toBe(true)
// Otherwise it is valid
element.image_file = { url: 'http://localhost' }
- expect(elementType.isInError({ element })).toBe(false)
+ expect(elementType.isInError(element, { element })).toBe(false)
// Image with image_source_type of 'url' must have an image_url
delete element.image_file
element.image_source_type = IMAGE_SOURCE_TYPES.URL
- expect(elementType.isInError({ element })).toBe(true)
+ expect(elementType.isInError(element, { element })).toBe(true)
// Otherwise it is valid
element.image_url = { formula: "'http://localhost'" }
- expect(elementType.isInError({ element })).toBe(false)
+ expect(elementType.isInError(element, { element })).toBe(false)
})
})
@@ -853,7 +1032,9 @@ describe('elementTypes tests', () => {
const elementType = testApp.getRegistry().get('element', 'button')
// Button with value and invalid workflowAction is invalid
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
})
test('Returns true if Button Element has errors, false otherwise', () => {
const page = {
@@ -867,15 +1048,21 @@ describe('elementTypes tests', () => {
const elementType = testApp.getRegistry().get('element', 'button')
// Button with missing value is invalid
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
// Button with value but missing workflowActions is invalid
element.value = { formula: "'click me'" }
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
// Button with value and workflowAction is valid
page.workflowActions = [{ element_id: 50, type: 'open_page' }]
- expect(elementType.isInError({ page, element, builder })).toBe(false)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ false
+ )
})
})
@@ -889,23 +1076,23 @@ describe('elementTypes tests', () => {
url: { formula: '' },
embed: { formula: '' },
}
- expect(elementType.isInError({ element })).toBe(true)
+ expect(elementType.isInError(element, { element })).toBe(true)
// Otherwise it is valid
element.url = { formula: "'http://localhost'" }
- expect(elementType.isInError({ element })).toBe(false)
+ expect(elementType.isInError(element, { element })).toBe(false)
// IFrame with source_type of 'embed' and missing embed is invalid
element.source_type = IFRAME_SOURCE_TYPES.EMBED
- expect(elementType.isInError({ element })).toBe(true)
+ expect(elementType.isInError(element, { element })).toBe(true)
// Otherwise it is valid
element.embed = { formula: "'http://localhost'" }
- expect(elementType.isInError({ element })).toBe(false)
+ expect(elementType.isInError(element, { element })).toBe(false)
// Default is to return no errors
element.source_type = 'foo'
- expect(elementType.isInError({ element })).toBe(false)
+ expect(elementType.isInError(element, { element })).toBe(false)
})
})
@@ -939,9 +1126,11 @@ describe('elementTypes tests', () => {
const elementType = testApp.getRegistry().get('element', 'form_container')
// Form container with value and workflowAction is valid
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
})
- test('Returns true if Form Container Element has errors, false otherwise', () => {
+ test.only('Returns true if Form Container Element has errors, false otherwise', () => {
const page = {
id: 1,
shared: false,
@@ -963,11 +1152,15 @@ describe('elementTypes tests', () => {
const elementType = testApp.getRegistry().get('element', 'form_container')
// Invalid if we have no workflow actions
- expect(elementType.isInError({ page, element })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
// Invalid if we have no children
page.workflowActions = [{ element_id: 50, type: 'open_page' }]
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
// Valid as we have all required fields
const childElement = {
@@ -978,7 +1171,9 @@ describe('elementTypes tests', () => {
}
page.elementMap = { 50: element, 51: childElement }
page.orderedElements = [element, childElement]
- expect(elementType.isInError({ page, element, builder })).toBe(false)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ false
+ )
})
})
@@ -1003,7 +1198,9 @@ describe('elementTypes tests', () => {
}
// Menu element with zero Menu items is invalid.
- expect(elementType.isInError({ page: {}, element, builder })).toBe(true)
+ expect(
+ elementType.isInError(element, { page: {}, element, builder })
+ ).toBe(true)
const menuItem = {
type: 'button',
@@ -1012,40 +1209,52 @@ describe('elementTypes tests', () => {
element.menu_items = [menuItem]
// Button Menu item without workflow actions is invalid.
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
page.workflowActions = [{ element_id: 50, type: 'open_page' }]
element.menu_items[0].name = ''
// Button Menu item with empty name is invalid.
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
element.menu_items[0].type = 'link'
element.menu_items[0].name = ''
// Link Menu item with empty name is invalid.
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
element.menu_items[0].name = 'sub link'
element.menu_items[0].navigation_type = 'page'
element.menu_items[0].navigate_to_page_id = ''
// Link Menu item - sublink with Page navigation but no page ID is invalid.
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
element.menu_items[0].name = 'sub link'
element.menu_items[0].navigation_type = 'custom'
element.menu_items[0].navigate_to_url = { formula: '' }
// Link Menu item - sublink with custom navigation but no URL is invalid.
- expect(elementType.isInError({ page, element, builder })).toBe(true)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ true
+ )
// Valid Button Menu item
element.menu_items[0].type = 'button'
element.menu_items[0].name = 'foo button'
page.workflowActions = [{ element_id: 50, type: 'open_page' }]
- expect(elementType.isInError({ page, element, builder })).toBe(false)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ false
+ )
// Valid Link Menu item - page
element.menu_items[0].type = 'link'
@@ -1053,7 +1262,9 @@ describe('elementTypes tests', () => {
element.menu_items[0].navigation_type = 'page'
element.menu_items[0].navigate_to_page_id = 10
- expect(elementType.isInError({ page, element, builder })).toBe(false)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ false
+ )
// Valid Link Menu item - custom
element.menu_items[0].type = 'link'
@@ -1063,7 +1274,9 @@ describe('elementTypes tests', () => {
formula: "'https://www.baserow.io'",
}
- expect(elementType.isInError({ page, element, builder })).toBe(false)
+ expect(elementType.isInError(element, { page, element, builder })).toBe(
+ false
+ )
})
})
diff --git a/web-frontend/test/unit/integrations/localBaserow/serviceTypes.spec.js b/web-frontend/test/unit/integrations/localBaserow/serviceTypes.spec.js
new file mode 100644
index 0000000000..c50f31017e
--- /dev/null
+++ b/web-frontend/test/unit/integrations/localBaserow/serviceTypes.spec.js
@@ -0,0 +1,152 @@
+import {
+ LocalBaserowListRowsServiceType,
+ LocalBaserowGetRowServiceType,
+} from '@baserow/modules/integrations/localBaserow/serviceTypes'
+import { TestApp } from '@baserow/test/helpers/testApp'
+
+describe('Local baserow service types', () => {
+ let testApp = null
+
+ beforeAll(() => {
+ testApp = new TestApp()
+ })
+
+ afterEach(() => {
+ testApp.afterEach()
+ })
+
+ test('Get service should prepareValuePath', () => {
+ const fakeApp = {}
+ const serviceType = new LocalBaserowGetRowServiceType(fakeApp)
+
+ const service = {
+ schema: {
+ properties: { id: { title: 'Id' }, field_42: { title: 'Field 42' } },
+ },
+ }
+
+ expect(serviceType.prepareValuePath(service, [])).toEqual([])
+ expect(serviceType.prepareValuePath(service, [0])).toEqual([0])
+ expect(serviceType.prepareValuePath(service, ['id'])).toEqual(['id'])
+ expect(serviceType.prepareValuePath(service, ['field_42'])).toEqual([
+ 'Field 42',
+ ])
+ expect(
+ serviceType.prepareValuePath(service, ['field_42', 'value'])
+ ).toEqual(['Field 42', 'value'])
+ })
+
+ test('List service should prepareValuePath', () => {
+ const fakeApp = {}
+ const serviceType = new LocalBaserowListRowsServiceType(fakeApp)
+
+ const service = {
+ schema: {
+ items: {
+ properties: { id: { title: 'Id' }, field_42: { title: 'Field 42' } },
+ },
+ },
+ }
+
+ expect(serviceType.prepareValuePath(service, [])).toEqual([])
+ expect(serviceType.prepareValuePath(service, [0])).toEqual([0])
+ expect(serviceType.prepareValuePath(service, ['id'])).toEqual(['id'])
+ expect(serviceType.prepareValuePath(service, ['field_42'])).toEqual([
+ 'Field 42',
+ ])
+ expect(
+ serviceType.prepareValuePath(service, ['field_42', 'value'])
+ ).toEqual(['Field 42', 'value'])
+ })
+
+ test('List service should resolve correctly in builder data provider', () => {
+ const dataProvider = testApp
+ .getRegistry()
+ .get('builderDataProvider', 'data_source')
+
+ const service = {
+ id: 1,
+ type: 'local_baserow_list_rows',
+ schema: {
+ items: {
+ properties: { id: { title: 'Id' }, field_42: { title: 'Field 42' } },
+ },
+ },
+ }
+
+ dataProvider.getDataSourceContent = jest.fn(() => [
+ { id: 1, 'Field 42': 'Field 42 content row 1' },
+ { id: 2, 'Field 42': 'Field 42 content row 2' },
+ ])
+
+ const page = { id: 2, dataSources: [service] }
+
+ const applicationContext = {
+ builder: {
+ pages: [{ id: 1, shared: true, dataSources: [] }, page],
+ },
+ page,
+ }
+
+ expect(dataProvider.getDataChunk(applicationContext, ['1'])).toEqual([
+ { id: 1, 'Field 42': 'Field 42 content row 1' },
+ { id: 2, 'Field 42': 'Field 42 content row 2' },
+ ])
+ expect(dataProvider.getDataChunk(applicationContext, ['1', '0'])).toEqual({
+ id: 1,
+ 'Field 42': 'Field 42 content row 1',
+ })
+ expect(dataProvider.getDataChunk(applicationContext, ['1', '1'])).toEqual({
+ id: 2,
+ 'Field 42': 'Field 42 content row 2',
+ })
+ expect(
+ dataProvider.getDataChunk(applicationContext, ['1', '1', 'id'])
+ ).toEqual(2)
+ expect(
+ dataProvider.getDataChunk(applicationContext, ['1', '1', 'field_42'])
+ ).toEqual('Field 42 content row 2')
+ expect(
+ dataProvider.getDataChunk(applicationContext, ['1', '*', 'field_42'])
+ ).toEqual(['Field 42 content row 1', 'Field 42 content row 2'])
+ })
+
+ test('List service should resolve correctly in builder data provider', () => {
+ const dataProvider = testApp
+ .getRegistry()
+ .get('builderDataProvider', 'data_source')
+
+ const service = {
+ id: 1,
+ type: 'local_baserow_get_row',
+ schema: {
+ properties: { id: { title: 'Id' }, field_42: { title: 'Field 42' } },
+ },
+ }
+
+ dataProvider.getDataSourceContent = jest.fn(() => ({
+ id: 1,
+ 'Field 42': 'Field 42 content',
+ }))
+
+ const page = { id: 2, dataSources: [service] }
+
+ const applicationContext = {
+ builder: {
+ pages: [{ id: 1, shared: true, dataSources: [] }, page],
+ },
+ page,
+ }
+
+ expect(dataProvider.getDataChunk(applicationContext, ['1'])).toEqual({
+ id: 1,
+ 'Field 42': 'Field 42 content',
+ })
+ expect(dataProvider.getDataChunk(applicationContext, ['1', 'id'])).toEqual(
+ 1
+ )
+ expect(
+ dataProvider.getDataChunk(applicationContext, ['1', 'field_42'])
+ ).toEqual('Field 42 content')
+ })
+})