Form Screen Templates Generate Relevant Row Ids, Permission Roles are Automatically Generated (#14192)

* url params

* wip

* wip

* wip

* wip

* permissions now

* change table/screen deletion

* form screen refactor

* refactor grid templates

* wip

* wip permissions

* wip

* wip

* lint

* lint

* wip

* before lint

* lint

* wip

* rename some stuff

* Dean PR Feedback

* remove unneeded awaits
This commit is contained in:
Gerard Burns 2024-07-29 12:59:24 +01:00 committed by GitHub
parent 9a181c9707
commit cf31c7ba3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 614 additions and 553 deletions

View File

@ -36,9 +36,11 @@
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
<div class="spectrum-InLineAlert-header">{header}</div>
{#each split as splitMsg}
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
{/each}
<slot>
{#each split as splitMsg}
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
{/each}
</slot>
{#if onConfirm}
<div class="spectrum-InLineAlert-footer button">
<Button {cta} secondary={cta ? false : true} on:click={onConfirm}

View File

@ -30,7 +30,7 @@
class:custom={!!color}
class:square
class:hoverable
style={`--color: ${color};`}
style={`--color: ${color ?? "var(--spectrum-global-color-gray-400)"};`}
class:spectrum-StatusLight--celery={celery}
class:spectrum-StatusLight--yellow={yellow}
class:spectrum-StatusLight--fuchsia={fuchsia}
@ -61,13 +61,17 @@
min-height: 0;
padding-top: 0;
padding-bottom: 0;
transition: color ease-out 130ms;
}
.spectrum-StatusLight.withText::before {
margin-right: 10px;
}
.spectrum-StatusLight::before {
transition: background-color ease-out 160ms;
}
.custom::before {
background: var(--color) !important;
background-color: var(--color) !important;
}
.square::before {
width: 14px;
@ -79,4 +83,14 @@
cursor: pointer;
color: var(--spectrum-global-color-gray-900);
}
.spectrum-StatusLight--sizeXS::before {
width: 10px;
height: 10px;
border-radius: 2px;
}
.spectrum-StatusLight--disabled::before {
background-color: var(--spectrum-global-color-gray-400) !important;
}
</style>

View File

@ -33,6 +33,5 @@
title="Confirm Deletion"
>
Are you sure you wish to delete the datasource
<i>{datasource.name}?</i>
This action cannot be undone.
<i>{datasource.name}</i>? This action cannot be undone.
</ConfirmDialog>

View File

@ -1,7 +1,7 @@
<script>
import { goto, params } from "@roxi/routify"
import { tables, datasources, screenStore } from "stores/builder"
import { Input, notifications } from "@budibase/bbui"
import { appStore, tables, datasources, screenStore } from "stores/builder"
import { InlineAlert, Link, Input, notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend"
@ -9,28 +9,41 @@
let confirmDeleteDialog
export const show = () => {
templateScreens = $screenStore.screens.filter(
screen => screen.autoTableId === table._id
)
willBeDeleted = ["All table data"].concat(
templateScreens.map(screen => `Screen ${screen.routing?.route || ""}`)
)
confirmDeleteDialog.show()
let screensPossiblyAffected = []
let viewsMessage = ""
let deleteTableName
const getViewsMessage = () => {
const views = Object.values(table?.views ?? [])
if (views.length < 1) {
return ""
}
if (views.length === 1) {
return ", including 1 view"
}
return `, including ${views.length} views`
}
let templateScreens
let willBeDeleted
let deleteTableName
export const show = () => {
viewsMessage = getViewsMessage()
screensPossiblyAffected = $screenStore.screens
.filter(
screen => screen.autoTableId === table._id && screen.routing?.route
)
.map(screen => ({
text: screen.routing.route,
url: `/builder/app/${$appStore.appId}/design/${screen._id}`,
}))
confirmDeleteDialog.show()
}
async function deleteTable() {
const isSelected = $params.tableId === table._id
try {
await tables.delete(table)
// Screens need deleted one at a time because of undo/redo
for (let screen of templateScreens) {
await screenStore.delete(screen)
}
if (table.sourceType === DB_TYPE_EXTERNAL) {
await datasources.fetch()
}
@ -46,6 +59,10 @@
function hideDeleteDialog() {
deleteTableName = ""
}
const autofillTableName = () => {
deleteTableName = table.name
}
</script>
<ConfirmDialog
@ -56,34 +73,103 @@
title="Confirm Deletion"
disabled={deleteTableName !== table.name}
>
<p>
Are you sure you wish to delete the table
<b>{table.name}?</b>
The following will also be deleted:
</p>
<b>
<div class="delete-items">
{#each willBeDeleted as item}
<div>{item}</div>
{/each}
</div>
</b>
<p>
This action cannot be undone - to continue please enter the table name below
to confirm.
</p>
<Input bind:value={deleteTableName} placeholder={table.name} />
<div class="content">
<p class="firstWarning">
Are you sure you wish to delete the table
<span class="tableNameLine">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<b on:click={autofillTableName} class="tableName">{table.name}</b>
<span>?</span>
</span>
</p>
<p class="secondWarning">All table data will be deleted{viewsMessage}.</p>
<p class="thirdWarning">This action <b>cannot be undone</b>.</p>
{#if screensPossiblyAffected.length > 0}
<div class="affectedScreens">
<InlineAlert
header="The following screens were originally generated from this table and may no longer function as expected"
>
<ul class="affectedScreensList">
{#each screensPossiblyAffected as item}
<li>
<Link quiet overBackground target="_blank" href={item.url}
>{item.text}</Link
>
</li>
{/each}
</ul>
</InlineAlert>
</div>
{/if}
<p class="fourthWarning">Please enter the app name below to confirm.</p>
<Input bind:value={deleteTableName} placeholder={table.name} />
</div>
</ConfirmDialog>
<style>
div.delete-items {
margin-top: 10px;
margin-bottom: 10px;
margin-left: 10px;
.content {
margin-top: 0;
max-width: 320px;
}
div.delete-items div {
.firstWarning {
margin: 0 0 12px;
max-width: 100%;
}
.tableNameLine {
display: inline-flex;
max-width: 100%;
vertical-align: bottom;
}
.tableName {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.secondWarning {
margin: 0;
max-width: 100%;
}
.thirdWarning {
margin: 0 0 12px;
max-width: 100%;
}
.affectedScreens {
margin: 18px 0;
max-width: 100%;
margin-bottom: 24px;
}
.affectedScreens :global(.spectrum-InLineAlert) {
max-width: 100%;
}
.affectedScreensList {
padding: 0;
margin-bottom: 0;
}
.affectedScreensList li {
display: block;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 4px;
font-weight: 600;
}
.fourthWarning {
margin: 12px 0 6px;
max-width: 100%;
}
</style>

View File

@ -0,0 +1,12 @@
<script>
import { RoleUtils } from "@budibase/frontend-core"
import { StatusLight } from "@budibase/bbui"
export let id
export let size = "M"
export let disabled = false
$: color = RoleUtils.getRoleColour(id)
</script>
<StatusLight square {disabled} {size} {color} />

View File

@ -1,108 +1,88 @@
<script>
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import DatasourceModal from "./DatasourceModal.svelte"
import ScreenRoleModal from "./ScreenRoleModal.svelte"
import sanitizeUrl from "helpers/sanitizeUrl"
import FormTypeModal from "./FormTypeModal.svelte"
import { Modal, notifications } from "@budibase/bbui"
import {
screenStore,
navigationStore,
tables,
permissions as permissionsStore,
builderStore,
} from "stores/builder"
import { auth } from "stores/portal"
import { get } from "svelte/store"
import getTemplates from "templates"
import { Roles } from "constants/backend"
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 gridListScreen from "templates/gridListScreen"
import gridScreen from "templates/gridScreen"
import gridDetailsScreen from "templates/gridDetailsScreen"
import { Roles } from "constants/backend"
let mode
let pendingScreen
// Modal refs
let screenDetailsModal
let datasourceModal
let screenAccessRoleModal
let formTypeModal
// Cache variables for workflow
let screenAccessRole = Roles.BASIC
let selectedTablesAndViews = []
let permissions = {}
let templates = null
let screens = null
export const show = newMode => {
mode = newMode
selectedTablesAndViews = []
permissions = {}
let selectedDatasources = null
let blankScreenUrl = null
let screenMode = null
let formType = null
// Creates an array of screens, checking and sanitising their URLs
const createScreens = async ({ screens, screenAccessRole }) => {
if (!screens?.length) {
return
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
datasourceModal.show()
} else if (mode === "blank") {
screenDetailsModal.show()
} else {
throw new Error("Invalid mode provided")
}
}
const createScreen = async screen => {
try {
let createdScreens = []
for (let screen of screens) {
// Check we aren't clashing with an existing URL
if (hasExistingUrl(screen.routing.route)) {
let suffix = 2
let candidateUrl = makeCandidateUrl(screen, suffix)
while (hasExistingUrl(candidateUrl)) {
candidateUrl = makeCandidateUrl(screen, ++suffix)
}
screen.routing.route = candidateUrl
// 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)
}
// Sanitise URL
screen.routing.route = sanitizeUrl(screen.routing.route)
// Use the currently selected role
if (!screenAccessRole) {
return
}
screen.routing.roleId = screenAccessRole
// Create the screen
const response = await screenStore.save(screen)
createdScreens.push(response)
// Add link in layout. We only ever actually create 1 screen now, even
// for autoscreens, so it's always safe to do this.
await navigationStore.saveLink(
screen.routing.route,
capitalise(screen.routing.route.split("/")[1]),
screenAccessRole
)
screen.routing.route = candidateUrl
}
return createdScreens
screen.routing.route = sanitizeUrl(screen.routing.route)
return await screenStore.save(screen)
} 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
)
// Checks if any screens exist in the store with the given route and
// currently selected role
const hasExistingUrl = url => {
const roleId = screenAccessRole
const hasExistingUrl = (url, screenAccessRole) => {
const screens = get(screenStore).screens.filter(
s => s.routing.roleId === roleId
s => s.routing.roleId === screenAccessRole
)
return !!screens.find(s => s.routing?.route === url)
}
// Constructs a candidate URL for a new screen, suffixing the base of the
// screen's URL with a given suffix.
// 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 || ""
@ -117,105 +97,79 @@
}
}
// Handler for NewScreenModal
export const show = newMode => {
mode = newMode
templates = null
screens = null
selectedDatasources = null
blankScreenUrl = null
screenMode = mode
pendingScreen = null
screenAccessRole = Roles.BASIC
formType = null
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
datasourceModal.show()
} else if (mode === "blank") {
let templates = getTemplates($tables.list)
const blankScreenTemplate = templates.find(
t => t.id === "createFromScratch"
)
pendingScreen = blankScreenTemplate.create()
screenDetailsModal.show()
} else {
throw new Error("Invalid mode provided")
}
}
// Handler for DatasourceModal confirmation, move to screen access select
const confirmScreenDatasources = async ({ datasources }) => {
selectedDatasources = datasources
if (screenMode === "form") {
const onSelectDatasources = async () => {
if (mode === "form") {
formTypeModal.show()
} else {
screenAccessRoleModal.show()
} else if (mode === "grid") {
await createGridScreen()
} else if (mode === "gridDetails") {
await createGridDetailsScreen()
}
}
// Handler for Datasource Screen Creation
const completeDatasourceScreenCreation = async () => {
templates =
mode === "grid"
? gridListScreen(selectedDatasources)
: gridDetailsScreen(selectedDatasources)
const createBlankScreen = async ({ screenUrl }) => {
const screenTemplate = blankScreen(screenUrl)
const screen = await createScreen(screenTemplate)
await addNavigationLink(screenTemplate)
const screens = templates.map(template => {
let screenTemplate = template.create()
screenTemplate.autoTableId = template.resourceId
return screenTemplate
})
const createdScreens = await createScreens({ screens, screenAccessRole })
loadNewScreen(createdScreens)
loadNewScreen(screen)
}
const confirmScreenBlank = async ({ screenUrl }) => {
blankScreenUrl = screenUrl
screenAccessRoleModal.show()
}
const createGridScreen = async () => {
let firstScreen = null
// Submit request for a blank screen
const confirmBlankScreenCreation = async ({
screenUrl,
screenAccessRole,
}) => {
if (!pendingScreen) {
return
}
pendingScreen.routing.route = screenUrl
const createdScreens = await createScreens({
screens: [pendingScreen],
screenAccessRole,
})
loadNewScreen(createdScreens)
}
for (let tableOrView of selectedTablesAndViews) {
const screenTemplate = gridScreen(
tableOrView,
permissions[tableOrView.id]
)
const onConfirmFormType = () => {
screenAccessRoleModal.show()
}
const screen = await createScreen(screenTemplate)
await addNavigationLink(screen)
const loadNewScreen = createdScreens => {
const lastScreen = createdScreens.slice(-1)[0]
// Go to new screen
if (lastScreen?.props?._children.length) {
// Focus on the main component for the streen type
const mainComponent = lastScreen?.props?._children?.[0]._id
$goto(`./${lastScreen._id}/${mainComponent}`)
} else {
$goto(`./${lastScreen._id}`)
firstScreen ??= screen
}
screenStore.select(lastScreen._id)
loadNewScreen(firstScreen)
}
const confirmFormScreenCreation = async () => {
templates = formScreen(selectedDatasources, { actionType: formType })
screens = templates.map(template => {
let screenTemplate = template.create()
return screenTemplate
})
const createdScreens = await createScreens({ screens, screenAccessRole })
const createGridDetailsScreen = async () => {
let firstScreen = null
for (let tableOrView of selectedTablesAndViews) {
const screenTemplate = gridDetailsScreen(
tableOrView,
permissions[tableOrView.id]
)
const screen = await createScreen(screenTemplate)
await addNavigationLink(screen)
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") {
const associatedTour =
@ -229,66 +183,89 @@
}
}
// Go to new screen
loadNewScreen(createdScreens)
loadNewScreen(firstScreen)
}
// Submit screen config for creation.
const confirmScreenCreation = async () => {
if (screenMode === "blank") {
confirmBlankScreenCreation({
screenUrl: blankScreenUrl,
screenAccessRole,
})
} else if (screenMode === "form") {
confirmFormScreenCreation()
const loadNewScreen = screen => {
if (screen?.props?._children.length) {
// Focus on the main component for the screen type
const mainComponent = screen?.props?._children?.[0]._id
$goto(`./${screen._id}/${mainComponent}`)
} else {
completeDatasourceScreenCreation()
$goto(`./${screen._id}`)
}
screenStore.select(screen._id)
}
const roleSelectBack = () => {
if (screenMode === "blank") {
screenDetailsModal.show()
const fetchPermission = resourceId => {
permissions[resourceId] = { loading: true, read: null, write: null }
permissionsStore
.forResource(resourceId)
.then(permission => {
if (permissions[resourceId]?.loading) {
permissions[resourceId] = {
loading: false,
read: permission?.read?.role,
write: permission?.write?.role,
}
}
})
.catch(e => {
console.error("Error fetching permission data: ", e)
if (permissions[resourceId]?.loading) {
permissions[resourceId] = {
loading: false,
read: Roles.PUBLIC,
write: Roles.PUBLIC,
}
}
})
}
const deletePermission = resourceId => {
delete permissions[resourceId]
permissions = permissions
}
const handleTableOrViewToggle = ({ detail: tableOrView }) => {
const alreadySelected = selectedTablesAndViews.some(
selected => selected.id === tableOrView.id
)
if (!alreadySelected) {
fetchPermission(tableOrView.id)
selectedTablesAndViews = [...selectedTablesAndViews, tableOrView]
} else {
datasourceModal.show()
deletePermission(tableOrView.id)
selectedTablesAndViews = selectedTablesAndViews.filter(
selected => selected.id !== tableOrView.id
)
}
}
</script>
<Modal bind:this={datasourceModal} autoFocus={false}>
<DatasourceModal {mode} onConfirm={confirmScreenDatasources} />
</Modal>
<Modal bind:this={screenAccessRoleModal}>
<ScreenRoleModal
onConfirm={() => {
confirmScreenCreation()
}}
bind:screenAccessRole
onCancel={roleSelectBack}
screenUrl={blankScreenUrl}
confirmText={screenMode === "form" ? "Confirm" : "Done"}
<DatasourceModal
{selectedTablesAndViews}
{permissions}
onConfirm={onSelectDatasources}
on:toggle={handleTableOrViewToggle}
/>
</Modal>
<Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal
onConfirm={confirmScreenBlank}
initialUrl={blankScreenUrl}
/>
<ScreenDetailsModal onConfirm={createBlankScreen} />
</Modal>
<Modal bind:this={formTypeModal}>
<FormTypeModal
onConfirm={onConfirmFormType}
onConfirm={createFormScreen}
onCancel={() => {
formTypeModal.hide()
datasourceModal.show()
}}
on:select={e => {
formType = e.detail
}}
type={formType}
/>
</Modal>

View File

@ -1,42 +1,95 @@
<script>
import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
import { datasources } from "stores/builder"
import { datasources as datasourcesStore } from "stores/builder"
import ICONS from "components/backend/DatasourceNavigator/icons"
import { IntegrationNames } from "constants"
import { onMount } from "svelte"
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte"
import { createEventDispatcher, onMount } from "svelte"
import TableOrViewOption from "./TableOrViewOption.svelte"
export let onCancel
export let onConfirm
export let selectedTablesAndViews
export let permissions
let selectedSources = []
const dispatch = createEventDispatcher()
$: filteredSources = $datasources.list?.filter(datasource => {
return datasource.source !== IntegrationNames.REST && datasource["entities"]
})
const toggleSelection = datasource => {
const exists = selectedSources.find(
d => d.resourceId === datasource.resourceId
const getViews = table => {
const views = Object.values(table.views || {}).filter(
view => view.version === 2
)
if (exists) {
selectedSources = selectedSources.filter(
d => d.resourceId === datasource.resourceId
)
} else {
selectedSources = [...selectedSources, datasource]
}
return views.map(view => ({
icon: "Remove",
name: view.name,
id: view.id,
clientData: {
...view,
type: "viewV2",
label: view.name,
},
}))
}
const confirmDatasourceSelection = async () => {
await onConfirm({
datasources: selectedSources,
})
const getTablesAndViews = datasource => {
let tablesAndViews = []
const rawTables = Array.isArray(datasource.entities)
? datasource.entities
: Object.values(datasource.entities ?? {})
for (const rawTable of rawTables) {
if (rawTable._id === "ta_users") {
continue
}
const table = {
icon: "Table",
name: rawTable.name,
id: rawTable._id,
clientData: {
...rawTable,
label: rawTable.name,
tableId: rawTable._id,
type: "table",
},
}
tablesAndViews = tablesAndViews.concat([table, ...getViews(rawTable)])
}
return tablesAndViews
}
const getDatasources = rawDatasources => {
const datasources = []
for (const rawDatasource of rawDatasources) {
if (
rawDatasource.source === IntegrationNames.REST ||
!rawDatasource["entities"]
) {
continue
}
const datasource = {
name: rawDatasource.name,
iconComponent: ICONS[rawDatasource.source],
tablesAndViews: getTablesAndViews(rawDatasource),
}
datasources.push(datasource)
}
return datasources
}
$: datasources = getDatasources($datasourcesStore.list)
const toggleSelection = tableOrView => {
dispatch("toggle", tableOrView)
}
onMount(async () => {
try {
await datasources.fetch()
await datasourcesStore.fetch()
} catch (error) {
notifications.error("Error fetching datasources")
}
@ -48,66 +101,35 @@
title="Autogenerated screens"
confirmText="Confirm"
cancelText="Back"
onConfirm={confirmDatasourceSelection}
{onCancel}
disabled={!selectedSources.length}
{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 filteredSources as datasource}
{@const entities = Array.isArray(datasource.entities)
? datasource.entities
: Object.values(datasource.entities || {})}
{#each datasources as datasource}
<div class="data-source-wrap">
<div class="data-source-header">
<svelte:component
this={ICONS[datasource.source]}
this={datasource.iconComponent}
height="24"
width="24"
/>
<div class="data-source-name">{datasource.name}</div>
</div>
<!-- List all tables -->
{#each entities.filter(table => table._id !== "ta_users") as table}
{@const views = Object.values(table.views || {}).filter(
view => view.version === 2
{#each datasource.tablesAndViews as tableOrView}
{@const selected = selectedTablesAndViews.some(
selected => selected.id === tableOrView.id
)}
{@const tableDS = {
tableId: table._id,
label: table.name,
resourceId: table._id,
type: "table",
}}
{@const selected = selectedSources.find(
datasource => datasource.resourceId === tableDS.resourceId
)}
<DatasourceTemplateRow
on:click={() => toggleSelection(tableDS)}
<TableOrViewOption
roles={permissions[tableOrView.id]}
on:click={() => toggleSelection(tableOrView)}
{selected}
datasource={tableDS}
{tableOrView}
/>
<!-- List all views inside this table -->
{#each views as view}
{@const viewDS = {
label: view.name,
id: view.id,
resourceId: view.id,
tableId: view.tableId,
type: "viewV2",
}}
{@const selected = selectedSources.find(
x => x.resourceId === viewDS.resourceId
)}
<DatasourceTemplateRow
on:click={() => toggleSelection(viewDS)}
{selected}
datasource={viewDS}
/>
{/each}
{/each}
</div>
{/each}
@ -118,8 +140,11 @@
<style>
.data-source-wrap {
padding-bottom: var(--spectrum-alias-item-padding-s);
display: grid;
display: flex;
flex-direction: column;
grid-gap: var(--spacing-s);
max-width: 100%;
min-width: 0;
}
.data-source-header {
display: flex;

View File

@ -1,45 +0,0 @@
<script>
import { Icon } from "@budibase/bbui"
export let datasource
export let selected = false
$: icon = datasource.type === "viewV2" ? "Remove" : "Table"
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="data-source-entry" class:selected on:click>
<Icon name={icon} color="var(--spectrum-global-color-gray-600)" />
{datasource.label}
{#if selected}
<span class="data-source-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
<style>
.data-source-entry {
cursor: pointer;
grid-gap: var(--spacing-m);
padding: var(--spectrum-alias-item-padding-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;
align-items: center;
}
.data-source-entry:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.data-source-check {
margin-left: auto;
}
.data-source-check :global(.spectrum-Icon) {
color: var(--spectrum-global-color-green-600);
}
</style>

View File

@ -1,12 +1,10 @@
<script>
import { ModalContent, Layout, Body, Icon } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
let type = null
export let onCancel = () => {}
export let onConfirm = () => {}
export let type
const dispatch = createEventDispatcher()
</script>
<span>
@ -14,7 +12,7 @@
title="Select form type"
confirmText="Done"
cancelText="Back"
{onConfirm}
onConfirm={() => onConfirm(type)}
{onCancel}
disabled={!type}
size="L"
@ -25,9 +23,7 @@
<div
class="form-type"
class:selected={type === "Create"}
on:click={() => {
dispatch("select", "Create")
}}
on:click={() => (type = "Create")}
>
<div class="form-type-wrap">
<div class="form-type-content">
@ -46,9 +42,7 @@
<div
class="form-type"
class:selected={type === "Update"}
on:click={() => {
dispatch("select", "Update")
}}
on:click={() => (type = "Update")}
>
<div class="form-type-wrap">
<div class="form-type-content">
@ -65,9 +59,7 @@
<div
class="form-type"
class:selected={type === "View"}
on:click={() => {
dispatch("select", "View")
}}
on:click={() => (type = "View")}
>
<div class="form-type-wrap">
<div class="form-type-content">

View File

@ -1,62 +0,0 @@
<script>
import { Select, ModalContent } from "@budibase/bbui"
import { RoleUtils } from "@budibase/frontend-core"
import { roles, screenStore } from "stores/builder"
import { get } from "svelte/store"
import { onMount } from "svelte"
export let onConfirm
export let onCancel
export let screenUrl
export let screenAccessRole
export let confirmText = "Done"
let error
const onChangeRole = e => {
const roleId = e.detail
if (routeExists(screenUrl, roleId)) {
error = "This URL is already taken for this access role"
} else {
error = null
}
}
const routeExists = (url, role) => {
if (!url || !role) {
return false
}
return get(screenStore).screens.some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === role
)
}
onMount(() => {
// Validate the initial role
onChangeRole({ detail: screenAccessRole })
})
</script>
<ModalContent
title="Access"
{confirmText}
cancelText="Back"
{onConfirm}
{onCancel}
disabled={!!error}
>
Select the level of access required to see these screens
<Select
bind:value={screenAccessRole}
on:change={onChangeRole}
label="Access"
{error}
getOptionLabel={role => role.name}
getOptionValue={role => role._id}
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
options={$roles}
placeholder={null}
/>
</ModalContent>

View File

@ -0,0 +1,112 @@
<script>
import { Icon, AbsTooltip } from "@budibase/bbui"
import RoleIcon from "components/common/RoleIcon.svelte"
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>
</div>
<style>
.datasource {
cursor: pointer;
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;
grid-gap: var(--spacing-m);
min-width: 0;
}
.content span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.datasource:hover {
border: 1px solid var(--grey-5);
}
.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

@ -63,6 +63,11 @@ export class Screen extends BaseStructure {
return this
}
autoTableId(autoTableId) {
this._json.autoTableId = autoTableId
return this
}
instanceName(name) {
this._json.props._instanceName = name
return this

View File

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

View File

@ -1,12 +0,0 @@
import { Screen } from "./Screen"
export default {
name: `Create from scratch`,
id: `createFromScratch`,
create: () => createScreen(),
table: `Create from scratch`,
}
const createScreen = () => {
return new Screen().instanceName("New Screen").json()
}

View File

@ -3,41 +3,47 @@ import { Component } from "./Component"
import sanitizeUrl from "helpers/sanitizeUrl"
export const FORM_TEMPLATE = "FORM_TEMPLATE"
export const formUrl = datasource => sanitizeUrl(`/${datasource.label}-form`)
// Mode not really necessary
export default function (datasources, config) {
if (!Array.isArray(datasources)) {
return []
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`)
}
return datasources.map(datasource => {
return {
name: `${datasource.label} - Form`,
create: () => createScreen(datasource, config),
id: FORM_TEMPLATE,
resourceId: datasource.resourceId,
}
})
}
const generateMultistepFormBlock = (dataSource, { actionType } = {}) => {
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,
dataSource: tableOrView.clientData,
steps: [{}],
rowId: actionType === "new" ? undefined : `{{ url.id }}`,
})
.instanceName(`${dataSource.label} - Multistep Form block`)
.instanceName(`${tableOrView.name} - Multistep Form block`)
return multistepFormBlock
}
const createScreen = (datasource, config) => {
const createScreen = (tableOrView, actionType, permissions) => {
return new Screen()
.route(formUrl(datasource))
.instanceName(`${datasource.label} - Form`)
.addChild(generateMultistepFormBlock(datasource, config))
.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

@ -5,24 +5,9 @@ import { generate } from "shortid"
import { makePropSafe as safe } from "@budibase/string-templates"
import { Utils } from "@budibase/frontend-core"
export default function (datasources) {
if (!Array.isArray(datasources)) {
return []
}
return datasources.map(datasource => {
return {
name: `${datasource.label} - List with panel`,
create: () => createScreen(datasource),
id: GRID_DETAILS_TEMPLATE,
resourceId: datasource.resourceId,
}
})
}
const gridDetailsUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
export const GRID_DETAILS_TEMPLATE = "GRID_DETAILS_TEMPLATE"
export const gridDetailsUrl = datasource => sanitizeUrl(`/${datasource.label}`)
const createScreen = datasource => {
const createScreen = (tableOrView, permissions) => {
/*
Create Row
*/
@ -47,7 +32,7 @@ const createScreen = datasource => {
type: "cta",
})
buttonGroup.instanceName(`${datasource.label} - Create`).customProps({
buttonGroup.instanceName(`${tableOrView.name} - Create`).customProps({
hAlign: "right",
buttons: [createButton.json()],
})
@ -62,7 +47,7 @@ const createScreen = datasource => {
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: datasource?.label,
text: tableOrView.name,
})
gridHeader.addChild(heading)
@ -72,7 +57,7 @@ const createScreen = datasource => {
"@budibase/standard-components/formblock"
)
createFormBlock.instanceName("Create row form block").customProps({
dataSource: datasource,
dataSource: tableOrView.clientData,
labelPosition: "left",
buttonPosition: "top",
actionType: "Create",
@ -83,7 +68,7 @@ const createScreen = datasource => {
showSaveButton: true,
saveButtonLabel: "Save",
actionType: "Create",
dataSource: datasource,
dataSource: tableOrView.clientData,
}),
})
@ -99,7 +84,7 @@ const createScreen = datasource => {
const editFormBlock = new Component("@budibase/standard-components/formblock")
editFormBlock.instanceName("Edit row form block").customProps({
dataSource: datasource,
dataSource: tableOrView.clientData,
labelPosition: "left",
buttonPosition: "top",
actionType: "Update",
@ -112,7 +97,7 @@ const createScreen = datasource => {
saveButtonLabel: "Save",
deleteButtonLabel: "Delete",
actionType: "Update",
dataSource: datasource,
dataSource: tableOrView.clientData,
}),
})
@ -121,7 +106,7 @@ const createScreen = datasource => {
const gridBlock = new Component("@budibase/standard-components/gridblock")
gridBlock
.customProps({
table: datasource,
table: tableOrView.clientData,
allowAddRows: false,
allowEditRows: false,
allowDeleteRows: false,
@ -145,14 +130,18 @@ const createScreen = datasource => {
},
],
})
.instanceName(`${datasource.label} - Table`)
.instanceName(`${tableOrView.name} - Table`)
return new Screen()
.route(gridDetailsUrl(datasource))
.instanceName(`${datasource.label} - List and details`)
.route(gridDetailsUrl(tableOrView))
.instanceName(`${tableOrView.name} - List and details`)
.role(permissions.write)
.autoTableId(tableOrView.resourceId)
.addChild(gridHeader)
.addChild(gridBlock)
.addChild(createRowSidePanel)
.addChild(detailsSidePanel)
.json()
}
export default createScreen

View File

@ -1,41 +0,0 @@
import sanitizeUrl from "helpers/sanitizeUrl"
import { Screen } from "./Screen"
import { Component } from "./Component"
export default function (datasources) {
if (!Array.isArray(datasources)) {
return []
}
return datasources.map(datasource => {
return {
name: `${datasource.label} - List`,
create: () => createScreen(datasource),
id: GRID_LIST_TEMPLATE,
resourceId: datasource.resourceId,
}
})
}
export const GRID_LIST_TEMPLATE = "GRID_LIST_TEMPLATE"
export const gridListUrl = datasource => sanitizeUrl(`/${datasource.label}`)
const createScreen = datasource => {
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: datasource?.label,
})
const gridBlock = new Component("@budibase/standard-components/gridblock")
.instanceName(`${datasource.label} - Table`)
.customProps({
table: datasource,
})
return new Screen()
.route(gridListUrl(datasource))
.instanceName(`${datasource.label} - List`)
.addChild(heading)
.addChild(gridBlock)
.json()
}

View File

@ -0,0 +1,30 @@
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,35 +0,0 @@
import gridListScreen from "./gridListScreen"
import gridDetailsScreen from "./gridDetailsScreen"
import createFromScratchScreen from "./createFromScratchScreen"
import formScreen from "./formScreen"
const allTemplates = datasources => [
...gridListScreen(datasources),
...gridDetailsScreen(datasources),
...formScreen(datasources),
]
// Allows us to apply common behaviour to all create() functions
const createTemplateOverride = template => () => {
const screen = template.create()
screen.name = screen.props._id
screen.routing.route = screen.routing.route.toLowerCase()
screen.template = template.id
return screen
}
export default datasources => {
const enrichTemplate = template => ({
...template,
create: createTemplateOverride(template),
})
const fromScratch = enrichTemplate(createFromScratchScreen)
const tableTemplates = allTemplates(datasources).map(enrichTemplate)
return [
fromScratch,
...tableTemplates.sort((templateA, templateB) => {
return templateA.name > templateB.name ? 1 : -1
}),
]
}