From 1ada790d507b76e4d2cb133a98465dc07b0b1b36 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Feb 2023 15:21:00 +0000 Subject: [PATCH 01/32] Fix postgres update for relationships --- packages/server/src/integrations/base/sql.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index e66795a6db..c722891910 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -502,9 +502,7 @@ class InternalBuilder { if (opts.disableReturning) { return query.update(parsedBody) } else { - return query - .update(parsedBody) - .returning(generateSelectStatement(json, knex)) + return query.update(parsedBody).returning("*") } } From e0b3976ee4338b543765bf6952726bae509606df Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Feb 2023 15:57:04 +0000 Subject: [PATCH 02/32] Add return select statement back on update --- packages/server/src/integrations/base/sql.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index c722891910..e66795a6db 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -502,7 +502,9 @@ class InternalBuilder { if (opts.disableReturning) { return query.update(parsedBody) } else { - return query.update(parsedBody).returning("*") + return query + .update(parsedBody) + .returning(generateSelectStatement(json, knex)) } } From be81767a465af3edd7c90514f42c0b8f28d2b0d0 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Feb 2023 15:57:56 +0000 Subject: [PATCH 03/32] Replace maps for foreach --- packages/server/src/api/controllers/row/ExternalRequest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 2faff95595..6e79971aa2 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -293,7 +293,7 @@ export class ExternalRequest { // we're not inserting a doc, will be a bunch of update calls const otherKey: string = field.throughFrom || linkTablePrimary const thisKey: string = field.throughTo || tablePrimary - row[key].map((relationship: any) => { + row[key].forEach((relationship: any) => { manyRelationships.push({ tableId: field.through || field.tableId, isUpdate: false, @@ -309,7 +309,7 @@ export class ExternalRequest { const thisKey: string = "id" // @ts-ignore const otherKey: string = field.fieldName - row[key].map((relationship: any) => { + row[key].forEach((relationship: any) => { manyRelationships.push({ tableId: field.tableId, isUpdate: true, From e0242d08832bfa25167c050780f5ed16b76405b2 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Feb 2023 16:29:18 +0000 Subject: [PATCH 04/32] Fix the many to one updates --- packages/server/src/api/controllers/row/ExternalRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 6e79971aa2..f62e7fe2d0 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -316,7 +316,7 @@ export class ExternalRequest { key: otherKey, [thisKey]: breakRowIdField(relationship)[0], // leave the ID for enrichment later - [otherKey]: `{{ literal ${tablePrimary} }}`, + [otherKey]: `{{ literal [${table.name}.${tablePrimary}] }}`, }) }) } From b9c54b6fe6a2da69a39004851850d967c2f5b299 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 22 Feb 2023 10:54:55 +0000 Subject: [PATCH 05/32] Update many to many --- .../src/api/controllers/row/ExternalRequest.ts | 13 ++++++------- packages/server/src/api/controllers/row/external.ts | 2 +- packages/server/src/integrations/base/sql.ts | 4 +--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index f62e7fe2d0..22a4a90865 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -629,10 +629,7 @@ export class ExternalRequest { * Creating the specific list of fields that we desire, and excluding the ones that are no use to us * is more performant and has the added benefit of protecting against this scenario. */ - buildFields( - table: Table, - includeRelations: IncludeRelationship = IncludeRelationship.INCLUDE - ) { + buildFields(table: Table, includeRelations: boolean) { function extractRealFields(table: Table, existing: string[] = []) { return Object.entries(table.schema) .filter( @@ -691,6 +688,10 @@ export class ExternalRequest { } filters = buildFilters(id, filters || {}, table) const relationships = this.buildRelationships(table) + + const includeSqlRelationships = + config.includeSqlRelationships === IncludeRelationship.INCLUDE + // clean up row on ingress using schema const processed = this.inputProcessing(row, table) row = processed.row @@ -708,9 +709,7 @@ export class ExternalRequest { }, resource: { // have to specify the fields to avoid column overlap (for SQL) - fields: isSql - ? this.buildFields(table, config.includeSqlRelationships) - : [], + fields: isSql ? this.buildFields(table, includeSqlRelationships) : [], }, filters, sort, diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts index 6120c3cdcb..1b2301f139 100644 --- a/packages/server/src/api/controllers/row/external.ts +++ b/packages/server/src/api/controllers/row/external.ts @@ -58,7 +58,7 @@ export async function patch(ctx: BBContext) { return handleRequest(Operation.UPDATE, tableId, { id: breakRowIdField(id), row: inputs, - includeSqlRelationships: IncludeRelationship.EXCLUDE, + includeSqlRelationships: IncludeRelationship.INCLUDE, }) } diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index e66795a6db..c722891910 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -502,9 +502,7 @@ class InternalBuilder { if (opts.disableReturning) { return query.update(parsedBody) } else { - return query - .update(parsedBody) - .returning(generateSelectStatement(json, knex)) + return query.update(parsedBody).returning("*") } } From e350f6b166ca69edfd9dfb24e37f7e47e824f812 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 22 Feb 2023 11:02:32 +0000 Subject: [PATCH 06/32] Undo literal changes --- packages/server/src/api/controllers/row/ExternalRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 22a4a90865..56e54e299e 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -316,7 +316,7 @@ export class ExternalRequest { key: otherKey, [thisKey]: breakRowIdField(relationship)[0], // leave the ID for enrichment later - [otherKey]: `{{ literal [${table.name}.${tablePrimary}] }}`, + [otherKey]: `{{ literal ${tablePrimary} }}`, }) }) } From 63af59a5b0038f1517ac6c8381a5fe5ffe1e3230 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 22 Feb 2023 14:59:42 +0100 Subject: [PATCH 07/32] Handle link fields --- .../src/api/controllers/row/ExternalRequest.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 56e54e299e..a262df8d69 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -177,9 +177,14 @@ function getEndpoint(tableId: string | undefined, operation: string) { function basicProcessing(row: Row, table: Table): Row { const thisRow: Row = {} // filter the row down to what is actually the row (not joined) - for (let fieldName of Object.keys(table.schema)) { + for (let field of Object.values(table.schema)) { + const fieldName = field.name + const pathValue = row[`${table.name}.${fieldName}`] - const value = pathValue != null ? pathValue : row[fieldName] + const value = + pathValue != null || field.type === FieldTypes.LINK + ? pathValue + : row[fieldName] // all responses include "select col as table.col" so that overlaps are handled if (value != null) { thisRow[fieldName] = value @@ -572,11 +577,18 @@ export class ExternalRequest { if (!linkTable || !linkPrimary) { return } + + const linkSecondary = + linkTable?.primary && + linkTable?.primary?.length > 1 && + linkTable?.primary[1] + const rows = related[key]?.rows || [] const found = rows.find( (row: { [key: string]: any }) => row[linkPrimary] === relationship.id || - row[linkPrimary] === body?.[linkPrimary] + (row[linkPrimary] === body?.[linkPrimary] && + (!linkSecondary || row[linkSecondary] === body?.[linkSecondary])) ) const operation = isUpdate ? Operation.UPDATE : Operation.CREATE if (!found) { From f00994af7f015d4792e14450178f23a0d55c7f94 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 22 Feb 2023 17:18:05 +0100 Subject: [PATCH 08/32] Fix wrong relationship mapping --- .../server/src/api/controllers/row/ExternalRequest.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index a262df8d69..1510e31881 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -384,6 +384,15 @@ export class ExternalRequest { ) { continue } + + if ( + relationship.from && + row[fromColumn] === undefined && + row[relationship.from] === null + ) { + continue + } + let linked = basicProcessing(row, linkedTable) if (!linked._id) { continue From 83649f1959e35d894b50cc44097c9a2f9520e749 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 22 Feb 2023 22:40:50 +0100 Subject: [PATCH 09/32] Setup o2m and m2m relationships --- .tool-versions | 2 +- .../api/controllers/row/ExternalRequest.ts | 32 ++--- .../src/integration-test/postgres.spec.ts | 134 +++++++++++------- 3 files changed, 100 insertions(+), 68 deletions(-) diff --git a/.tool-versions b/.tool-versions index 8a1af3c071..6ee8cc60be 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ nodejs 14.19.3 -python 3.11.1 \ No newline at end of file +python 3.10.0 \ No newline at end of file diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 1510e31881..79a598edf0 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -142,7 +142,11 @@ function cleanupConfig(config: RunConfig, table: Table): RunConfig { return config } -function generateIdForRow(row: Row | undefined, table: Table): string { +function generateIdForRow( + row: Row | undefined, + table: Table, + isLinked: boolean = false +): string { const primary = table.primary if (!row || !primary) { return "" @@ -151,7 +155,10 @@ function generateIdForRow(row: Row | undefined, table: Table): string { let idParts = [] for (let field of primary) { // need to handle table name + field or just field, depending on if relationships used - const fieldValue = row[`${table.name}.${field}`] || row[field] + let fieldValue = row[`${table.name}.${field}`] + if (!fieldValue && !isLinked) { + fieldValue = row[field] + } if (fieldValue) { idParts.push(fieldValue) } @@ -174,23 +181,20 @@ function getEndpoint(tableId: string | undefined, operation: string) { } } -function basicProcessing(row: Row, table: Table): Row { +function basicProcessing(row: Row, table: Table, isLinked: boolean): Row { const thisRow: Row = {} // filter the row down to what is actually the row (not joined) for (let field of Object.values(table.schema)) { const fieldName = field.name const pathValue = row[`${table.name}.${fieldName}`] - const value = - pathValue != null || field.type === FieldTypes.LINK - ? pathValue - : row[fieldName] + const value = pathValue != null || isLinked ? pathValue : row[fieldName] // all responses include "select col as table.col" so that overlaps are handled if (value != null) { thisRow[fieldName] = value } } - thisRow._id = generateIdForRow(row, table) + thisRow._id = generateIdForRow(row, table, isLinked) thisRow.tableId = table._id thisRow._rev = "rev" return processFormulas(table, thisRow) @@ -385,15 +389,7 @@ export class ExternalRequest { continue } - if ( - relationship.from && - row[fromColumn] === undefined && - row[relationship.from] === null - ) { - continue - } - - let linked = basicProcessing(row, linkedTable) + let linked = basicProcessing(row, linkedTable, true) if (!linked._id) { continue } @@ -441,7 +437,7 @@ export class ExternalRequest { ) continue } - const thisRow = fixArrayTypes(basicProcessing(row, table), table) + const thisRow = fixArrayTypes(basicProcessing(row, table, false), table) if (thisRow._id == null) { throw "Unable to generate row ID for SQL rows" } diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index c688600e8d..cc8760023f 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -23,11 +23,19 @@ jest.setTimeout(30000) jest.unmock("pg") +interface RandomForeignKeyConfig { + createOne2Many?: boolean + createMany2One?: number + createMany2Many?: number +} + describe("row api - postgres", () => { let makeRequest: MakeRequestResponse, postgresDatasource: Datasource, primaryPostgresTable: Table, - auxPostgresTable: Table + o2mInfo: { table: Table; fieldName: string }, + m2oInfo: { table: Table; fieldName: string }, + m2mInfo: { table: Table; fieldName: string } let host: string let port: number @@ -67,31 +75,46 @@ describe("row api - postgres", () => { }, }) - auxPostgresTable = await config.createTable({ - name: generator.word({ length: 10 }), - type: "external", - primary: ["id"], - schema: { - id: { - name: "id", - type: FieldType.AUTO, - constraints: { - presence: true, + async function createAuxTable(prefix: string) { + return await config.createTable({ + name: `${prefix}_${generator.word({ length: 6 })}`, + type: "external", + primary: ["id"], + schema: { + id: { + name: "id", + type: FieldType.AUTO, + constraints: { + presence: true, + }, + }, + title: { + name: "title", + type: FieldType.STRING, + constraints: { + presence: true, + }, }, }, - title: { - name: "title", - type: FieldType.STRING, - constraints: { - presence: true, - }, - }, - }, - sourceId: postgresDatasource._id, - }) + sourceId: postgresDatasource._id, + }) + } + + o2mInfo = { + table: await createAuxTable("o2m"), + fieldName: "oneToManyRelation", + } + m2oInfo = { + table: await createAuxTable("m2o"), + fieldName: "manyToOneRelation", + } + m2mInfo = { + table: await createAuxTable("m2m"), + fieldName: "manyToManyRelation", + } primaryPostgresTable = await config.createTable({ - name: generator.word({ length: 10 }), + name: `p_${generator.word({ length: 6 })}`, type: "external", primary: ["id"], schema: { @@ -117,25 +140,45 @@ describe("row api - postgres", () => { name: "value", type: FieldType.NUMBER, }, - linkedField: { + oneToManyRelation: { type: FieldType.LINK, constraints: { type: "array", presence: false, }, - fieldName: "foreignField", - name: "linkedField", + fieldName: o2mInfo.fieldName, + name: "oneToManyRelation", relationshipType: RelationshipTypes.ONE_TO_MANY, - tableId: auxPostgresTable._id, + tableId: o2mInfo.table._id, + }, + manyToOneRelation: { + type: FieldType.LINK, + constraints: { + type: "array", + presence: false, + }, + fieldName: m2oInfo.fieldName, + name: "manyToOneRelation", + relationshipType: RelationshipTypes.MANY_TO_ONE, + tableId: m2oInfo.table._id, + }, + manyToManyRelation: { + type: FieldType.LINK, + constraints: { + type: "array", + presence: false, + }, + fieldName: m2mInfo.fieldName, + name: "manyToManyRelation", + relationshipType: RelationshipTypes.MANY_TO_MANY, + tableId: m2mInfo.table._id, }, }, sourceId: postgresDatasource._id, }) }) - afterAll(async () => { - await config.end() - }) + afterAll(config.end) function generateRandomPrimaryRowData() { return { @@ -153,19 +196,20 @@ describe("row api - postgres", () => { async function createPrimaryRow(opts: { rowData: PrimaryRowData - createForeignRow?: boolean + createForeignRows?: RandomForeignKeyConfig }) { let { rowData } = opts let foreignRow: Row | undefined - if (opts?.createForeignRow) { + + if (opts?.createForeignRows?.createOne2Many) { foreignRow = await config.createRow({ - tableId: auxPostgresTable._id, + tableId: o2mInfo.table._id, title: generator.name(), }) rowData = { ...rowData, - [`fk_${auxPostgresTable.name}_foreignField`]: foreignRow.id, + [`fk_${o2mInfo.table.name}_${o2mInfo.fieldName}`]: foreignRow.id, } } @@ -197,9 +241,7 @@ describe("row api - postgres", () => { async function populatePrimaryRows( count: number, - opts?: { - createForeignRow?: boolean - } + opts?: RandomForeignKeyConfig ) { return await Promise.all( Array(count) @@ -210,7 +252,7 @@ describe("row api - postgres", () => { rowData, ...(await createPrimaryRow({ rowData, - createForeignRow: opts?.createForeignRow, + createForeignRows: opts, })), } }) @@ -295,7 +337,7 @@ describe("row api - postgres", () => { describe("given than a row exists", () => { let row: Row beforeEach(async () => { - let rowResponse = _.sample(await populatePrimaryRows(10))! + let rowResponse = _.sample(await populatePrimaryRows(1))! row = rowResponse.row }) @@ -422,7 +464,7 @@ describe("row api - postgres", () => { let foreignRow: Row beforeEach(async () => { let [createdRow] = await populatePrimaryRows(1, { - createForeignRow: true, + createOne2Many: true, }) row = createdRow.row foreignRow = createdRow.foreignRow! @@ -437,16 +479,10 @@ describe("row api - postgres", () => { ...row, _id: expect.any(String), _rev: expect.any(String), + [`fk_${o2mInfo.table.name}_${o2mInfo.fieldName}`]: foreignRow.id, }) - expect(res.body.foreignField).toBeUndefined() - - expect( - res.body[`fk_${auxPostgresTable.name}_foreignField`] - ).toBeDefined() - expect(res.body[`fk_${auxPostgresTable.name}_foreignField`]).toBe( - foreignRow.id - ) + expect(res.body[o2mInfo.fieldName]).toBeUndefined() }) }) }) @@ -672,7 +708,7 @@ describe("row api - postgres", () => { beforeEach(async () => { const rowsInfo = await createPrimaryRow({ rowData: generateRandomPrimaryRowData(), - createForeignRow: true, + createForeignRows: { createOne2Many: true }, }) row = rowsInfo.row @@ -687,7 +723,7 @@ describe("row api - postgres", () => { expect(foreignRow).toBeDefined() expect(res.body).toEqual({ ...row, - linkedField: [ + [o2mInfo.fieldName]: [ { ...foreignRow, }, From 70689c03690ba9440df4c0f8bee3c0d7ceaa074d Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 23 Feb 2023 00:06:57 +0100 Subject: [PATCH 10/32] Fix tests --- .../src/integration-test/postgres.spec.ts | 257 ++++++++++++++---- 1 file changed, 204 insertions(+), 53 deletions(-) diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index cc8760023f..1707e596fd 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -23,19 +23,25 @@ jest.setTimeout(30000) jest.unmock("pg") -interface RandomForeignKeyConfig { - createOne2Many?: boolean - createMany2One?: number - createMany2Many?: number +interface ForeignTableInfo { + table: Table + fieldName: string + relationshipType: RelationshipTypes +} + +interface ForeignRowsInfo { + row: Row + foreignKey: string + relationshipType: RelationshipTypes } describe("row api - postgres", () => { let makeRequest: MakeRequestResponse, postgresDatasource: Datasource, primaryPostgresTable: Table, - o2mInfo: { table: Table; fieldName: string }, - m2oInfo: { table: Table; fieldName: string }, - m2mInfo: { table: Table; fieldName: string } + o2mInfo: ForeignTableInfo, + m2oInfo: ForeignTableInfo, + m2mInfo: ForeignTableInfo let host: string let port: number @@ -103,14 +109,17 @@ describe("row api - postgres", () => { o2mInfo = { table: await createAuxTable("o2m"), fieldName: "oneToManyRelation", + relationshipType: RelationshipTypes.ONE_TO_MANY, } m2oInfo = { table: await createAuxTable("m2o"), fieldName: "manyToOneRelation", + relationshipType: RelationshipTypes.MANY_TO_ONE, } m2mInfo = { table: await createAuxTable("m2m"), fieldName: "manyToManyRelation", + relationshipType: RelationshipTypes.MANY_TO_MANY, } primaryPostgresTable = await config.createTable({ @@ -196,21 +205,43 @@ describe("row api - postgres", () => { async function createPrimaryRow(opts: { rowData: PrimaryRowData - createForeignRows?: RandomForeignKeyConfig + createForeignRows?: { + createOne2Many?: boolean + createMany2One?: number + createMany2Many?: number + } }) { let { rowData } = opts - let foreignRow: Row | undefined + let foreignRows: ForeignRowsInfo[] = [] - if (opts?.createForeignRows?.createOne2Many) { - foreignRow = await config.createRow({ - tableId: o2mInfo.table._id, + async function createForeignRow(tableInfo: ForeignTableInfo) { + const foreignRow = await config.createRow({ + tableId: tableInfo.table._id, title: generator.name(), }) + const foreignKey = `fk_${tableInfo.table.name}_${tableInfo.fieldName}` rowData = { ...rowData, - [`fk_${o2mInfo.table.name}_${o2mInfo.fieldName}`]: foreignRow.id, + [foreignKey]: foreignRow.id, } + foreignRows.push({ + row: foreignRow, + foreignKey, + relationshipType: tableInfo.relationshipType, + }) + } + + if (opts?.createForeignRows?.createOne2Many) { + await createForeignRow(o2mInfo) + } + + for (let i = 0; i < (opts?.createForeignRows?.createMany2One || 0); i++) { + await createForeignRow(m2oInfo) + } + + for (let i = 0; i < (opts?.createForeignRows?.createMany2Many || 0); i++) { + await createForeignRow(m2mInfo) } const row = await config.createRow({ @@ -218,7 +249,7 @@ describe("row api - postgres", () => { ...rowData, }) - return { row, foreignRow } + return { row, foreignRows } } async function createDefaultPgTable() { @@ -241,7 +272,11 @@ describe("row api - postgres", () => { async function populatePrimaryRows( count: number, - opts?: RandomForeignKeyConfig + opts?: { + createOne2Many?: boolean + createMany2One?: number + createMany2Many?: number + } ) { return await Promise.all( Array(count) @@ -461,28 +496,134 @@ describe("row api - postgres", () => { describe("given a row with relation data", () => { let row: Row - let foreignRow: Row - beforeEach(async () => { - let [createdRow] = await populatePrimaryRows(1, { - createOne2Many: true, + let rowData: { + name: string + description: string + value: number + } + let foreignRows: ForeignRowsInfo[] + + describe("with all relationship types", () => { + beforeEach(async () => { + let [createdRow] = await populatePrimaryRows(1, { + createOne2Many: true, + createMany2One: 3, + createMany2Many: 2, + }) + row = createdRow.row + rowData = createdRow.rowData + foreignRows = createdRow.foreignRows + }) + + it("only one to many foreign keys are retrieved", async () => { + const res = await getRow(primaryPostgresTable._id, row.id) + + expect(res.status).toBe(200) + + const one2ManyForeignRows = foreignRows.filter( + x => x.relationshipType === RelationshipTypes.ONE_TO_MANY + ) + expect(one2ManyForeignRows).toHaveLength(1) + + expect(res.body).toEqual({ + ...rowData, + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + [one2ManyForeignRows[0].foreignKey]: one2ManyForeignRows[0].row.id, + }) + + expect(res.body[o2mInfo.fieldName]).toBeUndefined() }) - row = createdRow.row - foreignRow = createdRow.foreignRow! }) - it("only foreign keys are retrieved", async () => { - const res = await getRow(primaryPostgresTable._id, row.id) - - expect(res.status).toBe(200) - - expect(res.body).toEqual({ - ...row, - _id: expect.any(String), - _rev: expect.any(String), - [`fk_${o2mInfo.table.name}_${o2mInfo.fieldName}`]: foreignRow.id, + describe("with only one to many", () => { + beforeEach(async () => { + let [createdRow] = await populatePrimaryRows(1, { + createOne2Many: true, + }) + row = createdRow.row + rowData = createdRow.rowData + foreignRows = createdRow.foreignRows }) - expect(res.body[o2mInfo.fieldName]).toBeUndefined() + it("only one to many foreign keys are retrieved", async () => { + const res = await getRow(primaryPostgresTable._id, row.id) + + expect(res.status).toBe(200) + + expect(foreignRows).toHaveLength(1) + + expect(res.body).toEqual({ + ...rowData, + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + [foreignRows[0].foreignKey]: foreignRows[0].row.id, + }) + + expect(res.body[o2mInfo.fieldName]).toBeUndefined() + }) + }) + + describe("with only many to one", () => { + beforeEach(async () => { + let [createdRow] = await populatePrimaryRows(1, { + createMany2One: 3, + }) + row = createdRow.row + rowData = createdRow.rowData + foreignRows = createdRow.foreignRows + }) + + it("only one to many foreign keys are retrieved", async () => { + const res = await getRow(primaryPostgresTable._id, row.id) + + expect(res.status).toBe(200) + + expect(foreignRows).toHaveLength(3) + + expect(res.body).toEqual({ + ...rowData, + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + }) + + expect(res.body[o2mInfo.fieldName]).toBeUndefined() + }) + }) + + describe("with only many to many", () => { + beforeEach(async () => { + let [createdRow] = await populatePrimaryRows(1, { + createMany2Many: 2, + }) + row = createdRow.row + rowData = createdRow.rowData + foreignRows = createdRow.foreignRows + }) + + it("only one to many foreign keys are retrieved", async () => { + const res = await getRow(primaryPostgresTable._id, row.id) + + expect(res.status).toBe(200) + + expect(foreignRows).toHaveLength(2) + + expect(res.body).toEqual({ + ...rowData, + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + }) + + expect(res.body[o2mInfo.fieldName]).toBeUndefined() + }) }) }) }) @@ -703,31 +844,41 @@ describe("row api - postgres", () => { const getAll = (tableId: string | undefined, rowId: string | undefined) => makeRequest("get", `/api/${tableId}/${rowId}/enrich`) describe("given a row with relation data", () => { - let row: Row, foreignRow: Row | undefined + let row: Row, rowData: PrimaryRowData, foreignRows: ForeignRowsInfo[] - beforeEach(async () => { - const rowsInfo = await createPrimaryRow({ - rowData: generateRandomPrimaryRowData(), - createForeignRows: { createOne2Many: true }, + describe("only with one to many data", () => { + beforeEach(async () => { + rowData = generateRandomPrimaryRowData() + const rowsInfo = await createPrimaryRow({ + rowData, + createForeignRows: { createOne2Many: true }, + }) + + row = rowsInfo.row + foreignRows = rowsInfo.foreignRows }) - row = rowsInfo.row - foreignRow = rowsInfo.foreignRow - }) + it("enrich populates the foreign field", async () => { + const res = await getAll(primaryPostgresTable._id, row.id) - it("enrich populates the foreign field", async () => { - const res = await getAll(primaryPostgresTable._id, row.id) + expect(res.status).toBe(200) - expect(res.status).toBe(200) - - expect(foreignRow).toBeDefined() - expect(res.body).toEqual({ - ...row, - [o2mInfo.fieldName]: [ - { - ...foreignRow, - }, - ], + expect(foreignRows).toHaveLength(1) + expect(res.body).toEqual({ + ...rowData, + [foreignRows[0].foreignKey]: foreignRows[0].row.id, + [o2mInfo.fieldName]: [ + { + ...foreignRows[0].row, + _id: expect.any(String), + _rev: expect.any(String), + }, + ], + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + }) }) }) }) @@ -750,7 +901,7 @@ describe("row api - postgres", () => { const rowsCount = 6 let rows: { row: Row - foreignRow: Row | undefined + foreignRows: ForeignRowsInfo[] rowData: PrimaryRowData }[] beforeEach(async () => { From 9e0d003038b594219abaa130d77807145541c47f Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 23 Feb 2023 10:28:24 +0100 Subject: [PATCH 11/32] Fix creation --- packages/server/src/api/controllers/row/ExternalRequest.ts | 1 + packages/server/src/integrations/base/sql.ts | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 79a598edf0..74d48cb93e 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -741,6 +741,7 @@ export class ExternalRequest { table, }, } + // can't really use response right now const response = await getDatasourceAndQuery(json) // handle many to many relationships now if we know the ID (could be auto increment) diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index c722891910..6871d7b0ba 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -415,9 +415,7 @@ class InternalBuilder { if (opts.disableReturning) { return query.insert(parsedBody) } else { - return query - .insert(parsedBody) - .returning(generateSelectStatement(json, knex)) + return query.insert(parsedBody).returning("*") } } From 7868fc657d5bf31c4cc7a9bef5eb2b77bfb2bf34 Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 23 Feb 2023 10:39:16 +0100 Subject: [PATCH 12/32] Fix many-to-one tests --- .../src/integration-test/postgres.spec.ts | 115 +++++++++++++++--- 1 file changed, 95 insertions(+), 20 deletions(-) diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index 1707e596fd..b9f356ded1 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -23,18 +23,6 @@ jest.setTimeout(30000) jest.unmock("pg") -interface ForeignTableInfo { - table: Table - fieldName: string - relationshipType: RelationshipTypes -} - -interface ForeignRowsInfo { - row: Row - foreignKey: string - relationshipType: RelationshipTypes -} - describe("row api - postgres", () => { let makeRequest: MakeRequestResponse, postgresDatasource: Datasource, @@ -86,10 +74,12 @@ describe("row api - postgres", () => { name: `${prefix}_${generator.word({ length: 6 })}`, type: "external", primary: ["id"], + primaryDisplay: "title", schema: { id: { name: "id", type: FieldType.AUTO, + autocolumn: true, constraints: { presence: true, }, @@ -130,6 +120,7 @@ describe("row api - postgres", () => { id: { name: "id", type: FieldType.AUTO, + autocolumn: true, constraints: { presence: true, }, @@ -159,6 +150,7 @@ describe("row api - postgres", () => { name: "oneToManyRelation", relationshipType: RelationshipTypes.ONE_TO_MANY, tableId: o2mInfo.table._id, + main: true, }, manyToOneRelation: { type: FieldType.LINK, @@ -170,6 +162,7 @@ describe("row api - postgres", () => { name: "manyToOneRelation", relationshipType: RelationshipTypes.MANY_TO_ONE, tableId: m2oInfo.table._id, + main: true, }, manyToManyRelation: { type: FieldType.LINK, @@ -181,6 +174,7 @@ describe("row api - postgres", () => { name: "manyToManyRelation", relationshipType: RelationshipTypes.MANY_TO_MANY, tableId: m2mInfo.table._id, + main: true, }, }, sourceId: postgresDatasource._id, @@ -203,6 +197,17 @@ describe("row api - postgres", () => { value: number } + type ForeignTableInfo = { + table: Table + fieldName: string + relationshipType: RelationshipTypes + } + + type ForeignRowsInfo = { + row: Row + relationshipType: RelationshipTypes + } + async function createPrimaryRow(opts: { rowData: PrimaryRowData createForeignRows?: { @@ -211,33 +216,61 @@ describe("row api - postgres", () => { createMany2Many?: number } }) { - let { rowData } = opts + let { rowData } = opts as any let foreignRows: ForeignRowsInfo[] = [] async function createForeignRow(tableInfo: ForeignTableInfo) { + const foreignKey = `fk_${tableInfo.table.name}_${tableInfo.fieldName}` + const foreignRow = await config.createRow({ tableId: tableInfo.table._id, title: generator.name(), }) - const foreignKey = `fk_${tableInfo.table.name}_${tableInfo.fieldName}` rowData = { ...rowData, [foreignKey]: foreignRow.id, } foreignRows.push({ row: foreignRow, - foreignKey, + relationshipType: tableInfo.relationshipType, }) } if (opts?.createForeignRows?.createOne2Many) { - await createForeignRow(o2mInfo) + const foreignKey = `fk_${o2mInfo.table.name}_${o2mInfo.fieldName}` + + const foreignRow = await config.createRow({ + tableId: o2mInfo.table._id, + title: generator.name(), + }) + + rowData = { + ...rowData, + [foreignKey]: foreignRow.id, + } + foreignRows.push({ + row: foreignRow, + relationshipType: o2mInfo.relationshipType, + }) } for (let i = 0; i < (opts?.createForeignRows?.createMany2One || 0); i++) { - await createForeignRow(m2oInfo) + const foreignRow = await config.createRow({ + tableId: m2oInfo.table._id, + title: generator.name(), + }) + + rowData = { + ...rowData, + [m2oInfo.fieldName]: rowData[m2oInfo.fieldName] || [], + } + rowData[m2oInfo.fieldName].push(foreignRow._id) + foreignRows.push({ + row: foreignRow, + relationshipType: RelationshipTypes.MANY_TO_ONE, + }) } for (let i = 0; i < (opts?.createForeignRows?.createMany2Many || 0); i++) { @@ -531,7 +564,8 @@ describe("row api - postgres", () => { tableId: row.tableId, _id: expect.any(String), _rev: expect.any(String), - [one2ManyForeignRows[0].foreignKey]: one2ManyForeignRows[0].row.id, + [`fk_${o2mInfo.table.name}_${o2mInfo.fieldName}`]: + one2ManyForeignRows[0].row.id, }) expect(res.body[o2mInfo.fieldName]).toBeUndefined() @@ -561,7 +595,8 @@ describe("row api - postgres", () => { tableId: row.tableId, _id: expect.any(String), _rev: expect.any(String), - [foreignRows[0].foreignKey]: foreignRows[0].row.id, + [`fk_${o2mInfo.table.name}_${o2mInfo.fieldName}`]: + foreignRows[0].row.id, }) expect(res.body[o2mInfo.fieldName]).toBeUndefined() @@ -866,7 +901,8 @@ describe("row api - postgres", () => { expect(foreignRows).toHaveLength(1) expect(res.body).toEqual({ ...rowData, - [foreignRows[0].foreignKey]: foreignRows[0].row.id, + [`fk_${o2mInfo.table.name}_${o2mInfo.fieldName}`]: + foreignRows[0].row.id, [o2mInfo.fieldName]: [ { ...foreignRows[0].row, @@ -881,6 +917,45 @@ describe("row api - postgres", () => { }) }) }) + + describe("only with many to one data", () => { + beforeEach(async () => { + rowData = generateRandomPrimaryRowData() + const rowsInfo = await createPrimaryRow({ + rowData, + createForeignRows: { + createMany2One: 2, + }, + }) + + row = rowsInfo.row + foreignRows = rowsInfo.foreignRows + }) + + it("enrich populates the foreign field", async () => { + const res = await getAll(primaryPostgresTable._id, row.id) + + expect(res.status).toBe(200) + + expect(res.body).toEqual({ + ...rowData, + [m2oInfo.fieldName]: [ + { + ...foreignRows[0].row, + [`fk_${m2oInfo.table.name}_${m2oInfo.fieldName}`]: row.id, + }, + { + ...foreignRows[1].row, + [`fk_${m2oInfo.table.name}_${m2oInfo.fieldName}`]: row.id, + }, + ], + id: row.id, + tableId: row.tableId, + _id: expect.any(String), + _rev: expect.any(String), + }) + }) + }) }) }) From 5207f5108035d01cfed3ab4f0f9d242d9149f8fa Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 23 Feb 2023 10:50:18 +0100 Subject: [PATCH 13/32] Test enrich for all relationship types --- .../src/integration-test/postgres.spec.ts | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index b9f356ded1..df0e4bac15 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -274,7 +274,20 @@ describe("row api - postgres", () => { } for (let i = 0; i < (opts?.createForeignRows?.createMany2Many || 0); i++) { - await createForeignRow(m2mInfo) + const foreignRow = await config.createRow({ + tableId: m2mInfo.table._id, + title: generator.name(), + }) + + rowData = { + ...rowData, + [m2mInfo.fieldName]: rowData[m2mInfo.fieldName] || [], + } + rowData[m2mInfo.fieldName].push(foreignRow._id) + foreignRows.push({ + row: foreignRow, + relationshipType: RelationshipTypes.MANY_TO_MANY, + }) } const row = await config.createRow({ @@ -513,7 +526,7 @@ describe("row api - postgres", () => { let rows: { row: Row; rowData: PrimaryRowData }[] beforeEach(async () => { - rows = await populatePrimaryRows(10) + rows = await populatePrimaryRows(5) }) it("a single row can be retrieved successfully", async () => { @@ -881,50 +894,15 @@ describe("row api - postgres", () => { describe("given a row with relation data", () => { let row: Row, rowData: PrimaryRowData, foreignRows: ForeignRowsInfo[] - describe("only with one to many data", () => { - beforeEach(async () => { - rowData = generateRandomPrimaryRowData() - const rowsInfo = await createPrimaryRow({ - rowData, - createForeignRows: { createOne2Many: true }, - }) - - row = rowsInfo.row - foreignRows = rowsInfo.foreignRows - }) - - it("enrich populates the foreign field", async () => { - const res = await getAll(primaryPostgresTable._id, row.id) - - expect(res.status).toBe(200) - - expect(foreignRows).toHaveLength(1) - expect(res.body).toEqual({ - ...rowData, - [`fk_${o2mInfo.table.name}_${o2mInfo.fieldName}`]: - foreignRows[0].row.id, - [o2mInfo.fieldName]: [ - { - ...foreignRows[0].row, - _id: expect.any(String), - _rev: expect.any(String), - }, - ], - id: row.id, - tableId: row.tableId, - _id: expect.any(String), - _rev: expect.any(String), - }) - }) - }) - - describe("only with many to one data", () => { + describe("with all relationship types", () => { beforeEach(async () => { rowData = generateRandomPrimaryRowData() const rowsInfo = await createPrimaryRow({ rowData, createForeignRows: { - createMany2One: 2, + createOne2Many: true, + createMany2One: 3, + createMany2Many: 2, }, }) @@ -932,22 +910,47 @@ describe("row api - postgres", () => { foreignRows = rowsInfo.foreignRows }) - it("enrich populates the foreign field", async () => { + it("enrich populates the foreign fields", async () => { const res = await getAll(primaryPostgresTable._id, row.id) expect(res.status).toBe(200) + const foreignRowsByType = _.groupBy( + foreignRows, + x => x.relationshipType + ) expect(res.body).toEqual({ ...rowData, + [`fk_${o2mInfo.table.name}_${o2mInfo.fieldName}`]: + foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row.id, + [o2mInfo.fieldName]: [ + { + ...foreignRowsByType[RelationshipTypes.ONE_TO_MANY][0].row, + _id: expect.any(String), + _rev: expect.any(String), + }, + ], [m2oInfo.fieldName]: [ { - ...foreignRows[0].row, + ...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][0].row, [`fk_${m2oInfo.table.name}_${m2oInfo.fieldName}`]: row.id, }, { - ...foreignRows[1].row, + ...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][1].row, [`fk_${m2oInfo.table.name}_${m2oInfo.fieldName}`]: row.id, }, + { + ...foreignRowsByType[RelationshipTypes.MANY_TO_ONE][2].row, + [`fk_${m2oInfo.table.name}_${m2oInfo.fieldName}`]: row.id, + }, + ], + [m2mInfo.fieldName]: [ + { + ...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][0].row, + }, + { + ...foreignRowsByType[RelationshipTypes.MANY_TO_MANY][1].row, + }, ], id: row.id, tableId: row.tableId, From 4c70b7fd9b9f5ece06df8437349d9ed7fc44857c Mon Sep 17 00:00:00 2001 From: Gerard Burns Date: Thu, 23 Feb 2023 12:06:55 +0000 Subject: [PATCH 14/32] Improve Add Screen Modal (#9759) * Improve Add Screen Modal * lint --------- Co-authored-by: Rory Powell --- .../screens/_components/NewScreenModal.svelte | 80 ++++++++++++------ .../_components/blankScreenPreview.png | Bin 0 -> 71063 bytes .../screens/_components/listScreenPreview.png | Bin 0 -> 108488 bytes 3 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/blankScreenPreview.png create mode 100644 packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/listScreenPreview.png diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/NewScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/NewScreenModal.svelte index b5cd28c702..e3a974b344 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/NewScreenModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/NewScreenModal.svelte @@ -1,11 +1,13 @@ - + onMount(() => { + document.addEventListener("keydown", handleKey) + return () => { + document.removeEventListener("keydown", handleKey) + } + }) + {#if inline} {#if visible} diff --git a/packages/bbui/src/helpers.js b/packages/bbui/src/helpers.js index b02783e0bd..f2246fbb49 100644 --- a/packages/bbui/src/helpers.js +++ b/packages/bbui/src/helpers.js @@ -104,6 +104,9 @@ export const deepSet = (obj, key, value) => { * @param obj the object to clone */ export const cloneDeep = obj => { + if (!obj) { + return obj + } return JSON.parse(JSON.stringify(obj)) } diff --git a/packages/builder/package.json b/packages/builder/package.json index 3f4b4fb274..b87cd3924b 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -72,6 +72,7 @@ "codemirror": "^5.59.0", "dayjs": "^1.11.2", "downloadjs": "1.4.7", + "fast-json-patch": "^3.1.1", "lodash": "4.17.21", "posthog-js": "^1.36.0", "remixicon": "2.5.0", diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 69bca7eac3..d15cdb6e98 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -5,12 +5,47 @@ import { getThemeStore } from "./store/theme" import { derived } from "svelte/store" import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" +import { createHistoryStore } from "builderStore/store/history" +import { get } from "svelte/store" export const store = getFrontendStore() export const automationStore = getAutomationStore() export const themeStore = getThemeStore() export const temporalStore = getTemporalStore() +// Setup history for screens +export const screenHistoryStore = createHistoryStore({ + getDoc: id => get(store).screens?.find(screen => screen._id === id), + selectDoc: store.actions.screens.select, + afterAction: () => { + // Ensure a valid component is selected + if (!get(selectedComponent)) { + store.update(state => ({ + ...state, + selectedComponentId: get(selectedScreen)?.props._id, + })) + } + }, +}) +store.actions.screens.save = screenHistoryStore.wrapSaveDoc( + store.actions.screens.save +) +store.actions.screens.delete = screenHistoryStore.wrapDeleteDoc( + store.actions.screens.delete +) + +// Setup history for automations +export const automationHistoryStore = createHistoryStore({ + getDoc: automationStore.actions.getDefinition, + selectDoc: automationStore.actions.select, +}) +automationStore.actions.save = automationHistoryStore.wrapSaveDoc( + automationStore.actions.save +) +automationStore.actions.delete = automationHistoryStore.wrapDeleteDoc( + automationStore.actions.delete +) + export const selectedScreen = derived(store, $store => { return $store.screens.find(screen => screen._id === $store.selectedScreenId) }) @@ -71,3 +106,13 @@ export const selectedComponentPath = derived( ).map(component => component._id) } ) + +// Derived automation state +export const selectedAutomation = derived(automationStore, $automationStore => { + if (!$automationStore.selectedAutomationId) { + return null + } + return $automationStore.automations?.find( + x => x._id === $automationStore.selectedAutomationId + ) +}) diff --git a/packages/builder/src/builderStore/store/automation/Automation.js b/packages/builder/src/builderStore/store/automation/Automation.js deleted file mode 100644 index af0c03cb5a..0000000000 --- a/packages/builder/src/builderStore/store/automation/Automation.js +++ /dev/null @@ -1,69 +0,0 @@ -import { generate } from "shortid" - -/** - * Class responsible for the traversing of the automation definition. - * Automation definitions are stored in linked lists. - */ -export default class Automation { - constructor(automation) { - this.automation = automation - } - - hasTrigger() { - return this.automation.definition.trigger - } - - addTestData(data) { - this.automation.testData = { ...this.automation.testData, ...data } - } - - addBlock(block, idx) { - // Make sure to add trigger if doesn't exist - if (!this.hasTrigger() && block.type === "TRIGGER") { - const trigger = { id: generate(), ...block } - this.automation.definition.trigger = trigger - return trigger - } - - const newBlock = { id: generate(), ...block } - this.automation.definition.steps.splice(idx, 0, newBlock) - return newBlock - } - - updateBlock(updatedBlock, id) { - const { steps, trigger } = this.automation.definition - - if (trigger && trigger.id === id) { - this.automation.definition.trigger = updatedBlock - return - } - - const stepIdx = steps.findIndex(step => step.id === id) - if (stepIdx < 0) throw new Error("Block not found.") - steps.splice(stepIdx, 1, updatedBlock) - this.automation.definition.steps = steps - } - - deleteBlock(id) { - const { steps, trigger } = this.automation.definition - - if (trigger && trigger.id === id) { - this.automation.definition.trigger = null - return - } - - const stepIdx = steps.findIndex(step => step.id === id) - if (stepIdx < 0) throw new Error("Block not found.") - steps.splice(stepIdx, 1) - this.automation.definition.steps = steps - } - - constructBlock(type, stepId, blockDefinition) { - return { - ...blockDefinition, - inputs: blockDefinition.inputs || {}, - stepId, - type, - } - } -} diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index af102ab694..dc1e2a2cc1 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -1,16 +1,18 @@ -import { writable } from "svelte/store" +import { writable, get } from "svelte/store" import { API } from "api" -import Automation from "./Automation" import { cloneDeep } from "lodash/fp" +import { generate } from "shortid" +import { selectedAutomation } from "builderStore" const initialAutomationState = { automations: [], + testResults: null, showTestPanel: false, blockDefinitions: { TRIGGER: [], ACTION: [], }, - selectedAutomation: null, + selectedAutomationId: null, } export const getAutomationStore = () => { @@ -37,49 +39,41 @@ const automationActions = store => ({ API.getAutomationDefinitions(), ]) store.update(state => { - let selected = state.selectedAutomation?.automation state.automations = responses[0] + state.automations.sort((a, b) => { + return a.name < b.name ? -1 : 1 + }) state.blockDefinitions = { TRIGGER: responses[1].trigger, ACTION: responses[1].action, } - // If previously selected find the new obj and select it - if (selected) { - selected = responses[0].filter( - automation => automation._id === selected._id - ) - state.selectedAutomation = new Automation(selected[0]) - } return state }) }, - create: async ({ name }) => { + create: async (name, trigger) => { const automation = { name, type: "automation", definition: { steps: [], + trigger, }, } - const response = await API.createAutomation(automation) - store.update(state => { - state.automations = [...state.automations, response.automation] - store.actions.select(response.automation) - return state - }) + const response = await store.actions.save(automation) + await store.actions.fetch() + store.actions.select(response._id) + return response }, duplicate: async automation => { - const response = await API.createAutomation({ + const response = await store.actions.save({ ...automation, name: `${automation.name} - copy`, _id: undefined, _ref: undefined, }) - store.update(state => { - state.automations = [...state.automations, response.automation] - store.actions.select(response.automation) - return state - }) + await store.actions.fetch() + store.actions.select(response._id) + return response }, save: async automation => { const response = await API.updateAutomation(automation) @@ -90,11 +84,13 @@ const automationActions = store => ({ ) if (existingIdx !== -1) { state.automations.splice(existingIdx, 1, updatedAutomation) - state.automations = [...state.automations] - store.actions.select(updatedAutomation) return state + } else { + state.automations = [...state.automations, updatedAutomation] } + return state }) + return response.automation }, delete: async automation => { await API.deleteAutomation({ @@ -102,34 +98,83 @@ const automationActions = store => ({ automationRev: automation?._rev, }) store.update(state => { - const existingIdx = state.automations.findIndex( - existing => existing._id === automation?._id + // Remove the automation + state.automations = state.automations.filter( + x => x._id !== automation._id ) - state.automations.splice(existingIdx, 1) - state.automations = [...state.automations] - state.selectedAutomation = null - state.selectedBlock = null + // Select a new automation if required + if (automation._id === state.selectedAutomationId) { + store.actions.select(state.automations[0]?._id) + } return state }) + await store.actions.fetch() + }, + updateBlockInputs: async (block, data) => { + // Create new modified block + let newBlock = { + ...block, + inputs: { + ...block.inputs, + ...data, + }, + } + + // Remove any nullish or empty string values + Object.keys(newBlock.inputs).forEach(key => { + const val = newBlock.inputs[key] + if (val == null || val === "") { + delete newBlock.inputs[key] + } + }) + + // Create new modified automation + const automation = get(selectedAutomation) + const newAutomation = store.actions.getUpdatedDefinition( + automation, + newBlock + ) + + // Don't save if no changes were made + if (JSON.stringify(newAutomation) === JSON.stringify(automation)) { + return + } + await store.actions.save(newAutomation) }, test: async (automation, testData) => { - store.update(state => { - state.selectedAutomation.testResults = null - return state - }) const result = await API.testAutomation({ automationId: automation?._id, testData, }) + if (!result?.trigger && !result?.steps?.length) { + throw "Something went wrong testing your automation" + } store.update(state => { - state.selectedAutomation.testResults = result + state.testResults = result return state }) }, - select: automation => { + getDefinition: id => { + return get(store).automations?.find(x => x._id === id) + }, + getUpdatedDefinition: (automation, block) => { + let newAutomation = cloneDeep(automation) + if (automation.definition.trigger?.id === block.id) { + newAutomation.definition.trigger = block + } else { + const idx = automation.definition.steps.findIndex(x => x.id === block.id) + newAutomation.definition.steps.splice(idx, 1, block) + } + return newAutomation + }, + select: id => { + if (!id || id === get(store).selectedAutomationId) { + return + } store.update(state => { - state.selectedAutomation = new Automation(cloneDeep(automation)) - state.selectedBlock = null + state.selectedAutomationId = id + state.testResults = null + state.showTestPanel = false return state }) }, @@ -147,48 +192,57 @@ const automationActions = store => ({ appId, }) }, - addTestDataToAutomation: data => { - store.update(state => { - state.selectedAutomation.addTestData(data) - return state - }) + addTestDataToAutomation: async data => { + let newAutomation = cloneDeep(get(selectedAutomation)) + newAutomation.testData = { + ...newAutomation.testData, + ...data, + } + await store.actions.save(newAutomation) }, - addBlockToAutomation: (block, blockIdx) => { - store.update(state => { - state.selectedBlock = state.selectedAutomation.addBlock( - cloneDeep(block), - blockIdx - ) - return state - }) + constructBlock(type, stepId, blockDefinition) { + return { + ...blockDefinition, + inputs: blockDefinition.inputs || {}, + stepId, + type, + id: generate(), + } }, - toggleFieldControl: value => { - store.update(state => { - state.selectedBlock.rowControl = value - return state - }) + addBlockToAutomation: async (block, blockIdx) => { + const automation = get(selectedAutomation) + let newAutomation = cloneDeep(automation) + if (!automation) { + return + } + newAutomation.definition.steps.splice(blockIdx, 0, block) + await store.actions.save(newAutomation) }, - deleteAutomationBlock: block => { - store.update(state => { - const idx = - state.selectedAutomation.automation.definition.steps.findIndex( - x => x.id === block.id - ) - state.selectedAutomation.deleteBlock(block.id) + /** + * "rowControl" appears to be the name of the flag used to determine whether + * a certain automation block uses values or bindings as inputs + */ + toggleRowControl: async (block, rowControl) => { + const newBlock = { ...block, rowControl } + const newAutomation = store.actions.getUpdatedDefinition( + get(selectedAutomation), + newBlock + ) + await store.actions.save(newAutomation) + }, + deleteAutomationBlock: async block => { + const automation = get(selectedAutomation) + let newAutomation = cloneDeep(automation) - // Select next closest step - const steps = state.selectedAutomation.automation.definition.steps - let nextSelectedBlock - if (steps[idx] != null) { - nextSelectedBlock = steps[idx] - } else if (steps[idx - 1] != null) { - nextSelectedBlock = steps[idx - 1] - } else { - nextSelectedBlock = - state.selectedAutomation.automation.definition.trigger || null - } - state.selectedBlock = nextSelectedBlock - return state - }) + // Delete trigger if required + if (newAutomation.definition.trigger?.id === block.id) { + delete newAutomation.definition.trigger + } else { + // Otherwise remove step + newAutomation.definition.steps = newAutomation.definition.steps.filter( + step => step.id !== block.id + ) + } + await store.actions.save(newAutomation) }, }) diff --git a/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js b/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js deleted file mode 100644 index 8378310c2e..0000000000 --- a/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import Automation from "../Automation" -import TEST_AUTOMATION from "./testAutomation" - -const TEST_BLOCK = { - id: "AUXJQGZY7", - name: "Delay", - icon: "ri-time-fill", - tagline: "Delay for {{time}} milliseconds", - description: "Delay the automation until an amount of time has passed.", - params: { time: "number" }, - type: "LOGIC", - args: { time: "5000" }, - stepId: "DELAY", -} - -describe("Automation Data Object", () => { - let automation - - beforeEach(() => { - automation = new Automation({ ...TEST_AUTOMATION }) - }) - - it("adds a automation block to the automation", () => { - automation.addBlock(TEST_BLOCK) - expect(automation.automation.definition) - }) - - it("updates a automation block with new attributes", () => { - const firstBlock = automation.automation.definition.steps[0] - const updatedBlock = { - ...firstBlock, - name: "UPDATED", - } - automation.updateBlock(updatedBlock, firstBlock.id) - expect(automation.automation.definition.steps[0]).toEqual(updatedBlock) - }) - - it("deletes a automation block successfully", () => { - const { steps } = automation.automation.definition - const originalLength = steps.length - - const lastBlock = steps[steps.length - 1] - automation.deleteBlock(lastBlock.id) - expect(automation.automation.definition.steps.length).toBeLessThan( - originalLength - ) - }) -}) diff --git a/packages/builder/src/builderStore/store/automation/tests/testAutomation.js b/packages/builder/src/builderStore/store/automation/tests/testAutomation.js deleted file mode 100644 index 3fafbaf1d0..0000000000 --- a/packages/builder/src/builderStore/store/automation/tests/testAutomation.js +++ /dev/null @@ -1,78 +0,0 @@ -export default { - name: "Test automation", - definition: { - steps: [ - { - id: "ANBDINAPS", - description: "Send an email.", - tagline: "Send email to {{to}}", - icon: "ri-mail-open-fill", - name: "Send Email", - params: { - to: "string", - from: "string", - subject: "longText", - text: "longText", - }, - type: "ACTION", - args: { - text: "A user was created!", - subject: "New Budibase User", - from: "budimaster@budibase.com", - to: "test@test.com", - }, - stepId: "SEND_EMAIL", - }, - ], - trigger: { - id: "iRzYMOqND", - name: "Row Saved", - event: "row:save", - icon: "ri-save-line", - tagline: "Row is added to {{table.name}}", - description: "Fired when a row is saved to your database.", - params: { table: "table" }, - type: "TRIGGER", - args: { - table: { - type: "table", - views: {}, - name: "users", - schema: { - name: { - type: "string", - constraints: { - type: "string", - length: { maximum: 123 }, - presence: { allowEmpty: false }, - }, - name: "name", - }, - age: { - type: "number", - constraints: { - type: "number", - presence: { allowEmpty: false }, - numericality: { - greaterThanOrEqualTo: "", - lessThanOrEqualTo: "", - }, - }, - name: "age", - }, - }, - _id: "c6b4e610cd984b588837bca27188a451", - _rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff", - }, - }, - stepId: "ROW_SAVED", - }, - }, - type: "automation", - ok: true, - id: "b384f861f4754e1693835324a7fcca62", - rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37", - live: false, - _id: "b384f861f4754e1693835324a7fcca62", - _rev: "108-4116829ec375e0481d0ecab9e83a2caf", -} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 56b8a599f0..d58a2d5b9e 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -1,6 +1,11 @@ import { get, writable } from "svelte/store" import { cloneDeep } from "lodash/fp" -import { selectedScreen, selectedComponent } from "builderStore" +import { + selectedScreen, + selectedComponent, + screenHistoryStore, + automationHistoryStore, +} from "builderStore" import { datasources, integrations, @@ -122,6 +127,8 @@ export const getFrontendStore = () => { navigation: application.navigation || {}, usedPlugins: application.usedPlugins || [], })) + screenHistoryStore.reset() + automationHistoryStore.reset() // Initialise backend stores database.set(application.instance) @@ -179,10 +186,7 @@ export const getFrontendStore = () => { } // Check screen isn't already selected - if ( - state.selectedScreenId === screen._id && - state.selectedComponentId === screen.props?._id - ) { + if (state.selectedScreenId === screen._id) { return } @@ -256,7 +260,7 @@ export const getFrontendStore = () => { } }, save: async screen => { - /* + /* Temporarily disabled to accomodate migration issues. store.actions.screens.validate(screen) */ @@ -347,6 +351,7 @@ export const getFrontendStore = () => { return state }) + return null }, updateSetting: async (screen, name, value) => { if (!screen || !name) { diff --git a/packages/builder/src/builderStore/store/history.js b/packages/builder/src/builderStore/store/history.js new file mode 100644 index 0000000000..0f21085c6a --- /dev/null +++ b/packages/builder/src/builderStore/store/history.js @@ -0,0 +1,319 @@ +import * as jsonpatch from "fast-json-patch/index.mjs" +import { writable, derived, get } from "svelte/store" + +const Operations = { + Add: "Add", + Delete: "Delete", + Change: "Change", +} + +const initialState = { + history: [], + position: 0, + loading: false, +} + +export const createHistoryStore = ({ + getDoc, + selectDoc, + beforeAction, + afterAction, +}) => { + // Use a derived store to check if we are able to undo or redo any operations + const store = writable(initialState) + const derivedStore = derived(store, $store => { + return { + ...$store, + canUndo: $store.position > 0, + canRedo: $store.position < $store.history.length, + } + }) + + // Wrapped versions of essential functions which we call ourselves when using + // undo and redo + let saveFn + let deleteFn + + /** + * Internal util to set the loading flag + */ + const startLoading = () => { + store.update(state => { + state.loading = true + return state + }) + } + + /** + * Internal util to unset the loading flag + */ + const stopLoading = () => { + store.update(state => { + state.loading = false + return state + }) + } + + /** + * Resets history state + */ + const reset = () => { + store.set(initialState) + } + + /** + * Adds or updates an operation in history. + * For internal use only. + * @param operation the operation to save + */ + const saveOperation = operation => { + store.update(state => { + // Update history + let history = state.history + let position = state.position + if (!operation.id) { + // Every time a new operation occurs we discard any redo potential + operation.id = Math.random() + history = [...history.slice(0, state.position), operation] + position += 1 + } else { + // If this is a redo/undo of an existing operation, just update history + // to replace the doc object as revisions may have changed + const idx = history.findIndex(op => op.id === operation.id) + history[idx].doc = operation.doc + } + return { history, position } + }) + } + + /** + * Wraps the save function, which asynchronously updates a doc. + * The returned function is an enriched version of the real save function so + * that we can control history. + * @param fn the save function + * @returns {function} a wrapped version of the save function + */ + const wrapSaveDoc = fn => { + saveFn = async (doc, operationId) => { + // Only works on a single doc at a time + if (!doc || Array.isArray(doc)) { + return + } + startLoading() + try { + const oldDoc = getDoc(doc._id) + const newDoc = jsonpatch.deepClone(await fn(doc)) + + // Store the change + if (!oldDoc) { + // If no old doc, this is an add operation + saveOperation({ + type: Operations.Add, + doc: newDoc, + id: operationId, + }) + } else { + // Otherwise this is a change operation + saveOperation({ + type: Operations.Change, + forwardPatch: jsonpatch.compare(oldDoc, doc), + backwardsPatch: jsonpatch.compare(doc, oldDoc), + doc: newDoc, + id: operationId, + }) + } + stopLoading() + return newDoc + } catch (error) { + // We want to allow errors to propagate up to normal handlers, but we + // want to stop loading first + stopLoading() + throw error + } + } + return saveFn + } + + /** + * Wraps the delete function, which asynchronously deletes a doc. + * The returned function is an enriched version of the real delete function so + * that we can control history. + * @param fn the delete function + * @returns {function} a wrapped version of the delete function + */ + const wrapDeleteDoc = fn => { + deleteFn = async (doc, operationId) => { + // Only works on a single doc at a time + if (!doc || Array.isArray(doc)) { + return + } + startLoading() + try { + const oldDoc = jsonpatch.deepClone(doc) + await fn(doc) + saveOperation({ + type: Operations.Delete, + doc: oldDoc, + id: operationId, + }) + stopLoading() + } catch (error) { + // We want to allow errors to propagate up to normal handlers, but we + // want to stop loading first + stopLoading() + throw error + } + } + return deleteFn + } + + /** + * Asynchronously undoes the previous operation. + * Optionally selects the changed document so that changes are visible. + * @returns {Promise} + */ + const undo = async () => { + // Sanity checks + const { canUndo, history, position, loading } = get(derivedStore) + if (!canUndo || loading) { + return + } + const operation = history[position - 1] + if (!operation) { + return + } + startLoading() + + // Before hook + await beforeAction?.(operation) + + // Update state immediately to prevent further clicks and to prevent bad + // history in the event of an update failing + store.update(state => { + return { + ...state, + position: state.position - 1, + } + }) + + // Undo the operation + try { + // Undo ADD + if (operation.type === Operations.Add) { + // Try to get the latest doc version to delete + const latestDoc = getDoc(operation.doc._id) + const doc = latestDoc || operation.doc + await deleteFn(doc, operation.id) + } + + // Undo DELETE + else if (operation.type === Operations.Delete) { + // Delete the _rev from the deleted doc so that we can save it as a new + // doc again without conflicts + let doc = jsonpatch.deepClone(operation.doc) + delete doc._rev + const created = await saveFn(doc, operation.id) + selectDoc?.(created?._id || doc._id) + } + + // Undo CHANGE + else { + // Get the current doc and apply the backwards patch on top of it + let doc = jsonpatch.deepClone(getDoc(operation.doc._id)) + if (doc) { + jsonpatch.applyPatch( + doc, + jsonpatch.deepClone(operation.backwardsPatch) + ) + await saveFn(doc, operation.id) + selectDoc?.(doc._id) + } + } + stopLoading() + } catch (error) { + stopLoading() + throw error + } + + // After hook + await afterAction?.(operation) + } + + /** + * Asynchronously redoes the previous undo. + * Optionally selects the changed document so that changes are visible. + * @returns {Promise} + */ + const redo = async () => { + // Sanity checks + const { canRedo, history, position, loading } = get(derivedStore) + if (!canRedo || loading) { + return + } + const operation = history[position] + if (!operation) { + return + } + startLoading() + + // Before hook + await beforeAction?.(operation) + + // Update state immediately to prevent further clicks and to prevent bad + // history in the event of an update failing + store.update(state => { + return { + ...state, + position: state.position + 1, + } + }) + + // Redo the operation + try { + // Redo ADD + if (operation.type === Operations.Add) { + // Delete the _rev from the deleted doc so that we can save it as a new + // doc again without conflicts + let doc = jsonpatch.deepClone(operation.doc) + delete doc._rev + const created = await saveFn(doc, operation.id) + selectDoc?.(created?._id || doc._id) + } + + // Redo DELETE + else if (operation.type === Operations.Delete) { + // Try to get the latest doc version to delete + const latestDoc = getDoc(operation.doc._id) + const doc = latestDoc || operation.doc + await deleteFn(doc, operation.id) + } + + // Redo CHANGE + else { + // Get the current doc and apply the forwards patch on top of it + let doc = jsonpatch.deepClone(getDoc(operation.doc._id)) + if (doc) { + jsonpatch.applyPatch(doc, jsonpatch.deepClone(operation.forwardPatch)) + await saveFn(doc, operation.id) + selectDoc?.(doc._id) + } + } + stopLoading() + } catch (error) { + stopLoading() + throw error + } + + // After hook + await afterAction?.(operation) + } + + return { + subscribe: derivedStore.subscribe, + wrapSaveDoc, + wrapDeleteDoc, + reset, + undo, + redo, + } +} diff --git a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte index e852ee1a0d..b80ba45086 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte @@ -1,10 +1,10 @@ -{#if automation} - +{#if $selectedAutomation} + {#key $selectedAutomation._id} + + {/key} {/if} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index caf8835b86..f30b49eb39 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -5,7 +5,6 @@ Detail, Body, Icon, - Tooltip, notifications, } from "@budibase/bbui" import { automationStore } from "builderStore" @@ -13,7 +12,6 @@ import { externalActions } from "./ExternalActions" export let blockIdx - export let blockComplete const disabled = { SEND_EMAIL_SMTP: { @@ -50,15 +48,12 @@ async function addBlockToAutomation() { try { - const newBlock = $automationStore.selectedAutomation.constructBlock( + const newBlock = automationStore.actions.constructBlock( "ACTION", actionVal.stepId, actionVal ) - automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1) - await automationStore.actions.save( - $automationStore.selectedAutomation?.automation - ) + await automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1) } catch (error) { notifications.error("Error saving automation") } @@ -66,20 +61,14 @@ { - blockComplete = true - addBlockToAutomation() - }} + onConfirm={addBlockToAutomation} > - Select an app or event. - - - Apps - + + Apps
{#each Object.entries(external) as [idx, action]}
- {idx.charAt(0).toUpperCase() + idx.slice(1)} + + {idx.charAt(0).toUpperCase() + idx.slice(1)} + +
{/each} +
+ Actions -
{#each Object.entries(internal) as [idx, action]} - {#if disabled[idx] && disabled[idx].disabled} - -
selectAction(action)} - > -
- - - {action.name} -
-
-
- {:else} -
selectAction(action)} - > -
- - - {action.name} -
+ {@const isDisabled = disabled[idx] && disabled[idx].disabled} +
selectAction(action)} + > +
+ + {action.name} + {#if isDisabled} + + {/if}
- {/if} +
{/each}
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 4b01616b54..63a3478ef3 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -1,5 +1,5 @@
-
+
{automation.name} -
-
-
-
-
- -
+
+ + +
{ testDataModal.show() @@ -62,15 +60,13 @@ icon="MultipleCheck" size="M">Run test -
- { - $automationStore.showTestPanel = true - }} - size="M">Test Details -
+ { + $automationStore.showTestPanel = true + }} + size="M">Test Details
@@ -80,7 +76,7 @@
{#if block.stepId !== ActionStepID.LOOP} @@ -105,6 +101,9 @@ diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index d6e5fcb36d..7484a60502 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -1,5 +1,5 @@
{}}> - {#if loopingSelected} + {#if loopBlock}
{ @@ -174,13 +142,8 @@
-
{ - onSelect(block) - }} - > - +
{}}> +
@@ -198,9 +161,7 @@ $automationStore.blockDefinitions.ACTION.LOOP.schema.inputs .properties )} - block={$automationStore.selectedAutomation?.automation.definition.steps.find( - x => x.blockToLoop === block.id - )} + block={loopBlock} {webhookModal} /> @@ -209,22 +170,28 @@ {/if} {/if} - - {#if !blockComplete} + (open = !open)} + /> + {#if open}
{#if !isTrigger}
- {#if !loopingSelected} - addLooping()} icon="Reuse" - >Add Looping + {#if !loopBlock} + addLooping()} icon="Reuse"> + Add Looping + {/if} {#if showBindingPicker}