Merge branch 'master' into v3-ui

This commit is contained in:
deanhannigan 2024-09-26 09:13:56 +01:00 committed by GitHub
commit 577dfb0d17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 198 additions and 19 deletions

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2.32.7", "version": "2.32.8",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

@ -1 +1 @@
Subproject commit 905773d70854a43c6ef2461c7a49671bff56fedc Subproject commit 558a32dfd1f55bd894804a503e7e1090937df88c

View File

@ -3,6 +3,7 @@ import * as context from "../context"
import { PostHog, PostHogOptions } from "posthog-node" import { PostHog, PostHogOptions } from "posthog-node"
import { FeatureFlag, IdentityType, UserCtx } from "@budibase/types" import { FeatureFlag, IdentityType, UserCtx } from "@budibase/types"
import tracer from "dd-trace" import tracer from "dd-trace"
import { Duration } from "../utils"
let posthog: PostHog | undefined let posthog: PostHog | undefined
export function init(opts?: PostHogOptions) { export function init(opts?: PostHogOptions) {
@ -16,6 +17,7 @@ export function init(opts?: PostHogOptions) {
posthog = new PostHog(env.POSTHOG_TOKEN, { posthog = new PostHog(env.POSTHOG_TOKEN, {
host: env.POSTHOG_API_HOST, host: env.POSTHOG_API_HOST,
personalApiKey: env.POSTHOG_PERSONAL_TOKEN, personalApiKey: env.POSTHOG_PERSONAL_TOKEN,
featureFlagsPollingInterval: Duration.fromMinutes(3).toMs(),
...opts, ...opts,
}) })
} else { } else {

View File

@ -396,6 +396,11 @@
padding: 6px 10px; padding: 6px 10px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.compact .placeholder {
height: fit-content;
}
.title { .title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -22,6 +22,8 @@ import {
TableSchema, TableSchema,
ViewFieldMetadata, ViewFieldMetadata,
RenameColumn, RenameColumn,
FeatureFlag,
BBReferenceFieldSubType,
} from "@budibase/types" } from "@budibase/types"
import { generator, mocks } from "@budibase/backend-core/tests" import { generator, mocks } from "@budibase/backend-core/tests"
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
@ -32,6 +34,7 @@ import {
roles, roles,
withEnv as withCoreEnv, withEnv as withCoreEnv,
setEnv as setCoreEnv, setEnv as setCoreEnv,
env,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import sdk from "../../../sdk" import sdk from "../../../sdk"
@ -694,22 +697,23 @@ describe.each([
) )
}) })
it("cannot update views v1", async () => { isInternal &&
const viewV1 = await config.api.legacyView.save({ it("cannot update views v1", async () => {
tableId: table._id!, const viewV1 = await config.api.legacyView.save({
name: generator.guid(), tableId: table._id!,
filters: [], name: generator.guid(),
schema: {}, filters: [],
}) schema: {},
})
await config.api.viewV2.update(viewV1 as unknown as ViewV2, { await config.api.viewV2.update(viewV1 as unknown as ViewV2, {
status: 400,
body: {
message: "Only views V2 can be updated",
status: 400, status: 400,
}, body: {
message: "Only views V2 can be updated",
status: 400,
},
})
}) })
})
it("cannot update the a view with unmatching ids between url and body", async () => { it("cannot update the a view with unmatching ids between url and body", async () => {
const anotherView = await config.api.viewV2.create({ const anotherView = await config.api.viewV2.create({
@ -2213,6 +2217,171 @@ describe.each([
}) })
) )
}) })
describe("foreign relationship columns", () => {
let envCleanup: () => void
beforeAll(() => {
const flags = [`*:${FeatureFlag.ENRICHED_RELATIONSHIPS}`]
if (env.TENANT_FEATURE_FLAGS) {
flags.push(...env.TENANT_FEATURE_FLAGS.split(","))
}
envCleanup = setCoreEnv({
TENANT_FEATURE_FLAGS: flags.join(","),
})
})
afterAll(() => {
envCleanup?.()
})
const createMainTable = async (
links: {
name: string
tableId: string
fk: string
}[]
) => {
const table = await config.api.table.save(
saveTableRequest({
schema: { title: { name: "title", type: FieldType.STRING } },
})
)
await config.api.table.save({
...table,
schema: {
...table.schema,
...links.reduce<TableSchema>((acc, c) => {
acc[c.name] = {
name: c.name,
relationshipType: RelationshipType.ONE_TO_MANY,
type: FieldType.LINK,
tableId: c.tableId,
fieldName: c.fk,
constraints: { type: "array" },
}
return acc
}, {}),
},
})
return table
}
const createAuxTable = (schema: TableSchema) =>
config.api.table.save(
saveTableRequest({
primaryDisplay: "name",
schema: {
...schema,
name: { name: "name", type: FieldType.STRING },
},
})
)
it("returns squashed fields respecting the view config", async () => {
const auxTable = await createAuxTable({
age: { name: "age", type: FieldType.NUMBER },
})
const auxRow = await config.api.row.save(auxTable._id!, {
name: generator.name(),
age: generator.age(),
})
const table = await createMainTable([
{ name: "aux", tableId: auxTable._id!, fk: "fk_aux" },
])
await config.api.row.save(table._id!, {
title: generator.word(),
aux: [auxRow],
})
const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
schema: {
title: { visible: true },
aux: {
visible: true,
columns: {
name: { visible: false, readonly: false },
age: { visible: true, readonly: true },
},
},
},
})
const response = await config.api.viewV2.search(view.id)
expect(response.rows).toEqual([
expect.objectContaining({
aux: [
{
_id: auxRow._id,
primaryDisplay: auxRow.name,
age: auxRow.age,
},
],
}),
])
})
it("enriches squashed fields", async () => {
const auxTable = await createAuxTable({
user: {
name: "user",
type: FieldType.BB_REFERENCE_SINGLE,
subtype: BBReferenceFieldSubType.USER,
constraints: { presence: true },
},
})
const table = await createMainTable([
{ name: "aux", tableId: auxTable._id!, fk: "fk_aux" },
])
const user = config.getUser()
const auxRow = await config.api.row.save(auxTable._id!, {
name: generator.name(),
user: user._id,
})
await config.api.row.save(table._id!, {
title: generator.word(),
aux: [auxRow],
})
const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
schema: {
title: { visible: true },
aux: {
visible: true,
columns: {
name: { visible: true, readonly: true },
user: { visible: true, readonly: true },
},
},
},
})
const response = await config.api.viewV2.search(view.id)
expect(response.rows).toEqual([
expect.objectContaining({
aux: [
{
_id: auxRow._id,
primaryDisplay: auxRow.name,
name: auxRow.name,
user: {
_id: user._id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
primaryDisplay: user.email,
},
},
],
}),
])
})
})
}) })
describe("permissions", () => { describe("permissions", () => {

View File

@ -10,7 +10,7 @@ import flatten from "lodash/flatten"
import { USER_METDATA_PREFIX } from "../utils" import { USER_METDATA_PREFIX } from "../utils"
import partition from "lodash/partition" import partition from "lodash/partition"
import { getGlobalUsersFromMetadata } from "../../utilities/global" import { getGlobalUsersFromMetadata } from "../../utilities/global"
import { processFormulas } from "../../utilities/rowProcessor" import { outputProcessing, processFormulas } from "../../utilities/rowProcessor"
import { context, features } from "@budibase/backend-core" import { context, features } from "@budibase/backend-core"
import { import {
ContextUser, ContextUser,
@ -275,7 +275,7 @@ export async function squashLinks<T = Row[] | Row>(
// will populate this as we find them // will populate this as we find them
const linkedTables = [table] const linkedTables = [table]
const isArray = Array.isArray(enriched) const isArray = Array.isArray(enriched)
const enrichedArray = !isArray ? [enriched] : enriched const enrichedArray = !isArray ? [enriched as Row] : (enriched as Row[])
for (const row of enrichedArray) { for (const row of enrichedArray) {
// this only fetches the table if its not already in array // this only fetches the table if its not already in array
const rowTable = await getLinkedTable(row.tableId!, linkedTables) const rowTable = await getLinkedTable(row.tableId!, linkedTables)
@ -292,6 +292,9 @@ export async function squashLinks<T = Row[] | Row>(
obj.primaryDisplay = getPrimaryDisplayValue(link, linkedTable) obj.primaryDisplay = getPrimaryDisplayValue(link, linkedTable)
if (viewSchema[column]?.columns) { if (viewSchema[column]?.columns) {
const enrichedLink = await outputProcessing(linkedTable, link, {
squash: false,
})
const squashFields = Object.entries(viewSchema[column].columns) const squashFields = Object.entries(viewSchema[column].columns)
.filter(([columnName, viewColumnConfig]) => { .filter(([columnName, viewColumnConfig]) => {
const tableColumn = linkedTable.schema[columnName] const tableColumn = linkedTable.schema[columnName]
@ -312,7 +315,7 @@ export async function squashLinks<T = Row[] | Row>(
.map(([columnName]) => columnName) .map(([columnName]) => columnName)
for (const relField of squashFields) { for (const relField of squashFields) {
obj[relField] = link[relField] obj[relField] = enrichedLink[relField]
} }
} }
@ -321,5 +324,5 @@ export async function squashLinks<T = Row[] | Row>(
row[column] = newLinks row[column] = newLinks
} }
} }
return isArray ? enrichedArray : enrichedArray[0] return (isArray ? enrichedArray : enrichedArray[0]) as T
} }