[BUDI-8460] Add New Step to Table Screen Template (#14203)

* [BUDI-8460] Add New Step to Table Screen Template Flow

* fix tests

* PR Feedback

---------

Co-authored-by: Andrew Kingston <andrew@kingston.dev>
This commit is contained in:
Gerard Burns 2024-08-19 11:44:09 +01:00 committed by GitHub
parent 23fd4bbbcb
commit 205a50156b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1297 additions and 546 deletions

View File

@ -6,8 +6,8 @@
export let onConfirm
export let onCancel
export let screenUrl
export let screenRole
export let route
export let role
export let confirmText = "Continue"
const appPrefix = "/app"
@ -15,17 +15,17 @@
let error
let modal
$: appUrl = screenUrl
? `${window.location.origin}${appPrefix}${screenUrl}`
$: appUrl = route
? `${window.location.origin}${appPrefix}${route}`
: `${window.location.origin}${appPrefix}`
const routeChanged = event => {
if (!event.detail.startsWith("/")) {
screenUrl = "/" + event.detail
route = "/" + event.detail
}
touched = true
screenUrl = sanitizeUrl(screenUrl)
if (routeExists(screenUrl)) {
route = sanitizeUrl(route)
if (routeExists(route)) {
error = "This URL is already taken for this access role"
} else {
error = null
@ -33,19 +33,19 @@
}
const routeExists = url => {
if (!screenRole) {
if (!role) {
return false
}
return get(screenStore).screens.some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === screenRole
screen.routing.roleId === role
)
}
const confirmScreenDetails = async () => {
await onConfirm({
screenUrl,
route,
})
}
</script>
@ -58,13 +58,13 @@
onConfirm={confirmScreenDetails}
{onCancel}
cancelText={"Back"}
disabled={!screenUrl || error || !touched}
disabled={!route || error || !touched}
>
<form on:submit|preventDefault={() => modal.confirm()}>
<Input
label="Enter a URL for the new screen"
{error}
bind:value={screenUrl}
bind:value={route}
on:change={routeChanged}
/>
<div class="app-server" title={appUrl}>

View File

@ -34,6 +34,7 @@
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import DataSourceCategory from "components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
import { API } from "api"
import { datasourceSelect as format } from "helpers/data/format"
export let value = {}
export let otherSources
@ -51,24 +52,15 @@
let modal
$: text = value?.label ?? "Choose an option"
$: tables = $tablesStore.list.map(m => ({
label: m.name,
tableId: m._id,
type: "table",
datasource: $datasources.list.find(
ds => ds._id === m.sourceId || m.datasourceId
),
}))
$: tables = $tablesStore.list.map(table =>
format.table(table, $datasources.list)
)
$: viewsV1 = $viewsStore.list.map(view => ({
...view,
label: view.name,
type: "view",
}))
$: viewsV2 = $viewsV2Store.list.map(view => ({
...view,
label: view.name,
type: "viewV2",
}))
$: viewsV2 = $viewsV2Store.list.map(format.viewV2)
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
$: queries = $queriesStore.list
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)

View File

@ -2,24 +2,14 @@
import { Select } from "@budibase/bbui"
import { createEventDispatcher, onMount } from "svelte"
import { tables as tablesStore, viewsV2 } from "stores/builder"
import { tableSelect as format } from "helpers/data/format"
export let value
const dispatch = createEventDispatcher()
$: tables = $tablesStore.list.map(table => ({
type: "table",
label: table.name,
tableId: table._id,
resourceId: table._id,
}))
$: views = $viewsV2.list.map(view => ({
type: "viewV2",
id: view.id,
label: view.name,
tableId: view.tableId,
resourceId: view.id,
}))
$: tables = $tablesStore.list.map(format.table)
$: views = $viewsV2.list.map(format.viewV2)
$: options = [...(tables || []), ...(views || [])]
const onChange = e => {

View File

@ -0,0 +1,31 @@
export const datasourceSelect = {
table: (table, datasources) => ({
label: table.name,
tableId: table._id,
type: "table",
datasource: datasources.find(
datasource => datasource._id === table.sourceId || table.datasourceId
),
}),
viewV2: view => ({
...view,
label: view.name,
type: "viewV2",
}),
}
export const tableSelect = {
table: table => ({
type: "table",
label: table.name,
tableId: table._id,
resourceId: table._id,
}),
viewV2: view => ({
type: "viewV2",
id: view.id,
label: view.name,
tableId: view.tableId,
resourceId: view.id,
}),
}

View File

@ -20,7 +20,7 @@
let confirmDeleteDialog
let screenDetailsModal
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
const createDuplicateScreen = async ({ route }) => {
// Create a dupe and ensure it is unique
let duplicateScreen = Helpers.cloneDeep(screen)
delete duplicateScreen._id
@ -28,9 +28,8 @@
duplicateScreen.props = makeComponentUnique(duplicateScreen.props)
// Attach the new name and URL
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
duplicateScreen.routing.route = sanitizeUrl(route)
duplicateScreen.routing.homeScreen = false
duplicateScreen.props._instanceName = screenName
try {
// Create the screen
@ -136,8 +135,8 @@
<Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal
onConfirm={createDuplicateScreen}
screenUrl={screen?.routing.route}
screenRole={screen?.routing.roleId}
route={screen?.routing.route}
role={screen?.routing.roleId}
confirmText="Duplicate"
/>
</Modal>

View File

@ -1,8 +1,9 @@
<script>
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import DatasourceModal from "./DatasourceModal.svelte"
import sanitizeUrl from "helpers/sanitizeUrl"
import FormTypeModal from "./FormTypeModal.svelte"
import TypeModal from "./TypeModal.svelte"
import tableTypes from "./tableTypes"
import formTypes from "./formTypes"
import { Modal, notifications } from "@budibase/bbui"
import {
screenStore,
@ -11,14 +12,9 @@
builderStore,
} from "stores/builder"
import { auth } from "stores/portal"
import { get } from "svelte/store"
import { capitalise } from "helpers"
import { goto } from "@roxi/routify"
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
import blankScreen from "templates/blankScreen"
import formScreen from "templates/formScreen"
import gridScreen from "templates/gridScreen"
import gridDetailsScreen from "templates/gridDetailsScreen"
import * as screenTemplating from "templates/screenTemplating"
import { Roles } from "constants/backend"
let mode
@ -26,16 +22,19 @@
let screenDetailsModal
let datasourceModal
let formTypeModal
let tableTypeModal
let selectedTablesAndViews = []
let permissions = {}
$: screens = $screenStore.screens
export const show = newMode => {
mode = newMode
selectedTablesAndViews = []
permissions = {}
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
if (mode === "table" || mode === "form") {
datasourceModal.show()
} else if (mode === "blank") {
screenDetailsModal.show()
@ -44,136 +43,83 @@
}
}
const createScreen = async screen => {
const createScreen = async screenTemplate => {
try {
// Check we aren't clashing with an existing URL
if (hasExistingUrl(screen.routing.route, screen.routing.roleId)) {
let suffix = 2
let candidateUrl = makeCandidateUrl(screen, suffix)
while (hasExistingUrl(candidateUrl, screen.routing.roleId)) {
candidateUrl = makeCandidateUrl(screen, ++suffix)
}
screen.routing.route = candidateUrl
}
screen.routing.route = sanitizeUrl(screen.routing.route)
return await screenStore.save(screen)
return await screenStore.save(screenTemplate)
} catch (error) {
console.error(error)
notifications.error("Error creating screens")
}
}
const addNavigationLink = async screen =>
await navigationStore.saveLink(
screen.routing.route,
capitalise(screen.routing.route.split("/")[1]),
screen.routing.roleId
)
const createScreens = async screenTemplates => {
const newScreens = []
// Checks if any screens exist in the store with the given route and
// currently selected role
const hasExistingUrl = (url, screenAccessRole) => {
const screens = get(screenStore).screens.filter(
s => s.routing.roleId === screenAccessRole
)
return !!screens.find(s => s.routing?.route === url)
for (let screenTemplate of screenTemplates) {
await addNavigationLink(
screenTemplate.data,
screenTemplate.navigationLinkLabel
)
newScreens.push(await createScreen(screenTemplate.data))
}
return newScreens
}
// Constructs a candidate URL for a new screen, appending a given suffix to the
// screen's URL
// e.g. "/sales/:id" => "/sales-1/:id"
const makeCandidateUrl = (screen, suffix) => {
let url = screen.routing?.route || ""
if (url.startsWith("/")) {
url = url.slice(1)
}
if (!url.includes("/")) {
return `/${url}-${suffix}`
} else {
const split = url.split("/")
return `/${split[0]}-${suffix}/${split.slice(1).join("/")}`
}
const addNavigationLink = async (screen, linkLabel) => {
if (linkLabel == null) return
await navigationStore.saveLink(
screen.routing.route,
linkLabel,
screen.routing.roleId
)
}
const onSelectDatasources = async () => {
if (mode === "form") {
formTypeModal.show()
} else if (mode === "grid") {
await createGridScreen()
} else if (mode === "gridDetails") {
await createGridDetailsScreen()
} else if (mode === "table") {
tableTypeModal.show()
}
}
const createBlankScreen = async ({ screenUrl }) => {
const screenTemplate = blankScreen(screenUrl)
const screen = await createScreen(screenTemplate)
await addNavigationLink(screenTemplate)
const createBlankScreen = async ({ route }) => {
const screenTemplates = screenTemplating.blank({ route, screens })
loadNewScreen(screen)
const newScreens = await createScreens(screenTemplates)
loadNewScreen(newScreens[0])
}
const createGridScreen = async () => {
let firstScreen = null
for (let tableOrView of selectedTablesAndViews) {
const screenTemplate = gridScreen(
const createTableScreen = async type => {
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
screenTemplating.table({
screens,
tableOrView,
permissions[tableOrView.id]
)
type,
permissions: permissions[tableOrView.id],
})
)
const screen = await createScreen(screenTemplate)
await addNavigationLink(screen)
firstScreen ??= screen
}
loadNewScreen(firstScreen)
const newScreens = await createScreens(screenTemplates)
loadNewScreen(newScreens[0])
}
const createGridDetailsScreen = async () => {
let firstScreen = null
for (let tableOrView of selectedTablesAndViews) {
const screenTemplate = gridDetailsScreen(
const createFormScreen = async type => {
const screenTemplates = selectedTablesAndViews.flatMap(tableOrView =>
screenTemplating.form({
screens,
tableOrView,
permissions[tableOrView.id]
)
type,
permissions: permissions[tableOrView.id],
})
)
const screen = await createScreen(screenTemplate)
await addNavigationLink(screen)
const newScreens = await createScreens(screenTemplates)
firstScreen ??= screen
}
loadNewScreen(firstScreen)
}
const createFormScreen = async formType => {
let firstScreen = null
for (let tableOrView of selectedTablesAndViews) {
const screenTemplate = formScreen(
tableOrView,
formType,
permissions[tableOrView.id]
)
const screen = await createScreen(screenTemplate)
// Only add a navigation link for `Create`, as both `Update` and `View`
// require an `id` in their URL in order to function.
if (formType === "Create") {
await addNavigationLink(screen)
}
firstScreen ??= screen
}
if (formType === "Update" || formType === "Create") {
if (type === "update" || type === "create") {
const associatedTour =
formType === "Update"
type === "update"
? TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE
: TOUR_KEYS.BUILDER_FORM_CREATE
@ -183,7 +129,7 @@
}
}
loadNewScreen(firstScreen)
loadNewScreen(newScreens[0])
}
const loadNewScreen = screen => {
@ -199,7 +145,11 @@
}
const fetchPermission = resourceId => {
permissions[resourceId] = { loading: true, read: null, write: null }
permissions[resourceId] = {
loading: true,
read: Roles.BASIC,
write: Roles.BASIC,
}
permissionsStore
.forResource(resourceId)
@ -218,8 +168,8 @@
if (permissions[resourceId]?.loading) {
permissions[resourceId] = {
loading: false,
read: Roles.PUBLIC,
write: Roles.PUBLIC,
read: Roles.BASIC,
write: Roles.BASIC,
}
}
})
@ -250,18 +200,31 @@
<Modal bind:this={datasourceModal} autoFocus={false}>
<DatasourceModal
{selectedTablesAndViews}
{permissions}
onConfirm={onSelectDatasources}
on:toggle={handleTableOrViewToggle}
/>
</Modal>
<Modal bind:this={tableTypeModal}>
<TypeModal
title="Choose how you want to manage rows"
types={tableTypes}
onConfirm={createTableScreen}
onCancel={() => {
tableTypeModal.hide()
datasourceModal.show()
}}
/>
</Modal>
<Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal onConfirm={createBlankScreen} />
</Modal>
<Modal bind:this={formTypeModal}>
<FormTypeModal
<TypeModal
title="Select form type"
types={formTypes}
onConfirm={createFormScreen}
onCancel={() => {
formTypeModal.hide()

View File

@ -1,14 +1,14 @@
<script>
import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
import { Body, ModalContent, Layout, notifications } from "@budibase/bbui"
import { datasources as datasourcesStore } from "stores/builder"
import ICONS from "components/backend/DatasourceNavigator/icons"
import { IntegrationNames } from "constants"
import { createEventDispatcher, onMount } from "svelte"
import TableOrViewOption from "./TableOrViewOption.svelte"
import * as format from "helpers/data/format"
export let onConfirm
export let selectedTablesAndViews
export let permissions
const dispatch = createEventDispatcher()
@ -21,38 +21,37 @@
icon: "Remove",
name: view.name,
id: view.id,
clientData: {
...view,
type: "viewV2",
label: view.name,
},
tableSelectFormat: format.tableSelect.viewV2(view),
datasourceSelectFormat: format.datasourceSelect.viewV2(view),
}))
}
const getTablesAndViews = datasource => {
let tablesAndViews = []
const rawTables = Array.isArray(datasource.entities)
const tables = Array.isArray(datasource.entities)
? datasource.entities
: Object.values(datasource.entities ?? {})
for (const rawTable of rawTables) {
if (rawTable._id === "ta_users") {
for (const table of tables) {
if (table._id === "ta_users") {
continue
}
const table = {
const formattedTable = {
icon: "Table",
name: rawTable.name,
id: rawTable._id,
clientData: {
...rawTable,
label: rawTable.name,
tableId: rawTable._id,
type: "table",
},
name: table.name,
id: table._id,
tableSelectFormat: format.tableSelect.table(table),
datasourceSelectFormat: format.datasourceSelect.table(
table,
$datasourcesStore.list
),
}
tablesAndViews = tablesAndViews.concat([table, ...getViews(rawTable)])
tablesAndViews = tablesAndViews.concat([
formattedTable,
...getViews(table),
])
}
return tablesAndViews
@ -96,60 +95,76 @@
})
</script>
<span>
<ModalContent
title="Autogenerated screens"
confirmText="Confirm"
cancelText="Back"
{onConfirm}
disabled={!selectedTablesAndViews.length}
size="L"
>
<Body size="S">
Select which datasources you would like to use to create your screens
</Body>
<Layout noPadding gap="S">
{#each datasources as datasource}
<div class="data-source-wrap">
<div class="data-source-header">
<svelte:component
this={datasource.iconComponent}
height="24"
width="24"
/>
<div class="data-source-name">{datasource.name}</div>
</div>
<!-- List all tables -->
{#each datasource.tablesAndViews as tableOrView}
{@const selected = selectedTablesAndViews.some(
selected => selected.id === tableOrView.id
)}
<TableOrViewOption
roles={permissions[tableOrView.id]}
on:click={() => toggleSelection(tableOrView)}
{selected}
{tableOrView}
/>
{/each}
<ModalContent
title="Autogenerated screens"
confirmText="Next"
cancelText="Cancel"
{onConfirm}
disabled={!selectedTablesAndViews.length}
size="L"
>
<Body size="S">
Select which datasources you would like to use to create your screens
</Body>
<Layout noPadding gap="S">
{#each datasources as datasource}
<div class="datasource">
<div class="header">
<svelte:component
this={datasource.iconComponent}
height="18"
width="18"
/>
<h2>{datasource.name}</h2>
</div>
{/each}
</Layout>
</ModalContent>
</span>
<!-- List all tables -->
{#each datasource.tablesAndViews as tableOrView}
{@const selected = selectedTablesAndViews.some(
selected => selected.id === tableOrView.id
)}
<TableOrViewOption
on:click={() => toggleSelection(tableOrView)}
{selected}
{tableOrView}
/>
{/each}
</div>
{/each}
</Layout>
</ModalContent>
<style>
.data-source-wrap {
padding-bottom: var(--spectrum-alias-item-padding-s);
.datasource {
padding-bottom: 15px;
display: flex;
flex-direction: column;
grid-gap: var(--spacing-s);
max-width: 100%;
min-width: 0;
}
.data-source-header {
.datasource:last-child {
padding-bottom: 0;
}
.header {
display: flex;
align-items: center;
gap: var(--spacing-m);
padding-bottom: var(--spacing-xs);
padding-bottom: var(--spacing-m);
}
.header :global(svg) {
flex-shrink: 0;
}
.header h2 {
padding-top: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
font-weight: 400;
margin: 0;
margin-left: 10px;
}
</style>

View File

@ -1,115 +0,0 @@
<script>
import { ModalContent, Layout, Body, Icon } from "@budibase/bbui"
let type = null
export let onCancel = () => {}
export let onConfirm = () => {}
</script>
<span>
<ModalContent
title="Select form type"
confirmText="Done"
cancelText="Back"
onConfirm={() => onConfirm(type)}
{onCancel}
disabled={!type}
size="L"
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Layout noPadding gap="S">
<div
class="form-type"
class:selected={type === "Create"}
on:click={() => (type = "Create")}
>
<div class="form-type-wrap">
<div class="form-type-content">
<Body noPadding>Create a new row</Body>
<Body size="S">
For capturing and storing new data from your users
</Body>
</div>
{#if type === "Create"}
<span class="form-type-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
</div>
<div
class="form-type"
class:selected={type === "Update"}
on:click={() => (type = "Update")}
>
<div class="form-type-wrap">
<div class="form-type-content">
<Body noPadding>Update an existing row</Body>
<Body size="S">For viewing and updating existing data</Body>
</div>
{#if type === "Update"}
<span class="form-type-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
</div>
<div
class="form-type"
class:selected={type === "View"}
on:click={() => (type = "View")}
>
<div class="form-type-wrap">
<div class="form-type-content">
<Body noPadding>View an existing row</Body>
<Body size="S">For a read only view of your data</Body>
</div>
{#if type === "View"}
<span class="form-type-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
</div>
</Layout>
</ModalContent>
</span>
<style>
.form-type {
cursor: pointer;
gap: var(--spacing-s);
padding: var(--spacing-m) var(--spacing-xl);
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
display: flex;
flex-direction: column;
}
.selected,
.form-type:hover {
background: var(--spectrum-alias-background-color-tertiary);
}
.form-type-wrap {
display: flex;
align-items: center;
justify-content: space-between;
}
.form-type :global(p:nth-child(2)) {
color: var(--grey-6);
}
.form-type-check {
margin-left: auto;
}
.form-type-check :global(.spectrum-Icon) {
color: var(--spectrum-global-color-green-600);
}
.form-type-content {
gap: var(--spacing-s);
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,49 +1,14 @@
<script>
import { Icon, AbsTooltip } from "@budibase/bbui"
import RoleIcon from "components/common/RoleIcon.svelte"
import { Icon } from "@budibase/bbui"
export let tableOrView
export let roles
export let selected = false
$: hideRoles = roles == undefined || roles?.loading
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div role="button" tabindex="0" class="datasource" class:selected on:click>
<div class="content">
<Icon name={tableOrView.icon} />
<span>{tableOrView.name}</span>
</div>
<div class:hideRoles class="roles">
<AbsTooltip
type="info"
text={`Screens that only read data will be generated with access "${roles?.read?.toLowerCase()}"`}
>
<div class="role">
<span>read</span>
<RoleIcon
size="XS"
id={roles?.read}
disabled={roles?.loading !== false}
/>
</div>
</AbsTooltip>
<AbsTooltip
type="info"
text={`Screens that write data will be generated with access "${roles?.write?.toLowerCase()}"`}
>
<div class="role">
<span>write</span>
<RoleIcon
size="XS"
id={roles?.write}
disabled={roles?.loading !== false}
/>
</div>
</AbsTooltip>
</div>
<Icon name={tableOrView.icon} />
<span>{tableOrView.name}</span>
</div>
<style>
@ -52,18 +17,8 @@
border: 1px solid var(--spectrum-global-color-gray-300);
transition: 160ms all;
border-radius: 4px;
display: flex;
align-items: center;
user-select: none;
background-color: var(--background);
}
.datasource :global(svg) {
transition: 160ms all;
color: var(--spectrum-global-color-gray-600);
}
.content {
padding: var(--spectrum-alias-item-padding-s);
display: flex;
align-items: center;
@ -71,7 +26,12 @@
min-width: 0;
}
.content span {
.datasource :global(svg) {
transition: 160ms all;
color: var(--spectrum-global-color-gray-600);
}
.datasource span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -84,29 +44,4 @@
.selected {
border: 1px solid var(--blue) !important;
}
.roles {
margin-left: auto;
display: flex;
flex-direction: column;
align-items: end;
padding-right: var(--spectrum-alias-item-padding-s);
opacity: 0.5;
transition: opacity 160ms;
}
.hideRoles {
opacity: 0;
pointer-events: none;
}
.role {
display: flex;
align-items: center;
}
.role span {
font-size: 11px;
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,82 @@
<script>
import { ModalContent, Layout, Body } from "@budibase/bbui"
let selectedType = null
export let title
export let types
export let onCancel = () => {}
export let onConfirm = () => {}
</script>
<ModalContent
{title}
confirmText="Done"
cancelText="Back"
onConfirm={() => onConfirm(selectedType)}
{onCancel}
disabled={!selectedType}
size="L"
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Layout noPadding gap="S">
{#each types as type}
<div
class="type"
class:selected={selectedType === type.id}
on:click={() => (selectedType = type.id)}
>
<div class="image">
<img alt={type.img.alt} src={type.img.src} />
</div>
<div class="typeContent">
<Body noPadding>{type.title}</Body>
<Body size="S">{type.description}</Body>
</div>
</div>
{/each}
</Layout>
</ModalContent>
<style>
.type {
cursor: pointer;
gap: var(--spacing-s);
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
display: flex;
overflow: hidden;
}
.type:hover {
border: 1px solid var(--grey-5);
}
.type.selected {
border: 1px solid var(--blue);
}
.type :global(p:nth-child(2)) {
color: var(--grey-6);
}
.typeContent {
box-sizing: border-box;
padding: var(--spacing-m) var(--spacing-xl);
flex-grow: 1;
gap: var(--spacing-s);
display: flex;
flex-direction: column;
}
.image {
min-width: 133px;
height: 73px;
background-color: var(--grey-2);
}
.image img {
height: 100%;
}
</style>

View File

@ -0,0 +1,35 @@
import formView from "./images/formView.svg"
import formUpdate from "./images/formUpdate.svg"
import formCreate from "./images/formCreate.svg"
const tableTypes = [
{
id: "create",
img: {
alt: "A form containing new data",
src: formCreate,
},
title: "Create a new row",
description: "For capturing and storing new data from your users",
},
{
id: "update",
img: {
alt: "A form containing edited data",
src: formUpdate,
},
title: "Update an existing row",
description: "For viewing and updating existing data",
},
{
id: "view",
img: {
alt: "A form containing read-only data",
src: formView,
},
title: "View an existing row",
description: "For a read only view of your data",
},
]
export default tableTypes

View File

@ -0,0 +1,15 @@
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="118" height="65" fill="#34BB84"/>
<rect width="118" height="65" fill="#34BB84"/>
<mask id="path-3-inside-1_51_2" fill="white">
<path d="M22 12H94V65H22V12Z"/>
</mask>
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_51_2)"/>
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_51_2)"/>
<defs>
<linearGradient id="paint0_linear_51_2" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.8"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1,20 @@
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="118" height="65" fill="#FD4F01"/>
<rect width="118" height="65" fill="#FD4F01"/>
<mask id="path-3-inside-1_49_949" fill="white">
<path d="M22 12H94V65H22V12Z"/>
</mask>
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_49_949)"/>
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_49_949)"/>
<rect x="46" y="29" width="15" height="4" fill="white" fill-opacity="0.97"/>
<rect x="46" y="45" width="15" height="4" fill="white" fill-opacity="0.97"/>
<rect x="46" y="35" width="25" height="6" fill="white" fill-opacity="0.97"/>
<rect x="46" y="51" width="25" height="6" fill="white" fill-opacity="0.97"/>
<path d="M46 21H71V25H46V21Z" fill="white" fill-opacity="0.97"/>
<defs>
<linearGradient id="paint0_linear_49_949" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.6"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,20 @@
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="118" height="65" fill="#FD4F01"/>
<rect width="118" height="65" fill="#FD4F01"/>
<mask id="path-3-inside-1_49_940" fill="white">
<path d="M22 12H94V65H22V12Z"/>
</mask>
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_49_940)"/>
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_49_940)"/>
<rect x="46" y="29" width="15" height="4" fill="white" fill-opacity="0.97"/>
<rect x="46" y="45" width="15" height="4" fill="white" fill-opacity="0.5"/>
<rect x="46" y="35" width="25" height="6" fill="white" fill-opacity="0.97"/>
<path d="M46 21H71V25H46V21Z" fill="white" fill-opacity="0.5"/>
<rect x="46" y="51" width="25" height="6" fill="white" fill-opacity="0.5"/>
<defs>
<linearGradient id="paint0_linear_49_940" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.6"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,20 @@
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="118" height="65" fill="#FD4F01"/>
<rect width="118" height="65" fill="#FD4F01"/>
<mask id="path-3-inside-1_49_931" fill="white">
<path d="M22 12H94V65H22V12Z"/>
</mask>
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_49_931)"/>
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_49_931)"/>
<rect x="46" y="29" width="15" height="4" fill="white" fill-opacity="0.5"/>
<rect x="46" y="45" width="15" height="4" fill="white" fill-opacity="0.5"/>
<rect x="46" y="35" width="25" height="6" fill="white" fill-opacity="0.5"/>
<path d="M46 21H71V25H46V21Z" fill="white" fill-opacity="0.5"/>
<rect x="46" y="51" width="25" height="6" fill="white" fill-opacity="0.5"/>
<defs>
<linearGradient id="paint0_linear_49_931" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.6"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,44 @@
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="118" height="65" fill="#765FFE"/>
<mask id="path-2-inside-1_4_100" fill="white">
<path d="M22 12H94V65H22V12Z"/>
</mask>
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_4_100)"/>
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-2-inside-1_4_100)"/>
<path d="M35.6901 23.0003H44.8169V28.0003H35.6901V23.0003Z" fill="white" fill-opacity="0.9"/>
<path d="M35.6901 28.5004H44.8169V33.5004H35.6901V28.5004Z" fill="white" fill-opacity="0.5"/>
<path d="M35.6901 34H44.8169V39H35.6901V34Z" fill="white" fill-opacity="0.5"/>
<path d="M35.6901 39.5002H44.8169V44.5002H35.6901V39.5002Z" fill="white" fill-opacity="0.5"/>
<path d="M35.6901 45.0003H44.8169V50.0003H35.6901V45.0003Z" fill="white" fill-opacity="0.5"/>
<path d="M35.6901 50.5004H44.8169V55.5004H35.6901V50.5004Z" fill="white" fill-opacity="0.5"/>
<path d="M45.3234 23.0003H54.4502V28.0003H45.3234V23.0003Z" fill="white" fill-opacity="0.9"/>
<path d="M45.3234 28.5004H54.4502V33.5004H45.3234V28.5004Z" fill="white" fill-opacity="0.5"/>
<path d="M45.3234 34H54.4502V39H45.3234V34Z" fill="white" fill-opacity="0.5"/>
<path d="M45.3234 39.5002H54.4502V44.5002H45.3234V39.5002Z" fill="white" fill-opacity="0.5"/>
<path d="M45.3234 45.0003H54.4502V50.0003H45.3234V45.0003Z" fill="white" fill-opacity="0.5"/>
<path d="M45.3234 50.5004H54.4502V55.5004H45.3234V50.5004Z" fill="white" fill-opacity="0.5"/>
<path d="M54.9576 23.0003H64.0844V28.0003H54.9576V23.0003Z" fill="white" fill-opacity="0.9"/>
<path d="M54.9576 28.5004H64.0844V33.5004H54.9576V28.5004Z" fill="white" fill-opacity="0.5"/>
<path d="M54.9576 34H64.0844V39H54.9576V34Z" fill="white" fill-opacity="0.5"/>
<path d="M54.9576 39.5002H64.0844V44.5002H54.9576V39.5002Z" fill="white" fill-opacity="0.5"/>
<path d="M54.9576 45.0003H64.0844V50.0003H54.9576V45.0003Z" fill="white" fill-opacity="0.5"/>
<path d="M54.9576 50.5004H64.0844V55.5004H54.9576V50.5004Z" fill="white" fill-opacity="0.5"/>
<path d="M64.5915 23.0003H73.7183V28.0003H64.5915V23.0003Z" fill="white" fill-opacity="0.9"/>
<path d="M64.5915 28.5004H73.7183V33.5004H64.5915V28.5004Z" fill="white" fill-opacity="0.5"/>
<path d="M64.5915 34H73.7183V39H64.5915V34Z" fill="white" fill-opacity="0.5"/>
<path d="M64.5915 39.5002H73.7183V44.5002H64.5915V39.5002Z" fill="white" fill-opacity="0.5"/>
<path d="M64.5915 45.0003H73.7183V50.0003H64.5915V45.0003Z" fill="white" fill-opacity="0.5"/>
<path d="M64.5915 50.5004H73.7183V55.5004H64.5915V50.5004Z" fill="white" fill-opacity="0.5"/>
<path d="M74.2253 23.0003H83.3521V28.0003H74.2253V23.0003Z" fill="white" fill-opacity="0.9"/>
<path d="M74.2253 28.5004H83.3521V33.5004H74.2253V28.5004Z" fill="white" fill-opacity="0.5"/>
<path d="M74.2253 34H83.3521V39H74.2253V34Z" fill="white" fill-opacity="0.5"/>
<path d="M74.2253 39.5002H83.3521V44.5002H74.2253V39.5002Z" fill="white" fill-opacity="0.5"/>
<path d="M74.2253 45.0003H83.3521V50.0003H74.2253V45.0003Z" fill="white" fill-opacity="0.5"/>
<path d="M74.2253 50.5004H83.3521V55.5004H74.2253V50.5004Z" fill="white" fill-opacity="0.5"/>
<defs>
<linearGradient id="paint0_linear_4_100" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.6"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,28 @@
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="118" height="65" fill="#765FFE"/>
<mask id="path-2-inside-1_4_138" fill="white">
<path d="M22 12H94V65H22V12Z"/>
</mask>
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_4_138)"/>
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-2-inside-1_4_138)"/>
<g filter="url(#filter0_d_4_138)">
<rect x="42" y="17" width="33" height="44" fill="white" fill-opacity="0.4" shape-rendering="crispEdges"/>
<rect x="42.5" y="17.5" width="32" height="43" stroke="white" stroke-opacity="0.3" shape-rendering="crispEdges"/>
</g>
<defs>
<filter id="filter0_d_4_138" x="39" y="15" width="39" height="50" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_138"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_138" result="shape"/>
</filter>
<linearGradient id="paint0_linear_4_138" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.6"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,20 @@
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="118" height="65" fill="#765FFE"/>
<rect width="118" height="65" fill="#765FFE"/>
<mask id="path-3-inside-1_56_20" fill="white">
<path d="M22 12H94V65H22V12Z"/>
</mask>
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_56_20)"/>
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-3-inside-1_56_20)"/>
<rect x="46" y="29" width="15" height="4" fill="white" fill-opacity="0.97"/>
<rect x="46" y="45" width="15" height="4" fill="white" fill-opacity="0.5"/>
<rect x="46" y="35" width="25" height="6" fill="white" fill-opacity="0.97"/>
<path d="M46 21H71V25H46V21Z" fill="white" fill-opacity="0.5"/>
<rect x="46" y="51" width="25" height="6" fill="white" fill-opacity="0.5"/>
<defs>
<linearGradient id="paint0_linear_56_20" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.6"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,28 @@
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="118" height="65" fill="#765FFE"/>
<mask id="path-2-inside-1_4_103" fill="white">
<path d="M22 12H94V65H22V12Z"/>
</mask>
<path d="M22 12H94V65H22V12Z" fill="url(#paint0_linear_4_103)"/>
<path d="M22 12V11H21V12H22ZM94 12H95V11H94V12ZM22 13H94V11H22V13ZM93 12V65H95V12H93ZM23 65V12H21V65H23Z" fill="white" fill-opacity="0.2" mask="url(#path-2-inside-1_4_103)"/>
<g filter="url(#filter0_d_4_103)">
<rect x="70" y="17" width="20" height="44" fill="white" fill-opacity="0.4" shape-rendering="crispEdges"/>
<rect x="70.5" y="17.5" width="19" height="43" stroke="white" stroke-opacity="0.3" shape-rendering="crispEdges"/>
</g>
<defs>
<filter id="filter0_d_4_103" x="67" y="15" width="26" height="50" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_103"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_103" result="shape"/>
</filter>
<linearGradient id="paint0_linear_4_103" x1="22" y1="12" x2="92.4561" y2="66.9785" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.6"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,10 +1,9 @@
<script>
import { Body } from "@budibase/bbui"
import CreationPage from "components/common/CreationPage.svelte"
import blankImage from "./images/blank.png"
import tableInline from "./images/tableInline.png"
import tableDetails from "./images/tableDetails.png"
import formImage from "./images/form.png"
import blank from "./images/blank.svg"
import table from "./images/tableInline.svg"
import form from "./images/formUpdate.svg"
import CreateScreenModal from "./CreateScreenModal.svelte"
import { screenStore } from "stores/builder"
@ -30,37 +29,27 @@
<div class="cards">
<div class="card" on:click={() => createScreenModal.show("blank")}>
<div class="image">
<img alt="" src={blankImage} />
<img alt="A blank screen" src={blank} />
</div>
<div class="text">
<Body size="S">Blank screen</Body>
<Body size="S">Blank</Body>
<Body size="XS">Add an empty blank screen</Body>
</div>
</div>
<div class="card" on:click={() => createScreenModal.show("grid")}>
<div class="card" on:click={() => createScreenModal.show("table")}>
<div class="image">
<img alt="" src={tableInline} />
<img alt="A table of data" src={table} />
</div>
<div class="text">
<Body size="S">Table with inline editing</Body>
<Body size="XS">View, edit and delete rows inline</Body>
</div>
</div>
<div class="card" on:click={() => createScreenModal.show("gridDetails")}>
<div class="image">
<img alt="" src={tableDetails} />
</div>
<div class="text">
<Body size="S">Table with details panel</Body>
<Body size="XS">Manage your row details in a side panel</Body>
<Body size="S">Table</Body>
<Body size="XS">List rows in a table</Body>
</div>
</div>
<div class="card" on:click={() => createScreenModal.show("form")}>
<div class="image">
<img alt="" src={formImage} />
<img alt="A form containing data" src={form} />
</div>
<div class="text">
<Body size="S">Form</Body>
@ -114,8 +103,9 @@
}
.card .image {
min-height: 130px;
min-width: 235px;
height: 127px;
background-color: var(--grey-2);
}
.text {

View File

@ -0,0 +1,45 @@
import tableInline from "./images/tableInline.svg"
import tableSidePanel from "./images/tableSidePanel.svg"
import tableModal from "./images/tableModal.svg"
import tableNewScreen from "./images/tableNewScreen.svg"
const tableTypes = [
{
id: "inline",
img: {
alt: "A table of data",
src: tableInline,
},
title: "Inline",
description: "Manage data directly on your table",
},
{
id: "sidePanel",
img: {
alt: "A side panel",
src: tableSidePanel,
},
title: "Side panel",
description: "Open row details in a side panel",
},
{
id: "modal",
img: {
alt: "A modal",
src: tableModal,
},
title: "Modal",
description: "Open row details in a modal",
},
{
id: "newScreen",
img: {
alt: "A new screen",
src: tableNewScreen,
},
title: "New screen",
description: "View row details on a separate screen",
},
]
export default tableTypes

View File

@ -1,6 +1,6 @@
import { v4 } from "uuid"
import { Component } from "templates/Component"
import { Screen } from "templates/Screen"
import { Screen } from "templates/screenTemplating/Screen"
import { get } from "svelte/store"
import {
BUDIBASE_INTERNAL_DB_ID,

View File

@ -2,11 +2,11 @@ import { Helpers } from "@budibase/bbui"
import { BaseStructure } from "./BaseStructure"
export class Component extends BaseStructure {
constructor(name) {
constructor(name, _id = Helpers.uuid()) {
super(false)
this._children = []
this._json = {
_id: Helpers.uuid(),
_id,
_component: name,
_styles: {
normal: {},
@ -50,4 +50,8 @@ export class Component extends BaseStructure {
this._json.text = text
return this
}
getId() {
return this._json._id
}
}

View File

@ -1,7 +0,0 @@
import { Screen } from "./Screen"
const blankScreen = route => {
return new Screen().instanceName("New Screen").route(route).json()
}
export default blankScreen

View File

@ -1,49 +0,0 @@
import { Screen } from "./Screen"
import { Component } from "./Component"
import sanitizeUrl from "helpers/sanitizeUrl"
export const FORM_TEMPLATE = "FORM_TEMPLATE"
export const formUrl = (tableOrView, actionType) => {
if (actionType === "Create") {
return sanitizeUrl(`/${tableOrView.name}/new`)
} else if (actionType === "Update") {
return sanitizeUrl(`/${tableOrView.name}/edit/:id`)
} else if (actionType === "View") {
return sanitizeUrl(`/${tableOrView.name}/view/:id`)
}
}
export const getRole = (permissions, actionType) => {
if (actionType === "View") {
return permissions.read
}
return permissions.write
}
const generateMultistepFormBlock = (tableOrView, actionType) => {
const multistepFormBlock = new Component(
"@budibase/standard-components/multistepformblock"
)
multistepFormBlock
.customProps({
actionType,
dataSource: tableOrView.clientData,
steps: [{}],
rowId: actionType === "new" ? undefined : `{{ url.id }}`,
})
.instanceName(`${tableOrView.name} - Multistep Form block`)
return multistepFormBlock
}
const createScreen = (tableOrView, actionType, permissions) => {
return new Screen()
.route(formUrl(tableOrView, actionType))
.instanceName(`${tableOrView.name} - Form`)
.role(getRole(permissions, actionType))
.autoTableId(tableOrView.id)
.addChild(generateMultistepFormBlock(tableOrView, actionType))
.json()
}
export default createScreen

View File

@ -1,30 +0,0 @@
import sanitizeUrl from "helpers/sanitizeUrl"
import { Screen } from "./Screen"
import { Component } from "./Component"
const gridUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
const createScreen = (tableOrView, permissions) => {
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: tableOrView.name,
})
const gridBlock = new Component("@budibase/standard-components/gridblock")
.instanceName(`${tableOrView.name} - Table`)
.customProps({
table: tableOrView.clientData,
})
return new Screen()
.route(gridUrl(tableOrView))
.instanceName(`${tableOrView.name} - List`)
.role(permissions.write)
.autoTableId(tableOrView.id)
.addChild(heading)
.addChild(gridBlock)
.json()
}
export default createScreen

View File

@ -1,4 +1,4 @@
import { BaseStructure } from "./BaseStructure"
import { BaseStructure } from "../BaseStructure"
import { Helpers } from "@budibase/bbui"
export class Screen extends BaseStructure {

View File

@ -0,0 +1,24 @@
import { Screen } from "./Screen"
import { capitalise } from "helpers"
import getValidRoute from "./getValidRoute"
import { Roles } from "constants/backend"
const blank = ({ route, screens }) => {
const validRoute = getValidRoute(screens, route, Roles.BASIC)
const template = new Screen()
.instanceName("Blank screen")
.role(Roles.BASIC)
.route(validRoute)
.json()
return [
{
data: template,
navigationLinkLabel:
validRoute === "/" ? null : capitalise(validRoute.split("/")[1]),
},
]
}
export default blank

View File

@ -0,0 +1,67 @@
import { Screen } from "./Screen"
import { Component } from "../Component"
import getValidRoute from "./getValidRoute"
export const getTypeSpecificRoute = (tableOrView, type) => {
if (type === "create") {
return `/${tableOrView.name}/new`
} else if (type === "update") {
return `/${tableOrView.name}/edit/:id`
} else if (type === "view") {
return `/${tableOrView.name}/view/:id`
}
}
const getRole = (permissions, type) => {
if (type === "view") {
return permissions.read
}
return permissions.write
}
const getActionType = type => {
if (type === "create") {
return "Create"
}
if (type === "update") {
return "Update"
}
if (type === "view") {
return "View"
}
}
const form = ({ tableOrView, type, permissions, screens }) => {
const typeSpecificRoute = getTypeSpecificRoute(tableOrView, type)
const role = getRole(permissions, type)
const multistepFormBlock = new Component(
"@budibase/standard-components/multistepformblock"
)
.customProps({
actionType: getActionType(type),
dataSource: tableOrView.tableSelectFormat,
steps: [{}],
rowId: type === "new" ? undefined : `{{ url.id }}`,
})
.instanceName(`${tableOrView.name} - Multistep Form block`)
const template = new Screen()
.route(getValidRoute(screens, typeSpecificRoute, role))
.instanceName(`${tableOrView.name} - Form`)
.role(role)
.autoTableId(tableOrView.id)
.addChild(multistepFormBlock)
.json()
return [
{
data: template,
navigationLinkLabel:
type === "create" ? `Create ${tableOrView.name}` : null,
},
]
}
export default form

View File

@ -0,0 +1,35 @@
import sanitizeUrl from "helpers/sanitizeUrl"
const arbitraryMax = 10000
const isScreenUrlValid = (screens, url, role) => {
return !screens.some(
screen => screen.routing?.route === url && screen.routing?.roleId === role
)
}
const getValidScreenUrl = (screens, url, role) => {
const [firstPathSegment = "", ...restPathSegments] = url
.split("/")
.filter(segment => segment !== "")
const restOfPath =
restPathSegments.length > 0 ? `/${restPathSegments.join("/")}` : ""
const naiveUrl = sanitizeUrl(`/${firstPathSegment}${restOfPath}`)
if (isScreenUrlValid(screens, naiveUrl, role)) {
return naiveUrl
}
for (let suffix = 2; suffix < arbitraryMax; suffix++) {
const suffixedUrl = sanitizeUrl(
`/${firstPathSegment}-${suffix}${restOfPath}`
)
if (isScreenUrlValid(screens, suffixedUrl, role)) {
return suffixedUrl
}
}
}
export default getValidScreenUrl

View File

@ -0,0 +1,3 @@
export { default as blank } from "./blank"
export { default as form } from "./form"
export { default as table } from "./table"

View File

@ -0,0 +1,25 @@
import inline from "./inline"
import modal from "./modal"
import sidePanel from "./sidePanel"
import newScreen from "./newScreen"
const createScreen = ({ tableOrView, type, permissions, screens }) => {
if (type === "inline") {
return inline({ tableOrView, permissions, screens })
}
if (type === "modal") {
return modal({ tableOrView, permissions, screens })
}
if (type === "sidePanel") {
return sidePanel({ tableOrView, permissions, screens })
}
if (type === "newScreen") {
return newScreen({ tableOrView, permissions, screens })
}
throw new Error(`Unrecognized table type ${type}`)
}
export default createScreen

View File

@ -0,0 +1,36 @@
import { Screen } from "../Screen"
import { Component } from "../../Component"
import { capitalise } from "helpers"
import getValidRoute from "../getValidRoute"
const inline = ({ tableOrView, permissions, screens }) => {
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: tableOrView.name,
})
const tableBlock = new Component("@budibase/standard-components/gridblock")
.instanceName(`${tableOrView.name} - Table`)
.customProps({
table: tableOrView.datasourceSelectFormat,
})
const screenTemplate = new Screen()
.route(getValidRoute(screens, tableOrView.name, permissions.write))
.instanceName(`${tableOrView.name} - List`)
.role(permissions.write)
.autoTableId(tableOrView.id)
.addChild(heading)
.addChild(tableBlock)
.json()
return [
{
data: screenTemplate,
navigationLinkLabel: capitalise(tableOrView.name),
},
]
}
export default inline

View File

@ -0,0 +1,157 @@
import { Screen } from "../Screen"
import { Component } from "../../Component"
import { generate } from "shortid"
import { makePropSafe as safe } from "@budibase/string-templates"
import { Utils } from "@budibase/frontend-core"
import { capitalise } from "helpers"
import getValidRoute from "../getValidRoute"
const modal = ({ tableOrView, permissions, screens }) => {
/*
Create Row
*/
const createRowModal = new Component("@budibase/standard-components/modal")
.instanceName("New row modal")
.customProps({
size: "large",
})
const buttonGroup = new Component("@budibase/standard-components/buttongroup")
const createButton = new Component("@budibase/standard-components/button")
createButton.customProps({
onClick: [
{
id: 0,
"##eventHandlerType": "Open Modal",
parameters: {
id: createRowModal._json._id,
},
},
],
text: "Create row",
type: "cta",
})
buttonGroup.instanceName(`${tableOrView.name} - Create`).customProps({
hAlign: "right",
buttons: [createButton.json()],
})
const tableHeader = new Component("@budibase/standard-components/container")
.instanceName("Heading container")
.customProps({
direction: "row",
hAlign: "stretch",
})
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: tableOrView.name,
})
tableHeader.addChild(heading)
tableHeader.addChild(buttonGroup)
const createFormBlock = new Component(
"@budibase/standard-components/formblock"
)
createFormBlock.instanceName("Create row form block").customProps({
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
buttonPosition: "top",
actionType: "Create",
title: "Create row",
buttons: Utils.buildFormBlockButtonConfig({
_id: createFormBlock._json._id,
showDeleteButton: false,
showSaveButton: true,
saveButtonLabel: "Save",
actionType: "Create",
dataSource: tableOrView.tableSelectFormat,
}),
})
createRowModal.addChild(createFormBlock)
/*
Edit Row
*/
const stateKey = `ID_${generate()}`
const detailsModal = new Component("@budibase/standard-components/modal")
.instanceName("Edit row modal")
.customProps({
size: "large",
})
const editFormBlock = new Component("@budibase/standard-components/formblock")
editFormBlock.instanceName("Edit row form block").customProps({
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
buttonPosition: "top",
actionType: "Update",
title: "Edit",
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
buttons: Utils.buildFormBlockButtonConfig({
_id: editFormBlock._json._id,
showDeleteButton: true,
showSaveButton: true,
saveButtonLabel: "Save",
deleteButtonLabel: "Delete",
actionType: "Update",
dataSource: tableOrView.tableSelectFormat,
}),
})
detailsModal.addChild(editFormBlock)
const tableBlock = new Component("@budibase/standard-components/gridblock")
tableBlock
.customProps({
table: tableOrView.datasourceSelectFormat,
allowAddRows: false,
allowEditRows: false,
allowDeleteRows: false,
onRowClick: [
{
id: 0,
"##eventHandlerType": "Update State",
parameters: {
key: stateKey,
type: "set",
persist: false,
value: `{{ ${safe("eventContext")}.${safe("row")}._id }}`,
},
},
{
id: 1,
"##eventHandlerType": "Open Modal",
parameters: {
id: detailsModal._json._id,
},
},
],
})
.instanceName(`${tableOrView.name} - Table`)
const template = new Screen()
.route(getValidRoute(screens, tableOrView.name, permissions.write))
.instanceName(`${tableOrView.name} - List and details`)
.role(permissions.write)
.autoTableId(tableOrView.id)
.addChild(tableHeader)
.addChild(tableBlock)
.addChild(createRowModal)
.addChild(detailsModal)
.json()
return [
{
data: template,
navigationLinkLabel: capitalise(tableOrView.name),
},
]
}
export default modal

View File

@ -0,0 +1,322 @@
import { Screen } from "../Screen"
import { Component } from "../../Component"
import { capitalise } from "helpers"
import { makePropSafe as safe } from "@budibase/string-templates"
import getValidRoute from "../getValidRoute"
import { Helpers } from "@budibase/bbui"
const getTableScreenTemplate = ({
route,
updateScreenRoute,
createScreenRoute,
tableOrView,
permissions,
}) => {
const newButton = new Component("@budibase/standard-components/button")
.instanceName("New button")
.customProps({
text: "Create row",
onClick: [
{
"##eventHandlerType": "Navigate To",
parameters: {
type: "url",
url: createScreenRoute,
},
},
],
})
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: tableOrView.name,
})
const tableHeader = new Component("@budibase/standard-components/container")
.instanceName("Heading container")
.customProps({
direction: "row",
hAlign: "stretch",
})
.addChild(heading)
.addChild(newButton)
const updateScreenRouteSegments = updateScreenRoute.split(":id")
if (updateScreenRouteSegments.length !== 2) {
throw new Error("Provided edit screen route is invalid")
}
const tableBlock = new Component("@budibase/standard-components/gridblock")
.instanceName(`${tableOrView.name} - Table`)
.customProps({
table: tableOrView.datasourceSelectFormat,
allowAddRows: false,
allowEditRows: false,
allowDeleteRows: false,
onRowClick: [
{
id: 0,
"##eventHandlerType": "Navigate To",
parameters: {
type: "url",
url: `${updateScreenRouteSegments[0]}{{ ${safe(
"eventContext"
)}.${safe("row")}._id }}${updateScreenRouteSegments[1]}`,
},
},
],
})
const template = new Screen()
.route(route)
.instanceName(`${tableOrView.name} - List`)
.role(permissions.write)
.autoTableId(tableOrView.id)
.addChild(tableHeader)
.addChild(tableBlock)
.json()
return {
data: template,
navigationLinkLabel: capitalise(tableOrView.name),
}
}
const getUpdateScreenTemplate = ({
route,
tableScreenRoute,
tableOrView,
permissions,
}) => {
const formBlockId = Helpers.uuid()
const formId = `${formBlockId}-form`
const repeaterId = `${formBlockId}-repeater`
const backButton = new Component("@budibase/standard-components/button")
.instanceName("Back button")
.customProps({
type: "primary",
icon: "ri-arrow-go-back-fill",
text: "Back",
onClick: [
{
"##eventHandlerType": "Navigate To",
parameters: {
type: "url",
url: tableScreenRoute,
},
},
],
})
const deleteButton = new Component("@budibase/standard-components/button")
.instanceName("Delete button")
.customProps({
type: "secondary",
text: "Delete",
onClick: [
{
"##eventHandlerType": "Delete Row",
parameters: {
confirm: true,
tableId: tableOrView.id,
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
},
},
{
"##eventHandlerType": "Navigate To",
parameters: {
type: "url",
url: tableScreenRoute,
},
},
],
})
const saveButton = new Component("@budibase/standard-components/button")
.instanceName("Save button")
.customProps({
type: "cta",
text: "Save",
onClick: [
{
"##eventHandlerType": "Validate Form",
parameters: {
componentId: formId,
},
},
{
"##eventHandlerType": "Save Row",
parameters: {
providerId: formId,
tableId: tableOrView.id,
},
},
{
"##eventHandlerType": "Navigate To",
parameters: {
type: "url",
url: tableScreenRoute,
},
},
],
})
const updateFormBlock = new Component(
"@budibase/standard-components/formblock",
formBlockId
)
.instanceName("Update row form block")
.customProps({
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
buttonPosition: "top",
actionType: "Update",
title: `Update ${tableOrView.name} row`,
buttons: [backButton.json(), saveButton.json(), deleteButton.json()],
})
const template = new Screen()
.route(route)
.instanceName(`Update row`)
.role(permissions.write)
.autoTableId(tableOrView.id)
.addChild(updateFormBlock)
.json()
return {
data: template,
navigationLinkLabel: null,
}
}
const getCreateScreenTemplate = ({
route,
tableScreenRoute,
tableOrView,
permissions,
}) => {
const formBlockId = Helpers.uuid()
const formId = `${formBlockId}-form`
const backButton = new Component("@budibase/standard-components/button")
.instanceName("Back button")
.customProps({
type: "primary",
icon: "ri-arrow-go-back-fill",
text: "Back",
onClick: [
{
"##eventHandlerType": "Navigate To",
parameters: {
type: "url",
url: tableScreenRoute,
},
},
],
})
const saveButton = new Component("@budibase/standard-components/button")
.instanceName("Save button")
.customProps({
type: "cta",
text: "Save",
onClick: [
{
"##eventHandlerType": "Validate Form",
parameters: {
componentId: formId,
},
},
{
"##eventHandlerType": "Save Row",
parameters: {
providerId: formId,
tableId: tableOrView.id,
},
},
{
"##eventHandlerType": "Navigate To",
parameters: {
type: "url",
url: tableScreenRoute,
},
},
],
})
const createFormBlock = new Component(
"@budibase/standard-components/formblock",
formBlockId
)
.instanceName("Create row form block")
.customProps({
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
buttonPosition: "top",
actionType: "Create",
title: `Create ${tableOrView.name} row`,
buttons: [backButton.json(), saveButton.json()],
})
const template = new Screen()
.route(route)
.instanceName("Create row")
.role(permissions.write)
.autoTableId(tableOrView.id)
.addChild(createFormBlock)
.json()
return {
data: template,
navigationLinkLabel: null,
}
}
const newScreen = ({ tableOrView, permissions, screens }) => {
const tableScreenRoute = getValidRoute(
screens,
tableOrView.name,
permissions.write
)
const updateScreenRoute = getValidRoute(
screens,
`/${tableOrView.name}/edit/:id`,
permissions.write
)
const createScreenRoute = getValidRoute(
screens,
`/${tableOrView.name}/new`,
permissions.write
)
const tableScreenTemplate = getTableScreenTemplate({
route: tableScreenRoute,
updateScreenRoute,
createScreenRoute,
permissions,
tableOrView,
})
const updateScreenTemplate = getUpdateScreenTemplate({
route: updateScreenRoute,
tableScreenRoute,
tableOrView,
permissions,
})
const createScreenTemplate = getCreateScreenTemplate({
route: createScreenRoute,
tableScreenRoute,
tableOrView,
permissions,
})
return [tableScreenTemplate, updateScreenTemplate, createScreenTemplate]
}
export default newScreen

View File

@ -1,13 +1,12 @@
import sanitizeUrl from "helpers/sanitizeUrl"
import { Screen } from "./Screen"
import { Component } from "./Component"
import { Screen } from "../Screen"
import { Component } from "../../Component"
import { generate } from "shortid"
import { makePropSafe as safe } from "@budibase/string-templates"
import { Utils } from "@budibase/frontend-core"
import { capitalise } from "helpers"
import getValidRoute from "../getValidRoute"
const gridDetailsUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
const createScreen = (tableOrView, permissions) => {
const sidePanel = ({ tableOrView, permissions, screens }) => {
/*
Create Row
*/
@ -37,7 +36,7 @@ const createScreen = (tableOrView, permissions) => {
buttons: [createButton.json()],
})
const gridHeader = new Component("@budibase/standard-components/container")
const tableHeader = new Component("@budibase/standard-components/container")
.instanceName("Heading container")
.customProps({
direction: "row",
@ -50,14 +49,14 @@ const createScreen = (tableOrView, permissions) => {
text: tableOrView.name,
})
gridHeader.addChild(heading)
gridHeader.addChild(buttonGroup)
tableHeader.addChild(heading)
tableHeader.addChild(buttonGroup)
const createFormBlock = new Component(
"@budibase/standard-components/formblock"
)
createFormBlock.instanceName("Create row form block").customProps({
dataSource: tableOrView.clientData,
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
buttonPosition: "top",
actionType: "Create",
@ -68,7 +67,7 @@ const createScreen = (tableOrView, permissions) => {
showSaveButton: true,
saveButtonLabel: "Save",
actionType: "Create",
dataSource: tableOrView.clientData,
dataSource: tableOrView.tableSelectFormat,
}),
})
@ -84,7 +83,7 @@ const createScreen = (tableOrView, permissions) => {
const editFormBlock = new Component("@budibase/standard-components/formblock")
editFormBlock.instanceName("Edit row form block").customProps({
dataSource: tableOrView.clientData,
dataSource: tableOrView.tableSelectFormat,
labelPosition: "left",
buttonPosition: "top",
actionType: "Update",
@ -97,16 +96,16 @@ const createScreen = (tableOrView, permissions) => {
saveButtonLabel: "Save",
deleteButtonLabel: "Delete",
actionType: "Update",
dataSource: tableOrView.clientData,
dataSource: tableOrView.tableSelectFormat,
}),
})
detailsSidePanel.addChild(editFormBlock)
const gridBlock = new Component("@budibase/standard-components/gridblock")
gridBlock
const tableBlock = new Component("@budibase/standard-components/gridblock")
tableBlock
.customProps({
table: tableOrView.clientData,
table: tableOrView.datasourceSelectFormat,
allowAddRows: false,
allowEditRows: false,
allowDeleteRows: false,
@ -132,16 +131,23 @@ const createScreen = (tableOrView, permissions) => {
})
.instanceName(`${tableOrView.name} - Table`)
return new Screen()
.route(gridDetailsUrl(tableOrView))
const template = new Screen()
.route(getValidRoute(screens, tableOrView.name, permissions.write))
.instanceName(`${tableOrView.name} - List and details`)
.role(permissions.write)
.autoTableId(tableOrView.resourceId)
.addChild(gridHeader)
.addChild(gridBlock)
.autoTableId(tableOrView.id)
.addChild(tableHeader)
.addChild(tableBlock)
.addChild(createRowSidePanel)
.addChild(detailsSidePanel)
.json()
return [
{
data: template,
navigationLinkLabel: capitalise(tableOrView.name),
},
]
}
export default createScreen
export default sidePanel

View File

@ -155,6 +155,7 @@ export const buildFormBlockButtonConfig = props => {
providerId: formId,
tableId: resourceId,
notificationOverride,
confirm: null,
},
},
{