Merge remote-tracking branch 'origin/v3-ui' into feature/automation-branching-ux

This commit is contained in:
Dean 2024-10-22 16:33:44 +01:00
commit 1c41471174
12 changed files with 124 additions and 73 deletions

View File

@ -118,14 +118,17 @@
if ($values.url) { if ($values.url) {
data.append("url", $values.url.trim()) data.append("url", $values.url.trim())
} }
data.append("useTemplate", template != null)
if (template) { if (template?.fromFile) {
data.append("templateName", template.name) data.append("useTemplate", true)
data.append("templateKey", template.key) data.append("fileToImport", $values.file)
data.append("templateFile", $values.file)
if ($values.encryptionPassword?.trim()) { if ($values.encryptionPassword?.trim()) {
data.append("encryptionPassword", $values.encryptionPassword.trim()) data.append("encryptionPassword", $values.encryptionPassword.trim())
} }
} else if (template) {
data.append("useTemplate", true)
data.append("templateName", template.name)
data.append("templateKey", template.key)
} }
// Create App // Create App

View File

@ -14,7 +14,7 @@
{ {
"name": "Layout", "name": "Layout",
"icon": "ClassicGridView", "icon": "ClassicGridView",
"children": ["container", "section", "sidepanel", "modal"] "children": ["container", "sidepanel", "modal"]
}, },
{ {
"name": "Data", "name": "Data",

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import Placeholder from "./Placeholder.svelte" import Placeholder from "../Placeholder.svelte"
const { styleable, builderStore } = getContext("sdk") const { styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext, setContext } from "svelte" import { getContext, setContext } from "svelte"
import Section from "../Section.svelte" import Section from "../deprecated/Section.svelte"
export let labelPosition = "above" export let labelPosition = "above"
export let type = "oneColumn" export let type = "oneColumn"

View File

@ -14,7 +14,6 @@ export { default as Placeholder } from "./Placeholder.svelte"
// User facing components // User facing components
export { default as container } from "./container/Container.svelte" export { default as container } from "./container/Container.svelte"
export { default as section } from "./Section.svelte"
export { default as dataprovider } from "./DataProvider.svelte" export { default as dataprovider } from "./DataProvider.svelte"
export { default as divider } from "./Divider.svelte" export { default as divider } from "./Divider.svelte"
export { default as screenslot } from "./ScreenSlot.svelte" export { default as screenslot } from "./ScreenSlot.svelte"
@ -50,3 +49,4 @@ export { default as navigation } from "./deprecated/Navigation.svelte"
export { default as cardhorizontal } from "./deprecated/CardHorizontal.svelte" export { default as cardhorizontal } from "./deprecated/CardHorizontal.svelte"
export { default as stackedlist } from "./deprecated/StackedList.svelte" export { default as stackedlist } from "./deprecated/StackedList.svelte"
export { default as card } from "./deprecated/Card.svelte" export { default as card } from "./deprecated/Card.svelte"
export { default as section } from "./deprecated/Section.svelte"

View File

@ -29,7 +29,7 @@ exports.createApp = async apiKey => {
const body = { const body = {
name, name,
url: `/${name}`, url: `/${name}`,
useTemplate: "true", useTemplate: true,
templateKey: "app/school-admin-panel", templateKey: "app/school-admin-panel",
templateName: "School Admin Panel", templateName: "School Admin Panel",
} }

View File

@ -23,6 +23,7 @@ import {
cache, cache,
context, context,
db as dbCore, db as dbCore,
docIds,
env as envCore, env as envCore,
ErrorCode, ErrorCode,
events, events,
@ -35,7 +36,6 @@ import {
import { USERS_TABLE_SCHEMA, DEFAULT_BB_DATASOURCE_ID } from "../../constants" import { USERS_TABLE_SCHEMA, DEFAULT_BB_DATASOURCE_ID } from "../../constants"
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default" import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
import { removeAppFromUserRoles } from "../../utilities/workerRequests" import { removeAppFromUserRoles } from "../../utilities/workerRequests"
import { stringToReadStream } from "../../utilities"
import { doesUserHaveLock } from "../../utilities/redis" import { doesUserHaveLock } from "../../utilities/redis"
import { cleanupAutomations } from "../../automations/utils" import { cleanupAutomations } from "../../automations/utils"
import { getUniqueRows } from "../../utilities/usageQuota/rows" import { getUniqueRows } from "../../utilities/usageQuota/rows"
@ -54,6 +54,11 @@ import {
DuplicateAppResponse, DuplicateAppResponse,
UpdateAppRequest, UpdateAppRequest,
UpdateAppResponse, UpdateAppResponse,
Database,
FieldType,
BBReferenceFieldSubType,
Row,
BBRequest,
} from "@budibase/types" } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -123,8 +128,7 @@ function checkAppName(
} }
interface AppTemplate { interface AppTemplate {
templateString?: string useTemplate?: boolean
useTemplate?: string
file?: { file?: {
type?: string type?: string
path: string path: string
@ -148,14 +152,7 @@ async function createInstance(appId: string, template: AppTemplate) {
await createRoutingView() await createRoutingView()
await createAllSearchIndex() await createAllSearchIndex()
// replicate the template data to the instance DB if (template && template.useTemplate) {
// this is currently very hard to test, downloading and importing template files
if (template && template.templateString) {
const { ok } = await db.load(stringToReadStream(template.templateString))
if (!ok) {
throw "Error loading database dump from memory."
}
} else if (template && template.useTemplate === "true") {
await sdk.backups.importApp(appId, db, template) await sdk.backups.importApp(appId, db, template)
} else { } else {
// create the users table // create the users table
@ -243,14 +240,15 @@ export async function fetchAppPackage(
async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) { async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[] const apps = (await dbCore.getAllApps({ dev: true })) as App[]
const { const { body } = ctx.request
name, const { name, url, encryptionPassword, templateKey } = body
url,
encryptionPassword, let useTemplate
useTemplate, if (typeof body.useTemplate === "string") {
templateKey, useTemplate = body.useTemplate === "true"
templateString, } else if (typeof body.useTemplate === "boolean") {
} = ctx.request.body useTemplate = body.useTemplate
}
checkAppName(ctx, apps, name) checkAppName(ctx, apps, name)
const appUrl = sdk.applications.getAppUrl({ name, url }) const appUrl = sdk.applications.getAppUrl({ name, url })
@ -259,16 +257,15 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
const instanceConfig: AppTemplate = { const instanceConfig: AppTemplate = {
useTemplate, useTemplate,
key: templateKey, key: templateKey,
templateString,
} }
if (ctx.request.files && ctx.request.files.templateFile) { if (ctx.request.files && ctx.request.files.fileToImport) {
instanceConfig.file = { instanceConfig.file = {
...(ctx.request.files.templateFile as any), ...(ctx.request.files.fileToImport as any),
password: encryptionPassword, password: encryptionPassword,
} }
} else if (typeof ctx.request.body.file?.path === "string") { } else if (typeof body.file?.path === "string") {
instanceConfig.file = { instanceConfig.file = {
path: ctx.request.body.file?.path, path: body.file?.path,
} }
} }
@ -279,6 +276,10 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
const instance = await createInstance(appId, instanceConfig) const instance = await createInstance(appId, instanceConfig)
const db = context.getAppDB() const db = context.getAppDB()
if (instanceConfig.useTemplate && !instanceConfig.file) {
await updateUserColumns(appId, db, ctx.user._id!)
}
const newApplication: App = { const newApplication: App = {
_id: DocumentType.APP_METADATA, _id: DocumentType.APP_METADATA,
_rev: undefined, _rev: undefined,
@ -375,21 +376,81 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
}) })
} }
async function creationEvents(request: any, app: App) { async function updateUserColumns(
appId: string,
db: Database,
toUserId: string
) {
await context.doInAppContext(appId, async () => {
const allTables = await sdk.tables.getAllTables()
const tablesWithUserColumns = []
for (const table of allTables) {
const userColumns = Object.values(table.schema).filter(
f =>
(f.type === FieldType.BB_REFERENCE ||
f.type === FieldType.BB_REFERENCE_SINGLE) &&
f.subtype === BBReferenceFieldSubType.USER
)
if (!userColumns.length) {
continue
}
tablesWithUserColumns.push({
tableId: table._id!,
columns: userColumns.map(c => c.name),
})
}
const docsToUpdate = []
for (const { tableId, columns } of tablesWithUserColumns) {
const docs = await db.allDocs<Row>(
docIds.getRowParams(tableId, null, { include_docs: true })
)
const rows = docs.rows.map(d => d.doc!)
for (const row of rows) {
let shouldUpdate = false
const updatedColumns = columns.reduce<Row>((newColumns, column) => {
if (row[column]) {
shouldUpdate = true
if (Array.isArray(row[column])) {
newColumns[column] = row[column]?.map(() => toUserId)
} else if (row[column]) {
newColumns[column] = toUserId
}
}
return newColumns
}, {})
if (shouldUpdate) {
docsToUpdate.push({
...row,
...updatedColumns,
})
}
}
}
await db.bulkDocs(docsToUpdate)
})
}
async function creationEvents(request: BBRequest<CreateAppRequest>, app: App) {
let creationFns: ((app: App) => Promise<void>)[] = [] let creationFns: ((app: App) => Promise<void>)[] = []
const body = request.body const { useTemplate, templateKey, file } = request.body
if (body.useTemplate === "true") { if (useTemplate === "true") {
// from template // from template
if (body.templateKey && body.templateKey !== "undefined") { if (templateKey && templateKey !== "undefined") {
creationFns.push(a => events.app.templateImported(a, body.templateKey)) creationFns.push(a => events.app.templateImported(a, templateKey))
} }
// from file // from file
else if (request.files?.templateFile) { else if (request.files?.fileToImport) {
creationFns.push(a => events.app.fileImported(a)) creationFns.push(a => events.app.fileImported(a))
} }
// from server file path // from server file path
else if (request.body.file) { else if (file) {
// explicitly pass in the newly created app id // explicitly pass in the newly created app id
creationFns.push(a => events.app.duplicated(a, app.appId)) creationFns.push(a => events.app.duplicated(a, app.appId))
} }
@ -399,16 +460,14 @@ async function creationEvents(request: any, app: App) {
} }
} }
if (!request.duplicate) { creationFns.push(a => events.app.created(a))
creationFns.push(a => events.app.created(a))
}
for (let fn of creationFns) { for (let fn of creationFns) {
await fn(app) await fn(app)
} }
} }
async function appPostCreate(ctx: UserCtx, app: App) { async function appPostCreate(ctx: UserCtx<CreateAppRequest, App>, app: App) {
const tenantId = tenancy.getTenantId() const tenantId = tenancy.getTenantId()
await migrations.backPopulateMigrations({ await migrations.backPopulateMigrations({
type: MigrationType.APP, type: MigrationType.APP,
@ -419,7 +478,7 @@ async function appPostCreate(ctx: UserCtx, app: App) {
await creationEvents(ctx.request, app) await creationEvents(ctx.request, app)
// app import, template creation and duplication // app import, template creation and duplication
if (ctx.request.body.useTemplate === "true") { if (ctx.request.body.useTemplate) {
const { rows } = await getUniqueRows([app.appId]) const { rows } = await getUniqueRows([app.appId])
const rowCount = rows ? rows.length : 0 const rowCount = rows ? rows.length : 0
if (rowCount) { if (rowCount) {

View File

@ -21,6 +21,7 @@ import tk from "timekeeper"
import * as uuid from "uuid" import * as uuid from "uuid"
import { structures } from "@budibase/backend-core/tests" import { structures } from "@budibase/backend-core/tests"
import nock from "nock" import nock from "nock"
import path from "path"
describe("/applications", () => { describe("/applications", () => {
let config = setup.getConfig() let config = setup.getConfig()
@ -137,11 +138,17 @@ describe("/applications", () => {
}) })
it("creates app from template", async () => { it("creates app from template", async () => {
nock("https://prod-budi-templates.s3-eu-west-1.amazonaws.com")
.get(`/templates/app/agency-client-portal.tar.gz`)
.replyWithFile(
200,
path.resolve(__dirname, "data", "agency-client-portal.tar.gz")
)
const app = await config.api.application.create({ const app = await config.api.application.create({
name: utils.newid(), name: utils.newid(),
useTemplate: "true", useTemplate: "true",
templateKey: "test", templateKey: "app/agency-client-portal",
templateString: "{}",
}) })
expect(app._id).toBeDefined() expect(app._id).toBeDefined()
expect(events.app.created).toHaveBeenCalledTimes(1) expect(events.app.created).toHaveBeenCalledTimes(1)
@ -152,7 +159,7 @@ describe("/applications", () => {
const app = await config.api.application.create({ const app = await config.api.application.create({
name: utils.newid(), name: utils.newid(),
useTemplate: "true", useTemplate: "true",
templateFile: "src/api/routes/tests/data/export.txt", fileToImport: "src/api/routes/tests/data/export.txt",
}) })
expect(app._id).toBeDefined() expect(app._id).toBeDefined()
expect(events.app.created).toHaveBeenCalledTimes(1) expect(events.app.created).toHaveBeenCalledTimes(1)
@ -172,7 +179,7 @@ describe("/applications", () => {
const app = await config.api.application.create({ const app = await config.api.application.create({
name: utils.newid(), name: utils.newid(),
useTemplate: "true", useTemplate: "true",
templateFile: "src/api/routes/tests/data/old-app.txt", fileToImport: "src/api/routes/tests/data/old-app.txt",
}) })
expect(app._id).toBeDefined() expect(app._id).toBeDefined()
expect(app.navigation).toBeDefined() expect(app.navigation).toBeDefined()

View File

@ -357,9 +357,7 @@ export function applicationValidator(opts = { isCreate: true }) {
_id: OPTIONAL_STRING, _id: OPTIONAL_STRING,
_rev: OPTIONAL_STRING, _rev: OPTIONAL_STRING,
url: OPTIONAL_STRING, url: OPTIONAL_STRING,
template: Joi.object({ template: Joi.object({}),
templateString: OPTIONAL_STRING,
}),
} }
const appNameValidator = Joi.string() const appNameValidator = Joi.string()
@ -392,9 +390,7 @@ export function applicationValidator(opts = { isCreate: true }) {
_rev: OPTIONAL_STRING, _rev: OPTIONAL_STRING,
name: appNameValidator, name: appNameValidator,
url: OPTIONAL_STRING, url: OPTIONAL_STRING,
template: Joi.object({ template: Joi.object({}).unknown(true),
templateString: OPTIONAL_STRING,
}).unknown(true),
snippets: snippetValidator, snippets: snippetValidator,
}).unknown(true) }).unknown(true)
) )

View File

@ -17,8 +17,8 @@ export class ApplicationAPI extends TestAPI {
app: CreateAppRequest, app: CreateAppRequest,
expectations?: Expectations expectations?: Expectations
): Promise<App> => { ): Promise<App> => {
const files = app.templateFile ? { templateFile: app.templateFile } : {} const files = app.fileToImport ? { fileToImport: app.fileToImport } : {}
delete app.templateFile delete app.fileToImport
return await this._post<App>("/api/applications", { return await this._post<App>("/api/applications", {
fields: app, fields: app,
files, files,

View File

@ -2,9 +2,6 @@ import env from "../environment"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { generateMetadataID } from "../db/utils" import { generateMetadataID } from "../db/utils"
import { Document } from "@budibase/types" import { Document } from "@budibase/types"
import stream from "stream"
const Readable = stream.Readable
export function wait(ms: number) { export function wait(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms)) return new Promise(resolve => setTimeout(resolve, ms))
@ -98,15 +95,6 @@ export function escapeDangerousCharacters(string: string) {
.replace(/[\t]/g, "\\t") .replace(/[\t]/g, "\\t")
} }
export function stringToReadStream(string: string) {
return new Readable({
read() {
this.push(string)
this.push(null)
},
})
}
export function formatBytes(bytes: string) { export function formatBytes(bytes: string) {
const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
const byteIncrements = 1024 const byteIncrements = 1024

View File

@ -7,10 +7,8 @@ export interface CreateAppRequest {
useTemplate?: string useTemplate?: string
templateName?: string templateName?: string
templateKey?: string templateKey?: string
templateFile?: string fileToImport?: string
includeSampleData?: boolean
encryptionPassword?: string encryptionPassword?: string
templateString?: string
file?: { path: string } file?: { path: string }
} }