Merge pull request #5786 from Budibase/feature/app-overview-section
Feature/app overview section
This commit is contained in:
commit
92ed60af54
|
@ -40,6 +40,10 @@
|
||||||
padding-left: var(--spacing-xl);
|
padding-left: var(--spacing-xl);
|
||||||
padding-right: var(--spacing-xl);
|
padding-right: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
.paddingX-XXL {
|
||||||
|
padding-left: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-right: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
}
|
||||||
.paddingY-S {
|
.paddingY-S {
|
||||||
padding-top: var(--spacing-s);
|
padding-top: var(--spacing-s);
|
||||||
padding-bottom: var(--spacing-s);
|
padding-bottom: var(--spacing-s);
|
||||||
|
@ -56,6 +60,10 @@
|
||||||
padding-top: var(--spacing-xl);
|
padding-top: var(--spacing-xl);
|
||||||
padding-bottom: var(--spacing-xl);
|
padding-bottom: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
.paddingY-XXL {
|
||||||
|
padding-top: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-bottom: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
}
|
||||||
.gap-XXS {
|
.gap-XXS {
|
||||||
grid-gap: var(--spacing-xs);
|
grid-gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
export let wide = false
|
export let wide = false
|
||||||
export let maxWidth = "80ch"
|
export let maxWidth = "80ch"
|
||||||
|
export let noPadding = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="--max-width: {maxWidth}" class:wide>
|
<div style="--max-width: {maxWidth}" class:wide class:noPadding>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -23,4 +24,9 @@
|
||||||
max-width: none;
|
max-width: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noPadding {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let selected = false
|
export let selected = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let dataCy
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
class:is-selected={selected}
|
class:is-selected={selected}
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
on:click
|
on:click
|
||||||
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
{#if heading}
|
{#if heading}
|
||||||
<h2 class="spectrum-SideNav-heading" id="nav-heading-{heading}">
|
<h2 class="spectrum-SideNav-heading" id="nav-heading-{heading}">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let selected = getContext("tab")
|
let selected = getContext("tab")
|
||||||
let tab
|
let tab_internal
|
||||||
let tabInfo
|
let tabInfo
|
||||||
|
|
||||||
const setTabInfo = () => {
|
const setTabInfo = () => {
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
// We just need to get this off the main thread to fix this, by using
|
// We just need to get this off the main thread to fix this, by using
|
||||||
// a 0ms timeout.
|
// a 0ms timeout.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tabInfo = tab?.getBoundingClientRect()
|
tabInfo = tab_internal?.getBoundingClientRect()
|
||||||
if (tabInfo && $selected.title === title) {
|
if (tabInfo && $selected.title === title) {
|
||||||
$selected.info = tabInfo
|
$selected.info = tabInfo
|
||||||
}
|
}
|
||||||
|
@ -27,14 +27,30 @@
|
||||||
setTabInfo()
|
setTabInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//Ensure that the underline is in the correct location
|
||||||
|
$: {
|
||||||
|
if ($selected.title === title && tab_internal) {
|
||||||
|
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
|
||||||
|
$selected = {
|
||||||
|
...$selected,
|
||||||
|
info: tab_internal.getBoundingClientRect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
$selected = { ...$selected, title, info: tab.getBoundingClientRect() }
|
$selected = {
|
||||||
|
...$selected,
|
||||||
|
title,
|
||||||
|
info: tab_internal.getBoundingClientRect(),
|
||||||
|
}
|
||||||
dispatch("click")
|
dispatch("click")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={tab}
|
bind:this={tab_internal}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
class:is-selected={$selected.title === title}
|
class:is-selected={$selected.title === title}
|
||||||
class="spectrum-Tabs-item"
|
class="spectrum-Tabs-item"
|
||||||
|
|
|
@ -0,0 +1,346 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
import clientPackage from "@budibase/client/package.json"
|
||||||
|
|
||||||
|
filterTests(['all'], () => {
|
||||||
|
context("Application Overview screen", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should be accessible from the applications list", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .title").eq(0)
|
||||||
|
.invoke('attr', 'data-cy')
|
||||||
|
.then(($dataCy) => {
|
||||||
|
const dataCy = $dataCy;
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.location().should((loc) => {
|
||||||
|
expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .title").eq(0)
|
||||||
|
.invoke('attr', 'data-cy')
|
||||||
|
.then(($dataCy) => {
|
||||||
|
const dataCy = $dataCy;
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("View").click({force: true})
|
||||||
|
|
||||||
|
cy.location().should((loc) => {
|
||||||
|
expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find a more suitable place for this.
|
||||||
|
it("Should allow unlocking in the app list", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .lock-status").eq(0).contains("Locked by you").click()
|
||||||
|
|
||||||
|
cy.unlockApp({ owned : true })
|
||||||
|
|
||||||
|
cy.get(".appTable").should("exist")
|
||||||
|
cy.get(".lock-status").should('not.be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow unlocking in the app overview screen", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".lock-status").eq(0).contains("Locked by you").click()
|
||||||
|
|
||||||
|
cy.unlockApp({ owned : true })
|
||||||
|
|
||||||
|
cy.get(".lock-status").should("not.be.visible")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect the deploy state of an app that hasn't been published.", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("be.disabled")
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Overview")
|
||||||
|
cy.get(".overview-tab").should("be.visible")
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-status']").within(() => {
|
||||||
|
cy.get(".status-display").contains("Unpublished")
|
||||||
|
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
cy.get(".status-text").contains("-")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect the app deployment state", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
|
||||||
|
|
||||||
|
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.wait(1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("not.be.disabled")
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-status']").within(() => {
|
||||||
|
cy.get(".status-display").contains("Published")
|
||||||
|
cy.get(".status-display .icon svg[aria-label='GlobeCheck']").should("exist")
|
||||||
|
cy.get(".status-text").contains("Last published a few seconds ago")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect an application that has been unpublished", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
|
||||||
|
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='Globe']")
|
||||||
|
.click({ force: true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
||||||
|
cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']")
|
||||||
|
.click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='unpublish-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".confirm-wrap button").click({ force: true }
|
||||||
|
)})
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-status']").within(() => {
|
||||||
|
cy.get(".status-display").contains("Unpublished")
|
||||||
|
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
cy.get(".status-text").contains("Last published a few seconds ago")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow the editing of the application icon", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".app-logo .edit-hover").should("exist").invoke("show").click()
|
||||||
|
|
||||||
|
cy.customiseAppIcon()
|
||||||
|
|
||||||
|
cy.get(".app-logo")
|
||||||
|
.within(() => {
|
||||||
|
cy.get('[aria-label]').eq(0).children()
|
||||||
|
.should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps')
|
||||||
|
cy.get(".app-icon")
|
||||||
|
.should('have.attr', 'style').and('contains', 'color')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect the last time the application was edited", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".header-right button").contains("Edit").click({ force: true });
|
||||||
|
|
||||||
|
cy.navigateToFrontend()
|
||||||
|
|
||||||
|
cy.addComponent("Elements", "Headline").then(componentId => {
|
||||||
|
cy.getComponent(componentId).should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='edited-by']").within(() => {
|
||||||
|
cy.get(".editor-name").contains("You")
|
||||||
|
cy.get(".last-edit-text").contains("Last edited a few seconds ago")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should reflect application version is up-to-date", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-version']").within(() => {
|
||||||
|
cy.get(".version-status").contains("You're running the latest!")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should navigate to the settings tab when clicking the App Version card header", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Overview")
|
||||||
|
cy.get(".overview-tab").should("be.visible")
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-version'] .dash-card-header").click({ force : true })
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||||
|
cy.get(".settings-tab").should("be.visible")
|
||||||
|
cy.get(".overview-tab").should("not.exist")
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should allow the upgrading of an application, if available.", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
cy.wait(500)
|
||||||
|
|
||||||
|
cy.location().then(loc => {
|
||||||
|
const params = loc.pathname.split("/")
|
||||||
|
const appId = params[params.length - 1]
|
||||||
|
cy.log(appId)
|
||||||
|
//Downgrade the app for the test
|
||||||
|
cy.alterAppVersion(appId, "0.0.1-alpha.0")
|
||||||
|
.then(()=>{
|
||||||
|
cy.reload()
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.log("Current deployment version: " + clientPackage.version)
|
||||||
|
|
||||||
|
cy.get(".version-status a").contains("Update").click()
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||||
|
|
||||||
|
cy.get(".version-section .page-action button").contains("Update").click({ force: true })
|
||||||
|
|
||||||
|
cy.intercept('POST', '**/applications/**/client/update').as('updateVersion')
|
||||||
|
cy.get(".spectrum-Modal.is-open button").contains("Update").click({ force: true })
|
||||||
|
|
||||||
|
cy.wait("@updateVersion")
|
||||||
|
.its('response.statusCode').should('eq', 200)
|
||||||
|
.then(() => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item").contains("Overview").click({ force: true })
|
||||||
|
cy.get(".overview-tab [data-cy='app-version']").within(() => {
|
||||||
|
cy.get(".spectrum-Heading").contains(clientPackage.version)
|
||||||
|
cy.get(".version-status").contains("You're running the latest!")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow editing of the app details.", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".spectrum-Tabs-item").contains("Settings").click()
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||||
|
cy.get(".settings-tab").should("be.visible")
|
||||||
|
|
||||||
|
cy.get(".details-section .page-action button").contains("Edit").click({ force: true })
|
||||||
|
cy.updateAppName("sample name")
|
||||||
|
|
||||||
|
//publish and check its disabled
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
|
||||||
|
|
||||||
|
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.wait(1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
cy.get(".spectrum-Tabs-item").contains("Settings").click()
|
||||||
|
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||||
|
|
||||||
|
cy.get(".details-section .page-action .spectrum-Button").scrollIntoView()
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".details-section .page-action .spectrum-Button").should("be.disabled")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow copying of the published application Id", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.publishApp("sample-name")
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
|
||||||
|
cy.get(".spectrum-Menu-item").contains("Copy App ID").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".spectrum-Toast-content").contains("App ID copied to clipboard.").should("be.visible")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow unpublishing of the application", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
|
||||||
|
cy.get(".spectrum-Menu-item").contains("Unpublish").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get("[data-cy='unpublish-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".confirm-wrap button").click({ force: true }
|
||||||
|
)})
|
||||||
|
|
||||||
|
cy.get(".overview-tab [data-cy='app-status']").within(() => {
|
||||||
|
cy.get(".status-display").contains("Unpublished")
|
||||||
|
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should allow deleting of the application", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".appTable .name").eq(0).click()
|
||||||
|
|
||||||
|
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
|
||||||
|
cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
//The test application was renamed earlier in the spec
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get("input").type("sample name")
|
||||||
|
cy.get(".spectrum-Button--warning").click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.location().should((loc) => {
|
||||||
|
expect(loc.pathname).to.eq('/builder/portal/apps')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable").should("not.exist")
|
||||||
|
|
||||||
|
cy.get(".welcome .container h1").contains("Let's create your first app!")
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
cy.deleteAllApps()
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
|
@ -19,7 +19,7 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
cy.get(".appTable .app-row-actions").eq(0)
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Preview")
|
cy.get(".spectrum-Button").contains("View")
|
||||||
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -29,22 +29,8 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
it("Should publish an application and correctly reflect that", () => {
|
it("Should publish an application and correctly reflect that", () => {
|
||||||
//Assuming the previous test was run and the unpublished app is open in edit mode.
|
//Assuming the previous test was run and the unpublished app is open in edit mode.
|
||||||
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true })
|
|
||||||
|
|
||||||
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible")
|
cy.publishApp("cypress-tests")
|
||||||
.within(() => {
|
|
||||||
cy.get(".spectrum-Button").contains("Publish").click({ force : true })
|
|
||||||
cy.wait(1000)
|
|
||||||
});
|
|
||||||
|
|
||||||
//Verify that the app url is presented correctly to the user
|
|
||||||
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
|
|
||||||
.should("be.visible")
|
|
||||||
.within(() => {
|
|
||||||
let appUrl = Cypress.config().baseUrl + '/app/cypress-tests'
|
|
||||||
cy.get("[data-cy='deployed-app-url'] input").should('have.value', appUrl)
|
|
||||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
|
@ -57,7 +43,7 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
cy.get(".appTable .app-row-actions").eq(0)
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("View app")
|
cy.get(".spectrum-Button").contains("View")
|
||||||
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -66,7 +52,7 @@ filterTests(['all'], () => {
|
||||||
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("[data-cy='publish-popover-action']").should("exist")
|
cy.get("[data-cy='publish-popover-action']").should("exist")
|
||||||
cy.get("button").contains("View app").should("exist")
|
cy.get("button").contains("View").should("exist")
|
||||||
cy.get(".publish-popover-message").should("have.text", "Last published a few seconds ago")
|
cy.get(".publish-popover-message").should("have.text", "Last published a few seconds ago")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -84,7 +70,7 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
cy.get(".appTable .app-row-actions").eq(0)
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("View app")
|
cy.get(".spectrum-Button").contains("View")
|
||||||
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -112,19 +112,9 @@ filterTests(['all'], () => {
|
||||||
cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => {
|
cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => {
|
||||||
cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true })
|
cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Modal")
|
|
||||||
.within(() => {
|
cy.updateAppName(changedName, noName)
|
||||||
if (noName == true) {
|
|
||||||
cy.get("input").clear()
|
|
||||||
cy.get(".spectrum-Dialog-grid").click()
|
|
||||||
.contains("App name must be letters, numbers and spaces only")
|
|
||||||
return cy
|
|
||||||
}
|
|
||||||
cy.get("input").clear()
|
|
||||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
|
||||||
cy.wait(500)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -32,6 +32,15 @@ Cypress.Commands.add("login", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("logOut", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.get(".user-dropdown .avatar > .icon").click({ force: true })
|
||||||
|
cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => {
|
||||||
|
cy.get("li[data-cy='user-logout']").click({ force: true })
|
||||||
|
})
|
||||||
|
cy.wait(2000)
|
||||||
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("closeModal", () => {
|
Cypress.Commands.add("closeModal", () => {
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".close-icon").click()
|
cy.get(".close-icon").click()
|
||||||
|
@ -209,6 +218,109 @@ Cypress.Commands.add("deleteAllApps", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("customiseAppIcon", () => {
|
||||||
|
// Select random icon
|
||||||
|
cy.get(".grid").within(() => {
|
||||||
|
cy.get(".icon-item")
|
||||||
|
.eq(Math.floor(Math.random() * 23) + 1)
|
||||||
|
.click()
|
||||||
|
})
|
||||||
|
// Select random colour
|
||||||
|
cy.get(".fill").click()
|
||||||
|
cy.get(".colors").within(() => {
|
||||||
|
cy.get(".color")
|
||||||
|
.eq(Math.floor(Math.random() * 33) + 1)
|
||||||
|
.click()
|
||||||
|
})
|
||||||
|
cy.intercept("**/applications/**").as("iconChange")
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.wait("@iconChange")
|
||||||
|
cy.get("@iconChange").its("response.statusCode").should("eq", 200)
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("alterAppVersion", (appId, version) => {
|
||||||
|
return cy
|
||||||
|
.request("put", `${Cypress.config().baseUrl}/api/applications/${appId}`, {
|
||||||
|
version: version || "0.0.1-alpha.0",
|
||||||
|
})
|
||||||
|
.then(resp => {
|
||||||
|
expect(resp.status).to.eq(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("updateAppName", (changedName, noName) => {
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
if (noName == true) {
|
||||||
|
cy.get("input").clear()
|
||||||
|
cy.get(".spectrum-Dialog-grid")
|
||||||
|
.click()
|
||||||
|
.contains("App name must be letters, numbers and spaces only")
|
||||||
|
return cy
|
||||||
|
}
|
||||||
|
cy.get("input").clear()
|
||||||
|
cy.get("input")
|
||||||
|
.eq(0)
|
||||||
|
.type(changedName)
|
||||||
|
.should("have.value", changedName)
|
||||||
|
.blur()
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("unlockApp", unlock_config => {
|
||||||
|
let config = { ...unlock_config }
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal .spectrum-Dialog[data-cy='app-lock-modal']")
|
||||||
|
.should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
if (config.owned) {
|
||||||
|
cy.get(".spectrum-Dialog-heading").contains("Locked by you")
|
||||||
|
cy.get(".lock-expiry-body").contains(
|
||||||
|
"This lock will expire in 10 minutes from now"
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept("**/lock").as("unlockApp")
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Release Lock")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait("@unlockApp")
|
||||||
|
cy.get("@unlockApp").its("response.statusCode").should("eq", 200)
|
||||||
|
cy.get("@unlockApp").its("response.body").should("deep.equal", {
|
||||||
|
message: "Lock released successfully.",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
//Show the name ?
|
||||||
|
cy.get(".lock-expiry-body").should("not.be.visible")
|
||||||
|
cy.get(".spectrum-Button").contains("Done")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("publishApp", resolvedAppPath => {
|
||||||
|
//Assumes you have navigated to an application first
|
||||||
|
cy.get(".toprightnav button.spectrum-Button")
|
||||||
|
.contains("Publish")
|
||||||
|
.click({ force: true })
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']")
|
||||||
|
.should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
//Verify that the app url is presented correctly to the user
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
|
||||||
|
.should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
let appUrl = Cypress.config().baseUrl + "/app/" + resolvedAppPath
|
||||||
|
cy.get("[data-cy='deployed-app-url'] input").should("have.value", appUrl)
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createTestApp", () => {
|
Cypress.Commands.add("createTestApp", () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
cy.deleteApp(appName)
|
cy.deleteApp(appName)
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
ModalContent,
|
||||||
|
Modal,
|
||||||
|
notifications,
|
||||||
|
ProgressCircle,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { auth, apps } from "stores/portal"
|
||||||
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
export let buttonSize = "M"
|
||||||
|
|
||||||
|
let APP_DEV_LOCK_SECONDS = 600 //common area for this?
|
||||||
|
let appLockModal
|
||||||
|
let processing = false
|
||||||
|
|
||||||
|
$: lockedBy = app?.lockedBy
|
||||||
|
$: lockedByYou = $auth.user.email === lockedBy?.email
|
||||||
|
|
||||||
|
$: lockIdentifer = `${
|
||||||
|
lockedBy && lockedBy.firstName ? lockedBy?.firstName : lockedBy?.email
|
||||||
|
}`
|
||||||
|
|
||||||
|
$: lockedByHeading =
|
||||||
|
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}`
|
||||||
|
|
||||||
|
const getExpiryDuration = app => {
|
||||||
|
if (!app?.lockedBy?.lockedAt) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
let expiry =
|
||||||
|
new Date(app.lockedBy.lockedAt).getTime() + APP_DEV_LOCK_SECONDS * 1000
|
||||||
|
return expiry - new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseLock = async () => {
|
||||||
|
processing = true
|
||||||
|
if (app) {
|
||||||
|
try {
|
||||||
|
await API.releaseAppLock(app.devId)
|
||||||
|
await apps.load()
|
||||||
|
notifications.success("Lock released successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error releasing lock")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notifications.error("No application is selected")
|
||||||
|
}
|
||||||
|
processing = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="lock-status">
|
||||||
|
{#if lockedBy}
|
||||||
|
<Button
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
icon="LockClosed"
|
||||||
|
size={buttonSize}
|
||||||
|
on:click={() => {
|
||||||
|
appLockModal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="lock-status-text">
|
||||||
|
{lockedByHeading}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:this={appLockModal}>
|
||||||
|
<ModalContent
|
||||||
|
title={lockedByHeading}
|
||||||
|
dataCy={"app-lock-modal"}
|
||||||
|
showConfirmButton={false}
|
||||||
|
showCancelButton={false}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Apps are locked to prevent work from being lost from overlapping changes
|
||||||
|
between your team.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if lockedByYou && getExpiryDuration(app) > 0}
|
||||||
|
<span class="lock-expiry-body">
|
||||||
|
{processStringSync(
|
||||||
|
"This lock will expire in {{ duration time 'millisecond' }} from now",
|
||||||
|
{
|
||||||
|
time: getExpiryDuration(app),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<div class="lock-modal-actions">
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
quiet={lockedBy && lockedByYou}
|
||||||
|
disabled={processing}
|
||||||
|
on:click={() => {
|
||||||
|
appLockModal.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="cancel"
|
||||||
|
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
||||||
|
>
|
||||||
|
</Button>
|
||||||
|
{#if lockedByYou}
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
disabled={processing}
|
||||||
|
on:click={() => {
|
||||||
|
releaseLock()
|
||||||
|
appLockModal.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if processing}
|
||||||
|
<ProgressCircle overBackground={true} size="S" />
|
||||||
|
{:else}
|
||||||
|
<span class="unlock">Release Lock</span>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lock-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.lock-status {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
max-width: 175px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, Detail } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let title = ""
|
||||||
|
export let actionIcon
|
||||||
|
export let action
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
|
$: actionDefined = typeof action === "function"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dash-card" data-cy={dataCy}>
|
||||||
|
<div class="dash-card-header" class:active={actionDefined} on:click={action}>
|
||||||
|
<span class="dash-card-title">
|
||||||
|
<Detail size="M">{title}</Detail>
|
||||||
|
</span>
|
||||||
|
<span class="dash-card-action">
|
||||||
|
{#if actionDefined}
|
||||||
|
<Icon name={actionIcon || "ChevronRight"} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="dash-card-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dash-card {
|
||||||
|
background: var(--spectrum-alias-background-color-primary);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
.dash-card-header {
|
||||||
|
padding: var(--spacing-xl) var(--spectrum-global-dimension-static-size-400);
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.dash-card-body {
|
||||||
|
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2);
|
||||||
|
}
|
||||||
|
.dash-card-title :global(.spectrum-Detail) {
|
||||||
|
color: var(
|
||||||
|
--spectrum-sidenav-heading-text-color,
|
||||||
|
var(--spectrum-global-color-gray-700)
|
||||||
|
);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.dash-card-header.active:hover {
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
import ChooseIconModal from "components/start/ChooseIconModal.svelte"
|
||||||
|
|
||||||
|
export let name
|
||||||
|
export let size
|
||||||
|
export let app
|
||||||
|
|
||||||
|
let iconModal
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="editable-icon">
|
||||||
|
<div
|
||||||
|
class="edit-hover"
|
||||||
|
on:click={() => {
|
||||||
|
iconModal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={"Edit"} size={"L"} />
|
||||||
|
</div>
|
||||||
|
<div class="app-icon">
|
||||||
|
<Icon {name} {size} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChooseIconModal {app} bind:this={iconModal} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editable-icon:hover .app-icon {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.editable-icon {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.editable-icon:hover .edit-hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.edit-hover {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
/* transition: opacity var(--spectrum-global-animation-duration-100) ease; */
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -11,6 +11,16 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import clientPackage from "@budibase/client/package.json"
|
import clientPackage from "@budibase/client/package.json"
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
updateModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
updateModal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
export let hideIcon = false
|
||||||
|
|
||||||
let updateModal
|
let updateModal
|
||||||
|
|
||||||
$: appId = $store.appId
|
$: appId = $store.appId
|
||||||
|
@ -57,9 +67,11 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if !hideIcon}
|
||||||
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
||||||
<Icon name="Refresh" hoverable on:click={updateModal.show} />
|
<Icon name="Refresh" hoverable on:click={updateModal.show} />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<Modal bind:this={updateModal}>
|
<Modal bind:this={updateModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="App version"
|
title="App version"
|
||||||
|
|
|
@ -1,33 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||||
Heading,
|
import AppLockModal from "../common/AppLockModal.svelte"
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
|
||||||
StatusLight,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let exportApp
|
export let exportApp
|
||||||
export let viewApp
|
|
||||||
export let editApp
|
export let editApp
|
||||||
export let updateApp
|
export let updateApp
|
||||||
export let deleteApp
|
export let deleteApp
|
||||||
export let previewApp
|
|
||||||
export let unpublishApp
|
export let unpublishApp
|
||||||
|
export let appOverview
|
||||||
export let releaseLock
|
export let releaseLock
|
||||||
export let editIcon
|
export let editIcon
|
||||||
export let copyAppId
|
export let copyAppId
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title" data-cy={`${app.devId}`}>
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
||||||
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
||||||
</div>
|
</div>
|
||||||
<div class="name" on:click={() => editApp(app)}>
|
<div class="name" on:click={() => appOverview(app)}>
|
||||||
<Heading size="XS">
|
<Heading size="XS">
|
||||||
{app.name}
|
{app.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
@ -44,19 +37,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<StatusLight
|
<AppLockModal {app} buttonSize="S" />
|
||||||
positive={!app.lockedYou && !app.lockedOther}
|
|
||||||
notice={app.lockedYou}
|
|
||||||
negative={app.lockedOther}
|
|
||||||
>
|
|
||||||
{#if app.lockedYou}
|
|
||||||
Locked by you
|
|
||||||
{:else if app.lockedOther}
|
|
||||||
Locked by {app.lockedBy.email}
|
|
||||||
{:else}
|
|
||||||
Open
|
|
||||||
{/if}
|
|
||||||
</StatusLight>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<div class="app-status">
|
<div class="app-status">
|
||||||
|
@ -71,23 +52,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div data-cy={`row_actions_${app.appId}`}>
|
<div data-cy={`row_actions_${app.appId}`}>
|
||||||
<div class="app-row-actions">
|
<div class="app-row-actions">
|
||||||
{#if app.deployed}
|
|
||||||
<Button size="S" secondary quiet on:click={() => viewApp(app)}
|
|
||||||
>View app
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<Button size="S" secondary quiet on:click={() => previewApp(app)}
|
|
||||||
>Preview
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<Button
|
<Button
|
||||||
size="S"
|
size="S"
|
||||||
cta
|
secondary
|
||||||
|
quiet
|
||||||
disabled={app.lockedOther}
|
disabled={app.lockedOther}
|
||||||
on:click={() => editApp(app)}
|
on:click={() => editApp(app)}
|
||||||
>
|
>Edit
|
||||||
Edit
|
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="S" cta on:click={() => appOverview(app)}>View</Button>
|
||||||
</div>
|
</div>
|
||||||
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
||||||
<span slot="control" class="app-row-actions-icon">
|
<span slot="control" class="app-row-actions-icon">
|
||||||
|
@ -123,6 +96,7 @@
|
||||||
}
|
}
|
||||||
.app-status {
|
.app-status {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-s);
|
||||||
grid-template-columns: 24px 100px;
|
grid-template-columns: 24px 100px;
|
||||||
}
|
}
|
||||||
.app-status span.disabled {
|
.app-status span.disabled {
|
||||||
|
|
|
@ -203,7 +203,9 @@
|
||||||
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
|
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
|
||||||
Close developer mode
|
Close developer mode
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon="LogOut" on:click={logout}>Log out</MenuItem>
|
<MenuItem dataCy="user-logout" icon="LogOut" on:click={logout}
|
||||||
|
>Log out
|
||||||
|
</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -332,7 +334,7 @@
|
||||||
|
|
||||||
.mobile-toggle,
|
.mobile-toggle,
|
||||||
.user-dropdown {
|
.user-dropdown {
|
||||||
flex: 1 1 0;
|
flex: 0 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reduce BBUI page padding */
|
/* Reduce BBUI page padding */
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
Body,
|
Body,
|
||||||
Modal,
|
Modal,
|
||||||
Divider,
|
Divider,
|
||||||
|
ActionButton,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
|
@ -60,16 +61,15 @@
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Layout noPadding gap="XL">
|
<Layout noPadding gap="XL">
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<ActionButton
|
||||||
quiet
|
|
||||||
secondary
|
secondary
|
||||||
icon={"ChevronLeft"}
|
icon={"ArrowLeft"}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$goto("../")
|
$goto("../")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</ActionButton>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import {
|
import {
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
Detail,
|
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
@ -11,7 +10,6 @@
|
||||||
notifications,
|
notifications,
|
||||||
Body,
|
Body,
|
||||||
Search,
|
Search,
|
||||||
Divider,
|
|
||||||
Helpers,
|
Helpers,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
|
@ -67,6 +65,9 @@
|
||||||
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$: lockedApps = filteredApps.filter(app => app?.lockedYou || app?.lockedOther)
|
||||||
|
$: unlocked = lockedApps?.length == 0
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
...app,
|
...app,
|
||||||
|
@ -179,8 +180,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewApp = app => {
|
const appOverview = app => {
|
||||||
window.open(`/${app.devId}`)
|
$goto(`../overview/${app.devId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const editApp = app => {
|
const editApp = app => {
|
||||||
|
@ -304,7 +305,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Layout noPadding gap="XL">
|
<Layout noPadding gap="M">
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
|
@ -314,7 +315,39 @@
|
||||||
{welcomeBody}
|
{welcomeBody}
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
{#if !$apps?.length}
|
||||||
|
<div class="buttons">
|
||||||
|
<Button
|
||||||
|
dataCy="create-app-btn"
|
||||||
|
size="M"
|
||||||
|
icon="Add"
|
||||||
|
cta
|
||||||
|
on:click={initiateAppCreation}
|
||||||
|
>
|
||||||
|
{createAppButtonText}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
dataCy="import-app-btn"
|
||||||
|
icon="Import"
|
||||||
|
size="L"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={initiateAppImport}
|
||||||
|
>
|
||||||
|
Import app
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !$apps?.length && $templates?.length}
|
||||||
|
<TemplateDisplay templates={$templates} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if enrichedApps.length}
|
||||||
|
<Layout noPadding gap="L">
|
||||||
|
<div class="title">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button
|
<Button
|
||||||
dataCy="create-app-btn"
|
dataCy="create-app-btn"
|
||||||
|
@ -349,23 +382,6 @@
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Layout gap="S" justifyItems="center">
|
|
||||||
<img class="img-logo img-size" alt="logo" src={Logo} />
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
<Divider size="S" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if !$apps?.length && $templates?.length}
|
|
||||||
<TemplateDisplay templates={$templates} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if enrichedApps.length}
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div class="title">
|
|
||||||
<Detail size="L">Apps</Detail>
|
|
||||||
{#if enrichedApps.length > 1}
|
{#if enrichedApps.length > 1}
|
||||||
<div class="app-actions">
|
<div class="app-actions">
|
||||||
{#if cloud}
|
{#if cloud}
|
||||||
|
@ -397,7 +413,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="appTable">
|
<div class="appTable" class:unlocked>
|
||||||
{#each filteredApps as app (app.appId)}
|
{#each filteredApps as app (app.appId)}
|
||||||
<AppRow
|
<AppRow
|
||||||
{copyAppId}
|
{copyAppId}
|
||||||
|
@ -410,7 +426,7 @@
|
||||||
{exportApp}
|
{exportApp}
|
||||||
{deleteApp}
|
{deleteApp}
|
||||||
{updateApp}
|
{updateApp}
|
||||||
{previewApp}
|
{appOverview}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -471,6 +487,9 @@
|
||||||
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.appTable {
|
||||||
|
border-top: var(--border-light);
|
||||||
|
}
|
||||||
.app-actions {
|
.app-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -478,7 +497,7 @@
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.title .welcome > .buttons {
|
.title .welcome > .buttons {
|
||||||
padding-top: 30px;
|
padding-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -514,6 +533,11 @@
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr auto;
|
grid-template-columns: 1fr 1fr 1fr 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appTable.unlocked {
|
||||||
|
grid-template-columns: 1fr 1fr auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
.appTable :global(> div) {
|
.appTable :global(> div) {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { Layout, Page, notifications, Button } from "@budibase/bbui"
|
import { Layout, Page, notifications, ActionButton } from "@budibase/bbui"
|
||||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { templates } from "stores/portal"
|
import { templates } from "stores/portal"
|
||||||
|
@ -25,16 +25,15 @@
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Layout noPadding gap="XL">
|
<Layout noPadding gap="XL">
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<ActionButton
|
||||||
quiet
|
|
||||||
secondary
|
secondary
|
||||||
icon={"ChevronLeft"}
|
icon={"ArrowLeft"}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$goto("../")
|
$goto("../")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</ActionButton>
|
||||||
</span>
|
</span>
|
||||||
{#if loaded && $templates?.length}
|
{#if loaded && $templates?.length}
|
||||||
<TemplateDisplay templates={$templates} />
|
<TemplateDisplay templates={$templates} />
|
||||||
|
|
|
@ -0,0 +1,413 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Page,
|
||||||
|
Button,
|
||||||
|
ActionButton,
|
||||||
|
ButtonGroup,
|
||||||
|
Heading,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
notifications,
|
||||||
|
ProgressCircle,
|
||||||
|
Input,
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Helpers,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import OverviewTab from "../_components/OverviewTab.svelte"
|
||||||
|
import SettingsTab from "../_components/SettingsTab.svelte"
|
||||||
|
import { API } from "api"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { apps, auth } from "stores/portal"
|
||||||
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import AppLockModal from "components/common/AppLockModal.svelte"
|
||||||
|
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
|
||||||
|
export let application
|
||||||
|
|
||||||
|
let promise = getPackage()
|
||||||
|
let loaded = false
|
||||||
|
let deletionModal
|
||||||
|
let unpublishModal
|
||||||
|
let appName = ""
|
||||||
|
|
||||||
|
// App
|
||||||
|
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||||
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||||
|
|
||||||
|
// Locking
|
||||||
|
$: lockedBy = selectedApp?.lockedBy
|
||||||
|
$: lockedByYou = $auth.user.email === lockedBy?.email
|
||||||
|
$: lockIdentifer = `${
|
||||||
|
lockedBy && Object.prototype.hasOwnProperty.call(lockedBy, "firstName")
|
||||||
|
? lockedBy?.firstName
|
||||||
|
: lockedBy?.email
|
||||||
|
}`
|
||||||
|
|
||||||
|
// App deployments
|
||||||
|
$: deployments = []
|
||||||
|
$: latestDeployments = deployments
|
||||||
|
.filter(
|
||||||
|
deployment =>
|
||||||
|
deployment.status === "SUCCESS" && application === deployment.appId
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||||
|
|
||||||
|
$: isPublished =
|
||||||
|
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
|
||||||
|
|
||||||
|
$: appUrl = `${window.origin}/app${selectedApp?.url}`
|
||||||
|
$: tabs = ["Overview", "Automation History", "Backups", "Settings"]
|
||||||
|
$: selectedTab = "Overview"
|
||||||
|
|
||||||
|
const backToAppList = () => {
|
||||||
|
$goto(`../../../portal/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTabChange = tabKey => {
|
||||||
|
if (tabKey === selectedTab) {
|
||||||
|
return
|
||||||
|
} else if (tabKey && tabs.indexOf(tabKey) > -1) {
|
||||||
|
selectedTab = tabKey
|
||||||
|
} else {
|
||||||
|
notifications.error("Invalid tab key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPackage() {
|
||||||
|
try {
|
||||||
|
const pkg = await API.fetchAppPackage(application)
|
||||||
|
await store.actions.initialise(pkg)
|
||||||
|
loaded = true
|
||||||
|
return pkg
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(`Error initialising app: ${error?.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviewPendingDeployments = (deployments, newDeployments) => {
|
||||||
|
if (deployments.length > 0) {
|
||||||
|
const pending = checkIncomingDeploymentStatus(deployments, newDeployments)
|
||||||
|
if (pending.length) {
|
||||||
|
notifications.warning(
|
||||||
|
"Deployment has been queued and will be processed shortly"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDeployments() {
|
||||||
|
try {
|
||||||
|
const newDeployments = await API.getAppDeployments()
|
||||||
|
reviewPendingDeployments(deployments, newDeployments)
|
||||||
|
return newDeployments
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error fetching deployment history")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewApp = () => {
|
||||||
|
if (isPublished) {
|
||||||
|
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, {
|
||||||
|
appId: $store.appId,
|
||||||
|
eventSource: EventSource.PORTAL,
|
||||||
|
})
|
||||||
|
window.open(appUrl, "_blank")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editApp = app => {
|
||||||
|
if (lockedBy && !lockedByYou) {
|
||||||
|
notifications.warning(
|
||||||
|
`App locked by ${lockIdentifer}. Please allow lock to expire or have them unlock this app.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$goto(`../../../app/${app.devId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyAppId = async app => {
|
||||||
|
await Helpers.copyToClipboard(app.prodId)
|
||||||
|
notifications.success("App ID copied to clipboard.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportApp = app => {
|
||||||
|
const id = isPublished ? app.prodId : app.devId
|
||||||
|
const appName = encodeURIComponent(app.name)
|
||||||
|
window.location = `/api/backups/export?appId=${id}&appname=${appName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpublishApp = app => {
|
||||||
|
selectedApp = app
|
||||||
|
unpublishModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmUnpublishApp = async () => {
|
||||||
|
if (!selectedApp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
analytics.captureEvent(Events.APP.UNPUBLISHED, {
|
||||||
|
appId: selectedApp.appId,
|
||||||
|
})
|
||||||
|
await API.unpublishApp(selectedApp.prodId)
|
||||||
|
await apps.load()
|
||||||
|
notifications.success("App unpublished successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error unpublishing app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteApp = app => {
|
||||||
|
selectedApp = app
|
||||||
|
deletionModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeleteApp = async () => {
|
||||||
|
if (!selectedApp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await API.deleteApp(selectedApp?.devId)
|
||||||
|
backToAppList()
|
||||||
|
notifications.success("App deleted successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error deleting app")
|
||||||
|
}
|
||||||
|
selectedApp = null
|
||||||
|
appName = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
store.actions.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
if (!apps.length) {
|
||||||
|
await apps.load()
|
||||||
|
}
|
||||||
|
await API.syncApp(application)
|
||||||
|
deployments = await fetchDeployments()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error initialising app overview")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="overview-wrap">
|
||||||
|
<Page wide noPadding>
|
||||||
|
{#await promise}
|
||||||
|
<span class="page-header">
|
||||||
|
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
|
||||||
|
Back
|
||||||
|
</ActionButton>
|
||||||
|
</span>
|
||||||
|
<div class="loading">
|
||||||
|
<ProgressCircle size="XL" />
|
||||||
|
</div>
|
||||||
|
{:then _}
|
||||||
|
<Layout paddingX="XXL" paddingY="XXL" gap="XL">
|
||||||
|
<span class="page-header" class:loaded>
|
||||||
|
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
|
||||||
|
Back
|
||||||
|
</ActionButton>
|
||||||
|
</span>
|
||||||
|
<div class="overview-header">
|
||||||
|
<div class="app-title">
|
||||||
|
<div class="app-logo">
|
||||||
|
<div
|
||||||
|
class="app-icon"
|
||||||
|
style="color: {selectedApp?.icon?.color || ''}"
|
||||||
|
>
|
||||||
|
<EditableIcon
|
||||||
|
app={selectedApp}
|
||||||
|
size="XL"
|
||||||
|
name={selectedApp?.icon?.name || "Apps"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-details">
|
||||||
|
<Heading size="M">{selectedApp?.name}</Heading>
|
||||||
|
<div class="app-url">{appUrl}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<AppLockModal app={selectedApp} />
|
||||||
|
<ButtonGroup gap="XS">
|
||||||
|
<Button
|
||||||
|
size="M"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
icon="Globe"
|
||||||
|
disabled={!isPublished}
|
||||||
|
on:click={viewApp}
|
||||||
|
dataCy="view-app"
|
||||||
|
>
|
||||||
|
View app
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="M"
|
||||||
|
cta
|
||||||
|
icon="Edit"
|
||||||
|
disabled={lockedBy && !lockedByYou}
|
||||||
|
on:click={() => {
|
||||||
|
editApp(selectedApp)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Edit</span>
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<ActionMenu align="right" dataCy="app-overview-menu-popover">
|
||||||
|
<span slot="control" class="app-overview-actions-icon">
|
||||||
|
<Icon hoverable name="More" />
|
||||||
|
</span>
|
||||||
|
<MenuItem on:click={() => exportApp(selectedApp)} icon="Download">
|
||||||
|
Export
|
||||||
|
</MenuItem>
|
||||||
|
{#if isPublished}
|
||||||
|
<MenuItem
|
||||||
|
on:click={() => unpublishApp(selectedApp)}
|
||||||
|
icon="GlobeRemove"
|
||||||
|
>
|
||||||
|
Unpublish
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem on:click={() => copyAppId(selectedApp)} icon="Copy">
|
||||||
|
Copy App ID
|
||||||
|
</MenuItem>
|
||||||
|
{/if}
|
||||||
|
{#if !isPublished}
|
||||||
|
<MenuItem on:click={() => deleteApp(selectedApp)} icon="Delete">
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
{/if}
|
||||||
|
</ActionMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<div class="tab-wrap">
|
||||||
|
<Tabs
|
||||||
|
selected={selectedTab}
|
||||||
|
noPadding
|
||||||
|
on:select={e => {
|
||||||
|
selectedTab = e.detail
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab title="Overview">
|
||||||
|
<OverviewTab
|
||||||
|
app={selectedApp}
|
||||||
|
deployments={latestDeployments}
|
||||||
|
navigateTab={handleTabChange}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
{#if false}
|
||||||
|
<Tab title="Automation History">
|
||||||
|
<div class="container">Automation History contents</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Backups">
|
||||||
|
<div class="container">Backups contents</div>
|
||||||
|
</Tab>
|
||||||
|
{/if}
|
||||||
|
<Tab title="Settings">
|
||||||
|
<SettingsTab app={selectedApp} />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={deletionModal}
|
||||||
|
title="Confirm deletion"
|
||||||
|
okText="Delete app"
|
||||||
|
onOk={confirmDeleteApp}
|
||||||
|
onCancel={() => (appName = null)}
|
||||||
|
disabled={appName !== selectedApp?.name}
|
||||||
|
>
|
||||||
|
Are you sure you want to delete the app <b>{selectedApp?.name}</b>?
|
||||||
|
|
||||||
|
<p>Please enter the app name below to confirm.</p>
|
||||||
|
<Input
|
||||||
|
bind:value={appName}
|
||||||
|
data-cy="delete-app-confirmation"
|
||||||
|
placeholder={selectedApp?.name}
|
||||||
|
/>
|
||||||
|
</ConfirmDialog>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={unpublishModal}
|
||||||
|
title="Confirm unpublish"
|
||||||
|
okText="Unpublish app"
|
||||||
|
onOk={confirmUnpublishApp}
|
||||||
|
dataCy={"unpublish-modal"}
|
||||||
|
>
|
||||||
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
|
</ConfirmDialog>
|
||||||
|
{:catch error}
|
||||||
|
<p>Something went wrong: {error.message}</p>
|
||||||
|
{/await}
|
||||||
|
</Page>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-url {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.overview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header.loaded {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-wrap :global(> div > .container),
|
||||||
|
.tab-wrap :global(.spectrum-Tabs) {
|
||||||
|
background-color: var(--background);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.overview-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.overview-wrap :global(.content > *) {
|
||||||
|
padding: calc(var(--spacing-xl) * 1.5) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-title {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.app-details :global(.spectrum-Heading) {
|
||||||
|
line-height: 1em;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.tab-wrap :global(.spectrum-Tabs) {
|
||||||
|
padding-left: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-right: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
padding-left: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-right: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
padding-top: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script>
|
||||||
|
//export let app
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="automation-tab" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.automation-tab {
|
||||||
|
color: pink;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,250 @@
|
||||||
|
<script>
|
||||||
|
import DashCard from "components/common/DashCard.svelte"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
Heading,
|
||||||
|
Link,
|
||||||
|
Avatar,
|
||||||
|
notifications,
|
||||||
|
Layout,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import clientPackage from "@budibase/client/package.json"
|
||||||
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
import { users, auth } from "stores/portal"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
export let deployments
|
||||||
|
export let navigateTab
|
||||||
|
|
||||||
|
const userInit = async () => {
|
||||||
|
try {
|
||||||
|
await users.init()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error getting user list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let userPromise = userInit()
|
||||||
|
|
||||||
|
$: updateAvailable = clientPackage.version !== $store.version
|
||||||
|
$: isPublished = app && app?.status === AppStatus.DEPLOYED
|
||||||
|
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
||||||
|
$: appEditorText = appEditor?.firstName || appEditor?.email
|
||||||
|
$: filteredUsers = !appEditorId
|
||||||
|
? []
|
||||||
|
: $users.filter(user => user._id === appEditorId)
|
||||||
|
|
||||||
|
$: appEditor = filteredUsers.length ? filteredUsers[0] : null
|
||||||
|
|
||||||
|
const getInitials = user => {
|
||||||
|
let initials = ""
|
||||||
|
initials += user.firstName ? user.firstName[0] : ""
|
||||||
|
initials += user.lastName ? user.lastName[0] : ""
|
||||||
|
|
||||||
|
return initials == "" ? user.email[0] : initials
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overview-tab">
|
||||||
|
<Layout paddingX="XXL" paddingY="XXL" gap="XL">
|
||||||
|
<div class="top">
|
||||||
|
<DashCard title={"App Status"} dataCy={"app-status"}>
|
||||||
|
<div class="status-content">
|
||||||
|
<div class="status-display">
|
||||||
|
{#if isPublished}
|
||||||
|
<Icon name="GlobeCheck" size="XL" disabled={false} />
|
||||||
|
<span>Published</span>
|
||||||
|
{:else}
|
||||||
|
<Icon name="GlobeStrike" size="XL" disabled={true} />
|
||||||
|
<span class="disabled"> Unpublished </span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-text">
|
||||||
|
{#if deployments?.length}
|
||||||
|
{processStringSync(
|
||||||
|
"Last published {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() -
|
||||||
|
new Date(deployments[0].updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{/if}
|
||||||
|
{#if !deployments?.length}
|
||||||
|
-
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashCard>
|
||||||
|
<DashCard title={"Last Edited"} dataCy={"edited-by"}>
|
||||||
|
<div class="last-edited-content">
|
||||||
|
{#await userPromise}
|
||||||
|
<Avatar size="M" initials={"-"} />
|
||||||
|
{:then _}
|
||||||
|
<div class="updated-by">
|
||||||
|
{#if appEditor}
|
||||||
|
<Avatar size="M" initials={getInitials(appEditor)} />
|
||||||
|
<div class="editor-name">
|
||||||
|
{appEditor._id === $auth.user._id ? "You" : appEditorText}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:catch error}
|
||||||
|
<p>Could not fetch user: {error.message}</p>
|
||||||
|
{/await}
|
||||||
|
<div class="last-edit-text">
|
||||||
|
{#if app}
|
||||||
|
{processStringSync(
|
||||||
|
"Last edited {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() - new Date(app?.updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashCard>
|
||||||
|
<DashCard
|
||||||
|
title={"App Version"}
|
||||||
|
showIcon={true}
|
||||||
|
action={() => {
|
||||||
|
navigateTab("Settings")
|
||||||
|
}}
|
||||||
|
dataCy={"app-version"}
|
||||||
|
>
|
||||||
|
<div class="version-content" data-cy={$store.version}>
|
||||||
|
<Heading size="XS">{$store.version}</Heading>
|
||||||
|
{#if updateAvailable}
|
||||||
|
<div class="version-status">
|
||||||
|
New version <strong>{clientPackage.version}</strong> is available
|
||||||
|
-
|
||||||
|
<Link
|
||||||
|
on:click={() => {
|
||||||
|
if (typeof navigateTab === "function") {
|
||||||
|
navigateTab("Settings")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="version-status">You're running the latest!</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</DashCard>
|
||||||
|
</div>
|
||||||
|
{#if false}
|
||||||
|
<div class="bottom">
|
||||||
|
<DashCard
|
||||||
|
title={"Automation History"}
|
||||||
|
action={() => {
|
||||||
|
navigateTab("Automation History")
|
||||||
|
}}
|
||||||
|
dataCy={"automation-history"}
|
||||||
|
>
|
||||||
|
<div class="automation-content">
|
||||||
|
<div class="automation-metrics">
|
||||||
|
<div class="succeeded">
|
||||||
|
<Heading size="XL">0</Heading>
|
||||||
|
<div class="metric-info">
|
||||||
|
<Icon name="CheckmarkCircle" />
|
||||||
|
Success
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="failed">
|
||||||
|
<Heading size="XL">0</Heading>
|
||||||
|
<div class="metric-info">
|
||||||
|
<Icon name="Alert" />
|
||||||
|
Error
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashCard>
|
||||||
|
<DashCard
|
||||||
|
title={"Backups"}
|
||||||
|
action={() => {
|
||||||
|
navigateTab("Backups")
|
||||||
|
}}
|
||||||
|
dataCy={"backups"}
|
||||||
|
>
|
||||||
|
<div class="backups-content">test</div>
|
||||||
|
</DashCard>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overview-tab {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-tab .top {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spectrum-alias-grid-gutter-medium);
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-tab .bottom,
|
||||||
|
.automation-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spectrum-alias-grid-gutter-large);
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.overview-tab .top {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.overview-tab .bottom {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.overview-tab .top,
|
||||||
|
.overview-tab .bottom {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.status-text,
|
||||||
|
.last-edit-text {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
.updated-by {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.succeeded :global(.icon) {
|
||||||
|
color: var(--spectrum-global-color-green-600);
|
||||||
|
}
|
||||||
|
.failed :global(.icon) {
|
||||||
|
color: var(
|
||||||
|
--spectrum-semantic-negative-color-default,
|
||||||
|
var(--spectrum-global-color-red-500)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.metric-info {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.version-status,
|
||||||
|
.last-edit-text,
|
||||||
|
.status-text {
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,131 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Divider,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Page,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import clientPackage from "@budibase/client/package.json"
|
||||||
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
|
||||||
|
export let app
|
||||||
|
|
||||||
|
let versionModal
|
||||||
|
let updatingModal
|
||||||
|
let selfHostPath =
|
||||||
|
"https://docs.budibase.com/docs/hosting-methods#self-host-budibase"
|
||||||
|
|
||||||
|
$: updateAvailable = clientPackage.version !== $store.version
|
||||||
|
$: appUrl = `${window.origin}/app${app?.url}`
|
||||||
|
$: appDeployed = app.status === AppStatus.DEPLOYED
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="settings-tab">
|
||||||
|
<Page wide={false}>
|
||||||
|
<Layout gap="XL" paddingY="XXL" paddingX="">
|
||||||
|
<span class="details-section">
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Name and URL</Heading>
|
||||||
|
<Divider />
|
||||||
|
<Body>
|
||||||
|
<div class="app-details">
|
||||||
|
<div class="app-name">
|
||||||
|
<div class="name-title detail-title">Name</div>
|
||||||
|
<div class="name">{app?.name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-url">
|
||||||
|
<div class="url-title detail-title">Url Path</div>
|
||||||
|
<div class="url">{appUrl}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-action">
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
updatingModal.show()
|
||||||
|
}}
|
||||||
|
disabled={appDeployed}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</span>
|
||||||
|
<span class="version-section">
|
||||||
|
<Layout gap="XS" paddingY="XXL" paddingX="">
|
||||||
|
<Heading size="S">App version</Heading>
|
||||||
|
<Divider />
|
||||||
|
<Body>
|
||||||
|
{#if updateAvailable}
|
||||||
|
<p class="version-status">
|
||||||
|
The app is currently using version
|
||||||
|
<strong>{$store.version}</strong>
|
||||||
|
but version <strong>{clientPackage.version}</strong> is available.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="version-status">
|
||||||
|
The app is currently using version
|
||||||
|
<strong>{$store.version}</strong>. You're running the latest!
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
Updates can contain new features, performance improvements and bug
|
||||||
|
fixes.
|
||||||
|
|
||||||
|
<div class="page-action">
|
||||||
|
<Button cta on:click={versionModal.show()}>Update app</Button>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</span>
|
||||||
|
<span class="selfhost-section">
|
||||||
|
<Layout gap="XS" paddingY="XXL" paddingX="">
|
||||||
|
<Heading size="S">Self-host Budibase</Heading>
|
||||||
|
<Divider />
|
||||||
|
<Body>
|
||||||
|
Self-host Budibase for free to get unlimited apps and more - and it
|
||||||
|
only takes a few minutes!
|
||||||
|
<div class="page-action">
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
window.open(selfHostPath, "_blank")
|
||||||
|
}}>Self-host Budibase</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</span>
|
||||||
|
</Layout>
|
||||||
|
<VersionModal bind:this={versionModal} hideIcon={true} />
|
||||||
|
<Modal bind:this={updatingModal} padding={false} width="600px">
|
||||||
|
<UpdateAppModal {app} />
|
||||||
|
</Modal>
|
||||||
|
</Page>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-action {
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.app-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.detail-title {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
font-size: var(
|
||||||
|
--spectrum-alias-font-size-default,
|
||||||
|
var(--spectrum-global-dimension-font-size-100)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,7 +6,7 @@ const {
|
||||||
setDebounce,
|
setDebounce,
|
||||||
} = require("../utilities/redis")
|
} = require("../utilities/redis")
|
||||||
const { doWithDB } = require("@budibase/backend-core/db")
|
const { doWithDB } = require("@budibase/backend-core/db")
|
||||||
const { DocumentTypes } = require("../db/utils")
|
const { DocumentTypes, getGlobalIDFromUserMetadataID } = require("../db/utils")
|
||||||
const { PermissionTypes } = require("@budibase/backend-core/permissions")
|
const { PermissionTypes } = require("@budibase/backend-core/permissions")
|
||||||
const { app: appCache } = require("@budibase/backend-core/cache")
|
const { app: appCache } = require("@budibase/backend-core/cache")
|
||||||
|
|
||||||
|
@ -51,6 +51,9 @@ async function updateAppUpdatedAt(ctx) {
|
||||||
await doWithDB(appId, async db => {
|
await doWithDB(appId, async db => {
|
||||||
const metadata = await db.get(DocumentTypes.APP_METADATA)
|
const metadata = await db.get(DocumentTypes.APP_METADATA)
|
||||||
metadata.updatedAt = new Date().toISOString()
|
metadata.updatedAt = new Date().toISOString()
|
||||||
|
|
||||||
|
metadata.updatedBy = getGlobalIDFromUserMetadataID(ctx.user.userId)
|
||||||
|
|
||||||
const response = await db.put(metadata)
|
const response = await db.put(metadata)
|
||||||
metadata._rev = response.rev
|
metadata._rev = response.rev
|
||||||
await appCache.invalidateAppMetadata(appId, metadata)
|
await appCache.invalidateAppMetadata(appId, metadata)
|
||||||
|
@ -67,7 +70,15 @@ module.exports = async (ctx, permType) => {
|
||||||
}
|
}
|
||||||
const isBuilderApi = permType === PermissionTypes.BUILDER
|
const isBuilderApi = permType === PermissionTypes.BUILDER
|
||||||
const referer = ctx.headers["referer"]
|
const referer = ctx.headers["referer"]
|
||||||
const editingApp = referer ? referer.includes(appId) : false
|
|
||||||
|
const overviewPath = "/builder/portal/overview/"
|
||||||
|
const overviewContext = !referer ? false : referer.includes(overviewPath)
|
||||||
|
if (overviewContext) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAppId = !referer ? false : referer.includes(appId)
|
||||||
|
const editingApp = referer ? hasAppId : false
|
||||||
// check this is a builder call and editing
|
// check this is a builder call and editing
|
||||||
if (!isBuilderApi || !editingApp) {
|
if (!isBuilderApi || !editingApp) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -48,7 +48,9 @@ exports.updateLock = async (devAppId, user) => {
|
||||||
...user,
|
...user,
|
||||||
userId: globalId,
|
userId: globalId,
|
||||||
_id: globalId,
|
_id: globalId,
|
||||||
|
lockedAt: new Date().getTime(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await devAppClient.store(devAppId, inputUser, APP_DEV_LOCK_SECONDS)
|
await devAppClient.store(devAppId, inputUser, APP_DEV_LOCK_SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue