Duplicate app behaviour and test updates
This commit is contained in:
parent
cbb7acbddb
commit
18f09f4e13
|
@ -13,6 +13,7 @@ import {
|
|||
AppVersionRevertedEvent,
|
||||
AppRevertedEvent,
|
||||
AppExportedEvent,
|
||||
AppDuplicatedEvent,
|
||||
} from "@budibase/types"
|
||||
|
||||
const created = async (app: App, timestamp?: string | number) => {
|
||||
|
@ -77,6 +78,17 @@ async function fileImported(app: App) {
|
|||
await publishEvent(Event.APP_FILE_IMPORTED, properties)
|
||||
}
|
||||
|
||||
async function duplicated(app: App, duplicateAppId: string) {
|
||||
const properties: AppDuplicatedEvent = {
|
||||
duplicateAppId,
|
||||
appId: app.appId,
|
||||
audited: {
|
||||
name: app.name,
|
||||
},
|
||||
}
|
||||
await publishEvent(Event.APP_DUPLICATED, properties)
|
||||
}
|
||||
|
||||
async function templateImported(app: App, templateKey: string) {
|
||||
const properties: AppTemplateImportedEvent = {
|
||||
appId: app.appId,
|
||||
|
@ -147,6 +159,7 @@ export default {
|
|||
published,
|
||||
unpublished,
|
||||
fileImported,
|
||||
duplicated,
|
||||
templateImported,
|
||||
versionUpdated,
|
||||
versionReverted,
|
||||
|
|
|
@ -15,6 +15,7 @@ beforeAll(async () => {
|
|||
|
||||
jest.spyOn(events.app, "created")
|
||||
jest.spyOn(events.app, "updated")
|
||||
jest.spyOn(events.app, "duplicated")
|
||||
jest.spyOn(events.app, "deleted")
|
||||
jest.spyOn(events.app, "published")
|
||||
jest.spyOn(events.app, "unpublished")
|
||||
|
|
|
@ -14,3 +14,4 @@ export { default as CoreStepper } from "./Stepper.svelte"
|
|||
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||
export { default as CoreSlider } from "./Slider.svelte"
|
||||
export { default as CoreFile } from "./File.svelte"
|
||||
export { default as CoreEnv } from "./EnvSwitch.svelte"
|
||||
|
|
|
@ -3,9 +3,16 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { apps } from "stores/portal"
|
||||
import { appStore } from "stores/builder"
|
||||
import { API } from "api"
|
||||
|
||||
export let appId
|
||||
export let appName
|
||||
export let onDeleteSuccess = () => {
|
||||
$goto("/builder")
|
||||
}
|
||||
|
||||
let deleting = false
|
||||
|
||||
export const show = () => {
|
||||
deletionModal.show()
|
||||
}
|
||||
|
@ -17,14 +24,24 @@
|
|||
let deletionModal
|
||||
let deletionConfirmationAppName
|
||||
|
||||
const copyName = () => {
|
||||
deletionConfirmationAppName = appName
|
||||
}
|
||||
|
||||
const deleteApp = async () => {
|
||||
if (!appId) {
|
||||
console.log("No app id provided")
|
||||
return
|
||||
}
|
||||
deleting = true
|
||||
try {
|
||||
await API.deleteApp($appStore.appId)
|
||||
await API.deleteApp(appId)
|
||||
apps.load()
|
||||
notifications.success("App deleted successfully")
|
||||
$goto("/builder")
|
||||
onDeleteSuccess()
|
||||
} catch (err) {
|
||||
notifications.error("Error deleting app")
|
||||
deleting = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -35,14 +52,19 @@
|
|||
okText="Delete"
|
||||
onOk={deleteApp}
|
||||
onCancel={() => (deletionConfirmationAppName = null)}
|
||||
disabled={deletionConfirmationAppName !== $appStore.name}
|
||||
disabled={deletionConfirmationAppName !== appName || deleting}
|
||||
>
|
||||
Are you sure you want to delete <b>{$appStore.name}</b>?
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
Are you sure you want to delete
|
||||
<b class="app-name" on:click={copyName}>{appName}</b>?
|
||||
<br />
|
||||
Please enter the app name below to confirm.
|
||||
<br /><br />
|
||||
<Input
|
||||
bind:value={deletionConfirmationAppName}
|
||||
placeholder={$appStore.name}
|
||||
/>
|
||||
<Input bind:value={deletionConfirmationAppName} placeholder={appName} />
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.app-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import AppRowContext from "./AppRowContext.svelte"
|
||||
|
||||
export let app
|
||||
export let lockedAction
|
||||
|
@ -74,12 +75,10 @@
|
|||
|
||||
{#if isBuilder}
|
||||
<div class="app-row-actions">
|
||||
<Button size="S" secondary on:click={lockedAction || goToOverview}>
|
||||
Manage
|
||||
</Button>
|
||||
<Button size="S" primary on:click={lockedAction || goToBuilder}>
|
||||
<Button size="S" secondary on:click={lockedAction || goToBuilder}>
|
||||
Edit
|
||||
</Button>
|
||||
<AppRowContext {app} />
|
||||
</div>
|
||||
{:else if app.deployed}
|
||||
<!-- this can happen if an app builder has app user access to an app -->
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
|
||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||
import ExportAppModal from "./ExportAppModal.svelte"
|
||||
import DuplicateAppModal from "./DuplicateAppModal.svelte"
|
||||
|
||||
export let app
|
||||
|
||||
let deleteModal
|
||||
let exportModal
|
||||
let duplicateModal
|
||||
|
||||
let exportPublishedVersion = false
|
||||
</script>
|
||||
|
||||
<DeleteModal
|
||||
bind:this={deleteModal}
|
||||
appId={app.devId}
|
||||
appName={app.name}
|
||||
onDeleteSuccess={() => {}}
|
||||
/>
|
||||
|
||||
<Modal bind:this={exportModal} padding={false}>
|
||||
<ExportAppModal {app} published={exportPublishedVersion} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={duplicateModal} padding={false}>
|
||||
<DuplicateAppModal appId={app.devId} appName={app.name} />
|
||||
</Modal>
|
||||
|
||||
<ActionMenu align="right">
|
||||
<div slot="control" class="icon">
|
||||
<Icon size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem
|
||||
icon="Copy"
|
||||
on:click={() => {
|
||||
duplicateModal.show()
|
||||
}}
|
||||
>
|
||||
Duplicate
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="Export"
|
||||
on:click={() => {
|
||||
exportPublishedVersion = false
|
||||
exportModal.show()
|
||||
}}
|
||||
>
|
||||
Export latest edited app
|
||||
</MenuItem>
|
||||
{#if app.deployed}
|
||||
<MenuItem
|
||||
icon="Export"
|
||||
on:click={() => {
|
||||
exportPublishedVersion = true
|
||||
exportModal.show()
|
||||
}}
|
||||
>
|
||||
Export latest published app
|
||||
</MenuItem>
|
||||
{/if}
|
||||
<MenuItem
|
||||
icon="Delete"
|
||||
on:click={() => {
|
||||
deleteModal.show()
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
|
@ -0,0 +1,156 @@
|
|||
<script>
|
||||
import {
|
||||
ModalContent,
|
||||
Input,
|
||||
notifications,
|
||||
Layout,
|
||||
keepOpen,
|
||||
} from "@budibase/bbui"
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import { writable, get } from "svelte/store"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
import { apps } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import { API } from "api"
|
||||
|
||||
export let appId
|
||||
export let appName
|
||||
|
||||
const validation = createValidationStore()
|
||||
const values = writable({ name: appName + " copy", url: null })
|
||||
const appPrefix = "/app"
|
||||
|
||||
let defaultAppName = appName + " copy"
|
||||
let duplicating = false
|
||||
|
||||
$: {
|
||||
const { url } = $values
|
||||
|
||||
validation.check({
|
||||
...$values,
|
||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||
})
|
||||
}
|
||||
|
||||
const resolveAppName = name => {
|
||||
return name ? name.trim() : null
|
||||
}
|
||||
|
||||
const resolveAppUrl = name => {
|
||||
let parsedName
|
||||
const resolvedName = resolveAppName(name)
|
||||
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
|
||||
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
|
||||
return encodeURI(parsedUrl)
|
||||
}
|
||||
|
||||
const nameToUrl = appName => {
|
||||
let resolvedUrl = resolveAppUrl(appName)
|
||||
tidyUrl(resolvedUrl)
|
||||
}
|
||||
|
||||
const tidyUrl = url => {
|
||||
if (url && !url.startsWith("/")) {
|
||||
url = `/${url}`
|
||||
}
|
||||
$values.url = url === "" ? null : url
|
||||
}
|
||||
|
||||
const duplicateApp = async () => {
|
||||
duplicating = true
|
||||
|
||||
let data = new FormData()
|
||||
data.append("name", $values.name.trim())
|
||||
if ($values.url) {
|
||||
data.append("url", $values.url.trim())
|
||||
}
|
||||
|
||||
try {
|
||||
await API.duplicateApp(data, appId)
|
||||
apps.load()
|
||||
notifications.success("App duplicated successfully")
|
||||
} catch (err) {
|
||||
notifications.error("Error duplicating app")
|
||||
duplicating = false
|
||||
}
|
||||
}
|
||||
|
||||
const setupValidation = async () => {
|
||||
const applications = get(apps)
|
||||
appValidation.name(validation, { apps: applications })
|
||||
appValidation.url(validation, { apps: applications })
|
||||
|
||||
const { url } = $values
|
||||
validation.check({
|
||||
...$values,
|
||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||
})
|
||||
}
|
||||
|
||||
$: appUrl = `${window.location.origin}${
|
||||
$values.url
|
||||
? `${appPrefix}${$values.url}`
|
||||
: `${appPrefix}${resolveAppUrl($values.name)}`
|
||||
}`
|
||||
|
||||
onMount(async () => {
|
||||
nameToUrl($values.name)
|
||||
await setupValidation()
|
||||
})
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={"Duplicate App"}
|
||||
onConfirm={async () => {
|
||||
validation.check({
|
||||
...$values,
|
||||
})
|
||||
if ($validation.valid) {
|
||||
await duplicateApp()
|
||||
} else {
|
||||
return keepOpen
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Layout gap="S" noPadding>
|
||||
<Input
|
||||
autofocus={true}
|
||||
bind:value={$values.name}
|
||||
disabled={duplicating}
|
||||
error={$validation.touched.name && $validation.errors.name}
|
||||
on:blur={() => ($validation.touched.name = true)}
|
||||
on:change={nameToUrl($values.name)}
|
||||
label="Name"
|
||||
placeholder={defaultAppName}
|
||||
/>
|
||||
<span>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
disabled={duplicating}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
on:change={tidyUrl($values.url)}
|
||||
label="URL"
|
||||
placeholder={$values.url
|
||||
? $values.url
|
||||
: `/${resolveAppUrl($values.name)}`}
|
||||
/>
|
||||
{#if $values.url && $values.url !== "" && !$validation.errors.url}
|
||||
<div class="app-server" title={appUrl}>
|
||||
{appUrl}
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
.app-server {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
margin-top: 10px;
|
||||
width: 320px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
|
@ -121,6 +121,7 @@
|
|||
<Input
|
||||
type="password"
|
||||
label="Password"
|
||||
autocomplete="new-password"
|
||||
placeholder="Type here..."
|
||||
bind:value={password}
|
||||
error={$validation.errors.password}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { Page, Layout, AbsTooltip, TooltipPosition } from "@budibase/bbui"
|
||||
import { url, isActive } from "@roxi/routify"
|
||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||
import { isOnlyUser } from "stores/builder"
|
||||
import { isOnlyUser, appStore } from "stores/builder"
|
||||
|
||||
let deleteModal
|
||||
</script>
|
||||
|
@ -67,7 +67,11 @@
|
|||
</Page>
|
||||
</div>
|
||||
|
||||
<DeleteModal bind:this={deleteModal} />
|
||||
<DeleteModal
|
||||
bind:this={deleteModal}
|
||||
appId={$appStore.appId}
|
||||
appName={$appStore.name}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.delete-action :global(.text) {
|
||||
|
|
|
@ -83,6 +83,18 @@ export const buildAppEndpoints = API => ({
|
|||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Duplicate an existing app
|
||||
* @param app the app to dupe
|
||||
*/
|
||||
duplicateApp: async (app, appId) => {
|
||||
return await API.post({
|
||||
url: `/api/applications/${appId}/duplicate`,
|
||||
body: app,
|
||||
json: false,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an application using an export - the body
|
||||
* should be of type FormData, with a "file" and a "password" if encrypted.
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
env as envCore,
|
||||
ErrorCode,
|
||||
events,
|
||||
HTTPError,
|
||||
migrations,
|
||||
objectStore,
|
||||
roles,
|
||||
|
@ -119,7 +120,7 @@ interface AppTemplate {
|
|||
templateString: string
|
||||
useTemplate: string
|
||||
file?: {
|
||||
type: string
|
||||
type?: string
|
||||
path: string
|
||||
password?: string
|
||||
}
|
||||
|
@ -252,6 +253,10 @@ async function performAppCreate(ctx: UserCtx) {
|
|||
...(ctx.request.files.templateFile as any),
|
||||
password: encryptionPassword,
|
||||
}
|
||||
} else if (typeof ctx.request.body.file?.path === "string") {
|
||||
instanceConfig.file = {
|
||||
path: ctx.request.body.file?.path,
|
||||
}
|
||||
}
|
||||
const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
|
||||
const appId = generateDevAppID(generateAppID(tenantId))
|
||||
|
@ -361,12 +366,20 @@ async function creationEvents(request: any, app: App) {
|
|||
else if (request.files?.templateFile) {
|
||||
creationFns.push(a => events.app.fileImported(a))
|
||||
}
|
||||
// from server file path
|
||||
else if (request.body.file && request.duplicate) {
|
||||
// explicitly pass in the newly created app id
|
||||
creationFns.push(a => events.app.duplicated(a, app.appId))
|
||||
}
|
||||
// unknown
|
||||
else {
|
||||
console.error("Could not determine template creation event")
|
||||
}
|
||||
}
|
||||
creationFns.push(a => events.app.created(a))
|
||||
|
||||
if (!request.duplicate) {
|
||||
creationFns.push(a => events.app.created(a))
|
||||
}
|
||||
|
||||
for (let fn of creationFns) {
|
||||
await fn(app)
|
||||
|
@ -380,7 +393,9 @@ async function appPostCreate(ctx: UserCtx, app: App) {
|
|||
tenantId,
|
||||
appId: app.appId,
|
||||
})
|
||||
|
||||
await creationEvents(ctx.request, app)
|
||||
|
||||
// app import & template creation
|
||||
if (ctx.request.body.useTemplate === "true") {
|
||||
const { rows } = await getUniqueRows([app.appId])
|
||||
|
@ -613,6 +628,61 @@ export async function importToApp(ctx: UserCtx) {
|
|||
ctx.body = { message: "app updated" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of the latest dev application.
|
||||
* Performs an export of the app, then imports from the export dir path
|
||||
*/
|
||||
export async function duplicateApp(ctx: UserCtx) {
|
||||
const { name: appName, url: possibleUrl } = ctx.request.body
|
||||
const { appId: sourceAppId } = ctx.params
|
||||
const [app] = await dbCore.getAppsByIDs([sourceAppId])
|
||||
|
||||
if (!app) {
|
||||
ctx.throw(404, "Source app not found")
|
||||
}
|
||||
|
||||
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
||||
|
||||
checkAppName(ctx, apps, appName)
|
||||
const url = sdk.applications.getAppUrl({ name: appName, url: possibleUrl })
|
||||
checkAppUrl(ctx, apps, url)
|
||||
|
||||
const tmpPath = await sdk.backups.exportApp(sourceAppId, {
|
||||
excludeRows: false,
|
||||
tar: false,
|
||||
})
|
||||
|
||||
// Build a create request that triggers an import from the export path above.
|
||||
const createReq: any = {
|
||||
request: {
|
||||
body: {
|
||||
name: appName,
|
||||
url: possibleUrl,
|
||||
useTemplate: "true",
|
||||
file: {
|
||||
path: tmpPath,
|
||||
},
|
||||
},
|
||||
// Mark the create as a duplicate to kick off the correct event.
|
||||
duplicate: true,
|
||||
},
|
||||
}
|
||||
|
||||
await create(createReq)
|
||||
const { body: newApplication } = createReq
|
||||
|
||||
if (!newApplication) {
|
||||
ctx.throw(500, "There was a problem duplicating the application")
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
message: "app duplicated",
|
||||
duplicateAppId: newApplication?.appId,
|
||||
sourceAppId,
|
||||
}
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
export async function updateAppPackage(appPackage: any, appId: any) {
|
||||
return context.doInAppContext(appId, async () => {
|
||||
const db = context.getAppDB()
|
||||
|
|
|
@ -59,6 +59,11 @@ router
|
|||
authorized(permissions.GLOBAL_BUILDER),
|
||||
controller.destroy
|
||||
)
|
||||
.post(
|
||||
"/api/applications/:appId/duplicate",
|
||||
authorized(permissions.GLOBAL_BUILDER),
|
||||
controller.duplicateApp
|
||||
)
|
||||
.post(
|
||||
"/api/applications/:appId/import",
|
||||
authorized(permissions.BUILDER),
|
||||
|
|
|
@ -107,6 +107,44 @@ describe("/applications", () => {
|
|||
expect(events.app.created).toBeCalledTimes(1)
|
||||
expect(events.app.fileImported).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should reject with a known name", async () => {
|
||||
let createError
|
||||
|
||||
const app = config.getApp()
|
||||
expect(app).toBeDefined()
|
||||
try {
|
||||
await config.createApp(app?.name!)
|
||||
} catch (err: any) {
|
||||
createError = err
|
||||
// Must be reset back to its original value or it breaks all
|
||||
// subsequent tests
|
||||
config.appId = app?.appId!
|
||||
}
|
||||
|
||||
expect(createError).toBeDefined()
|
||||
expect(createError.message).toEqual(
|
||||
"Error 400 - App name is already in use."
|
||||
)
|
||||
})
|
||||
|
||||
it("should reject with a known url", async () => {
|
||||
let createError
|
||||
|
||||
const app = config.getApp()
|
||||
expect(app).toBeDefined()
|
||||
try {
|
||||
await config.createApp("made up", app?.url!)
|
||||
} catch (err: any) {
|
||||
createError = err
|
||||
config.appId = app?.appId!
|
||||
}
|
||||
|
||||
expect(createError).toBeDefined()
|
||||
expect(createError.message).toEqual(
|
||||
"Error 400 - App URL is already in use."
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
|
@ -325,6 +363,63 @@ describe("/applications", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("POST /api/applications/:appId/duplicate", () => {
|
||||
it("should duplicate an existing app", async () => {
|
||||
await config.createApp("to-dupe")
|
||||
const sourceAppId = config.getProdAppId()
|
||||
|
||||
const resp = await request
|
||||
.post(`/api/applications/${sourceAppId}/duplicate`)
|
||||
.field("name", "to-dupe copy")
|
||||
.field("url", "/to-dupe-copy")
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(events.app.duplicated).toBeCalled()
|
||||
expect(resp.body.duplicateAppId).toBeDefined()
|
||||
expect(resp.body.sourceAppId).toEqual(sourceAppId)
|
||||
expect(resp.body.duplicateAppId).not.toEqual(sourceAppId)
|
||||
})
|
||||
|
||||
it("should reject an unknown app id with a 404", async () => {
|
||||
await request
|
||||
.post(`/api/applications/app_1234_not_real/duplicate`)
|
||||
.field("name", "to-dupe copy")
|
||||
.field("url", "/to-dupe-copy")
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(404)
|
||||
})
|
||||
|
||||
it("should reject with a known name", async () => {
|
||||
await config.createApp("known name")
|
||||
const sourceAppId = config.getProdAppId()
|
||||
const resp = await request
|
||||
.post(`/api/applications/${sourceAppId}/duplicate`)
|
||||
.field("name", "known name")
|
||||
.field("url", "/known-name")
|
||||
.set(config.defaultHeaders())
|
||||
.expect(400)
|
||||
|
||||
expect(resp.body.message).toEqual("App name is already in use.")
|
||||
})
|
||||
|
||||
it("should reject with a known url", async () => {
|
||||
await config.createApp("known-url")
|
||||
const sourceAppId = config.getProdAppId()
|
||||
|
||||
const resp = await request
|
||||
.post(`/api/applications/${sourceAppId}/duplicate`)
|
||||
.field("name", "this is fine")
|
||||
.field("url", "/known-url")
|
||||
.set(config.defaultHeaders())
|
||||
.expect(400)
|
||||
|
||||
expect(resp.body.message).toEqual("App URL is already in use.")
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /api/applications/:appId/sync", () => {
|
||||
it("should not sync automation logs", async () => {
|
||||
// setup the apps
|
||||
|
|
|
@ -24,7 +24,7 @@ import tar from "tar"
|
|||
|
||||
type TemplateType = {
|
||||
file?: {
|
||||
type: string
|
||||
type?: string
|
||||
path: string
|
||||
password?: string
|
||||
}
|
||||
|
|
|
@ -550,12 +550,16 @@ export default class TestConfiguration {
|
|||
}
|
||||
|
||||
// APP
|
||||
async createApp(appName: string): Promise<App> {
|
||||
async createApp(appName: string, url?: string): Promise<App> {
|
||||
// create dev app
|
||||
// clear any old app
|
||||
this.appId = null
|
||||
this.app = await context.doInTenant(this.tenantId!, async () => {
|
||||
const app = await this._req({ name: appName }, null, appController.create)
|
||||
const app = await this._req(
|
||||
{ name: appName, url },
|
||||
null,
|
||||
appController.create
|
||||
)
|
||||
this.appId = app.appId!
|
||||
return app
|
||||
})
|
||||
|
|
|
@ -44,6 +44,14 @@ export interface AppFileImportedEvent extends BaseEvent {
|
|||
}
|
||||
}
|
||||
|
||||
export interface AppDuplicatedEvent extends BaseEvent {
|
||||
duplicateAppId: string
|
||||
appId: string
|
||||
audited: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AppTemplateImportedEvent extends BaseEvent {
|
||||
appId: string
|
||||
templateKey: string
|
||||
|
|
|
@ -60,6 +60,7 @@ export enum Event {
|
|||
APP_CREATED = "app:created",
|
||||
APP_UPDATED = "app:updated",
|
||||
APP_DELETED = "app:deleted",
|
||||
APP_DUPLICATED = "app:duplicated",
|
||||
APP_PUBLISHED = "app:published",
|
||||
APP_UNPUBLISHED = "app:unpublished",
|
||||
APP_TEMPLATE_IMPORTED = "app:template:imported",
|
||||
|
@ -259,6 +260,7 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
|
|||
[Event.APP_CREATED]: `App "{{ name }}" created`,
|
||||
[Event.APP_UPDATED]: `App "{{ name }}" updated`,
|
||||
[Event.APP_DELETED]: `App "{{ name }}" deleted`,
|
||||
[Event.APP_DUPLICATED]: `App "{{ name }}" duplicated`,
|
||||
[Event.APP_PUBLISHED]: `App "{{ name }}" published`,
|
||||
[Event.APP_UNPUBLISHED]: `App "{{ name }}" unpublished`,
|
||||
[Event.APP_TEMPLATE_IMPORTED]: `App "{{ name }}" template imported`,
|
||||
|
|
Loading…
Reference in New Issue