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:
parent
9a181c9707
commit
cf31c7ba3f
|
@ -36,9 +36,11 @@
|
||||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="spectrum-InLineAlert-header">{header}</div>
|
<div class="spectrum-InLineAlert-header">{header}</div>
|
||||||
{#each split as splitMsg}
|
<slot>
|
||||||
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
|
{#each split as splitMsg}
|
||||||
{/each}
|
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
|
||||||
|
{/each}
|
||||||
|
</slot>
|
||||||
{#if onConfirm}
|
{#if onConfirm}
|
||||||
<div class="spectrum-InLineAlert-footer button">
|
<div class="spectrum-InLineAlert-footer button">
|
||||||
<Button {cta} secondary={cta ? false : true} on:click={onConfirm}
|
<Button {cta} secondary={cta ? false : true} on:click={onConfirm}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
class:custom={!!color}
|
class:custom={!!color}
|
||||||
class:square
|
class:square
|
||||||
class:hoverable
|
class:hoverable
|
||||||
style={`--color: ${color};`}
|
style={`--color: ${color ?? "var(--spectrum-global-color-gray-400)"};`}
|
||||||
class:spectrum-StatusLight--celery={celery}
|
class:spectrum-StatusLight--celery={celery}
|
||||||
class:spectrum-StatusLight--yellow={yellow}
|
class:spectrum-StatusLight--yellow={yellow}
|
||||||
class:spectrum-StatusLight--fuchsia={fuchsia}
|
class:spectrum-StatusLight--fuchsia={fuchsia}
|
||||||
|
@ -61,13 +61,17 @@
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
transition: color ease-out 130ms;
|
|
||||||
}
|
}
|
||||||
.spectrum-StatusLight.withText::before {
|
.spectrum-StatusLight.withText::before {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-StatusLight::before {
|
||||||
|
transition: background-color ease-out 160ms;
|
||||||
|
}
|
||||||
|
|
||||||
.custom::before {
|
.custom::before {
|
||||||
background: var(--color) !important;
|
background-color: var(--color) !important;
|
||||||
}
|
}
|
||||||
.square::before {
|
.square::before {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
|
@ -79,4 +83,14 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--spectrum-global-color-gray-900);
|
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>
|
</style>
|
||||||
|
|
|
@ -33,6 +33,5 @@
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
>
|
>
|
||||||
Are you sure you wish to delete the datasource
|
Are you sure you wish to delete the datasource
|
||||||
<i>{datasource.name}?</i>
|
<i>{datasource.name}</i>? This action cannot be undone.
|
||||||
This action cannot be undone.
|
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { tables, datasources, screenStore } from "stores/builder"
|
import { appStore, tables, datasources, screenStore } from "stores/builder"
|
||||||
import { Input, notifications } from "@budibase/bbui"
|
import { InlineAlert, Link, Input, notifications } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { DB_TYPE_EXTERNAL } from "constants/backend"
|
import { DB_TYPE_EXTERNAL } from "constants/backend"
|
||||||
|
|
||||||
|
@ -9,28 +9,41 @@
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
|
||||||
export const show = () => {
|
let screensPossiblyAffected = []
|
||||||
templateScreens = $screenStore.screens.filter(
|
let viewsMessage = ""
|
||||||
screen => screen.autoTableId === table._id
|
let deleteTableName
|
||||||
)
|
|
||||||
willBeDeleted = ["All table data"].concat(
|
const getViewsMessage = () => {
|
||||||
templateScreens.map(screen => `Screen ${screen.routing?.route || ""}`)
|
const views = Object.values(table?.views ?? [])
|
||||||
)
|
if (views.length < 1) {
|
||||||
confirmDeleteDialog.show()
|
return ""
|
||||||
|
}
|
||||||
|
if (views.length === 1) {
|
||||||
|
return ", including 1 view"
|
||||||
|
}
|
||||||
|
|
||||||
|
return `, including ${views.length} views`
|
||||||
}
|
}
|
||||||
|
|
||||||
let templateScreens
|
export const show = () => {
|
||||||
let willBeDeleted
|
viewsMessage = getViewsMessage()
|
||||||
let deleteTableName
|
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() {
|
async function deleteTable() {
|
||||||
const isSelected = $params.tableId === table._id
|
const isSelected = $params.tableId === table._id
|
||||||
try {
|
try {
|
||||||
await tables.delete(table)
|
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) {
|
if (table.sourceType === DB_TYPE_EXTERNAL) {
|
||||||
await datasources.fetch()
|
await datasources.fetch()
|
||||||
}
|
}
|
||||||
|
@ -46,6 +59,10 @@
|
||||||
function hideDeleteDialog() {
|
function hideDeleteDialog() {
|
||||||
deleteTableName = ""
|
deleteTableName = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autofillTableName = () => {
|
||||||
|
deleteTableName = table.name
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
@ -56,34 +73,103 @@
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
disabled={deleteTableName !== table.name}
|
disabled={deleteTableName !== table.name}
|
||||||
>
|
>
|
||||||
<p>
|
<div class="content">
|
||||||
Are you sure you wish to delete the table
|
<p class="firstWarning">
|
||||||
<b>{table.name}?</b>
|
Are you sure you wish to delete the table
|
||||||
The following will also be deleted:
|
<span class="tableNameLine">
|
||||||
</p>
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<b>
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="delete-items">
|
<b on:click={autofillTableName} class="tableName">{table.name}</b>
|
||||||
{#each willBeDeleted as item}
|
<span>?</span>
|
||||||
<div>{item}</div>
|
</span>
|
||||||
{/each}
|
</p>
|
||||||
</div>
|
|
||||||
</b>
|
<p class="secondWarning">All table data will be deleted{viewsMessage}.</p>
|
||||||
<p>
|
<p class="thirdWarning">This action <b>cannot be undone</b>.</p>
|
||||||
This action cannot be undone - to continue please enter the table name below
|
|
||||||
to confirm.
|
{#if screensPossiblyAffected.length > 0}
|
||||||
</p>
|
<div class="affectedScreens">
|
||||||
<Input bind:value={deleteTableName} placeholder={table.name} />
|
<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>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div.delete-items {
|
.content {
|
||||||
margin-top: 10px;
|
margin-top: 0;
|
||||||
margin-bottom: 10px;
|
max-width: 320px;
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
margin-top: 4px;
|
||||||
font-weight: 600;
|
}
|
||||||
|
|
||||||
|
.fourthWarning {
|
||||||
|
margin: 12px 0 6px;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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} />
|
|
@ -1,108 +1,88 @@
|
||||||
<script>
|
<script>
|
||||||
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||||
import DatasourceModal from "./DatasourceModal.svelte"
|
import DatasourceModal from "./DatasourceModal.svelte"
|
||||||
import ScreenRoleModal from "./ScreenRoleModal.svelte"
|
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||||
import FormTypeModal from "./FormTypeModal.svelte"
|
import FormTypeModal from "./FormTypeModal.svelte"
|
||||||
import { Modal, notifications } from "@budibase/bbui"
|
import { Modal, notifications } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
screenStore,
|
screenStore,
|
||||||
navigationStore,
|
navigationStore,
|
||||||
tables,
|
permissions as permissionsStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import getTemplates from "templates"
|
|
||||||
import { Roles } from "constants/backend"
|
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
|
import blankScreen from "templates/blankScreen"
|
||||||
import formScreen from "templates/formScreen"
|
import formScreen from "templates/formScreen"
|
||||||
import gridListScreen from "templates/gridListScreen"
|
import gridScreen from "templates/gridScreen"
|
||||||
import gridDetailsScreen from "templates/gridDetailsScreen"
|
import gridDetailsScreen from "templates/gridDetailsScreen"
|
||||||
|
import { Roles } from "constants/backend"
|
||||||
|
|
||||||
let mode
|
let mode
|
||||||
let pendingScreen
|
|
||||||
|
|
||||||
// Modal refs
|
|
||||||
let screenDetailsModal
|
let screenDetailsModal
|
||||||
let datasourceModal
|
let datasourceModal
|
||||||
let screenAccessRoleModal
|
|
||||||
let formTypeModal
|
let formTypeModal
|
||||||
|
|
||||||
// Cache variables for workflow
|
let selectedTablesAndViews = []
|
||||||
let screenAccessRole = Roles.BASIC
|
let permissions = {}
|
||||||
|
|
||||||
let templates = null
|
export const show = newMode => {
|
||||||
let screens = null
|
mode = newMode
|
||||||
|
selectedTablesAndViews = []
|
||||||
|
permissions = {}
|
||||||
|
|
||||||
let selectedDatasources = null
|
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
|
||||||
let blankScreenUrl = null
|
datasourceModal.show()
|
||||||
let screenMode = null
|
} else if (mode === "blank") {
|
||||||
let formType = null
|
screenDetailsModal.show()
|
||||||
|
} else {
|
||||||
// Creates an array of screens, checking and sanitising their URLs
|
throw new Error("Invalid mode provided")
|
||||||
const createScreens = async ({ screens, screenAccessRole }) => {
|
|
||||||
if (!screens?.length) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createScreen = async screen => {
|
||||||
try {
|
try {
|
||||||
let createdScreens = []
|
// Check we aren't clashing with an existing URL
|
||||||
|
if (hasExistingUrl(screen.routing.route, screen.routing.roleId)) {
|
||||||
for (let screen of screens) {
|
let suffix = 2
|
||||||
// Check we aren't clashing with an existing URL
|
let candidateUrl = makeCandidateUrl(screen, suffix)
|
||||||
if (hasExistingUrl(screen.routing.route)) {
|
while (hasExistingUrl(candidateUrl, screen.routing.roleId)) {
|
||||||
let suffix = 2
|
candidateUrl = makeCandidateUrl(screen, ++suffix)
|
||||||
let candidateUrl = makeCandidateUrl(screen, suffix)
|
|
||||||
while (hasExistingUrl(candidateUrl)) {
|
|
||||||
candidateUrl = makeCandidateUrl(screen, ++suffix)
|
|
||||||
}
|
|
||||||
screen.routing.route = candidateUrl
|
|
||||||
}
|
}
|
||||||
|
screen.routing.route = candidateUrl
|
||||||
// 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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdScreens
|
screen.routing.route = sanitizeUrl(screen.routing.route)
|
||||||
|
|
||||||
|
return await screenStore.save(screen)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error("Error creating screens")
|
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
|
// Checks if any screens exist in the store with the given route and
|
||||||
// currently selected role
|
// currently selected role
|
||||||
const hasExistingUrl = url => {
|
const hasExistingUrl = (url, screenAccessRole) => {
|
||||||
const roleId = screenAccessRole
|
|
||||||
const screens = get(screenStore).screens.filter(
|
const screens = get(screenStore).screens.filter(
|
||||||
s => s.routing.roleId === roleId
|
s => s.routing.roleId === screenAccessRole
|
||||||
)
|
)
|
||||||
return !!screens.find(s => s.routing?.route === url)
|
return !!screens.find(s => s.routing?.route === url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructs a candidate URL for a new screen, suffixing the base of the
|
// Constructs a candidate URL for a new screen, appending a given suffix to the
|
||||||
// screen's URL with a given suffix.
|
// screen's URL
|
||||||
// e.g. "/sales/:id" => "/sales-1/:id"
|
// e.g. "/sales/:id" => "/sales-1/:id"
|
||||||
const makeCandidateUrl = (screen, suffix) => {
|
const makeCandidateUrl = (screen, suffix) => {
|
||||||
let url = screen.routing?.route || ""
|
let url = screen.routing?.route || ""
|
||||||
|
@ -117,105 +97,79 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler for NewScreenModal
|
const onSelectDatasources = async () => {
|
||||||
export const show = newMode => {
|
if (mode === "form") {
|
||||||
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") {
|
|
||||||
formTypeModal.show()
|
formTypeModal.show()
|
||||||
} else {
|
} else if (mode === "grid") {
|
||||||
screenAccessRoleModal.show()
|
await createGridScreen()
|
||||||
|
} else if (mode === "gridDetails") {
|
||||||
|
await createGridDetailsScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler for Datasource Screen Creation
|
const createBlankScreen = async ({ screenUrl }) => {
|
||||||
const completeDatasourceScreenCreation = async () => {
|
const screenTemplate = blankScreen(screenUrl)
|
||||||
templates =
|
const screen = await createScreen(screenTemplate)
|
||||||
mode === "grid"
|
await addNavigationLink(screenTemplate)
|
||||||
? gridListScreen(selectedDatasources)
|
|
||||||
: gridDetailsScreen(selectedDatasources)
|
|
||||||
|
|
||||||
const screens = templates.map(template => {
|
loadNewScreen(screen)
|
||||||
let screenTemplate = template.create()
|
|
||||||
screenTemplate.autoTableId = template.resourceId
|
|
||||||
return screenTemplate
|
|
||||||
})
|
|
||||||
const createdScreens = await createScreens({ screens, screenAccessRole })
|
|
||||||
loadNewScreen(createdScreens)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmScreenBlank = async ({ screenUrl }) => {
|
const createGridScreen = async () => {
|
||||||
blankScreenUrl = screenUrl
|
let firstScreen = null
|
||||||
screenAccessRoleModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit request for a blank screen
|
for (let tableOrView of selectedTablesAndViews) {
|
||||||
const confirmBlankScreenCreation = async ({
|
const screenTemplate = gridScreen(
|
||||||
screenUrl,
|
tableOrView,
|
||||||
screenAccessRole,
|
permissions[tableOrView.id]
|
||||||
}) => {
|
)
|
||||||
if (!pendingScreen) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pendingScreen.routing.route = screenUrl
|
|
||||||
const createdScreens = await createScreens({
|
|
||||||
screens: [pendingScreen],
|
|
||||||
screenAccessRole,
|
|
||||||
})
|
|
||||||
loadNewScreen(createdScreens)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onConfirmFormType = () => {
|
const screen = await createScreen(screenTemplate)
|
||||||
screenAccessRoleModal.show()
|
await addNavigationLink(screen)
|
||||||
}
|
|
||||||
|
|
||||||
const loadNewScreen = createdScreens => {
|
firstScreen ??= screen
|
||||||
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}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
screenStore.select(lastScreen._id)
|
loadNewScreen(firstScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmFormScreenCreation = async () => {
|
const createGridDetailsScreen = async () => {
|
||||||
templates = formScreen(selectedDatasources, { actionType: formType })
|
let firstScreen = null
|
||||||
screens = templates.map(template => {
|
|
||||||
let screenTemplate = template.create()
|
for (let tableOrView of selectedTablesAndViews) {
|
||||||
return screenTemplate
|
const screenTemplate = gridDetailsScreen(
|
||||||
})
|
tableOrView,
|
||||||
const createdScreens = await createScreens({ screens, screenAccessRole })
|
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") {
|
if (formType === "Update" || formType === "Create") {
|
||||||
const associatedTour =
|
const associatedTour =
|
||||||
|
@ -229,66 +183,89 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to new screen
|
loadNewScreen(firstScreen)
|
||||||
loadNewScreen(createdScreens)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit screen config for creation.
|
const loadNewScreen = screen => {
|
||||||
const confirmScreenCreation = async () => {
|
if (screen?.props?._children.length) {
|
||||||
if (screenMode === "blank") {
|
// Focus on the main component for the screen type
|
||||||
confirmBlankScreenCreation({
|
const mainComponent = screen?.props?._children?.[0]._id
|
||||||
screenUrl: blankScreenUrl,
|
$goto(`./${screen._id}/${mainComponent}`)
|
||||||
screenAccessRole,
|
|
||||||
})
|
|
||||||
} else if (screenMode === "form") {
|
|
||||||
confirmFormScreenCreation()
|
|
||||||
} else {
|
} else {
|
||||||
completeDatasourceScreenCreation()
|
$goto(`./${screen._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screenStore.select(screen._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleSelectBack = () => {
|
const fetchPermission = resourceId => {
|
||||||
if (screenMode === "blank") {
|
permissions[resourceId] = { loading: true, read: null, write: null }
|
||||||
screenDetailsModal.show()
|
|
||||||
|
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 {
|
} else {
|
||||||
datasourceModal.show()
|
deletePermission(tableOrView.id)
|
||||||
|
selectedTablesAndViews = selectedTablesAndViews.filter(
|
||||||
|
selected => selected.id !== tableOrView.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={datasourceModal} autoFocus={false}>
|
<Modal bind:this={datasourceModal} autoFocus={false}>
|
||||||
<DatasourceModal {mode} onConfirm={confirmScreenDatasources} />
|
<DatasourceModal
|
||||||
</Modal>
|
{selectedTablesAndViews}
|
||||||
|
{permissions}
|
||||||
<Modal bind:this={screenAccessRoleModal}>
|
onConfirm={onSelectDatasources}
|
||||||
<ScreenRoleModal
|
on:toggle={handleTableOrViewToggle}
|
||||||
onConfirm={() => {
|
|
||||||
confirmScreenCreation()
|
|
||||||
}}
|
|
||||||
bind:screenAccessRole
|
|
||||||
onCancel={roleSelectBack}
|
|
||||||
screenUrl={blankScreenUrl}
|
|
||||||
confirmText={screenMode === "form" ? "Confirm" : "Done"}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={screenDetailsModal}>
|
<Modal bind:this={screenDetailsModal}>
|
||||||
<ScreenDetailsModal
|
<ScreenDetailsModal onConfirm={createBlankScreen} />
|
||||||
onConfirm={confirmScreenBlank}
|
|
||||||
initialUrl={blankScreenUrl}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={formTypeModal}>
|
<Modal bind:this={formTypeModal}>
|
||||||
<FormTypeModal
|
<FormTypeModal
|
||||||
onConfirm={onConfirmFormType}
|
onConfirm={createFormScreen}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
formTypeModal.hide()
|
formTypeModal.hide()
|
||||||
datasourceModal.show()
|
datasourceModal.show()
|
||||||
}}
|
}}
|
||||||
on:select={e => {
|
|
||||||
formType = e.detail
|
|
||||||
}}
|
|
||||||
type={formType}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,42 +1,95 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
|
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 ICONS from "components/backend/DatasourceNavigator/icons"
|
||||||
import { IntegrationNames } from "constants"
|
import { IntegrationNames } from "constants"
|
||||||
import { onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte"
|
import TableOrViewOption from "./TableOrViewOption.svelte"
|
||||||
|
|
||||||
export let onCancel
|
|
||||||
export let onConfirm
|
export let onConfirm
|
||||||
|
export let selectedTablesAndViews
|
||||||
|
export let permissions
|
||||||
|
|
||||||
let selectedSources = []
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: filteredSources = $datasources.list?.filter(datasource => {
|
const getViews = table => {
|
||||||
return datasource.source !== IntegrationNames.REST && datasource["entities"]
|
const views = Object.values(table.views || {}).filter(
|
||||||
})
|
view => view.version === 2
|
||||||
|
|
||||||
const toggleSelection = datasource => {
|
|
||||||
const exists = selectedSources.find(
|
|
||||||
d => d.resourceId === datasource.resourceId
|
|
||||||
)
|
)
|
||||||
if (exists) {
|
|
||||||
selectedSources = selectedSources.filter(
|
return views.map(view => ({
|
||||||
d => d.resourceId === datasource.resourceId
|
icon: "Remove",
|
||||||
)
|
name: view.name,
|
||||||
} else {
|
id: view.id,
|
||||||
selectedSources = [...selectedSources, datasource]
|
clientData: {
|
||||||
}
|
...view,
|
||||||
|
type: "viewV2",
|
||||||
|
label: view.name,
|
||||||
|
},
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDatasourceSelection = async () => {
|
const getTablesAndViews = datasource => {
|
||||||
await onConfirm({
|
let tablesAndViews = []
|
||||||
datasources: selectedSources,
|
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 () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await datasources.fetch()
|
await datasourcesStore.fetch()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error fetching datasources")
|
notifications.error("Error fetching datasources")
|
||||||
}
|
}
|
||||||
|
@ -48,66 +101,35 @@
|
||||||
title="Autogenerated screens"
|
title="Autogenerated screens"
|
||||||
confirmText="Confirm"
|
confirmText="Confirm"
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
onConfirm={confirmDatasourceSelection}
|
{onConfirm}
|
||||||
{onCancel}
|
disabled={!selectedTablesAndViews.length}
|
||||||
disabled={!selectedSources.length}
|
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Select which datasources you would like to use to create your screens
|
Select which datasources you would like to use to create your screens
|
||||||
</Body>
|
</Body>
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
{#each filteredSources as datasource}
|
{#each datasources as datasource}
|
||||||
{@const entities = Array.isArray(datasource.entities)
|
|
||||||
? datasource.entities
|
|
||||||
: Object.values(datasource.entities || {})}
|
|
||||||
<div class="data-source-wrap">
|
<div class="data-source-wrap">
|
||||||
<div class="data-source-header">
|
<div class="data-source-header">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={ICONS[datasource.source]}
|
this={datasource.iconComponent}
|
||||||
height="24"
|
height="24"
|
||||||
width="24"
|
width="24"
|
||||||
/>
|
/>
|
||||||
<div class="data-source-name">{datasource.name}</div>
|
<div class="data-source-name">{datasource.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- List all tables -->
|
<!-- List all tables -->
|
||||||
{#each entities.filter(table => table._id !== "ta_users") as table}
|
{#each datasource.tablesAndViews as tableOrView}
|
||||||
{@const views = Object.values(table.views || {}).filter(
|
{@const selected = selectedTablesAndViews.some(
|
||||||
view => view.version === 2
|
selected => selected.id === tableOrView.id
|
||||||
)}
|
)}
|
||||||
{@const tableDS = {
|
<TableOrViewOption
|
||||||
tableId: table._id,
|
roles={permissions[tableOrView.id]}
|
||||||
label: table.name,
|
on:click={() => toggleSelection(tableOrView)}
|
||||||
resourceId: table._id,
|
|
||||||
type: "table",
|
|
||||||
}}
|
|
||||||
{@const selected = selectedSources.find(
|
|
||||||
datasource => datasource.resourceId === tableDS.resourceId
|
|
||||||
)}
|
|
||||||
<DatasourceTemplateRow
|
|
||||||
on:click={() => toggleSelection(tableDS)}
|
|
||||||
{selected}
|
{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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -118,8 +140,11 @@
|
||||||
<style>
|
<style>
|
||||||
.data-source-wrap {
|
.data-source-wrap {
|
||||||
padding-bottom: var(--spectrum-alias-item-padding-s);
|
padding-bottom: var(--spectrum-alias-item-padding-s);
|
||||||
display: grid;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
grid-gap: var(--spacing-s);
|
grid-gap: var(--spacing-s);
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.data-source-header {
|
.data-source-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,12 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Layout, Body, Icon } from "@budibase/bbui"
|
import { ModalContent, Layout, Body, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
|
let type = null
|
||||||
|
|
||||||
export let onCancel = () => {}
|
export let onCancel = () => {}
|
||||||
export let onConfirm = () => {}
|
export let onConfirm = () => {}
|
||||||
export let type
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
|
@ -14,7 +12,7 @@
|
||||||
title="Select form type"
|
title="Select form type"
|
||||||
confirmText="Done"
|
confirmText="Done"
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
{onConfirm}
|
onConfirm={() => onConfirm(type)}
|
||||||
{onCancel}
|
{onCancel}
|
||||||
disabled={!type}
|
disabled={!type}
|
||||||
size="L"
|
size="L"
|
||||||
|
@ -25,9 +23,7 @@
|
||||||
<div
|
<div
|
||||||
class="form-type"
|
class="form-type"
|
||||||
class:selected={type === "Create"}
|
class:selected={type === "Create"}
|
||||||
on:click={() => {
|
on:click={() => (type = "Create")}
|
||||||
dispatch("select", "Create")
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="form-type-wrap">
|
<div class="form-type-wrap">
|
||||||
<div class="form-type-content">
|
<div class="form-type-content">
|
||||||
|
@ -46,9 +42,7 @@
|
||||||
<div
|
<div
|
||||||
class="form-type"
|
class="form-type"
|
||||||
class:selected={type === "Update"}
|
class:selected={type === "Update"}
|
||||||
on:click={() => {
|
on:click={() => (type = "Update")}
|
||||||
dispatch("select", "Update")
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="form-type-wrap">
|
<div class="form-type-wrap">
|
||||||
<div class="form-type-content">
|
<div class="form-type-content">
|
||||||
|
@ -65,9 +59,7 @@
|
||||||
<div
|
<div
|
||||||
class="form-type"
|
class="form-type"
|
||||||
class:selected={type === "View"}
|
class:selected={type === "View"}
|
||||||
on:click={() => {
|
on:click={() => (type = "View")}
|
||||||
dispatch("select", "View")
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="form-type-wrap">
|
<div class="form-type-wrap">
|
||||||
<div class="form-type-content">
|
<div class="form-type-content">
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -63,6 +63,11 @@ export class Screen extends BaseStructure {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autoTableId(autoTableId) {
|
||||||
|
this._json.autoTableId = autoTableId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
instanceName(name) {
|
instanceName(name) {
|
||||||
this._json.props._instanceName = name
|
this._json.props._instanceName = name
|
||||||
return this
|
return this
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Screen } from "./Screen"
|
||||||
|
|
||||||
|
const blankScreen = route => {
|
||||||
|
return new Screen().instanceName("New Screen").route(route).json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default blankScreen
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -3,41 +3,47 @@ import { Component } from "./Component"
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||||
|
|
||||||
export const FORM_TEMPLATE = "FORM_TEMPLATE"
|
export const FORM_TEMPLATE = "FORM_TEMPLATE"
|
||||||
export const formUrl = datasource => sanitizeUrl(`/${datasource.label}-form`)
|
export const formUrl = (tableOrView, actionType) => {
|
||||||
|
if (actionType === "Create") {
|
||||||
// Mode not really necessary
|
return sanitizeUrl(`/${tableOrView.name}/new`)
|
||||||
export default function (datasources, config) {
|
} else if (actionType === "Update") {
|
||||||
if (!Array.isArray(datasources)) {
|
return sanitizeUrl(`/${tableOrView.name}/edit/:id`)
|
||||||
return []
|
} 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(
|
const multistepFormBlock = new Component(
|
||||||
"@budibase/standard-components/multistepformblock"
|
"@budibase/standard-components/multistepformblock"
|
||||||
)
|
)
|
||||||
multistepFormBlock
|
multistepFormBlock
|
||||||
.customProps({
|
.customProps({
|
||||||
actionType,
|
actionType,
|
||||||
dataSource,
|
dataSource: tableOrView.clientData,
|
||||||
steps: [{}],
|
steps: [{}],
|
||||||
|
rowId: actionType === "new" ? undefined : `{{ url.id }}`,
|
||||||
})
|
})
|
||||||
.instanceName(`${dataSource.label} - Multistep Form block`)
|
.instanceName(`${tableOrView.name} - Multistep Form block`)
|
||||||
return multistepFormBlock
|
return multistepFormBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScreen = (datasource, config) => {
|
const createScreen = (tableOrView, actionType, permissions) => {
|
||||||
return new Screen()
|
return new Screen()
|
||||||
.route(formUrl(datasource))
|
.route(formUrl(tableOrView, actionType))
|
||||||
.instanceName(`${datasource.label} - Form`)
|
.instanceName(`${tableOrView.name} - Form`)
|
||||||
.addChild(generateMultistepFormBlock(datasource, config))
|
.role(getRole(permissions, actionType))
|
||||||
|
.autoTableId(tableOrView.id)
|
||||||
|
.addChild(generateMultistepFormBlock(tableOrView, actionType))
|
||||||
.json()
|
.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default createScreen
|
||||||
|
|
|
@ -5,24 +5,9 @@ import { generate } from "shortid"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export default function (datasources) {
|
const gridDetailsUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GRID_DETAILS_TEMPLATE = "GRID_DETAILS_TEMPLATE"
|
const createScreen = (tableOrView, permissions) => {
|
||||||
export const gridDetailsUrl = datasource => sanitizeUrl(`/${datasource.label}`)
|
|
||||||
|
|
||||||
const createScreen = datasource => {
|
|
||||||
/*
|
/*
|
||||||
Create Row
|
Create Row
|
||||||
*/
|
*/
|
||||||
|
@ -47,7 +32,7 @@ const createScreen = datasource => {
|
||||||
type: "cta",
|
type: "cta",
|
||||||
})
|
})
|
||||||
|
|
||||||
buttonGroup.instanceName(`${datasource.label} - Create`).customProps({
|
buttonGroup.instanceName(`${tableOrView.name} - Create`).customProps({
|
||||||
hAlign: "right",
|
hAlign: "right",
|
||||||
buttons: [createButton.json()],
|
buttons: [createButton.json()],
|
||||||
})
|
})
|
||||||
|
@ -62,7 +47,7 @@ const createScreen = datasource => {
|
||||||
const heading = new Component("@budibase/standard-components/heading")
|
const heading = new Component("@budibase/standard-components/heading")
|
||||||
.instanceName("Table heading")
|
.instanceName("Table heading")
|
||||||
.customProps({
|
.customProps({
|
||||||
text: datasource?.label,
|
text: tableOrView.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
gridHeader.addChild(heading)
|
gridHeader.addChild(heading)
|
||||||
|
@ -72,7 +57,7 @@ const createScreen = datasource => {
|
||||||
"@budibase/standard-components/formblock"
|
"@budibase/standard-components/formblock"
|
||||||
)
|
)
|
||||||
createFormBlock.instanceName("Create row form block").customProps({
|
createFormBlock.instanceName("Create row form block").customProps({
|
||||||
dataSource: datasource,
|
dataSource: tableOrView.clientData,
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
buttonPosition: "top",
|
buttonPosition: "top",
|
||||||
actionType: "Create",
|
actionType: "Create",
|
||||||
|
@ -83,7 +68,7 @@ const createScreen = datasource => {
|
||||||
showSaveButton: true,
|
showSaveButton: true,
|
||||||
saveButtonLabel: "Save",
|
saveButtonLabel: "Save",
|
||||||
actionType: "Create",
|
actionType: "Create",
|
||||||
dataSource: datasource,
|
dataSource: tableOrView.clientData,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -99,7 +84,7 @@ const createScreen = datasource => {
|
||||||
|
|
||||||
const editFormBlock = new Component("@budibase/standard-components/formblock")
|
const editFormBlock = new Component("@budibase/standard-components/formblock")
|
||||||
editFormBlock.instanceName("Edit row form block").customProps({
|
editFormBlock.instanceName("Edit row form block").customProps({
|
||||||
dataSource: datasource,
|
dataSource: tableOrView.clientData,
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
buttonPosition: "top",
|
buttonPosition: "top",
|
||||||
actionType: "Update",
|
actionType: "Update",
|
||||||
|
@ -112,7 +97,7 @@ const createScreen = datasource => {
|
||||||
saveButtonLabel: "Save",
|
saveButtonLabel: "Save",
|
||||||
deleteButtonLabel: "Delete",
|
deleteButtonLabel: "Delete",
|
||||||
actionType: "Update",
|
actionType: "Update",
|
||||||
dataSource: datasource,
|
dataSource: tableOrView.clientData,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -121,7 +106,7 @@ const createScreen = datasource => {
|
||||||
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
||||||
gridBlock
|
gridBlock
|
||||||
.customProps({
|
.customProps({
|
||||||
table: datasource,
|
table: tableOrView.clientData,
|
||||||
allowAddRows: false,
|
allowAddRows: false,
|
||||||
allowEditRows: false,
|
allowEditRows: false,
|
||||||
allowDeleteRows: false,
|
allowDeleteRows: false,
|
||||||
|
@ -145,14 +130,18 @@ const createScreen = datasource => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.instanceName(`${datasource.label} - Table`)
|
.instanceName(`${tableOrView.name} - Table`)
|
||||||
|
|
||||||
return new Screen()
|
return new Screen()
|
||||||
.route(gridDetailsUrl(datasource))
|
.route(gridDetailsUrl(tableOrView))
|
||||||
.instanceName(`${datasource.label} - List and details`)
|
.instanceName(`${tableOrView.name} - List and details`)
|
||||||
|
.role(permissions.write)
|
||||||
|
.autoTableId(tableOrView.resourceId)
|
||||||
.addChild(gridHeader)
|
.addChild(gridHeader)
|
||||||
.addChild(gridBlock)
|
.addChild(gridBlock)
|
||||||
.addChild(createRowSidePanel)
|
.addChild(createRowSidePanel)
|
||||||
.addChild(detailsSidePanel)
|
.addChild(detailsSidePanel)
|
||||||
.json()
|
.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default createScreen
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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
|
|
@ -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
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
Loading…
Reference in New Issue