Merge branch 'v3-ui' of github.com:Budibase/budibase into v3-ui

This commit is contained in:
Andrew Kingston 2024-10-11 09:39:42 +01:00
commit bf11917e1b
No known key found for this signature in database
18 changed files with 166 additions and 25 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.15", "version": "2.32.16",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -211,6 +211,17 @@ export class DatabaseImpl implements Database {
}) })
} }
async tryGet<T extends Document>(id?: string): Promise<T | undefined> {
try {
return await this.get<T>(id)
} catch (err: any) {
if (err.statusCode === 404) {
return undefined
}
throw err
}
}
async getMultiple<T extends Document>( async getMultiple<T extends Document>(
ids: string[], ids: string[],
opts?: { allowMissing?: boolean; excludeDocs?: boolean } opts?: { allowMissing?: boolean; excludeDocs?: boolean }

View File

@ -42,6 +42,13 @@ export class DDInstrumentedDatabase implements Database {
}) })
} }
tryGet<T extends Document>(id?: string | undefined): Promise<T | undefined> {
return tracer.trace("db.tryGet", span => {
span?.addTags({ db_name: this.name, doc_id: id })
return this.db.tryGet(id)
})
}
getMultiple<T extends Document>( getMultiple<T extends Document>(
ids: string[], ids: string[],
opts?: { allowMissing?: boolean | undefined } | undefined opts?: { allowMissing?: boolean | undefined } | undefined

View File

@ -435,7 +435,9 @@ export function getExternalRoleID(roleId: string, version?: string) {
roleId.startsWith(DocumentType.ROLE) && roleId.startsWith(DocumentType.ROLE) &&
(isBuiltin(roleId) || version === RoleIDVersion.NAME) (isBuiltin(roleId) || version === RoleIDVersion.NAME)
) { ) {
return roleId.split(`${DocumentType.ROLE}${SEPARATOR}`)[1] const parts = roleId.split(SEPARATOR)
parts.shift()
return parts.join(SEPARATOR)
} }
return roleId return roleId
} }

View File

@ -124,7 +124,7 @@
subtype: column.subtype, subtype: column.subtype,
visible: column.visible, visible: column.visible,
readonly: column.readonly, readonly: column.readonly,
constraints: column.constraints, // This is needed to properly display "users" column icon: column.icon,
}, },
} }
}) })

View File

@ -14,7 +14,13 @@
function daysUntilCancel() { function daysUntilCancel() {
const cancelAt = license?.billing?.subscription?.cancelAt const cancelAt = license?.billing?.subscription?.cancelAt
const diffTime = Math.abs(cancelAt - new Date().getTime()) / 1000 const diffTime = Math.abs(cancelAt - new Date().getTime()) / 1000
return Math.floor(diffTime / oneDayInSeconds) const days = Math.floor(diffTime / oneDayInSeconds)
if (days === 1) {
return "tomorrow."
} else if (days === 0) {
return "today."
}
return `in ${days} days.`
} }
</script> </script>
@ -28,7 +34,7 @@
extraLinkAction={$licensing.goToUpgradePage} extraLinkAction={$licensing.goToUpgradePage}
showCloseButton={false} showCloseButton={false}
> >
Your free trial will end in {daysUntilCancel()} days. Your free trial will end {daysUntilCancel()}
</Banner> </Banner>
</div> </div>
{/if} {/if}

View File

@ -2,6 +2,10 @@ import { helpers } from "@budibase/shared-core"
import { TypeIconMap } from "../constants" import { TypeIconMap } from "../constants"
export const getColumnIcon = column => { export const getColumnIcon = column => {
if (column.schema.icon) {
return column.schema.icon
}
if (column.schema.autocolumn) { if (column.schema.autocolumn) {
return "MagicWand" return "MagicWand"
} }

View File

@ -21,14 +21,15 @@ export async function find(ctx: Ctx<void, RowActionsResponse>) {
const table = await getTable(ctx) const table = await getTable(ctx)
const tableId = table._id! const tableId = table._id!
if (!(await sdk.rowActions.docExists(tableId))) { const rowActions = await sdk.rowActions.getAll(tableId)
if (!rowActions) {
ctx.body = { ctx.body = {
actions: {}, actions: {},
} }
return return
} }
const { actions } = await sdk.rowActions.getAll(tableId) const { actions } = rowActions
const result: RowActionsResponse = { const result: RowActionsResponse = {
actions: Object.entries(actions).reduce<Record<string, RowActionResponse>>( actions: Object.entries(actions).reduce<Record<string, RowActionResponse>>(
(acc, [key, action]) => ({ (acc, [key, action]) => ({

View File

@ -139,6 +139,7 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
export async function destroy(ctx: UserCtx) { export async function destroy(ctx: UserCtx) {
const appId = ctx.appId const appId = ctx.appId
const tableId = ctx.params.tableId const tableId = ctx.params.tableId
await sdk.rowActions.deleteAll(tableId)
const deletedTable = await pickApi({ tableId }).destroy(ctx) const deletedTable = await pickApi({ tableId }).destroy(ctx)
await events.table.deleted(deletedTable) await events.table.deleted(deletedTable)
ctx.eventEmitter && ctx.eventEmitter &&

View File

@ -161,7 +161,7 @@ describe("/roles", () => {
it("should not fetch higher level accessible roles when a custom role header is provided", async () => { it("should not fetch higher level accessible roles when a custom role header is provided", async () => {
await createRole({ await createRole({
name: `CUSTOM_ROLE`, name: `custom_role_1`,
inherits: roles.BUILTIN_ROLE_IDS.BASIC, inherits: roles.BUILTIN_ROLE_IDS.BASIC,
permissionId: permissions.BuiltinPermissionID.READ_ONLY, permissionId: permissions.BuiltinPermissionID.READ_ONLY,
version: "name", version: "name",
@ -170,11 +170,11 @@ describe("/roles", () => {
.get("/api/roles/accessible") .get("/api/roles/accessible")
.set({ .set({
...config.defaultHeaders(), ...config.defaultHeaders(),
"x-budibase-role": "CUSTOM_ROLE", "x-budibase-role": "custom_role_1",
}) })
.expect(200) .expect(200)
expect(res.body.length).toBe(3) expect(res.body.length).toBe(3)
expect(res.body[0]).toBe("CUSTOM_ROLE") expect(res.body[0]).toBe("custom_role_1")
expect(res.body[1]).toBe("BASIC") expect(res.body[1]).toBe("BASIC")
expect(res.body[2]).toBe("PUBLIC") expect(res.body[2]).toBe("PUBLIC")
}) })

View File

@ -1043,4 +1043,44 @@ describe("/rowsActions", () => {
) )
}) })
}) })
describe("scenarios", () => {
// https://linear.app/budibase/issue/BUDI-8717/
it("should not brick the app when deleting a table with row actions", async () => {
const view = await config.api.viewV2.create({
tableId,
name: generator.guid(),
schema: {
name: { visible: true },
},
})
const rowAction = await config.api.rowAction.save(tableId, {
name: generator.guid(),
})
await config.api.rowAction.setViewPermission(
tableId,
view.id,
rowAction.id
)
let actionsResp = await config.api.rowAction.find(tableId)
expect(actionsResp.actions[rowAction.id]).toEqual({
...rowAction,
allowedSources: [tableId, view.id],
})
const table = await config.api.table.get(tableId)
await config.api.table.destroy(table._id!, table._rev!)
// In the bug reported by Conor, when a delete got deleted its row action
// document was not being cleaned up. This meant there existed code paths
// that would find it and try to reference the tables within it, resulting
// in errors.
await config.api.automation.fetchEnriched({
status: 200,
})
})
})
}) })

View File

@ -26,13 +26,13 @@ export async function getBuilderData(
return tableNameCache[tableId] return tableNameCache[tableId]
} }
const rowActionNameCache: Record<string, TableRowActions> = {} const rowActionNameCache: Record<string, TableRowActions | undefined> = {}
async function getRowActionName(tableId: string, rowActionId: string) { async function getRowActionName(tableId: string, rowActionId: string) {
if (!rowActionNameCache[tableId]) { if (!rowActionNameCache[tableId]) {
rowActionNameCache[tableId] = await sdk.rowActions.getAll(tableId) rowActionNameCache[tableId] = await sdk.rowActions.getAll(tableId)
} }
return rowActionNameCache[tableId].actions[rowActionId]?.name return rowActionNameCache[tableId]?.actions[rowActionId]?.name
} }
const result: Record<string, AutomationBuilderData> = {} const result: Record<string, AutomationBuilderData> = {}
@ -51,6 +51,10 @@ export async function getBuilderData(
const tableName = await getTableName(tableId) const tableName = await getTableName(tableId)
const rowActionName = await getRowActionName(tableId, rowActionId) const rowActionName = await getRowActionName(tableId, rowActionId)
if (!rowActionName) {
throw new Error(`Row action not found: ${rowActionId}`)
}
result[automation._id!] = { result[automation._id!] = {
displayName: rowActionName, displayName: rowActionName,
triggerInfo: { triggerInfo: {

View File

@ -1,5 +1,6 @@
import { context, docIds, HTTPError, utils } from "@budibase/backend-core" import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
import { import {
Automation,
AutomationTriggerStepId, AutomationTriggerStepId,
SEPARATOR, SEPARATOR,
TableRowActions, TableRowActions,
@ -102,7 +103,25 @@ export async function get(tableId: string, rowActionId: string) {
export async function getAll(tableId: string) { export async function getAll(tableId: string) {
const db = context.getAppDB() const db = context.getAppDB()
const rowActionsId = generateRowActionsID(tableId) const rowActionsId = generateRowActionsID(tableId)
return await db.get<TableRowActions>(rowActionsId) return await db.tryGet<TableRowActions>(rowActionsId)
}
export async function deleteAll(tableId: string) {
const db = context.getAppDB()
const doc = await getAll(tableId)
if (!doc) {
return
}
const automationIds = Object.values(doc.actions).map(a => a.automationId)
const automations = await db.getMultiple<Automation>(automationIds)
for (const automation of automations) {
await sdk.automations.remove(automation._id!, automation._rev!)
}
await db.remove(doc)
} }
export async function docExists(tableId: string) { export async function docExists(tableId: string) {
@ -223,9 +242,8 @@ export async function run(tableId: any, rowActionId: any, rowId: string) {
throw new HTTPError("Table not found", 404) throw new HTTPError("Table not found", 404)
} }
const { actions } = await getAll(tableId) const rowActions = await getAll(tableId)
const rowAction = rowActions?.actions[rowActionId]
const rowAction = actions[rowActionId]
if (!rowAction) { if (!rowAction) {
throw new HTTPError("Row action not found", 404) throw new HTTPError("Row action not found", 404)
} }

View File

@ -1,4 +1,5 @@
import { import {
BBReferenceFieldSubType,
CalculationType, CalculationType,
canGroupBy, canGroupBy,
FeatureFlag, FeatureFlag,
@ -7,6 +8,7 @@ import {
PermissionLevel, PermissionLevel,
RelationSchemaField, RelationSchemaField,
RenameColumn, RenameColumn,
RequiredKeys,
Table, Table,
TableSchema, TableSchema,
View, View,
@ -322,13 +324,26 @@ export async function enrichSchema(
const viewFieldSchema = viewFields[relTableFieldName] const viewFieldSchema = viewFields[relTableFieldName]
const isVisible = !!viewFieldSchema?.visible const isVisible = !!viewFieldSchema?.visible
const isReadonly = !!viewFieldSchema?.readonly const isReadonly = !!viewFieldSchema?.readonly
result[relTableFieldName] = { const enrichedFieldSchema: RequiredKeys<ViewV2ColumnEnriched> = {
...relTableField,
...viewFieldSchema,
name: relTableField.name,
visible: isVisible, visible: isVisible,
readonly: isReadonly, readonly: isReadonly,
order: viewFieldSchema?.order,
width: viewFieldSchema?.width,
icon: relTableField.icon,
type: relTableField.type,
subtype: relTableField.subtype,
} }
if (
!enrichedFieldSchema.icon &&
relTableField.type === FieldType.BB_REFERENCE &&
relTableField.subtype === BBReferenceFieldSubType.USER &&
!helpers.schema.isDeprecatedSingleUserColumn(relTableField)
) {
// Forcing the icon, otherwise we would need to pass the constraints to show the proper icon
enrichedFieldSchema.icon = "UserGroup"
}
result[relTableFieldName] = enrichedFieldSchema
} }
return result return result
} }

View File

@ -355,13 +355,11 @@ describe("table sdk", () => {
visible: true, visible: true,
columns: { columns: {
title: { title: {
name: "title",
type: "string", type: "string",
visible: true, visible: true,
readonly: true, readonly: true,
}, },
age: { age: {
name: "age",
type: "number", type: "number",
visible: false, visible: false,
readonly: false, readonly: false,

View File

@ -1,4 +1,4 @@
import { Automation } from "@budibase/types" import { Automation, FetchAutomationResponse } from "@budibase/types"
import { Expectations, TestAPI } from "./base" import { Expectations, TestAPI } from "./base"
export class AutomationAPI extends TestAPI { export class AutomationAPI extends TestAPI {
@ -14,6 +14,26 @@ export class AutomationAPI extends TestAPI {
) )
return result return result
} }
fetch = async (
expectations?: Expectations
): Promise<FetchAutomationResponse> => {
return await this._get<FetchAutomationResponse>(`/api/automations`, {
expectations,
})
}
fetchEnriched = async (
expectations?: Expectations
): Promise<FetchAutomationResponse> => {
return await this._get<FetchAutomationResponse>(
`/api/automations?enrich=true`,
{
expectations,
}
)
}
post = async ( post = async (
body: Automation, body: Automation,
expectations?: Expectations expectations?: Expectations

View File

@ -129,7 +129,12 @@ export interface Database {
name: string name: string
exists(): Promise<boolean> exists(): Promise<boolean>
/**
* @deprecated the plan is to get everything using `tryGet` instead, then rename
* `tryGet` to `get`.
*/
get<T extends Document>(id?: string): Promise<T> get<T extends Document>(id?: string): Promise<T>
tryGet<T extends Document>(id?: string): Promise<T | undefined>
exists(docId: string): Promise<boolean> exists(docId: string): Promise<boolean>
getMultiple<T extends Document>( getMultiple<T extends Document>(
ids: string[], ids: string[],

View File

@ -1,4 +1,10 @@
import { FieldSchema, RelationSchemaField, ViewV2 } from "../documents" import {
FieldSchema,
FieldSubType,
FieldType,
RelationSchemaField,
ViewV2,
} from "../documents"
export interface ViewV2Enriched extends ViewV2 { export interface ViewV2Enriched extends ViewV2 {
schema?: { schema?: {
@ -8,4 +14,7 @@ export interface ViewV2Enriched extends ViewV2 {
} }
} }
export type ViewV2ColumnEnriched = RelationSchemaField & FieldSchema export interface ViewV2ColumnEnriched extends RelationSchemaField {
type: FieldType
subtype?: FieldSubType
}