Feedback changes. Permission change for app delete from GLOBAL_BUILDER to BUILDER. Minor updates to quota behaviour for apps.
This commit is contained in:
parent
3716054313
commit
320b443ca4
|
@ -56,9 +56,10 @@
|
||||||
disabled={deletionConfirmationAppName !== appName || deleting}
|
disabled={deletionConfirmationAppName !== appName || deleting}
|
||||||
>
|
>
|
||||||
Are you sure you want to delete
|
Are you sure you want to delete
|
||||||
<b class="app-name" role="button" tabindex={-1} on:click={copyName}>
|
<span class="app-name" role="button" tabindex={-1} on:click={copyName}>
|
||||||
{appName}
|
{appName}
|
||||||
</b>?
|
</span>?
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
Please enter the app name below to confirm.
|
Please enter the app name below to confirm.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
@ -68,5 +69,7 @@
|
||||||
<style>
|
<style>
|
||||||
.app-name {
|
.app-name {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -31,17 +31,11 @@
|
||||||
: null}
|
: null}
|
||||||
>
|
>
|
||||||
<Body>
|
<Body>
|
||||||
You are currently on our <span class="free-plan">Free plan</span>. Upgrade
|
You have exceeded the app limit for your current plan. Upgrade to get
|
||||||
to our Pro plan to get unlimited apps and additional features.
|
unlimited apps and additional features!
|
||||||
</Body>
|
</Body>
|
||||||
{#if !$auth.user.accountPortalAccess}
|
{#if !$auth.user.accountPortalAccess}
|
||||||
<Body>Please contact the account holder to upgrade.</Body>
|
<Body>Please contact the account holder to upgrade.</Body>
|
||||||
{/if}
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
|
||||||
.free-plan {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
|
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
|
||||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||||
|
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||||
import ExportAppModal from "./ExportAppModal.svelte"
|
import ExportAppModal from "./ExportAppModal.svelte"
|
||||||
import DuplicateAppModal from "./DuplicateAppModal.svelte"
|
import DuplicateAppModal from "./DuplicateAppModal.svelte"
|
||||||
|
import { auth, licensing } from "stores/portal"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let align = "right"
|
export let align = "right"
|
||||||
|
@ -11,21 +13,32 @@
|
||||||
let exportModal
|
let exportModal
|
||||||
let duplicateModal
|
let duplicateModal
|
||||||
let exportPublishedVersion = false
|
let exportPublishedVersion = false
|
||||||
|
let appLimitModal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
bind:this={deleteModal}
|
bind:this={deleteModal}
|
||||||
appId={app.devId}
|
appId={app.devId}
|
||||||
appName={app.name}
|
appName={app.name}
|
||||||
onDeleteSuccess={() => {}}
|
onDeleteSuccess={async () => {
|
||||||
|
await licensing.init()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AppLimitModal bind:this={appLimitModal} />
|
||||||
|
|
||||||
<Modal bind:this={exportModal} padding={false}>
|
<Modal bind:this={exportModal} padding={false}>
|
||||||
<ExportAppModal {app} published={exportPublishedVersion} />
|
<ExportAppModal {app} published={exportPublishedVersion} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={duplicateModal} padding={false}>
|
<Modal bind:this={duplicateModal} padding={false}>
|
||||||
<DuplicateAppModal appId={app.devId} appName={app.name} />
|
<DuplicateAppModal
|
||||||
|
appId={app.devId}
|
||||||
|
appName={app.name}
|
||||||
|
onDuplicateSuccess={async () => {
|
||||||
|
await licensing.init()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<ActionMenu {align} on:open on:close>
|
<ActionMenu {align} on:open on:close>
|
||||||
|
@ -35,7 +48,11 @@
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Copy"
|
icon="Copy"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
duplicateModal.show()
|
if ($licensing?.usageMetrics?.apps < 100) {
|
||||||
|
duplicateModal.show()
|
||||||
|
} else {
|
||||||
|
appLimitModal.show()
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Duplicate
|
Duplicate
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
export let appId
|
export let appId
|
||||||
export let appName
|
export let appName
|
||||||
|
export let onDuplicateSuccess = () => {}
|
||||||
|
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
const values = writable({ name: appName + " copy", url: null })
|
const values = writable({ name: appName + " copy", url: null })
|
||||||
|
@ -68,6 +69,7 @@
|
||||||
try {
|
try {
|
||||||
await API.duplicateApp(data, appId)
|
await API.duplicateApp(data, appId)
|
||||||
apps.load()
|
apps.load()
|
||||||
|
onDuplicateSuccess()
|
||||||
notifications.success("App duplicated successfully")
|
notifications.success("App duplicated successfully")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error duplicating app")
|
notifications.error("Error duplicating app")
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { url, isActive } from "@roxi/routify"
|
import { url, isActive } from "@roxi/routify"
|
||||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||||
import { isOnlyUser, appStore } from "stores/builder"
|
import { isOnlyUser, appStore } from "stores/builder"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
let deleteModal
|
let deleteModal
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { apps, sideBarCollapsed } from "stores/portal"
|
import { apps, sideBarCollapsed, auth } from "stores/portal"
|
||||||
import { params, goto } from "@roxi/routify"
|
import { params, goto } from "@roxi/routify"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import NavHeader from "components/common/NavHeader.svelte"
|
import NavHeader from "components/common/NavHeader.svelte"
|
||||||
import AppRowContext from "components/start/AppRowContext.svelte"
|
import AppRowContext from "components/start/AppRowContext.svelte"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let searchString
|
let searchString
|
||||||
let opened
|
let opened
|
||||||
|
@ -54,16 +55,18 @@
|
||||||
highlighted={opened == app.appId}
|
highlighted={opened == app.appId}
|
||||||
on:click={() => $goto(`./${app.appId}`)}
|
on:click={() => $goto(`./${app.appId}`)}
|
||||||
>
|
>
|
||||||
<AppRowContext
|
{#if sdk.users.isBuilder($auth.user, app?.devId)}
|
||||||
{app}
|
<AppRowContext
|
||||||
align="left"
|
{app}
|
||||||
on:open={() => {
|
align="left"
|
||||||
opened = app.appId
|
on:open={() => {
|
||||||
}}
|
opened = app.appId
|
||||||
on:close={() => {
|
}}
|
||||||
opened = null
|
on:close={() => {
|
||||||
}}
|
opened = null
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -51,6 +51,8 @@ import {
|
||||||
CreateAppRequest,
|
CreateAppRequest,
|
||||||
FetchAppDefinitionResponse,
|
FetchAppDefinitionResponse,
|
||||||
FetchAppPackageResponse,
|
FetchAppPackageResponse,
|
||||||
|
DuplicateAppRequest,
|
||||||
|
DuplicateAppResponse,
|
||||||
} 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"
|
||||||
|
@ -378,7 +380,7 @@ async function creationEvents(request: any, app: App) {
|
||||||
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 && request.duplicate) {
|
else if (request.body.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))
|
||||||
}
|
}
|
||||||
|
@ -407,7 +409,7 @@ async function appPostCreate(ctx: UserCtx, app: App) {
|
||||||
|
|
||||||
await creationEvents(ctx.request, app)
|
await creationEvents(ctx.request, app)
|
||||||
|
|
||||||
// app import & template creation
|
// app import, template creation and duplication
|
||||||
if (ctx.request.body.useTemplate === "true") {
|
if (ctx.request.body.useTemplate === "true") {
|
||||||
const { rows } = await getUniqueRows([app.appId])
|
const { rows } = await getUniqueRows([app.appId])
|
||||||
const rowCount = rows ? rows.length : 0
|
const rowCount = rows ? rows.length : 0
|
||||||
|
@ -436,7 +438,7 @@ async function appPostCreate(ctx: UserCtx, app: App) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(ctx: UserCtx) {
|
export async function create(ctx: UserCtx<CreateAppRequest, App>) {
|
||||||
const newApplication = await quotas.addApp(() => performAppCreate(ctx))
|
const newApplication = await quotas.addApp(() => performAppCreate(ctx))
|
||||||
await appPostCreate(ctx, newApplication)
|
await appPostCreate(ctx, newApplication)
|
||||||
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
||||||
|
@ -645,7 +647,9 @@ export async function importToApp(ctx: UserCtx) {
|
||||||
* Create a copy of the latest dev application.
|
* Create a copy of the latest dev application.
|
||||||
* Performs an export of the app, then imports from the export dir path
|
* Performs an export of the app, then imports from the export dir path
|
||||||
*/
|
*/
|
||||||
export async function duplicateApp(ctx: UserCtx) {
|
export async function duplicateApp(
|
||||||
|
ctx: UserCtx<DuplicateAppRequest, DuplicateAppResponse>
|
||||||
|
) {
|
||||||
const { name: appName, url: possibleUrl } = ctx.request.body
|
const { name: appName, url: possibleUrl } = ctx.request.body
|
||||||
const { appId: sourceAppId } = ctx.params
|
const { appId: sourceAppId } = ctx.params
|
||||||
const [app] = await dbCore.getAppsByIDs([sourceAppId])
|
const [app] = await dbCore.getAppsByIDs([sourceAppId])
|
||||||
|
@ -665,24 +669,28 @@ export async function duplicateApp(ctx: UserCtx) {
|
||||||
tar: false,
|
tar: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build a create request that triggers an import from the export path above.
|
const createRequestBody: CreateAppRequest = {
|
||||||
const createReq: any = {
|
name: appName,
|
||||||
request: {
|
url: possibleUrl,
|
||||||
body: {
|
useTemplate: "true",
|
||||||
name: appName,
|
// The app export path
|
||||||
url: possibleUrl,
|
file: {
|
||||||
useTemplate: "true",
|
path: tmpPath,
|
||||||
file: {
|
|
||||||
path: tmpPath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Mark the create as a duplicate to kick off the correct event.
|
|
||||||
duplicate: true,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
await create(createReq)
|
// Build a new request
|
||||||
const { body: newApplication } = createReq
|
const createRequest = {
|
||||||
|
roleId: ctx.roleId,
|
||||||
|
user: ctx.user,
|
||||||
|
request: {
|
||||||
|
body: createRequestBody,
|
||||||
|
},
|
||||||
|
} as UserCtx<CreateAppRequest, App>
|
||||||
|
|
||||||
|
// Build the new application
|
||||||
|
await create(createRequest)
|
||||||
|
const { body: newApplication } = createRequest
|
||||||
|
|
||||||
if (!newApplication) {
|
if (!newApplication) {
|
||||||
ctx.throw(500, "There was a problem duplicating the application")
|
ctx.throw(500, "There was a problem duplicating the application")
|
||||||
|
|
|
@ -55,12 +55,12 @@ router
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/applications/:appId",
|
"/api/applications/:appId",
|
||||||
authorized(permissions.GLOBAL_BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
controller.destroy
|
controller.destroy
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/applications/:appId/duplicate",
|
"/api/applications/:appId/duplicate",
|
||||||
authorized(permissions.GLOBAL_BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
controller.duplicateApp
|
controller.duplicateApp
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
|
|
|
@ -95,40 +95,13 @@ describe("/applications", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should reject with a known name", async () => {
|
it("should reject with a known name", async () => {
|
||||||
let createError
|
await config.api.application.create({ name: app.name }, { status: 400 })
|
||||||
|
|
||||||
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 () => {
|
it("should reject with a known url", async () => {
|
||||||
let createError
|
await config.api.application.create(
|
||||||
|
{ name: "made up", url: app?.url! },
|
||||||
const app = config.getApp()
|
{ status: 400 }
|
||||||
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."
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -268,73 +241,60 @@ describe("/applications", () => {
|
||||||
|
|
||||||
describe("POST /api/applications/:appId/duplicate", () => {
|
describe("POST /api/applications/:appId/duplicate", () => {
|
||||||
it("should duplicate an existing app", async () => {
|
it("should duplicate an existing app", async () => {
|
||||||
await config.createApp("to-dupe")
|
const resp = await config.api.application.duplicateApp(
|
||||||
const sourceAppId = config.getProdAppId()
|
app.appId,
|
||||||
|
{
|
||||||
const resp = await config.duplicateApp(sourceAppId, {
|
name: "to-dupe copy",
|
||||||
name: "to-dupe copy",
|
url: "/to-dupe-copy",
|
||||||
url: "/to-dupe-copy",
|
},
|
||||||
})
|
{
|
||||||
|
status: 200,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
expect(events.app.duplicated).toBeCalled()
|
expect(events.app.duplicated).toBeCalled()
|
||||||
expect(resp.duplicateAppId).toBeDefined()
|
expect(resp.duplicateAppId).toBeDefined()
|
||||||
expect(resp.sourceAppId).toEqual(sourceAppId)
|
expect(resp.sourceAppId).toEqual(app.appId)
|
||||||
expect(resp.duplicateAppId).not.toEqual(sourceAppId)
|
expect(resp.duplicateAppId).not.toEqual(app.appId)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should reject an unknown app id with a 404", async () => {
|
it("should reject an unknown app id with a 404", async () => {
|
||||||
let dupeError
|
await config.api.application.duplicateApp(
|
||||||
try {
|
app.appId.slice(0, -1) + "a",
|
||||||
await config.duplicateApp("app_fake", {
|
{
|
||||||
name: "to-dupe copy",
|
name: "to-dupe 123",
|
||||||
url: "/to-dupe-copy",
|
url: "/to-dupe-123",
|
||||||
})
|
},
|
||||||
} catch (err: any) {
|
{
|
||||||
dupeError = err
|
status: 404,
|
||||||
}
|
}
|
||||||
|
)
|
||||||
expect(dupeError).toBeDefined()
|
|
||||||
expect(dupeError.message).toEqual("Error 404 - Source app not found")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should reject with a known name", async () => {
|
it("should reject with a known name", async () => {
|
||||||
await config.createApp("known name")
|
const resp = await config.api.application.duplicateApp(
|
||||||
const sourceAppId = config.getProdAppId()
|
app.appId,
|
||||||
|
{
|
||||||
let dupeError
|
name: app.name,
|
||||||
try {
|
|
||||||
await config.duplicateApp(sourceAppId, {
|
|
||||||
name: "known name",
|
|
||||||
url: "/known-name",
|
url: "/known-name",
|
||||||
})
|
},
|
||||||
} catch (err: any) {
|
{ status: 400 }
|
||||||
dupeError = err
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(dupeError).toBeDefined()
|
|
||||||
expect(dupeError.message).toEqual(
|
|
||||||
"Error 400 - App name is already in use."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expect(resp.message).toEqual("App name is already in use.")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should reject with a known url", async () => {
|
it("should reject with a known url", async () => {
|
||||||
await config.createApp("known-url")
|
const resp = await config.api.application.duplicateApp(
|
||||||
const sourceAppId = config.getProdAppId()
|
app.appId,
|
||||||
|
{
|
||||||
let dupeError
|
|
||||||
try {
|
|
||||||
await config.duplicateApp(sourceAppId, {
|
|
||||||
name: "this is fine",
|
name: "this is fine",
|
||||||
url: "/known-url",
|
url: app.url,
|
||||||
})
|
},
|
||||||
} catch (err: any) {
|
{ status: 400 }
|
||||||
dupeError = err
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(dupeError).toBeDefined()
|
|
||||||
expect(dupeError.message).toEqual(
|
|
||||||
"Error 400 - App URL is already in use."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expect(resp.message).toEqual("App URL is already in use.")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -590,16 +590,6 @@ export default class TestConfiguration {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async duplicateApp(appId: string, fields: object) {
|
|
||||||
return context.doInTenant(
|
|
||||||
this.tenantId!,
|
|
||||||
async () =>
|
|
||||||
await this._req(appController.duplicateApp, fields, {
|
|
||||||
appId,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async publish() {
|
async publish() {
|
||||||
await this._req(deployController.publishApp)
|
await this._req(deployController.publishApp)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
type CreateAppRequest,
|
type CreateAppRequest,
|
||||||
type FetchAppDefinitionResponse,
|
type FetchAppDefinitionResponse,
|
||||||
type FetchAppPackageResponse,
|
type FetchAppPackageResponse,
|
||||||
|
DuplicateAppResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
import { AppStatus } from "../../../db/utils"
|
import { AppStatus } from "../../../db/utils"
|
||||||
|
@ -70,6 +71,22 @@ export class ApplicationAPI extends TestAPI {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicateApp = async (
|
||||||
|
appId: string,
|
||||||
|
fields: object,
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<DuplicateAppResponse> => {
|
||||||
|
let headers = {
|
||||||
|
...this.config.defaultHeaders(),
|
||||||
|
[constants.Header.APP_ID]: appId,
|
||||||
|
}
|
||||||
|
return this._post(`/api/applications/${appId}/duplicate`, {
|
||||||
|
headers,
|
||||||
|
fields,
|
||||||
|
expectations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
getDefinition = async (
|
getDefinition = async (
|
||||||
appId: string,
|
appId: string,
|
||||||
expectations?: Expectations
|
expectations?: Expectations
|
||||||
|
|
|
@ -11,7 +11,18 @@ export interface CreateAppRequest {
|
||||||
includeSampleData?: boolean
|
includeSampleData?: boolean
|
||||||
encryptionPassword?: string
|
encryptionPassword?: string
|
||||||
templateString?: string
|
templateString?: string
|
||||||
file?: any
|
file?: { path: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DuplicateAppRequest {
|
||||||
|
name: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DuplicateAppResponse {
|
||||||
|
duplicateAppId: string
|
||||||
|
sourceAppId: string
|
||||||
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchAppDefinitionResponse {
|
export interface FetchAppDefinitionResponse {
|
||||||
|
|
Loading…
Reference in New Issue