Merge branch 'v3-ui' of github.com:Budibase/budibase into v3-ui
This commit is contained in:
commit
bf11917e1b
|
@ -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/*",
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]) => ({
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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")
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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[],
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue