diff --git a/lerna.json b/lerna.json index 272a1dd0c6..10d36c9eaf 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.32.7", + "version": "2.32.8", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/backend-core/src/features/index.ts b/packages/backend-core/src/features/index.ts index 0765d09036..6ac9749b27 100644 --- a/packages/backend-core/src/features/index.ts +++ b/packages/backend-core/src/features/index.ts @@ -3,6 +3,7 @@ import * as context from "../context" import { PostHog, PostHogOptions } from "posthog-node" import { FeatureFlag, IdentityType, UserCtx } from "@budibase/types" import tracer from "dd-trace" +import { Duration } from "../utils" let posthog: PostHog | undefined export function init(opts?: PostHogOptions) { @@ -16,6 +17,7 @@ export function init(opts?: PostHogOptions) { posthog = new PostHog(env.POSTHOG_TOKEN, { host: env.POSTHOG_API_HOST, personalApiKey: env.POSTHOG_PERSONAL_TOKEN, + featureFlagsPollingInterval: Duration.fromMinutes(3).toMs(), ...opts, }) } else { diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index c69bf0d6bb..2922d88e7a 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -396,6 +396,11 @@ padding: 6px 10px; margin-bottom: 8px; } + + .compact .placeholder { + height: fit-content; + } + .title { display: flex; flex-direction: row; diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index c4a39ae8a9..e38e4c2ed5 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -22,6 +22,8 @@ import { TableSchema, ViewFieldMetadata, RenameColumn, + FeatureFlag, + BBReferenceFieldSubType, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" @@ -32,6 +34,7 @@ import { roles, withEnv as withCoreEnv, setEnv as setCoreEnv, + env, } from "@budibase/backend-core" import sdk from "../../../sdk" @@ -694,22 +697,23 @@ describe.each([ ) }) - it("cannot update views v1", async () => { - const viewV1 = await config.api.legacyView.save({ - tableId: table._id!, - name: generator.guid(), - filters: [], - schema: {}, - }) + isInternal && + it("cannot update views v1", async () => { + const viewV1 = await config.api.legacyView.save({ + tableId: table._id!, + name: generator.guid(), + filters: [], + schema: {}, + }) - await config.api.viewV2.update(viewV1 as unknown as ViewV2, { - status: 400, - body: { - message: "Only views V2 can be updated", + await config.api.viewV2.update(viewV1 as unknown as ViewV2, { 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 () => { 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((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", () => { diff --git a/packages/server/src/db/linkedRows/index.ts b/packages/server/src/db/linkedRows/index.ts index 2c8d1f77ac..c2b043785f 100644 --- a/packages/server/src/db/linkedRows/index.ts +++ b/packages/server/src/db/linkedRows/index.ts @@ -10,7 +10,7 @@ import flatten from "lodash/flatten" import { USER_METDATA_PREFIX } from "../utils" import partition from "lodash/partition" import { getGlobalUsersFromMetadata } from "../../utilities/global" -import { processFormulas } from "../../utilities/rowProcessor" +import { outputProcessing, processFormulas } from "../../utilities/rowProcessor" import { context, features } from "@budibase/backend-core" import { ContextUser, @@ -275,7 +275,7 @@ export async function squashLinks( // will populate this as we find them const linkedTables = [table] const isArray = Array.isArray(enriched) - const enrichedArray = !isArray ? [enriched] : enriched + const enrichedArray = !isArray ? [enriched as Row] : (enriched as Row[]) for (const row of enrichedArray) { // this only fetches the table if its not already in array const rowTable = await getLinkedTable(row.tableId!, linkedTables) @@ -292,6 +292,9 @@ export async function squashLinks( obj.primaryDisplay = getPrimaryDisplayValue(link, linkedTable) if (viewSchema[column]?.columns) { + const enrichedLink = await outputProcessing(linkedTable, link, { + squash: false, + }) const squashFields = Object.entries(viewSchema[column].columns) .filter(([columnName, viewColumnConfig]) => { const tableColumn = linkedTable.schema[columnName] @@ -312,7 +315,7 @@ export async function squashLinks( .map(([columnName]) => columnName) for (const relField of squashFields) { - obj[relField] = link[relField] + obj[relField] = enrichedLink[relField] } } @@ -321,5 +324,5 @@ export async function squashLinks( row[column] = newLinks } } - return isArray ? enrichedArray : enrichedArray[0] + return (isArray ? enrichedArray : enrichedArray[0]) as T }