Merge pull request #5266 from Budibase/feature/new-screen-addition-ui

Screen workflow updates
This commit is contained in:
deanhannigan 2022-04-25 14:30:36 +01:00 committed by GitHub
commit 80853d7c09
9 changed files with 612 additions and 191 deletions

View File

@ -4,12 +4,44 @@ filterTests(['smoke', 'all'], () => {
context("Auto Screens UI", () => { context("Auto Screens UI", () => {
before(() => { before(() => {
cy.login() cy.login()
cy.createTestApp()
}) })
it("should disable the autogenerated screen options if no sources are available", () => {
cy.createApp("First Test App", false)
cy.closeModal();
cy.contains("Design").click()
cy.get("[aria-label=AddCircle]").click()
cy.get(".spectrum-Modal").within(() => {
cy.get(".item.disabled").contains("Autogenerated screens")
cy.get(".confirm-wrap .spectrum-Button").should('be.disabled')
})
cy.deleteAllApps()
});
it("should not display incompatible sources", () => {
cy.createApp("Test App")
cy.selectExternalDatasource("REST")
cy.selectExternalDatasource("S3")
cy.get(".spectrum-Modal").within(() => {
cy.get(".spectrum-Button").contains("Save and continue to query").click({ force : true })
})
cy.navigateToAutogeneratedModal()
cy.get('.data-source-entry').should('have.length', 1)
cy.get('.data-source-entry')
cy.deleteAllApps()
});
it("should generate internal table screens", () => { it("should generate internal table screens", () => {
// Create autogenerated screens from the internal table cy.createTestApp()
cy.createAutogeneratedScreens(["Cypress Tests"]) // Create Autogenerated screens from the internal table
cy.createDatasourceScreen(["Cypress Tests"])
// Confirm screens have been auto generated // Confirm screens have been auto generated
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true }) cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
cy.get(".nav-items-container").should('contain', 'cypress-tests/:id') cy.get(".nav-items-container").should('contain', 'cypress-tests/:id')
@ -21,8 +53,8 @@ filterTests(['smoke', 'all'], () => {
const initialTable = "Cypress Tests" const initialTable = "Cypress Tests"
const secondTable = "Table Two" const secondTable = "Table Two"
cy.createTable(secondTable) cy.createTable(secondTable)
// Create autogenerated screens from the internal tables // Create Autogenerated screens from the internal tables
cy.createAutogeneratedScreens([initialTable, secondTable]) cy.createDatasourceScreen([initialTable, secondTable])
// Confirm screens have been auto generated // Confirm screens have been auto generated
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true }) cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
// Previously generated tables are suffixed with numbers - as expected // Previously generated tables are suffixed with numbers - as expected
@ -33,6 +65,25 @@ filterTests(['smoke', 'all'], () => {
.and('contain', 'table-two/new/row') .and('contain', 'table-two/new/row')
}) })
it("should generate multiple internal table screens with the same screen access level", () => {
//The tables created in the previous step still exist
cy.createTable("Table Three")
cy.createTable("Table Four")
cy.createDatasourceScreen(["Table Three", "Table Four"], "Admin")
cy.get(".nav-items-container").contains("table-three").click()
cy.get(".nav-items-container").should('contain', 'table-three/:id')
.and('contain', 'table-three/new/row')
cy.get(".nav-items-container").contains("table-four").click()
cy.get(".nav-items-container").should('contain', 'table-four/:id')
.and('contain', 'table-four/new/row')
//The access level should now be set to admin. Previous screens should be filtered.
cy.get(".nav-items-container").contains("table-two").should('not.exist')
cy.get(".nav-items-container").contains("cypress-tests").should('not.exist')
})
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
it("should generate data source screens", () => { it("should generate data source screens", () => {
// Using MySQL data source for testing this // Using MySQL data source for testing this
@ -40,8 +91,9 @@ filterTests(['smoke', 'all'], () => {
// Select & configure MySQL data source // Select & configure MySQL data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.addDatasourceConfig(datasource) cy.addDatasourceConfig(datasource)
// Create autogenerated screens from a MySQL table - MySQL contains books table // Create Autogenerated screens from a MySQL table - MySQL contains books table
cy.createAutogeneratedScreens(["books"]) cy.createDatasourceScreen(["books"])
cy.get(".nav-items-container").contains("books").click() cy.get(".nav-items-container").contains("books").click()
cy.get(".nav-items-container").should('contain', 'books/:id') cy.get(".nav-items-container").should('contain', 'books/:id')
.and('contain', 'books/new/row') .and('contain', 'books/new/row')

View File

@ -26,7 +26,7 @@ filterTests(['smoke', 'all'], () => {
it("should add a URL param binding", () => { it("should add a URL param binding", () => {
const paramName = "foo" const paramName = "foo"
cy.createScreen("Test Param", `/test/:${paramName}`) cy.createScreen(`/test/:${paramName}`)
cy.addComponent("Elements", "Paragraph").then(componentId => { cy.addComponent("Elements", "Paragraph").then(componentId => {
addSettingBinding("text", `URL.${paramName}`) addSettingBinding("text", `URL.${paramName}`)
// The builder preview pages don't have a real URL, so all we can do // The builder preview pages don't have a real URL, so all we can do

View File

@ -9,17 +9,33 @@ filterTests(["smoke", "all"], () => {
}) })
it("Should successfully create a screen", () => { it("Should successfully create a screen", () => {
cy.createScreen("Test Screen", "/test") cy.createScreen("/test")
cy.get(".nav-items-container").within(() => { cy.get(".nav-items-container").within(() => {
cy.contains("/test").should("exist") cy.contains("/test").should("exist")
}) })
}) })
it("Should update the url", () => { it("Should update the url", () => {
cy.createScreen("Test Screen", "test with spaces") cy.createScreen("test with spaces")
cy.get(".nav-items-container").within(() => { cy.get(".nav-items-container").within(() => {
cy.contains("/test-with-spaces").should("exist") cy.contains("/test-with-spaces").should("exist")
}) })
}) })
it("Should create a blank screen with the selected access level", () => {
cy.createScreen("admin only", "Admin")
cy.get(".nav-items-container").within(() => {
cy.contains("/admin-only").should("exist")
})
cy.createScreen("open to all", "Public")
cy.get(".nav-items-container").within(() => {
cy.contains("/open-to-all").should("exist")
//The access level should now be set to admin. Previous screens should be filtered.
cy.get(".nav-item").contains("/test-screen").should("not.exist")
})
})
}) })
}) })

View File

@ -32,7 +32,17 @@ Cypress.Commands.add("login", () => {
}) })
}) })
Cypress.Commands.add("createApp", name => { Cypress.Commands.add("closeModal", () => {
cy.get(".spectrum-Modal").within(() => {
cy.get(".close-icon").click()
cy.wait(500)
})
})
Cypress.Commands.add("createApp", (name, addDefaultTable) => {
const shouldCreateDefaultTable =
typeof addDefaultTable != "boolean" ? true : addDefaultTable
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500) cy.wait(500)
cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
@ -51,7 +61,9 @@ Cypress.Commands.add("createApp", name => {
cy.get(".spectrum-ButtonGroup").contains("Create app").click() cy.get(".spectrum-ButtonGroup").contains("Create app").click()
cy.wait(10000) cy.wait(10000)
}) })
if (shouldCreateDefaultTable) {
cy.createTable("Cypress Tests", true) cy.createTable("Cypress Tests", true)
}
}) })
Cypress.Commands.add("deleteApp", name => { Cypress.Commands.add("deleteApp", name => {
@ -135,7 +147,7 @@ Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.deleteApp(appName) cy.deleteApp(appName)
cy.createApp(appName, "This app is used for Cypress testing.") cy.createApp(appName, "This app is used for Cypress testing.")
cy.createScreen("home", "home") cy.createScreen("home")
}) })
Cypress.Commands.add("createTestTableWithData", () => { Cypress.Commands.add("createTestTableWithData", () => {
@ -275,33 +287,99 @@ Cypress.Commands.add("navigateToDataSection", () => {
cy.contains("Data").click() cy.contains("Data").click()
}) })
Cypress.Commands.add("createScreen", (screenName, route) => { //Blank
Cypress.Commands.add("createScreen", (route, accessLevelLabel) => {
cy.contains("Design").click() cy.contains("Design").click()
cy.get("[aria-label=AddCircle]").click() cy.get("[aria-label=AddCircle]").click()
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get(".item").contains("Blank").click() cy.get(".item").contains("Blank screen").click()
cy.get(".spectrum-Button").contains("Add screens").click({ force: true }) cy.get(".spectrum-Button").contains("Continue").click({ force: true })
cy.wait(500) cy.wait(500)
}) })
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Form-itemField").eq(0).type(screenName) cy.get(".spectrum-Form-itemField").eq(0).type(route)
cy.get(".spectrum-Form-itemField").eq(1).type(route)
cy.get(".spectrum-Button").contains("Continue").click({ force: true }) cy.get(".spectrum-Button").contains("Continue").click({ force: true })
cy.wait(1000) cy.wait(1000)
}) })
cy.get(".spectrum-Modal").within(() => {
if (accessLevelLabel) {
cy.get(".spectrum-Picker-label").click()
cy.wait(500)
cy.contains(accessLevelLabel).click()
}
cy.get(".spectrum-Button").contains("Done").click({ force: true })
})
}) })
Cypress.Commands.add("createAutogeneratedScreens", screenNames => { Cypress.Commands.add(
"createDatasourceScreen",
(datasourceNames, accessLevelLabel) => {
cy.contains("Design").click()
cy.get("[aria-label=AddCircle]").click()
cy.get(".spectrum-Modal").within(() => {
cy.get(".item").contains("Autogenerated screens").click()
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
cy.wait(500)
})
cy.get(".spectrum-Modal [data-cy='data-source-modal']").within(() => {
for (let i = 0; i < datasourceNames.length; i++) {
cy.get(".data-source-entry").contains(datasourceNames[i]).click()
//Ensure the check mark is visible
cy.get(".data-source-entry")
.contains(datasourceNames[i])
.get(".data-source-check")
.should("exist")
}
cy.get(".spectrum-Button").contains("Confirm").click({ force: true })
})
cy.get(".spectrum-Modal").within(() => {
if (accessLevelLabel) {
cy.get(".spectrum-Picker-label").click()
cy.wait(500)
cy.contains(accessLevelLabel).click()
}
cy.get(".spectrum-Button").contains("Done").click({ force: true })
})
cy.contains("Design").click()
}
)
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
// Screen name must already exist within data source // Screen name must already exist within data source
cy.contains("Design").click() cy.contains("Design").click()
cy.get("[aria-label=AddCircle]").click() cy.get("[aria-label=AddCircle]").click()
for (let i = 0; i < screenNames.length; i++) { cy.get(".spectrum-Modal").within(() => {
cy.get(".item").contains(screenNames[i]).click() cy.get(".item").contains("Autogenerated screens").click()
} cy.get(".spectrum-Button").contains("Continue").click({ force: true })
cy.get(".spectrum-Button").contains("Add screens").click({ force: true }) cy.wait(500)
cy.wait(4000) })
}) })
Cypress.Commands.add(
"createAutogeneratedScreens",
(screenNames, accessLevelLabel) => {
cy.navigateToAutogeneratedModal()
for (let i = 0; i < screenNames.length; i++) {
cy.get(".data-source-entry").contains(screenNames[i]).click()
}
cy.get(".spectrum-Modal").within(() => {
if (accessLevelLabel) {
cy.get(".spectrum-Picker-label").click()
cy.wait(500)
cy.contains(accessLevelLabel).click()
}
cy.get(".spectrum-Button").contains("Confirm").click({ force: true })
cy.wait(4000)
})
}
)
Cypress.Commands.add("addRow", values => { Cypress.Commands.add("addRow", values => {
cy.contains("Create row").click() cy.contains("Create row").click()
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {

View File

@ -22,6 +22,7 @@ export const Events = {
}, },
SCREEN: { SCREEN: {
CREATED: "Screen Created", CREATED: "Screen Created",
CREATE_ROLE_UPDATED: "Changed Role On Screen Creation",
}, },
AUTOMATION: { AUTOMATION: {
CREATED: "Automation Created", CREATED: "Automation Created",

View File

@ -0,0 +1,199 @@
<script>
import { store } from "builderStore"
import { ModalContent, Layout, notifications, Icon } from "@budibase/bbui"
import { tables, datasources } from "stores/backend"
import getTemplates from "builderStore/store/screenTemplates"
import ICONS from "../../backend/DatasourceNavigator/icons"
import { IntegrationNames } from "constants"
import { onMount } from "svelte"
export let onCancel
export let onConfirm
export let initalScreens = []
let selectedScreens = [...initalScreens]
const toggleScreenSelection = (table, datasource) => {
if (selectedScreens.find(s => s.table === table.name)) {
selectedScreens = selectedScreens.filter(
screen => screen.table !== table.name
)
} else {
let partialTemplates = getTemplates($store, $tables.list).reduce(
(acc, template) => {
if (template.table === table.name) {
template.datasource = datasource.name
acc.push(template)
}
return acc
},
[]
)
selectedScreens = [...partialTemplates, ...selectedScreens]
}
}
const confirmDatasourceSelection = async () => {
await onConfirm({
templates: selectedScreens,
})
}
$: filteredSources = Array.isArray($datasources.list)
? $datasources.list.reduce((acc, datasource) => {
if (
datasource.source !== IntegrationNames.REST &&
datasource["entities"]
) {
acc.push(datasource)
}
return acc
}, [])
: []
onMount(async () => {
try {
await datasources.fetch()
} catch (error) {
notifications.error("Error fetching datasources")
}
})
</script>
<span data-cy="data-source-modal">
<ModalContent
title="Create CRUD Screens"
confirmText="Confirm"
cancelText="Back"
onConfirm={confirmDatasourceSelection}
{onCancel}
disabled={!selectedScreens.length}
size="L"
>
<Layout noPadding gap="S">
{#each filteredSources as datasource}
<div class="data-source-wrap">
<div class="data-source-header">
<div class="datasource-icon">
<svelte:component
this={ICONS[datasource.source]}
height="24"
width="24"
/>
</div>
<div class="data-source-name">{datasource.name}</div>
</div>
{#if Array.isArray(datasource.entities)}
{#each datasource.entities.filter(table => table._id !== "ta_users") as table}
<div
class="data-source-entry"
class:selected={selectedScreens.find(
x => x.table === table.name
)}
on:click={() => toggleScreenSelection(table, datasource)}
>
<svg
width="16px"
height="16px"
class="spectrum-Icon"
style="color: white"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
{table.name}
{#if selectedScreens.find(x => x.table === table.name)}
<span class="data-source-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
{/each}
{/if}
{#if datasource["entities"] && !Array.isArray(datasource.entities)}
{#each Object.keys(datasource.entities).filter(table => table._id !== "ta_users") as table_key}
<div
class="data-source-entry"
class:selected={selectedScreens.find(
x => x.table === datasource.entities[table_key].name
)}
on:click={() =>
toggleScreenSelection(
datasource.entities[table_key],
datasource
)}
>
<svg
width="16px"
height="16px"
class="spectrum-Icon"
style="color: white"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
{datasource.entities[table_key].name}
{#if selectedScreens.find(x => x.table === datasource.entities[table_key].name)}
<span class="data-source-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
{/each}
{/if}
</div>
{/each}
</Layout>
</ModalContent>
</span>
<style>
.data-source-wrap {
padding-bottom: var(--spectrum-alias-item-padding-s);
display: grid;
grid-gap: var(--spacing-xs);
}
.data-source-header {
display: flex;
align-items: center;
}
.data-source-entry {
cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
background: var(--spectrum-alias-background-color-primary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
border-width: 1px;
display: flex;
align-items: center;
}
.data-source-entry:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.data-source-name {
padding: var(--spectrum-alias-item-padding-s);
min-height: var(--spectrum-icon-size-s);
}
.data-source-entry .data-source-check {
margin-left: auto;
}
.data-source-entry :global(.spectrum-Icon) {
min-width: 16px;
}
.data-source-entry .data-source-check :global(.spectrum-Icon) {
color: var(--spectrum-global-color-green-600);
display: block;
}
</style>

View File

@ -1,117 +1,98 @@
<script> <script>
import { store } from "builderStore"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { import { ModalContent, Body, Layout, Icon, Heading } from "@budibase/bbui"
ModalContent,
Body,
Detail,
Layout,
Icon,
ProgressCircle,
} from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates"
export let onConfirm export let onConfirm
export let onCancel export let onCancel
export let showProgressCircle = false
const blankScreen = "createFromScratch" let autoCreateModeKey = "autoCreate"
let blankScreenModeKey = "blankScreen"
let selectedScreens = [] let selectedScreenMode
let templates = getTemplates($store, $tables.list)
$: blankSelected = selectedScreens?.length === 1
$: autoSelected = selectedScreens?.length > 0 && !blankSelected
const toggleScreenSelection = table => {
if (selectedScreens.find(s => s.table === table.name)) {
selectedScreens = selectedScreens.filter(
screen => screen.table !== table.name
)
} else {
let partialTemplates = getTemplates($store, $tables.list).filter(
template => template.table === table.name
)
selectedScreens = [...partialTemplates, ...selectedScreens]
}
}
const confirmScreenSelection = async () => { const confirmScreenSelection = async () => {
await onConfirm(selectedScreens) await onConfirm(selectedScreenMode)
} }
</script> </script>
<div> <div>
<ModalContent <ModalContent
title="Add screens" title="Add screens"
confirmText="Add screens" confirmText="Continue"
cancelText="Cancel" cancelText="Cancel"
onConfirm={confirmScreenSelection} onConfirm={confirmScreenSelection}
{onCancel} {onCancel}
disabled={!selectedScreens.length} disabled={!selectedScreenMode}
size="L" size="L"
> >
<Body size="S">
Please select the screens you would like to add to your application.
Autogenerated screens come with CRUD functionality.
</Body>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<Detail size="S">Blank screen</Detail>
<div <div
class="item" class="screen-type item"
class:selected={selectedScreens.find(x => x.id.includes(blankScreen))} class:selected={selectedScreenMode == blankScreenModeKey}
on:click={() => on:click={() => {
toggleScreenSelection(templates.find(t => t.id === blankScreen))} selectedScreenMode = blankScreenModeKey
class:disabled={autoSelected} }}
> >
<div data-cy="blank-screen" class="content"> <div data-cy="blank-screen" class="content screen-type-wrap">
<div class="text">Blank</div> <Icon name="WebPage" />
<div class="screen-type-text">
<Heading size="XS">Blank screen</Heading>
<Body size="S">Add a blank screen</Body>
</div>
</div> </div>
<div <div
style="color: var(--spectrum-global-color-green-600); float: right" style="color: var(--spectrum-global-color-green-600); float: right"
> >
{#if selectedScreens.find(x => x.id === blankScreen)} {#if selectedScreenMode == blankScreenModeKey}
<div class="checkmark-spacing"> <div class="checkmark-spacing">
<Icon size="S" name="CheckmarkCircleOutline" /> <Icon size="S" name="CheckmarkCircle" />
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>
{#if $tables.list.filter(table => table._id !== "ta_users").length > 0}
<Detail size="S">Autogenerated Screens</Detail>
{#each $tables.list.filter(table => table._id !== "ta_users") as table}
<div <div
class:disabled={blankSelected} class="screen-type item"
class:selected={selectedScreens.find(x => x.table === table.name)} class:selected={selectedScreenMode == autoCreateModeKey}
on:click={() => toggleScreenSelection(table)} on:click={() => {
class="item" selectedScreenMode = autoCreateModeKey
}}
class:disabled={!$tables.list.filter(table => table._id !== "ta_users")
.length}
> >
<div class="content"> <div data-cy="autogenerated-screens" class="content screen-type-wrap">
<div class="text">{table.name}</div> <Icon name="WebPages" />
<div class="screen-type-text">
<Heading size="XS">Autogenerated screens</Heading>
<Body size="S">
Add autogenerated screens with CRUD functionality to get a working
app quickly! (Requires a data source)
</Body>
</div>
</div> </div>
<div <div
style="color: var(--spectrum-global-color-green-600); float: right" style="color: var(--spectrum-global-color-green-600); float: right"
> >
{#if selectedScreens.find(x => x.table === table.name)} {#if selectedScreenMode == autoCreateModeKey}
<div class="checkmark-spacing"> <div class="checkmark-spacing">
<Icon size="S" name="CheckmarkCircleOutline" /> <Icon size="S" name="CheckmarkCircle" />
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>
{/each}
{/if}
</Layout> </Layout>
<div slot="footer">
{#if showProgressCircle}
<div class="footer-progress"><ProgressCircle size="S" /></div>
{/if}
</div>
</ModalContent> </ModalContent>
</div> </div>
<style> <style>
.screen-type.item {
padding: var(--spectrum-alias-item-padding-xl);
}
.screen-type-wrap {
display: flex;
flex-direction: row;
align-items: center;
}
.disabled { .disabled {
opacity: 0.3; opacity: 0.3;
pointer-events: none; pointer-events: none;
@ -119,22 +100,9 @@
.checkmark-spacing { .checkmark-spacing {
margin-right: var(--spacing-m); margin-right: var(--spacing-m);
} }
.content { .content {
letter-spacing: 0px; letter-spacing: 0px;
} }
.footer-progress {
margin-top: var(--spacing-s);
}
.text {
font-weight: 600;
margin-left: var(--spacing-m);
font-size: 14px;
text-transform: capitalize;
}
.item { .item {
cursor: pointer; cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall); grid-gap: var(--spectrum-alias-grid-margin-xsmall);
@ -143,16 +111,22 @@
transition: 0.3s all; transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px; border-radius: 4px;
box-sizing: border-box;
border-width: 1px; border-width: 1px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 60px;
} }
.item:hover, .item:hover,
.selected { .selected {
background: var(--spectrum-alias-background-color-tertiary); background: var(--spectrum-alias-background-color-tertiary);
} }
.screen-type-wrap .screen-type-text {
padding-left: var(--spectrum-alias-item-padding-xl);
}
.screen-type-wrap :global(.spectrum-Icon) {
min-width: var(--spectrum-icon-size-m);
}
.screen-type-wrap :global(.spectrum-Heading) {
padding-bottom: var(--spectrum-alias-item-padding-s);
}
</style> </style>

View File

@ -1,18 +1,23 @@
<script> <script>
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui" import { ModalContent, Input } from "@budibase/bbui"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { selectedAccessRole, allScreens } from "builderStore" import { selectedAccessRole, allScreens } from "builderStore"
import { get } from "svelte/store" import { get } from "svelte/store"
export let onConfirm export let onConfirm
export let onCancel export let onCancel
export let showProgressCircle = false
export let screenName
export let screenUrl export let screenUrl
export let confirmText = "Continue" export let confirmText = "Continue"
let routeError let routeError
let touched = false let touched = false
let screenAccessRole = $selectedAccessRole + ""
const appPrefix = "/app"
$: appUrl = screenUrl
? `${window.location.origin}${appPrefix}${screenUrl}`
: `${window.location.origin}${appPrefix}`
const routeChanged = event => { const routeChanged = event => {
if (!event.detail.startsWith("/")) { if (!event.detail.startsWith("/")) {
@ -38,7 +43,6 @@
const confirmScreenDetails = async () => { const confirmScreenDetails = async () => {
await onConfirm({ await onConfirm({
screenName,
screenUrl, screenUrl,
}) })
} }
@ -51,24 +55,25 @@
onConfirm={confirmScreenDetails} onConfirm={confirmScreenDetails}
{onCancel} {onCancel}
cancelText={"Back"} cancelText={"Back"}
disabled={!screenName || !screenUrl || routeError || !touched} disabled={!screenAccessRole || !screenUrl || routeError || !touched}
> >
<Input label="Name" bind:value={screenName} />
<Input <Input
label="URL" label="Enter a URL for the new screen"
error={routeError} error={routeError}
bind:value={screenUrl} bind:value={screenUrl}
on:change={routeChanged} on:change={routeChanged}
/> />
<div slot="footer"> <div class="app-server" title={appUrl}>
{#if showProgressCircle} {appUrl}
<div class="footer-progress"><ProgressCircle size="S" /></div>
{/if}
</div> </div>
</ModalContent> </ModalContent>
<style> <style>
.footer-progress { .app-server {
margin-top: var(--spacing-s); color: var(--spectrum-global-color-gray-600);
width: 320px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
</style> </style>

View File

@ -1,34 +1,46 @@
<script> <script>
import ScreenDetailsModal from "components/design/NavigationPanel/ScreenDetailsModal.svelte" import ScreenDetailsModal from "components/design/NavigationPanel/ScreenDetailsModal.svelte"
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte" import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
import DatasourceModal from "components/design/NavigationPanel/DatasourceModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { Modal, notifications } from "@budibase/bbui" import { Modal, ModalContent, Select, notifications } from "@budibase/bbui"
import { store, selectedAccessRole } from "builderStore" import { store, selectedAccessRole } from "builderStore"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import { get } from "svelte/store" import { get } from "svelte/store"
import getTemplates from "builderStore/store/screenTemplates"
import { tables, roles } from "stores/backend"
let pendingScreen let pendingScreen
let showProgressCircle = false
// Modal refs // Modal refs
let newScreenModal let newScreenModal
let screenDetailsModal let screenDetailsModal
let datasourceModal
let screenAccessRoleModal
// Cache variables for workflow
let screenAccessRole = $selectedAccessRole + ""
let selectedTemplates = null
let blankScreenUrl = null
let screenMode = null
// External handler to show the screen wizard // External handler to show the screen wizard
export const showModal = () => { export const showModal = () => {
newScreenModal.show() selectedTemplates = null
blankScreenUrl = null
screenMode = null
newScreenModal.show()
// Reset state when showing modal again // Reset state when showing modal again
pendingScreen = null pendingScreen = null
showProgressCircle = false
} }
// Creates an array of screens, checking and sanitising their URLs // Creates an array of screens, checking and sanitising their URLs
const createScreens = async screens => { const createScreens = async ({ screens, screenAccessRole }) => {
if (!screens?.length) { if (!screens?.length) {
return return
} }
showProgressCircle = true
try { try {
for (let screen of screens) { for (let screen of screens) {
@ -46,7 +58,9 @@
screen.routing.route = sanitizeUrl(screen.routing.route) screen.routing.route = sanitizeUrl(screen.routing.route)
// Use the currently selected role // Use the currently selected role
screen.routing.roleId = get(selectedAccessRole) || "BASIC" screen.routing.roleId = screenAccessRole
? screenAccessRole
: get(selectedAccessRole) || "BASIC"
// Create the screen // Create the screen
await store.actions.screens.save(screen) await store.actions.screens.save(screen)
@ -55,6 +69,8 @@
if (screen.template) { if (screen.template) {
analytics.captureEvent(Events.SCREEN.CREATED, { analytics.captureEvent(Events.SCREEN.CREATED, {
template: screen.template, template: screen.template,
datasource: screen.datasource,
screenAccessRole,
}) })
} }
@ -69,8 +85,6 @@
} catch (error) { } catch (error) {
notifications.error("Error creating screens") notifications.error("Error creating screens")
} }
showProgressCircle = false
} }
// Checks if any screens exist in the store with the given route and // Checks if any screens exist in the store with the given route and
@ -98,38 +112,120 @@
} }
// Handler for NewScreenModal // Handler for NewScreenModal
const confirmScreenSelection = async templates => { const confirmScreenSelection = async mode => {
// Handle template selection screenMode = mode
if (templates?.length > 1) {
// Autoscreens, so create immediately if (mode == "autoCreate") {
const screens = templates.map(template => template.create()) datasourceModal.show()
await createScreens(screens)
} else { } else {
// Empty screen, so proceed to the next modal let templates = getTemplates($store, $tables.list)
pendingScreen = templates[0].create() const blankScreenTemplate = templates.find(
t => t.id === "createFromScratch"
)
pendingScreen = blankScreenTemplate.create()
screenDetailsModal.show() screenDetailsModal.show()
} }
} }
// Handler for ScreenDetailsModal // Handler for DatasourceModal confirmation, move to screen access select
const confirmScreenDetails = async ({ screenName, screenUrl }) => { const confirmScreenDatasources = async ({ templates }) => {
selectedTemplates = templates
screenAccessRoleModal.show()
}
// Handler for Datasource Screen Creation
const completeDatasourceScreenCreation = async () => {
// // Handle template selection
if (selectedTemplates?.length > 1) {
// Autoscreens, so create immediately
const screens = selectedTemplates.map(template => {
let screenTemplate = template.create()
screenTemplate.datasource = template.datasource
return screenTemplate
})
await createScreens({ screens, screenAccessRole })
}
}
const confirmScreenBlank = async ({ screenUrl }) => {
blankScreenUrl = screenUrl
screenAccessRoleModal.show()
}
// Submit request for a blank screen
const confirmBlankScreenCreation = async ({
screenUrl,
screenAccessRole,
}) => {
if (!pendingScreen) { if (!pendingScreen) {
return return
} }
pendingScreen.props._instanceName = screenName
pendingScreen.routing.route = screenUrl pendingScreen.routing.route = screenUrl
await createScreens([pendingScreen]) await createScreens({ screens: [pendingScreen], screenAccessRole })
}
// Submit screen config for creation.
const confirmScreenCreation = async () => {
if (screenMode === "blankScreen") {
confirmBlankScreenCreation({
screenUrl: blankScreenUrl,
screenAccessRole,
})
} else {
completeDatasourceScreenCreation()
}
}
const roleSelectBack = () => {
if (screenMode === "blankScreen") {
screenDetailsModal.show()
} else {
datasourceModal.show()
}
} }
</script> </script>
<Modal bind:this={newScreenModal}> <Modal bind:this={newScreenModal}>
<NewScreenModal onConfirm={confirmScreenSelection} {showProgressCircle} /> <NewScreenModal onConfirm={confirmScreenSelection} />
</Modal>
<Modal bind:this={datasourceModal}>
<DatasourceModal
onConfirm={confirmScreenDatasources}
onCancel={() => newScreenModal.show()}
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
/>
</Modal>
<Modal bind:this={screenAccessRoleModal}>
<ModalContent
title={"Create CRUD Screens"}
confirmText={"Done"}
cancelText={"Back"}
onConfirm={confirmScreenCreation}
onCancel={roleSelectBack}
>
Select which level of access you want your screens to have
<Select
bind:value={screenAccessRole}
on:change={() => {
analytics.captureEvent(Events.SCREEN.CREATE_ROLE_UPDATED, {
screenAccessRole,
})
}}
label="Access"
getOptionLabel={role => role.name}
getOptionValue={role => role._id}
getOptionColor={role => role.color}
options={$roles}
/>
</ModalContent>
</Modal> </Modal>
<Modal bind:this={screenDetailsModal}> <Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal <ScreenDetailsModal
{showProgressCircle} onConfirm={confirmScreenBlank}
onConfirm={confirmScreenDetails}
onCancel={() => newScreenModal.show()} onCancel={() => newScreenModal.show()}
initialUrl={blankScreenUrl}
/> />
</Modal> </Modal>