Merge pull request #15499 from Budibase/feature/pre-empt-data-source-deletion
Data section resource deletion warning
This commit is contained in:
commit
b4086e1187
|
@ -83,11 +83,15 @@ export function isViewId(id: string): boolean {
|
||||||
/**
|
/**
|
||||||
* Check if a given ID is that of a datasource or datasource plus.
|
* Check if a given ID is that of a datasource or datasource plus.
|
||||||
*/
|
*/
|
||||||
export const isDatasourceId = (id: string): boolean => {
|
export function isDatasourceId(id: string): boolean {
|
||||||
// this covers both datasources and datasource plus
|
// this covers both datasources and datasource plus
|
||||||
return !!id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
|
return !!id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isQueryId(id: string): boolean {
|
||||||
|
return !!id && id.startsWith(`${DocumentType.QUERY}${SEPARATOR}`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets parameters for retrieving workspaces.
|
* Gets parameters for retrieving workspaces.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@spectrum-css/textfield/dist/index-vars.css"
|
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||||
import { createEventDispatcher, onMount, tick } from "svelte"
|
import { createEventDispatcher, onMount, tick } from "svelte"
|
||||||
|
import type { UIEvent } from "@budibase/types"
|
||||||
|
|
||||||
export let value = null
|
export let value: string | null = null
|
||||||
export let placeholder: string | undefined = undefined
|
export let placeholder: string | undefined = undefined
|
||||||
export let type = "text"
|
export let type = "text"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -11,7 +12,7 @@
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let align: "left" | "right" | "center" | undefined = undefined
|
export let align: "left" | "right" | "center" | undefined = undefined
|
||||||
export let autofocus = false
|
export let autofocus: boolean | null = false
|
||||||
export let autocomplete: boolean | undefined
|
export let autocomplete: boolean | undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -24,7 +25,7 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
const float = parseFloat(newValue)
|
const float = parseFloat(newValue as string)
|
||||||
newValue = isNaN(float) ? null : float
|
newValue = isNaN(float) ? null : float
|
||||||
}
|
}
|
||||||
dispatch("change", newValue)
|
dispatch("change", newValue)
|
||||||
|
@ -37,31 +38,31 @@
|
||||||
focus = true
|
focus = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const onBlur = (event: any) => {
|
const onBlur = (event: UIEvent) => {
|
||||||
if (readonly || disabled) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
focus = false
|
focus = false
|
||||||
updateValue(event.target.value)
|
updateValue(event?.target?.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInput = (event: any) => {
|
const onInput = (event: UIEvent) => {
|
||||||
if (readonly || !updateOnChange || disabled) {
|
if (readonly || !updateOnChange || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updateValue(event.target.value)
|
updateValue(event.target?.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValueOnEnter = (event: any) => {
|
const updateValueOnEnter = (event: UIEvent) => {
|
||||||
if (readonly || disabled) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
updateValue(event.target.value)
|
updateValue(event.target?.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInputMode = (type: any) => {
|
const getInputMode = (type: string) => {
|
||||||
if (type === "bigint") {
|
if (type === "bigint") {
|
||||||
return "numeric"
|
return "numeric"
|
||||||
}
|
}
|
||||||
|
@ -77,7 +78,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
focus = autofocus
|
focus = autofocus || false
|
||||||
if (focus) {
|
if (focus) {
|
||||||
await tick()
|
await tick()
|
||||||
field.focus()
|
field.focus()
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import IntegrationIcon from "@/components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
import IntegrationIcon from "@/components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import UpdateDatasourceModal from "@/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte"
|
import UpdateDatasourceModal from "@/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte"
|
||||||
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
|
import DeleteDataConfirmModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
||||||
|
@ -71,7 +71,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<UpdateDatasourceModal {datasource} bind:this={editModal} />
|
<UpdateDatasourceModal {datasource} bind:this={editModal} />
|
||||||
<DeleteConfirmationModal {datasource} bind:this={deleteConfirmationModal} />
|
<DeleteDataConfirmModal
|
||||||
|
source={datasource}
|
||||||
|
bind:this={deleteConfirmationModal}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.datasource-icon {
|
.datasource-icon {
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import { datasources } from "@/stores/builder"
|
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
|
||||||
|
|
||||||
export let datasource
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
export const show = () => {
|
|
||||||
confirmDeleteDialog.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteDatasource() {
|
|
||||||
try {
|
|
||||||
const isSelected = datasource.selected || datasource.containsSelected
|
|
||||||
await datasources.delete(datasource)
|
|
||||||
notifications.success("Datasource deleted")
|
|
||||||
if (isSelected) {
|
|
||||||
$goto("./datasource")
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error deleting datasource")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
okText="Delete Datasource"
|
|
||||||
onOk={deleteDatasource}
|
|
||||||
title="Confirm Deletion"
|
|
||||||
>
|
|
||||||
Are you sure you wish to delete the datasource
|
|
||||||
<i>{datasource.name}</i>? This action cannot be undone.
|
|
||||||
</ConfirmDialog>
|
|
|
@ -6,19 +6,18 @@
|
||||||
} from "@/helpers/data/utils"
|
} from "@/helpers/data/utils"
|
||||||
import { goto as gotoStore, isActive } from "@roxi/routify"
|
import { goto as gotoStore, isActive } from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
datasources,
|
|
||||||
queries,
|
queries,
|
||||||
userSelectedResourceMap,
|
userSelectedResourceMap,
|
||||||
contextMenuStore,
|
contextMenuStore,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import NavItem from "@/components/common/NavItem.svelte"
|
import NavItem from "@/components/common/NavItem.svelte"
|
||||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
import DeleteDataConfirmModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte"
|
||||||
import { notifications, Icon } from "@budibase/bbui"
|
import { notifications, Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let query
|
export let query
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteModal
|
||||||
|
|
||||||
// goto won't work in the context menu callback if the store is called directly
|
// goto won't work in the context menu callback if the store is called directly
|
||||||
$: goto = $gotoStore
|
$: goto = $gotoStore
|
||||||
|
@ -31,7 +30,7 @@
|
||||||
keyBind: null,
|
keyBind: null,
|
||||||
visible: true,
|
visible: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
callback: confirmDeleteDialog.show,
|
callback: confirmDeleteModal.show,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "Duplicate",
|
icon: "Duplicate",
|
||||||
|
@ -51,20 +50,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteQuery() {
|
|
||||||
try {
|
|
||||||
// Go back to the datasource if we are deleting the active query
|
|
||||||
if ($queries.selectedQueryId === query._id) {
|
|
||||||
goto(`./datasource/${query.datasourceId}`)
|
|
||||||
}
|
|
||||||
await queries.delete(query)
|
|
||||||
await datasources.fetch()
|
|
||||||
notifications.success("Query deleted")
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error deleting query")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openContextMenu = e => {
|
const openContextMenu = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
@ -90,14 +75,7 @@
|
||||||
<Icon size="S" hoverable name="MoreSmallList" on:click={openContextMenu} />
|
<Icon size="S" hoverable name="MoreSmallList" on:click={openContextMenu} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
||||||
<ConfirmDialog
|
<DeleteDataConfirmModal source={query} bind:this={confirmDeleteModal} />
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
okText="Delete Query"
|
|
||||||
onOk={deleteQuery}
|
|
||||||
title="Confirm Deletion"
|
|
||||||
>
|
|
||||||
Are you sure you wish to delete this query? This action cannot be undone.
|
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,175 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto, params } from "@roxi/routify"
|
|
||||||
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"
|
|
||||||
|
|
||||||
export let table
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
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`
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if (table.sourceType === DB_TYPE_EXTERNAL) {
|
|
||||||
await datasources.fetch()
|
|
||||||
}
|
|
||||||
notifications.success("Table deleted")
|
|
||||||
if (isSelected) {
|
|
||||||
$goto(`./datasource/${table.datasourceId}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error(`Error deleting table - ${error.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideDeleteDialog() {
|
|
||||||
deleteTableName = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const autofillTableName = () => {
|
|
||||||
deleteTableName = table.name
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
okText="Delete Table"
|
|
||||||
onOk={deleteTable}
|
|
||||||
onCancel={hideDeleteDialog}
|
|
||||||
title="Confirm Deletion"
|
|
||||||
disabled={deleteTableName !== 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 table name below to confirm.</p>
|
|
||||||
<Input bind:value={deleteTableName} placeholder={table.name} />
|
|
||||||
</div>
|
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
margin-top: 0;
|
|
||||||
max-width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fourthWarning {
|
|
||||||
margin: 12px 0 6px;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -8,7 +8,7 @@
|
||||||
import NavItem from "@/components/common/NavItem.svelte"
|
import NavItem from "@/components/common/NavItem.svelte"
|
||||||
import { isActive } from "@roxi/routify"
|
import { isActive } from "@roxi/routify"
|
||||||
import EditModal from "./EditModal.svelte"
|
import EditModal from "./EditModal.svelte"
|
||||||
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
|
import DeleteConfirmationModal from "../../modals/DeleteDataConfirmationModal.svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||||
|
|
||||||
|
@ -65,4 +65,4 @@
|
||||||
{/if}
|
{/if}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<EditModal {table} bind:this={editModal} />
|
<EditModal {table} bind:this={editModal} />
|
||||||
<DeleteConfirmationModal {table} bind:this={deleteConfirmationModal} />
|
<DeleteConfirmationModal source={table} bind:this={deleteConfirmationModal} />
|
||||||
|
|
|
@ -0,0 +1,226 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Link, notifications } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
appStore,
|
||||||
|
datasources,
|
||||||
|
queries,
|
||||||
|
screenStore,
|
||||||
|
tables,
|
||||||
|
views,
|
||||||
|
viewsV2,
|
||||||
|
} from "@/stores/builder"
|
||||||
|
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||||
|
import { helpers, utils } from "@budibase/shared-core"
|
||||||
|
import { SourceType } from "@budibase/types"
|
||||||
|
import { goto, params } from "@roxi/routify"
|
||||||
|
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import type { Table, ViewV2, View, Datasource, Query } from "@budibase/types"
|
||||||
|
|
||||||
|
export let source: Table | ViewV2 | Datasource | Query | undefined
|
||||||
|
|
||||||
|
let confirmDeleteDialog: any
|
||||||
|
let affectedScreens: { text: string; url: string }[] = []
|
||||||
|
let sourceType: SourceType | undefined = undefined
|
||||||
|
|
||||||
|
const getDatasourceQueries = () => {
|
||||||
|
if (sourceType !== SourceType.DATASOURCE) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const sourceId = getSourceID()
|
||||||
|
const queryList = get(queries).list.filter(
|
||||||
|
query => query.datasourceId === sourceId
|
||||||
|
)
|
||||||
|
return queryList
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSourceID(): string {
|
||||||
|
if (!source) {
|
||||||
|
throw new Error("No data source provided.")
|
||||||
|
}
|
||||||
|
if ("id" in source) {
|
||||||
|
return source.id
|
||||||
|
}
|
||||||
|
return source._id!
|
||||||
|
}
|
||||||
|
|
||||||
|
export const show = async () => {
|
||||||
|
const usage = await screenStore.usageInScreens(getSourceID())
|
||||||
|
affectedScreens = processScreens(usage.screens)
|
||||||
|
sourceType = usage.sourceType
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function processScreens(
|
||||||
|
screens: { url: string; _id: string }[]
|
||||||
|
): { text: string; url: string }[] {
|
||||||
|
return screens.map(({ url, _id }) => ({
|
||||||
|
text: url,
|
||||||
|
url: `/builder/app/${$appStore.appId}/design/${_id}`,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDeleteDialog() {
|
||||||
|
sourceType = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTable(table: Table & { datasourceId?: string }) {
|
||||||
|
const isSelected = $params.tableId === table._id
|
||||||
|
try {
|
||||||
|
await tables.delete({
|
||||||
|
_id: table._id!,
|
||||||
|
_rev: table._rev!,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (table.sourceType === DB_TYPE_EXTERNAL) {
|
||||||
|
await datasources.fetch()
|
||||||
|
}
|
||||||
|
notifications.success("Table deleted")
|
||||||
|
if (isSelected) {
|
||||||
|
$goto(`./datasource/${table.datasourceId}`)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
notifications.error(`Error deleting table - ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteView(view: ViewV2 | View) {
|
||||||
|
try {
|
||||||
|
if (helpers.views.isV2(view)) {
|
||||||
|
await viewsV2.delete(view as ViewV2)
|
||||||
|
} else {
|
||||||
|
await views.delete(view as View)
|
||||||
|
}
|
||||||
|
notifications.success("View deleted")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting view")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDatasource(datasource: Datasource) {
|
||||||
|
try {
|
||||||
|
await datasources.delete(datasource)
|
||||||
|
notifications.success("Datasource deleted")
|
||||||
|
const isSelected =
|
||||||
|
get(datasources).selectedDatasourceId === datasource._id
|
||||||
|
if (isSelected) {
|
||||||
|
$goto("./datasource")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting datasource")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteQuery(query: Query) {
|
||||||
|
try {
|
||||||
|
// Go back to the datasource if we are deleting the active query
|
||||||
|
if ($queries.selectedQueryId === query._id) {
|
||||||
|
$goto(`./datasource/${query.datasourceId}`)
|
||||||
|
}
|
||||||
|
await queries.delete(query)
|
||||||
|
await datasources.fetch()
|
||||||
|
notifications.success("Query deleted")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting query")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSource() {
|
||||||
|
if (!source || !sourceType) {
|
||||||
|
throw new Error("Unable to delete - no data source found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sourceType) {
|
||||||
|
case SourceType.TABLE:
|
||||||
|
return await deleteTable(source as Table)
|
||||||
|
case SourceType.VIEW:
|
||||||
|
return await deleteView(source as ViewV2)
|
||||||
|
case SourceType.QUERY:
|
||||||
|
return await deleteQuery(source as Query)
|
||||||
|
case SourceType.DATASOURCE:
|
||||||
|
return await deleteDatasource(source as Datasource)
|
||||||
|
default:
|
||||||
|
utils.unreachable(sourceType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMessage(sourceType: string) {
|
||||||
|
if (!source) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const screenCount = affectedScreens.length
|
||||||
|
let message = `Removing ${source?.name} `
|
||||||
|
let initialLength = message.length
|
||||||
|
if (sourceType === SourceType.TABLE) {
|
||||||
|
const views = "views" in source ? Object.values(source?.views ?? []) : []
|
||||||
|
message += `will delete its data${
|
||||||
|
views.length
|
||||||
|
? `${screenCount ? "," : " and"} views (${views.length})`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
} else if (sourceType === SourceType.DATASOURCE) {
|
||||||
|
const queryList = getDatasourceQueries()
|
||||||
|
if (queryList.length) {
|
||||||
|
message += `will delete its queries (${queryList.length})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (screenCount) {
|
||||||
|
message +=
|
||||||
|
initialLength !== message.length
|
||||||
|
? ", and break connected screens:"
|
||||||
|
: "will break connected screens:"
|
||||||
|
} else {
|
||||||
|
message += "."
|
||||||
|
}
|
||||||
|
return message.length !== initialLength ? message : ""
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
okText="Delete"
|
||||||
|
onOk={deleteSource}
|
||||||
|
onCancel={hideDeleteDialog}
|
||||||
|
title={`Are you sure you want to delete this ${sourceType}?`}
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
{#if sourceType}
|
||||||
|
<p class="warning">
|
||||||
|
{buildMessage(sourceType)}
|
||||||
|
{#if affectedScreens.length > 0}
|
||||||
|
<span class="screens">
|
||||||
|
{#each affectedScreens as item, idx}
|
||||||
|
<Link overBackground target="_blank" href={item.url}
|
||||||
|
>{item.text}{idx !== affectedScreens.length - 1
|
||||||
|
? ","
|
||||||
|
: ""}</Link
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<p class="warning">
|
||||||
|
<b>This action cannot be undone.</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
margin-top: 0;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screens {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -8,7 +8,7 @@
|
||||||
export let onOk = undefined
|
export let onOk = undefined
|
||||||
export let onCancel = undefined
|
export let onCancel = undefined
|
||||||
export let warning = true
|
export let warning = true
|
||||||
export let disabled
|
export let disabled = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
<script>
|
|
||||||
import { views, viewsV2 } from "@/stores/builder"
|
|
||||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let view
|
|
||||||
|
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
export const show = () => {
|
|
||||||
confirmDeleteDialog.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteView() {
|
|
||||||
try {
|
|
||||||
if (view.version === 2) {
|
|
||||||
await viewsV2.delete(view)
|
|
||||||
} else {
|
|
||||||
await views.delete(view)
|
|
||||||
}
|
|
||||||
notifications.success("View deleted")
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
notifications.error("Error deleting view")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
body={`Are you sure you wish to delete the view '${view.name}'? Your data will be deleted and this action cannot be undone.`}
|
|
||||||
okText="Delete View"
|
|
||||||
onOk={deleteView}
|
|
||||||
title="Confirm Deletion"
|
|
||||||
/>
|
|
|
@ -10,9 +10,8 @@
|
||||||
import { Icon, ActionButton, ActionMenu, MenuItem } from "@budibase/bbui"
|
import { Icon, ActionButton, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||||
import { params, url } from "@roxi/routify"
|
import { params, url } from "@roxi/routify"
|
||||||
import EditViewModal from "./EditViewModal.svelte"
|
import EditViewModal from "./EditViewModal.svelte"
|
||||||
import DeleteViewModal from "./DeleteViewModal.svelte"
|
|
||||||
import EditTableModal from "@/components/backend/TableNavigator/TableNavItem/EditModal.svelte"
|
import EditTableModal from "@/components/backend/TableNavigator/TableNavItem/EditModal.svelte"
|
||||||
import DeleteTableModal from "@/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte"
|
import DeleteConfirmationModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte"
|
||||||
import { UserAvatars } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||||
import { TableNames } from "@/constants"
|
import { TableNames } from "@/constants"
|
||||||
|
@ -314,12 +313,12 @@
|
||||||
|
|
||||||
{#if table && tableEditable}
|
{#if table && tableEditable}
|
||||||
<EditTableModal {table} bind:this={editTableModal} />
|
<EditTableModal {table} bind:this={editTableModal} />
|
||||||
<DeleteTableModal {table} bind:this={deleteTableModal} />
|
<DeleteConfirmationModal source={table} bind:this={deleteTableModal} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if editableView}
|
{#if editableView}
|
||||||
<EditViewModal view={editableView} bind:this={editViewModal} />
|
<EditViewModal view={editableView} bind:this={editViewModal} />
|
||||||
<DeleteViewModal view={editableView} bind:this={deleteViewModal} />
|
<DeleteConfirmationModal source={editableView} bind:this={deleteViewModal} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -500,6 +500,13 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a list of screens that are used by a given source ID (table, view, datasource, query)
|
||||||
|
*/
|
||||||
|
async usageInScreens(sourceId: string) {
|
||||||
|
return API.usageInScreens(sourceId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const screenStore = new ScreenStore()
|
export const screenStore = new ScreenStore()
|
||||||
|
|
|
@ -2,12 +2,14 @@ import {
|
||||||
DeleteScreenResponse,
|
DeleteScreenResponse,
|
||||||
SaveScreenRequest,
|
SaveScreenRequest,
|
||||||
SaveScreenResponse,
|
SaveScreenResponse,
|
||||||
|
UsageInScreensResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { BaseAPIClient } from "./types"
|
import { BaseAPIClient } from "./types"
|
||||||
|
|
||||||
export interface ScreenEndpoints {
|
export interface ScreenEndpoints {
|
||||||
saveScreen: (screen: SaveScreenRequest) => Promise<SaveScreenResponse>
|
saveScreen: (screen: SaveScreenRequest) => Promise<SaveScreenResponse>
|
||||||
deleteScreen: (id: string, rev: string) => Promise<DeleteScreenResponse>
|
deleteScreen: (id: string, rev: string) => Promise<DeleteScreenResponse>
|
||||||
|
usageInScreens: (sourceId: string) => Promise<UsageInScreensResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
||||||
|
@ -32,4 +34,10 @@ export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
||||||
url: `/api/screens/${id}/${rev}`,
|
url: `/api/screens/${id}/${rev}`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
usageInScreens: async sourceId => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/screens/usage/${sourceId}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,34 +1,30 @@
|
||||||
import { getScreenParams, generateScreenID, DocumentType } from "../../db/utils"
|
import { DocumentType, generateScreenID } from "../../db/utils"
|
||||||
import {
|
import {
|
||||||
events,
|
|
||||||
context,
|
context,
|
||||||
tenancy,
|
|
||||||
db as dbCore,
|
db as dbCore,
|
||||||
|
events,
|
||||||
roles,
|
roles,
|
||||||
|
tenancy,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { updateAppPackage } from "./application"
|
import { updateAppPackage } from "./application"
|
||||||
import {
|
import {
|
||||||
Plugin,
|
DeleteScreenResponse,
|
||||||
ScreenProps,
|
|
||||||
Screen,
|
|
||||||
UserCtx,
|
|
||||||
FetchScreenResponse,
|
FetchScreenResponse,
|
||||||
|
Plugin,
|
||||||
SaveScreenRequest,
|
SaveScreenRequest,
|
||||||
SaveScreenResponse,
|
SaveScreenResponse,
|
||||||
DeleteScreenResponse,
|
Screen,
|
||||||
|
ScreenProps,
|
||||||
|
ScreenUsage,
|
||||||
|
UsageInScreensResponse,
|
||||||
|
UserCtx,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { builderSocket } from "../../websockets"
|
import { builderSocket } from "../../websockets"
|
||||||
|
import sdk from "../../sdk"
|
||||||
|
import { sdk as sharedSdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx<void, FetchScreenResponse>) {
|
export async function fetch(ctx: UserCtx<void, FetchScreenResponse>) {
|
||||||
const db = context.getAppDB()
|
const screens = await sdk.screens.fetch()
|
||||||
|
|
||||||
const screens = (
|
|
||||||
await db.allDocs(
|
|
||||||
getScreenParams(null, {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).rows.map((el: any) => el.doc)
|
|
||||||
|
|
||||||
const roleId = ctx.user?.role?._id as string
|
const roleId = ctx.user?.role?._id as string
|
||||||
if (!roleId) {
|
if (!roleId) {
|
||||||
|
@ -140,3 +136,23 @@ function findPlugins(component: ScreenProps, foundPlugins: string[]) {
|
||||||
}
|
}
|
||||||
component._children.forEach(child => findPlugins(child, foundPlugins))
|
component._children.forEach(child => findPlugins(child, foundPlugins))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function usage(ctx: UserCtx<void, UsageInScreensResponse>) {
|
||||||
|
const sourceId = ctx.params.sourceId
|
||||||
|
const sourceType = sdk.common.getSourceType(sourceId)
|
||||||
|
const allScreens = await sdk.screens.fetch()
|
||||||
|
const response: ScreenUsage[] = []
|
||||||
|
for (let screen of allScreens) {
|
||||||
|
const found = sharedSdk.screens.findInSettings(screen, sourceId)
|
||||||
|
if (found.length !== 0) {
|
||||||
|
response.push({
|
||||||
|
url: screen.routing.route,
|
||||||
|
_id: screen._id!,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.body = {
|
||||||
|
sourceType,
|
||||||
|
screens: response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,5 +19,10 @@ router
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.BUILDER),
|
||||||
controller.destroy
|
controller.destroy
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/api/screens/usage/:sourceId",
|
||||||
|
authorized(permissions.BUILDER),
|
||||||
|
controller.usage
|
||||||
|
)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -1,9 +1,24 @@
|
||||||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { events, roles } from "@budibase/backend-core"
|
import { events, roles } from "@budibase/backend-core"
|
||||||
import { Screen, Role, BuiltinPermissionID } from "@budibase/types"
|
import {
|
||||||
|
Screen,
|
||||||
|
Role,
|
||||||
|
BuiltinPermissionID,
|
||||||
|
SourceType,
|
||||||
|
UsageInScreensResponse,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
const { basicScreen } = setup.structures
|
const {
|
||||||
|
basicScreen,
|
||||||
|
createTableScreen,
|
||||||
|
createViewScreen,
|
||||||
|
createQueryScreen,
|
||||||
|
basicTable,
|
||||||
|
viewV2,
|
||||||
|
basicQuery,
|
||||||
|
basicDatasource,
|
||||||
|
} = setup.structures
|
||||||
|
|
||||||
describe("/screens", () => {
|
describe("/screens", () => {
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
@ -18,7 +33,7 @@ describe("/screens", () => {
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
it("should be able to create a layout", async () => {
|
it("should be able to create a layout", async () => {
|
||||||
const screens = await config.api.screen.list({ status: 200 })
|
const screens = await config.api.screen.list()
|
||||||
expect(screens.length).toEqual(1)
|
expect(screens.length).toEqual(1)
|
||||||
expect(screens.some(s => s._id === screen._id)).toEqual(true)
|
expect(screens.some(s => s._id === screen._id)).toEqual(true)
|
||||||
})
|
})
|
||||||
|
@ -52,28 +67,22 @@ describe("/screens", () => {
|
||||||
inherits: [role1._id!, role2._id!],
|
inherits: [role1._id!, role2._id!],
|
||||||
permissionId: BuiltinPermissionID.WRITE,
|
permissionId: BuiltinPermissionID.WRITE,
|
||||||
})
|
})
|
||||||
screen1 = await config.api.screen.save(
|
screen1 = await config.api.screen.save({
|
||||||
{
|
...basicScreen(),
|
||||||
...basicScreen(),
|
routing: {
|
||||||
routing: {
|
roleId: role1._id!,
|
||||||
roleId: role1._id!,
|
route: "/foo",
|
||||||
route: "/foo",
|
homeScreen: false,
|
||||||
homeScreen: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ status: 200 }
|
})
|
||||||
)
|
screen2 = await config.api.screen.save({
|
||||||
screen2 = await config.api.screen.save(
|
...basicScreen(),
|
||||||
{
|
routing: {
|
||||||
...basicScreen(),
|
roleId: role2._id!,
|
||||||
routing: {
|
route: "/bar",
|
||||||
roleId: role2._id!,
|
homeScreen: false,
|
||||||
route: "/bar",
|
|
||||||
homeScreen: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ status: 200 }
|
})
|
||||||
)
|
|
||||||
// get into prod app
|
// get into prod app
|
||||||
await config.publish()
|
await config.publish()
|
||||||
})
|
})
|
||||||
|
@ -81,10 +90,7 @@ describe("/screens", () => {
|
||||||
async function checkScreens(roleId: string, screenIds: string[]) {
|
async function checkScreens(roleId: string, screenIds: string[]) {
|
||||||
await config.loginAsRole(roleId, async () => {
|
await config.loginAsRole(roleId, async () => {
|
||||||
const res = await config.api.application.getDefinition(
|
const res = await config.api.application.getDefinition(
|
||||||
config.prodAppId!,
|
config.getProdAppId()
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
expect(res.screens.length).toEqual(screenIds.length)
|
expect(res.screens.length).toEqual(screenIds.length)
|
||||||
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort())
|
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort())
|
||||||
|
@ -114,10 +120,7 @@ describe("/screens", () => {
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const res = await config.api.application.getDefinition(
|
const res = await config.api.application.getDefinition(
|
||||||
config.prodAppId!,
|
config.prodAppId!
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
const screenIds = [screen._id!, screen1._id!]
|
const screenIds = [screen._id!, screen1._id!]
|
||||||
expect(res.screens.length).toEqual(screenIds.length)
|
expect(res.screens.length).toEqual(screenIds.length)
|
||||||
|
@ -134,9 +137,7 @@ describe("/screens", () => {
|
||||||
|
|
||||||
it("should be able to create a screen", async () => {
|
it("should be able to create a screen", async () => {
|
||||||
const screen = basicScreen()
|
const screen = basicScreen()
|
||||||
const responseScreen = await config.api.screen.save(screen, {
|
const responseScreen = await config.api.screen.save(screen)
|
||||||
status: 200,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(responseScreen._rev).toBeDefined()
|
expect(responseScreen._rev).toBeDefined()
|
||||||
expect(responseScreen.name).toEqual(screen.name)
|
expect(responseScreen.name).toEqual(screen.name)
|
||||||
|
@ -145,13 +146,13 @@ describe("/screens", () => {
|
||||||
|
|
||||||
it("should be able to update a screen", async () => {
|
it("should be able to update a screen", async () => {
|
||||||
const screen = basicScreen()
|
const screen = basicScreen()
|
||||||
let responseScreen = await config.api.screen.save(screen, { status: 200 })
|
let responseScreen = await config.api.screen.save(screen)
|
||||||
screen._id = responseScreen._id
|
screen._id = responseScreen._id
|
||||||
screen._rev = responseScreen._rev
|
screen._rev = responseScreen._rev
|
||||||
screen.name = "edit"
|
screen.name = "edit"
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
responseScreen = await config.api.screen.save(screen, { status: 200 })
|
responseScreen = await config.api.screen.save(screen)
|
||||||
|
|
||||||
expect(responseScreen._rev).toBeDefined()
|
expect(responseScreen._rev).toBeDefined()
|
||||||
expect(responseScreen.name).toEqual(screen.name)
|
expect(responseScreen.name).toEqual(screen.name)
|
||||||
|
@ -171,8 +172,7 @@ describe("/screens", () => {
|
||||||
it("should be able to delete the screen", async () => {
|
it("should be able to delete the screen", async () => {
|
||||||
const response = await config.api.screen.destroy(
|
const response = await config.api.screen.destroy(
|
||||||
screen._id!,
|
screen._id!,
|
||||||
screen._rev!,
|
screen._rev!
|
||||||
{ status: 200 }
|
|
||||||
)
|
)
|
||||||
expect(response.message).toBeDefined()
|
expect(response.message).toBeDefined()
|
||||||
expect(events.screen.deleted).toHaveBeenCalledTimes(1)
|
expect(events.screen.deleted).toHaveBeenCalledTimes(1)
|
||||||
|
@ -186,4 +186,57 @@ describe("/screens", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("usage", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await config.init()
|
||||||
|
await config.api.screen.save(basicScreen())
|
||||||
|
})
|
||||||
|
|
||||||
|
function confirmScreen(usage: UsageInScreensResponse, screen: Screen) {
|
||||||
|
expect(usage.screens.length).toEqual(1)
|
||||||
|
expect(usage.screens[0].url).toEqual(screen.routing.route)
|
||||||
|
expect(usage.screens[0]._id).toEqual(screen._id!)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should find table usage", async () => {
|
||||||
|
const table = await config.api.table.save(basicTable())
|
||||||
|
const screen = await config.api.screen.save(
|
||||||
|
createTableScreen("BudibaseDB", table)
|
||||||
|
)
|
||||||
|
const usage = await config.api.screen.usage(table._id!)
|
||||||
|
expect(usage.sourceType).toEqual(SourceType.TABLE)
|
||||||
|
confirmScreen(usage, screen)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should find view usage", async () => {
|
||||||
|
const table = await config.api.table.save(basicTable())
|
||||||
|
const view = await config.api.viewV2.create(
|
||||||
|
viewV2.createRequest(table._id!),
|
||||||
|
{ status: 201 }
|
||||||
|
)
|
||||||
|
const screen = await config.api.screen.save(
|
||||||
|
createViewScreen("BudibaseDB", view)
|
||||||
|
)
|
||||||
|
const usage = await config.api.screen.usage(view.id)
|
||||||
|
expect(usage.sourceType).toEqual(SourceType.VIEW)
|
||||||
|
confirmScreen(usage, screen)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should find datasource/query usage", async () => {
|
||||||
|
const datasource = await config.api.datasource.create(
|
||||||
|
basicDatasource().datasource
|
||||||
|
)
|
||||||
|
const query = await config.api.query.save(basicQuery(datasource._id!))
|
||||||
|
const screen = await config.api.screen.save(
|
||||||
|
createQueryScreen(datasource._id!, query)
|
||||||
|
)
|
||||||
|
const dsUsage = await config.api.screen.usage(datasource._id!)
|
||||||
|
expect(dsUsage.sourceType).toEqual(SourceType.DATASOURCE)
|
||||||
|
confirmScreen(dsUsage, screen)
|
||||||
|
const queryUsage = await config.api.screen.usage(query._id!)
|
||||||
|
expect(queryUsage.sourceType).toEqual(SourceType.QUERY)
|
||||||
|
confirmScreen(queryUsage, screen)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { roles } from "@budibase/backend-core"
|
import { roles } from "@budibase/backend-core"
|
||||||
import { BASE_LAYOUT_PROP_IDS } from "./layouts"
|
import { BASE_LAYOUT_PROP_IDS } from "./layouts"
|
||||||
import { Screen } from "@budibase/types"
|
import { Screen, Table, Query, ViewV2, Component } from "@budibase/types"
|
||||||
|
|
||||||
export function createHomeScreen(
|
export function createHomeScreen(
|
||||||
config: {
|
config: {
|
||||||
|
@ -53,3 +53,177 @@ export function createHomeScreen(
|
||||||
name: "home-screen",
|
name: "home-screen",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function heading(text: string): Component {
|
||||||
|
return {
|
||||||
|
_id: "c1bff24cd821e41d18c894ac77a80ef99",
|
||||||
|
_component: "@budibase/standard-components/heading",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_instanceName: "Table heading",
|
||||||
|
_children: [],
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTableScreen(
|
||||||
|
datasourceName: string,
|
||||||
|
table: Table
|
||||||
|
): Screen {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
_id: "cad0a0904cacd4678a2ac094e293db1a5",
|
||||||
|
_component: "@budibase/standard-components/container",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_children: [
|
||||||
|
heading("table"),
|
||||||
|
{
|
||||||
|
_id: "ca6304be2079147bb9933092c4f8ce6fa",
|
||||||
|
_component: "@budibase/standard-components/gridblock",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_instanceName: "table - Table",
|
||||||
|
_children: [],
|
||||||
|
table: {
|
||||||
|
label: table.name,
|
||||||
|
tableId: table._id!,
|
||||||
|
type: "table",
|
||||||
|
datasourceName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: "table - List",
|
||||||
|
layout: "grid",
|
||||||
|
direction: "column",
|
||||||
|
hAlign: "stretch",
|
||||||
|
vAlign: "top",
|
||||||
|
size: "grow",
|
||||||
|
gap: "M",
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
route: "/table",
|
||||||
|
roleId: "ADMIN",
|
||||||
|
homeScreen: false,
|
||||||
|
},
|
||||||
|
name: "screen-id",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createViewScreen(datasourceName: string, view: ViewV2): Screen {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
_id: "cc359092bbd6c4e10b57827155edb7872",
|
||||||
|
_component: "@budibase/standard-components/container",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_children: [
|
||||||
|
heading("view"),
|
||||||
|
{
|
||||||
|
_id: "ccb4a9e3734794864b5c65b012a0bdc5a",
|
||||||
|
_component: "@budibase/standard-components/gridblock",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_instanceName: "view - Table",
|
||||||
|
_children: [],
|
||||||
|
table: {
|
||||||
|
...view,
|
||||||
|
name: view.name,
|
||||||
|
tableId: view.tableId,
|
||||||
|
id: view.id,
|
||||||
|
label: view.name,
|
||||||
|
type: "viewV2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: "view - List",
|
||||||
|
layout: "grid",
|
||||||
|
direction: "column",
|
||||||
|
hAlign: "stretch",
|
||||||
|
vAlign: "top",
|
||||||
|
size: "grow",
|
||||||
|
gap: "M",
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
route: "/view",
|
||||||
|
roleId: "ADMIN",
|
||||||
|
homeScreen: false,
|
||||||
|
},
|
||||||
|
name: "view-id",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createQueryScreen(datasourceId: string, query: Query): Screen {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
_id: "cc59b217aed264939a6c5249eee39cb25",
|
||||||
|
_component: "@budibase/standard-components/container",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_id: "c33a4a6e3cb5343158a08625c06b5cd7c",
|
||||||
|
_component: "@budibase/standard-components/gridblock",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
},
|
||||||
|
_instanceName: "New Table",
|
||||||
|
table: {
|
||||||
|
...query,
|
||||||
|
label: query.name,
|
||||||
|
_id: query._id!,
|
||||||
|
name: query.name,
|
||||||
|
datasourceId: datasourceId,
|
||||||
|
type: "query",
|
||||||
|
},
|
||||||
|
initialSortOrder: "Ascending",
|
||||||
|
allowAddRows: true,
|
||||||
|
allowEditRows: true,
|
||||||
|
allowDeleteRows: true,
|
||||||
|
stripeRows: false,
|
||||||
|
quiet: false,
|
||||||
|
columns: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: "Blank screen",
|
||||||
|
layout: "grid",
|
||||||
|
direction: "column",
|
||||||
|
hAlign: "stretch",
|
||||||
|
vAlign: "top",
|
||||||
|
size: "grow",
|
||||||
|
gap: "M",
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
route: "/query",
|
||||||
|
roleId: "BASIC",
|
||||||
|
homeScreen: false,
|
||||||
|
},
|
||||||
|
name: "screen-id",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./utils"
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { SourceType } from "@budibase/types"
|
||||||
|
import { docIds } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
export function getSourceType(sourceId: string): SourceType {
|
||||||
|
if (docIds.isTableId(sourceId)) {
|
||||||
|
return SourceType.TABLE
|
||||||
|
} else if (docIds.isViewId(sourceId)) {
|
||||||
|
return SourceType.VIEW
|
||||||
|
} else if (docIds.isDatasourceId(sourceId)) {
|
||||||
|
return SourceType.DATASOURCE
|
||||||
|
} else if (docIds.isQueryId(sourceId)) {
|
||||||
|
return SourceType.QUERY
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown source type for source "${sourceId}"`)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./screens"
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { getScreenParams } from "../../../db/utils"
|
||||||
|
import { context } from "@budibase/backend-core"
|
||||||
|
import { Screen } from "@budibase/types"
|
||||||
|
|
||||||
|
export async function fetch(): Promise<Screen[]> {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
|
||||||
|
return (
|
||||||
|
await db.allDocs<Screen>(
|
||||||
|
getScreenParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).rows.map(el => el.doc!)
|
||||||
|
}
|
|
@ -11,6 +11,10 @@ export function isExternal(opts: { table?: Table; tableId?: string }): boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isInternal(opts: { table?: Table; tableId?: string }): boolean {
|
||||||
|
return !isExternal(opts)
|
||||||
|
}
|
||||||
|
|
||||||
export function isTable(table: any): table is Table {
|
export function isTable(table: any): table is Table {
|
||||||
return table._id && docIds.isTableId(table._id)
|
return table._id && docIds.isTableId(table._id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,14 @@ export function isView(view: any): view is ViewV2 {
|
||||||
return view.id && docIds.isViewId(view.id) && view.version === 2
|
return view.id && docIds.isViewId(view.id) && view.version === 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isInternal(viewId: string) {
|
||||||
|
if (!docIds.isViewId(viewId)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||||
|
return !isExternalTableID(tableId)
|
||||||
|
}
|
||||||
|
|
||||||
function guardDuplicateCalculationFields(view: Omit<ViewV2, "id" | "version">) {
|
function guardDuplicateCalculationFields(view: Omit<ViewV2, "id" | "version">) {
|
||||||
const seen: Record<string, Record<CalculationType, boolean>> = {}
|
const seen: Record<string, Record<CalculationType, boolean>> = {}
|
||||||
const calculationFields = helpers.views.calculationFields(view)
|
const calculationFields = helpers.views.calculationFields(view)
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { default as plugins } from "./plugins"
|
||||||
import * as views from "./app/views"
|
import * as views from "./app/views"
|
||||||
import * as permissions from "./app/permissions"
|
import * as permissions from "./app/permissions"
|
||||||
import * as rowActions from "./app/rowActions"
|
import * as rowActions from "./app/rowActions"
|
||||||
|
import * as screens from "./app/screens"
|
||||||
|
import * as common from "./app/common"
|
||||||
|
|
||||||
const sdk = {
|
const sdk = {
|
||||||
backups,
|
backups,
|
||||||
|
@ -22,10 +24,12 @@ const sdk = {
|
||||||
datasources,
|
datasources,
|
||||||
queries,
|
queries,
|
||||||
plugins,
|
plugins,
|
||||||
|
screens,
|
||||||
views,
|
views,
|
||||||
permissions,
|
permissions,
|
||||||
links,
|
links,
|
||||||
rowActions,
|
rowActions,
|
||||||
|
common,
|
||||||
}
|
}
|
||||||
|
|
||||||
// default export for TS
|
// default export for TS
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Screen } from "@budibase/types"
|
import { Screen, UsageInScreensResponse } from "@budibase/types"
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
|
|
||||||
export class ScreenAPI extends TestAPI {
|
export class ScreenAPI extends TestAPI {
|
||||||
|
@ -28,4 +28,16 @@ export class ScreenAPI extends TestAPI {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usage = async (
|
||||||
|
sourceId: string,
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<UsageInScreensResponse> => {
|
||||||
|
return this._post<UsageInScreensResponse>(
|
||||||
|
`/api/screens/usage/${sourceId}`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,11 @@ import {
|
||||||
import { LoopInput } from "../../definitions/automations"
|
import { LoopInput } from "../../definitions/automations"
|
||||||
import { merge } from "lodash"
|
import { merge } from "lodash"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
export {
|
||||||
|
createTableScreen,
|
||||||
|
createQueryScreen,
|
||||||
|
createViewScreen,
|
||||||
|
} from "../../constants/screens"
|
||||||
|
|
||||||
const { BUILTIN_ROLE_IDS } = roles
|
const { BUILTIN_ROLE_IDS } = roles
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * as applications from "./applications"
|
export * as applications from "./applications"
|
||||||
export * as automations from "./automations"
|
export * as automations from "./automations"
|
||||||
export * as users from "./users"
|
export * as users from "./users"
|
||||||
|
export * as screens from "./screens"
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Screen, Component } from "@budibase/types"
|
||||||
|
|
||||||
|
export function findInSettings(screen: Screen, toFind: string) {
|
||||||
|
const foundIn: { setting: string; value: string }[] = []
|
||||||
|
function recurse(props: Component, parentKey = "") {
|
||||||
|
for (const [key, value] of Object.entries(props)) {
|
||||||
|
if (!value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof value === "string" && value.includes(toFind)) {
|
||||||
|
foundIn.push({
|
||||||
|
setting: parentKey ? `${parentKey}.${key}` : key,
|
||||||
|
value: value,
|
||||||
|
})
|
||||||
|
} else if (typeof value === "object") {
|
||||||
|
recurse(value, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recurse(screen.props)
|
||||||
|
return foundIn
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { ScreenRoutingJson, Screen } from "../../../documents"
|
import { ScreenRoutingJson, Screen, SourceType } from "../../../documents"
|
||||||
|
|
||||||
export interface FetchScreenRoutingResponse {
|
export interface FetchScreenRoutingResponse {
|
||||||
routes: ScreenRoutingJson
|
routes: ScreenRoutingJson
|
||||||
|
@ -15,3 +15,13 @@ export interface SaveScreenResponse extends Screen {}
|
||||||
export interface DeleteScreenResponse {
|
export interface DeleteScreenResponse {
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScreenUsage {
|
||||||
|
url: string
|
||||||
|
_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsageInScreensResponse {
|
||||||
|
sourceType: SourceType
|
||||||
|
screens: ScreenUsage[]
|
||||||
|
}
|
||||||
|
|
|
@ -57,3 +57,10 @@ export interface RestConfig {
|
||||||
}
|
}
|
||||||
dynamicVariables?: DynamicVariable[]
|
dynamicVariables?: DynamicVariable[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SourceType {
|
||||||
|
DATASOURCE = "datasource",
|
||||||
|
QUERY = "query",
|
||||||
|
TABLE = "table",
|
||||||
|
VIEW = "view",
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface UIEvent extends Omit<Event, "target"> {
|
||||||
|
currentTarget: EventTarget & HTMLInputElement
|
||||||
|
key?: string
|
||||||
|
target?: any
|
||||||
|
}
|
|
@ -3,3 +3,4 @@ export * from "./bindings"
|
||||||
export * from "./components"
|
export * from "./components"
|
||||||
export * from "./dataFetch"
|
export * from "./dataFetch"
|
||||||
export * from "./datasource"
|
export * from "./datasource"
|
||||||
|
export * from "./common"
|
||||||
|
|
Loading…
Reference in New Issue