Merge branch 'master' into execute-script-v2
This commit is contained in:
commit
33a91cef67
23
README.md
23
README.md
|
@ -54,17 +54,21 @@
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
### Build and ship real software
|
### Build and ship real software
|
||||||
|
|
||||||
Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing users with a great experience.
|
Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing users with a great experience.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Open source and extensible
|
### Open source and extensible
|
||||||
|
|
||||||
Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
|
Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Load data or start from scratch
|
### Load data or start from scratch
|
||||||
|
|
||||||
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MariaDB, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MariaDB, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -82,10 +86,12 @@ Budibase comes out of the box with beautifully designed, powerful components whi
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Automate processes, integrate with other tools and connect to webhooks
|
### Automate processes, integrate with other tools and connect to webhooks
|
||||||
|
|
||||||
Save time by automating manual processes and workflows. From connecting to webhooks to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
Save time by automating manual processes and workflows. From connecting to webhooks to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Integrate with your favorite tools
|
### Integrate with your favorite tools
|
||||||
|
|
||||||
Budibase integrates with a number of popular tools allowing you to build apps that perfectly fit your stack.
|
Budibase integrates with a number of popular tools allowing you to build apps that perfectly fit your stack.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -94,6 +100,7 @@ Budibase integrates with a number of popular tools allowing you to build apps th
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Deploy with confidence and security
|
### Deploy with confidence and security
|
||||||
|
|
||||||
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user management to the group manager.
|
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user management to the group manager.
|
||||||
|
|
||||||
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
||||||
|
@ -104,15 +111,15 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
||||||
## Budibase Public API
|
## Budibase Public API
|
||||||
|
|
||||||
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
|
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
|
||||||
|
|
||||||
- Budibase as a backend
|
- Budibase as a backend
|
||||||
- Interoperability
|
- Interoperability
|
||||||
|
|
||||||
|
|
||||||
#### Docs
|
#### Docs
|
||||||
|
|
||||||
You can learn more about the Budibase API at the following places:
|
You can learn more about the Budibase API at the following places:
|
||||||
|
|
||||||
- [General documentation](https://docs.budibase.com/docs/public-api): Learn how to get your API key, how to use spec, and how to use Postman
|
- [General documentation](https://docs.budibase.com/docs/public-api): Learn how to get your API key, how to use spec, and how to use Postman
|
||||||
|
@ -132,10 +139,8 @@ Deploy Budibase using Docker, Kubernetes, and Digital Ocean on your existing inf
|
||||||
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
|
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
|
||||||
- [Portainer](https://docs.budibase.com/docs/portainer)
|
- [Portainer](https://docs.budibase.com/docs/portainer)
|
||||||
|
|
||||||
|
|
||||||
### [Get started with Budibase Cloud](https://budibase.com)
|
### [Get started with Budibase Cloud](https://budibase.com)
|
||||||
|
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
## 🎓 Learning Budibase
|
## 🎓 Learning Budibase
|
||||||
|
@ -143,7 +148,6 @@ Deploy Budibase using Docker, Kubernetes, and Digital Ocean on your existing inf
|
||||||
The Budibase documentation [lives here](https://docs.budibase.com/docs).
|
The Budibase documentation [lives here](https://docs.budibase.com/docs).
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
## 💬 Community
|
## 💬 Community
|
||||||
|
@ -152,25 +156,24 @@ If you have a question or would like to talk with other Budibase users and join
|
||||||
|
|
||||||
<br /><br /><br />
|
<br /><br /><br />
|
||||||
|
|
||||||
|
|
||||||
## ❗ Code of conduct
|
## ❗ Code of conduct
|
||||||
|
|
||||||
Budibase is dedicated to providing everyone a welcoming, diverse, and harassment-free experience. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
|
Budibase is dedicated to providing everyone a welcoming, diverse, and harassment-free experience. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
|
|
||||||
## 🙌 Contributing to Budibase
|
## 🙌 Contributing to Budibase
|
||||||
|
|
||||||
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API, please create an issue first. This way, we can ensure your work is not in vain.
|
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API, please create an issue first. This way, we can ensure your work is not in vain.
|
||||||
Environment setup instructions are available [here](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
|
Environment setup instructions are available [here](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
|
||||||
|
|
||||||
### Not Sure Where to Start?
|
### Not Sure Where to Start?
|
||||||
A good place to start contributing is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
|
||||||
|
A good place to start contributing is by looking for the [good first issue](https://github.com/Budibase/budibase/labels/good%20first%20issue) tag.
|
||||||
|
|
||||||
### How the repository is organized
|
### How the repository is organized
|
||||||
|
|
||||||
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
|
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
|
||||||
|
|
||||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client-side svelte application.
|
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client-side svelte application.
|
||||||
|
@ -183,7 +186,6 @@ For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). The client and component libraries are licensed as [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - so the apps you build can be licensed however you like.
|
Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). The client and component libraries are licensed as [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - so the apps you build can be licensed however you like.
|
||||||
|
@ -202,7 +204,6 @@ If you are having issues between updates of the builder, please use the guide [h
|
||||||
|
|
||||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/Budibase/budibase/graphs/contributors">
|
<a href="https://github.com/Budibase/budibase/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=Budibase/budibase" />
|
<img src="https://contrib.rocks/image?repo=Budibase/budibase" />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "3.4.3",
|
"version": "3.4.4",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -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,25 +1,25 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import "@spectrum-css/actionbutton/dist/index-vars.css"
|
import "@spectrum-css/actionbutton/dist/index-vars.css"
|
||||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { hexToRGBA } from "../helpers"
|
import { hexToRGBA } from "../helpers"
|
||||||
|
|
||||||
export let quiet = false
|
export let quiet: boolean = false
|
||||||
export let selected = false
|
export let selected: boolean = false
|
||||||
export let disabled = false
|
export let disabled: boolean = false
|
||||||
export let icon = ""
|
export let icon: string = ""
|
||||||
export let size = "M"
|
export let size: "S" | "M" | "L" = "M"
|
||||||
export let active = false
|
export let active: boolean = false
|
||||||
export let fullWidth = false
|
export let fullWidth: boolean = false
|
||||||
export let noPadding = false
|
export let noPadding: boolean = false
|
||||||
export let tooltip = ""
|
export let tooltip: string = ""
|
||||||
export let accentColor = null
|
export let accentColor: string | null = null
|
||||||
|
|
||||||
let showTooltip = false
|
let showTooltip = false
|
||||||
|
|
||||||
$: accentStyle = getAccentStyle(accentColor)
|
$: accentStyle = getAccentStyle(accentColor)
|
||||||
|
|
||||||
const getAccentStyle = color => {
|
const getAccentStyle = (color: string | null) => {
|
||||||
if (!color) {
|
if (!color) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import "@spectrum-css/divider/dist/index-vars.css"
|
import "@spectrum-css/divider/dist/index-vars.css"
|
||||||
|
|
||||||
export let size = "M"
|
export let size: "S" | "M" | "L" = "M"
|
||||||
|
|
||||||
export let vertical = false
|
export let vertical = false
|
||||||
export let noMargin = false
|
export let noMargin = false
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import "@spectrum-css/link/dist/index-vars.css"
|
import "@spectrum-css/link/dist/index-vars.css"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
|
||||||
export let href = "#"
|
export let href: string | null = "#"
|
||||||
export let size = "M"
|
export let size: "S" | "M" | "L" = "M"
|
||||||
export let quiet = false
|
export let quiet: boolean = false
|
||||||
export let primary = false
|
export let primary: boolean = false
|
||||||
export let secondary = false
|
export let secondary: boolean = false
|
||||||
export let overBackground = false
|
export let overBackground: boolean = false
|
||||||
export let target = undefined
|
export let target: string | undefined = undefined
|
||||||
export let download = undefined
|
export let download: boolean | undefined = undefined
|
||||||
export let disabled = false
|
export let disabled: boolean = false
|
||||||
export let tooltip = null
|
export let tooltip: string | null = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import StatusLight from "../StatusLight/StatusLight.svelte"
|
import StatusLight from "../StatusLight/StatusLight.svelte"
|
||||||
|
|
||||||
export let icon = null
|
export let icon: string | undefined = undefined
|
||||||
export let iconColor = null
|
export let iconColor: string | undefined = undefined
|
||||||
export let title = null
|
export let title: string | undefined = undefined
|
||||||
export let subtitle = null
|
export let subtitle: string | undefined = undefined
|
||||||
export let url = null
|
export let url: string | undefined = undefined
|
||||||
export let hoverable = false
|
export let hoverable: boolean = false
|
||||||
export let showArrow = false
|
export let showArrow: boolean = false
|
||||||
export let selected = false
|
export let selected: boolean = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { ActionButton, List, ListItem, Button } from "@budibase/bbui"
|
import { Button } from "@budibase/bbui"
|
||||||
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
import ScreensPopover from "@/components/common/ScreensPopover.svelte"
|
||||||
import { screenStore, appStore } from "@/stores/builder"
|
import { screenStore } from "@/stores/builder"
|
||||||
import { getContext, createEventDispatcher } from "svelte"
|
import { getContext, createEventDispatcher } from "svelte"
|
||||||
|
import type { Screen, ScreenUsage } from "@budibase/types"
|
||||||
const { datasource } = getContext("grid")
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let popover
|
const { datasource }: { datasource: any } = getContext("grid")
|
||||||
|
|
||||||
|
let popover: any
|
||||||
|
|
||||||
$: ds = $datasource
|
$: ds = $datasource
|
||||||
$: resourceId = ds?.type === "table" ? ds.tableId : ds?.id
|
$: resourceId = ds?.type === "table" ? ds.tableId : ds?.id
|
||||||
$: connectedScreens = findConnectedScreens($screenStore.screens, resourceId)
|
$: connectedScreens = findConnectedScreens($screenStore.screens, resourceId)
|
||||||
$: screenCount = connectedScreens.length
|
$: screenUsage = connectedScreens.map(
|
||||||
|
(screen: Screen): ScreenUsage => ({
|
||||||
|
url: screen.routing?.route,
|
||||||
|
_id: screen._id!,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const findConnectedScreens = (screens, resourceId) => {
|
const findConnectedScreens = (
|
||||||
|
screens: Screen[],
|
||||||
|
resourceId: string
|
||||||
|
): Screen[] => {
|
||||||
return screens.filter(screen => {
|
return screens.filter(screen => {
|
||||||
return JSON.stringify(screen).includes(`"${resourceId}"`)
|
return JSON.stringify(screen).includes(`"${resourceId}"`)
|
||||||
})
|
})
|
||||||
|
@ -26,34 +35,16 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailPopover title="Screens" bind:this={popover}>
|
<ScreensPopover
|
||||||
<svelte:fragment slot="anchor" let:open>
|
bind:this={popover}
|
||||||
<ActionButton
|
screens={screenUsage}
|
||||||
icon="WebPage"
|
icon="WebPage"
|
||||||
selected={open || screenCount}
|
|
||||||
quiet
|
|
||||||
accentColor="#364800"
|
accentColor="#364800"
|
||||||
>
|
showCount
|
||||||
Screens{screenCount ? `: ${screenCount}` : ""}
|
>
|
||||||
</ActionButton>
|
<svelte:fragment slot="footer">
|
||||||
</svelte:fragment>
|
|
||||||
{#if !connectedScreens.length}
|
|
||||||
There aren't any screens connected to this data.
|
|
||||||
{:else}
|
|
||||||
The following screens are connected to this data.
|
|
||||||
<List>
|
|
||||||
{#each connectedScreens as screen}
|
|
||||||
<ListItem
|
|
||||||
title={screen.routing.route}
|
|
||||||
url={`/builder/app/${$appStore.appId}/design/${screen._id}`}
|
|
||||||
showArrow
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</List>
|
|
||||||
{/if}
|
|
||||||
<div>
|
|
||||||
<Button secondary icon="WebPage" on:click={generateScreen}>
|
<Button secondary icon="WebPage" on:click={generateScreen}>
|
||||||
Generate app screen
|
Generate app screen
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</svelte:fragment>
|
||||||
</DetailPopover>
|
</ScreensPopover>
|
||||||
|
|
|
@ -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,3 +1,26 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="8" height="8">
|
<script lang="ts">
|
||||||
<circle cx="4" cy="4" r="4" stroke-width="0" fill="currentColor" />
|
export let color = "currentColor"
|
||||||
|
export let size: "S" | "M" = "M"
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
S: 6,
|
||||||
|
M: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
$: sizePx = sizes[size]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox={`0 0 ${sizePx} ${sizePx}`}
|
||||||
|
width={`${sizePx}`}
|
||||||
|
height={`${sizePx}`}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx={sizePx / 2}
|
||||||
|
cy={sizePx / 2}
|
||||||
|
r={sizePx / 2}
|
||||||
|
stroke-width="0"
|
||||||
|
fill={color}
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 157 B After Width: | Height: | Size: 417 B |
|
@ -0,0 +1,58 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ActionButton,
|
||||||
|
PopoverAlignment,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||||
|
import { appStore } from "@/stores/builder"
|
||||||
|
import type { ScreenUsage } from "@budibase/types"
|
||||||
|
|
||||||
|
export let screens: ScreenUsage[] = []
|
||||||
|
export let icon = "DeviceDesktop"
|
||||||
|
export let accentColor: string | null | undefined = null
|
||||||
|
export let showCount = false
|
||||||
|
export let align = PopoverAlignment.Left
|
||||||
|
|
||||||
|
let popover: any
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
popover?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
popover?.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DetailPopover title="Screens" bind:this={popover} {align}>
|
||||||
|
<svelte:fragment slot="anchor" let:open>
|
||||||
|
<ActionButton
|
||||||
|
{icon}
|
||||||
|
quiet
|
||||||
|
selected={open || !!(showCount && screens.length)}
|
||||||
|
{accentColor}
|
||||||
|
on:click={show}
|
||||||
|
>
|
||||||
|
Screens{showCount && screens.length ? `: ${screens.length}` : ""}
|
||||||
|
</ActionButton>
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
{#if !screens.length}
|
||||||
|
There aren't any screens connected to this data.
|
||||||
|
{:else}
|
||||||
|
The following screens are connected to this data.
|
||||||
|
<List>
|
||||||
|
{#each screens as screen}
|
||||||
|
<ListItem
|
||||||
|
title={screen.url}
|
||||||
|
url={`/builder/app/${$appStore.appId}/design/${screen._id}`}
|
||||||
|
showArrow
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</List>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<slot name="footer" />
|
||||||
|
</DetailPopover>
|
|
@ -25,16 +25,16 @@
|
||||||
export let wide
|
export let wide
|
||||||
|
|
||||||
let highlightType
|
let highlightType
|
||||||
|
let domElement
|
||||||
|
|
||||||
$: highlightedProp = $builderStore.highlightedSetting
|
$: highlightedProp = $builderStore.highlightedSetting
|
||||||
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
||||||
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
||||||
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
||||||
|
|
||||||
$: if (value) {
|
$: isHighlighted = highlightedProp?.key === key
|
||||||
highlightType =
|
|
||||||
highlightedProp?.key === key ? `highlighted-${highlightedProp?.type}` : ""
|
$: highlightType = isHighlighted ? `highlighted-${highlightedProp?.type}` : ""
|
||||||
}
|
|
||||||
|
|
||||||
const getAllBindings = (bindings, componentBindings, nested) => {
|
const getAllBindings = (bindings, componentBindings, nested) => {
|
||||||
if (!nested) {
|
if (!nested) {
|
||||||
|
@ -74,9 +74,19 @@
|
||||||
? defaultValue
|
? defaultValue
|
||||||
: enriched
|
: enriched
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToElement(element) {
|
||||||
|
element?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: highlightedProp && isHighlighted && scrollToElement(domElement)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
bind:this={domElement}
|
||||||
id={`${key}-prop-control-wrap`}
|
id={`${key}-prop-control-wrap`}
|
||||||
class={`property-control ${highlightType}`}
|
class={`property-control ${highlightType}`}
|
||||||
class:wide={!label || labelHidden || wide === true}
|
class:wide={!label || labelHidden || wide === true}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
import { screenStore } from "@/stores/builder"
|
||||||
|
import ScreensPopover from "@/components/common/ScreensPopover.svelte"
|
||||||
|
import type { ScreenUsage } from "@budibase/types"
|
||||||
|
|
||||||
|
export let sourceId: string
|
||||||
|
|
||||||
|
let screens: ScreenUsage[] = []
|
||||||
|
let popover: any
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
popover?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
popover?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
let response = await screenStore.usageInScreens(sourceId)
|
||||||
|
screens = response?.screens
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ScreensPopover
|
||||||
|
bind:this={popover}
|
||||||
|
{screens}
|
||||||
|
icon="WebPage"
|
||||||
|
accentColor="#364800"
|
||||||
|
showCount
|
||||||
|
/>
|
|
@ -23,6 +23,7 @@
|
||||||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||||
import QueryViewerSavePromptModal from "./QueryViewerSavePromptModal.svelte"
|
import QueryViewerSavePromptModal from "./QueryViewerSavePromptModal.svelte"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
||||||
|
|
||||||
export let query
|
export let query
|
||||||
let queryHash
|
let queryHash
|
||||||
|
@ -170,6 +171,7 @@
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
<ConnectedQueryScreens sourceId={query._id} />
|
||||||
<Button disabled={loading} on:click={runQuery} overBackground>
|
<Button disabled={loading} on:click={runQuery} overBackground>
|
||||||
<Icon size="S" name="Play" />
|
<Icon size="S" name="Play" />
|
||||||
Run query</Button
|
Run query</Button
|
||||||
|
@ -384,6 +386,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
runtimeToReadableMap,
|
runtimeToReadableMap,
|
||||||
toBindingsArray,
|
toBindingsArray,
|
||||||
} from "@/dataBinding"
|
} from "@/dataBinding"
|
||||||
|
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
||||||
|
|
||||||
export let queryId
|
export let queryId
|
||||||
|
|
||||||
|
@ -502,11 +503,14 @@
|
||||||
on:change={() => (query.flags.urlName = false)}
|
on:change={() => (query.flags.urlName = false)}
|
||||||
on:save={saveQuery}
|
on:save={saveQuery}
|
||||||
/>
|
/>
|
||||||
|
<div class="controls">
|
||||||
|
<ConnectedQueryScreens sourceId={query._id} />
|
||||||
<div class="access">
|
<div class="access">
|
||||||
<Label>Access</Label>
|
<Label>Access</Label>
|
||||||
<AccessLevelSelect {query} {saveId} />
|
<AccessLevelSelect {query} {saveId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="url-block">
|
<div class="url-block">
|
||||||
<div class="verb">
|
<div class="verb">
|
||||||
<Select
|
<Select
|
||||||
|
@ -825,6 +829,12 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
.access {
|
.access {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
import AppPreview from "./AppPreview.svelte"
|
import AppPreview from "./AppPreview.svelte"
|
||||||
import { screenStore, appStore } from "@/stores/builder"
|
import { screenStore, appStore } from "@/stores/builder"
|
||||||
import UndoRedoControl from "@/components/common/UndoRedoControl.svelte"
|
import UndoRedoControl from "@/components/common/UndoRedoControl.svelte"
|
||||||
|
import ScreenErrorsButton from "./ScreenErrorsButton.svelte"
|
||||||
|
import { Divider } from "@budibase/bbui"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-panel">
|
<div class="app-panel">
|
||||||
|
@ -15,6 +17,8 @@
|
||||||
{#if $appStore.clientFeatures.devicePreview}
|
{#if $appStore.clientFeatures.devicePreview}
|
||||||
<DevicePreviewSelect />
|
<DevicePreviewSelect />
|
||||||
{/if}
|
{/if}
|
||||||
|
<Divider vertical />
|
||||||
|
<ScreenErrorsButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -62,7 +66,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
|
@ -183,16 +183,6 @@
|
||||||
toggleAddComponent()
|
toggleAddComponent()
|
||||||
} else if (type === "highlight-setting") {
|
} else if (type === "highlight-setting") {
|
||||||
builderStore.highlightSetting(data.setting, "error")
|
builderStore.highlightSetting(data.setting, "error")
|
||||||
|
|
||||||
// Also scroll setting into view
|
|
||||||
const selector = `#${data.setting}-prop-control`
|
|
||||||
const element = document.querySelector(selector)?.parentElement
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "center",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (type === "eject-block") {
|
} else if (type === "eject-block") {
|
||||||
const { id, definition } = data
|
const { id, definition } = data
|
||||||
await componentStore.handleEjectBlock(id, definition)
|
await componentStore.handleEjectBlock(id, definition)
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { UIComponentError } from "@budibase/types"
|
||||||
|
import {
|
||||||
|
builderStore,
|
||||||
|
componentStore,
|
||||||
|
screenComponentErrorList,
|
||||||
|
screenComponentsList,
|
||||||
|
} from "@/stores/builder"
|
||||||
|
import {
|
||||||
|
AbsTooltip,
|
||||||
|
ActionButton,
|
||||||
|
Icon,
|
||||||
|
Link,
|
||||||
|
Popover,
|
||||||
|
PopoverAlignment,
|
||||||
|
TooltipPosition,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import CircleIndicator from "@/components/common/Icons/CircleIndicator.svelte"
|
||||||
|
|
||||||
|
let button: any
|
||||||
|
let popover: any
|
||||||
|
|
||||||
|
$: hasErrors = !!$screenComponentErrorList.length
|
||||||
|
|
||||||
|
function getErrorTitle(error: UIComponentError) {
|
||||||
|
const titleParts = [
|
||||||
|
$screenComponentsList.find(c => c._id === error.componentId)!
|
||||||
|
._instanceName,
|
||||||
|
]
|
||||||
|
if (error.errorType === "setting" && error.cause === "invalid") {
|
||||||
|
titleParts.push(error.label)
|
||||||
|
}
|
||||||
|
return titleParts.join(" - ")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onErrorClick(error: UIComponentError) {
|
||||||
|
componentStore.select(error.componentId)
|
||||||
|
if (error.errorType === "setting") {
|
||||||
|
builderStore.highlightSetting(error.key, "error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={button} class="error-button">
|
||||||
|
<AbsTooltip
|
||||||
|
text={!hasErrors ? "No errors found!" : ""}
|
||||||
|
position={TooltipPosition.Top}
|
||||||
|
>
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
disabled={!hasErrors}
|
||||||
|
on:click={() => popover.show()}
|
||||||
|
size="M"
|
||||||
|
icon="Alert"
|
||||||
|
/>
|
||||||
|
{#if hasErrors}
|
||||||
|
<div class="error-indicator">
|
||||||
|
<CircleIndicator
|
||||||
|
size="S"
|
||||||
|
color="var(--spectrum-global-color-static-red-600)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</AbsTooltip>
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
bind:this={popover}
|
||||||
|
anchor={button}
|
||||||
|
align={PopoverAlignment.Right}
|
||||||
|
maxWidth={400}
|
||||||
|
showPopover={hasErrors}
|
||||||
|
>
|
||||||
|
<div class="error-popover">
|
||||||
|
{#each $screenComponentErrorList as error}
|
||||||
|
<div class="error">
|
||||||
|
<Icon
|
||||||
|
name="Alert"
|
||||||
|
color="var(--spectrum-global-color-static-red-600)"
|
||||||
|
size="S"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Link overBackground on:click={() => onErrorClick(error)}>
|
||||||
|
{getErrorTitle(error)}
|
||||||
|
</Link>:
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
|
{@html error.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error-button {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.error-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
.error-popover {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.error-popover .error {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.error-popover .error:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-popover .error :global(mark) {
|
||||||
|
background: unset;
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
.error-popover .error :global(.spectrum-Link) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -21,7 +21,7 @@ import {
|
||||||
tables,
|
tables,
|
||||||
componentTreeNodesStore,
|
componentTreeNodesStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
screenComponents,
|
screenComponentsList,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding"
|
import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding"
|
||||||
import {
|
import {
|
||||||
|
@ -450,7 +450,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentName = getSequentialName(
|
const componentName = getSequentialName(
|
||||||
get(screenComponents),
|
get(screenComponentsList),
|
||||||
`New ${definition.friendlyName || definition.name}`,
|
`New ${definition.friendlyName || definition.name}`,
|
||||||
{
|
{
|
||||||
getName: c => c._instanceName,
|
getName: c => c._instanceName,
|
||||||
|
|
|
@ -17,9 +17,9 @@ import { deploymentStore } from "./deployments.js"
|
||||||
import { contextMenuStore } from "./contextMenu.js"
|
import { contextMenuStore } from "./contextMenu.js"
|
||||||
import { snippets } from "./snippets"
|
import { snippets } from "./snippets"
|
||||||
import {
|
import {
|
||||||
screenComponents,
|
screenComponentsList,
|
||||||
screenComponentErrors,
|
screenComponentErrors,
|
||||||
findComponentsBySettingsType,
|
screenComponentErrorList,
|
||||||
} from "./screenComponent"
|
} from "./screenComponent"
|
||||||
|
|
||||||
// Backend
|
// Backend
|
||||||
|
@ -72,9 +72,9 @@ export {
|
||||||
snippets,
|
snippets,
|
||||||
rowActions,
|
rowActions,
|
||||||
appPublished,
|
appPublished,
|
||||||
screenComponents,
|
screenComponentsList,
|
||||||
screenComponentErrors,
|
screenComponentErrors,
|
||||||
findComponentsBySettingsType,
|
screenComponentErrorList,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reset = () => {
|
export const reset = () => {
|
||||||
|
|
|
@ -4,11 +4,10 @@ import { selectedScreen } from "./screens"
|
||||||
import { viewsV2 } from "./viewsV2"
|
import { viewsV2 } from "./viewsV2"
|
||||||
import {
|
import {
|
||||||
UIDatasourceType,
|
UIDatasourceType,
|
||||||
Screen,
|
|
||||||
Component,
|
Component,
|
||||||
UIComponentError,
|
UIComponentError,
|
||||||
ScreenProps,
|
|
||||||
ComponentDefinition,
|
ComponentDefinition,
|
||||||
|
DependsOnComponentSetting,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { queries } from "./queries"
|
import { queries } from "./queries"
|
||||||
import { views } from "./views"
|
import { views } from "./views"
|
||||||
|
@ -21,14 +20,11 @@ import { getSettingsDefinition } from "@budibase/frontend-core"
|
||||||
function reduceBy<TItem extends {}, TKey extends keyof TItem>(
|
function reduceBy<TItem extends {}, TKey extends keyof TItem>(
|
||||||
key: TKey,
|
key: TKey,
|
||||||
list: TItem[]
|
list: TItem[]
|
||||||
): Record<string, any> {
|
): Record<string, TItem> {
|
||||||
return list.reduce(
|
return list.reduce<Record<string, TItem>>((result, item) => {
|
||||||
(result, item) => ({
|
result[item[key] as string] = item
|
||||||
...result,
|
return result
|
||||||
[item[key] as string]: item,
|
}, {})
|
||||||
}),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const friendlyNameByType: Partial<Record<UIDatasourceType, string>> = {
|
const friendlyNameByType: Partial<Record<UIDatasourceType, string>> = {
|
||||||
|
@ -46,7 +42,18 @@ const validationKeyByType: Record<UIDatasourceType, string | null> = {
|
||||||
jsonarray: "value",
|
jsonarray: "value",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const screenComponentErrors = derived(
|
export const screenComponentsList = derived(
|
||||||
|
[selectedScreen],
|
||||||
|
([$selectedScreen]): Component[] => {
|
||||||
|
if (!$selectedScreen) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return findAllComponents($selectedScreen.props)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const screenComponentErrorList = derived(
|
||||||
[selectedScreen, tables, views, viewsV2, queries, componentStore],
|
[selectedScreen, tables, views, viewsV2, queries, componentStore],
|
||||||
([
|
([
|
||||||
$selectedScreen,
|
$selectedScreen,
|
||||||
|
@ -55,9 +62,9 @@ export const screenComponentErrors = derived(
|
||||||
$viewsV2,
|
$viewsV2,
|
||||||
$queries,
|
$queries,
|
||||||
$componentStore,
|
$componentStore,
|
||||||
]): Record<string, UIComponentError[]> => {
|
]): UIComponentError[] => {
|
||||||
if (!$selectedScreen) {
|
if (!$selectedScreen) {
|
||||||
return {}
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasources = {
|
const datasources = {
|
||||||
|
@ -69,26 +76,38 @@ export const screenComponentErrors = derived(
|
||||||
|
|
||||||
const { components: definitions } = $componentStore
|
const { components: definitions } = $componentStore
|
||||||
|
|
||||||
const errors = {
|
const errors: UIComponentError[] = []
|
||||||
...getInvalidDatasources($selectedScreen, datasources, definitions),
|
|
||||||
...getMissingAncestors($selectedScreen, definitions),
|
function checkComponentErrors(component: Component, ancestors: string[]) {
|
||||||
...getMissingRequiredSettings($selectedScreen, definitions),
|
errors.push(...getInvalidDatasources(component, datasources, definitions))
|
||||||
|
errors.push(...getMissingRequiredSettings(component, definitions))
|
||||||
|
errors.push(...getMissingAncestors(component, definitions, ancestors))
|
||||||
|
|
||||||
|
for (const child of component._children || []) {
|
||||||
|
checkComponentErrors(child, [...ancestors, component._component])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkComponentErrors($selectedScreen?.props, [])
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
function getInvalidDatasources(
|
function getInvalidDatasources(
|
||||||
screen: Screen,
|
component: Component,
|
||||||
datasources: Record<string, any>,
|
datasources: Record<string, any>,
|
||||||
definitions: Record<string, ComponentDefinition>
|
definitions: Record<string, ComponentDefinition>
|
||||||
) {
|
) {
|
||||||
const result: Record<string, UIComponentError[]> = {}
|
const result: UIComponentError[] = []
|
||||||
for (const { component, setting } of findComponentsBySettingsType(
|
|
||||||
screen,
|
const datasourceTypes = ["table", "dataSource"]
|
||||||
["table", "dataSource"],
|
|
||||||
definitions
|
const possibleSettings = definitions[component._component]?.settings?.filter(
|
||||||
)) {
|
s => datasourceTypes.includes(s.type)
|
||||||
|
)
|
||||||
|
if (possibleSettings) {
|
||||||
|
for (const setting of possibleSettings) {
|
||||||
const componentSettings = component[setting.key]
|
const componentSettings = component[setting.key]
|
||||||
if (!componentSettings) {
|
if (!componentSettings) {
|
||||||
continue
|
continue
|
||||||
|
@ -107,52 +126,72 @@ function getInvalidDatasources(
|
||||||
const componentDatasources = {
|
const componentDatasources = {
|
||||||
...reduceBy("rowId", bindings.extractRelationships(componentBindings)),
|
...reduceBy("rowId", bindings.extractRelationships(componentBindings)),
|
||||||
...reduceBy("value", bindings.extractFields(componentBindings)),
|
...reduceBy("value", bindings.extractFields(componentBindings)),
|
||||||
...reduceBy("value", bindings.extractJSONArrayFields(componentBindings)),
|
...reduceBy(
|
||||||
|
"value",
|
||||||
|
bindings.extractJSONArrayFields(componentBindings)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceId = componentSettings[validationKey]
|
const resourceId = componentSettings[validationKey]
|
||||||
if (!{ ...datasources, ...componentDatasources }[resourceId]) {
|
if (!{ ...datasources, ...componentDatasources }[resourceId]) {
|
||||||
const friendlyTypeName = friendlyNameByType[type] ?? type
|
const friendlyTypeName = friendlyNameByType[type] ?? type
|
||||||
result[component._id!] = [
|
result.push({
|
||||||
{
|
componentId: component._id!,
|
||||||
key: setting.key,
|
key: setting.key,
|
||||||
|
label: setting.label || setting.key,
|
||||||
message: `The ${friendlyTypeName} named "${label}" could not be found`,
|
message: `The ${friendlyTypeName} named "${label}" could not be found`,
|
||||||
|
|
||||||
errorType: "setting",
|
errorType: "setting",
|
||||||
},
|
cause: "invalid",
|
||||||
]
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDependsOn(dependsOn: DependsOnComponentSetting | undefined): {
|
||||||
|
key?: string
|
||||||
|
value?: string
|
||||||
|
} {
|
||||||
|
if (dependsOn === undefined) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof dependsOn === "string") {
|
||||||
|
return { key: dependsOn }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key: dependsOn.setting, value: dependsOn.value }
|
||||||
|
}
|
||||||
|
|
||||||
function getMissingRequiredSettings(
|
function getMissingRequiredSettings(
|
||||||
screen: Screen,
|
component: Component,
|
||||||
definitions: Record<string, ComponentDefinition>
|
definitions: Record<string, ComponentDefinition>
|
||||||
) {
|
) {
|
||||||
const allComponents = findAllComponents(screen.props) as Component[]
|
const result: UIComponentError[] = []
|
||||||
|
|
||||||
const result: Record<string, UIComponentError[]> = {}
|
|
||||||
for (const component of allComponents) {
|
|
||||||
const definition = definitions[component._component]
|
const definition = definitions[component._component]
|
||||||
|
|
||||||
const settings = getSettingsDefinition(definition)
|
const settings = getSettingsDefinition(definition)
|
||||||
|
|
||||||
const missingRequiredSettings = settings.filter((setting: any) => {
|
const missingRequiredSettings = settings.filter(setting => {
|
||||||
let empty =
|
let empty = component[setting.key] == null || component[setting.key] === ""
|
||||||
component[setting.key] == null || component[setting.key] === ""
|
|
||||||
let missing = setting.required && empty
|
let missing = setting.required && empty
|
||||||
|
|
||||||
// Check if this setting depends on another, as it may not be required
|
// Check if this setting depends on another, as it may not be required
|
||||||
if (setting.dependsOn) {
|
if (setting.dependsOn) {
|
||||||
const dependsOnKey = setting.dependsOn.setting || setting.dependsOn
|
const { key: dependsOnKey, value: dependsOnValue } = parseDependsOn(
|
||||||
const dependsOnValue = setting.dependsOn.value
|
setting.dependsOn
|
||||||
const realDependentValue = component[dependsOnKey]
|
)
|
||||||
|
const realDependentValue =
|
||||||
|
component[dependsOnKey as keyof typeof component]
|
||||||
|
|
||||||
const sectionDependsOnKey =
|
const { key: sectionDependsOnKey, value: sectionDependsOnValue } =
|
||||||
setting.sectionDependsOn?.setting || setting.sectionDependsOn
|
parseDependsOn(setting.sectionDependsOn)
|
||||||
const sectionDependsOnValue = setting.sectionDependsOn?.value
|
const sectionRealDependentValue =
|
||||||
const sectionRealDependentValue = component[sectionDependsOnKey]
|
component[sectionDependsOnKey as keyof typeof component]
|
||||||
|
|
||||||
if (dependsOnValue == null && realDependentValue == null) {
|
if (dependsOnValue == null && realDependentValue == null) {
|
||||||
return false
|
return false
|
||||||
|
@ -173,12 +212,16 @@ function getMissingRequiredSettings(
|
||||||
})
|
})
|
||||||
|
|
||||||
if (missingRequiredSettings?.length) {
|
if (missingRequiredSettings?.length) {
|
||||||
result[component._id!] = missingRequiredSettings.map((s: any) => ({
|
result.push(
|
||||||
|
...missingRequiredSettings.map<UIComponentError>(s => ({
|
||||||
|
componentId: component._id!,
|
||||||
key: s.key,
|
key: s.key,
|
||||||
|
label: s.label || s.key,
|
||||||
message: `Add the <mark>${s.label}</mark> setting to start using your component`,
|
message: `Add the <mark>${s.label}</mark> setting to start using your component`,
|
||||||
errorType: "setting",
|
errorType: "setting",
|
||||||
|
cause: "missing",
|
||||||
}))
|
}))
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -186,22 +229,17 @@ function getMissingRequiredSettings(
|
||||||
|
|
||||||
const BudibasePrefix = "@budibase/standard-components/"
|
const BudibasePrefix = "@budibase/standard-components/"
|
||||||
function getMissingAncestors(
|
function getMissingAncestors(
|
||||||
screen: Screen,
|
component: Component,
|
||||||
definitions: Record<string, ComponentDefinition>
|
definitions: Record<string, ComponentDefinition>,
|
||||||
) {
|
ancestors: string[]
|
||||||
const result: Record<string, UIComponentError[]> = {}
|
): UIComponentError[] {
|
||||||
|
|
||||||
function checkMissingAncestors(component: Component, ancestors: string[]) {
|
|
||||||
for (const child of component._children || []) {
|
|
||||||
checkMissingAncestors(child, [...ancestors, component._component])
|
|
||||||
}
|
|
||||||
|
|
||||||
const definition = definitions[component._component]
|
const definition = definitions[component._component]
|
||||||
|
|
||||||
if (!definition?.requiredAncestors?.length) {
|
if (!definition?.requiredAncestors?.length) {
|
||||||
return
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result: UIComponentError[] = []
|
||||||
const missingAncestors = definition.requiredAncestors.filter(
|
const missingAncestors = definition.requiredAncestors.filter(
|
||||||
ancestor => !ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
ancestor => !ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
||||||
)
|
)
|
||||||
|
@ -211,9 +249,11 @@ function getMissingAncestors(
|
||||||
return name.endsWith("s") ? `${name}'` : `${name}s`
|
return name.endsWith("s") ? `${name}'` : `${name}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
result[component._id!] = missingAncestors.map(ancestor => {
|
result.push(
|
||||||
|
...missingAncestors.map<UIComponentError>(ancestor => {
|
||||||
const ancestorDefinition = definitions[`${BudibasePrefix}${ancestor}`]
|
const ancestorDefinition = definitions[`${BudibasePrefix}${ancestor}`]
|
||||||
return {
|
return {
|
||||||
|
componentId: component._id!,
|
||||||
message: `${pluralise(definition.name)} need to be inside a
|
message: `${pluralise(definition.name)} need to be inside a
|
||||||
<mark>${ancestorDefinition.name}</mark>`,
|
<mark>${ancestorDefinition.name}</mark>`,
|
||||||
errorType: "ancestor-setting",
|
errorType: "ancestor-setting",
|
||||||
|
@ -223,59 +263,19 @@ function getMissingAncestors(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkMissingAncestors(screen.props, [])
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findComponentsBySettingsType(
|
|
||||||
screen: Screen,
|
|
||||||
type: string | string[],
|
|
||||||
definitions: Record<string, ComponentDefinition>
|
|
||||||
) {
|
|
||||||
const typesArray = Array.isArray(type) ? type : [type]
|
|
||||||
|
|
||||||
const result: {
|
|
||||||
component: Component
|
|
||||||
setting: {
|
|
||||||
type: string
|
|
||||||
key: string
|
|
||||||
}
|
|
||||||
}[] = []
|
|
||||||
|
|
||||||
function recurseFieldComponentsInChildren(component: ScreenProps) {
|
|
||||||
if (!component) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const definition = definitions[component._component]
|
|
||||||
|
|
||||||
const setting = definition?.settings?.find((s: any) =>
|
|
||||||
typesArray.includes(s.type)
|
|
||||||
)
|
)
|
||||||
if (setting) {
|
|
||||||
result.push({
|
|
||||||
component,
|
|
||||||
setting: { type: setting.type, key: setting.key },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
component._children?.forEach(child => {
|
|
||||||
recurseFieldComponentsInChildren(child)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recurseFieldComponentsInChildren(screen?.props)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const screenComponents = derived(
|
export const screenComponentErrors = derived(
|
||||||
[selectedScreen],
|
[screenComponentErrorList],
|
||||||
([$selectedScreen]) => {
|
([$list]): Record<string, UIComponentError[]> => {
|
||||||
if (!$selectedScreen) {
|
return $list.reduce<Record<string, UIComponentError[]>>((obj, error) => {
|
||||||
return []
|
obj[error.componentId] ??= []
|
||||||
}
|
obj[error.componentId].push(error)
|
||||||
return findAllComponents($selectedScreen.props) as Component[]
|
return obj
|
||||||
|
}, {})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -3089,12 +3089,6 @@
|
||||||
"type": "tableConditions",
|
"type": "tableConditions",
|
||||||
"label": "Conditions",
|
"label": "Conditions",
|
||||||
"key": "conditions"
|
"key": "conditions"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"label": "Format",
|
|
||||||
"key": "format",
|
|
||||||
"info": "Changing format will display values as text"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -7691,8 +7685,7 @@
|
||||||
{
|
{
|
||||||
"type": "columns/grid",
|
"type": "columns/grid",
|
||||||
"key": "columns",
|
"key": "columns",
|
||||||
"resetOn": "table",
|
"resetOn": "table"
|
||||||
"nested": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import { get, derived, readable } from "svelte/store"
|
import { get, derived, readable } from "svelte/store"
|
||||||
import { featuresStore } from "stores"
|
import { featuresStore } from "stores"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
// import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
// table is actually any datasource, but called table for legacy compatibility
|
// table is actually any datasource, but called table for legacy compatibility
|
||||||
export let table
|
export let table
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
order: idx,
|
order: idx,
|
||||||
conditions: column.conditions,
|
conditions: column.conditions,
|
||||||
visible: !!column.active,
|
visible: !!column.active,
|
||||||
format: createFormatter(column),
|
// format: createFormatter(column),
|
||||||
}
|
}
|
||||||
if (column.width) {
|
if (column.width) {
|
||||||
overrides[column.field].width = column.width
|
overrides[column.field].width = column.width
|
||||||
|
@ -114,12 +114,12 @@
|
||||||
return overrides
|
return overrides
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFormatter = column => {
|
// const createFormatter = column => {
|
||||||
if (typeof column.format !== "string" || !column.format.trim().length) {
|
// if (typeof column.format !== "string" || !column.format.trim().length) {
|
||||||
return null
|
// return null
|
||||||
}
|
// }
|
||||||
return row => processStringSync(column.format, { [id]: row })
|
// return row => processStringSync(column.format, { [id]: row })
|
||||||
}
|
// }
|
||||||
|
|
||||||
const enrichButtons = buttons => {
|
const enrichButtons = buttons => {
|
||||||
if (!buttons?.length) {
|
if (!buttons?.length) {
|
||||||
|
|
|
@ -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(),
|
...basicScreen(),
|
||||||
routing: {
|
routing: {
|
||||||
roleId: role2._id!,
|
roleId: role2._id!,
|
||||||
route: "/bar",
|
route: "/bar",
|
||||||
homeScreen: false,
|
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
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
interface BaseUIComponentError {
|
interface BaseUIComponentError {
|
||||||
|
componentId: string
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UISettingComponentError extends BaseUIComponentError {
|
interface UISettingComponentError extends BaseUIComponentError {
|
||||||
errorType: "setting"
|
errorType: "setting"
|
||||||
key: string
|
key: string
|
||||||
|
label: string
|
||||||
|
cause: "missing" | "invalid"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UIAncestorComponentError extends BaseUIComponentError {
|
interface UIAncestorComponentError extends BaseUIComponentError {
|
||||||
|
|
|
@ -15,20 +15,24 @@ export interface ComponentDefinition {
|
||||||
illegalChildren: string[]
|
illegalChildren: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DependsOnComponentSetting =
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
setting: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ComponentSetting {
|
export interface ComponentSetting {
|
||||||
key: string
|
key: string
|
||||||
type: string
|
type: string
|
||||||
label?: string
|
label?: string
|
||||||
section?: string
|
section?: string
|
||||||
name?: string
|
name?: string
|
||||||
|
required?: boolean
|
||||||
defaultValue?: any
|
defaultValue?: any
|
||||||
selectAllFields?: boolean
|
selectAllFields?: boolean
|
||||||
resetOn?: string | string[]
|
resetOn?: string | string[]
|
||||||
settings?: ComponentSetting[]
|
settings?: ComponentSetting[]
|
||||||
dependsOn?:
|
dependsOn?: DependsOnComponentSetting
|
||||||
| string
|
sectionDependsOn?: DependsOnComponentSetting
|
||||||
| {
|
|
||||||
setting: string
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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