Merge branch 'master' into grid-layout-improved

This commit is contained in:
Andrew Kingston 2024-08-09 10:02:02 +01:00 committed by GitHub
commit 2b32db4286
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 111 additions and 36 deletions

View File

@ -1,6 +1,6 @@
import env from "../environment" import env from "../environment"
export const PASSWORD_MIN_LENGTH = +(env.PASSWORD_MIN_LENGTH || 8) export const PASSWORD_MIN_LENGTH = +(env.PASSWORD_MIN_LENGTH || 12)
export const PASSWORD_MAX_LENGTH = +(env.PASSWORD_MAX_LENGTH || 512) export const PASSWORD_MAX_LENGTH = +(env.PASSWORD_MAX_LENGTH || 512)
export function validatePassword( export function validatePassword(

View File

@ -4,7 +4,7 @@ import { PASSWORD_MAX_LENGTH, validatePassword } from "../auth"
describe("auth", () => { describe("auth", () => {
describe("validatePassword", () => { describe("validatePassword", () => {
it("a valid password returns successful", () => { it("a valid password returns successful", () => {
expect(validatePassword("password")).toEqual({ valid: true }) expect(validatePassword("password123!")).toEqual({ valid: true })
}) })
it.each([ it.each([
@ -14,7 +14,7 @@ describe("auth", () => {
])("%s returns unsuccessful", (_, password) => { ])("%s returns unsuccessful", (_, password) => {
expect(validatePassword(password as string)).toEqual({ expect(validatePassword(password as string)).toEqual({
valid: false, valid: false,
error: "Password invalid. Minimum 8 characters.", error: "Password invalid. Minimum 12 characters.",
}) })
}) })

View File

@ -21,7 +21,7 @@ export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
_id: userId, _id: userId,
userId, userId,
email: newEmail(), email: newEmail(),
password: "password", password: "password123!",
roles: { app_test: "admin" }, roles: { app_test: "admin" },
firstName: generator.first(), firstName: generator.first(),
lastName: generator.last(), lastName: generator.last(),

View File

@ -102,7 +102,9 @@
{onOptionMouseenter} {onOptionMouseenter}
{onOptionMouseleave} {onOptionMouseleave}
isPlaceholder={value == null || value === ""} isPlaceholder={value == null || value === ""}
placeholderOption={placeholder === false ? null : placeholder} placeholderOption={placeholder === false
? null
: placeholder || "Choose an option"}
isOptionSelected={option => compareOptionAndValue(option, value)} isOptionSelected={option => compareOptionAndValue(option, value)}
onSelectOption={selectOption} onSelectOption={selectOption}
{loading} {loading}

View File

@ -157,7 +157,7 @@
</span> </span>
{:else if schema.type === "link"} {:else if schema.type === "link"}
<LinkedRowSelector <LinkedRowSelector
linkedRows={fieldData} linkedData={fieldData}
{schema} {schema}
on:change={e => on:change={e =>
onChange({ onChange({
@ -169,7 +169,7 @@
/> />
{:else if schema.type === "bb_reference" || schema.type === "bb_reference_single"} {:else if schema.type === "bb_reference" || schema.type === "bb_reference_single"}
<LinkedRowSelector <LinkedRowSelector
linkedRows={fieldData} linkedData={fieldData}
{schema} {schema}
linkedTableId={"ta_users"} linkedTableId={"ta_users"}
on:change={e => on:change={e =>

View File

@ -146,7 +146,7 @@
{:else if type === "link"} {:else if type === "link"}
<LinkedRowSelector <LinkedRowSelector
{error} {error}
linkedRows={value} linkedData={value}
schema={meta} schema={meta}
on:change={e => (value = e.detail)} on:change={e => (value = e.detail)}
/> />

View File

@ -6,7 +6,7 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let schema export let schema
export let linkedRows = [] export let linkedData
export let useLabel = true export let useLabel = true
export let linkedTableId export let linkedTableId
export let label export let label
@ -15,14 +15,26 @@
let rows = [] let rows = []
let linkedIds = [] let linkedIds = []
$: linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map( $: fieldValue = getFieldValue(linkedData, schema)
row => row?._id || row
)
$: label = label || capitalise(schema.name) $: label = label || capitalise(schema.name)
$: linkedTableId = linkedTableId || schema.tableId $: linkedTableId = linkedTableId || schema.tableId
$: linkedTable = $tables.list.find(table => table._id === linkedTableId) $: linkedTable = $tables.list.find(table => table._id === linkedTableId)
$: fetchRows(linkedTableId) $: fetchRows(linkedTableId)
const getFieldValue = val => {
const linkedIds = (Array.isArray(val) ? val : [])?.map(
row => row?._id || row
)
if (
schema.relationshipType === "one-to-many" ||
schema.type === "bb_reference_single"
) {
return linkedIds[0]
} else {
return linkedIds
}
}
async function fetchRows(linkedTableId) { async function fetchRows(linkedTableId) {
try { try {
rows = await API.fetchTableData(linkedTableId) rows = await API.fetchTableData(linkedTableId)
@ -45,7 +57,7 @@
</Label> </Label>
{:else if schema.relationshipType === "one-to-many" || schema.type === "bb_reference_single"} {:else if schema.relationshipType === "one-to-many" || schema.type === "bb_reference_single"}
<Select <Select
value={linkedIds?.[0]} value={fieldValue}
options={rows} options={rows}
getOptionLabel={getPrettyName} getOptionLabel={getPrettyName}
getOptionValue={row => row._id} getOptionValue={row => row._id}
@ -58,7 +70,7 @@
/> />
{:else} {:else}
<Multiselect <Multiselect
value={linkedIds} value={fieldValue}
label={useLabel ? label : null} label={useLabel ? label : null}
options={rows} options={rows}
getOptionLabel={getPrettyName} getOptionLabel={getPrettyName}

View File

@ -8,7 +8,7 @@
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher, setContext } from "svelte" import { createEventDispatcher, setContext } from "svelte"
import { isJSBinding } from "@budibase/string-templates" import { isJSBinding, findHBSBlocks } from "@budibase/string-templates"
export let panel = ClientBindingPanel export let panel = ClientBindingPanel
export let value = "" export let value = ""
@ -105,9 +105,11 @@
datetime: isValidDate, datetime: isValidDate,
link: hasValidLinks, link: hasValidLinks,
bb_reference: hasValidLinks, bb_reference: hasValidLinks,
bb_reference_single: hasValidLinks,
array: hasValidOptions, array: hasValidOptions,
longform: value => !isJSBinding(value), longform: value => !isJSBinding(value),
json: value => !isJSBinding(value), json: value => !isJSBinding(value),
options: value => !isJSBinding(value) && !findHBSBlocks(value)?.length,
boolean: isValidBoolean, boolean: isValidBoolean,
attachment: false, attachment: false,
attachment_single: false, attachment_single: false,

View File

@ -1,14 +1,17 @@
<script> <script>
import { FancyForm, FancyInput } from "@budibase/bbui" import { FancyForm, FancyInput } from "@budibase/bbui"
import { createValidationStore, requiredValidator } from "helpers/validation" import { createValidationStore, requiredValidator } from "helpers/validation"
import { admin } from "stores/portal"
export let password export let password
export let passwordForm export let passwordForm
export let error export let error
$: passwordMinLength = $admin.passwordMinLength ?? 12
const validatePassword = value => { const validatePassword = value => {
if (!value || value.length < 12) { if (!value || value.length < passwordMinLength) {
return "Please enter at least 12 characters. We recommend using machine generated or random passwords." return `Please enter at least ${passwordMinLength} characters. We recommend using machine generated or random passwords.`
} }
return null return null
} }
@ -31,7 +34,8 @@
!$firstPassword || !$firstPassword ||
!$firstTouched || !$firstTouched ||
!$repeatTouched || !$repeatTouched ||
$firstPassword !== $repeatPassword $firstPassword !== $repeatPassword ||
firstPasswordError
</script> </script>
<FancyForm bind:this={passwordForm}> <FancyForm bind:this={passwordForm}>

View File

@ -80,6 +80,7 @@ const componentMap = {
"field/barcodeqr": FormFieldSelect, "field/barcodeqr": FormFieldSelect,
"field/signature_single": FormFieldSelect, "field/signature_single": FormFieldSelect,
"field/bb_reference": FormFieldSelect, "field/bb_reference": FormFieldSelect,
"field/bb_reference_single": FormFieldSelect,
// Some validation types are the same as others, so not all types are // Some validation types are the same as others, so not all types are
// explicitly listed here. e.g. options uses string validation // explicitly listed here. e.g. options uses string validation
"validation/string": ValidationEditor, "validation/string": ValidationEditor,

View File

@ -14,8 +14,15 @@
notifications.error("Failed to update password") notifications.error("Failed to update password")
} }
} }
const handleKeydown = evt => {
if (evt.key === "Enter" && !error && password) {
updatePassword()
}
}
</script> </script>
<svelte:window on:keydown={handleKeydown} />
<ModalContent <ModalContent
title="Update password" title="Update password"
confirmText="Update password" confirmText="Update password"

View File

@ -18,7 +18,7 @@
let password = null let password = null
const validation = createValidationStore() const validation = createValidationStore()
validation.addValidatorType("password", "password", true, { minLength: 8 }) validation.addValidatorType("password", "password", true, { minLength: 12 })
$: validation.observe("password", password) $: validation.observe("password", password)
const Step = { CONFIG: "config", SET_PASSWORD: "set_password" } const Step = { CONFIG: "config", SET_PASSWORD: "set_password" }

View File

@ -21,6 +21,7 @@
let submitted = false let submitted = false
$: tenantId = $auth.tenantId $: tenantId = $auth.tenantId
$: passwordMinLength = $admin.passwordMinLength ?? 12
async function save() { async function save() {
form.validate() form.validate()
@ -35,14 +36,25 @@
await API.createAdminUser(adminUser) await API.createAdminUser(adminUser)
notifications.success("Admin user created") notifications.success("Admin user created")
await admin.init() await admin.init()
await auth.login({
username: formData?.email.trim(),
password: formData?.password,
})
$goto("../portal") $goto("../portal")
} catch (error) { } catch (error) {
submitted = false submitted = false
notifications.error(error.message || "Failed to create admin user") notifications.error(error.message || "Failed to create admin user")
} }
} }
const handleKeydown = evt => {
if (evt.key === "Enter") {
save()
}
}
</script> </script>
<svelte:window on:keydown={handleKeydown} />
<TestimonialPage> <TestimonialPage>
<Layout gap="M" noPadding> <Layout gap="M" noPadding>
<Layout justifyItems="center" noPadding> <Layout justifyItems="center" noPadding>
@ -83,9 +95,15 @@
validate={() => { validate={() => {
let fieldError = {} let fieldError = {}
fieldError["password"] = !formData.password if (!formData.password) {
? "Please enter a password" fieldError["password"] = "Please enter a password"
: undefined } else if (formData.password.length < passwordMinLength) {
fieldError[
"password"
] = `Password must be at least ${passwordMinLength} characters`
} else {
fieldError["password"] = undefined
}
fieldError["confirmationPassword"] = fieldError["confirmationPassword"] =
!passwordsMatch( !passwordsMatch(

View File

@ -9,7 +9,7 @@
FancyInput, FancyInput,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { users, organisation, auth } from "stores/portal" import { users, organisation, auth, admin } from "stores/portal"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { TestimonialPage } from "@budibase/frontend-core/src/components" import { TestimonialPage } from "@budibase/frontend-core/src/components"
import { onMount } from "svelte" import { onMount } from "svelte"
@ -23,6 +23,7 @@
let loaded = false let loaded = false
$: company = $organisation.company || "Budibase" $: company = $organisation.company || "Budibase"
$: passwordMinLength = $admin.passwordMinLength ?? 12
async function acceptInvite() { async function acceptInvite() {
form.validate() form.validate()
@ -85,8 +86,15 @@
notifications.error("Error getting invite config") notifications.error("Error getting invite config")
} }
}) })
const handleKeydown = evt => {
if (evt.key === "Enter") {
acceptInvite()
}
}
</script> </script>
<svelte:window on:keydown={handleKeydown} />
{#if loaded} {#if loaded}
<TestimonialPage> <TestimonialPage>
<Layout gap="M" noPadding> <Layout gap="M" noPadding>
@ -154,8 +162,8 @@
function validatePassword() { function validatePassword() {
if (!formData.password) { if (!formData.password) {
return "Please enter a password" return "Please enter a password"
} else if (formData.password.length < 8) { } else if (formData.password.length < passwordMinLength) {
return "Please enter at least 8 characters" return `Please enter at least ${passwordMinLength} characters`
} }
return undefined return undefined
} }

View File

@ -50,6 +50,7 @@ export function createAdminStore() {
store.baseUrl = environment.baseUrl store.baseUrl = environment.baseUrl
store.offlineMode = environment.offlineMode store.offlineMode = environment.offlineMode
store.maintenance = environment.maintenance store.maintenance = environment.maintenance
store.passwordMinLength = environment.passwordMinLength
return store return store
}) })
} }

View File

@ -129,6 +129,11 @@
width: 100%; width: 100%;
min-width: unset; min-width: unset;
} }
.signature-cell img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.signature-cell.light img { .signature-cell.light img {
-webkit-filter: invert(100%); -webkit-filter: invert(100%);
filter: invert(100%); filter: invert(100%);

View File

@ -566,7 +566,13 @@ class GoogleSheetsIntegration implements DatasourcePlus {
query.filters.equal[`_${GOOGLE_SHEETS_PRIMARY_KEY}`] = id query.filters.equal[`_${GOOGLE_SHEETS_PRIMARY_KEY}`] = id
} }
} }
let filtered = dataFilters.runQuery(rows, query.filters || {}) let filtered = dataFilters.runQuery(
rows,
query.filters || {},
(row: GoogleSpreadsheetRow, headerKey: string) => {
return row.get(headerKey)
}
)
if (hasFilters && query.paginate) { if (hasFilters && query.paginate) {
filtered = filtered.slice(offset, offset + limit) filtered = filtered.slice(offset, offset + limit)
} }

View File

@ -436,8 +436,15 @@ export const search = (
* Performs a client-side search on an array of data * Performs a client-side search on an array of data
* @param docs the data * @param docs the data
* @param query the JSON query * @param query the JSON query
* @param findInDoc optional fn when trying to extract a value
* from custom doc type e.g. Google Sheets
*
*/ */
export const runQuery = (docs: Record<string, any>[], query: SearchFilters) => { export const runQuery = (
docs: Record<string, any>[],
query: SearchFilters,
findInDoc: Function = deepGet
) => {
if (!docs || !Array.isArray(docs)) { if (!docs || !Array.isArray(docs)) {
return [] return []
} }
@ -464,7 +471,7 @@ export const runQuery = (docs: Record<string, any>[], query: SearchFilters) => {
for (const [key, testValue] of Object.entries(query[type] || {})) { for (const [key, testValue] of Object.entries(query[type] || {})) {
const valueToCheck = isLogicalSearchOperator(type) const valueToCheck = isLogicalSearchOperator(type)
? doc ? doc
: deepGet(doc, removeKeyNumbering(key)) : findInDoc(doc, removeKeyNumbering(key))
const result = test(valueToCheck, testValue) const result = test(valueToCheck, testValue)
if (query.allOr && result) { if (query.allOr && result) {
return true return true

View File

@ -42,6 +42,7 @@ export const fetch = async (ctx: Ctx) => {
baseUrl: env.PLATFORM_URL, baseUrl: env.PLATFORM_URL,
isDev: env.isDev() && !env.isTest(), isDev: env.isDev() && !env.isTest(),
maintenance: [], maintenance: [],
passwordMinLength: env.PASSWORD_MIN_LENGTH,
} }
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {

View File

@ -66,7 +66,7 @@ describe("/api/global/auth", () => {
it("should return 403 with incorrect credentials", async () => { it("should return 403 with incorrect credentials", async () => {
const tenantId = config.tenantId! const tenantId = config.tenantId!
const email = config.user?.email! const email = config.user?.email!
const password = "incorrect" const password = "incorrect123"
const response = await config.api.auth.login( const response = await config.api.auth.login(
tenantId, tenantId,
@ -83,7 +83,7 @@ describe("/api/global/auth", () => {
it("should return 403 when user doesn't exist", async () => { it("should return 403 when user doesn't exist", async () => {
const tenantId = config.tenantId! const tenantId = config.tenantId!
const email = "invaliduser@example.com" const email = "invaliduser@example.com"
const password = "password" const password = "password123!"
const response = await config.api.auth.login( const response = await config.api.auth.login(
tenantId, tenantId,
@ -203,7 +203,7 @@ describe("/api/global/auth", () => {
) )
delete user.password delete user.password
const newPassword = "newpassword" const newPassword = "newpassword1"
const res = await config.api.auth.updatePassword(code!, newPassword) const res = await config.api.auth.updatePassword(code!, newPassword)
user = (await config.getUser(user.email))! user = (await config.getUser(user.email))!

View File

@ -32,7 +32,7 @@ describe("/api/global/self", () => {
const res = await config.api.self const res = await config.api.self
.updateSelf(user, { .updateSelf(user, {
password: "newPassword", password: "newPassword1",
}) })
.expect(200) .expect(200)

View File

@ -29,7 +29,7 @@ describe("/api/global/tenant", () => {
const tenantInfo: TenantInfo = { const tenantInfo: TenantInfo = {
owner: { owner: {
email: "test@example.com", email: "test@example.com",
password: "PASSWORD", password: "PASSWORD123!",
ssoId: "SSO_ID", ssoId: "SSO_ID",
givenName: "Jane", givenName: "Jane",
familyName: "Doe", familyName: "Doe",

View File

@ -26,6 +26,7 @@ const environment = {
SALT_ROUNDS: process.env.SALT_ROUNDS, SALT_ROUNDS: process.env.SALT_ROUNDS,
REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_PASSWORD: process.env.REDIS_PASSWORD,
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
PASSWORD_MIN_LENGTH: process.env.PASSWORD_MIN_LENGTH,
// urls // urls
MINIO_URL: process.env.MINIO_URL, MINIO_URL: process.env.MINIO_URL,
COUCH_DB_URL: process.env.COUCH_DB_URL, COUCH_DB_URL: process.env.COUCH_DB_URL,

View File

@ -44,7 +44,7 @@ class TestConfiguration {
tenantId: string tenantId: string
user?: User user?: User
apiKey?: string apiKey?: string
userPassword = "password" userPassword = "password123!"
constructor(opts: { openServer: boolean } = { openServer: true }) { constructor(opts: { openServer: boolean } = { openServer: true }) {
// default to cloud hosting // default to cloud hosting

View File

@ -48,7 +48,7 @@ export class UserAPI extends TestAPI {
return this.request return this.request
.post(`/api/global/users/invite/accept`) .post(`/api/global/users/invite/accept`)
.send({ .send({
password: "newpassword", password: "newpassword1",
inviteCode: code, inviteCode: code,
firstName: "Ted", firstName: "Ted",
}) })
@ -101,7 +101,7 @@ export class UserAPI extends TestAPI {
if (!request) { if (!request) {
request = { request = {
email: structures.email(), email: structures.email(),
password: generator.string({ length: 8 }), password: generator.string({ length: 12 }),
tenantId: structures.tenant.id(), tenantId: structures.tenant.id(),
} }
} }