Merge remote-tracking branch 'origin/master' into feature/screen-query-usage-ui
This commit is contained in:
commit
5031213b23
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.3.6",
|
"version": "3.4.4",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -1,45 +1,58 @@
|
||||||
import events from "events"
|
import events from "events"
|
||||||
import { newid } from "../utils"
|
import { newid } from "../utils"
|
||||||
import { Queue, QueueOptions, JobOptions } from "./queue"
|
import { Queue, QueueOptions, JobOptions } from "./queue"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
import { Job, JobId, JobInformation } from "bull"
|
||||||
|
|
||||||
interface JobMessage {
|
function jobToJobInformation(job: Job): JobInformation {
|
||||||
|
let cron = ""
|
||||||
|
let every = -1
|
||||||
|
let tz: string | undefined = undefined
|
||||||
|
let endDate: number | undefined = undefined
|
||||||
|
|
||||||
|
const repeat = job.opts?.repeat
|
||||||
|
if (repeat) {
|
||||||
|
endDate = repeat.endDate ? new Date(repeat.endDate).getTime() : Date.now()
|
||||||
|
tz = repeat.tz
|
||||||
|
if ("cron" in repeat) {
|
||||||
|
cron = repeat.cron
|
||||||
|
} else {
|
||||||
|
every = repeat.every
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: job.id.toString(),
|
||||||
|
name: "",
|
||||||
|
key: job.id.toString(),
|
||||||
|
tz,
|
||||||
|
endDate,
|
||||||
|
cron,
|
||||||
|
every,
|
||||||
|
next: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JobMessage<T = any> extends Partial<Job<T>> {
|
||||||
id: string
|
id: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
queue: string
|
queue: Queue<T>
|
||||||
data: any
|
data: any
|
||||||
opts?: JobOptions
|
opts?: JobOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bull works with a Job wrapper around all messages that contains a lot more information about
|
* This is designed to replicate Bull (https://github.com/OptimalBits/bull) in
|
||||||
* the state of the message, this object constructor implements the same schema of Bull jobs
|
* memory as a sort of mock. It is relatively simple, using an event emitter
|
||||||
* for the sake of maintaining API consistency.
|
* internally to register when messages are available to the consumers - in can
|
||||||
* @param queue The name of the queue which the message will be carried on.
|
* support many inputs and many consumers.
|
||||||
* @param message The JSON message which will be passed back to the consumer.
|
|
||||||
* @returns A new job which can now be put onto the queue, this is mostly an
|
|
||||||
* internal structure so that an in memory queue can be easily swapped for a Bull queue.
|
|
||||||
*/
|
|
||||||
function newJob(queue: string, message: any, opts?: JobOptions): JobMessage {
|
|
||||||
return {
|
|
||||||
id: newid(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
queue: queue,
|
|
||||||
data: message,
|
|
||||||
opts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is designed to replicate Bull (https://github.com/OptimalBits/bull) in memory as a sort of mock.
|
|
||||||
* It is relatively simple, using an event emitter internally to register when messages are available
|
|
||||||
* to the consumers - in can support many inputs and many consumers.
|
|
||||||
*/
|
*/
|
||||||
class InMemoryQueue implements Partial<Queue> {
|
class InMemoryQueue implements Partial<Queue> {
|
||||||
_name: string
|
_name: string
|
||||||
_opts?: QueueOptions
|
_opts?: QueueOptions
|
||||||
_messages: JobMessage[]
|
_messages: JobMessage[]
|
||||||
_queuedJobIds: Set<string>
|
_queuedJobIds: Set<string>
|
||||||
_emitter: NodeJS.EventEmitter
|
_emitter: NodeJS.EventEmitter<{ message: [JobMessage]; completed: [Job] }>
|
||||||
_runCount: number
|
_runCount: number
|
||||||
_addCount: number
|
_addCount: number
|
||||||
|
|
||||||
|
@ -69,34 +82,29 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
*/
|
*/
|
||||||
async process(concurrencyOrFunc: number | any, func?: any) {
|
async process(concurrencyOrFunc: number | any, func?: any) {
|
||||||
func = typeof concurrencyOrFunc === "number" ? func : concurrencyOrFunc
|
func = typeof concurrencyOrFunc === "number" ? func : concurrencyOrFunc
|
||||||
this._emitter.on("message", async () => {
|
this._emitter.on("message", async message => {
|
||||||
if (this._messages.length <= 0) {
|
let resp = func(message)
|
||||||
return
|
|
||||||
}
|
|
||||||
let msg = this._messages.shift()
|
|
||||||
|
|
||||||
let resp = func(msg)
|
|
||||||
|
|
||||||
async function retryFunc(fnc: any) {
|
async function retryFunc(fnc: any) {
|
||||||
try {
|
try {
|
||||||
await fnc
|
await fnc
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await new Promise<void>(r => setTimeout(() => r(), 50))
|
await helpers.wait(50)
|
||||||
|
await retryFunc(func(message))
|
||||||
await retryFunc(func(msg))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.then != null) {
|
if (resp.then != null) {
|
||||||
try {
|
try {
|
||||||
await retryFunc(resp)
|
await retryFunc(resp)
|
||||||
|
this._emitter.emit("completed", message as Job)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._runCount++
|
this._runCount++
|
||||||
const jobId = msg?.opts?.jobId?.toString()
|
const jobId = message.opts?.jobId?.toString()
|
||||||
if (jobId && msg?.opts?.removeOnComplete) {
|
if (jobId && message.opts?.removeOnComplete) {
|
||||||
this._queuedJobIds.delete(jobId)
|
this._queuedJobIds.delete(jobId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -130,9 +138,16 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const pushMessage = () => {
|
const pushMessage = () => {
|
||||||
this._messages.push(newJob(this._name, data, opts))
|
const message: JobMessage = {
|
||||||
|
id: newid(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
queue: this as unknown as Queue,
|
||||||
|
data,
|
||||||
|
opts,
|
||||||
|
}
|
||||||
|
this._messages.push(message)
|
||||||
this._addCount++
|
this._addCount++
|
||||||
this._emitter.emit("message")
|
this._emitter.emit("message", message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const delay = opts?.delay
|
const delay = opts?.delay
|
||||||
|
@ -158,13 +173,6 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
console.log(cronJobId)
|
console.log(cronJobId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Implemented for tests
|
|
||||||
*/
|
|
||||||
async getRepeatableJobs() {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeJobs(_pattern: string) {
|
async removeJobs(_pattern: string) {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
@ -176,13 +184,31 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJob() {
|
async getJob(id: JobId) {
|
||||||
|
for (const message of this._messages) {
|
||||||
|
if (message.id === id) {
|
||||||
|
return message as Job
|
||||||
|
}
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
on() {
|
on(event: string, callback: (...args: any[]) => void): Queue {
|
||||||
// do nothing
|
// @ts-expect-error - this callback can be one of many types
|
||||||
return this as any
|
this._emitter.on(event, callback)
|
||||||
|
return this as unknown as Queue
|
||||||
|
}
|
||||||
|
|
||||||
|
async count() {
|
||||||
|
return this._messages.length
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCompletedCount() {
|
||||||
|
return this._runCount
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRepeatableJobs() {
|
||||||
|
return this._messages.map(job => jobToJobInformation(job as Job))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -388,7 +388,7 @@ class InternalBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof input === "string") {
|
if (typeof input === "string" && schema.type === FieldType.DATETIME) {
|
||||||
if (isInvalidISODateString(input)) {
|
if (isInvalidISODateString(input)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import { Feature, License, Quotas } from "@budibase/types"
|
import {
|
||||||
|
Feature,
|
||||||
|
License,
|
||||||
|
MonthlyQuotaName,
|
||||||
|
QuotaType,
|
||||||
|
QuotaUsageType,
|
||||||
|
} from "@budibase/types"
|
||||||
import cloneDeep from "lodash/cloneDeep"
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
|
import merge from "lodash/merge"
|
||||||
|
|
||||||
let CLOUD_FREE_LICENSE: License
|
let CLOUD_FREE_LICENSE: License
|
||||||
let UNLIMITED_LICENSE: License
|
let UNLIMITED_LICENSE: License
|
||||||
|
@ -27,18 +34,19 @@ export function initInternal(opts: {
|
||||||
|
|
||||||
export interface UseLicenseOpts {
|
export interface UseLicenseOpts {
|
||||||
features?: Feature[]
|
features?: Feature[]
|
||||||
quotas?: Quotas
|
monthlyQuotas?: [MonthlyQuotaName, number][]
|
||||||
}
|
}
|
||||||
|
|
||||||
// LICENSES
|
// LICENSES
|
||||||
|
|
||||||
export const useLicense = (license: License, opts?: UseLicenseOpts) => {
|
export const useLicense = (license: License, opts?: UseLicenseOpts) => {
|
||||||
if (opts) {
|
if (opts?.features) {
|
||||||
if (opts.features) {
|
|
||||||
license.features.push(...opts.features)
|
license.features.push(...opts.features)
|
||||||
}
|
}
|
||||||
if (opts.quotas) {
|
if (opts?.monthlyQuotas) {
|
||||||
license.quotas = opts.quotas
|
for (const [name, value] of opts.monthlyQuotas) {
|
||||||
|
license.quotas[QuotaType.USAGE][QuotaUsageType.MONTHLY][name].value =
|
||||||
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,12 +65,9 @@ export const useCloudFree = () => {
|
||||||
|
|
||||||
// FEATURES
|
// FEATURES
|
||||||
|
|
||||||
const useFeature = (feature: Feature) => {
|
const useFeature = (feature: Feature, extra?: Partial<UseLicenseOpts>) => {
|
||||||
const license = cloneDeep(getCachedLicense() || UNLIMITED_LICENSE)
|
const license = cloneDeep(getCachedLicense() || UNLIMITED_LICENSE)
|
||||||
const opts: UseLicenseOpts = {
|
const opts: UseLicenseOpts = merge({ features: [feature] }, extra)
|
||||||
features: [feature],
|
|
||||||
}
|
|
||||||
|
|
||||||
return useLicense(license, opts)
|
return useLicense(license, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,8 +107,12 @@ export const useAppBuilders = () => {
|
||||||
return useFeature(Feature.APP_BUILDERS)
|
return useFeature(Feature.APP_BUILDERS)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBudibaseAI = () => {
|
export const useBudibaseAI = (opts?: { monthlyQuota?: number }) => {
|
||||||
return useFeature(Feature.BUDIBASE_AI)
|
return useFeature(Feature.BUDIBASE_AI, {
|
||||||
|
monthlyQuotas: [
|
||||||
|
[MonthlyQuotaName.BUDIBASE_AI_CREDITS, opts?.monthlyQuota || 1000],
|
||||||
|
],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAICustomConfigs = () => {
|
export const useAICustomConfigs = () => {
|
||||||
|
|
|
@ -80,7 +80,7 @@
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"easymde": "^2.16.1",
|
"easymde": "^2.16.1",
|
||||||
"svelte-dnd-action": "^0.9.8",
|
"svelte-dnd-action": "^0.9.8",
|
||||||
"svelte-portal": "^1.0.0"
|
"svelte-portal": "^2.2.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"loader-utils": "1.4.1"
|
"loader-utils": "1.4.1"
|
||||||
|
|
|
@ -1,3 +1,17 @@
|
||||||
|
type ClickOutsideCallback = (event: MouseEvent) => void | undefined
|
||||||
|
|
||||||
|
interface ClickOutsideOpts {
|
||||||
|
callback?: ClickOutsideCallback
|
||||||
|
anchor?: HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Handler {
|
||||||
|
id: number
|
||||||
|
element: HTMLElement
|
||||||
|
anchor: HTMLElement
|
||||||
|
callback?: ClickOutsideCallback
|
||||||
|
}
|
||||||
|
|
||||||
// These class names will never trigger a callback if clicked, no matter what
|
// These class names will never trigger a callback if clicked, no matter what
|
||||||
const ignoredClasses = [
|
const ignoredClasses = [
|
||||||
".download-js-link",
|
".download-js-link",
|
||||||
|
@ -14,18 +28,20 @@ const conditionallyIgnoredClasses = [
|
||||||
".drawer-wrapper",
|
".drawer-wrapper",
|
||||||
".spectrum-Popover",
|
".spectrum-Popover",
|
||||||
]
|
]
|
||||||
let clickHandlers = []
|
let clickHandlers: Handler[] = []
|
||||||
let candidateTarget
|
let candidateTarget: HTMLElement | undefined
|
||||||
|
|
||||||
// Processes a "click outside" event and invokes callbacks if our source element
|
// Processes a "click outside" event and invokes callbacks if our source element
|
||||||
// is valid
|
// is valid
|
||||||
const handleClick = event => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
const target = (e.target || e.relatedTarget) as HTMLElement
|
||||||
|
|
||||||
// Ignore click if this is an ignored class
|
// Ignore click if this is an ignored class
|
||||||
if (event.target.closest('[data-ignore-click-outside="true"]')) {
|
if (target.closest('[data-ignore-click-outside="true"]')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (let className of ignoredClasses) {
|
for (let className of ignoredClasses) {
|
||||||
if (event.target.closest(className)) {
|
if (target.closest(className)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,41 +49,41 @@ const handleClick = event => {
|
||||||
// Process handlers
|
// Process handlers
|
||||||
clickHandlers.forEach(handler => {
|
clickHandlers.forEach(handler => {
|
||||||
// Check that the click isn't inside the target
|
// Check that the click isn't inside the target
|
||||||
if (handler.element.contains(event.target)) {
|
if (handler.element.contains(target)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore clicks for certain classes unless we're nested inside them
|
// Ignore clicks for certain classes unless we're nested inside them
|
||||||
for (let className of conditionallyIgnoredClasses) {
|
for (let className of conditionallyIgnoredClasses) {
|
||||||
const sourceInside = handler.anchor.closest(className) != null
|
const sourceInside = handler.anchor.closest(className) != null
|
||||||
const clickInside = event.target.closest(className) != null
|
const clickInside = target.closest(className) != null
|
||||||
if (clickInside && !sourceInside) {
|
if (clickInside && !sourceInside) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.callback?.(event)
|
handler.callback?.(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// On mouse up we only trigger a "click outside" callback if we targetted the
|
// On mouse up we only trigger a "click outside" callback if we targetted the
|
||||||
// same element that we did on mouse down. This fixes all sorts of issues where
|
// same element that we did on mouse down. This fixes all sorts of issues where
|
||||||
// we get annoying callbacks firing when we drag to select text.
|
// we get annoying callbacks firing when we drag to select text.
|
||||||
const handleMouseUp = e => {
|
const handleMouseUp = (e: MouseEvent) => {
|
||||||
if (candidateTarget === e.target) {
|
if (candidateTarget === e.target) {
|
||||||
handleClick(e)
|
handleClick(e)
|
||||||
}
|
}
|
||||||
candidateTarget = null
|
candidateTarget = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// On mouse down we store which element was targetted for comparison later
|
// On mouse down we store which element was targetted for comparison later
|
||||||
const handleMouseDown = e => {
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
// Only handle the primary mouse button here.
|
// Only handle the primary mouse button here.
|
||||||
// We handle context menu (right click) events in another handler.
|
// We handle context menu (right click) events in another handler.
|
||||||
if (e.button !== 0) {
|
if (e.button !== 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
candidateTarget = e.target
|
candidateTarget = e.target as HTMLElement
|
||||||
|
|
||||||
// Clear any previous listeners in case of multiple down events, and register
|
// Clear any previous listeners in case of multiple down events, and register
|
||||||
// a single mouse up listener
|
// a single mouse up listener
|
||||||
|
@ -75,14 +91,29 @@ const handleMouseDown = e => {
|
||||||
document.addEventListener("click", handleMouseUp, true)
|
document.addEventListener("click", handleMouseUp, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle iframe clicks by detecting a loss of focus on the main window
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (document.activeElement?.tagName === "IFRAME") {
|
||||||
|
handleClick(
|
||||||
|
new MouseEvent("click", { relatedTarget: document.activeElement })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Global singleton listeners for our events
|
// Global singleton listeners for our events
|
||||||
document.addEventListener("mousedown", handleMouseDown)
|
document.addEventListener("mousedown", handleMouseDown)
|
||||||
document.addEventListener("contextmenu", handleClick)
|
document.addEventListener("contextmenu", handleClick)
|
||||||
|
window.addEventListener("blur", handleBlur)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or updates a click handler
|
* Adds or updates a click handler
|
||||||
*/
|
*/
|
||||||
const updateHandler = (id, element, anchor, callback) => {
|
const updateHandler = (
|
||||||
|
id: number,
|
||||||
|
element: HTMLElement,
|
||||||
|
anchor: HTMLElement,
|
||||||
|
callback: ClickOutsideCallback | undefined
|
||||||
|
) => {
|
||||||
let existingHandler = clickHandlers.find(x => x.id === id)
|
let existingHandler = clickHandlers.find(x => x.id === id)
|
||||||
if (!existingHandler) {
|
if (!existingHandler) {
|
||||||
clickHandlers.push({ id, element, anchor, callback })
|
clickHandlers.push({ id, element, anchor, callback })
|
||||||
|
@ -94,27 +125,52 @@ const updateHandler = (id, element, anchor, callback) => {
|
||||||
/**
|
/**
|
||||||
* Removes a click handler
|
* Removes a click handler
|
||||||
*/
|
*/
|
||||||
const removeHandler = id => {
|
const removeHandler = (id: number) => {
|
||||||
clickHandlers = clickHandlers.filter(x => x.id !== id)
|
clickHandlers = clickHandlers.filter(x => x.id !== id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Svelte action to apply a click outside handler for a certain element
|
* Svelte action to apply a click outside handler for a certain element.
|
||||||
* opts.anchor is an optional param specifying the real root source of the
|
* opts.anchor is an optional param specifying the real root source of the
|
||||||
* component being observed. This is required for things like popovers, where
|
* component being observed. This is required for things like popovers, where
|
||||||
* the element using the clickoutside action is the popover, but the popover is
|
* the element using the clickoutside action is the popover, but the popover is
|
||||||
* rendered at the root of the DOM somewhere, whereas the popover anchor is the
|
* rendered at the root of the DOM somewhere, whereas the popover anchor is the
|
||||||
* element we actually want to consider when determining the source component.
|
* element we actually want to consider when determining the source component.
|
||||||
*/
|
*/
|
||||||
export default (element, opts) => {
|
export default (
|
||||||
|
element: HTMLElement,
|
||||||
|
opts?: ClickOutsideOpts | ClickOutsideCallback
|
||||||
|
) => {
|
||||||
const id = Math.random()
|
const id = Math.random()
|
||||||
const update = newOpts => {
|
|
||||||
const callback =
|
const isCallback = (
|
||||||
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
|
opts?: ClickOutsideOpts | ClickOutsideCallback
|
||||||
const anchor = newOpts?.anchor || element
|
): opts is ClickOutsideCallback => {
|
||||||
|
return typeof opts === "function"
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOpts = (
|
||||||
|
opts?: ClickOutsideOpts | ClickOutsideCallback
|
||||||
|
): opts is ClickOutsideOpts => {
|
||||||
|
return opts != null && typeof opts === "object"
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = (newOpts?: ClickOutsideOpts | ClickOutsideCallback) => {
|
||||||
|
let callback: ClickOutsideCallback | undefined
|
||||||
|
let anchor = element
|
||||||
|
if (isCallback(newOpts)) {
|
||||||
|
callback = newOpts
|
||||||
|
} else if (isOpts(newOpts)) {
|
||||||
|
callback = newOpts.callback
|
||||||
|
if (newOpts.anchor) {
|
||||||
|
anchor = newOpts.anchor
|
||||||
|
}
|
||||||
|
}
|
||||||
updateHandler(id, element, anchor, callback)
|
updateHandler(id, element, anchor, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
update(opts)
|
update(opts)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update,
|
update,
|
||||||
destroy: () => removeHandler(id),
|
destroy: () => removeHandler(id),
|
|
@ -1,13 +1,7 @@
|
||||||
/**
|
|
||||||
* Valid alignment options are
|
|
||||||
* - left
|
|
||||||
* - right
|
|
||||||
* - left-outside
|
|
||||||
* - right-outside
|
|
||||||
**/
|
|
||||||
|
|
||||||
// Strategies are defined as [Popover]To[Anchor].
|
// Strategies are defined as [Popover]To[Anchor].
|
||||||
// They can apply for both horizontal and vertical alignment.
|
// They can apply for both horizontal and vertical alignment.
|
||||||
|
import { PopoverAlignment } from "../constants"
|
||||||
|
|
||||||
type Strategy =
|
type Strategy =
|
||||||
| "StartToStart"
|
| "StartToStart"
|
||||||
| "EndToEnd"
|
| "EndToEnd"
|
||||||
|
@ -33,7 +27,7 @@ export type UpdateHandler = (
|
||||||
|
|
||||||
interface Opts {
|
interface Opts {
|
||||||
anchor?: HTMLElement
|
anchor?: HTMLElement
|
||||||
align: string
|
align: PopoverAlignment
|
||||||
maxHeight?: number
|
maxHeight?: number
|
||||||
maxWidth?: number
|
maxWidth?: number
|
||||||
minWidth?: number
|
minWidth?: number
|
||||||
|
@ -174,24 +168,33 @@ export default function positionDropdown(element: HTMLElement, opts: Opts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine X strategy
|
// Determine X strategy
|
||||||
if (align === "right") {
|
if (align === PopoverAlignment.Right) {
|
||||||
applyXStrategy("EndToEnd")
|
applyXStrategy("EndToEnd")
|
||||||
} else if (align === "right-outside" || align === "right-context-menu") {
|
} else if (
|
||||||
|
align === PopoverAlignment.RightOutside ||
|
||||||
|
align === PopoverAlignment.RightContextMenu
|
||||||
|
) {
|
||||||
applyXStrategy("StartToEnd")
|
applyXStrategy("StartToEnd")
|
||||||
} else if (align === "left-outside" || align === "left-context-menu") {
|
} else if (
|
||||||
|
align === PopoverAlignment.LeftOutside ||
|
||||||
|
align === PopoverAlignment.LeftContextMenu
|
||||||
|
) {
|
||||||
applyXStrategy("EndToStart")
|
applyXStrategy("EndToStart")
|
||||||
} else if (align === "center") {
|
} else if (align === PopoverAlignment.Center) {
|
||||||
applyXStrategy("MidPoint")
|
applyXStrategy("MidPoint")
|
||||||
} else {
|
} else {
|
||||||
applyXStrategy("StartToStart")
|
applyXStrategy("StartToStart")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine Y strategy
|
// Determine Y strategy
|
||||||
if (align === "right-outside" || align === "left-outside") {
|
if (
|
||||||
|
align === PopoverAlignment.RightOutside ||
|
||||||
|
align === PopoverAlignment.LeftOutside
|
||||||
|
) {
|
||||||
applyYStrategy("MidPoint")
|
applyYStrategy("MidPoint")
|
||||||
} else if (
|
} else if (
|
||||||
align === "right-context-menu" ||
|
align === PopoverAlignment.RightContextMenu ||
|
||||||
align === "left-context-menu"
|
align === PopoverAlignment.LeftContextMenu
|
||||||
) {
|
) {
|
||||||
applyYStrategy("StartToStart")
|
applyYStrategy("StartToStart")
|
||||||
if (styles.top) {
|
if (styles.top) {
|
||||||
|
@ -204,11 +207,11 @@ export default function positionDropdown(element: HTMLElement, opts: Opts) {
|
||||||
// Handle screen overflow
|
// Handle screen overflow
|
||||||
if (doesXOverflow()) {
|
if (doesXOverflow()) {
|
||||||
// Swap left to right
|
// Swap left to right
|
||||||
if (align === "left") {
|
if (align === PopoverAlignment.Left) {
|
||||||
applyXStrategy("EndToEnd")
|
applyXStrategy("EndToEnd")
|
||||||
}
|
}
|
||||||
// Swap right-outside to left-outside
|
// Swap right-outside to left-outside
|
||||||
else if (align === "right-outside") {
|
else if (align === PopoverAlignment.RightOutside) {
|
||||||
applyXStrategy("EndToStart")
|
applyXStrategy("EndToStart")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -225,10 +228,13 @@ export default function positionDropdown(element: HTMLElement, opts: Opts) {
|
||||||
applyXStrategy("EndToStart")
|
applyXStrategy("EndToStart")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Othewise invert as normal
|
// Otherwise invert as normal
|
||||||
else {
|
else {
|
||||||
// If using an outside strategy then lock to the bottom of the screen
|
// If using an outside strategy then lock to the bottom of the screen
|
||||||
if (align === "left-outside" || align === "right-outside") {
|
if (
|
||||||
|
align === PopoverAlignment.LeftOutside ||
|
||||||
|
align === PopoverAlignment.RightOutside
|
||||||
|
) {
|
||||||
applyYStrategy("ScreenEdge")
|
applyYStrategy("ScreenEdge")
|
||||||
}
|
}
|
||||||
// Otherwise flip above
|
// Otherwise flip above
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import clickOutside from "../../Actions/click_outside"
|
import clickOutside from "../../Actions/click_outside"
|
||||||
import Popover from "../../Popover/Popover.svelte"
|
import Popover from "../../Popover/Popover.svelte"
|
||||||
|
import { PopoverAlignment } from "../../constants"
|
||||||
|
|
||||||
export let value: string | undefined = undefined
|
export let value: string | undefined = undefined
|
||||||
export let id: string | undefined = undefined
|
export let id: string | undefined = undefined
|
||||||
|
@ -97,11 +98,16 @@
|
||||||
<Popover
|
<Popover
|
||||||
{anchor}
|
{anchor}
|
||||||
{open}
|
{open}
|
||||||
align="left"
|
align={PopoverAlignment.Left}
|
||||||
on:close={() => (open = false)}
|
on:close={() => (open = false)}
|
||||||
useAnchorWidth
|
useAnchorWidth
|
||||||
>
|
>
|
||||||
<div class="popover-content" use:clickOutside={() => (open = false)}>
|
<div
|
||||||
|
class="popover-content"
|
||||||
|
use:clickOutside={() => {
|
||||||
|
open = false
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ul class="spectrum-Menu" role="listbox">
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
{#if options && Array.isArray(options)}
|
{#if options && Array.isArray(options)}
|
||||||
{#each options as option}
|
{#each options as option}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
<script>
|
<script lang="ts" context="module">
|
||||||
|
type O = any
|
||||||
|
type V = any
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
import "@spectrum-css/picker/dist/index-vars.css"
|
import "@spectrum-css/picker/dist/index-vars.css"
|
||||||
import "@spectrum-css/popover/dist/index-vars.css"
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
import "@spectrum-css/menu/dist/index-vars.css"
|
import "@spectrum-css/menu/dist/index-vars.css"
|
||||||
|
@ -11,43 +16,55 @@
|
||||||
import Tags from "../../Tags/Tags.svelte"
|
import Tags from "../../Tags/Tags.svelte"
|
||||||
import Tag from "../../Tags/Tag.svelte"
|
import Tag from "../../Tags/Tag.svelte"
|
||||||
import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte"
|
import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte"
|
||||||
|
import { PopoverAlignment } from "../../constants"
|
||||||
|
|
||||||
export let id = null
|
export let id: string | undefined = undefined
|
||||||
export let disabled = false
|
export let disabled: boolean = false
|
||||||
export let fieldText = ""
|
export let fieldText: string = ""
|
||||||
export let fieldIcon = ""
|
export let fieldIcon: string = ""
|
||||||
export let fieldColour = ""
|
export let fieldColour: string = ""
|
||||||
export let isPlaceholder = false
|
export let isPlaceholder: boolean = false
|
||||||
export let placeholderOption = null
|
export let placeholderOption: string | undefined | boolean = undefined
|
||||||
export let options = []
|
export let options: O[] = []
|
||||||
export let isOptionSelected = () => false
|
export let isOptionSelected = (option: O) => option as unknown as boolean
|
||||||
export let isOptionEnabled = () => true
|
export let isOptionEnabled = (option: O, _index?: number) =>
|
||||||
export let onSelectOption = () => {}
|
option as unknown as boolean
|
||||||
export let getOptionLabel = option => option
|
export let onSelectOption: (_value: V) => void = () => {}
|
||||||
export let getOptionValue = option => option
|
export let getOptionLabel = (option: O, _index?: number) => `${option}`
|
||||||
export let getOptionIcon = () => null
|
export let getOptionValue = (option: O, _index?: number) =>
|
||||||
|
option as unknown as V
|
||||||
|
export let getOptionIcon = (option: O, _index?: number) =>
|
||||||
|
option?.icon ?? undefined
|
||||||
|
export let getOptionColour = (option: O, _index?: number) =>
|
||||||
|
option?.colour ?? undefined
|
||||||
|
export let getOptionSubtitle = (option: O, _index?: number) =>
|
||||||
|
option?.subtitle ?? undefined
|
||||||
export let useOptionIconImage = false
|
export let useOptionIconImage = false
|
||||||
export let getOptionColour = () => null
|
export let open: boolean = false
|
||||||
export let getOptionSubtitle = () => null
|
export let readonly: boolean = false
|
||||||
export let open = false
|
export let quiet: boolean = false
|
||||||
export let readonly = false
|
export let autoWidth: boolean | undefined = false
|
||||||
export let quiet = false
|
export let autocomplete: boolean = false
|
||||||
export let autoWidth = false
|
export let sort: boolean = false
|
||||||
export let autocomplete = false
|
export let searchTerm: string | null = null
|
||||||
export let sort = false
|
export let customPopoverHeight: string | undefined = undefined
|
||||||
export let searchTerm = null
|
export let align: PopoverAlignment | undefined = PopoverAlignment.Left
|
||||||
export let customPopoverHeight
|
export let footer: string | undefined = undefined
|
||||||
export let align = "left"
|
export let customAnchor: HTMLElement | undefined = undefined
|
||||||
export let footer = null
|
export let loading: boolean = false
|
||||||
export let customAnchor = null
|
export let onOptionMouseenter: (
|
||||||
export let loading
|
_e: MouseEvent,
|
||||||
export let onOptionMouseenter = () => {}
|
_option: any
|
||||||
export let onOptionMouseleave = () => {}
|
) => void = () => {}
|
||||||
|
export let onOptionMouseleave: (
|
||||||
|
_e: MouseEvent,
|
||||||
|
_option: any
|
||||||
|
) => void = () => {}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let button
|
let button: any
|
||||||
let component
|
let component: any
|
||||||
|
|
||||||
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
|
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
|
||||||
$: filteredOptions = getFilteredOptions(
|
$: filteredOptions = getFilteredOptions(
|
||||||
|
@ -56,7 +73,7 @@
|
||||||
getOptionLabel
|
getOptionLabel
|
||||||
)
|
)
|
||||||
|
|
||||||
const onClick = e => {
|
const onClick = (e: MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
dispatch("click")
|
dispatch("click")
|
||||||
|
@ -67,7 +84,11 @@
|
||||||
open = !open
|
open = !open
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSortedOptions = (options, getLabel, sort) => {
|
const getSortedOptions = (
|
||||||
|
options: any[],
|
||||||
|
getLabel: (_option: any) => string,
|
||||||
|
sort: boolean
|
||||||
|
) => {
|
||||||
if (!options?.length || !Array.isArray(options)) {
|
if (!options?.length || !Array.isArray(options)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -81,17 +102,21 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilteredOptions = (options, term, getLabel) => {
|
const getFilteredOptions = (
|
||||||
|
options: any[],
|
||||||
|
term: string | null,
|
||||||
|
getLabel: (_option: any) => string
|
||||||
|
) => {
|
||||||
if (autocomplete && term) {
|
if (autocomplete && term) {
|
||||||
const lowerCaseTerm = term.toLowerCase()
|
const lowerCaseTerm = term.toLowerCase()
|
||||||
return options.filter(option => {
|
return options.filter((option: any) => {
|
||||||
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
const onScroll = e => {
|
const onScroll = (e: any) => {
|
||||||
const scrollPxThreshold = 100
|
const scrollPxThreshold = 100
|
||||||
const scrollPositionFromBottom =
|
const scrollPositionFromBottom =
|
||||||
e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop
|
e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop
|
||||||
|
@ -151,18 +176,20 @@
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<Popover
|
<Popover
|
||||||
anchor={customAnchor ? customAnchor : button}
|
anchor={customAnchor ? customAnchor : button}
|
||||||
align={align || "left"}
|
align={align || PopoverAlignment.Left}
|
||||||
{open}
|
{open}
|
||||||
on:close={() => (open = false)}
|
on:close={() => (open = false)}
|
||||||
useAnchorWidth={!autoWidth}
|
useAnchorWidth={!autoWidth}
|
||||||
maxWidth={autoWidth ? 400 : null}
|
maxWidth={autoWidth ? 400 : undefined}
|
||||||
customHeight={customPopoverHeight}
|
customHeight={customPopoverHeight}
|
||||||
maxHeight={360}
|
maxHeight={360}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="popover-content"
|
class="popover-content"
|
||||||
class:auto-width={autoWidth}
|
class:auto-width={autoWidth}
|
||||||
use:clickOutside={() => (open = false)}
|
use:clickOutside={() => {
|
||||||
|
open = false
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{#if autocomplete}
|
{#if autocomplete}
|
||||||
<Search
|
<Search
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import "@spectrum-css/search/dist/index-vars.css"
|
import "@spectrum-css/search/dist/index-vars.css"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let value = null
|
export let value: any = null
|
||||||
export let placeholder = null
|
export let placeholder: string | undefined = undefined
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let id = null
|
export let id = null
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let inputRef
|
export let inputRef: HTMLElement | undefined = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let focus = false
|
let focus = false
|
||||||
|
|
||||||
const updateValue = value => {
|
const updateValue = (value: any) => {
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,19 +21,19 @@
|
||||||
focus = true
|
focus = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const onBlur = event => {
|
const onBlur = (event: any) => {
|
||||||
focus = false
|
focus = false
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInput = event => {
|
const onInput = (event: any) => {
|
||||||
if (!updateOnChange) {
|
if (!updateOnChange) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValueOnEnter = event => {
|
const updateValueOnEnter = (event: any) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
updateValue(event.target.value)
|
updateValue(event.target.value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,44 @@
|
||||||
<script>
|
<script lang="ts" context="module">
|
||||||
|
type O = any
|
||||||
|
type V = any
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import Picker from "./Picker.svelte"
|
import Picker from "./Picker.svelte"
|
||||||
|
import { PopoverAlignment } from "../../constants"
|
||||||
|
|
||||||
export let value = null
|
export let value: V | null = null
|
||||||
export let id = null
|
export let id: string | undefined = undefined
|
||||||
export let placeholder = "Choose an option"
|
export let placeholder: string | boolean = "Choose an option"
|
||||||
export let disabled = false
|
export let disabled: boolean = false
|
||||||
export let options = []
|
export let options: O[] = []
|
||||||
export let getOptionLabel = option => option
|
export let getOptionLabel = (option: O, _index?: number) => `${option}`
|
||||||
export let getOptionValue = option => option
|
export let getOptionValue = (option: O, _index?: number) =>
|
||||||
export let getOptionIcon = () => null
|
option as unknown as V
|
||||||
export let getOptionColour = () => null
|
export let getOptionIcon = (option: O, _index?: number) =>
|
||||||
export let getOptionSubtitle = () => null
|
option?.icon ?? undefined
|
||||||
export let compare = null
|
export let getOptionColour = (option: O, _index?: number) =>
|
||||||
|
option?.colour ?? undefined
|
||||||
|
export let getOptionSubtitle = (option: O, _index?: number) =>
|
||||||
|
option?.subtitle ?? undefined
|
||||||
|
export let compare = (option: O, value: V) => option === value
|
||||||
export let useOptionIconImage = false
|
export let useOptionIconImage = false
|
||||||
export let isOptionEnabled
|
export let isOptionEnabled = (option: O, _index?: number) =>
|
||||||
export let readonly = false
|
option as unknown as boolean
|
||||||
export let quiet = false
|
export let readonly: boolean = false
|
||||||
export let autoWidth = false
|
export let quiet: boolean = false
|
||||||
export let autocomplete = false
|
export let autoWidth: boolean = false
|
||||||
export let sort = false
|
export let autocomplete: boolean = false
|
||||||
export let align
|
export let sort: boolean = false
|
||||||
export let footer = null
|
export let align: PopoverAlignment | undefined = PopoverAlignment.Left
|
||||||
export let open = false
|
export let footer: string | undefined = undefined
|
||||||
export let tag = null
|
export let open: boolean = false
|
||||||
export let searchTerm = null
|
export let searchTerm: string | undefined = undefined
|
||||||
export let loading
|
export let loading: boolean | undefined = undefined
|
||||||
export let onOptionMouseenter = () => {}
|
export let onOptionMouseenter = () => {}
|
||||||
export let onOptionMouseleave = () => {}
|
export let onOptionMouseleave = () => {}
|
||||||
|
export let customPopoverHeight: string | undefined = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -35,24 +46,28 @@
|
||||||
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
||||||
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||||
|
|
||||||
function compareOptionAndValue(option, value) {
|
function compareOptionAndValue(option: O, value: V) {
|
||||||
return typeof compare === "function"
|
return typeof compare === "function"
|
||||||
? compare(option, value)
|
? compare(option, value)
|
||||||
: option === value
|
: option === value
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFieldAttribute = (getAttribute, value, options) => {
|
const getFieldAttribute = (getAttribute: any, value: V[], options: O[]) => {
|
||||||
// Wait for options to load if there is a value but no options
|
// Wait for options to load if there is a value but no options
|
||||||
if (!options?.length) {
|
if (!options?.length) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
const index = options.findIndex((option, idx) =>
|
const index = options.findIndex((option: any, idx: number) =>
|
||||||
compareOptionAndValue(getOptionValue(option, idx), value)
|
compareOptionAndValue(getOptionValue(option, idx), value)
|
||||||
)
|
)
|
||||||
return index !== -1 ? getAttribute(options[index], index) : null
|
return index !== -1 ? getAttribute(options[index], index) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFieldText = (value, options, placeholder) => {
|
const getFieldText = (
|
||||||
|
value: any,
|
||||||
|
options: any,
|
||||||
|
placeholder: boolean | string
|
||||||
|
) => {
|
||||||
if (value == null || value === "") {
|
if (value == null || value === "") {
|
||||||
// Explicit false means use no placeholder and allow an empty fields
|
// Explicit false means use no placeholder and allow an empty fields
|
||||||
if (placeholder === false) {
|
if (placeholder === false) {
|
||||||
|
@ -67,7 +82,7 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectOption = value => {
|
const selectOption = (value: V) => {
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
|
@ -98,14 +113,14 @@
|
||||||
{isOptionEnabled}
|
{isOptionEnabled}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{sort}
|
{sort}
|
||||||
{tag}
|
|
||||||
{onOptionMouseenter}
|
{onOptionMouseenter}
|
||||||
{onOptionMouseleave}
|
{onOptionMouseleave}
|
||||||
isPlaceholder={value == null || value === ""}
|
isPlaceholder={value == null || value === ""}
|
||||||
placeholderOption={placeholder === false
|
placeholderOption={placeholder === false
|
||||||
? null
|
? undefined
|
||||||
: placeholder || "Choose an option"}
|
: placeholder || "Choose an option"}
|
||||||
isOptionSelected={option => compareOptionAndValue(option, value)}
|
isOptionSelected={option => compareOptionAndValue(option, value)}
|
||||||
onSelectOption={selectOption}
|
onSelectOption={selectOption}
|
||||||
{loading}
|
{loading}
|
||||||
|
{customPopoverHeight}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,23 +4,23 @@
|
||||||
import type { UIEvent } from "@budibase/types"
|
import type { UIEvent } from "@budibase/types"
|
||||||
|
|
||||||
export let value: string | null = null
|
export let value: string | null = null
|
||||||
export let placeholder: string | null = null
|
export let placeholder: string | undefined = undefined
|
||||||
export let type = "text"
|
export let type = "text"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let id = null
|
export let id = null
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let align: string | null = null
|
export let align: "left" | "right" | "center" | undefined = undefined
|
||||||
export let autofocus: boolean | null = false
|
export let autofocus: boolean | null = false
|
||||||
export let autocomplete: string | null = null
|
export let autocomplete: boolean | undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let field: any
|
let field: any
|
||||||
let focus = false
|
let focus = false
|
||||||
|
|
||||||
const updateValue = (newValue: string | number | null) => {
|
const updateValue = (newValue: any) => {
|
||||||
if (readonly || disabled) {
|
if (readonly || disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,13 @@
|
||||||
return type === "number" ? "decimal" : "text"
|
return type === "number" ? "decimal" : "text"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: autocompleteValue =
|
||||||
|
typeof autocomplete === "boolean"
|
||||||
|
? autocomplete
|
||||||
|
? "on"
|
||||||
|
: "off"
|
||||||
|
: undefined
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
focus = autofocus || false
|
focus = autofocus || false
|
||||||
|
@ -105,7 +112,7 @@
|
||||||
class="spectrum-Textfield-input"
|
class="spectrum-Textfield-input"
|
||||||
style={align ? `text-align: ${align};` : ""}
|
style={align ? `text-align: ${align};` : ""}
|
||||||
inputmode={getInputMode(type)}
|
inputmode={getInputMode(type)}
|
||||||
{autocomplete}
|
autocomplete={autocompleteValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
import FieldLabel from "./FieldLabel.svelte"
|
import FieldLabel from "./FieldLabel.svelte"
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
|
||||||
export let id: string | null = null
|
export let id: string | undefined = undefined
|
||||||
export let label: string | null = null
|
export let label: string | undefined = undefined
|
||||||
export let labelPosition = "above"
|
export let labelPosition: string = "above"
|
||||||
export let error: string | null = null
|
export let error: string | undefined = undefined
|
||||||
export let helpText: string | null = null
|
export let helpText: string | undefined = undefined
|
||||||
export let tooltip = ""
|
export let tooltip: string | undefined = undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="spectrum-Form-item" class:above={labelPosition === "above"}>
|
<div class="spectrum-Form-item" class:above={labelPosition === "above"}>
|
||||||
|
|
|
@ -3,19 +3,19 @@
|
||||||
import TextField from "./Core/TextField.svelte"
|
import TextField from "./Core/TextField.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let value: string | null = null
|
export let value: any = undefined
|
||||||
export let label: string | null = null
|
export let label: string | undefined = undefined
|
||||||
export let labelPosition = "above"
|
export let labelPosition = "above"
|
||||||
export let placeholder: string | null = null
|
export let placeholder: string | undefined = undefined
|
||||||
export let type = "text"
|
export let type = "text"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let error = null
|
export let error: string | undefined = undefined
|
||||||
export let updateOnChange = true
|
export let updateOnChange = true
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let autofocus: boolean | null = null
|
export let autofocus: boolean | undefined = undefined
|
||||||
export let autocomplete: string | null = null
|
export let autocomplete: boolean | undefined = undefined
|
||||||
export let helpText = null
|
export let helpText: string | undefined = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = (e: any) => {
|
const onChange = (e: any) => {
|
||||||
|
|
|
@ -1,44 +1,54 @@
|
||||||
<script>
|
<script lang="ts" context="module">
|
||||||
|
type O = any
|
||||||
|
type V = any
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import Select from "./Core/Select.svelte"
|
import Select from "./Core/Select.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { PopoverAlignment } from "../constants"
|
||||||
|
|
||||||
export let value = null
|
export let value: V | undefined = undefined
|
||||||
export let label = undefined
|
export let label: string | undefined = undefined
|
||||||
export let disabled = false
|
export let disabled: boolean = false
|
||||||
export let readonly = false
|
export let readonly: boolean = false
|
||||||
export let labelPosition = "above"
|
export let labelPosition: string = "above"
|
||||||
export let error = null
|
export let error: string | undefined = undefined
|
||||||
export let placeholder = "Choose an option"
|
export let placeholder: string | boolean = "Choose an option"
|
||||||
export let options = []
|
export let options: O[] = []
|
||||||
export let getOptionLabel = option => extractProperty(option, "label")
|
export let getOptionLabel = (option: O, _index?: number) =>
|
||||||
export let getOptionValue = option => extractProperty(option, "value")
|
extractProperty(option, "label")
|
||||||
export let getOptionSubtitle = option => option?.subtitle
|
export let getOptionValue = (option: O, _index?: number) =>
|
||||||
export let getOptionIcon = option => option?.icon
|
extractProperty(option, "value")
|
||||||
export let getOptionColour = option => option?.colour
|
export let getOptionSubtitle = (option: O, _index?: number) =>
|
||||||
|
option?.subtitle
|
||||||
|
export let getOptionIcon = (option: O, _index?: number) => option?.icon
|
||||||
|
export let getOptionColour = (option: O, _index?: number) => option?.colour
|
||||||
export let useOptionIconImage = false
|
export let useOptionIconImage = false
|
||||||
export let isOptionEnabled = undefined
|
export let isOptionEnabled:
|
||||||
export let quiet = false
|
| ((_option: O, _index?: number) => boolean)
|
||||||
export let autoWidth = false
|
| undefined = undefined
|
||||||
export let sort = false
|
export let quiet: boolean = false
|
||||||
export let tooltip = ""
|
export let autoWidth: boolean = false
|
||||||
export let autocomplete = false
|
export let sort: boolean = false
|
||||||
export let customPopoverHeight = undefined
|
export let tooltip: string | undefined = undefined
|
||||||
export let align = undefined
|
export let autocomplete: boolean = false
|
||||||
export let footer = null
|
export let customPopoverHeight: string | undefined = undefined
|
||||||
export let tag = null
|
export let align: PopoverAlignment | undefined = PopoverAlignment.Left
|
||||||
export let helpText = null
|
export let footer: string | undefined = undefined
|
||||||
export let compare = undefined
|
export let helpText: string | undefined = undefined
|
||||||
|
export let compare: any = undefined
|
||||||
export let onOptionMouseenter = () => {}
|
export let onOptionMouseenter = () => {}
|
||||||
export let onOptionMouseleave = () => {}
|
export let onOptionMouseleave = () => {}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = (e: CustomEvent<any>) => {
|
||||||
value = e.detail
|
value = e.detail
|
||||||
dispatch("change", e.detail)
|
dispatch("change", e.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractProperty = (value, property) => {
|
const extractProperty = (value: any, property: any) => {
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
return value[property]
|
return value[property]
|
||||||
}
|
}
|
||||||
|
@ -49,7 +59,6 @@
|
||||||
<Field {helpText} {label} {labelPosition} {error} {tooltip}>
|
<Field {helpText} {label} {labelPosition} {error} {tooltip}>
|
||||||
<Select
|
<Select
|
||||||
{quiet}
|
{quiet}
|
||||||
{error}
|
|
||||||
{disabled}
|
{disabled}
|
||||||
{readonly}
|
{readonly}
|
||||||
{value}
|
{value}
|
||||||
|
@ -68,7 +77,6 @@
|
||||||
{isOptionEnabled}
|
{isOptionEnabled}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{customPopoverHeight}
|
{customPopoverHeight}
|
||||||
{tag}
|
|
||||||
{compare}
|
{compare}
|
||||||
{onOptionMouseenter}
|
{onOptionMouseenter}
|
||||||
{onOptionMouseleave}
|
{onOptionMouseleave}
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
|
||||||
default as AbsTooltip,
|
import { TooltipPosition, TooltipType } from "../constants"
|
||||||
TooltipPosition,
|
|
||||||
TooltipType,
|
|
||||||
} from "../Tooltip/AbsTooltip.svelte"
|
|
||||||
|
|
||||||
export let name: string = "Add"
|
export let name: string = "Add"
|
||||||
|
export let size: "XS" | "S" | "M" | "L" | "XL" = "M"
|
||||||
export let hidden: boolean = false
|
export let hidden: boolean = false
|
||||||
export let size = "M"
|
|
||||||
export let hoverable: boolean = false
|
export let hoverable: boolean = false
|
||||||
export let disabled: boolean = false
|
export let disabled: boolean = false
|
||||||
export let color: string | undefined = undefined
|
export let color: string | undefined = undefined
|
||||||
|
@ -81,17 +78,6 @@
|
||||||
color: var(--spectrum-global-color-gray-500) !important;
|
color: var(--spectrum-global-color-gray-500) !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
left: 50%;
|
|
||||||
bottom: calc(100% + 4px);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
text-align: center;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spectrum-Icon--sizeXS {
|
.spectrum-Icon--sizeXS {
|
||||||
width: var(--spectrum-global-dimension-size-150);
|
width: var(--spectrum-global-dimension-size-150);
|
||||||
height: var(--spectrum-global-dimension-size-150);
|
height: var(--spectrum-global-dimension-size-150);
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
export let primary = false
|
export let primary = false
|
||||||
export let secondary = false
|
export let secondary = false
|
||||||
export let overBackground = false
|
export let overBackground = false
|
||||||
export let target
|
export let target = undefined
|
||||||
export let download = null
|
export let download = undefined
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let tooltip = null
|
export let tooltip = null
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
export interface PopoverAPI {
|
||||||
|
show: () => void
|
||||||
|
hide: () => void
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@spectrum-css/popover/dist/index-vars.css"
|
import "@spectrum-css/popover/dist/index-vars.css"
|
||||||
// @ts-expect-error no types for the version of svelte-portal we're on.
|
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
import { createEventDispatcher, getContext, onDestroy } from "svelte"
|
import { createEventDispatcher, getContext, onDestroy } from "svelte"
|
||||||
import positionDropdown, {
|
import positionDropdown, {
|
||||||
|
@ -10,12 +16,10 @@
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
import Context from "../context"
|
import Context from "../context"
|
||||||
import type { KeyboardEventHandler } from "svelte/elements"
|
import type { KeyboardEventHandler } from "svelte/elements"
|
||||||
|
import { PopoverAlignment } from "../constants"
|
||||||
const dispatch = createEventDispatcher<{ open: void; close: void }>()
|
|
||||||
|
|
||||||
export let anchor: HTMLElement
|
export let anchor: HTMLElement
|
||||||
export let align: "left" | "right" | "left-outside" | "right-outside" =
|
export let align: PopoverAlignment = PopoverAlignment.Right
|
||||||
"right"
|
|
||||||
export let portalTarget: string | undefined = undefined
|
export let portalTarget: string | undefined = undefined
|
||||||
export let minWidth: number | undefined = undefined
|
export let minWidth: number | undefined = undefined
|
||||||
export let maxWidth: number | undefined = undefined
|
export let maxWidth: number | undefined = undefined
|
||||||
|
@ -26,19 +30,24 @@
|
||||||
export let offset = 4
|
export let offset = 4
|
||||||
export let customHeight: string | undefined = undefined
|
export let customHeight: string | undefined = undefined
|
||||||
export let animate = true
|
export let animate = true
|
||||||
export let customZindex: string | undefined = undefined
|
export let customZIndex: number | undefined = undefined
|
||||||
export let handlePostionUpdate: UpdateHandler | undefined = undefined
|
export let handlePositionUpdate: UpdateHandler | undefined = undefined
|
||||||
export let showPopover = true
|
export let showPopover = true
|
||||||
export let clickOutsideOverride = false
|
export let clickOutsideOverride = false
|
||||||
export let resizable = true
|
export let resizable = true
|
||||||
export let wrap = false
|
export let wrap = false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{ open: void; close: void }>()
|
||||||
const animationDuration = 260
|
const animationDuration = 260
|
||||||
|
|
||||||
let timeout: ReturnType<typeof setTimeout>
|
let timeout: ReturnType<typeof setTimeout>
|
||||||
let blockPointerEvents = false
|
let blockPointerEvents = false
|
||||||
|
|
||||||
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
// Portal library lacks types, so we have to type this as any even though it's
|
||||||
|
// actually a string
|
||||||
|
$: target = (portalTarget ||
|
||||||
|
getContext(Context.PopoverRoot) ||
|
||||||
|
".spectrum") as any
|
||||||
$: {
|
$: {
|
||||||
// Disable pointer events for the initial part of the animation, because we
|
// Disable pointer events for the initial part of the animation, because we
|
||||||
// fly from top to bottom and initially can be positioned under the cursor,
|
// fly from top to bottom and initially can be positioned under the cursor,
|
||||||
|
@ -118,7 +127,7 @@
|
||||||
minWidth,
|
minWidth,
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset,
|
offset,
|
||||||
customUpdate: handlePostionUpdate,
|
customUpdate: handlePositionUpdate,
|
||||||
resizable,
|
resizable,
|
||||||
wrap,
|
wrap,
|
||||||
}}
|
}}
|
||||||
|
@ -128,11 +137,11 @@
|
||||||
}}
|
}}
|
||||||
on:keydown={handleEscape}
|
on:keydown={handleEscape}
|
||||||
class="spectrum-Popover is-open"
|
class="spectrum-Popover is-open"
|
||||||
class:customZindex
|
class:customZIndex
|
||||||
class:hidden={!showPopover}
|
class:hidden={!showPopover}
|
||||||
class:blockPointerEvents
|
class:blockPointerEvents
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style="height: {customHeight}; --customZindex: {customZindex};"
|
style="height: {customHeight}; --customZIndex: {customZIndex};"
|
||||||
transition:fly|local={{
|
transition:fly|local={{
|
||||||
y: -20,
|
y: -20,
|
||||||
duration: animate ? animationDuration : 0,
|
duration: animate ? animationDuration : 0,
|
||||||
|
@ -162,7 +171,7 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.customZindex {
|
.customZIndex {
|
||||||
z-index: var(--customZindex) !important;
|
z-index: var(--customZIndex) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
<script>
|
|
||||||
import { View } from "svench";
|
|
||||||
import Popover from "./Popover.svelte";
|
|
||||||
import Button from "../Button/Button.svelte";
|
|
||||||
import TextButton from "../Button/TextButton.svelte";
|
|
||||||
import Icon from "../Icons/Icon.svelte";
|
|
||||||
import Input from "../Form/Input.svelte";
|
|
||||||
import Select from "../Form/Select.svelte";
|
|
||||||
|
|
||||||
let anchorRight;
|
|
||||||
let anchorLeft;
|
|
||||||
let dropdownRight;
|
|
||||||
let dropdownLeft;
|
|
||||||
|
|
||||||
const options = ["Column 1", "Column 2", "Super cool column"];
|
|
||||||
const option1s = ["Is", "Is not", "Contains" , "Does not contain"];
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.button-group {
|
|
||||||
margin-top: var(--spacing-l);
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
margin: 0 0 var(--spacing-l) 0;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group-column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: [boolean-start] 60px [boolean-end property-start] 120px [property-end opererator-start] 110px [operator-end value-start] auto [value-end menu-start] 32px [menu-end];
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
margin-bottom: var(--spacing-l);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin:0;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<View name="Simple popover">
|
|
||||||
<div bind:this={anchorLeft}>
|
|
||||||
<Button text on:click={dropdownLeft.show}>
|
|
||||||
<Icon name="view" />
|
|
||||||
Add View
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Popover bind:this={dropdownLeft} anchor={anchorLeft} align="left">
|
|
||||||
<h6>Add New View</h6>
|
|
||||||
<Input thin placeholder="Enter your name" />
|
|
||||||
<div class="button-group">
|
|
||||||
<Button secondary on:click={() => alert('Clicked!')}>Cancel</Button>
|
|
||||||
<Button primary on:click={() => alert('Clicked!')}>Add New View</Button>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View name="Stacked columns">
|
|
||||||
<div bind:this={anchorRight}>
|
|
||||||
<Button text on:click={dropdownRight.show}>
|
|
||||||
<Icon name="addrow" />
|
|
||||||
Add Row
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Popover bind:this={dropdownRight} anchor={anchorRight}>
|
|
||||||
<h6>Add New Row</h6>
|
|
||||||
<div class="input-group-column">
|
|
||||||
<Input thin placeholder="Enter your string" />
|
|
||||||
<Input thin placeholder="Enter your string" />
|
|
||||||
<Input thin placeholder="Enter your string" />
|
|
||||||
</div>
|
|
||||||
<div class="button-group">
|
|
||||||
<Button secondary on:click={() => alert('Clicked!')}>Cancel</Button>
|
|
||||||
<Button primary on:click={() => alert('Clicked!')}>Add New Row</Button>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View name="Multiple inputs in a row">
|
|
||||||
<div bind:this={anchorLeft}>
|
|
||||||
<Button text on:click={dropdownLeft.show}>
|
|
||||||
<Icon name="filter" />
|
|
||||||
Add Filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Popover bind:this={dropdownLeft} anchor={anchorLeft} align="left">
|
|
||||||
<h6>Add New Filter</h6>
|
|
||||||
<div class="input-group-row">
|
|
||||||
<p>Where</p>
|
|
||||||
<Select secondary thin name="Test">
|
|
||||||
{#each options as option}
|
|
||||||
<option value={option}>{option}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
<Select secondary thin name="Test">
|
|
||||||
{#each option1s as option1}
|
|
||||||
<option value={option1}>{option1}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
<Input thin placeholder="Enter your name" />
|
|
||||||
<Button text on:click={() => alert('Clicked!')}>
|
|
||||||
<Icon name="close" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button text on:click={() => alert('Clicked!')}>Add Filter</Button>
|
|
||||||
</Popover>
|
|
||||||
</View>
|
|
|
@ -1,25 +1,25 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import "@spectrum-css/statuslight"
|
import "@spectrum-css/statuslight"
|
||||||
|
|
||||||
export let size = "M"
|
export let size: string = "M"
|
||||||
export let celery = false
|
export let celery: boolean = false
|
||||||
export let yellow = false
|
export let yellow: boolean = false
|
||||||
export let fuchsia = false
|
export let fuchsia: boolean = false
|
||||||
export let indigo = false
|
export let indigo: boolean = false
|
||||||
export let seafoam = false
|
export let seafoam: boolean = false
|
||||||
export let chartreuse = false
|
export let chartreuse: boolean = false
|
||||||
export let magenta = false
|
export let magenta: boolean = false
|
||||||
export let purple = false
|
export let purple: boolean = false
|
||||||
export let neutral = false
|
export let neutral: boolean = false
|
||||||
export let info = false
|
export let info: boolean = false
|
||||||
export let positive = false
|
export let positive: boolean = false
|
||||||
export let notice = false
|
export let notice: boolean = false
|
||||||
export let negative = false
|
export let negative: boolean = false
|
||||||
export let disabled = false
|
export let disabled: boolean = false
|
||||||
export let active = false
|
export let active: boolean = false
|
||||||
export let color = null
|
export let color: string | undefined = undefined
|
||||||
export let square = false
|
export let square: boolean = false
|
||||||
export let hoverable = false
|
export let hoverable: boolean = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
|
|
@ -1,38 +1,24 @@
|
||||||
<script context="module">
|
<script lang="ts">
|
||||||
export const TooltipPosition = {
|
|
||||||
Top: "top",
|
|
||||||
Right: "right",
|
|
||||||
Bottom: "bottom",
|
|
||||||
Left: "left",
|
|
||||||
}
|
|
||||||
export const TooltipType = {
|
|
||||||
Default: "default",
|
|
||||||
Info: "info",
|
|
||||||
Positive: "positive",
|
|
||||||
Negative: "negative",
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import "@spectrum-css/tooltip/dist/index-vars.css"
|
import "@spectrum-css/tooltip/dist/index-vars.css"
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
import { TooltipPosition, TooltipType } from "../constants"
|
||||||
|
|
||||||
export let position = TooltipPosition.Top
|
export let position: TooltipPosition = TooltipPosition.Top
|
||||||
export let type = TooltipType.Default
|
export let type: TooltipType = TooltipType.Default
|
||||||
export let text = ""
|
export let text: string = ""
|
||||||
export let fixed = false
|
export let fixed: boolean = false
|
||||||
export let color = ""
|
export let color: string | undefined = undefined
|
||||||
export let noWrap = false
|
export let noWrap: boolean = false
|
||||||
|
|
||||||
let wrapper
|
let wrapper: HTMLElement | undefined
|
||||||
let hovered = false
|
let hovered = false
|
||||||
let left
|
let left: number | undefined
|
||||||
let top
|
let top: number | undefined
|
||||||
let visible = false
|
let visible = false
|
||||||
let timeout
|
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||||
let interval
|
let interval: ReturnType<typeof setInterval> | undefined
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (hovered || fixed) {
|
if (hovered || fixed) {
|
||||||
|
@ -49,8 +35,8 @@
|
||||||
const updateTooltipPosition = () => {
|
const updateTooltipPosition = () => {
|
||||||
const node = wrapper?.children?.[0]
|
const node = wrapper?.children?.[0]
|
||||||
if (!node) {
|
if (!node) {
|
||||||
left = null
|
left = undefined
|
||||||
top = null
|
top = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const bounds = node.getBoundingClientRect()
|
const bounds = node.getBoundingClientRect()
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
export enum PopoverAlignment {
|
||||||
|
Left = "left",
|
||||||
|
Right = "right",
|
||||||
|
LeftOutside = "left-outside",
|
||||||
|
RightOutside = "right-outside",
|
||||||
|
Center = "center",
|
||||||
|
RightContextMenu = "right-context-menu",
|
||||||
|
LeftContextMenu = "left-context-menu",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TooltipPosition {
|
||||||
|
Top = "top",
|
||||||
|
Right = "right",
|
||||||
|
Bottom = "bottom",
|
||||||
|
Left = "left",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TooltipType {
|
||||||
|
Default = "default",
|
||||||
|
Info = "info",
|
||||||
|
Positive = "positive",
|
||||||
|
Negative = "negative",
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import "./bbui.css"
|
import "./bbui.css"
|
||||||
|
|
||||||
// Spectrum icons
|
|
||||||
import "@spectrum-css/icon/dist/index-vars.css"
|
import "@spectrum-css/icon/dist/index-vars.css"
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
export * from "./constants"
|
||||||
|
|
||||||
// Form components
|
// Form components
|
||||||
export { default as Input } from "./Form/Input.svelte"
|
export { default as Input } from "./Form/Input.svelte"
|
||||||
export { default as Stepper } from "./Form/Stepper.svelte"
|
export { default as Stepper } from "./Form/Stepper.svelte"
|
||||||
|
@ -45,7 +46,7 @@ export { default as ClearButton } from "./ClearButton/ClearButton.svelte"
|
||||||
export { default as Icon } from "./Icon/Icon.svelte"
|
export { default as Icon } from "./Icon/Icon.svelte"
|
||||||
export { default as IconAvatar } from "./Icon/IconAvatar.svelte"
|
export { default as IconAvatar } from "./Icon/IconAvatar.svelte"
|
||||||
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
||||||
export { default as Popover } from "./Popover/Popover.svelte"
|
export { default as Popover, type PopoverAPI } from "./Popover/Popover.svelte"
|
||||||
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
||||||
export { default as ProgressCircle } from "./ProgressCircle/ProgressCircle.svelte"
|
export { default as ProgressCircle } from "./ProgressCircle/ProgressCircle.svelte"
|
||||||
export { default as Label } from "./Label/Label.svelte"
|
export { default as Label } from "./Label/Label.svelte"
|
||||||
|
@ -92,7 +93,6 @@ export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
||||||
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
||||||
export { default as Accordion } from "./Accordion/Accordion.svelte"
|
export { default as Accordion } from "./Accordion/Accordion.svelte"
|
||||||
export { default as AbsTooltip } from "./Tooltip/AbsTooltip.svelte"
|
export { default as AbsTooltip } from "./Tooltip/AbsTooltip.svelte"
|
||||||
export { TooltipPosition, TooltipType } from "./Tooltip/AbsTooltip.svelte"
|
|
||||||
|
|
||||||
// Renderers
|
// Renderers
|
||||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
"shortid": "2.2.15",
|
"shortid": "2.2.15",
|
||||||
"svelte-dnd-action": "^0.9.8",
|
"svelte-dnd-action": "^0.9.8",
|
||||||
"svelte-loading-spinners": "^0.1.1",
|
"svelte-loading-spinners": "^0.1.1",
|
||||||
"svelte-portal": "1.0.0",
|
"svelte-portal": "^2.2.1",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { InlineAlert, Link, Input, notifications } from "@budibase/bbui"
|
import { Link, notifications } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
appStore,
|
appStore,
|
||||||
datasources,
|
datasources,
|
||||||
|
@ -10,9 +10,11 @@
|
||||||
viewsV2,
|
viewsV2,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||||
|
import { helpers, utils } from "@budibase/shared-core"
|
||||||
import { SourceType } from "@budibase/types"
|
import { SourceType } from "@budibase/types"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||||
|
import { get } from "svelte/store"
|
||||||
import type { Table, ViewV2, View, Datasource, Query } from "@budibase/types"
|
import type { Table, ViewV2, View, Datasource, Query } from "@budibase/types"
|
||||||
|
|
||||||
export let source: Table | ViewV2 | Datasource | Query | undefined
|
export let source: Table | ViewV2 | Datasource | Query | undefined
|
||||||
|
@ -20,22 +22,16 @@
|
||||||
let confirmDeleteDialog: any
|
let confirmDeleteDialog: any
|
||||||
let affectedScreens: { text: string; url: string }[] = []
|
let affectedScreens: { text: string; url: string }[] = []
|
||||||
let sourceType: SourceType | undefined = undefined
|
let sourceType: SourceType | undefined = undefined
|
||||||
let viewsMessage: string = ""
|
|
||||||
let deleteSourceName: string | undefined
|
|
||||||
|
|
||||||
const getViewsMessage = () => {
|
const getDatasourceQueries = () => {
|
||||||
if (!source || !("views" in source)) {
|
if (sourceType !== SourceType.DATASOURCE) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
const views = Object.values(source?.views ?? [])
|
const sourceId = getSourceID()
|
||||||
if (views.length < 1) {
|
const queryList = get(queries).list.filter(
|
||||||
return ""
|
query => query.datasourceId === sourceId
|
||||||
}
|
)
|
||||||
if (views.length === 1) {
|
return queryList
|
||||||
return ", including 1 view"
|
|
||||||
}
|
|
||||||
|
|
||||||
return `, including ${views.length} views`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSourceID(): string {
|
function getSourceID(): string {
|
||||||
|
@ -49,8 +45,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
export const show = async () => {
|
export const show = async () => {
|
||||||
viewsMessage = getViewsMessage()
|
const usage = await screenStore.usageInScreens(getSourceID())
|
||||||
const usage = await screenStore.usageOfScreens(getSourceID())
|
|
||||||
affectedScreens = processScreens(usage.screens)
|
affectedScreens = processScreens(usage.screens)
|
||||||
sourceType = usage.sourceType
|
sourceType = usage.sourceType
|
||||||
confirmDeleteDialog.show()
|
confirmDeleteDialog.show()
|
||||||
|
@ -66,11 +61,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideDeleteDialog() {
|
function hideDeleteDialog() {
|
||||||
deleteSourceName = ""
|
sourceType = undefined
|
||||||
}
|
|
||||||
|
|
||||||
const autofillSourceName = () => {
|
|
||||||
deleteSourceName = source?.name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteTable(table: Table & { datasourceId?: string }) {
|
async function deleteTable(table: Table & { datasourceId?: string }) {
|
||||||
|
@ -95,18 +86,31 @@
|
||||||
|
|
||||||
async function deleteView(view: ViewV2 | View) {
|
async function deleteView(view: ViewV2 | View) {
|
||||||
try {
|
try {
|
||||||
if ("version" in view && view.version === 2) {
|
if (helpers.views.isV2(view)) {
|
||||||
await viewsV2.delete(view as ViewV2)
|
await viewsV2.delete(view as ViewV2)
|
||||||
} else {
|
} else {
|
||||||
await views.delete(view as View)
|
await views.delete(view as View)
|
||||||
}
|
}
|
||||||
notifications.success("View deleted")
|
notifications.success("View deleted")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
|
||||||
notifications.error("Error deleting view")
|
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) {
|
async function deleteQuery(query: Query) {
|
||||||
try {
|
try {
|
||||||
// Go back to the datasource if we are deleting the active query
|
// Go back to the datasource if we are deleting the active query
|
||||||
|
@ -134,57 +138,71 @@
|
||||||
case SourceType.QUERY:
|
case SourceType.QUERY:
|
||||||
return await deleteQuery(source as Query)
|
return await deleteQuery(source as Query)
|
||||||
case SourceType.DATASOURCE:
|
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>
|
</script>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
okText={`Delete ${sourceType}`}
|
okText="Delete"
|
||||||
onOk={deleteSource}
|
onOk={deleteSource}
|
||||||
onCancel={hideDeleteDialog}
|
onCancel={hideDeleteDialog}
|
||||||
title="Confirm Deletion"
|
title={`Are you sure you want to delete this ${sourceType}?`}
|
||||||
disabled={deleteSourceName !== source?.name}
|
|
||||||
>
|
>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p class="firstWarning">
|
{#if sourceType}
|
||||||
Are you sure you wish to delete the {sourceType}
|
<p class="warning">
|
||||||
<span class="sourceNameLine">
|
{buildMessage(sourceType)}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<b on:click={autofillSourceName} class="sourceName">{source?.name}</b>
|
|
||||||
<span>?</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="secondWarning">
|
|
||||||
All {sourceType} data will be deleted{viewsMessage}.
|
|
||||||
</p>
|
|
||||||
<p class="thirdWarning">This action <b>cannot be undone</b>.</p>
|
|
||||||
|
|
||||||
{#if affectedScreens.length > 0}
|
{#if affectedScreens.length > 0}
|
||||||
<div class="affectedScreens">
|
<span class="screens">
|
||||||
<InlineAlert
|
{#each affectedScreens as item, idx}
|
||||||
header={`The following screens use this ${sourceType} and may no longer function as expected`}
|
<Link overBackground target="_blank" href={item.url}
|
||||||
|
>{item.text}{idx !== affectedScreens.length - 1
|
||||||
|
? ","
|
||||||
|
: ""}</Link
|
||||||
>
|
>
|
||||||
<ul class="affectedScreensList">
|
|
||||||
{#each affectedScreens as item}
|
|
||||||
<li>
|
|
||||||
<Link quiet overBackground target="_blank" href={item.url}
|
|
||||||
>{item.text}</Link
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</span>
|
||||||
</InlineAlert>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
<p class="fourthWarning">
|
|
||||||
Please enter the "<b on:click={autofillSourceName} class="sourceName"
|
|
||||||
>{source?.name}</b
|
|
||||||
>" below to confirm.
|
|
||||||
</p>
|
</p>
|
||||||
<Input bind:value={deleteSourceName} placeholder={source?.name} />
|
{/if}
|
||||||
|
<p class="warning">
|
||||||
|
<b>This action cannot be undone.</b>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
@ -194,61 +212,15 @@
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.firstWarning {
|
.warning {
|
||||||
margin: 0 0 12px;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sourceNameLine {
|
|
||||||
display: inline-flex;
|
|
||||||
max-width: 100%;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sourceName {
|
|
||||||
flex-grow: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondWarning {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thirdWarning {
|
.screens {
|
||||||
margin: 0 0 12px;
|
display: flex;
|
||||||
max-width: 100%;
|
flex-direction: row;
|
||||||
}
|
padding-bottom: var(--spacing-l);
|
||||||
|
gap: var(--spacing-xs);
|
||||||
.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>
|
</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,15 +1,20 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Popover, Icon } from "@budibase/bbui"
|
import {
|
||||||
import { PopoverAlign } from "@budibase/types"
|
Popover,
|
||||||
|
Icon,
|
||||||
|
PopoverAlignment,
|
||||||
|
type PopoverAPI,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
export let title: string = ""
|
export let title: string = ""
|
||||||
export let align: PopoverAlign = PopoverAlign.Left
|
export let subtitle: string | undefined = undefined
|
||||||
|
export let align: PopoverAlignment = PopoverAlignment.Left
|
||||||
export let showPopover: boolean = true
|
export let showPopover: boolean = true
|
||||||
export let width: number = 400
|
export let width: number | undefined = undefined
|
||||||
|
|
||||||
let popover: any
|
let popover: PopoverAPI | undefined
|
||||||
let anchor: HTMLElement
|
let anchor: HTMLElement | undefined
|
||||||
let open: boolean
|
let open: boolean = false
|
||||||
|
|
||||||
export const show = () => popover?.show()
|
export const show = () => popover?.show()
|
||||||
export const hide = () => popover?.hide()
|
export const hide = () => popover?.hide()
|
||||||
|
@ -31,21 +36,25 @@
|
||||||
{showPopover}
|
{showPopover}
|
||||||
on:open
|
on:open
|
||||||
on:close
|
on:close
|
||||||
customZindex={100}
|
customZIndex={100}
|
||||||
>
|
>
|
||||||
<div class="detail-popover">
|
<div class="detail-popover">
|
||||||
<div class="detail-popover__header">
|
<div class="detail-popover__header">
|
||||||
<div class="detail-popover__title">
|
<div class="detail-popover__title">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
|
||||||
<Icon
|
<Icon
|
||||||
name="Close"
|
name="Close"
|
||||||
hoverable
|
hoverable
|
||||||
color="var(--spectrum-global-color-gray-600)"
|
color="var(--spectrum-global-color-gray-600)"
|
||||||
hoverColor="var(--spectum-global-color-gray-900)"
|
hoverColor="var(--spectrum-global-color-gray-900)"
|
||||||
on:click={hide}
|
on:click={hide}
|
||||||
|
size="S"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{#if subtitle}
|
||||||
|
<div class="detail-popover__subtitle">{subtitle}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="detail-popover__body">
|
<div class="detail-popover__body">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,14 +66,18 @@
|
||||||
background-color: var(--spectrum-alias-background-color-primary);
|
background-color: var(--spectrum-alias-background-color-primary);
|
||||||
}
|
}
|
||||||
.detail-popover__header {
|
.detail-popover__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
padding: var(--spacing-l) var(--spacing-xl);
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.detail-popover__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
|
||||||
padding: var(--spacing-l) var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.detail-popover__title {
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,281 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
interface JSONViewerClickContext {
|
||||||
|
label: string | undefined
|
||||||
|
value: any
|
||||||
|
path: (string | number)[]
|
||||||
|
}
|
||||||
|
export interface JSONViewerClickEvent {
|
||||||
|
detail: JSONViewerClickContext
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let label: string | undefined = undefined
|
||||||
|
export let value: any = undefined
|
||||||
|
export let root: boolean = true
|
||||||
|
export let path: (string | number)[] = []
|
||||||
|
export let showCopyIcon: boolean = false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const Colors = {
|
||||||
|
Array: "var(--spectrum-global-color-gray-600)",
|
||||||
|
Object: "var(--spectrum-global-color-gray-600)",
|
||||||
|
Other: "var(--spectrum-global-color-blue-700)",
|
||||||
|
Undefined: "var(--spectrum-global-color-gray-600)",
|
||||||
|
Null: "var(--spectrum-global-color-yellow-700)",
|
||||||
|
String: "var(--spectrum-global-color-orange-700)",
|
||||||
|
Number: "var(--spectrum-global-color-purple-700)",
|
||||||
|
True: "var(--spectrum-global-color-celery-700)",
|
||||||
|
False: "var(--spectrum-global-color-red-700)",
|
||||||
|
Date: "var(--spectrum-global-color-green-700)",
|
||||||
|
}
|
||||||
|
|
||||||
|
let expanded = false
|
||||||
|
let valueExpanded = false
|
||||||
|
let clickContext: JSONViewerClickContext
|
||||||
|
|
||||||
|
$: isArray = Array.isArray(value)
|
||||||
|
$: isObject = value?.toString?.() === "[object Object]"
|
||||||
|
$: primitive = !(isArray || isObject)
|
||||||
|
$: keys = getKeys(isArray, isObject, value)
|
||||||
|
$: expandable = keys.length > 0
|
||||||
|
$: displayValue = getDisplayValue(isArray, isObject, keys, value)
|
||||||
|
$: style = getStyle(isArray, isObject, value)
|
||||||
|
$: clickContext = { value, label, path }
|
||||||
|
|
||||||
|
const getKeys = (isArray: boolean, isObject: boolean, value: any) => {
|
||||||
|
if (isArray) {
|
||||||
|
return [...value.keys()]
|
||||||
|
}
|
||||||
|
if (isObject) {
|
||||||
|
return Object.keys(value).sort()
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluralise = (text: string, number: number) => {
|
||||||
|
return number === 1 ? text : text + "s"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDisplayValue = (
|
||||||
|
isArray: boolean,
|
||||||
|
isObject: boolean,
|
||||||
|
keys: any[],
|
||||||
|
value: any
|
||||||
|
) => {
|
||||||
|
if (isArray) {
|
||||||
|
return `[] ${keys.length} ${pluralise("item", keys.length)}`
|
||||||
|
}
|
||||||
|
if (isObject) {
|
||||||
|
return `{} ${keys.length} ${pluralise("key", keys.length)}`
|
||||||
|
}
|
||||||
|
if (typeof value === "object" && typeof value?.toString === "function") {
|
||||||
|
return value.toString()
|
||||||
|
} else {
|
||||||
|
return JSON.stringify(value, null, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyle = (isArray: boolean, isObject: boolean, value: any) => {
|
||||||
|
return `color:${getColor(isArray, isObject, value)};`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getColor = (isArray: boolean, isObject: boolean, value: any) => {
|
||||||
|
if (isArray) {
|
||||||
|
return Colors.Array
|
||||||
|
}
|
||||||
|
if (isObject) {
|
||||||
|
return Colors.Object
|
||||||
|
}
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return Colors.Date
|
||||||
|
}
|
||||||
|
switch (value) {
|
||||||
|
case undefined:
|
||||||
|
return Colors.Undefined
|
||||||
|
case null:
|
||||||
|
return Colors.Null
|
||||||
|
case true:
|
||||||
|
return Colors.True
|
||||||
|
case false:
|
||||||
|
return Colors.False
|
||||||
|
}
|
||||||
|
switch (typeof value) {
|
||||||
|
case "string":
|
||||||
|
return Colors.String
|
||||||
|
case "number":
|
||||||
|
return Colors.Number
|
||||||
|
}
|
||||||
|
return Colors.Other
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="binding-node">
|
||||||
|
{#if label != null}
|
||||||
|
<div class="binding-text">
|
||||||
|
<div class="binding-arrow" class:expanded>
|
||||||
|
{#if expandable}
|
||||||
|
<Icon
|
||||||
|
name="Play"
|
||||||
|
hoverable
|
||||||
|
color="var(--spectrum-global-color-gray-600)"
|
||||||
|
hoverColor="var(--spectrum-global-color-gray-900)"
|
||||||
|
on:click={() => (expanded = !expanded)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="binding-label"
|
||||||
|
class:primitive
|
||||||
|
class:expandable
|
||||||
|
on:click={() => (expanded = !expanded)}
|
||||||
|
on:click={() => dispatch("click-label", clickContext)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="binding-value"
|
||||||
|
class:primitive
|
||||||
|
class:expanded={valueExpanded}
|
||||||
|
{style}
|
||||||
|
on:click={() => (valueExpanded = !valueExpanded)}
|
||||||
|
on:click={() => dispatch("click-value", clickContext)}
|
||||||
|
>
|
||||||
|
{displayValue}
|
||||||
|
</div>
|
||||||
|
{#if showCopyIcon}
|
||||||
|
<div class="copy-value-icon">
|
||||||
|
<Icon
|
||||||
|
name="Copy"
|
||||||
|
size="XS"
|
||||||
|
hoverable
|
||||||
|
color="var(--spectrum-global-color-gray-600)"
|
||||||
|
hoverColor="var(--spectrum-global-color-gray-900)"
|
||||||
|
on:click={() => dispatch("click-copy", clickContext)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if expandable && (expanded || label == null)}
|
||||||
|
<div class="binding-children" class:root>
|
||||||
|
{#each keys as key}
|
||||||
|
<svelte:self
|
||||||
|
label={key}
|
||||||
|
value={value[key]}
|
||||||
|
root={false}
|
||||||
|
path={[...path, key]}
|
||||||
|
{showCopyIcon}
|
||||||
|
on:click-label
|
||||||
|
on:click-value
|
||||||
|
on:click-copy
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.binding-node {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expand arrow */
|
||||||
|
.binding-arrow {
|
||||||
|
margin: -3px 6px -2px 4px;
|
||||||
|
flex: 0 0 9px;
|
||||||
|
transition: transform 130ms ease-out;
|
||||||
|
}
|
||||||
|
.binding-arrow :global(svg) {
|
||||||
|
width: 9px;
|
||||||
|
}
|
||||||
|
.binding-arrow.expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main text wrapper */
|
||||||
|
.binding-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Size label and value according to type */
|
||||||
|
.binding-label {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
margin-right: 8px;
|
||||||
|
transition: color 130ms ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.binding-label.expandable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
}
|
||||||
|
.binding-value {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: filter 130ms ease-out;
|
||||||
|
}
|
||||||
|
.binding-value.primitive:hover {
|
||||||
|
filter: brightness(1.25);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.binding-value.expanded {
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: wrap;
|
||||||
|
}
|
||||||
|
.binding-label.primitive {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
.binding-value.primitive {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trim spans in the highlighted HTML */
|
||||||
|
.binding-value :global(span) {
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy icon for value */
|
||||||
|
.copy-value-icon {
|
||||||
|
display: none;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.binding-text:hover .copy-value-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Children wrapper */
|
||||||
|
.binding-children {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
border-left: 1px solid var(--spectrum-global-color-gray-400);
|
||||||
|
margin-left: 20px;
|
||||||
|
padding-left: 3px;
|
||||||
|
}
|
||||||
|
.binding-children.root {
|
||||||
|
border-left: none;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -25,7 +25,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
customZindex={998}
|
customZIndex={998}
|
||||||
bind:this={formPopover}
|
bind:this={formPopover}
|
||||||
align="center"
|
align="center"
|
||||||
anchor={formPopoverAnchor}
|
anchor={formPopoverAnchor}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { Icon, Input, Drawer, Button } from "@budibase/bbui"
|
import { Icon, Input, Drawer, Button } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
|
@ -10,25 +10,25 @@
|
||||||
import { builderStore } from "@/stores/builder"
|
import { builderStore } from "@/stores/builder"
|
||||||
|
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
export let value = ""
|
export let value: any = ""
|
||||||
export let bindings = []
|
export let bindings: any[] = []
|
||||||
export let title
|
export let title: string | undefined = undefined
|
||||||
export let placeholder
|
export let placeholder: string | undefined = undefined
|
||||||
export let label
|
export let label: string | undefined = undefined
|
||||||
export let disabled = false
|
export let disabled: boolean = false
|
||||||
export let allowHBS = true
|
export let allowHBS: boolean = true
|
||||||
export let allowJS = true
|
export let allowJS: boolean = true
|
||||||
export let allowHelpers = true
|
export let allowHelpers: boolean = true
|
||||||
export let updateOnChange = true
|
export let updateOnChange: boolean = true
|
||||||
export let key
|
export let key: string | null = null
|
||||||
export let disableBindings = false
|
export let disableBindings: boolean = false
|
||||||
export let forceModal = false
|
export let forceModal: boolean = false
|
||||||
export let context = null
|
export let context = null
|
||||||
export let autocomplete
|
export let autocomplete: boolean | undefined = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let bindingDrawer
|
let bindingDrawer: any
|
||||||
let currentVal = value
|
let currentVal = value
|
||||||
|
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
const saveBinding = () => {
|
const saveBinding = () => {
|
||||||
onChange(tempValue)
|
onChange(tempValue)
|
||||||
onBlur()
|
onBlur()
|
||||||
builderStore.propertyFocus()
|
builderStore.propertyFocus(null)
|
||||||
bindingDrawer.hide()
|
bindingDrawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
save: saveBinding,
|
save: saveBinding,
|
||||||
})
|
})
|
||||||
|
|
||||||
const onChange = value => {
|
const onChange = (value: any) => {
|
||||||
currentVal = readableToRuntimeBinding(bindings, value)
|
currentVal = readableToRuntimeBinding(bindings, value)
|
||||||
dispatch("change", currentVal)
|
dispatch("change", currentVal)
|
||||||
}
|
}
|
||||||
|
@ -55,8 +55,8 @@
|
||||||
dispatch("blur", currentVal)
|
dispatch("blur", currentVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrawerHide = e => {
|
const onDrawerHide = (e: any) => {
|
||||||
builderStore.propertyFocus()
|
builderStore.propertyFocus(null)
|
||||||
dispatch("drawerHide", e.detail)
|
dispatch("drawerHide", e.detail)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
anchor={primaryDisplayColumnAnchor}
|
anchor={primaryDisplayColumnAnchor}
|
||||||
item={columns.primary}
|
item={columns.primary}
|
||||||
on:change={e => columns.update(e.detail)}
|
on:change={e => columns.update(e.detail)}
|
||||||
|
{bindings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
export let item
|
export let item
|
||||||
export let anchor
|
export let anchor
|
||||||
|
export let bindings
|
||||||
|
|
||||||
let draggableStore = writable({
|
let draggableStore = writable({
|
||||||
selected: null,
|
selected: null,
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
componentInstance={item}
|
componentInstance={item}
|
||||||
{parseSettings}
|
{parseSettings}
|
||||||
on:change
|
on:change
|
||||||
|
{bindings}
|
||||||
>
|
>
|
||||||
<div slot="header" class="type-icon">
|
<div slot="header" class="type-icon">
|
||||||
<Icon name={icon} />
|
<Icon name={icon} />
|
||||||
|
|
|
@ -69,6 +69,7 @@ const toGridFormat = draggableListColumns => {
|
||||||
active: entry.active,
|
active: entry.active,
|
||||||
width: entry.width,
|
width: entry.width,
|
||||||
conditions: entry.conditions,
|
conditions: entry.conditions,
|
||||||
|
format: entry.format,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +86,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
|
||||||
columnType: column.columnType || schema[column.field].type,
|
columnType: column.columnType || schema[column.field].type,
|
||||||
width: column.width,
|
width: column.width,
|
||||||
conditions: column.conditions,
|
conditions: column.conditions,
|
||||||
|
format: column.format,
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "@/dataBinding"
|
} from "@/dataBinding"
|
||||||
import { builderStore } from "@/stores/builder"
|
import { builderStore } from "@/stores/builder"
|
||||||
import { onDestroy } from "svelte"
|
|
||||||
|
|
||||||
export let label = ""
|
export let label = ""
|
||||||
export let labelHidden = false
|
export let labelHidden = false
|
||||||
|
@ -32,7 +31,7 @@
|
||||||
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
||||||
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
||||||
|
|
||||||
$: if (!Array.isArray(value)) {
|
$: if (value) {
|
||||||
highlightType =
|
highlightType =
|
||||||
highlightedProp?.key === key ? `highlighted-${highlightedProp?.type}` : ""
|
highlightedProp?.key === key ? `highlighted-${highlightedProp?.type}` : ""
|
||||||
}
|
}
|
||||||
|
@ -75,12 +74,6 @@
|
||||||
? defaultValue
|
? defaultValue
|
||||||
: enriched
|
: enriched
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (highlightedProp) {
|
|
||||||
builderStore.highlightSetting(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -150,10 +143,10 @@
|
||||||
.property-control.highlighted {
|
.property-control.highlighted {
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
border-color: var(--spectrum-global-color-static-red-600);
|
border-color: var(--spectrum-global-color-static-red-600);
|
||||||
margin-top: -3.5px;
|
margin-top: -4px;
|
||||||
margin-bottom: -3.5px;
|
margin-bottom: -4px;
|
||||||
padding-bottom: 3.5px;
|
padding-bottom: 4px;
|
||||||
padding-top: 3.5px;
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-control.property-focus :global(input) {
|
.property-control.property-focus :global(input) {
|
||||||
|
@ -172,7 +165,7 @@
|
||||||
}
|
}
|
||||||
.text {
|
.text {
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
color: var(--grey-6);
|
color: var(--spectrum-global-color-gray-700);
|
||||||
grid-column: 2 / 2;
|
grid-column: 2 / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,8 +96,8 @@
|
||||||
maxWidth={300}
|
maxWidth={300}
|
||||||
dismissible={false}
|
dismissible={false}
|
||||||
offset={12}
|
offset={12}
|
||||||
handlePostionUpdate={tourStep?.positionHandler}
|
handlePositionUpdate={tourStep?.positionHandler}
|
||||||
customZindex={3}
|
customZIndex={3}
|
||||||
>
|
>
|
||||||
<div class="tour-content">
|
<div class="tour-content">
|
||||||
<Layout noPadding gap="M">
|
<Layout noPadding gap="M">
|
||||||
|
|
|
@ -1159,10 +1159,17 @@ export const buildFormSchema = (component, asset) => {
|
||||||
* Returns an array of the keys of any state variables which are set anywhere
|
* Returns an array of the keys of any state variables which are set anywhere
|
||||||
* in the app.
|
* in the app.
|
||||||
*/
|
*/
|
||||||
export const getAllStateVariables = () => {
|
export const getAllStateVariables = screen => {
|
||||||
// Find all button action settings in all components
|
let assets = []
|
||||||
|
if (screen) {
|
||||||
|
// only include state variables from a specific screen
|
||||||
|
assets.push(screen)
|
||||||
|
} else {
|
||||||
|
// otherwise include state variables from all screens
|
||||||
|
assets = getAllAssets()
|
||||||
|
}
|
||||||
let eventSettings = []
|
let eventSettings = []
|
||||||
getAllAssets().forEach(asset => {
|
assets.forEach(asset => {
|
||||||
findAllMatchingComponents(asset.props, component => {
|
findAllMatchingComponents(asset.props, component => {
|
||||||
const settings = componentStore.getComponentSettings(component._component)
|
const settings = componentStore.getComponentSettings(component._component)
|
||||||
const nestedTypes = [
|
const nestedTypes = [
|
||||||
|
@ -1214,11 +1221,17 @@ export const getAllStateVariables = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add on load settings from screens
|
// Add on load settings from screens
|
||||||
|
if (screen) {
|
||||||
|
if (screen.onLoad) {
|
||||||
|
eventSettings.push(screen.onLoad)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
get(screenStore).screens.forEach(screen => {
|
get(screenStore).screens.forEach(screen => {
|
||||||
if (screen.onLoad) {
|
if (screen.onLoad) {
|
||||||
eventSettings.push(screen.onLoad)
|
eventSettings.push(screen.onLoad)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Extract all state keys from any "update state" actions in each setting
|
// Extract all state keys from any "update state" actions in each setting
|
||||||
let bindingSet = new Set()
|
let bindingSet = new Set()
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
} from "@/dataBinding"
|
} from "@/dataBinding"
|
||||||
import { ActionButton, notifications } from "@budibase/bbui"
|
import { ActionButton, notifications } from "@budibase/bbui"
|
||||||
import { capitalise } from "@/helpers"
|
import { capitalise } from "@/helpers"
|
||||||
|
import { builderStore } from "@/stores/builder"
|
||||||
import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
|
||||||
import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js"
|
import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js"
|
||||||
|
|
||||||
|
@ -55,6 +56,17 @@
|
||||||
$: id = $selectedComponent?._id
|
$: id = $selectedComponent?._id
|
||||||
$: id, (section = tabs[0])
|
$: id, (section = tabs[0])
|
||||||
$: componentName = getComponentName(componentInstance)
|
$: componentName = getComponentName(componentInstance)
|
||||||
|
|
||||||
|
$: highlightedSetting = $builderStore.highlightedSetting
|
||||||
|
$: if (highlightedSetting) {
|
||||||
|
if (highlightedSetting.key === "_conditions") {
|
||||||
|
section = "conditions"
|
||||||
|
} else if (highlightedSetting.key === "_styles") {
|
||||||
|
section = "styles"
|
||||||
|
} else {
|
||||||
|
section = "settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $selectedComponent}
|
{#if $selectedComponent}
|
||||||
|
@ -98,7 +110,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
{#if section == "settings"}
|
{#if section === "settings"}
|
||||||
<TourWrap
|
<TourWrap
|
||||||
stepKeys={[
|
stepKeys={[
|
||||||
BUILDER_FORM_CREATE_STEPS,
|
BUILDER_FORM_CREATE_STEPS,
|
||||||
|
@ -115,7 +127,7 @@
|
||||||
/>
|
/>
|
||||||
</TourWrap>
|
</TourWrap>
|
||||||
{/if}
|
{/if}
|
||||||
{#if section == "styles"}
|
{#if section === "styles"}
|
||||||
<DesignSection
|
<DesignSection
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
{componentBindings}
|
{componentBindings}
|
||||||
|
@ -130,7 +142,7 @@
|
||||||
componentTitle={title}
|
componentTitle={title}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if section == "conditions"}
|
{#if section === "conditions"}
|
||||||
<ConditionalUISection
|
<ConditionalUISection
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
|
|
|
@ -190,7 +190,7 @@
|
||||||
<Icon name="DragHandle" size="XL" />
|
<Icon name="DragHandle" size="XL" />
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
placeholder={null}
|
placeholder={false}
|
||||||
options={actionOptions}
|
options={actionOptions}
|
||||||
bind:value={condition.action}
|
bind:value={condition.action}
|
||||||
/>
|
/>
|
||||||
|
@ -227,7 +227,7 @@
|
||||||
on:change={e => (condition.newValue = e.detail)}
|
on:change={e => (condition.newValue = e.detail)}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
placeholder={null}
|
placeholder={false}
|
||||||
options={getOperatorOptions(condition)}
|
options={getOperatorOptions(condition)}
|
||||||
bind:value={condition.operator}
|
bind:value={condition.operator}
|
||||||
on:change={e => onOperatorChange(condition, e.detail)}
|
on:change={e => onOperatorChange(condition, e.detail)}
|
||||||
|
@ -236,7 +236,7 @@
|
||||||
disabled={condition.noValue || condition.operator === "oneOf"}
|
disabled={condition.noValue || condition.operator === "oneOf"}
|
||||||
options={valueTypeOptions}
|
options={valueTypeOptions}
|
||||||
bind:value={condition.valueType}
|
bind:value={condition.valueType}
|
||||||
placeholder={null}
|
placeholder={false}
|
||||||
on:change={e => onValueTypeChange(condition, e.detail)}
|
on:change={e => onValueTypeChange(condition, e.detail)}
|
||||||
/>
|
/>
|
||||||
{#if ["string", "number"].includes(condition.valueType)}
|
{#if ["string", "number"].includes(condition.valueType)}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import { componentStore } from "@/stores/builder"
|
import { componentStore } from "@/stores/builder"
|
||||||
import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte"
|
import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte"
|
||||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||||
|
import { builderStore } from "@/stores/builder"
|
||||||
|
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let componentDefinition
|
export let componentDefinition
|
||||||
|
@ -18,6 +19,8 @@
|
||||||
let tempValue
|
let tempValue
|
||||||
let drawer
|
let drawer
|
||||||
|
|
||||||
|
$: highlighted = $builderStore.highlightedSetting?.key === "_conditions"
|
||||||
|
|
||||||
const openDrawer = () => {
|
const openDrawer = () => {
|
||||||
tempValue = JSON.parse(JSON.stringify(componentInstance?._conditions ?? []))
|
tempValue = JSON.parse(JSON.stringify(componentInstance?._conditions ?? []))
|
||||||
drawer.show()
|
drawer.show()
|
||||||
|
@ -52,7 +55,9 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DetailSummary name={"Conditions"} collapsible={false}>
|
<DetailSummary name={"Conditions"} collapsible={false}>
|
||||||
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
|
<div class:highlighted>
|
||||||
|
<ActionButton fullWidth on:click={openDrawer}>{conditionText}</ActionButton>
|
||||||
|
</div>
|
||||||
</DetailSummary>
|
</DetailSummary>
|
||||||
<Drawer bind:this={drawer} title="Conditions">
|
<Drawer bind:this={drawer} title="Conditions">
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
|
@ -61,3 +66,13 @@
|
||||||
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
|
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
|
||||||
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
|
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.highlighted {
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
border-left: 4px solid var(--spectrum-semantic-informative-color-background);
|
||||||
|
transition: background 130ms ease-out, border-color 130ms ease-out;
|
||||||
|
margin: -4px calc(-1 * var(--spacing-xl));
|
||||||
|
padding: 4px var(--spacing-xl) 4px calc(var(--spacing-xl) - 4px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableBinding,
|
runtimeToReadableBinding,
|
||||||
} from "@/dataBinding"
|
} from "@/dataBinding"
|
||||||
|
import { builderStore } from "@/stores/builder"
|
||||||
|
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let componentDefinition
|
export let componentDefinition
|
||||||
|
@ -32,6 +33,8 @@
|
||||||
|
|
||||||
$: icon = componentDefinition?.icon
|
$: icon = componentDefinition?.icon
|
||||||
|
|
||||||
|
$: highlighted = $builderStore.highlightedSetting?.key === "_styles"
|
||||||
|
|
||||||
const openDrawer = () => {
|
const openDrawer = () => {
|
||||||
tempValue = runtimeToReadableBinding(
|
tempValue = runtimeToReadableBinding(
|
||||||
bindings,
|
bindings,
|
||||||
|
@ -55,7 +58,7 @@
|
||||||
name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`}
|
name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`}
|
||||||
collapsible={false}
|
collapsible={false}
|
||||||
>
|
>
|
||||||
<div>
|
<div class:highlighted>
|
||||||
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton>
|
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</DetailSummary>
|
</DetailSummary>
|
||||||
|
@ -97,4 +100,12 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlighted {
|
||||||
|
background: var(--spectrum-global-color-gray-300);
|
||||||
|
border-left: 4px solid var(--spectrum-semantic-informative-color-background);
|
||||||
|
transition: background 130ms ease-out, border-color 130ms ease-out;
|
||||||
|
margin: -4px calc(-1 * var(--spacing-xl));
|
||||||
|
padding: 4px var(--spacing-xl) 4px calc(var(--spacing-xl) - 4px);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Layout gap="XS" paddingX="L" paddingY="XL">
|
<Layout gap="XS" paddingX="XL" paddingY="XL">
|
||||||
{#if activeTab === "theme"}
|
{#if activeTab === "theme"}
|
||||||
<ThemePanel />
|
<ThemePanel />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
if (id === `${$screenStore.selectedScreenId}-screen`) return true
|
if (id === `${$screenStore.selectedScreenId}-screen`) return true
|
||||||
if (id === `${$screenStore.selectedScreenId}-navigation`) return true
|
if (id === `${$screenStore.selectedScreenId}-navigation`) return true
|
||||||
|
|
||||||
return !!findComponent($selectedScreen.props, id)
|
return !!findComponent($selectedScreen?.props, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep URL and state in sync for selected component ID
|
// Keep URL and state in sync for selected component ID
|
||||||
|
|
|
@ -50,6 +50,9 @@
|
||||||
margin-bottom: 9px;
|
margin-bottom: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.header-left :global(div) {
|
.header-left :global(div) {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { Helpers, notifications } from "@budibase/bbui"
|
||||||
|
import { processObjectSync } from "@budibase/string-templates"
|
||||||
|
import {
|
||||||
|
previewStore,
|
||||||
|
selectedScreen,
|
||||||
|
componentStore,
|
||||||
|
snippets,
|
||||||
|
} from "@/stores/builder"
|
||||||
|
import { getBindableProperties } from "@/dataBinding"
|
||||||
|
import JSONViewer, {
|
||||||
|
type JSONViewerClickEvent,
|
||||||
|
} from "@/components/common/JSONViewer.svelte"
|
||||||
|
|
||||||
|
// Minimal typing for the real data binding structure, as none exists
|
||||||
|
type DataBinding = {
|
||||||
|
category: string
|
||||||
|
runtimeBinding: string
|
||||||
|
readableBinding: string
|
||||||
|
}
|
||||||
|
|
||||||
|
$: previewContext = $previewStore.selectedComponentContext || {}
|
||||||
|
$: selectedComponentId = $componentStore.selectedComponentId
|
||||||
|
$: context = makeContext(previewContext, bindings)
|
||||||
|
$: bindings = getBindableProperties($selectedScreen, selectedComponentId)
|
||||||
|
|
||||||
|
const makeContext = (
|
||||||
|
previewContext: Record<string, any>,
|
||||||
|
bindings: DataBinding[]
|
||||||
|
) => {
|
||||||
|
// Create a single big array to enrich in one go
|
||||||
|
const bindingStrings = bindings.map(binding => {
|
||||||
|
if (binding.runtimeBinding.startsWith('trim "')) {
|
||||||
|
// Account for nasty hardcoded HBS bindings for roles, for legacy
|
||||||
|
// compatibility
|
||||||
|
return `{{ ${binding.runtimeBinding} }}`
|
||||||
|
} else {
|
||||||
|
return `{{ literal ${binding.runtimeBinding} }}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const bindingEvaluations = processObjectSync(bindingStrings, {
|
||||||
|
...previewContext,
|
||||||
|
snippets: $snippets,
|
||||||
|
}) as any[]
|
||||||
|
|
||||||
|
// Deeply set values for all readable bindings
|
||||||
|
const enrichedBindings: any[] = bindings.map((binding, idx) => {
|
||||||
|
return {
|
||||||
|
...binding,
|
||||||
|
value: bindingEvaluations[idx],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let context = {}
|
||||||
|
for (let binding of enrichedBindings) {
|
||||||
|
Helpers.deepSet(context, binding.readableBinding, binding.value)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyBinding = (e: JSONViewerClickEvent) => {
|
||||||
|
const readableBinding = `{{ ${e.detail.path.join(".")} }}`
|
||||||
|
Helpers.copyToClipboard(readableBinding)
|
||||||
|
notifications.success("Binding copied to clipboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(previewStore.requestComponentContext)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bindings-panel">
|
||||||
|
<JSONViewer value={context} showCopyIcon on:click-copy={copyBinding} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bindings-panel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: var(--spacing-xl) var(--spacing-l) var(--spacing-l)
|
||||||
|
var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { notifications, Icon, Body } from "@budibase/bbui"
|
import { notifications, Icon } from "@budibase/bbui"
|
||||||
import { isActive, goto } from "@roxi/routify"
|
|
||||||
import {
|
import {
|
||||||
selectedScreen,
|
selectedScreen,
|
||||||
screenStore,
|
screenStore,
|
||||||
|
@ -13,23 +12,12 @@
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
import { dndStore, DropPosition } from "./dndStore.js"
|
import { dndStore, DropPosition } from "./dndStore.js"
|
||||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
|
||||||
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||||
import getScreenContextMenuItems from "./getScreenContextMenuItems"
|
import getScreenContextMenuItems from "./getScreenContextMenuItems"
|
||||||
|
|
||||||
let scrolling = false
|
|
||||||
|
|
||||||
$: screenComponentId = `${$screenStore.selectedScreenId}-screen`
|
$: screenComponentId = `${$screenStore.selectedScreenId}-screen`
|
||||||
$: navComponentId = `${$screenStore.selectedScreenId}-navigation`
|
$: navComponentId = `${$screenStore.selectedScreenId}-navigation`
|
||||||
|
|
||||||
const toNewComponentRoute = () => {
|
|
||||||
if ($isActive(`./:componentId/new`)) {
|
|
||||||
$goto(`./:componentId`)
|
|
||||||
} else {
|
|
||||||
$goto(`./:componentId/new`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDrop = async () => {
|
const onDrop = async () => {
|
||||||
try {
|
try {
|
||||||
await dndStore.actions.drop()
|
await dndStore.actions.drop()
|
||||||
|
@ -39,10 +27,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScroll = e => {
|
|
||||||
scrolling = e.target.scrollTop !== 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const hover = hoverStore.hover
|
const hover = hoverStore.hover
|
||||||
|
|
||||||
// showCopy is used to hide the copy button when the user right-clicks the empty
|
// showCopy is used to hide the copy button when the user right-clicks the empty
|
||||||
|
@ -72,17 +56,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<div class="components">
|
<div class="components">
|
||||||
<div class="header" class:scrolling>
|
|
||||||
<Body size="S">Components</Body>
|
|
||||||
<div on:click={toNewComponentRoute} class="addButton">
|
|
||||||
<Icon name="Add" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="list-panel">
|
<div class="list-panel">
|
||||||
<ComponentScrollWrapper on:scroll={handleScroll}>
|
<ComponentScrollWrapper>
|
||||||
<ul
|
<ul
|
||||||
class="componentTree"
|
class="componentTree"
|
||||||
on:contextmenu={e => openScreenContextMenu(e, false)}
|
on:contextmenu={e => openScreenContextMenu(e, false)}
|
||||||
|
@ -159,7 +135,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</ComponentScrollWrapper>
|
</ComponentScrollWrapper>
|
||||||
</div>
|
</div>
|
||||||
<ComponentKeyHandler />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -168,35 +143,13 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
padding-top: var(--spacing-l);
|
||||||
|
|
||||||
.header {
|
|
||||||
height: 50px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: var(--spacing-l);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
transition: border-bottom 130ms ease-out;
|
|
||||||
}
|
|
||||||
.header.scrolling {
|
|
||||||
border-bottom: var(--border-light);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.components :global(.nav-item) {
|
.components :global(.nav-item) {
|
||||||
padding-right: 8px !important;
|
padding-right: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addButton {
|
|
||||||
margin-left: auto;
|
|
||||||
color: var(--grey-7);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addButton:hover {
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-panel {
|
.list-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -1,25 +1,60 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import ScreenList from "./ScreenList/index.svelte"
|
import ScreenList from "./ScreenList/index.svelte"
|
||||||
import ComponentList from "./ComponentList/index.svelte"
|
import ComponentList from "./ComponentList/index.svelte"
|
||||||
import { getHorizontalResizeActions } from "@/components/common/resizable"
|
import { getHorizontalResizeActions } from "@/components/common/resizable"
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
import StatePanel from "./StatePanel.svelte"
|
||||||
|
import BindingsPanel from "./BindingsPanel.svelte"
|
||||||
|
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||||
|
|
||||||
const [resizable, resizableHandle] = getHorizontalResizeActions()
|
const [resizable, resizableHandle] = getHorizontalResizeActions()
|
||||||
|
|
||||||
|
const Tabs = {
|
||||||
|
Components: "Components",
|
||||||
|
Bindings: "Bindings",
|
||||||
|
State: "State",
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeTab = Tabs.Components
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panel" use:resizable>
|
<div class="panel" use:resizable>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ScreenList />
|
<ScreenList />
|
||||||
|
<div class="tabs">
|
||||||
|
{#each Object.values(Tabs) as tab}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
selected={activeTab === tab}
|
||||||
|
on:click={() => (activeTab = tab)}
|
||||||
|
>
|
||||||
|
<div class="tab-label">
|
||||||
|
{tab}
|
||||||
|
{#if tab !== Tabs.Components}
|
||||||
|
<div class="new">NEW</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</ActionButton>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if activeTab === Tabs.Components}
|
||||||
<ComponentList />
|
<ComponentList />
|
||||||
|
{:else if activeTab === Tabs.Bindings}
|
||||||
|
<BindingsPanel />
|
||||||
|
{:else if activeTab === Tabs.State}
|
||||||
|
<div class="tab-content"><StatePanel /></div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="divider">
|
<div class="divider">
|
||||||
<div class="dividerClickExtender" role="separator" use:resizableHandle />
|
<div class="dividerClickExtender" role="separator" use:resizableHandle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ComponentKeyHandler />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.panel {
|
.panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 270px;
|
min-width: 310px;
|
||||||
width: 310px;
|
width: 310px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -34,6 +69,34 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
padding: var(--spacing-m) var(--spacing-l);
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.tab-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.new {
|
||||||
|
font-size: 8px;
|
||||||
|
background: var(--bb-indigo);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 1px 3px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -45,7 +108,6 @@
|
||||||
background: var(--spectrum-global-color-gray-300);
|
background: var(--spectrum-global-color-gray-300);
|
||||||
cursor: row-resize;
|
cursor: row-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dividerClickExtender {
|
.dividerClickExtender {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
|
|
|
@ -0,0 +1,336 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { Select } from "@budibase/bbui"
|
||||||
|
import type {
|
||||||
|
Component,
|
||||||
|
ComponentCondition,
|
||||||
|
ComponentSetting,
|
||||||
|
EventHandler,
|
||||||
|
Screen,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { getAllStateVariables, getBindableProperties } from "@/dataBinding"
|
||||||
|
import {
|
||||||
|
componentStore,
|
||||||
|
selectedScreen,
|
||||||
|
builderStore,
|
||||||
|
previewStore,
|
||||||
|
} from "@/stores/builder"
|
||||||
|
import {
|
||||||
|
decodeJSBinding,
|
||||||
|
findHBSBlocks,
|
||||||
|
isJSBinding,
|
||||||
|
processStringSync,
|
||||||
|
} from "@budibase/string-templates"
|
||||||
|
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
|
interface ComponentUsingState {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
setting: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedKey: string | undefined = undefined
|
||||||
|
let componentsUsingState: ComponentUsingState[] = []
|
||||||
|
let componentsUpdatingState: ComponentUsingState[] = []
|
||||||
|
let editorValue: string = ""
|
||||||
|
|
||||||
|
$: selectStateKey($selectedScreen, selectedKey)
|
||||||
|
$: keyOptions = getAllStateVariables($selectedScreen)
|
||||||
|
$: bindings = getBindableProperties(
|
||||||
|
$selectedScreen,
|
||||||
|
$componentStore.selectedComponentId
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-select first valid state key
|
||||||
|
$: {
|
||||||
|
if (keyOptions.length && !keyOptions.includes(selectedKey)) {
|
||||||
|
selectedKey = keyOptions[0]
|
||||||
|
} else if (!keyOptions.length) {
|
||||||
|
selectedKey = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectStateKey = (
|
||||||
|
screen: Screen | undefined,
|
||||||
|
key: string | undefined
|
||||||
|
) => {
|
||||||
|
if (screen && key) {
|
||||||
|
searchComponents(screen, key)
|
||||||
|
editorValue = $previewStore.selectedComponentContext?.state?.[key] ?? ""
|
||||||
|
} else {
|
||||||
|
editorValue = ""
|
||||||
|
componentsUsingState = []
|
||||||
|
componentsUpdatingState = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchComponents = (screen: Screen, stateKey: string) => {
|
||||||
|
const { props, onLoad, _id } = screen
|
||||||
|
componentsUsingState = findComponentsUsingState(props, stateKey)
|
||||||
|
componentsUpdatingState = findComponentsUpdatingState(props, stateKey)
|
||||||
|
|
||||||
|
// Check screen load actions which are outside the component hierarchy
|
||||||
|
if (eventUpdatesState(onLoad, stateKey)) {
|
||||||
|
componentsUpdatingState.push({
|
||||||
|
id: _id!,
|
||||||
|
name: "Screen - On load",
|
||||||
|
setting: "onLoad",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if an event setting updates a certain state key
|
||||||
|
const eventUpdatesState = (
|
||||||
|
handlers: EventHandler[] | undefined,
|
||||||
|
stateKey: string
|
||||||
|
) => {
|
||||||
|
return handlers?.some(handler => {
|
||||||
|
return (
|
||||||
|
handler["##eventHandlerType"] === "Update State" &&
|
||||||
|
handler.parameters?.key === stateKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if a setting for the given component updates a certain state key
|
||||||
|
const settingUpdatesState = (
|
||||||
|
component: Record<string, any>,
|
||||||
|
setting: ComponentSetting,
|
||||||
|
stateKey: string
|
||||||
|
) => {
|
||||||
|
if (setting.type === "event") {
|
||||||
|
return eventUpdatesState(component[setting.key], stateKey)
|
||||||
|
} else if (setting.type === "buttonConfiguration") {
|
||||||
|
const buttons = component[setting.key]
|
||||||
|
if (Array.isArray(buttons)) {
|
||||||
|
for (let button of buttons) {
|
||||||
|
if (eventUpdatesState(button.onClick, stateKey)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if a condition updates a certain state key
|
||||||
|
const conditionUpdatesState = (
|
||||||
|
condition: ComponentCondition,
|
||||||
|
settings: ComponentSetting[],
|
||||||
|
stateKey: string
|
||||||
|
) => {
|
||||||
|
const setting = settings.find(s => s.key === condition.setting)
|
||||||
|
if (!setting) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const component = { [setting.key]: condition.settingValue }
|
||||||
|
return settingUpdatesState(component, setting, stateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const findComponentsUpdatingState = (
|
||||||
|
component: Component,
|
||||||
|
stateKey: string,
|
||||||
|
foundComponents: ComponentUsingState[] = []
|
||||||
|
): ComponentUsingState[] => {
|
||||||
|
const { _children, _conditions, _component, _instanceName, _id } = component
|
||||||
|
const settings = componentStore
|
||||||
|
.getComponentSettings(_component)
|
||||||
|
.filter(s => s.type === "event" || s.type === "buttonConfiguration")
|
||||||
|
|
||||||
|
// Check all settings of this component
|
||||||
|
settings.forEach(setting => {
|
||||||
|
if (settingUpdatesState(component, setting, stateKey)) {
|
||||||
|
const label = setting.label || setting.key
|
||||||
|
foundComponents.push({
|
||||||
|
id: _id!,
|
||||||
|
name: `${_instanceName} - ${label}`,
|
||||||
|
setting: setting.key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if conditions update these settings to update this state key
|
||||||
|
if (_conditions?.some(c => conditionUpdatesState(c, settings, stateKey))) {
|
||||||
|
foundComponents.push({
|
||||||
|
id: _id!,
|
||||||
|
name: `${_instanceName} - Conditions`,
|
||||||
|
setting: "_conditions",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check children
|
||||||
|
_children?.forEach(child => {
|
||||||
|
findComponentsUpdatingState(child, stateKey, foundComponents)
|
||||||
|
})
|
||||||
|
return foundComponents
|
||||||
|
}
|
||||||
|
|
||||||
|
const findComponentsUsingState = (
|
||||||
|
component: Component,
|
||||||
|
stateKey: string,
|
||||||
|
componentsUsingState: ComponentUsingState[] = []
|
||||||
|
): ComponentUsingState[] => {
|
||||||
|
const settings = componentStore.getComponentSettings(component._component)
|
||||||
|
|
||||||
|
// Check all settings of this component
|
||||||
|
const settingsWithState = getSettingsUsingState(component, stateKey)
|
||||||
|
settingsWithState.forEach(setting => {
|
||||||
|
// Get readable label for this setting
|
||||||
|
let label = settings.find(s => s.key === setting)?.label || setting
|
||||||
|
if (setting === "_conditions") {
|
||||||
|
label = "Conditions"
|
||||||
|
} else if (setting === "_styles") {
|
||||||
|
label = "Styles"
|
||||||
|
}
|
||||||
|
componentsUsingState.push({
|
||||||
|
id: component._id!,
|
||||||
|
name: `${component._instanceName} - ${label}`,
|
||||||
|
setting,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check children
|
||||||
|
component._children?.forEach(child => {
|
||||||
|
findComponentsUsingState(child, stateKey, componentsUsingState)
|
||||||
|
})
|
||||||
|
return componentsUsingState
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSettingsUsingState = (
|
||||||
|
component: Component,
|
||||||
|
stateKey: string
|
||||||
|
): string[] => {
|
||||||
|
return Object.entries(component)
|
||||||
|
.filter(([key]) => key !== "_children")
|
||||||
|
.filter(([_, value]) => hasStateBinding(JSON.stringify(value), stateKey))
|
||||||
|
.map(([key]) => key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasStateBinding = (value: string, stateKey: string): boolean => {
|
||||||
|
const bindings = findHBSBlocks(value).map(binding => {
|
||||||
|
const sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||||
|
return isJSBinding(sanitizedBinding)
|
||||||
|
? decodeJSBinding(sanitizedBinding)
|
||||||
|
: sanitizedBinding
|
||||||
|
})
|
||||||
|
return bindings.join(" ").includes(stateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickComponentLink = (component: ComponentUsingState) => {
|
||||||
|
componentStore.select(component.id)
|
||||||
|
builderStore.highlightSetting(component.setting)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStateInspectorChange = (e: CustomEvent) => {
|
||||||
|
if (!selectedKey || !$previewStore.selectedComponentContext) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const stateUpdate = {
|
||||||
|
[selectedKey]: processStringSync(
|
||||||
|
e.detail,
|
||||||
|
$previewStore.selectedComponentContext
|
||||||
|
),
|
||||||
|
}
|
||||||
|
previewStore.updateState(stateUpdate)
|
||||||
|
editorValue = e.detail
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
previewStore.requestComponentContext()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="state-panel">
|
||||||
|
<Select
|
||||||
|
label="State variable"
|
||||||
|
bind:value={selectedKey}
|
||||||
|
placeholder={keyOptions.length > 0 ? false : "No state variables found"}
|
||||||
|
options={keyOptions}
|
||||||
|
/>
|
||||||
|
{#if selectedKey && keyOptions.length > 0}
|
||||||
|
<DrawerBindableInput
|
||||||
|
value={editorValue}
|
||||||
|
title={`Set value for "${selectedKey}"`}
|
||||||
|
placeholder="Enter a value"
|
||||||
|
label="Set temporary value for design preview"
|
||||||
|
on:change={e => handleStateInspectorChange(e)}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if componentsUsingState.length > 0}
|
||||||
|
<div class="section">
|
||||||
|
<span class="text">Updates</span>
|
||||||
|
<div class="updates-section">
|
||||||
|
{#each componentsUsingState as component}
|
||||||
|
<button
|
||||||
|
class="component-link updates-colour"
|
||||||
|
on:click={() => onClickComponentLink(component)}
|
||||||
|
>
|
||||||
|
{component.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if componentsUpdatingState.length > 0}
|
||||||
|
<div class="section">
|
||||||
|
<span class="text">Controlled by</span>
|
||||||
|
<div class="updates-section">
|
||||||
|
{#each componentsUpdatingState as component}
|
||||||
|
<button
|
||||||
|
class="component-link controlled-by-colour"
|
||||||
|
on:click={() => onClickComponentLink(component)}
|
||||||
|
>
|
||||||
|
{component.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.state-panel {
|
||||||
|
background-color: var(--spectrum-alias-background-color-primary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.updates-colour {
|
||||||
|
color: var(--bb-indigo-light);
|
||||||
|
}
|
||||||
|
.controlled-by-colour {
|
||||||
|
color: var(--spectrum-global-color-orange-700);
|
||||||
|
}
|
||||||
|
.component-link {
|
||||||
|
display: inline-block;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: filter 130ms ease-out;
|
||||||
|
}
|
||||||
|
.component-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
.updates-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -40,26 +40,33 @@ function setupEnv(hosting, features = {}, flags = {}) {
|
||||||
describe("AISettings", () => {
|
describe("AISettings", () => {
|
||||||
let instance = null
|
let instance = null
|
||||||
|
|
||||||
|
const setupDOM = () => {
|
||||||
|
instance = render(AISettings, {})
|
||||||
|
const modalContainer = document.createElement("div")
|
||||||
|
modalContainer.classList.add("modal-container")
|
||||||
|
instance.baseElement.appendChild(modalContainer)
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks()
|
vi.restoreAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("that the AISettings is rendered", () => {
|
it("that the AISettings is rendered", () => {
|
||||||
instance = render(AISettings, {})
|
setupDOM()
|
||||||
expect(instance).toBeDefined()
|
expect(instance).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Licensing", () => {
|
describe("Licensing", () => {
|
||||||
it("should show the premium label on self host for custom configs", async () => {
|
it("should show the premium label on self host for custom configs", async () => {
|
||||||
setupEnv(Hosting.Self)
|
setupEnv(Hosting.Self)
|
||||||
instance = render(AISettings, {})
|
setupDOM()
|
||||||
const premiumTag = instance.queryByText("Premium")
|
const premiumTag = instance.queryByText("Premium")
|
||||||
expect(premiumTag).toBeInTheDocument()
|
expect(premiumTag).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should show the enterprise label on cloud for custom configs", async () => {
|
it("should show the enterprise label on cloud for custom configs", async () => {
|
||||||
setupEnv(Hosting.Cloud)
|
setupEnv(Hosting.Cloud)
|
||||||
instance = render(AISettings, {})
|
setupDOM()
|
||||||
const enterpriseTag = instance.queryByText("Enterprise")
|
const enterpriseTag = instance.queryByText("Enterprise")
|
||||||
expect(enterpriseTag).toBeInTheDocument()
|
expect(enterpriseTag).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
@ -69,7 +76,7 @@ describe("AISettings", () => {
|
||||||
let configModal
|
let configModal
|
||||||
|
|
||||||
setupEnv(Hosting.Cloud)
|
setupEnv(Hosting.Cloud)
|
||||||
instance = render(AISettings)
|
setupDOM()
|
||||||
addConfigurationButton = instance.queryByText("Add configuration")
|
addConfigurationButton = instance.queryByText("Add configuration")
|
||||||
expect(addConfigurationButton).toBeInTheDocument()
|
expect(addConfigurationButton).toBeInTheDocument()
|
||||||
await fireEvent.click(addConfigurationButton)
|
await fireEvent.click(addConfigurationButton)
|
||||||
|
@ -86,7 +93,7 @@ describe("AISettings", () => {
|
||||||
{ customAIConfigsEnabled: true },
|
{ customAIConfigsEnabled: true },
|
||||||
{ AI_CUSTOM_CONFIGS: true }
|
{ AI_CUSTOM_CONFIGS: true }
|
||||||
)
|
)
|
||||||
instance = render(AISettings)
|
setupDOM()
|
||||||
addConfigurationButton = instance.queryByText("Add configuration")
|
addConfigurationButton = instance.queryByText("Add configuration")
|
||||||
expect(addConfigurationButton).toBeInTheDocument()
|
expect(addConfigurationButton).toBeInTheDocument()
|
||||||
await fireEvent.click(addConfigurationButton)
|
await fireEvent.click(addConfigurationButton)
|
||||||
|
@ -103,7 +110,7 @@ describe("AISettings", () => {
|
||||||
{ customAIConfigsEnabled: true },
|
{ customAIConfigsEnabled: true },
|
||||||
{ AI_CUSTOM_CONFIGS: true }
|
{ AI_CUSTOM_CONFIGS: true }
|
||||||
)
|
)
|
||||||
instance = render(AISettings)
|
setupDOM()
|
||||||
addConfigurationButton = instance.queryByText("Add configuration")
|
addConfigurationButton = instance.queryByText("Add configuration")
|
||||||
expect(addConfigurationButton).toBeInTheDocument()
|
expect(addConfigurationButton).toBeInTheDocument()
|
||||||
await fireEvent.click(addConfigurationButton)
|
await fireEvent.click(addConfigurationButton)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
previewStore,
|
previewStore,
|
||||||
tables,
|
tables,
|
||||||
componentTreeNodesStore,
|
componentTreeNodesStore,
|
||||||
|
builderStore,
|
||||||
screenComponents,
|
screenComponents,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding"
|
import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding"
|
||||||
|
@ -32,7 +33,10 @@ import {
|
||||||
import { BudiStore } from "../BudiStore"
|
import { BudiStore } from "../BudiStore"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import {
|
import {
|
||||||
|
ComponentDefinition,
|
||||||
|
ComponentSetting,
|
||||||
Component as ComponentType,
|
Component as ComponentType,
|
||||||
|
ComponentCondition,
|
||||||
FieldType,
|
FieldType,
|
||||||
Screen,
|
Screen,
|
||||||
Table,
|
Table,
|
||||||
|
@ -53,29 +57,6 @@ export interface ComponentState {
|
||||||
selectedScreenId?: string | null
|
selectedScreenId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentDefinition {
|
|
||||||
component: string
|
|
||||||
name: string
|
|
||||||
friendlyName?: string
|
|
||||||
hasChildren?: boolean
|
|
||||||
settings?: ComponentSetting[]
|
|
||||||
features?: Record<string, boolean>
|
|
||||||
typeSupportPresets?: Record<string, any>
|
|
||||||
legalDirectChildren: string[]
|
|
||||||
illegalChildren: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComponentSetting {
|
|
||||||
key: string
|
|
||||||
type: string
|
|
||||||
section?: string
|
|
||||||
name?: string
|
|
||||||
defaultValue?: any
|
|
||||||
selectAllFields?: boolean
|
|
||||||
resetOn?: string | string[]
|
|
||||||
settings?: ComponentSetting[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const INITIAL_COMPONENTS_STATE: ComponentState = {
|
export const INITIAL_COMPONENTS_STATE: ComponentState = {
|
||||||
components: {},
|
components: {},
|
||||||
customComponents: [],
|
customComponents: [],
|
||||||
|
@ -743,14 +724,16 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
select(id: string) {
|
||||||
*
|
|
||||||
* @param {string} componentId
|
|
||||||
*/
|
|
||||||
select(componentId: string) {
|
|
||||||
this.update(state => {
|
this.update(state => {
|
||||||
state.selectedComponentId = componentId
|
// Only clear highlights if selecting a different component
|
||||||
return state
|
if (!id.includes(state.selectedComponentId!)) {
|
||||||
|
builderStore.highlightSetting()
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedComponentId: id,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1132,7 +1115,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateConditions(conditions: Record<string, any>) {
|
async updateConditions(conditions: ComponentCondition[]) {
|
||||||
await this.patch((component: Component) => {
|
await this.patch((component: Component) => {
|
||||||
component._conditions = conditions
|
component._conditions = conditions
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,7 +16,11 @@ import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
|
||||||
import { deploymentStore } from "./deployments.js"
|
import { deploymentStore } from "./deployments.js"
|
||||||
import { contextMenuStore } from "./contextMenu.js"
|
import { contextMenuStore } from "./contextMenu.js"
|
||||||
import { snippets } from "./snippets"
|
import { snippets } from "./snippets"
|
||||||
import { screenComponents, screenComponentErrors } from "./screenComponent"
|
import {
|
||||||
|
screenComponents,
|
||||||
|
screenComponentErrors,
|
||||||
|
findComponentsBySettingsType,
|
||||||
|
} from "./screenComponent"
|
||||||
|
|
||||||
// Backend
|
// Backend
|
||||||
import { tables } from "./tables"
|
import { tables } from "./tables"
|
||||||
|
@ -70,6 +74,7 @@ export {
|
||||||
appPublished,
|
appPublished,
|
||||||
screenComponents,
|
screenComponents,
|
||||||
screenComponentErrors,
|
screenComponentErrors,
|
||||||
|
findComponentsBySettingsType,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reset = () => {
|
export const reset = () => {
|
||||||
|
|
|
@ -82,6 +82,10 @@ export class PreviewStore extends BudiStore<PreviewState> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateState(data: Record<string, any>) {
|
||||||
|
this.sendEvent("builder-state", data)
|
||||||
|
}
|
||||||
|
|
||||||
requestComponentContext() {
|
requestComponentContext() {
|
||||||
this.sendEvent("request-context")
|
this.sendEvent("request-context")
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,17 @@ import {
|
||||||
UIDatasourceType,
|
UIDatasourceType,
|
||||||
Screen,
|
Screen,
|
||||||
Component,
|
Component,
|
||||||
|
UIComponentError,
|
||||||
ScreenProps,
|
ScreenProps,
|
||||||
|
ComponentDefinition,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { queries } from "./queries"
|
import { queries } from "./queries"
|
||||||
import { views } from "./views"
|
import { views } from "./views"
|
||||||
import { bindings, featureFlag } from "@/helpers"
|
|
||||||
import { getBindableProperties } from "@/dataBinding"
|
|
||||||
import { componentStore, ComponentDefinition } from "./components"
|
|
||||||
import { findAllComponents } from "@/helpers/components"
|
import { findAllComponents } from "@/helpers/components"
|
||||||
|
import { bindings } from "@/helpers"
|
||||||
|
import { getBindableProperties } from "@/dataBinding"
|
||||||
|
import { componentStore } from "./components"
|
||||||
|
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,
|
||||||
|
@ -52,61 +55,10 @@ export const screenComponentErrors = derived(
|
||||||
$viewsV2,
|
$viewsV2,
|
||||||
$queries,
|
$queries,
|
||||||
$componentStore,
|
$componentStore,
|
||||||
]): Record<string, string[]> => {
|
]): Record<string, UIComponentError[]> => {
|
||||||
if (!featureFlag.isEnabled("CHECK_COMPONENT_SETTINGS_ERRORS")) {
|
if (!$selectedScreen) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
function getInvalidDatasources(
|
|
||||||
screen: Screen,
|
|
||||||
datasources: Record<string, any>
|
|
||||||
) {
|
|
||||||
const result: Record<string, string[]> = {}
|
|
||||||
|
|
||||||
for (const { component, setting } of findComponentsBySettingsType(
|
|
||||||
screen,
|
|
||||||
["table", "dataSource"],
|
|
||||||
$componentStore.components
|
|
||||||
)) {
|
|
||||||
const componentSettings = component[setting.key]
|
|
||||||
if (!componentSettings) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const { label } = componentSettings
|
|
||||||
const type = componentSettings.type as UIDatasourceType
|
|
||||||
|
|
||||||
const validationKey = validationKeyByType[type]
|
|
||||||
if (!validationKey) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentBindings = getBindableProperties(
|
|
||||||
$selectedScreen,
|
|
||||||
component._id
|
|
||||||
)
|
|
||||||
|
|
||||||
const componentDatasources = {
|
|
||||||
...reduceBy(
|
|
||||||
"rowId",
|
|
||||||
bindings.extractRelationships(componentBindings)
|
|
||||||
),
|
|
||||||
...reduceBy("value", bindings.extractFields(componentBindings)),
|
|
||||||
...reduceBy(
|
|
||||||
"value",
|
|
||||||
bindings.extractJSONArrayFields(componentBindings)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceId = componentSettings[validationKey]
|
|
||||||
if (!{ ...datasources, ...componentDatasources }[resourceId]) {
|
|
||||||
const friendlyTypeName = friendlyNameByType[type] ?? type
|
|
||||||
result[component._id!] = [
|
|
||||||
`The ${friendlyTypeName} named "${label}" could not be found`,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const datasources = {
|
const datasources = {
|
||||||
...reduceBy("_id", $tables.list),
|
...reduceBy("_id", $tables.list),
|
||||||
|
@ -115,16 +67,170 @@ export const screenComponentErrors = derived(
|
||||||
...reduceBy("_id", $queries.list),
|
...reduceBy("_id", $queries.list),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$selectedScreen) {
|
const { components: definitions } = $componentStore
|
||||||
// Skip validation if a screen is not selected.
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getInvalidDatasources($selectedScreen, datasources)
|
const errors = {
|
||||||
|
...getInvalidDatasources($selectedScreen, datasources, definitions),
|
||||||
|
...getMissingAncestors($selectedScreen, definitions),
|
||||||
|
...getMissingRequiredSettings($selectedScreen, definitions),
|
||||||
|
}
|
||||||
|
return errors
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
function findComponentsBySettingsType(
|
function getInvalidDatasources(
|
||||||
|
screen: Screen,
|
||||||
|
datasources: Record<string, any>,
|
||||||
|
definitions: Record<string, ComponentDefinition>
|
||||||
|
) {
|
||||||
|
const result: Record<string, UIComponentError[]> = {}
|
||||||
|
for (const { component, setting } of findComponentsBySettingsType(
|
||||||
|
screen,
|
||||||
|
["table", "dataSource"],
|
||||||
|
definitions
|
||||||
|
)) {
|
||||||
|
const componentSettings = component[setting.key]
|
||||||
|
if (!componentSettings) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const { label } = componentSettings
|
||||||
|
const type = componentSettings.type as UIDatasourceType
|
||||||
|
|
||||||
|
const validationKey = validationKeyByType[type]
|
||||||
|
if (!validationKey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentBindings = getBindableProperties(screen, component._id)
|
||||||
|
|
||||||
|
const componentDatasources = {
|
||||||
|
...reduceBy("rowId", bindings.extractRelationships(componentBindings)),
|
||||||
|
...reduceBy("value", bindings.extractFields(componentBindings)),
|
||||||
|
...reduceBy("value", bindings.extractJSONArrayFields(componentBindings)),
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceId = componentSettings[validationKey]
|
||||||
|
if (!{ ...datasources, ...componentDatasources }[resourceId]) {
|
||||||
|
const friendlyTypeName = friendlyNameByType[type] ?? type
|
||||||
|
result[component._id!] = [
|
||||||
|
{
|
||||||
|
key: setting.key,
|
||||||
|
message: `The ${friendlyTypeName} named "${label}" could not be found`,
|
||||||
|
errorType: "setting",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMissingRequiredSettings(
|
||||||
|
screen: Screen,
|
||||||
|
definitions: Record<string, ComponentDefinition>
|
||||||
|
) {
|
||||||
|
const allComponents = findAllComponents(screen.props) as Component[]
|
||||||
|
|
||||||
|
const result: Record<string, UIComponentError[]> = {}
|
||||||
|
for (const component of allComponents) {
|
||||||
|
const definition = definitions[component._component]
|
||||||
|
|
||||||
|
const settings = getSettingsDefinition(definition)
|
||||||
|
|
||||||
|
const missingRequiredSettings = settings.filter((setting: any) => {
|
||||||
|
let empty =
|
||||||
|
component[setting.key] == null || component[setting.key] === ""
|
||||||
|
let missing = setting.required && empty
|
||||||
|
|
||||||
|
// Check if this setting depends on another, as it may not be required
|
||||||
|
if (setting.dependsOn) {
|
||||||
|
const dependsOnKey = setting.dependsOn.setting || setting.dependsOn
|
||||||
|
const dependsOnValue = setting.dependsOn.value
|
||||||
|
const realDependentValue = component[dependsOnKey]
|
||||||
|
|
||||||
|
const sectionDependsOnKey =
|
||||||
|
setting.sectionDependsOn?.setting || setting.sectionDependsOn
|
||||||
|
const sectionDependsOnValue = setting.sectionDependsOn?.value
|
||||||
|
const sectionRealDependentValue = component[sectionDependsOnKey]
|
||||||
|
|
||||||
|
if (dependsOnValue == null && realDependentValue == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (dependsOnValue != null && dependsOnValue !== realDependentValue) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
sectionDependsOnValue != null &&
|
||||||
|
sectionDependsOnValue !== sectionRealDependentValue
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return missing
|
||||||
|
})
|
||||||
|
|
||||||
|
if (missingRequiredSettings?.length) {
|
||||||
|
result[component._id!] = missingRequiredSettings.map((s: any) => ({
|
||||||
|
key: s.key,
|
||||||
|
message: `Add the <mark>${s.label}</mark> setting to start using your component`,
|
||||||
|
errorType: "setting",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudibasePrefix = "@budibase/standard-components/"
|
||||||
|
function getMissingAncestors(
|
||||||
|
screen: Screen,
|
||||||
|
definitions: Record<string, ComponentDefinition>
|
||||||
|
) {
|
||||||
|
const result: Record<string, UIComponentError[]> = {}
|
||||||
|
|
||||||
|
function checkMissingAncestors(component: Component, ancestors: string[]) {
|
||||||
|
for (const child of component._children || []) {
|
||||||
|
checkMissingAncestors(child, [...ancestors, component._component])
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = definitions[component._component]
|
||||||
|
|
||||||
|
if (!definition?.requiredAncestors?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingAncestors = definition.requiredAncestors.filter(
|
||||||
|
ancestor => !ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (missingAncestors.length) {
|
||||||
|
const pluralise = (name: string) => {
|
||||||
|
return name.endsWith("s") ? `${name}'` : `${name}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
result[component._id!] = missingAncestors.map(ancestor => {
|
||||||
|
const ancestorDefinition = definitions[`${BudibasePrefix}${ancestor}`]
|
||||||
|
return {
|
||||||
|
message: `${pluralise(definition.name)} need to be inside a
|
||||||
|
<mark>${ancestorDefinition.name}</mark>`,
|
||||||
|
errorType: "ancestor-setting",
|
||||||
|
ancestor: {
|
||||||
|
name: ancestorDefinition.name,
|
||||||
|
fullType: `${BudibasePrefix}${ancestor}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMissingAncestors(screen.props, [])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findComponentsBySettingsType(
|
||||||
screen: Screen,
|
screen: Screen,
|
||||||
type: string | string[],
|
type: string | string[],
|
||||||
definitions: Record<string, ComponentDefinition>
|
definitions: Record<string, ComponentDefinition>
|
||||||
|
@ -149,10 +255,10 @@ function findComponentsBySettingsType(
|
||||||
const setting = definition?.settings?.find((s: any) =>
|
const setting = definition?.settings?.find((s: any) =>
|
||||||
typesArray.includes(s.type)
|
typesArray.includes(s.type)
|
||||||
)
|
)
|
||||||
if (setting && "type" in setting) {
|
if (setting) {
|
||||||
result.push({
|
result.push({
|
||||||
component,
|
component,
|
||||||
setting: { type: setting.type!, key: setting.key! },
|
setting: { type: setting.type, key: setting.key },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
component._children?.forEach(child => {
|
component._children?.forEach(child => {
|
||||||
|
|
|
@ -19,8 +19,8 @@ import {
|
||||||
Screen,
|
Screen,
|
||||||
Component,
|
Component,
|
||||||
SaveScreenResponse,
|
SaveScreenResponse,
|
||||||
|
ComponentDefinition,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { ComponentDefinition } from "./components"
|
|
||||||
|
|
||||||
interface ScreenState {
|
interface ScreenState {
|
||||||
screens: Screen[]
|
screens: Screen[]
|
||||||
|
@ -504,8 +504,8 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
||||||
/**
|
/**
|
||||||
* Provides a list of screens that are used by a given source ID (table, view, datasource, query)
|
* Provides a list of screens that are used by a given source ID (table, view, datasource, query)
|
||||||
*/
|
*/
|
||||||
async usageOfScreens(sourceId: string) {
|
async usageInScreens(sourceId: string) {
|
||||||
return API.usageOfScreens(sourceId)
|
return API.usageInScreens(sourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext, onMount } from "svelte"
|
import { getContext, setContext, onMount } from "svelte"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import {
|
import { enrichProps, propsAreSame } from "utils/componentProps"
|
||||||
enrichProps,
|
import { getSettingsDefinition } from "@budibase/frontend-core"
|
||||||
propsAreSame,
|
|
||||||
getSettingsDefinition,
|
|
||||||
} from "utils/componentProps"
|
|
||||||
import {
|
import {
|
||||||
builderStore,
|
builderStore,
|
||||||
devToolsStore,
|
devToolsStore,
|
||||||
|
@ -29,7 +26,6 @@
|
||||||
import EmptyPlaceholder from "components/app/EmptyPlaceholder.svelte"
|
import EmptyPlaceholder from "components/app/EmptyPlaceholder.svelte"
|
||||||
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
||||||
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
||||||
import { BudibasePrefix } from "../stores/components.js"
|
|
||||||
import {
|
import {
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
findHBSBlocks,
|
findHBSBlocks,
|
||||||
|
@ -102,8 +98,6 @@
|
||||||
let definition
|
let definition
|
||||||
let settingsDefinition
|
let settingsDefinition
|
||||||
let settingsDefinitionMap
|
let settingsDefinitionMap
|
||||||
let missingRequiredSettings = false
|
|
||||||
let componentErrors = false
|
|
||||||
|
|
||||||
// Temporary styles which can be added in the app preview for things like
|
// Temporary styles which can be added in the app preview for things like
|
||||||
// DND. We clear these whenever a new instance is received.
|
// DND. We clear these whenever a new instance is received.
|
||||||
|
@ -141,18 +135,11 @@
|
||||||
$: componentErrors = instance?._meta?.errors
|
$: componentErrors = instance?._meta?.errors
|
||||||
$: hasChildren = !!definition?.hasChildren
|
$: hasChildren = !!definition?.hasChildren
|
||||||
$: showEmptyState = definition?.showEmptyState !== false
|
$: showEmptyState = definition?.showEmptyState !== false
|
||||||
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
|
$: hasMissingRequiredSettings = !!componentErrors?.find(
|
||||||
|
e => e.errorType === "setting"
|
||||||
|
)
|
||||||
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
||||||
$: hasComponentErrors = componentErrors?.length > 0
|
$: hasComponentErrors = componentErrors?.length > 0
|
||||||
$: requiredAncestors = definition?.requiredAncestors || []
|
|
||||||
$: missingRequiredAncestors = requiredAncestors.filter(
|
|
||||||
ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`)
|
|
||||||
)
|
|
||||||
$: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0
|
|
||||||
$: errorState =
|
|
||||||
hasMissingRequiredSettings ||
|
|
||||||
hasMissingRequiredAncestors ||
|
|
||||||
hasComponentErrors
|
|
||||||
|
|
||||||
// Interactive components can be selected, dragged and highlighted inside
|
// Interactive components can be selected, dragged and highlighted inside
|
||||||
// the builder preview
|
// the builder preview
|
||||||
|
@ -218,7 +205,7 @@
|
||||||
styles: normalStyles,
|
styles: normalStyles,
|
||||||
draggable,
|
draggable,
|
||||||
definition,
|
definition,
|
||||||
errored: errorState,
|
errored: hasComponentErrors,
|
||||||
}
|
}
|
||||||
|
|
||||||
// When dragging and dropping, pad components to allow dropping between
|
// When dragging and dropping, pad components to allow dropping between
|
||||||
|
@ -251,9 +238,8 @@
|
||||||
name,
|
name,
|
||||||
editing,
|
editing,
|
||||||
type: instance._component,
|
type: instance._component,
|
||||||
errorState,
|
errorState: hasComponentErrors,
|
||||||
parent: id,
|
parent: id,
|
||||||
ancestors: [...($component?.ancestors ?? []), instance._component],
|
|
||||||
path: [...($component?.path ?? []), id],
|
path: [...($component?.path ?? []), id],
|
||||||
darkMode,
|
darkMode,
|
||||||
})
|
})
|
||||||
|
@ -310,40 +296,6 @@
|
||||||
staticSettings = instanceSettings.staticSettings
|
staticSettings = instanceSettings.staticSettings
|
||||||
dynamicSettings = instanceSettings.dynamicSettings
|
dynamicSettings = instanceSettings.dynamicSettings
|
||||||
|
|
||||||
// Check if we have any missing required settings
|
|
||||||
missingRequiredSettings = settingsDefinition.filter(setting => {
|
|
||||||
let empty = instance[setting.key] == null || instance[setting.key] === ""
|
|
||||||
let missing = setting.required && empty
|
|
||||||
|
|
||||||
// Check if this setting depends on another, as it may not be required
|
|
||||||
if (setting.dependsOn) {
|
|
||||||
const dependsOnKey = setting.dependsOn.setting || setting.dependsOn
|
|
||||||
const dependsOnValue = setting.dependsOn.value
|
|
||||||
const realDependentValue = instance[dependsOnKey]
|
|
||||||
|
|
||||||
const sectionDependsOnKey =
|
|
||||||
setting.sectionDependsOn?.setting || setting.sectionDependsOn
|
|
||||||
const sectionDependsOnValue = setting.sectionDependsOn?.value
|
|
||||||
const sectionRealDependentValue = instance[sectionDependsOnKey]
|
|
||||||
|
|
||||||
if (dependsOnValue == null && realDependentValue == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (dependsOnValue != null && dependsOnValue !== realDependentValue) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
sectionDependsOnValue != null &&
|
|
||||||
sectionDependsOnValue !== sectionRealDependentValue
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return missing
|
|
||||||
})
|
|
||||||
|
|
||||||
// When considering bindings we can ignore children, so we remove that
|
// When considering bindings we can ignore children, so we remove that
|
||||||
// before storing the reference stringified version
|
// before storing the reference stringified version
|
||||||
const noChildren = JSON.stringify({ ...instance, _children: null })
|
const noChildren = JSON.stringify({ ...instance, _children: null })
|
||||||
|
@ -686,7 +638,7 @@
|
||||||
class:pad
|
class:pad
|
||||||
class:parent={hasChildren}
|
class:parent={hasChildren}
|
||||||
class:block={isBlock}
|
class:block={isBlock}
|
||||||
class:error={errorState}
|
class:error={hasComponentErrors}
|
||||||
class:root={isRoot}
|
class:root={isRoot}
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
|
@ -694,12 +646,8 @@
|
||||||
data-parent={$component.id}
|
data-parent={$component.id}
|
||||||
use:gridLayout={gridMetadata}
|
use:gridLayout={gridMetadata}
|
||||||
>
|
>
|
||||||
{#if errorState}
|
{#if hasComponentErrors}
|
||||||
<ComponentErrorState
|
<ComponentErrorState {componentErrors} />
|
||||||
{missingRequiredSettings}
|
|
||||||
{missingRequiredAncestors}
|
|
||||||
{componentErrors}
|
|
||||||
/>
|
|
||||||
{:else}
|
{:else}
|
||||||
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
||||||
{#if children.length}
|
{#if children.length}
|
||||||
|
|
|
@ -5,6 +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"
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
let gridContext
|
let gridContext
|
||||||
let minHeight = 0
|
let minHeight = 0
|
||||||
|
|
||||||
|
$: id = $component.id
|
||||||
$: currentTheme = $context?.device?.theme
|
$: currentTheme = $context?.device?.theme
|
||||||
$: darkMode = !currentTheme?.includes("light")
|
$: darkMode = !currentTheme?.includes("light")
|
||||||
$: parsedColumns = getParsedColumns(columns)
|
$: parsedColumns = getParsedColumns(columns)
|
||||||
|
@ -65,7 +67,6 @@
|
||||||
const clean = gridContext?.rows.actions.cleanRow || (x => x)
|
const clean = gridContext?.rows.actions.cleanRow || (x => x)
|
||||||
const cleaned = rows.map(clean)
|
const cleaned = rows.map(clean)
|
||||||
const goldenRow = generateGoldenSample(cleaned)
|
const goldenRow = generateGoldenSample(cleaned)
|
||||||
const id = get(component).id
|
|
||||||
return {
|
return {
|
||||||
// Not sure what this one is for...
|
// Not sure what this one is for...
|
||||||
[id]: goldenRow,
|
[id]: goldenRow,
|
||||||
|
@ -104,6 +105,7 @@
|
||||||
order: idx,
|
order: idx,
|
||||||
conditions: column.conditions,
|
conditions: column.conditions,
|
||||||
visible: !!column.active,
|
visible: !!column.active,
|
||||||
|
// format: createFormatter(column),
|
||||||
}
|
}
|
||||||
if (column.width) {
|
if (column.width) {
|
||||||
overrides[column.field].width = column.width
|
overrides[column.field].width = column.width
|
||||||
|
@ -112,6 +114,13 @@
|
||||||
return overrides
|
return overrides
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const createFormatter = column => {
|
||||||
|
// if (typeof column.format !== "string" || !column.format.trim().length) {
|
||||||
|
// return null
|
||||||
|
// }
|
||||||
|
// return row => processStringSync(column.format, { [id]: row })
|
||||||
|
// }
|
||||||
|
|
||||||
const enrichButtons = buttons => {
|
const enrichButtons = buttons => {
|
||||||
if (!buttons?.length) {
|
if (!buttons?.length) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Toggle } from "@budibase/bbui"
|
import { Layout, Toggle } from "@budibase/bbui"
|
||||||
|
import { getSettingsDefinition } from "@budibase/frontend-core"
|
||||||
import DevToolsStat from "./DevToolsStat.svelte"
|
import DevToolsStat from "./DevToolsStat.svelte"
|
||||||
import { componentStore } from "stores/index.js"
|
import { componentStore } from "stores/index.js"
|
||||||
import { getSettingsDefinition } from "utils/componentProps.js"
|
|
||||||
|
|
||||||
let showEnrichedSettings = true
|
let showEnrichedSettings = true
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import MissingRequiredSetting from "./MissingRequiredSetting.svelte"
|
import { UIComponentError } from "@budibase/types"
|
||||||
import MissingRequiredAncestor from "./MissingRequiredAncestor.svelte"
|
import ComponentErrorStateCta from "./ComponentErrorStateCTA.svelte"
|
||||||
|
|
||||||
export let missingRequiredSettings:
|
export let componentErrors: UIComponentError[] | undefined
|
||||||
| { key: string; label: string }[]
|
|
||||||
| undefined
|
|
||||||
export let missingRequiredAncestors: string[] | undefined
|
|
||||||
export let componentErrors: string[] | undefined
|
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, builderStore } = getContext("sdk")
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
|
|
||||||
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
|
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
|
||||||
$: requiredSetting = missingRequiredSettings?.[0]
|
|
||||||
$: requiredAncestor = missingRequiredAncestors?.[0]
|
|
||||||
$: errorMessage = componentErrors?.[0]
|
$: errorMessage = componentErrors?.[0]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -23,12 +17,10 @@
|
||||||
{#if $component.errorState}
|
{#if $component.errorState}
|
||||||
<div class="component-placeholder" use:styleable={styles}>
|
<div class="component-placeholder" use:styleable={styles}>
|
||||||
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
|
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
|
||||||
{#if requiredAncestor}
|
{#if errorMessage}
|
||||||
<MissingRequiredAncestor {requiredAncestor} />
|
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||||
{:else if errorMessage}
|
{@html errorMessage.message}
|
||||||
{errorMessage}
|
<ComponentErrorStateCta error={errorMessage} />
|
||||||
{:else if requiredSetting}
|
|
||||||
<MissingRequiredSetting {requiredSetting} />
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { UIComponentError } from "@budibase/types"
|
||||||
|
|
||||||
|
export let error: UIComponentError | undefined
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { builderStore } = getContext("sdk")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
{#if error.errorType === "setting"}
|
||||||
|
<span>-</span>
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<span
|
||||||
|
class="spectrum-Link"
|
||||||
|
on:click={() => {
|
||||||
|
builderStore.actions.highlightSetting(error.key)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show me
|
||||||
|
</span>
|
||||||
|
{:else if error.errorType === "ancestor-setting"}
|
||||||
|
<span>-</span>
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<span
|
||||||
|
class="spectrum-Link"
|
||||||
|
on:click={() => {
|
||||||
|
builderStore.actions.addParentComponent(
|
||||||
|
$component.id,
|
||||||
|
error.ancestor.fullType
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add {error.ancestor.name}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
|
@ -1,43 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { BudibasePrefix } from "stores/components"
|
|
||||||
|
|
||||||
export let requiredAncestor
|
|
||||||
|
|
||||||
const component = getContext("component")
|
|
||||||
const { builderStore, componentStore } = getContext("sdk")
|
|
||||||
|
|
||||||
$: definition = componentStore.actions.getComponentDefinition($component.type)
|
|
||||||
$: fullAncestorType = `${BudibasePrefix}${requiredAncestor}`
|
|
||||||
$: ancestorDefinition =
|
|
||||||
componentStore.actions.getComponentDefinition(fullAncestorType)
|
|
||||||
$: pluralName = getPluralName(definition?.name, $component.type)
|
|
||||||
$: ancestorName = getAncestorName(ancestorDefinition?.name, requiredAncestor)
|
|
||||||
|
|
||||||
const getPluralName = (name, type) => {
|
|
||||||
if (!name) {
|
|
||||||
name = type.replace(BudibasePrefix, "")
|
|
||||||
}
|
|
||||||
return name.endsWith("s") ? `${name}'` : `${name}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAncestorName = name => {
|
|
||||||
return name || requiredAncestor
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{pluralName} need to be inside a
|
|
||||||
<mark>{ancestorName}</mark>
|
|
||||||
</span>
|
|
||||||
<span>-</span>
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<span
|
|
||||||
class="spectrum-Link"
|
|
||||||
on:click={() => {
|
|
||||||
builderStore.actions.addParentComponent($component.id, fullAncestorType)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add {ancestorName}
|
|
||||||
</span>
|
|
|
@ -1,22 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
export let requiredSetting
|
|
||||||
|
|
||||||
const { builderStore } = getContext("sdk")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
Add the <mark>{requiredSetting.label}</mark> setting to start using your component
|
|
||||||
</span>
|
|
||||||
<span>-</span>
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<span
|
|
||||||
class="spectrum-Link"
|
|
||||||
on:click={() => {
|
|
||||||
builderStore.actions.highlightSetting(requiredSetting.key)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Show me
|
|
||||||
</span>
|
|
|
@ -15,6 +15,7 @@ export const ActionTypes = {
|
||||||
|
|
||||||
export const DNDPlaceholderID = "dnd-placeholder"
|
export const DNDPlaceholderID = "dnd-placeholder"
|
||||||
export const ScreenslotType = "screenslot"
|
export const ScreenslotType = "screenslot"
|
||||||
|
export const ScreenslotID = "screenslot"
|
||||||
export const GridRowHeight = 24
|
export const GridRowHeight = 24
|
||||||
export const GridColumns = 12
|
export const GridColumns = 12
|
||||||
export const GridSpacing = 4
|
export const GridSpacing = 4
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
dndStore,
|
dndStore,
|
||||||
eventStore,
|
eventStore,
|
||||||
hoverStore,
|
hoverStore,
|
||||||
|
stateStore,
|
||||||
} from "./stores"
|
} from "./stores"
|
||||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
@ -87,8 +88,10 @@ const loadBudibase = async () => {
|
||||||
dndStore.actions.reset()
|
dndStore.actions.reset()
|
||||||
}
|
}
|
||||||
} else if (type === "request-context") {
|
} else if (type === "request-context") {
|
||||||
const { selectedComponentInstance } = get(componentStore)
|
const { selectedComponentInstance, screenslotInstance } =
|
||||||
const context = selectedComponentInstance?.getDataContext()
|
get(componentStore)
|
||||||
|
const instance = selectedComponentInstance || screenslotInstance
|
||||||
|
const context = instance?.getDataContext()
|
||||||
let stringifiedContext = null
|
let stringifiedContext = null
|
||||||
try {
|
try {
|
||||||
stringifiedContext = JSON.stringify(context)
|
stringifiedContext = JSON.stringify(context)
|
||||||
|
@ -102,6 +105,9 @@ const loadBudibase = async () => {
|
||||||
hoverStore.actions.hoverComponent(data, false)
|
hoverStore.actions.hoverComponent(data, false)
|
||||||
} else if (type === "builder-meta") {
|
} else if (type === "builder-meta") {
|
||||||
builderStore.actions.setMetadata(data)
|
builderStore.actions.setMetadata(data)
|
||||||
|
} else if (type === "builder-state") {
|
||||||
|
const [[key, value]] = Object.entries(data)
|
||||||
|
stateStore.actions.setValue(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,15 @@ export interface SDK {
|
||||||
generateGoldenSample: any
|
generateGoldenSample: any
|
||||||
builderStore: Readable<{
|
builderStore: Readable<{
|
||||||
inBuilder: boolean
|
inBuilder: boolean
|
||||||
}>
|
}> & {
|
||||||
|
actions: {
|
||||||
|
highlightSetting: (key: string) => void
|
||||||
|
addParentComponent: (
|
||||||
|
componentId: string,
|
||||||
|
fullAncestorType: string
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Component = Readable<{
|
export type Component = Readable<{
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { screenStore } from "./screens"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
import Router from "../components/Router.svelte"
|
import Router from "../components/Router.svelte"
|
||||||
import * as AppComponents from "../components/app/index.js"
|
import * as AppComponents from "../components/app/index.js"
|
||||||
import { ScreenslotType } from "../constants"
|
import { ScreenslotID, ScreenslotType } from "../constants"
|
||||||
|
|
||||||
export const BudibasePrefix = "@budibase/standard-components/"
|
export const BudibasePrefix = "@budibase/standard-components/"
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ const createComponentStore = () => {
|
||||||
selectedComponentDefinition: definition,
|
selectedComponentDefinition: definition,
|
||||||
selectedComponentPath: selectedPath?.map(component => component._id),
|
selectedComponentPath: selectedPath?.map(component => component._id),
|
||||||
mountedComponentCount: Object.keys($store.mountedComponents).length,
|
mountedComponentCount: Object.keys($store.mountedComponents).length,
|
||||||
|
screenslotInstance: $store.mountedComponents[ScreenslotID],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { findComponentById, findComponentParent } from "../utils/components.js"
|
import { findComponentById, findComponentParent } from "../utils/components.js"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { DNDPlaceholderID } from "constants"
|
import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "constants"
|
||||||
|
|
||||||
const createScreenStore = () => {
|
const createScreenStore = () => {
|
||||||
const store = derived(
|
const store = derived(
|
||||||
|
@ -171,8 +171,8 @@ const createScreenStore = () => {
|
||||||
_component: "@budibase/standard-components/layout",
|
_component: "@budibase/standard-components/layout",
|
||||||
_children: [
|
_children: [
|
||||||
{
|
{
|
||||||
_component: "screenslot",
|
_component: ScreenslotType,
|
||||||
_id: "screenslot",
|
_id: ScreenslotID,
|
||||||
_styles: {
|
_styles: {
|
||||||
normal: {
|
normal: {
|
||||||
flex: "1 1 auto",
|
flex: "1 1 auto",
|
||||||
|
|
|
@ -97,26 +97,3 @@ export const propsUseBinding = (props, bindingKey) => {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the definition of this component's settings from the manifest
|
|
||||||
*/
|
|
||||||
export const getSettingsDefinition = definition => {
|
|
||||||
if (!definition) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let settings = []
|
|
||||||
definition.settings?.forEach(setting => {
|
|
||||||
if (setting.section) {
|
|
||||||
settings = settings.concat(
|
|
||||||
(setting.settings || [])?.map(childSetting => ({
|
|
||||||
...childSetting,
|
|
||||||
sectionDependsOn: setting.dependsOn,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
settings.push(setting)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,14 +2,14 @@ import {
|
||||||
DeleteScreenResponse,
|
DeleteScreenResponse,
|
||||||
SaveScreenRequest,
|
SaveScreenRequest,
|
||||||
SaveScreenResponse,
|
SaveScreenResponse,
|
||||||
UsageOfScreensResponse,
|
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>
|
||||||
usageOfScreens: (sourceId: string) => Promise<UsageOfScreensResponse>
|
usageInScreens: (sourceId: string) => Promise<UsageInScreensResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
||||||
|
@ -35,7 +35,7 @@ export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
usageOfScreens: async sourceId => {
|
usageInScreens: async sourceId => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/screens/usage/${sourceId}`,
|
url: `/api/screens/usage/${sourceId}`,
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import GridCell from "./GridCell.svelte"
|
import GridCell from "./GridCell.svelte"
|
||||||
import { getCellRenderer } from "../lib/renderers"
|
import { getCellRenderer } from "../lib/renderers"
|
||||||
import { derived, writable } from "svelte/store"
|
import { derived, writable } from "svelte/store"
|
||||||
|
import TextCell from "./TextCell.svelte"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rows,
|
rows,
|
||||||
|
@ -36,11 +37,17 @@
|
||||||
|
|
||||||
let api
|
let api
|
||||||
|
|
||||||
|
// Get the appropriate cell renderer and value
|
||||||
|
$: hasCustomFormat = column.format && !row._isNewRow
|
||||||
|
$: renderer = hasCustomFormat ? TextCell : getCellRenderer(column)
|
||||||
|
$: value = hasCustomFormat ? row.__formatted?.[column.name] : row[column.name]
|
||||||
|
|
||||||
// Get the error for this cell if the cell is focused or selected
|
// Get the error for this cell if the cell is focused or selected
|
||||||
$: error = getErrorStore(rowFocused, cellId)
|
$: error = getErrorStore(rowFocused, cellId)
|
||||||
|
|
||||||
// Determine if the cell is editable
|
// Determine if the cell is editable
|
||||||
$: readonly =
|
$: readonly =
|
||||||
|
hasCustomFormat ||
|
||||||
columns.actions.isReadonly(column) ||
|
columns.actions.isReadonly(column) ||
|
||||||
(!$config.canEditRows && !row._isNewRow)
|
(!$config.canEditRows && !row._isNewRow)
|
||||||
|
|
||||||
|
@ -69,7 +76,7 @@
|
||||||
onKeyDown: (...params) => api?.onKeyDown?.(...params),
|
onKeyDown: (...params) => api?.onKeyDown?.(...params),
|
||||||
isReadonly: () => readonly,
|
isReadonly: () => readonly,
|
||||||
getType: () => column.schema.type,
|
getType: () => column.schema.type,
|
||||||
getValue: () => row[column.name],
|
getValue: () => value,
|
||||||
setValue: (value, options = { apply: true }) => {
|
setValue: (value, options = { apply: true }) => {
|
||||||
validation.actions.setError(cellId, null)
|
validation.actions.setError(cellId, null)
|
||||||
updateValue({
|
updateValue({
|
||||||
|
@ -136,9 +143,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={getCellRenderer(column)}
|
this={renderer}
|
||||||
bind:api
|
bind:api
|
||||||
value={row[column.name]}
|
{value}
|
||||||
schema={column.schema}
|
schema={column.schema}
|
||||||
onChange={cellAPI.setValue}
|
onChange={cellAPI.setValue}
|
||||||
{focused}
|
{focused}
|
||||||
|
|
|
@ -53,7 +53,6 @@ export const getCellRenderer = (column: UIColumn) => {
|
||||||
if (column.calculationType) {
|
if (column.calculationType) {
|
||||||
return NumberCell
|
return NumberCell
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
getCellRendererByType(column.schema?.cellRenderType) ||
|
getCellRendererByType(column.schema?.cellRenderType) ||
|
||||||
getCellRendererByType(column.schema?.type) ||
|
getCellRendererByType(column.schema?.type) ||
|
||||||
|
|
|
@ -188,6 +188,7 @@ export const initialise = (context: StoreContext) => {
|
||||||
conditions: fieldSchema.conditions,
|
conditions: fieldSchema.conditions,
|
||||||
related: fieldSchema.related,
|
related: fieldSchema.related,
|
||||||
calculationType: fieldSchema.calculationType,
|
calculationType: fieldSchema.calculationType,
|
||||||
|
format: fieldSchema.format,
|
||||||
__left: undefined as any, // TODO
|
__left: undefined as any, // TODO
|
||||||
__idx: undefined as any, // TODO
|
__idx: undefined as any, // TODO
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { Store as StoreContext } from "."
|
||||||
|
|
||||||
interface IndexedUIRow extends UIRow {
|
interface IndexedUIRow extends UIRow {
|
||||||
__idx: number
|
__idx: number
|
||||||
|
__formatted: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RowStore {
|
interface RowStore {
|
||||||
|
@ -114,17 +115,19 @@ export const createStores = (): RowStore => {
|
||||||
export const deriveStores = (context: StoreContext): RowDerivedStore => {
|
export const deriveStores = (context: StoreContext): RowDerivedStore => {
|
||||||
const { rows, enrichedSchema } = context
|
const { rows, enrichedSchema } = context
|
||||||
|
|
||||||
// Enrich rows with an index property and any pending changes
|
// Enrich rows with an index property and additional values
|
||||||
const enrichedRows = derived(
|
const enrichedRows = derived(
|
||||||
[rows, enrichedSchema],
|
[rows, enrichedSchema],
|
||||||
([$rows, $enrichedSchema]) => {
|
([$rows, $enrichedSchema]) => {
|
||||||
const customColumns = Object.values($enrichedSchema || {}).filter(
|
// Find columns which require additional processing
|
||||||
f => f.related
|
const cols = Object.values($enrichedSchema || {})
|
||||||
)
|
const relatedColumns = cols.filter(col => col.related)
|
||||||
return $rows.map<IndexedUIRow>((row, idx) => ({
|
const formattedColumns = cols.filter(col => col.format)
|
||||||
...row,
|
|
||||||
__idx: idx,
|
return $rows.map<IndexedUIRow>((row, idx) => {
|
||||||
...customColumns.reduce<Record<string, string>>((map, column) => {
|
// Derive any values that need enriched from related rows
|
||||||
|
const relatedValues = relatedColumns.reduce<Record<string, string>>(
|
||||||
|
(map, column) => {
|
||||||
const fromField = $enrichedSchema![column.related!.field]
|
const fromField = $enrichedSchema![column.related!.field]
|
||||||
map[column.name] = getRelatedTableValues(
|
map[column.name] = getRelatedTableValues(
|
||||||
row,
|
row,
|
||||||
|
@ -132,8 +135,24 @@ export const deriveStores = (context: StoreContext): RowDerivedStore => {
|
||||||
fromField
|
fromField
|
||||||
)
|
)
|
||||||
return map
|
return map
|
||||||
}, {}),
|
},
|
||||||
}))
|
{}
|
||||||
|
)
|
||||||
|
// Derive any display-only formatted values for this row
|
||||||
|
const formattedValues = formattedColumns.reduce<Record<string, any>>(
|
||||||
|
(map, column) => {
|
||||||
|
map[column.name] = column.format!(row)
|
||||||
|
return map
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
...relatedValues,
|
||||||
|
__formatted: formattedValues,
|
||||||
|
__idx: idx,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -791,6 +810,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
|
||||||
let clone: Row = { ...row }
|
let clone: Row = { ...row }
|
||||||
delete clone.__idx
|
delete clone.__idx
|
||||||
delete clone.__metadata
|
delete clone.__metadata
|
||||||
|
delete clone.__formatted
|
||||||
if (!get(hasBudibaseIdentifiers) && isGeneratedRowID(clone._id!)) {
|
if (!get(hasBudibaseIdentifiers) && isGeneratedRowID(clone._id!)) {
|
||||||
delete clone._id
|
delete clone._id
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { ComponentDefinition, ComponentSetting } from "@budibase/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the definition of this component's settings from the manifest
|
||||||
|
*/
|
||||||
|
export const getSettingsDefinition = (
|
||||||
|
definition: ComponentDefinition
|
||||||
|
): ComponentSetting[] => {
|
||||||
|
if (!definition) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let settings: ComponentSetting[] = []
|
||||||
|
definition.settings?.forEach(setting => {
|
||||||
|
if (setting.section) {
|
||||||
|
settings = settings.concat(
|
||||||
|
(setting.settings || [])?.map(childSetting => ({
|
||||||
|
...childSetting,
|
||||||
|
sectionDependsOn: setting.dependsOn,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
settings.push(setting)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return settings
|
||||||
|
}
|
|
@ -13,3 +13,4 @@ export * from "./download"
|
||||||
export * from "./settings"
|
export * from "./settings"
|
||||||
export * from "./relatedColumns"
|
export * from "./relatedColumns"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
|
export * from "./components"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 43a5785ccb4f83ce929b29f05ea0a62199fcdf23
|
Subproject commit 8cbaa80a9cc1152c6cd53722e64da7d824da6e16
|
|
@ -1,23 +1,23 @@
|
||||||
import { 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,
|
||||||
UsageOfScreensResponse,
|
ScreenProps,
|
||||||
ScreenUsage,
|
ScreenUsage,
|
||||||
|
UsageInScreensResponse,
|
||||||
|
UserCtx,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { builderSocket } from "../../websockets"
|
import { builderSocket } from "../../websockets"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
@ -137,7 +137,7 @@ 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, UsageOfScreensResponse>) {
|
export async function usage(ctx: UserCtx<void, UsageInScreensResponse>) {
|
||||||
const sourceId = ctx.params.sourceId
|
const sourceId = ctx.params.sourceId
|
||||||
const sourceType = sdk.common.getSourceType(sourceId)
|
const sourceType = sdk.common.getSourceType(sourceId)
|
||||||
const allScreens = await sdk.screens.fetch()
|
const allScreens = await sdk.screens.fetch()
|
||||||
|
|
|
@ -107,11 +107,8 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should ensure you can't have a branch as not a last step", async () => {
|
it("Should ensure you can't have a branch as not a last step", async () => {
|
||||||
const automation = createAutomationBuilder({
|
const automation = createAutomationBuilder(config)
|
||||||
name: "String Equality Branching",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
})
|
|
||||||
.appAction({ fields: { status: "active" } })
|
|
||||||
.branch({
|
.branch({
|
||||||
activeBranch: {
|
activeBranch: {
|
||||||
steps: stepBuilder =>
|
steps: stepBuilder =>
|
||||||
|
@ -134,11 +131,8 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should check validation on an automation that has a branch step with no children", async () => {
|
it("Should check validation on an automation that has a branch step with no children", async () => {
|
||||||
const automation = createAutomationBuilder({
|
const automation = createAutomationBuilder(config)
|
||||||
name: "String Equality Branching",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
})
|
|
||||||
.appAction({ fields: { status: "active" } })
|
|
||||||
.branch({})
|
.branch({})
|
||||||
.serverLog({ text: "Inactive user" })
|
.serverLog({ text: "Inactive user" })
|
||||||
.build()
|
.build()
|
||||||
|
@ -153,11 +147,8 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should check validation on a branch step with empty conditions", async () => {
|
it("Should check validation on a branch step with empty conditions", async () => {
|
||||||
const automation = createAutomationBuilder({
|
const automation = createAutomationBuilder(config)
|
||||||
name: "String Equality Branching",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
})
|
|
||||||
.appAction({ fields: { status: "active" } })
|
|
||||||
.branch({
|
.branch({
|
||||||
activeBranch: {
|
activeBranch: {
|
||||||
steps: stepBuilder =>
|
steps: stepBuilder =>
|
||||||
|
@ -177,11 +168,8 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should check validation on an branch that has a condition that is not valid", async () => {
|
it("Should check validation on an branch that has a condition that is not valid", async () => {
|
||||||
const automation = createAutomationBuilder({
|
const automation = createAutomationBuilder(config)
|
||||||
name: "String Equality Branching",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
})
|
|
||||||
.appAction({ fields: { status: "active" } })
|
|
||||||
.branch({
|
.branch({
|
||||||
activeBranch: {
|
activeBranch: {
|
||||||
steps: stepBuilder =>
|
steps: stepBuilder =>
|
||||||
|
@ -252,12 +240,8 @@ describe("/automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to access platformUrl, logoUrl and company in the automation", async () => {
|
it("should be able to access platformUrl, logoUrl and company in the automation", async () => {
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Test Automation",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.serverLog({
|
.serverLog({
|
||||||
text: "{{ settings.url }}",
|
text: "{{ settings.url }}",
|
||||||
})
|
})
|
||||||
|
@ -267,7 +251,7 @@ describe("/automations", () => {
|
||||||
.serverLog({
|
.serverLog({
|
||||||
text: "{{ settings.company }}",
|
text: "{{ settings.company }}",
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(result.steps[0].outputs.message).toEndWith("https://example.com")
|
expect(result.steps[0].outputs.message).toEndWith("https://example.com")
|
||||||
expect(result.steps[1].outputs.message).toEndWith(
|
expect(result.steps[1].outputs.message).toEndWith(
|
||||||
|
|
|
@ -169,6 +169,18 @@ if (descriptions.length) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetRowUsage = async () => {
|
||||||
|
await config.doInContext(
|
||||||
|
undefined,
|
||||||
|
async () =>
|
||||||
|
await quotas.setUsage(
|
||||||
|
0,
|
||||||
|
StaticQuotaName.ROWS,
|
||||||
|
QuotaUsageType.STATIC
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const getRowUsage = async () => {
|
const getRowUsage = async () => {
|
||||||
const { total } = await config.doInContext(undefined, () =>
|
const { total } = await config.doInContext(undefined, () =>
|
||||||
quotas.getCurrentUsageValues(
|
quotas.getCurrentUsageValues(
|
||||||
|
@ -206,6 +218,10 @@ if (descriptions.length) {
|
||||||
table = await config.api.table.save(defaultTable())
|
table = await config.api.table.save(defaultTable())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await resetRowUsage()
|
||||||
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("creates a new row successfully", async () => {
|
it("creates a new row successfully", async () => {
|
||||||
const rowUsage = await getRowUsage()
|
const rowUsage = await getRowUsage()
|
||||||
|
@ -3317,6 +3333,7 @@ if (descriptions.length) {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
mocks.licenses.useBudibaseAI()
|
mocks.licenses.useBudibaseAI()
|
||||||
mocks.licenses.useAICustomConfigs()
|
mocks.licenses.useAICustomConfigs()
|
||||||
|
|
||||||
envCleanup = setEnv({
|
envCleanup = setEnv({
|
||||||
OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd",
|
OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd",
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,14 +6,14 @@ import {
|
||||||
Role,
|
Role,
|
||||||
BuiltinPermissionID,
|
BuiltinPermissionID,
|
||||||
SourceType,
|
SourceType,
|
||||||
UsageOfScreensResponse,
|
UsageInScreensResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
basicScreen,
|
basicScreen,
|
||||||
tableScreen,
|
createTableScreen,
|
||||||
viewScreen,
|
createViewScreen,
|
||||||
queryScreen,
|
createQueryScreen,
|
||||||
basicTable,
|
basicTable,
|
||||||
viewV2,
|
viewV2,
|
||||||
basicQuery,
|
basicQuery,
|
||||||
|
@ -33,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)
|
||||||
})
|
})
|
||||||
|
@ -67,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()
|
||||||
})
|
})
|
||||||
|
@ -96,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())
|
||||||
|
@ -129,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)
|
||||||
|
@ -149,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)
|
||||||
|
@ -160,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)
|
||||||
|
@ -186,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)
|
||||||
|
@ -205,69 +190,51 @@ describe("/screens", () => {
|
||||||
describe("usage", () => {
|
describe("usage", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
await config.api.screen.save(basicScreen(), {
|
await config.api.screen.save(basicScreen())
|
||||||
status: 200,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function confirmScreen(usage: UsageOfScreensResponse, screen: Screen) {
|
function confirmScreen(usage: UsageInScreensResponse, screen: Screen) {
|
||||||
expect(usage.screens.length).toEqual(1)
|
expect(usage.screens.length).toEqual(1)
|
||||||
expect(usage.screens[0].url).toEqual(screen.routing.route)
|
expect(usage.screens[0].url).toEqual(screen.routing.route)
|
||||||
expect(usage.screens[0]._id).toEqual(screen._id!)
|
expect(usage.screens[0]._id).toEqual(screen._id!)
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should find table usage", async () => {
|
it("should find table usage", async () => {
|
||||||
const table = await config.api.table.save(basicTable(), { status: 200 })
|
const table = await config.api.table.save(basicTable())
|
||||||
const screen = await config.api.screen.save(
|
const screen = await config.api.screen.save(
|
||||||
tableScreen("BudibaseDB", table),
|
createTableScreen("BudibaseDB", table)
|
||||||
{ status: 200 }
|
|
||||||
)
|
)
|
||||||
const usage = await config.api.screen.usage(table._id!, { status: 200 })
|
const usage = await config.api.screen.usage(table._id!)
|
||||||
expect(usage.sourceType).toEqual(SourceType.TABLE)
|
expect(usage.sourceType).toEqual(SourceType.TABLE)
|
||||||
confirmScreen(usage, screen)
|
confirmScreen(usage, screen)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should find view usage", async () => {
|
it("should find view usage", async () => {
|
||||||
const table = await config.api.table.save(basicTable(), { status: 200 })
|
const table = await config.api.table.save(basicTable())
|
||||||
const view = await config.api.viewV2.create(
|
const view = await config.api.viewV2.create(
|
||||||
viewV2.createRequest(table._id!),
|
viewV2.createRequest(table._id!),
|
||||||
{ status: 201 }
|
{ status: 201 }
|
||||||
)
|
)
|
||||||
const screen = await config.api.screen.save(
|
const screen = await config.api.screen.save(
|
||||||
viewScreen("BudibaseDB", view),
|
createViewScreen("BudibaseDB", view)
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
const usage = await config.api.screen.usage(view.id, { status: 200 })
|
const usage = await config.api.screen.usage(view.id)
|
||||||
expect(usage.sourceType).toEqual(SourceType.VIEW)
|
expect(usage.sourceType).toEqual(SourceType.VIEW)
|
||||||
confirmScreen(usage, screen)
|
confirmScreen(usage, screen)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should find datasource/query usage", async () => {
|
it("should find datasource/query usage", async () => {
|
||||||
const datasource = await config.api.datasource.create(
|
const datasource = await config.api.datasource.create(
|
||||||
basicDatasource().datasource,
|
basicDatasource().datasource
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
const query = await config.api.query.save(basicQuery(datasource._id!), {
|
const query = await config.api.query.save(basicQuery(datasource._id!))
|
||||||
status: 200,
|
|
||||||
})
|
|
||||||
const screen = await config.api.screen.save(
|
const screen = await config.api.screen.save(
|
||||||
queryScreen(datasource._id!, query),
|
createQueryScreen(datasource._id!, query)
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
const dsUsage = await config.api.screen.usage(datasource._id!, {
|
const dsUsage = await config.api.screen.usage(datasource._id!)
|
||||||
status: 200,
|
|
||||||
})
|
|
||||||
expect(dsUsage.sourceType).toEqual(SourceType.DATASOURCE)
|
expect(dsUsage.sourceType).toEqual(SourceType.DATASOURCE)
|
||||||
confirmScreen(dsUsage, screen)
|
confirmScreen(dsUsage, screen)
|
||||||
const queryUsage = await config.api.screen.usage(query._id!, {
|
const queryUsage = await config.api.screen.usage(query._id!)
|
||||||
status: 200,
|
|
||||||
})
|
|
||||||
expect(queryUsage.sourceType).toEqual(SourceType.QUERY)
|
expect(queryUsage.sourceType).toEqual(SourceType.QUERY)
|
||||||
confirmScreen(queryUsage, screen)
|
confirmScreen(queryUsage, screen)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1040,6 +1040,12 @@ if (descriptions.length) {
|
||||||
string: { name: "FO" },
|
string: { name: "FO" },
|
||||||
}).toContainExactly([{ name: "foo" }])
|
}).toContainExactly([{ name: "foo" }])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should not coerce string to date for string columns", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
string: { name: "2020-01-01" },
|
||||||
|
}).toFindNothing()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("range", () => {
|
describe("range", () => {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import * as automation from "../threads/automation"
|
||||||
import { backups } from "@budibase/pro"
|
import { backups } from "@budibase/pro"
|
||||||
import { getAppMigrationQueue } from "../appMigrations/queue"
|
import { getAppMigrationQueue } from "../appMigrations/queue"
|
||||||
import { createBullBoard } from "@bull-board/api"
|
import { createBullBoard } from "@bull-board/api"
|
||||||
import BullQueue from "bull"
|
import { AutomationData } from "@budibase/types"
|
||||||
|
|
||||||
export const automationQueue: BullQueue.Queue = queue.createQueue(
|
export const automationQueue = queue.createQueue<AutomationData>(
|
||||||
queue.JobQueue.AUTOMATION,
|
queue.JobQueue.AUTOMATION,
|
||||||
{ removeStalledCb: automation.removeStalled }
|
{ removeStalledCb: automation.removeStalled }
|
||||||
)
|
)
|
||||||
|
@ -16,24 +16,20 @@ const PATH_PREFIX = "/bulladmin"
|
||||||
|
|
||||||
export async function init() {
|
export async function init() {
|
||||||
// Set up queues for bull board admin
|
// Set up queues for bull board admin
|
||||||
|
const queues = [new BullAdapter(automationQueue)]
|
||||||
|
|
||||||
const backupQueue = backups.getBackupQueue()
|
const backupQueue = backups.getBackupQueue()
|
||||||
const appMigrationQueue = getAppMigrationQueue()
|
|
||||||
const queues = [automationQueue]
|
|
||||||
if (backupQueue) {
|
if (backupQueue) {
|
||||||
queues.push(backupQueue)
|
queues.push(new BullAdapter(backupQueue))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appMigrationQueue = getAppMigrationQueue()
|
||||||
if (appMigrationQueue) {
|
if (appMigrationQueue) {
|
||||||
queues.push(appMigrationQueue)
|
queues.push(new BullAdapter(appMigrationQueue))
|
||||||
}
|
}
|
||||||
const adapters = []
|
|
||||||
const serverAdapter: any = new KoaAdapter()
|
const serverAdapter = new KoaAdapter()
|
||||||
for (let queue of queues) {
|
createBullBoard({ queues, serverAdapter })
|
||||||
adapters.push(new BullAdapter(queue))
|
|
||||||
}
|
|
||||||
createBullBoard({
|
|
||||||
queues: adapters,
|
|
||||||
serverAdapter,
|
|
||||||
})
|
|
||||||
serverAdapter.setBasePath(PATH_PREFIX)
|
serverAdapter.setBasePath(PATH_PREFIX)
|
||||||
return serverAdapter.registerPlugin()
|
return serverAdapter.registerPlugin()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { processEvent } from "./utils"
|
import { processEvent } from "./utils"
|
||||||
import { automationQueue } from "./bullboard"
|
import { automationQueue } from "./bullboard"
|
||||||
import { rebootTrigger } from "./triggers"
|
import { rebootTrigger } from "./triggers"
|
||||||
import BullQueue from "bull"
|
|
||||||
import { automationsEnabled } from "../features"
|
import { automationsEnabled } from "../features"
|
||||||
|
|
||||||
export { automationQueue } from "./bullboard"
|
export { automationQueue } from "./bullboard"
|
||||||
|
@ -25,6 +24,6 @@ export async function init() {
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getQueues(): BullQueue.Queue[] {
|
export function getQueue() {
|
||||||
return [automationQueue]
|
return automationQueue
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,11 @@ import { basicAutomation } from "../../tests/utilities/structures"
|
||||||
import { wait } from "../../utilities"
|
import { wait } from "../../utilities"
|
||||||
import { makePartial } from "../../tests/utilities"
|
import { makePartial } from "../../tests/utilities"
|
||||||
import { cleanInputValues } from "../automationUtils"
|
import { cleanInputValues } from "../automationUtils"
|
||||||
import * as setup from "./utilities"
|
|
||||||
import { Automation } from "@budibase/types"
|
import { Automation } from "@budibase/types"
|
||||||
|
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
||||||
|
|
||||||
describe("Run through some parts of the automations system", () => {
|
describe("Run through some parts of the automations system", () => {
|
||||||
let config = setup.getConfig()
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await automation.init()
|
await automation.init()
|
||||||
|
@ -30,7 +30,7 @@ describe("Run through some parts of the automations system", () => {
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await automation.shutdown()
|
await automation.shutdown()
|
||||||
setup.afterAll()
|
config.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to init in builder", async () => {
|
it("should be able to init in builder", async () => {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import * as automation from "../../index"
|
import * as automation from "../index"
|
||||||
import * as setup from "../utilities"
|
|
||||||
import { Table, AutomationStatus } from "@budibase/types"
|
import { Table, AutomationStatus } from "@budibase/types"
|
||||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||||
|
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
||||||
|
|
||||||
describe("Branching automations", () => {
|
describe("Branching automations", () => {
|
||||||
let config = setup.getConfig(),
|
const config = new TestConfiguration()
|
||||||
table: Table
|
let table: Table
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await automation.init()
|
await automation.init()
|
||||||
|
@ -14,7 +14,9 @@ describe("Branching automations", () => {
|
||||||
await config.createRow()
|
await config.createRow()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(() => {
|
||||||
|
config.end()
|
||||||
|
})
|
||||||
|
|
||||||
it("should run a multiple nested branching automation", async () => {
|
it("should run a multiple nested branching automation", async () => {
|
||||||
const firstLogId = "11111111-1111-1111-1111-111111111111"
|
const firstLogId = "11111111-1111-1111-1111-111111111111"
|
||||||
|
@ -22,12 +24,8 @@ describe("Branching automations", () => {
|
||||||
const branch2LogId = "33333333-3333-3333-3333-333333333333"
|
const branch2LogId = "33333333-3333-3333-3333-333333333333"
|
||||||
const branch2Id = "44444444-4444-4444-4444-444444444444"
|
const branch2Id = "44444444-4444-4444-4444-444444444444"
|
||||||
|
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test Trigger with Loop and Create Row",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.serverLog(
|
.serverLog(
|
||||||
{ text: "Starting automation" },
|
{ text: "Starting automation" },
|
||||||
{ stepName: "FirstLog", stepId: firstLogId }
|
{ stepName: "FirstLog", stepId: firstLogId }
|
||||||
|
@ -78,19 +76,15 @@ describe("Branching automations", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps[3].outputs.status).toContain("branch1 branch taken")
|
expect(results.steps[3].outputs.status).toContain("branch1 branch taken")
|
||||||
expect(results.steps[4].outputs.message).toContain("Branch 1.1")
|
expect(results.steps[4].outputs.message).toContain("Branch 1.1")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should execute correct branch based on string equality", async () => {
|
it("should execute correct branch based on string equality", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "String Equality Branching",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: { status: "active" } })
|
|
||||||
.branch({
|
.branch({
|
||||||
activeBranch: {
|
activeBranch: {
|
||||||
steps: stepBuilder => stepBuilder.serverLog({ text: "Active user" }),
|
steps: stepBuilder => stepBuilder.serverLog({ text: "Active user" }),
|
||||||
|
@ -106,7 +100,7 @@ describe("Branching automations", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: { status: "active" } })
|
||||||
expect(results.steps[0].outputs.status).toContain(
|
expect(results.steps[0].outputs.status).toContain(
|
||||||
"activeBranch branch taken"
|
"activeBranch branch taken"
|
||||||
)
|
)
|
||||||
|
@ -114,12 +108,8 @@ describe("Branching automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle multiple conditions with AND operator", async () => {
|
it("should handle multiple conditions with AND operator", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Multiple AND Conditions Branching",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: { status: "active", role: "admin" } })
|
|
||||||
.branch({
|
.branch({
|
||||||
activeAdminBranch: {
|
activeAdminBranch: {
|
||||||
steps: stepBuilder =>
|
steps: stepBuilder =>
|
||||||
|
@ -140,18 +130,14 @@ describe("Branching automations", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: { status: "active", role: "admin" } })
|
||||||
|
|
||||||
expect(results.steps[1].outputs.message).toContain("Active admin user")
|
expect(results.steps[1].outputs.message).toContain("Active admin user")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle multiple conditions with OR operator", async () => {
|
it("should handle multiple conditions with OR operator", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Multiple OR Conditions Branching",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: { status: "test", role: "user" } })
|
|
||||||
.branch({
|
.branch({
|
||||||
specialBranch: {
|
specialBranch: {
|
||||||
steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }),
|
steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }),
|
||||||
|
@ -176,18 +162,14 @@ describe("Branching automations", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: { status: "test", role: "user" } })
|
||||||
|
|
||||||
expect(results.steps[1].outputs.message).toContain("Special user")
|
expect(results.steps[1].outputs.message).toContain("Special user")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should stop the branch automation when no conditions are met", async () => {
|
it("should stop the branch automation when no conditions are met", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Multiple OR Conditions Branching",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: { status: "test", role: "user" } })
|
|
||||||
.createRow({ row: { name: "Test", tableId: table._id } })
|
.createRow({ row: { name: "Test", tableId: table._id } })
|
||||||
.branch({
|
.branch({
|
||||||
specialBranch: {
|
specialBranch: {
|
||||||
|
@ -213,8 +195,7 @@ describe("Branching automations", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.serverLog({ text: "Test" })
|
.test({ fields: { status: "test", role: "user" } })
|
||||||
.run()
|
|
||||||
|
|
||||||
expect(results.steps[1].outputs.status).toEqual(
|
expect(results.steps[1].outputs.status).toEqual(
|
||||||
AutomationStatus.NO_CONDITION_MET
|
AutomationStatus.NO_CONDITION_MET
|
||||||
|
@ -223,12 +204,8 @@ describe("Branching automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("evaluate multiple conditions", async () => {
|
it("evaluate multiple conditions", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "evaluate multiple conditions",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: { test_trigger: true } })
|
|
||||||
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
|
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
|
||||||
.branch({
|
.branch({
|
||||||
specialBranch: {
|
specialBranch: {
|
||||||
|
@ -262,18 +239,14 @@ describe("Branching automations", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: { test_trigger: true } })
|
||||||
|
|
||||||
expect(results.steps[2].outputs.message).toContain("Special user")
|
expect(results.steps[2].outputs.message).toContain("Special user")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("evaluate multiple conditions with interpolated text", async () => {
|
it("evaluate multiple conditions with interpolated text", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "evaluate multiple conditions",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: { test_trigger: true } })
|
|
||||||
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
|
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
|
||||||
.branch({
|
.branch({
|
||||||
specialBranch: {
|
specialBranch: {
|
||||||
|
@ -303,7 +276,7 @@ describe("Branching automations", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: { test_trigger: true } })
|
||||||
|
|
||||||
expect(results.steps[2].outputs.message).toContain("Special user")
|
expect(results.steps[2].outputs.message).toContain("Special user")
|
||||||
})
|
})
|
|
@ -1,22 +0,0 @@
|
||||||
import { runStep, actions, getConfig } from "./utilities"
|
|
||||||
import { reset } from "timekeeper"
|
|
||||||
|
|
||||||
// need real Date for this test
|
|
||||||
reset()
|
|
||||||
|
|
||||||
describe("test the delay logic", () => {
|
|
||||||
const config = getConfig()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to run the delay", async () => {
|
|
||||||
const time = 100
|
|
||||||
const before = Date.now()
|
|
||||||
await runStep(config, actions.DELAY.stepId, { time: time })
|
|
||||||
const now = Date.now()
|
|
||||||
// divide by two just so that test will always pass as long as there was some sort of delay
|
|
||||||
expect(now - before).toBeGreaterThanOrEqual(time / 2)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
|
||||||
import * as setup from "./utilities"
|
|
||||||
|
|
||||||
describe("test the delete row action", () => {
|
|
||||||
let table: any,
|
|
||||||
row: any,
|
|
||||||
config = setup.getConfig()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
table = await config.createTable()
|
|
||||||
row = await config.createRow()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
|
||||||
|
|
||||||
it("should be able to run the delete row action", async () => {
|
|
||||||
const builder = createAutomationBuilder({
|
|
||||||
name: "Delete Row Automation",
|
|
||||||
})
|
|
||||||
|
|
||||||
await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.deleteRow({
|
|
||||||
tableId: table._id,
|
|
||||||
id: row._id,
|
|
||||||
revision: row._rev,
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
|
|
||||||
await config.api.row.get(table._id, row._id, {
|
|
||||||
status: 404,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should check invalid inputs return an error", async () => {
|
|
||||||
const builder = createAutomationBuilder({
|
|
||||||
name: "Invalid Inputs Automation",
|
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.deleteRow({ tableId: "", id: "", revision: "" })
|
|
||||||
.run()
|
|
||||||
|
|
||||||
expect(results.steps[0].outputs.success).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return an error when table doesn't exist", async () => {
|
|
||||||
const builder = createAutomationBuilder({
|
|
||||||
name: "Nonexistent Table Automation",
|
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.deleteRow({
|
|
||||||
tableId: "invalid",
|
|
||||||
id: "invalid",
|
|
||||||
revision: "invalid",
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
|
|
||||||
expect(results.steps[0].outputs.success).toEqual(false)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities"
|
|
||||||
import nock from "nock"
|
|
||||||
|
|
||||||
describe("test the outgoing webhook action", () => {
|
|
||||||
let config = getConfig()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(_afterAll)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
nock.cleanAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to run the action", async () => {
|
|
||||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
|
||||||
const res = await runStep(config, actions.discord.stepId, {
|
|
||||||
url: "http://www.example.com",
|
|
||||||
username: "joe_bloggs",
|
|
||||||
})
|
|
||||||
expect(res.response.foo).toEqual("bar")
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,64 +0,0 @@
|
||||||
import * as setup from "./utilities"
|
|
||||||
import { automations } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
const FilterConditions = automations.steps.filter.FilterConditions
|
|
||||||
|
|
||||||
describe("test the filter logic", () => {
|
|
||||||
const config = setup.getConfig()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function checkFilter(
|
|
||||||
field: any,
|
|
||||||
condition: string,
|
|
||||||
value: any,
|
|
||||||
pass = true
|
|
||||||
) {
|
|
||||||
let res = await setup.runStep(config, setup.actions.FILTER.stepId, {
|
|
||||||
field,
|
|
||||||
condition,
|
|
||||||
value,
|
|
||||||
})
|
|
||||||
expect(res.result).toEqual(pass)
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should be able test equality", async () => {
|
|
||||||
await checkFilter("hello", FilterConditions.EQUAL, "hello", true)
|
|
||||||
await checkFilter("hello", FilterConditions.EQUAL, "no", false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to test greater than", async () => {
|
|
||||||
await checkFilter(10, FilterConditions.GREATER_THAN, 5, true)
|
|
||||||
await checkFilter(10, FilterConditions.GREATER_THAN, 15, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to test less than", async () => {
|
|
||||||
await checkFilter(5, FilterConditions.LESS_THAN, 10, true)
|
|
||||||
await checkFilter(15, FilterConditions.LESS_THAN, 10, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to in-equality", async () => {
|
|
||||||
await checkFilter("hello", FilterConditions.NOT_EQUAL, "no", true)
|
|
||||||
await checkFilter(10, FilterConditions.NOT_EQUAL, 10, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("check number coercion", async () => {
|
|
||||||
await checkFilter("10", FilterConditions.GREATER_THAN, "5", true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("check date coercion", async () => {
|
|
||||||
await checkFilter(
|
|
||||||
new Date().toISOString(),
|
|
||||||
FilterConditions.GREATER_THAN,
|
|
||||||
new Date(-10000).toISOString(),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("check objects always false", async () => {
|
|
||||||
await checkFilter({}, FilterConditions.EQUAL, {}, false)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,153 +0,0 @@
|
||||||
import * as automation from "../index"
|
|
||||||
import * as triggers from "../triggers"
|
|
||||||
import { loopAutomation } from "../../tests/utilities/structures"
|
|
||||||
import { context } from "@budibase/backend-core"
|
|
||||||
import * as setup from "./utilities"
|
|
||||||
import { Table, LoopStepType, AutomationResults } from "@budibase/types"
|
|
||||||
import * as loopUtils from "../loopUtils"
|
|
||||||
import { LoopInput } from "../../definitions/automations"
|
|
||||||
|
|
||||||
describe("Attempt to run a basic loop automation", () => {
|
|
||||||
let config = setup.getConfig(),
|
|
||||||
table: Table
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await automation.init()
|
|
||||||
await config.init()
|
|
||||||
table = await config.createTable()
|
|
||||||
await config.createRow()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
|
||||||
|
|
||||||
async function runLoop(loopOpts?: LoopInput): Promise<AutomationResults> {
|
|
||||||
const appId = config.getAppId()
|
|
||||||
return await context.doInAppContext(appId, async () => {
|
|
||||||
const params = { fields: { appId } }
|
|
||||||
const result = await triggers.externalTrigger(
|
|
||||||
loopAutomation(table._id!, loopOpts),
|
|
||||||
params,
|
|
||||||
{ getResponses: true }
|
|
||||||
)
|
|
||||||
if ("outputs" in result && !result.outputs.success) {
|
|
||||||
throw new Error("Unable to proceed - failed to return anything.")
|
|
||||||
}
|
|
||||||
return result as AutomationResults
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
it("attempt to run a basic loop", async () => {
|
|
||||||
const resp = await runLoop()
|
|
||||||
expect(resp.steps[2].outputs.iterations).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("test a loop with a string", async () => {
|
|
||||||
const resp = await runLoop({
|
|
||||||
option: LoopStepType.STRING,
|
|
||||||
binding: "a,b,c",
|
|
||||||
})
|
|
||||||
expect(resp.steps[2].outputs.iterations).toBe(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("test a loop with a binding that returns an integer", async () => {
|
|
||||||
const resp = await runLoop({
|
|
||||||
option: LoopStepType.ARRAY,
|
|
||||||
binding: "{{ 1 }}",
|
|
||||||
})
|
|
||||||
expect(resp.steps[2].outputs.iterations).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("replaceFakeBindings", () => {
|
|
||||||
it("should replace loop bindings in nested objects", () => {
|
|
||||||
const originalStepInput = {
|
|
||||||
schema: {
|
|
||||||
name: {
|
|
||||||
type: "string",
|
|
||||||
constraints: {
|
|
||||||
type: "string",
|
|
||||||
length: { maximum: null },
|
|
||||||
presence: false,
|
|
||||||
},
|
|
||||||
name: "name",
|
|
||||||
display: { type: "Text" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
row: {
|
|
||||||
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
|
|
||||||
name: "{{ loop.currentItem.pokemon }}",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const loopStepNumber = 3
|
|
||||||
|
|
||||||
const result = loopUtils.replaceFakeBindings(
|
|
||||||
originalStepInput,
|
|
||||||
loopStepNumber
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
schema: {
|
|
||||||
name: {
|
|
||||||
type: "string",
|
|
||||||
constraints: {
|
|
||||||
type: "string",
|
|
||||||
length: { maximum: null },
|
|
||||||
presence: false,
|
|
||||||
},
|
|
||||||
name: "name",
|
|
||||||
display: { type: "Text" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
row: {
|
|
||||||
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
|
|
||||||
name: "{{ steps.3.currentItem.pokemon }}",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should handle null values in nested objects", () => {
|
|
||||||
const originalStepInput = {
|
|
||||||
nullValue: null,
|
|
||||||
nestedNull: {
|
|
||||||
someKey: null,
|
|
||||||
},
|
|
||||||
validValue: "{{ loop.someValue }}",
|
|
||||||
}
|
|
||||||
|
|
||||||
const loopStepNumber = 2
|
|
||||||
|
|
||||||
const result = loopUtils.replaceFakeBindings(
|
|
||||||
originalStepInput,
|
|
||||||
loopStepNumber
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
nullValue: null,
|
|
||||||
nestedNull: {
|
|
||||||
someKey: null,
|
|
||||||
},
|
|
||||||
validValue: "{{ steps.2.someValue }}",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should handle empty objects and arrays", () => {
|
|
||||||
const originalStepInput = {
|
|
||||||
emptyObject: {},
|
|
||||||
emptyArray: [],
|
|
||||||
nestedEmpty: {
|
|
||||||
emptyObj: {},
|
|
||||||
emptyArr: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const loopStepNumber = 1
|
|
||||||
|
|
||||||
const result = loopUtils.replaceFakeBindings(
|
|
||||||
originalStepInput,
|
|
||||||
loopStepNumber
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result).toEqual(originalStepInput)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { getConfig, afterAll, runStep, actions } from "./utilities"
|
|
||||||
import nock from "nock"
|
|
||||||
|
|
||||||
describe("test the outgoing webhook action", () => {
|
|
||||||
let config = getConfig()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
nock.cleanAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to run the action", async () => {
|
|
||||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
|
||||||
const res = await runStep(config, actions.integromat.stepId, {
|
|
||||||
url: "http://www.example.com",
|
|
||||||
})
|
|
||||||
expect(res.response.foo).toEqual("bar")
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should add the payload props when a JSON string is provided", async () => {
|
|
||||||
const payload = {
|
|
||||||
value1: 1,
|
|
||||||
value2: 2,
|
|
||||||
value3: 3,
|
|
||||||
value4: 4,
|
|
||||||
value5: 5,
|
|
||||||
name: "Adam",
|
|
||||||
age: 9,
|
|
||||||
}
|
|
||||||
|
|
||||||
nock("http://www.example.com/")
|
|
||||||
.post("/", payload)
|
|
||||||
.reply(200, { foo: "bar" })
|
|
||||||
|
|
||||||
const res = await runStep(config, actions.integromat.stepId, {
|
|
||||||
body: { value: JSON.stringify(payload) },
|
|
||||||
url: "http://www.example.com",
|
|
||||||
})
|
|
||||||
expect(res.response.foo).toEqual("bar")
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
|
||||||
const res = await runStep(config, actions.integromat.stepId, {
|
|
||||||
body: { value: "{ invalid json }" },
|
|
||||||
url: "http://www.example.com",
|
|
||||||
})
|
|
||||||
expect(res.httpStatus).toEqual(400)
|
|
||||||
expect(res.response).toEqual("Invalid payload JSON")
|
|
||||||
expect(res.success).toEqual(false)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,71 +0,0 @@
|
||||||
import { getConfig, afterAll, runStep, actions } from "./utilities"
|
|
||||||
import nock from "nock"
|
|
||||||
|
|
||||||
describe("test the outgoing webhook action", () => {
|
|
||||||
let config = getConfig()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
nock.cleanAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to run the action and default to 'get'", async () => {
|
|
||||||
nock("http://www.example.com/").get("/").reply(200, { foo: "bar" })
|
|
||||||
const res = await runStep(config, actions.n8n.stepId, {
|
|
||||||
url: "http://www.example.com",
|
|
||||||
body: {
|
|
||||||
test: "IGNORE_ME",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(res.response.foo).toEqual("bar")
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should add the payload props when a JSON string is provided", async () => {
|
|
||||||
nock("http://www.example.com/")
|
|
||||||
.post("/", { name: "Adam", age: 9 })
|
|
||||||
.reply(200)
|
|
||||||
const res = await runStep(config, actions.n8n.stepId, {
|
|
||||||
body: {
|
|
||||||
value: JSON.stringify({ name: "Adam", age: 9 }),
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
url: "http://www.example.com",
|
|
||||||
})
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
|
||||||
const payload = `{ value1 1 }`
|
|
||||||
const res = await runStep(config, actions.n8n.stepId, {
|
|
||||||
value1: "ONE",
|
|
||||||
body: {
|
|
||||||
value: payload,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
url: "http://www.example.com",
|
|
||||||
})
|
|
||||||
expect(res.httpStatus).toEqual(400)
|
|
||||||
expect(res.response).toEqual("Invalid payload JSON")
|
|
||||||
expect(res.success).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should not append the body if the method is HEAD", async () => {
|
|
||||||
nock("http://www.example.com/")
|
|
||||||
.head("/", body => body === "")
|
|
||||||
.reply(200)
|
|
||||||
const res = await runStep(config, actions.n8n.stepId, {
|
|
||||||
url: "http://www.example.com",
|
|
||||||
method: "HEAD",
|
|
||||||
body: {
|
|
||||||
test: "IGNORE_ME",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,165 +0,0 @@
|
||||||
import { getConfig, afterAll as _afterAll } from "./utilities"
|
|
||||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
|
||||||
import { OpenAI } from "openai"
|
|
||||||
import { setEnv as setCoreEnv } from "@budibase/backend-core"
|
|
||||||
import * as pro from "@budibase/pro"
|
|
||||||
import { Model } from "@budibase/types"
|
|
||||||
|
|
||||||
jest.mock("openai", () => ({
|
|
||||||
OpenAI: jest.fn().mockImplementation(() => ({
|
|
||||||
chat: {
|
|
||||||
completions: {
|
|
||||||
create: jest.fn(() => ({
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
message: {
|
|
||||||
content: "This is a test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}))
|
|
||||||
jest.mock("@budibase/pro", () => ({
|
|
||||||
...jest.requireActual("@budibase/pro"),
|
|
||||||
ai: {
|
|
||||||
LargeLanguageModel: {
|
|
||||||
forCurrentTenant: jest.fn().mockImplementation(() => ({
|
|
||||||
llm: {},
|
|
||||||
init: jest.fn(),
|
|
||||||
run: jest.fn(),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
features: {
|
|
||||||
isAICustomConfigsEnabled: jest.fn(),
|
|
||||||
isBudibaseAIEnabled: jest.fn(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const mockedPro = jest.mocked(pro)
|
|
||||||
const mockedOpenAI = OpenAI as jest.MockedClass<typeof OpenAI>
|
|
||||||
|
|
||||||
const OPENAI_PROMPT = "What is the meaning of life?"
|
|
||||||
|
|
||||||
describe("test the openai action", () => {
|
|
||||||
let config = getConfig()
|
|
||||||
let resetEnv: () => void | undefined
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
setCoreEnv({ SELF_HOSTED: true })
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
resetEnv = setCoreEnv({ OPENAI_API_KEY: "abc123" })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
resetEnv()
|
|
||||||
jest.clearAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(_afterAll)
|
|
||||||
|
|
||||||
it("should be able to receive a response from ChatGPT given a prompt", async () => {
|
|
||||||
setCoreEnv({ SELF_HOSTED: true })
|
|
||||||
|
|
||||||
const result = await createAutomationBuilder({
|
|
||||||
name: "Test OpenAI Response",
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.openai(
|
|
||||||
{ prompt: OPENAI_PROMPT, model: Model.GPT_4O_MINI },
|
|
||||||
{ stepName: "Basic OpenAI Query" }
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
|
|
||||||
expect(result.steps[0].outputs.response).toEqual("This is a test")
|
|
||||||
expect(result.steps[0].outputs.success).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should present the correct error message when a prompt is not provided", async () => {
|
|
||||||
const result = await createAutomationBuilder({
|
|
||||||
name: "Test OpenAI No Prompt",
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.openai(
|
|
||||||
{ prompt: "", model: Model.GPT_4O_MINI },
|
|
||||||
{ stepName: "Empty Prompt Query" }
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
|
|
||||||
expect(result.steps[0].outputs.response).toEqual(
|
|
||||||
"Budibase OpenAI Automation Failed: No prompt supplied"
|
|
||||||
)
|
|
||||||
expect(result.steps[0].outputs.success).toBeFalsy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should present the correct error message when an error is thrown from the createChatCompletion call", async () => {
|
|
||||||
mockedOpenAI.mockImplementation(
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
chat: {
|
|
||||||
completions: {
|
|
||||||
create: jest.fn(() => {
|
|
||||||
throw new Error(
|
|
||||||
"An error occurred while calling createChatCompletion"
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as any)
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await createAutomationBuilder({
|
|
||||||
name: "Test OpenAI Error",
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.openai(
|
|
||||||
{ prompt: OPENAI_PROMPT, model: Model.GPT_4O_MINI },
|
|
||||||
{ stepName: "Error Producing Query" }
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
|
|
||||||
expect(result.steps[0].outputs.response).toEqual(
|
|
||||||
"Error: An error occurred while calling createChatCompletion"
|
|
||||||
)
|
|
||||||
expect(result.steps[0].outputs.success).toBeFalsy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should ensure that the pro AI module is called when the budibase AI features are enabled", async () => {
|
|
||||||
jest.spyOn(pro.features, "isBudibaseAIEnabled").mockResolvedValue(true)
|
|
||||||
jest.spyOn(pro.features, "isAICustomConfigsEnabled").mockResolvedValue(true)
|
|
||||||
|
|
||||||
const prompt = "What is the meaning of life?"
|
|
||||||
await createAutomationBuilder({
|
|
||||||
name: "Test OpenAI Pro Features",
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.openai(
|
|
||||||
{
|
|
||||||
model: Model.GPT_4O_MINI,
|
|
||||||
prompt,
|
|
||||||
},
|
|
||||||
{ stepName: "Pro Features Query" }
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
|
|
||||||
expect(pro.ai.LargeLanguageModel.forCurrentTenant).toHaveBeenCalledWith(
|
|
||||||
"gpt-4o-mini"
|
|
||||||
)
|
|
||||||
|
|
||||||
const llmInstance =
|
|
||||||
mockedPro.ai.LargeLanguageModel.forCurrentTenant.mock.results[0].value
|
|
||||||
// init does not appear to be called currently
|
|
||||||
// expect(llmInstance.init).toHaveBeenCalled()
|
|
||||||
expect(llmInstance.run).toHaveBeenCalledWith(prompt)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities"
|
|
||||||
import nock from "nock"
|
|
||||||
|
|
||||||
describe("test the outgoing webhook action", () => {
|
|
||||||
const config = getConfig()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(_afterAll)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
nock.cleanAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to run the action", async () => {
|
|
||||||
nock("http://www.example.com")
|
|
||||||
.post("/", { a: 1 })
|
|
||||||
.reply(200, { foo: "bar" })
|
|
||||||
const res = await runStep(config, actions.OUTGOING_WEBHOOK.stepId, {
|
|
||||||
requestMethod: "POST",
|
|
||||||
url: "www.example.com",
|
|
||||||
requestBody: JSON.stringify({ a: 1 }),
|
|
||||||
})
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
expect(res.response.foo).toEqual("bar")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return an error if something goes wrong in fetch", async () => {
|
|
||||||
const res = await runStep(config, actions.OUTGOING_WEBHOOK.stepId, {
|
|
||||||
requestMethod: "GET",
|
|
||||||
url: "www.invalid.com",
|
|
||||||
})
|
|
||||||
expect(res.success).toEqual(false)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,43 +1,36 @@
|
||||||
import * as automation from "../../index"
|
import * as automation from "../index"
|
||||||
import * as setup from "../utilities"
|
|
||||||
import { LoopStepType, FieldType, Table, Datasource } from "@budibase/types"
|
import { LoopStepType, FieldType, Table, Datasource } from "@budibase/types"
|
||||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||||
import {
|
import {
|
||||||
DatabaseName,
|
DatabaseName,
|
||||||
datasourceDescribe,
|
datasourceDescribe,
|
||||||
} from "../../../integrations/tests/utils"
|
} from "../../integrations/tests/utils"
|
||||||
import { Knex } from "knex"
|
import { Knex } from "knex"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
import { automations } from "@budibase/shared-core"
|
import { automations } from "@budibase/shared-core"
|
||||||
|
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
||||||
|
import { basicTable } from "../../tests/utilities/structures"
|
||||||
|
|
||||||
const FilterConditions = automations.steps.filter.FilterConditions
|
const FilterConditions = automations.steps.filter.FilterConditions
|
||||||
|
|
||||||
describe("Automation Scenarios", () => {
|
describe("Automation Scenarios", () => {
|
||||||
let config = setup.getConfig()
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await automation.init()
|
await automation.init()
|
||||||
await config.init()
|
await config.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(() => {
|
||||||
|
config.end()
|
||||||
|
})
|
||||||
|
|
||||||
describe("Row Automations", () => {
|
describe("Row Automations", () => {
|
||||||
it("should trigger an automation which then creates a row", async () => {
|
it("should trigger an automation which then creates a row", async () => {
|
||||||
const table = await config.createTable()
|
const table = await config.api.table.save(basicTable())
|
||||||
|
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test Row Save and Create",
|
.onRowUpdated({ tableId: table._id! })
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.rowUpdated(
|
|
||||||
{ tableId: table._id! },
|
|
||||||
{
|
|
||||||
row: { name: "Test", description: "TEST" },
|
|
||||||
id: "1234",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.createRow({
|
.createRow({
|
||||||
row: {
|
row: {
|
||||||
name: "{{trigger.row.name}}",
|
name: "{{trigger.row.name}}",
|
||||||
|
@ -45,7 +38,10 @@ describe("Automation Scenarios", () => {
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.test({
|
||||||
|
row: { name: "Test", description: "TEST" },
|
||||||
|
id: "1234",
|
||||||
|
})
|
||||||
|
|
||||||
expect(results.steps).toHaveLength(1)
|
expect(results.steps).toHaveLength(1)
|
||||||
|
|
||||||
|
@ -58,45 +54,35 @@ describe("Automation Scenarios", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should trigger an automation which querys the database", async () => {
|
it("should trigger an automation which queries the database", async () => {
|
||||||
const table = await config.createTable()
|
const table = await config.api.table.save(basicTable())
|
||||||
const row = {
|
const row = {
|
||||||
name: "Test Row",
|
name: "Test Row",
|
||||||
description: "original description",
|
description: "original description",
|
||||||
tableId: table._id,
|
|
||||||
}
|
}
|
||||||
await config.createRow(row)
|
await config.api.row.save(table._id!, row)
|
||||||
await config.createRow(row)
|
await config.api.row.save(table._id!, row)
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test Row Save and Create",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.queryRows({
|
.queryRows({
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps).toHaveLength(1)
|
expect(results.steps).toHaveLength(1)
|
||||||
expect(results.steps[0].outputs.rows).toHaveLength(2)
|
expect(results.steps[0].outputs.rows).toHaveLength(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should trigger an automation which querys the database then deletes a row", async () => {
|
it("should trigger an automation which queries the database then deletes a row", async () => {
|
||||||
const table = await config.createTable()
|
const table = await config.api.table.save(basicTable())
|
||||||
const row = {
|
const row = {
|
||||||
name: "DFN",
|
name: "DFN",
|
||||||
description: "original description",
|
description: "original description",
|
||||||
tableId: table._id,
|
|
||||||
}
|
}
|
||||||
await config.createRow(row)
|
await config.api.row.save(table._id!, row)
|
||||||
await config.createRow(row)
|
await config.api.row.save(table._id!, row)
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test Row Save and Create",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.queryRows({
|
.queryRows({
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
})
|
})
|
||||||
|
@ -107,7 +93,7 @@ describe("Automation Scenarios", () => {
|
||||||
.queryRows({
|
.queryRows({
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps).toHaveLength(3)
|
expect(results.steps).toHaveLength(3)
|
||||||
expect(results.steps[1].outputs.success).toBeTruthy()
|
expect(results.steps[1].outputs.success).toBeTruthy()
|
||||||
|
@ -115,7 +101,8 @@ describe("Automation Scenarios", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should trigger an automation which creates and then updates a row", async () => {
|
it("should trigger an automation which creates and then updates a row", async () => {
|
||||||
const table = await config.createTable({
|
const table = await config.api.table.save({
|
||||||
|
...basicTable(),
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
type: "table",
|
type: "table",
|
||||||
schema: {
|
schema: {
|
||||||
|
@ -136,12 +123,8 @@ describe("Automation Scenarios", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test Create and Update Row",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.createRow(
|
.createRow(
|
||||||
{
|
{
|
||||||
row: {
|
row: {
|
||||||
|
@ -170,7 +153,7 @@ describe("Automation Scenarios", () => {
|
||||||
},
|
},
|
||||||
{ stepName: "QueryRowsStep" }
|
{ stepName: "QueryRowsStep" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps).toHaveLength(3)
|
expect(results.steps).toHaveLength(3)
|
||||||
|
|
||||||
|
@ -202,20 +185,15 @@ describe("Automation Scenarios", () => {
|
||||||
|
|
||||||
describe("Name Based Automations", () => {
|
describe("Name Based Automations", () => {
|
||||||
it("should fetch and delete a rpw using automation naming", async () => {
|
it("should fetch and delete a rpw using automation naming", async () => {
|
||||||
const table = await config.createTable()
|
const table = await config.api.table.save(basicTable())
|
||||||
const row = {
|
const row = {
|
||||||
name: "DFN",
|
name: "DFN",
|
||||||
description: "original description",
|
description: "original description",
|
||||||
tableId: table._id,
|
|
||||||
}
|
}
|
||||||
await config.createRow(row)
|
await config.api.row.save(table._id!, row)
|
||||||
await config.createRow(row)
|
await config.api.row.save(table._id!, row)
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test Query and Delete Row",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.queryRows(
|
.queryRows(
|
||||||
{
|
{
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
|
@ -229,7 +207,7 @@ describe("Automation Scenarios", () => {
|
||||||
.queryRows({
|
.queryRows({
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps).toHaveLength(3)
|
expect(results.steps).toHaveLength(3)
|
||||||
expect(results.steps[1].outputs.success).toBeTruthy()
|
expect(results.steps[1].outputs.success).toBeTruthy()
|
||||||
|
@ -240,7 +218,8 @@ describe("Automation Scenarios", () => {
|
||||||
let table: Table
|
let table: Table
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
table = await config.createTable({
|
table = await config.api.table.save({
|
||||||
|
...basicTable(),
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
type: "table",
|
type: "table",
|
||||||
schema: {
|
schema: {
|
||||||
|
@ -263,12 +242,8 @@ describe("Automation Scenarios", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should stop an automation if the condition is not met", async () => {
|
it("should stop an automation if the condition is not met", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test Equal",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.createRow({
|
.createRow({
|
||||||
row: {
|
row: {
|
||||||
name: "Equal Test",
|
name: "Equal Test",
|
||||||
|
@ -285,7 +260,7 @@ describe("Automation Scenarios", () => {
|
||||||
value: 20,
|
value: 20,
|
||||||
})
|
})
|
||||||
.serverLog({ text: "Equal condition met" })
|
.serverLog({ text: "Equal condition met" })
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps[2].outputs.success).toBeTrue()
|
expect(results.steps[2].outputs.success).toBeTrue()
|
||||||
expect(results.steps[2].outputs.result).toBeFalse()
|
expect(results.steps[2].outputs.result).toBeFalse()
|
||||||
|
@ -293,12 +268,8 @@ describe("Automation Scenarios", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should continue the automation if the condition is met", async () => {
|
it("should continue the automation if the condition is met", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test Not Equal",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.createRow({
|
.createRow({
|
||||||
row: {
|
row: {
|
||||||
name: "Not Equal Test",
|
name: "Not Equal Test",
|
||||||
|
@ -315,7 +286,7 @@ describe("Automation Scenarios", () => {
|
||||||
value: 20,
|
value: 20,
|
||||||
})
|
})
|
||||||
.serverLog({ text: "Not Equal condition met" })
|
.serverLog({ text: "Not Equal condition met" })
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps[2].outputs.success).toBeTrue()
|
expect(results.steps[2].outputs.success).toBeTrue()
|
||||||
expect(results.steps[2].outputs.result).toBeTrue()
|
expect(results.steps[2].outputs.result).toBeTrue()
|
||||||
|
@ -364,12 +335,8 @@ describe("Automation Scenarios", () => {
|
||||||
it.each(testCases)(
|
it.each(testCases)(
|
||||||
"should pass the filter when condition is $condition",
|
"should pass the filter when condition is $condition",
|
||||||
async ({ condition, value, rowValue, expectPass }) => {
|
async ({ condition, value, rowValue, expectPass }) => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: `Test ${condition}`,
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.createRow({
|
.createRow({
|
||||||
row: {
|
row: {
|
||||||
name: `${condition} Test`,
|
name: `${condition} Test`,
|
||||||
|
@ -388,7 +355,7 @@ describe("Automation Scenarios", () => {
|
||||||
.serverLog({
|
.serverLog({
|
||||||
text: `${condition} condition ${expectPass ? "passed" : "failed"}`,
|
text: `${condition} condition ${expectPass ? "passed" : "failed"}`,
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps[2].outputs.result).toBe(expectPass)
|
expect(results.steps[2].outputs.result).toBe(expectPass)
|
||||||
if (expectPass) {
|
if (expectPass) {
|
||||||
|
@ -401,35 +368,24 @@ describe("Automation Scenarios", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Check user is passed through from row trigger", async () => {
|
it("Check user is passed through from row trigger", async () => {
|
||||||
const table = await config.createTable()
|
const table = await config.api.table.save(basicTable())
|
||||||
|
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test a user is successfully passed from the trigger",
|
.onRowUpdated({ tableId: table._id! })
|
||||||
})
|
.serverLog({ text: "{{ [user].[email] }}" })
|
||||||
|
.test({
|
||||||
const results = await builder
|
|
||||||
.rowUpdated(
|
|
||||||
{ tableId: table._id! },
|
|
||||||
{
|
|
||||||
row: { name: "Test", description: "TEST" },
|
row: { name: "Test", description: "TEST" },
|
||||||
id: "1234",
|
id: "1234",
|
||||||
}
|
})
|
||||||
)
|
|
||||||
.serverLog({ text: "{{ [user].[email] }}" })
|
|
||||||
.run()
|
|
||||||
|
|
||||||
expect(results.steps[0].outputs.message).toContain("example.com")
|
expect(results.steps[0].outputs.message).toContain("example.com")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Check user is passed through from app trigger", async () => {
|
it("Check user is passed through from app trigger", async () => {
|
||||||
const builder = createAutomationBuilder({
|
const results = await createAutomationBuilder(config)
|
||||||
name: "Test a user is successfully passed from the trigger",
|
.onAppAction()
|
||||||
})
|
|
||||||
|
|
||||||
const results = await builder
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.serverLog({ text: "{{ [user].[email] }}" })
|
.serverLog({ text: "{{ [user].[email] }}" })
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps[0].outputs.message).toContain("example.com")
|
expect(results.steps[0].outputs.message).toContain("example.com")
|
||||||
})
|
})
|
||||||
|
@ -449,7 +405,8 @@ if (descriptions.length) {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should query an external database for some data then insert than into an internal table", async () => {
|
it("should query an external database for some data then insert than into an internal table", async () => {
|
||||||
const newTable = await config.createTable({
|
const newTable = await config.api.table.save({
|
||||||
|
...basicTable(),
|
||||||
name: "table",
|
name: "table",
|
||||||
type: "table",
|
type: "table",
|
||||||
schema: {
|
schema: {
|
||||||
|
@ -484,22 +441,21 @@ if (descriptions.length) {
|
||||||
|
|
||||||
await client(tableName).insert(rows)
|
await client(tableName).insert(rows)
|
||||||
|
|
||||||
const query = await setup.saveTestQuery(
|
const query = await config.api.query.save({
|
||||||
config,
|
name: "test query",
|
||||||
client,
|
datasourceId: datasource._id!,
|
||||||
tableName,
|
parameters: [],
|
||||||
datasource
|
fields: {
|
||||||
)
|
sql: client(tableName).select("*").toSQL().toNative().sql,
|
||||||
|
},
|
||||||
const builder = createAutomationBuilder({
|
transformer: "",
|
||||||
name: "Test external query and save",
|
schema: {},
|
||||||
config,
|
readable: true,
|
||||||
|
queryVerb: "read",
|
||||||
})
|
})
|
||||||
|
|
||||||
const results = await builder
|
const results = await createAutomationBuilder(config)
|
||||||
.appAction({
|
.onAppAction()
|
||||||
fields: {},
|
|
||||||
})
|
|
||||||
.executeQuery({
|
.executeQuery({
|
||||||
query: {
|
query: {
|
||||||
queryId: query._id!,
|
queryId: query._id!,
|
||||||
|
@ -519,7 +475,7 @@ if (descriptions.length) {
|
||||||
.queryRows({
|
.queryRows({
|
||||||
tableId: newTable._id!,
|
tableId: newTable._id!,
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(results.steps).toHaveLength(3)
|
expect(results.steps).toHaveLength(3)
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities"
|
|
||||||
|
|
||||||
describe("test the server log action", () => {
|
|
||||||
let config = getConfig()
|
|
||||||
let inputs: any
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
inputs = {
|
|
||||||
text: "log message",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
afterAll(_afterAll)
|
|
||||||
|
|
||||||
it("should be able to log the text", async () => {
|
|
||||||
let res = await runStep(config, actions.SERVER_LOG.stepId, inputs)
|
|
||||||
expect(res.message).toEqual(`App ${config.getAppId()} - ${inputs.text}`)
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,31 +1,31 @@
|
||||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
import * as automation from "../index"
|
import * as automation from "../../index"
|
||||||
import * as setup from "./utilities"
|
|
||||||
import { Table } from "@budibase/types"
|
import { Table } from "@budibase/types"
|
||||||
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
|
import { basicTable } from "../../../tests/utilities/structures"
|
||||||
|
|
||||||
describe("Execute Bash Automations", () => {
|
describe("Execute Bash Automations", () => {
|
||||||
let config = setup.getConfig(),
|
const config = new TestConfiguration()
|
||||||
table: Table
|
let table: Table
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await automation.init()
|
await automation.init()
|
||||||
await config.init()
|
await config.init()
|
||||||
table = await config.createTable()
|
table = await config.api.table.save(basicTable())
|
||||||
await config.createRow({
|
await config.api.row.save(table._id!, {
|
||||||
name: "test row",
|
name: "test row",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
tableId: table._id!,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(() => {
|
||||||
|
automation.shutdown()
|
||||||
|
config.end()
|
||||||
|
})
|
||||||
|
|
||||||
it("should use trigger data in bash command and pass output to subsequent steps", async () => {
|
it("should use trigger data in bash command and pass output to subsequent steps", async () => {
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Bash with Trigger Data",
|
.onAppAction()
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: { command: "hello world" } })
|
|
||||||
.bash(
|
.bash(
|
||||||
{ code: "echo '{{ trigger.fields.command }}'" },
|
{ code: "echo '{{ trigger.fields.command }}'" },
|
||||||
{ stepName: "Echo Command" }
|
{ stepName: "Echo Command" }
|
||||||
|
@ -34,7 +34,7 @@ describe("Execute Bash Automations", () => {
|
||||||
{ text: "Bash output was: {{ steps.[Echo Command].stdout }}" },
|
{ text: "Bash output was: {{ steps.[Echo Command].stdout }}" },
|
||||||
{ stepName: "Log Output" }
|
{ stepName: "Log Output" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: { command: "hello world" } })
|
||||||
|
|
||||||
expect(result.steps[0].outputs.stdout).toEqual("hello world\n")
|
expect(result.steps[0].outputs.stdout).toEqual("hello world\n")
|
||||||
expect(result.steps[1].outputs.message).toContain(
|
expect(result.steps[1].outputs.message).toContain(
|
||||||
|
@ -43,11 +43,8 @@ describe("Execute Bash Automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should chain multiple bash commands using previous outputs", async () => {
|
it("should chain multiple bash commands using previous outputs", async () => {
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Chained Bash Commands",
|
.onAppAction()
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: { filename: "testfile.txt" } })
|
|
||||||
.bash(
|
.bash(
|
||||||
{ code: "echo 'initial content' > {{ trigger.fields.filename }}" },
|
{ code: "echo 'initial content' > {{ trigger.fields.filename }}" },
|
||||||
{ stepName: "Create File" }
|
{ stepName: "Create File" }
|
||||||
|
@ -60,18 +57,15 @@ describe("Execute Bash Automations", () => {
|
||||||
{ code: "rm {{ trigger.fields.filename }}" },
|
{ code: "rm {{ trigger.fields.filename }}" },
|
||||||
{ stepName: "Cleanup" }
|
{ stepName: "Cleanup" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: { filename: "testfile.txt" } })
|
||||||
|
|
||||||
expect(result.steps[1].outputs.stdout).toEqual("INITIAL CONTENT\n")
|
expect(result.steps[1].outputs.stdout).toEqual("INITIAL CONTENT\n")
|
||||||
expect(result.steps[1].outputs.success).toEqual(true)
|
expect(result.steps[1].outputs.success).toEqual(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should integrate bash output with row operations", async () => {
|
it("should integrate bash output with row operations", async () => {
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Bash with Row Operations",
|
.onAppAction()
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.queryRows(
|
.queryRows(
|
||||||
{
|
{
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
|
@ -89,7 +83,7 @@ describe("Execute Bash Automations", () => {
|
||||||
{ text: "{{ steps.[Process Row Data].stdout }}" },
|
{ text: "{{ steps.[Process Row Data].stdout }}" },
|
||||||
{ stepName: "Log Result" }
|
{ stepName: "Log Result" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(result.steps[1].outputs.stdout).toContain(
|
expect(result.steps[1].outputs.stdout).toContain(
|
||||||
"Row data: test row - test description"
|
"Row data: test row - test description"
|
||||||
|
@ -100,11 +94,8 @@ describe("Execute Bash Automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle bash output in conditional logic", async () => {
|
it("should handle bash output in conditional logic", async () => {
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Bash with Conditional",
|
.onAppAction()
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: { threshold: "5" } })
|
|
||||||
.bash(
|
.bash(
|
||||||
{ code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" },
|
{ code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" },
|
||||||
{ stepName: "Calculate Value" }
|
{ stepName: "Calculate Value" }
|
||||||
|
@ -122,7 +113,7 @@ describe("Execute Bash Automations", () => {
|
||||||
{ text: "Value was {{ steps.[Check Value].value }}" },
|
{ text: "Value was {{ steps.[Check Value].value }}" },
|
||||||
{ stepName: "Log Result" }
|
{ stepName: "Log Result" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: { threshold: "5" } })
|
||||||
|
|
||||||
expect(result.steps[0].outputs.stdout).toEqual("10\n")
|
expect(result.steps[0].outputs.stdout).toEqual("10\n")
|
||||||
expect(result.steps[1].outputs.value).toEqual("high")
|
expect(result.steps[1].outputs.value).toEqual("high")
|
||||||
|
@ -130,17 +121,14 @@ describe("Execute Bash Automations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle null values gracefully", async () => {
|
it("should handle null values gracefully", async () => {
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Null Bash Input",
|
.onAppAction()
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: {} })
|
|
||||||
.bash(
|
.bash(
|
||||||
//@ts-ignore
|
// @ts-expect-error - testing null input
|
||||||
{ code: null },
|
{ code: null },
|
||||||
{ stepName: "Null Command" }
|
{ stepName: "Null Command" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: {} })
|
||||||
|
|
||||||
expect(result.steps[0].outputs.stdout).toBe(
|
expect(result.steps[0].outputs.stdout).toBe(
|
||||||
"Budibase bash automation failed: Invalid inputs"
|
"Budibase bash automation failed: Invalid inputs"
|
|
@ -1,7 +1,11 @@
|
||||||
import * as setup from "./utilities"
|
import {
|
||||||
import { basicTableWithAttachmentField } from "../../tests/utilities/structures"
|
basicTable,
|
||||||
|
basicTableWithAttachmentField,
|
||||||
|
} from "../../../tests/utilities/structures"
|
||||||
import { objectStore } from "@budibase/backend-core"
|
import { objectStore } from "@budibase/backend-core"
|
||||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||||
|
import { Row, Table } from "@budibase/types"
|
||||||
|
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||||
|
|
||||||
async function uploadTestFile(filename: string) {
|
async function uploadTestFile(filename: string) {
|
||||||
let bucket = "testbucket"
|
let bucket = "testbucket"
|
||||||
|
@ -10,19 +14,20 @@ async function uploadTestFile(filename: string) {
|
||||||
filename,
|
filename,
|
||||||
body: Buffer.from("test data"),
|
body: Buffer.from("test data"),
|
||||||
})
|
})
|
||||||
let presignedUrl = await objectStore.getPresignedUrl(bucket, filename, 60000)
|
let presignedUrl = objectStore.getPresignedUrl(bucket, filename, 60000)
|
||||||
|
|
||||||
return presignedUrl
|
return presignedUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("test the create row action", () => {
|
describe("test the create row action", () => {
|
||||||
let table: any
|
const config = new TestConfiguration()
|
||||||
let row: any
|
|
||||||
let config = setup.getConfig()
|
let table: Table
|
||||||
|
let row: Row
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
table = await config.createTable()
|
table = await config.api.table.save(basicTable())
|
||||||
row = {
|
row = {
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
name: "test",
|
name: "test",
|
||||||
|
@ -30,28 +35,27 @@ describe("test the create row action", () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(() => {
|
||||||
|
config.end()
|
||||||
|
})
|
||||||
|
|
||||||
it("should be able to run the action", async () => {
|
it("should be able to run the action", async () => {
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Test Create Row Flow",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: { status: "new" } })
|
|
||||||
.serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" })
|
.serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" })
|
||||||
.createRow({ row }, { stepName: "CreateRow" })
|
.createRow({ row }, { stepName: "CreateRow" })
|
||||||
.serverLog(
|
.serverLog(
|
||||||
{ text: "Row created with ID: {{ stepsByName.CreateRow.row._id }}" },
|
{ text: "Row created with ID: {{ stepsByName.CreateRow.row._id }}" },
|
||||||
{ stepName: "CreationLog" }
|
{ stepName: "CreationLog" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: { status: "new" } })
|
||||||
|
|
||||||
expect(result.steps[1].outputs.success).toBeDefined()
|
expect(result.steps[1].outputs.success).toBeDefined()
|
||||||
expect(result.steps[1].outputs.id).toBeDefined()
|
expect(result.steps[1].outputs.id).toBeDefined()
|
||||||
expect(result.steps[1].outputs.revision).toBeDefined()
|
expect(result.steps[1].outputs.revision).toBeDefined()
|
||||||
|
|
||||||
const gottenRow = await config.api.row.get(
|
const gottenRow = await config.api.row.get(
|
||||||
table._id,
|
table._id!,
|
||||||
result.steps[1].outputs.id
|
result.steps[1].outputs.id
|
||||||
)
|
)
|
||||||
expect(gottenRow.name).toEqual("test")
|
expect(gottenRow.name).toEqual("test")
|
||||||
|
@ -62,12 +66,8 @@ describe("test the create row action", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return an error (not throw) when bad info provided", async () => {
|
it("should return an error (not throw) when bad info provided", async () => {
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Test Create Row Error Flow",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: { status: "error" } })
|
|
||||||
.serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" })
|
.serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" })
|
||||||
.createRow(
|
.createRow(
|
||||||
{
|
{
|
||||||
|
@ -78,18 +78,14 @@ describe("test the create row action", () => {
|
||||||
},
|
},
|
||||||
{ stepName: "CreateRow" }
|
{ stepName: "CreateRow" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: { status: "error" } })
|
||||||
|
|
||||||
expect(result.steps[1].outputs.success).toEqual(false)
|
expect(result.steps[1].outputs.success).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should check invalid inputs return an error", async () => {
|
it("should check invalid inputs return an error", async () => {
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Test Create Row Invalid Flow",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: { status: "invalid" } })
|
|
||||||
.serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" })
|
.serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" })
|
||||||
.createRow({ row: {} }, { stepName: "CreateRow" })
|
.createRow({ row: {} }, { stepName: "CreateRow" })
|
||||||
.filter({
|
.filter({
|
||||||
|
@ -101,18 +97,18 @@ describe("test the create row action", () => {
|
||||||
{ text: "This log should not appear" },
|
{ text: "This log should not appear" },
|
||||||
{ stepName: "SkippedLog" }
|
{ stepName: "SkippedLog" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: { status: "invalid" } })
|
||||||
|
|
||||||
expect(result.steps[1].outputs.success).toEqual(false)
|
expect(result.steps[1].outputs.success).toEqual(false)
|
||||||
expect(result.steps.length).toBeLessThan(4)
|
expect(result.steps.length).toBeLessThan(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should check that an attachment field is sent to storage and parsed", async () => {
|
it("should check that an attachment field is sent to storage and parsed", async () => {
|
||||||
let attachmentTable = await config.createTable(
|
let attachmentTable = await config.api.table.save(
|
||||||
basicTableWithAttachmentField()
|
basicTableWithAttachmentField()
|
||||||
)
|
)
|
||||||
|
|
||||||
let attachmentRow: any = {
|
let attachmentRow: Row = {
|
||||||
tableId: attachmentTable._id,
|
tableId: attachmentTable._id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,12 +122,8 @@ describe("test the create row action", () => {
|
||||||
]
|
]
|
||||||
|
|
||||||
attachmentRow.file_attachment = attachmentObject
|
attachmentRow.file_attachment = attachmentObject
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Test Create Row Attachment Flow",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: { type: "attachment" } })
|
|
||||||
.serverLog(
|
.serverLog(
|
||||||
{ text: "Processing attachment upload" },
|
{ text: "Processing attachment upload" },
|
||||||
{ stepName: "StartLog" }
|
{ stepName: "StartLog" }
|
||||||
|
@ -148,7 +140,7 @@ describe("test the create row action", () => {
|
||||||
},
|
},
|
||||||
{ stepName: "UploadLog" }
|
{ stepName: "UploadLog" }
|
||||||
)
|
)
|
||||||
.run()
|
.test({ fields: { type: "attachment" } })
|
||||||
|
|
||||||
expect(result.steps[1].outputs.success).toEqual(true)
|
expect(result.steps[1].outputs.success).toEqual(true)
|
||||||
expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key")
|
expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key")
|
||||||
|
@ -165,11 +157,11 @@ describe("test the create row action", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should check that an single attachment field is sent to storage and parsed", async () => {
|
it("should check that an single attachment field is sent to storage and parsed", async () => {
|
||||||
let attachmentTable = await config.createTable(
|
let attachmentTable = await config.api.table.save(
|
||||||
basicTableWithAttachmentField()
|
basicTableWithAttachmentField()
|
||||||
)
|
)
|
||||||
|
|
||||||
let attachmentRow: any = {
|
let attachmentRow: Row = {
|
||||||
tableId: attachmentTable._id,
|
tableId: attachmentTable._id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,12 +173,8 @@ describe("test the create row action", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentRow.single_file_attachment = attachmentObject
|
attachmentRow.single_file_attachment = attachmentObject
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Test Create Row Single Attachment Flow",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: { type: "single-attachment" } })
|
|
||||||
.serverLog(
|
.serverLog(
|
||||||
{ text: "Processing single attachment" },
|
{ text: "Processing single attachment" },
|
||||||
{ stepName: "StartLog" }
|
{ stepName: "StartLog" }
|
||||||
|
@ -221,7 +209,7 @@ describe("test the create row action", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: { type: "single-attachment" } })
|
||||||
|
|
||||||
expect(result.steps[1].outputs.success).toEqual(true)
|
expect(result.steps[1].outputs.success).toEqual(true)
|
||||||
expect(result.steps[1].outputs.row.single_file_attachment).toHaveProperty(
|
expect(result.steps[1].outputs.row.single_file_attachment).toHaveProperty(
|
||||||
|
@ -240,11 +228,11 @@ describe("test the create row action", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should check that attachment without the correct keys throws an error", async () => {
|
it("should check that attachment without the correct keys throws an error", async () => {
|
||||||
let attachmentTable = await config.createTable(
|
let attachmentTable = await config.api.table.save(
|
||||||
basicTableWithAttachmentField()
|
basicTableWithAttachmentField()
|
||||||
)
|
)
|
||||||
|
|
||||||
let attachmentRow: any = {
|
let attachmentRow: Row = {
|
||||||
tableId: attachmentTable._id,
|
tableId: attachmentTable._id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,12 +244,8 @@ describe("test the create row action", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentRow.single_file_attachment = attachmentObject
|
attachmentRow.single_file_attachment = attachmentObject
|
||||||
const result = await createAutomationBuilder({
|
const result = await createAutomationBuilder(config)
|
||||||
name: "Test Create Row Invalid Attachment Flow",
|
.onAppAction()
|
||||||
appId: config.getAppId(),
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
.appAction({ fields: { type: "invalid-attachment" } })
|
|
||||||
.serverLog(
|
.serverLog(
|
||||||
{ text: "Testing invalid attachment keys" },
|
{ text: "Testing invalid attachment keys" },
|
||||||
{ stepName: "StartLog" }
|
{ stepName: "StartLog" }
|
||||||
|
@ -294,7 +278,7 @@ describe("test the create row action", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.test({ fields: { type: "invalid-attachment" } })
|
||||||
|
|
||||||
expect(result.steps[1].outputs.success).toEqual(false)
|
expect(result.steps[1].outputs.success).toEqual(false)
|
||||||
expect(result.steps[1].outputs.response).toEqual(
|
expect(result.steps[1].outputs.response).toEqual(
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue