Merge master.
This commit is contained in:
commit
e238a9cf02
|
@ -9,4 +9,5 @@ packages/backend-core/coverage
|
|||
packages/builder/.routify
|
||||
packages/sdk/sdk
|
||||
packages/pro/coverage
|
||||
**/*.ivm.bundle.js
|
||||
**/*.ivm.bundle.js
|
||||
!**/bson-polyfills.ivm.bundle.js
|
25
README.md
25
README.md
|
@ -54,17 +54,21 @@
|
|||
</h3>
|
||||
|
||||
<br /><br />
|
||||
|
||||
## ✨ 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.
|
||||
<br /><br />
|
||||
|
||||
### 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.
|
||||
<br /><br />
|
||||
|
||||
### 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).
|
||||
|
||||
<p align="center">
|
||||
|
@ -82,10 +86,12 @@ Budibase comes out of the box with beautifully designed, powerful components whi
|
|||
<br /><br />
|
||||
|
||||
### 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).
|
||||
<br /><br />
|
||||
|
||||
### Integrate with your favorite tools
|
||||
|
||||
Budibase integrates with a number of popular tools allowing you to build apps that perfectly fit your stack.
|
||||
|
||||
<p align="center">
|
||||
|
@ -94,6 +100,7 @@ Budibase integrates with a number of popular tools allowing you to build apps th
|
|||
<br /><br />
|
||||
|
||||
### 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.
|
||||
|
||||
- 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 />
|
||||
|
||||
|
||||
## 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:
|
||||
|
||||
- Budibase as a backend
|
||||
- Interoperability
|
||||
|
||||
|
||||
#### Docs
|
||||
|
||||
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
|
||||
|
@ -132,10 +139,8 @@ Deploy Budibase using Docker, Kubernetes, and Digital Ocean on your existing inf
|
|||
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
|
||||
- [Portainer](https://docs.budibase.com/docs/portainer)
|
||||
|
||||
|
||||
### [Get started with Budibase Cloud](https://budibase.com)
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 🎓 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).
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 💬 Community
|
||||
|
@ -152,25 +156,24 @@ If you have a question or would like to talk with other Budibase users and join
|
|||
|
||||
<br /><br /><br />
|
||||
|
||||
|
||||
## ❗ 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.
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 🙌 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.
|
||||
Environment setup instructions are available [here](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
@ -183,7 +186,6 @@ For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase
|
|||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 📝 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.
|
||||
|
@ -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)):
|
||||
|
||||
|
||||
<a href="https://github.com/Budibase/budibase/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Budibase/budibase" />
|
||||
</a>
|
||||
|
|
|
@ -41,12 +41,11 @@ module.exports = {
|
|||
if (
|
||||
/^@budibase\/[^/]+\/.*$/.test(importPath) &&
|
||||
importPath !== "@budibase/backend-core/tests" &&
|
||||
importPath !== "@budibase/string-templates/test/utils" &&
|
||||
importPath !== "@budibase/client/manifest.json"
|
||||
importPath !== "@budibase/string-templates/test/utils"
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests, @budibase/string-templates/test/utils and @budibase/client/manifest.json.`,
|
||||
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.3.3",
|
||||
"version": "3.4.4",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -83,11 +83,15 @@ export function isViewId(id: string): boolean {
|
|||
/**
|
||||
* Check if a given ID is that of a datasource or datasource plus.
|
||||
*/
|
||||
export const isDatasourceId = (id: string): boolean => {
|
||||
export function isDatasourceId(id: string): boolean {
|
||||
// this covers both datasources and datasource plus
|
||||
return !!id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
|
||||
}
|
||||
|
||||
export function isQueryId(id: string): boolean {
|
||||
return !!id && id.startsWith(`${DocumentType.QUERY}${SEPARATOR}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets parameters for retrieving workspaces.
|
||||
*/
|
||||
|
|
|
@ -1,45 +1,58 @@
|
|||
import events from "events"
|
||||
import { newid } from "../utils"
|
||||
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
|
||||
timestamp: number
|
||||
queue: string
|
||||
queue: Queue<T>
|
||||
data: any
|
||||
opts?: JobOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Bull works with a Job wrapper around all messages that contains a lot more information about
|
||||
* the state of the message, this object constructor implements the same schema of Bull jobs
|
||||
* for the sake of maintaining API consistency.
|
||||
* @param queue The name of the queue which the message will be carried on.
|
||||
* @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.
|
||||
* 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> {
|
||||
_name: string
|
||||
_opts?: QueueOptions
|
||||
_messages: JobMessage[]
|
||||
_queuedJobIds: Set<string>
|
||||
_emitter: NodeJS.EventEmitter
|
||||
_emitter: NodeJS.EventEmitter<{ message: [JobMessage]; completed: [Job] }>
|
||||
_runCount: number
|
||||
_addCount: number
|
||||
|
||||
|
@ -69,34 +82,29 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
*/
|
||||
async process(concurrencyOrFunc: number | any, func?: any) {
|
||||
func = typeof concurrencyOrFunc === "number" ? func : concurrencyOrFunc
|
||||
this._emitter.on("message", async () => {
|
||||
if (this._messages.length <= 0) {
|
||||
return
|
||||
}
|
||||
let msg = this._messages.shift()
|
||||
|
||||
let resp = func(msg)
|
||||
this._emitter.on("message", async message => {
|
||||
let resp = func(message)
|
||||
|
||||
async function retryFunc(fnc: any) {
|
||||
try {
|
||||
await fnc
|
||||
} catch (e: any) {
|
||||
await new Promise<void>(r => setTimeout(() => r(), 50))
|
||||
|
||||
await retryFunc(func(msg))
|
||||
await helpers.wait(50)
|
||||
await retryFunc(func(message))
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.then != null) {
|
||||
try {
|
||||
await retryFunc(resp)
|
||||
this._emitter.emit("completed", message as Job)
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
this._runCount++
|
||||
const jobId = msg?.opts?.jobId?.toString()
|
||||
if (jobId && msg?.opts?.removeOnComplete) {
|
||||
const jobId = message.opts?.jobId?.toString()
|
||||
if (jobId && message.opts?.removeOnComplete) {
|
||||
this._queuedJobIds.delete(jobId)
|
||||
}
|
||||
})
|
||||
|
@ -130,9 +138,16 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
}
|
||||
|
||||
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._emitter.emit("message")
|
||||
this._emitter.emit("message", message)
|
||||
}
|
||||
|
||||
const delay = opts?.delay
|
||||
|
@ -158,13 +173,6 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
console.log(cronJobId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented for tests
|
||||
*/
|
||||
async getRepeatableJobs() {
|
||||
return []
|
||||
}
|
||||
|
||||
async removeJobs(_pattern: string) {
|
||||
// no-op
|
||||
}
|
||||
|
@ -176,13 +184,31 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
return []
|
||||
}
|
||||
|
||||
async getJob() {
|
||||
async getJob(id: JobId) {
|
||||
for (const message of this._messages) {
|
||||
if (message.id === id) {
|
||||
return message as Job
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
on() {
|
||||
// do nothing
|
||||
return this as any
|
||||
on(event: string, callback: (...args: any[]) => void): Queue {
|
||||
// @ts-expect-error - this callback can be one of many types
|
||||
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)) {
|
||||
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 merge from "lodash/merge"
|
||||
|
||||
let CLOUD_FREE_LICENSE: License
|
||||
let UNLIMITED_LICENSE: License
|
||||
|
@ -27,18 +34,19 @@ export function initInternal(opts: {
|
|||
|
||||
export interface UseLicenseOpts {
|
||||
features?: Feature[]
|
||||
quotas?: Quotas
|
||||
monthlyQuotas?: [MonthlyQuotaName, number][]
|
||||
}
|
||||
|
||||
// LICENSES
|
||||
|
||||
export const useLicense = (license: License, opts?: UseLicenseOpts) => {
|
||||
if (opts) {
|
||||
if (opts.features) {
|
||||
license.features.push(...opts.features)
|
||||
}
|
||||
if (opts.quotas) {
|
||||
license.quotas = opts.quotas
|
||||
if (opts?.features) {
|
||||
license.features.push(...opts.features)
|
||||
}
|
||||
if (opts?.monthlyQuotas) {
|
||||
for (const [name, value] of opts.monthlyQuotas) {
|
||||
license.quotas[QuotaType.USAGE][QuotaUsageType.MONTHLY][name].value =
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,12 +65,9 @@ export const useCloudFree = () => {
|
|||
|
||||
// FEATURES
|
||||
|
||||
const useFeature = (feature: Feature) => {
|
||||
const useFeature = (feature: Feature, extra?: Partial<UseLicenseOpts>) => {
|
||||
const license = cloneDeep(getCachedLicense() || UNLIMITED_LICENSE)
|
||||
const opts: UseLicenseOpts = {
|
||||
features: [feature],
|
||||
}
|
||||
|
||||
const opts: UseLicenseOpts = merge({ features: [feature] }, extra)
|
||||
return useLicense(license, opts)
|
||||
}
|
||||
|
||||
|
@ -102,8 +107,12 @@ export const useAppBuilders = () => {
|
|||
return useFeature(Feature.APP_BUILDERS)
|
||||
}
|
||||
|
||||
export const useBudibaseAI = () => {
|
||||
return useFeature(Feature.BUDIBASE_AI)
|
||||
export const useBudibaseAI = (opts?: { monthlyQuota?: number }) => {
|
||||
return useFeature(Feature.BUDIBASE_AI, {
|
||||
monthlyQuotas: [
|
||||
[MonthlyQuotaName.BUDIBASE_AI_CREDITS, opts?.monthlyQuota || 1000],
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const useAICustomConfigs = () => {
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
"dayjs": "^1.10.8",
|
||||
"easymde": "^2.16.1",
|
||||
"svelte-dnd-action": "^0.9.8",
|
||||
"svelte-portal": "^1.0.0"
|
||||
"svelte-portal": "^2.2.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"loader-utils": "1.4.1"
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "@spectrum-css/actionbutton/dist/index-vars.css"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
import { hexToRGBA } from "../helpers"
|
||||
|
||||
export let quiet = false
|
||||
export let selected = false
|
||||
export let disabled = false
|
||||
export let icon = ""
|
||||
export let size = "M"
|
||||
export let active = false
|
||||
export let fullWidth = false
|
||||
export let noPadding = false
|
||||
export let tooltip = ""
|
||||
export let accentColor = null
|
||||
export let quiet: boolean = false
|
||||
export let selected: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let icon: string = ""
|
||||
export let size: "S" | "M" | "L" = "M"
|
||||
export let active: boolean = false
|
||||
export let fullWidth: boolean = false
|
||||
export let noPadding: boolean = false
|
||||
export let tooltip: string = ""
|
||||
export let accentColor: string | null = null
|
||||
|
||||
let showTooltip = false
|
||||
|
||||
$: accentStyle = getAccentStyle(accentColor)
|
||||
|
||||
const getAccentStyle = color => {
|
||||
const getAccentStyle = (color: string | null) => {
|
||||
if (!color) {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -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
|
||||
const ignoredClasses = [
|
||||
".download-js-link",
|
||||
|
@ -14,18 +28,20 @@ const conditionallyIgnoredClasses = [
|
|||
".drawer-wrapper",
|
||||
".spectrum-Popover",
|
||||
]
|
||||
let clickHandlers = []
|
||||
let candidateTarget
|
||||
let clickHandlers: Handler[] = []
|
||||
let candidateTarget: HTMLElement | undefined
|
||||
|
||||
// Processes a "click outside" event and invokes callbacks if our source element
|
||||
// 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
|
||||
if (event.target.closest('[data-ignore-click-outside="true"]')) {
|
||||
if (target.closest('[data-ignore-click-outside="true"]')) {
|
||||
return
|
||||
}
|
||||
for (let className of ignoredClasses) {
|
||||
if (event.target.closest(className)) {
|
||||
if (target.closest(className)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -33,41 +49,41 @@ const handleClick = event => {
|
|||
// Process handlers
|
||||
clickHandlers.forEach(handler => {
|
||||
// Check that the click isn't inside the target
|
||||
if (handler.element.contains(event.target)) {
|
||||
if (handler.element.contains(target)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore clicks for certain classes unless we're nested inside them
|
||||
for (let className of conditionallyIgnoredClasses) {
|
||||
const sourceInside = handler.anchor.closest(className) != null
|
||||
const clickInside = event.target.closest(className) != null
|
||||
const clickInside = target.closest(className) != null
|
||||
if (clickInside && !sourceInside) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handler.callback?.(event)
|
||||
handler.callback?.(e)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
// we get annoying callbacks firing when we drag to select text.
|
||||
const handleMouseUp = e => {
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
if (candidateTarget === e.target) {
|
||||
handleClick(e)
|
||||
}
|
||||
candidateTarget = null
|
||||
candidateTarget = undefined
|
||||
}
|
||||
|
||||
// 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.
|
||||
// We handle context menu (right click) events in another handler.
|
||||
if (e.button !== 0) {
|
||||
return
|
||||
}
|
||||
candidateTarget = e.target
|
||||
candidateTarget = e.target as HTMLElement
|
||||
|
||||
// Clear any previous listeners in case of multiple down events, and register
|
||||
// a single mouse up listener
|
||||
|
@ -75,14 +91,29 @@ const handleMouseDown = e => {
|
|||
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
|
||||
document.addEventListener("mousedown", handleMouseDown)
|
||||
document.addEventListener("contextmenu", handleClick)
|
||||
window.addEventListener("blur", handleBlur)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
if (!existingHandler) {
|
||||
clickHandlers.push({ id, element, anchor, callback })
|
||||
|
@ -94,27 +125,52 @@ const updateHandler = (id, element, anchor, callback) => {
|
|||
/**
|
||||
* Removes a click handler
|
||||
*/
|
||||
const removeHandler = id => {
|
||||
const removeHandler = (id: number) => {
|
||||
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
|
||||
* component being observed. This is required for things like popovers, where
|
||||
* 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
|
||||
* 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 update = newOpts => {
|
||||
const callback =
|
||||
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
|
||||
const anchor = newOpts?.anchor || element
|
||||
|
||||
const isCallback = (
|
||||
opts?: ClickOutsideOpts | ClickOutsideCallback
|
||||
): 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)
|
||||
}
|
||||
|
||||
update(opts)
|
||||
|
||||
return {
|
||||
update,
|
||||
destroy: () => removeHandler(id),
|
|
@ -1,13 +1,7 @@
|
|||
/**
|
||||
* Valid alignment options are
|
||||
* - left
|
||||
* - right
|
||||
* - left-outside
|
||||
* - right-outside
|
||||
**/
|
||||
|
||||
// Strategies are defined as [Popover]To[Anchor].
|
||||
// They can apply for both horizontal and vertical alignment.
|
||||
import { PopoverAlignment } from "../constants"
|
||||
|
||||
type Strategy =
|
||||
| "StartToStart"
|
||||
| "EndToEnd"
|
||||
|
@ -33,7 +27,7 @@ export type UpdateHandler = (
|
|||
|
||||
interface Opts {
|
||||
anchor?: HTMLElement
|
||||
align: string
|
||||
align: PopoverAlignment
|
||||
maxHeight?: number
|
||||
maxWidth?: number
|
||||
minWidth?: number
|
||||
|
@ -174,24 +168,33 @@ export default function positionDropdown(element: HTMLElement, opts: Opts) {
|
|||
}
|
||||
|
||||
// Determine X strategy
|
||||
if (align === "right") {
|
||||
if (align === PopoverAlignment.Right) {
|
||||
applyXStrategy("EndToEnd")
|
||||
} else if (align === "right-outside" || align === "right-context-menu") {
|
||||
} else if (
|
||||
align === PopoverAlignment.RightOutside ||
|
||||
align === PopoverAlignment.RightContextMenu
|
||||
) {
|
||||
applyXStrategy("StartToEnd")
|
||||
} else if (align === "left-outside" || align === "left-context-menu") {
|
||||
} else if (
|
||||
align === PopoverAlignment.LeftOutside ||
|
||||
align === PopoverAlignment.LeftContextMenu
|
||||
) {
|
||||
applyXStrategy("EndToStart")
|
||||
} else if (align === "center") {
|
||||
} else if (align === PopoverAlignment.Center) {
|
||||
applyXStrategy("MidPoint")
|
||||
} else {
|
||||
applyXStrategy("StartToStart")
|
||||
}
|
||||
|
||||
// Determine Y strategy
|
||||
if (align === "right-outside" || align === "left-outside") {
|
||||
if (
|
||||
align === PopoverAlignment.RightOutside ||
|
||||
align === PopoverAlignment.LeftOutside
|
||||
) {
|
||||
applyYStrategy("MidPoint")
|
||||
} else if (
|
||||
align === "right-context-menu" ||
|
||||
align === "left-context-menu"
|
||||
align === PopoverAlignment.RightContextMenu ||
|
||||
align === PopoverAlignment.LeftContextMenu
|
||||
) {
|
||||
applyYStrategy("StartToStart")
|
||||
if (styles.top) {
|
||||
|
@ -204,11 +207,11 @@ export default function positionDropdown(element: HTMLElement, opts: Opts) {
|
|||
// Handle screen overflow
|
||||
if (doesXOverflow()) {
|
||||
// Swap left to right
|
||||
if (align === "left") {
|
||||
if (align === PopoverAlignment.Left) {
|
||||
applyXStrategy("EndToEnd")
|
||||
}
|
||||
// Swap right-outside to left-outside
|
||||
else if (align === "right-outside") {
|
||||
else if (align === PopoverAlignment.RightOutside) {
|
||||
applyXStrategy("EndToStart")
|
||||
}
|
||||
}
|
||||
|
@ -225,10 +228,13 @@ export default function positionDropdown(element: HTMLElement, opts: Opts) {
|
|||
applyXStrategy("EndToStart")
|
||||
}
|
||||
}
|
||||
// Othewise invert as normal
|
||||
// Otherwise invert as normal
|
||||
else {
|
||||
// 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")
|
||||
}
|
||||
// Otherwise flip above
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "@spectrum-css/divider/dist/index-vars.css"
|
||||
|
||||
export let size = "M"
|
||||
export let size: "S" | "M" | "L" = "M"
|
||||
|
||||
export let vertical = false
|
||||
export let noMargin = false
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
import { createEventDispatcher } from "svelte"
|
||||
import clickOutside from "../../Actions/click_outside"
|
||||
import Popover from "../../Popover/Popover.svelte"
|
||||
import { PopoverAlignment } from "../../constants"
|
||||
|
||||
export let value: string | undefined = undefined
|
||||
export let id: string | undefined = undefined
|
||||
|
@ -97,11 +98,16 @@
|
|||
<Popover
|
||||
{anchor}
|
||||
{open}
|
||||
align="left"
|
||||
align={PopoverAlignment.Left}
|
||||
on:close={() => (open = false)}
|
||||
useAnchorWidth
|
||||
>
|
||||
<div class="popover-content" use:clickOutside={() => (open = false)}>
|
||||
<div
|
||||
class="popover-content"
|
||||
use:clickOutside={() => {
|
||||
open = false
|
||||
}}
|
||||
>
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
{#if options && Array.isArray(options)}
|
||||
{#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/popover/dist/index-vars.css"
|
||||
import "@spectrum-css/menu/dist/index-vars.css"
|
||||
|
@ -11,44 +16,55 @@
|
|||
import Tags from "../../Tags/Tags.svelte"
|
||||
import Tag from "../../Tags/Tag.svelte"
|
||||
import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte"
|
||||
import { PopoverAlignment } from "../../constants"
|
||||
|
||||
export let id = null
|
||||
export let disabled = false
|
||||
export let fieldText = ""
|
||||
export let fieldIcon = ""
|
||||
export let fieldColour = ""
|
||||
export let isPlaceholder = false
|
||||
export let placeholderOption = null
|
||||
export let options = []
|
||||
export let isOptionSelected = () => false
|
||||
export let isOptionEnabled = () => true
|
||||
export let onSelectOption = () => {}
|
||||
export let getOptionLabel = option => option
|
||||
export let getOptionValue = option => option
|
||||
export let getOptionIcon = () => null
|
||||
export let id: string | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
export let fieldText: string = ""
|
||||
export let fieldIcon: string = ""
|
||||
export let fieldColour: string = ""
|
||||
export let isPlaceholder: boolean = false
|
||||
export let placeholderOption: string | undefined | boolean = undefined
|
||||
export let options: O[] = []
|
||||
export let isOptionSelected = (option: O) => option as unknown as boolean
|
||||
export let isOptionEnabled = (option: O, _index?: number) =>
|
||||
option as unknown as boolean
|
||||
export let onSelectOption: (_value: V) => void = () => {}
|
||||
export let getOptionLabel = (option: O, _index?: number) => `${option}`
|
||||
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 getOptionColour = () => null
|
||||
export let getOptionSubtitle = () => null
|
||||
export let open = false
|
||||
export let readonly = false
|
||||
export let quiet = false
|
||||
export let autoWidth = false
|
||||
export let autocomplete = false
|
||||
export let sort = false
|
||||
export let searchTerm = null
|
||||
export let customPopoverHeight
|
||||
export let align = "left"
|
||||
export let footer = null
|
||||
export let customAnchor = null
|
||||
export let filter = true
|
||||
export let loading
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
export let open: boolean = false
|
||||
export let readonly: boolean = false
|
||||
export let quiet: boolean = false
|
||||
export let autoWidth: boolean | undefined = false
|
||||
export let autocomplete: boolean = false
|
||||
export let sort: boolean = false
|
||||
export let searchTerm: string | null = null
|
||||
export let customPopoverHeight: string | undefined = undefined
|
||||
export let align: PopoverAlignment | undefined = PopoverAlignment.Left
|
||||
export let footer: string | undefined = undefined
|
||||
export let customAnchor: HTMLElement | undefined = undefined
|
||||
export let loading: boolean = false
|
||||
export let onOptionMouseenter: (
|
||||
_e: MouseEvent,
|
||||
_option: any
|
||||
) => void = () => {}
|
||||
export let onOptionMouseleave: (
|
||||
_e: MouseEvent,
|
||||
_option: any
|
||||
) => void = () => {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let button
|
||||
let component
|
||||
let button: any
|
||||
let component: any
|
||||
|
||||
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
|
||||
$: filteredOptions = getFilteredOptions(
|
||||
|
@ -57,7 +73,7 @@
|
|||
getOptionLabel
|
||||
)
|
||||
|
||||
const onClick = e => {
|
||||
const onClick = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
dispatch("click")
|
||||
|
@ -68,7 +84,11 @@
|
|||
open = !open
|
||||
}
|
||||
|
||||
const getSortedOptions = (options, getLabel, sort) => {
|
||||
const getSortedOptions = (
|
||||
options: any[],
|
||||
getLabel: (_option: any) => string,
|
||||
sort: boolean
|
||||
) => {
|
||||
if (!options?.length || !Array.isArray(options)) {
|
||||
return []
|
||||
}
|
||||
|
@ -82,17 +102,21 @@
|
|||
})
|
||||
}
|
||||
|
||||
const getFilteredOptions = (options, term, getLabel) => {
|
||||
if (autocomplete && term && filter) {
|
||||
const getFilteredOptions = (
|
||||
options: any[],
|
||||
term: string | null,
|
||||
getLabel: (_option: any) => string
|
||||
) => {
|
||||
if (autocomplete && term) {
|
||||
const lowerCaseTerm = term.toLowerCase()
|
||||
return options.filter(option => {
|
||||
return options.filter((option: any) => {
|
||||
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
const onScroll = e => {
|
||||
const onScroll = (e: any) => {
|
||||
const scrollPxThreshold = 100
|
||||
const scrollPositionFromBottom =
|
||||
e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop
|
||||
|
@ -152,18 +176,20 @@
|
|||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<Popover
|
||||
anchor={customAnchor ? customAnchor : button}
|
||||
align={align || "left"}
|
||||
align={align || PopoverAlignment.Left}
|
||||
{open}
|
||||
on:close={() => (open = false)}
|
||||
useAnchorWidth={!autoWidth}
|
||||
maxWidth={autoWidth ? 400 : null}
|
||||
maxWidth={autoWidth ? 400 : undefined}
|
||||
customHeight={customPopoverHeight}
|
||||
maxHeight={360}
|
||||
>
|
||||
<div
|
||||
class="popover-content"
|
||||
class:auto-width={autoWidth}
|
||||
use:clickOutside={() => (open = false)}
|
||||
use:clickOutside={() => {
|
||||
open = false
|
||||
}}
|
||||
>
|
||||
{#if autocomplete}
|
||||
<Search
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "@spectrum-css/search/dist/index-vars.css"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let value = null
|
||||
export let placeholder = null
|
||||
export let value: any = null
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let disabled = false
|
||||
export let id = null
|
||||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
export let inputRef
|
||||
export let inputRef: HTMLElement | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let focus = false
|
||||
|
||||
const updateValue = value => {
|
||||
const updateValue = (value: any) => {
|
||||
dispatch("change", value)
|
||||
}
|
||||
|
||||
|
@ -21,19 +21,19 @@
|
|||
focus = true
|
||||
}
|
||||
|
||||
const onBlur = event => {
|
||||
const onBlur = (event: any) => {
|
||||
focus = false
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const onInput = event => {
|
||||
const onInput = (event: any) => {
|
||||
if (!updateOnChange) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
const updateValueOnEnter = (event: any) => {
|
||||
if (event.key === "Enter") {
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
|
|
@ -1,34 +1,44 @@
|
|||
<script>
|
||||
<script lang="ts" context="module">
|
||||
type O = any
|
||||
type V = any
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Picker from "./Picker.svelte"
|
||||
import { PopoverAlignment } from "../../constants"
|
||||
|
||||
export let value = null
|
||||
export let id = null
|
||||
export let placeholder = "Choose an option"
|
||||
export let disabled = false
|
||||
export let options = []
|
||||
export let getOptionLabel = option => option
|
||||
export let getOptionValue = option => option
|
||||
export let getOptionIcon = () => null
|
||||
export let getOptionColour = () => null
|
||||
export let getOptionSubtitle = () => null
|
||||
export let compare = null
|
||||
export let value: V | null = null
|
||||
export let id: string | undefined = undefined
|
||||
export let placeholder: string | boolean = "Choose an option"
|
||||
export let disabled: boolean = false
|
||||
export let options: O[] = []
|
||||
export let getOptionLabel = (option: O, _index?: number) => `${option}`
|
||||
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 compare = (option: O, value: V) => option === value
|
||||
export let useOptionIconImage = false
|
||||
export let isOptionEnabled
|
||||
export let readonly = false
|
||||
export let quiet = false
|
||||
export let autoWidth = false
|
||||
export let autocomplete = false
|
||||
export let sort = false
|
||||
export let align
|
||||
export let footer = null
|
||||
export let open = false
|
||||
export let tag = null
|
||||
export let searchTerm = null
|
||||
export let filter = true
|
||||
export let loading
|
||||
export let isOptionEnabled = (option: O, _index?: number) =>
|
||||
option as unknown as boolean
|
||||
export let readonly: boolean = false
|
||||
export let quiet: boolean = false
|
||||
export let autoWidth: boolean = false
|
||||
export let autocomplete: boolean = false
|
||||
export let sort: boolean = false
|
||||
export let align: PopoverAlignment | undefined = PopoverAlignment.Left
|
||||
export let footer: string | undefined = undefined
|
||||
export let open: boolean = false
|
||||
export let searchTerm: string | undefined = undefined
|
||||
export let loading: boolean | undefined = undefined
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
export let customPopoverHeight: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -36,24 +46,28 @@
|
|||
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
||||
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||
|
||||
function compareOptionAndValue(option, value) {
|
||||
function compareOptionAndValue(option: O, value: V) {
|
||||
return typeof compare === "function"
|
||||
? compare(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
|
||||
if (!options?.length) {
|
||||
return ""
|
||||
}
|
||||
const index = options.findIndex((option, idx) =>
|
||||
const index = options.findIndex((option: any, idx: number) =>
|
||||
compareOptionAndValue(getOptionValue(option, idx), value)
|
||||
)
|
||||
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 === "") {
|
||||
// Explicit false means use no placeholder and allow an empty fields
|
||||
if (placeholder === false) {
|
||||
|
@ -68,7 +82,7 @@
|
|||
)
|
||||
}
|
||||
|
||||
const selectOption = value => {
|
||||
const selectOption = (value: V) => {
|
||||
dispatch("change", value)
|
||||
open = false
|
||||
}
|
||||
|
@ -98,16 +112,15 @@
|
|||
{useOptionIconImage}
|
||||
{isOptionEnabled}
|
||||
{autocomplete}
|
||||
{filter}
|
||||
{sort}
|
||||
{tag}
|
||||
{onOptionMouseenter}
|
||||
{onOptionMouseleave}
|
||||
isPlaceholder={value == null || value === ""}
|
||||
placeholderOption={placeholder === false
|
||||
? null
|
||||
? undefined
|
||||
: placeholder || "Choose an option"}
|
||||
isOptionSelected={option => compareOptionAndValue(option, value)}
|
||||
onSelectOption={selectOption}
|
||||
{loading}
|
||||
{customPopoverHeight}
|
||||
/>
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||
import { createEventDispatcher, onMount, tick } from "svelte"
|
||||
import type { UIEvent } from "@budibase/types"
|
||||
|
||||
export let value = null
|
||||
export let placeholder = null
|
||||
export let value: string | null = null
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let type = "text"
|
||||
export let disabled = false
|
||||
export let id = null
|
||||
export let readonly = false
|
||||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
export let align
|
||||
export let autofocus = false
|
||||
export let autocomplete = null
|
||||
export let align: "left" | "right" | "center" | undefined = undefined
|
||||
export let autofocus: boolean | null = false
|
||||
export let autocomplete: boolean | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let field
|
||||
let field: any
|
||||
let focus = false
|
||||
|
||||
const updateValue = newValue => {
|
||||
const updateValue = (newValue: any) => {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
if (type === "number") {
|
||||
const float = parseFloat(newValue)
|
||||
const float = parseFloat(newValue as string)
|
||||
newValue = isNaN(float) ? null : float
|
||||
}
|
||||
dispatch("change", newValue)
|
||||
|
@ -37,40 +38,47 @@
|
|||
focus = true
|
||||
}
|
||||
|
||||
const onBlur = event => {
|
||||
const onBlur = (event: UIEvent) => {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
focus = false
|
||||
updateValue(event.target.value)
|
||||
updateValue(event?.target?.value)
|
||||
}
|
||||
|
||||
const onInput = event => {
|
||||
const onInput = (event: UIEvent) => {
|
||||
if (readonly || !updateOnChange || disabled) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
updateValue(event.target?.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
const updateValueOnEnter = (event: UIEvent) => {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
updateValue(event.target.value)
|
||||
updateValue(event.target?.value)
|
||||
}
|
||||
}
|
||||
|
||||
const getInputMode = type => {
|
||||
const getInputMode = (type: string) => {
|
||||
if (type === "bigint") {
|
||||
return "numeric"
|
||||
}
|
||||
return type === "number" ? "decimal" : "text"
|
||||
}
|
||||
|
||||
$: autocompleteValue =
|
||||
typeof autocomplete === "boolean"
|
||||
? autocomplete
|
||||
? "on"
|
||||
: "off"
|
||||
: undefined
|
||||
|
||||
onMount(async () => {
|
||||
if (disabled) return
|
||||
focus = autofocus
|
||||
focus = autofocus || false
|
||||
if (focus) {
|
||||
await tick()
|
||||
field.focus()
|
||||
|
@ -104,7 +112,7 @@
|
|||
class="spectrum-Textfield-input"
|
||||
style={align ? `text-align: ${align};` : ""}
|
||||
inputmode={getInputMode(type)}
|
||||
{autocomplete}
|
||||
autocomplete={autocompleteValue}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "@spectrum-css/fieldlabel/dist/index-vars.css"
|
||||
import FieldLabel from "./FieldLabel.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
|
||||
export let id = null
|
||||
export let label = null
|
||||
export let labelPosition = "above"
|
||||
export let error = null
|
||||
export let helpText = null
|
||||
export let tooltip = ""
|
||||
export let id: string | undefined = undefined
|
||||
export let label: string | undefined = undefined
|
||||
export let labelPosition: string = "above"
|
||||
export let error: string | undefined = undefined
|
||||
export let helpText: string | undefined = undefined
|
||||
export let tooltip: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="spectrum-Form-item" class:above={labelPosition === "above"}>
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import Field from "./Field.svelte"
|
||||
import TextField from "./Core/TextField.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let value = null
|
||||
export let label = null
|
||||
export let value: any = undefined
|
||||
export let label: string | undefined = undefined
|
||||
export let labelPosition = "above"
|
||||
export let placeholder = null
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let type = "text"
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let error = null
|
||||
export let error: string | undefined = undefined
|
||||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
export let autofocus
|
||||
export let autocomplete
|
||||
export let helpText = null
|
||||
export let autofocus: boolean | undefined = undefined
|
||||
export let autocomplete: boolean | undefined = undefined
|
||||
export let helpText: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
const onChange = (e: any) => {
|
||||
value = e.detail
|
||||
dispatch("change", e.detail)
|
||||
}
|
||||
|
@ -27,7 +27,6 @@
|
|||
<Field {helpText} {label} {labelPosition} {error}>
|
||||
<TextField
|
||||
{updateOnChange}
|
||||
{error}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{value}
|
||||
|
|
|
@ -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 Select from "./Core/Select.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { PopoverAlignment } from "../constants"
|
||||
|
||||
export let value = null
|
||||
export let label = undefined
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let labelPosition = "above"
|
||||
export let error = null
|
||||
export let placeholder = "Choose an option"
|
||||
export let options = []
|
||||
export let getOptionLabel = option => extractProperty(option, "label")
|
||||
export let getOptionValue = option => extractProperty(option, "value")
|
||||
export let getOptionSubtitle = option => option?.subtitle
|
||||
export let getOptionIcon = option => option?.icon
|
||||
export let getOptionColour = option => option?.colour
|
||||
export let value: V | undefined = undefined
|
||||
export let label: string | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
export let readonly: boolean = false
|
||||
export let labelPosition: string = "above"
|
||||
export let error: string | undefined = undefined
|
||||
export let placeholder: string | boolean = "Choose an option"
|
||||
export let options: O[] = []
|
||||
export let getOptionLabel = (option: O, _index?: number) =>
|
||||
extractProperty(option, "label")
|
||||
export let getOptionValue = (option: O, _index?: number) =>
|
||||
extractProperty(option, "value")
|
||||
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 isOptionEnabled = undefined
|
||||
export let quiet = false
|
||||
export let autoWidth = false
|
||||
export let sort = false
|
||||
export let tooltip = ""
|
||||
export let autocomplete = false
|
||||
export let customPopoverHeight = undefined
|
||||
export let align = undefined
|
||||
export let footer = null
|
||||
export let tag = null
|
||||
export let helpText = null
|
||||
export let compare = undefined
|
||||
export let isOptionEnabled:
|
||||
| ((_option: O, _index?: number) => boolean)
|
||||
| undefined = undefined
|
||||
export let quiet: boolean = false
|
||||
export let autoWidth: boolean = false
|
||||
export let sort: boolean = false
|
||||
export let tooltip: string | undefined = undefined
|
||||
export let autocomplete: boolean = false
|
||||
export let customPopoverHeight: string | undefined = undefined
|
||||
export let align: PopoverAlignment | undefined = PopoverAlignment.Left
|
||||
export let footer: string | undefined = undefined
|
||||
export let helpText: string | undefined = undefined
|
||||
export let compare: any = undefined
|
||||
export let onOptionMouseenter = () => {}
|
||||
export let onOptionMouseleave = () => {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
const onChange = (e: CustomEvent<any>) => {
|
||||
value = e.detail
|
||||
dispatch("change", e.detail)
|
||||
}
|
||||
|
||||
const extractProperty = (value, property) => {
|
||||
const extractProperty = (value: any, property: any) => {
|
||||
if (value && typeof value === "object") {
|
||||
return value[property]
|
||||
}
|
||||
|
@ -49,7 +59,6 @@
|
|||
<Field {helpText} {label} {labelPosition} {error} {tooltip}>
|
||||
<Select
|
||||
{quiet}
|
||||
{error}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{value}
|
||||
|
@ -68,7 +77,6 @@
|
|||
{isOptionEnabled}
|
||||
{autocomplete}
|
||||
{customPopoverHeight}
|
||||
{tag}
|
||||
{compare}
|
||||
{onOptionMouseenter}
|
||||
{onOptionMouseleave}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
default as AbsTooltip,
|
||||
TooltipPosition,
|
||||
TooltipType,
|
||||
} from "../Tooltip/AbsTooltip.svelte"
|
||||
import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
|
||||
import { TooltipPosition, TooltipType } from "../constants"
|
||||
|
||||
export let name: string = "Add"
|
||||
export let size: "XS" | "S" | "M" | "L" | "XL" = "M"
|
||||
export let hidden: boolean = false
|
||||
export let size = "M"
|
||||
export let hoverable: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let color: string | undefined = undefined
|
||||
|
@ -81,17 +78,6 @@
|
|||
color: var(--spectrum-global-color-gray-500) !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 {
|
||||
width: var(--spectrum-global-dimension-size-150);
|
||||
height: var(--spectrum-global-dimension-size-150);
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "@spectrum-css/link/dist/index-vars.css"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
|
||||
export let href = "#"
|
||||
export let size = "M"
|
||||
export let quiet = false
|
||||
export let primary = false
|
||||
export let secondary = false
|
||||
export let overBackground = false
|
||||
export let target
|
||||
export let download
|
||||
export let disabled = false
|
||||
export let tooltip = null
|
||||
export let href: string | null = "#"
|
||||
export let size: "S" | "M" | "L" = "M"
|
||||
export let quiet: boolean = false
|
||||
export let primary: boolean = false
|
||||
export let secondary: boolean = false
|
||||
export let overBackground: boolean = false
|
||||
export let target: string | undefined = undefined
|
||||
export let download: boolean | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
export let tooltip: string | null = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import StatusLight from "../StatusLight/StatusLight.svelte"
|
||||
|
||||
export let icon = null
|
||||
export let iconColor = null
|
||||
export let title = null
|
||||
export let subtitle = null
|
||||
export let url = null
|
||||
export let hoverable = false
|
||||
export let showArrow = false
|
||||
export let selected = false
|
||||
export let icon: string | undefined = undefined
|
||||
export let iconColor: string | undefined = undefined
|
||||
export let title: string | undefined = undefined
|
||||
export let subtitle: string | undefined = undefined
|
||||
export let url: string | undefined = undefined
|
||||
export let hoverable: boolean = false
|
||||
export let showArrow: boolean = false
|
||||
export let selected: boolean = false
|
||||
</script>
|
||||
|
||||
<a
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
<script context="module" lang="ts">
|
||||
export interface PopoverAPI {
|
||||
show: () => void
|
||||
hide: () => void
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
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 { createEventDispatcher, getContext, onDestroy } from "svelte"
|
||||
import positionDropdown, {
|
||||
|
@ -10,12 +16,10 @@
|
|||
import { fly } from "svelte/transition"
|
||||
import Context from "../context"
|
||||
import type { KeyboardEventHandler } from "svelte/elements"
|
||||
|
||||
const dispatch = createEventDispatcher<{ open: void; close: void }>()
|
||||
import { PopoverAlignment } from "../constants"
|
||||
|
||||
export let anchor: HTMLElement
|
||||
export let align: "left" | "right" | "left-outside" | "right-outside" =
|
||||
"right"
|
||||
export let align: PopoverAlignment = PopoverAlignment.Right
|
||||
export let portalTarget: string | undefined = undefined
|
||||
export let minWidth: number | undefined = undefined
|
||||
export let maxWidth: number | undefined = undefined
|
||||
|
@ -26,19 +30,24 @@
|
|||
export let offset = 4
|
||||
export let customHeight: string | undefined = undefined
|
||||
export let animate = true
|
||||
export let customZindex: string | undefined = undefined
|
||||
export let handlePostionUpdate: UpdateHandler | undefined = undefined
|
||||
export let customZIndex: number | undefined = undefined
|
||||
export let handlePositionUpdate: UpdateHandler | undefined = undefined
|
||||
export let showPopover = true
|
||||
export let clickOutsideOverride = false
|
||||
export let resizable = true
|
||||
export let wrap = false
|
||||
|
||||
const dispatch = createEventDispatcher<{ open: void; close: void }>()
|
||||
const animationDuration = 260
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout>
|
||||
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
|
||||
// fly from top to bottom and initially can be positioned under the cursor,
|
||||
|
@ -118,7 +127,7 @@
|
|||
minWidth,
|
||||
useAnchorWidth,
|
||||
offset,
|
||||
customUpdate: handlePostionUpdate,
|
||||
customUpdate: handlePositionUpdate,
|
||||
resizable,
|
||||
wrap,
|
||||
}}
|
||||
|
@ -128,11 +137,11 @@
|
|||
}}
|
||||
on:keydown={handleEscape}
|
||||
class="spectrum-Popover is-open"
|
||||
class:customZindex
|
||||
class:customZIndex
|
||||
class:hidden={!showPopover}
|
||||
class:blockPointerEvents
|
||||
role="presentation"
|
||||
style="height: {customHeight}; --customZindex: {customZindex};"
|
||||
style="height: {customHeight}; --customZIndex: {customZIndex};"
|
||||
transition:fly|local={{
|
||||
y: -20,
|
||||
duration: animate ? animationDuration : 0,
|
||||
|
@ -162,7 +171,7 @@
|
|||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.customZindex {
|
||||
z-index: var(--customZindex) !important;
|
||||
.customZIndex {
|
||||
z-index: var(--customZIndex) !important;
|
||||
}
|
||||
</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"
|
||||
|
||||
export let size = "M"
|
||||
export let celery = false
|
||||
export let yellow = false
|
||||
export let fuchsia = false
|
||||
export let indigo = false
|
||||
export let seafoam = false
|
||||
export let chartreuse = false
|
||||
export let magenta = false
|
||||
export let purple = false
|
||||
export let neutral = false
|
||||
export let info = false
|
||||
export let positive = false
|
||||
export let notice = false
|
||||
export let negative = false
|
||||
export let disabled = false
|
||||
export let active = false
|
||||
export let color = null
|
||||
export let square = false
|
||||
export let hoverable = false
|
||||
export let size: string = "M"
|
||||
export let celery: boolean = false
|
||||
export let yellow: boolean = false
|
||||
export let fuchsia: boolean = false
|
||||
export let indigo: boolean = false
|
||||
export let seafoam: boolean = false
|
||||
export let chartreuse: boolean = false
|
||||
export let magenta: boolean = false
|
||||
export let purple: boolean = false
|
||||
export let neutral: boolean = false
|
||||
export let info: boolean = false
|
||||
export let positive: boolean = false
|
||||
export let notice: boolean = false
|
||||
export let negative: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let active: boolean = false
|
||||
export let color: string | undefined = undefined
|
||||
export let square: boolean = false
|
||||
export let hoverable: boolean = false
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
|
|
@ -1,38 +1,24 @@
|
|||
<script context="module">
|
||||
export const TooltipPosition = {
|
||||
Top: "top",
|
||||
Right: "right",
|
||||
Bottom: "bottom",
|
||||
Left: "left",
|
||||
}
|
||||
export const TooltipType = {
|
||||
Default: "default",
|
||||
Info: "info",
|
||||
Positive: "positive",
|
||||
Negative: "negative",
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Portal from "svelte-portal"
|
||||
import { fade } from "svelte/transition"
|
||||
import "@spectrum-css/tooltip/dist/index-vars.css"
|
||||
import { onDestroy } from "svelte"
|
||||
import { TooltipPosition, TooltipType } from "../constants"
|
||||
|
||||
export let position = TooltipPosition.Top
|
||||
export let type = TooltipType.Default
|
||||
export let text = ""
|
||||
export let fixed = false
|
||||
export let color = ""
|
||||
export let noWrap = false
|
||||
export let position: TooltipPosition = TooltipPosition.Top
|
||||
export let type: TooltipType = TooltipType.Default
|
||||
export let text: string = ""
|
||||
export let fixed: boolean = false
|
||||
export let color: string | undefined = undefined
|
||||
export let noWrap: boolean = false
|
||||
|
||||
let wrapper
|
||||
let wrapper: HTMLElement | undefined
|
||||
let hovered = false
|
||||
let left
|
||||
let top
|
||||
let left: number | undefined
|
||||
let top: number | undefined
|
||||
let visible = false
|
||||
let timeout
|
||||
let interval
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
$: {
|
||||
if (hovered || fixed) {
|
||||
|
@ -49,8 +35,8 @@
|
|||
const updateTooltipPosition = () => {
|
||||
const node = wrapper?.children?.[0]
|
||||
if (!node) {
|
||||
left = null
|
||||
top = null
|
||||
left = undefined
|
||||
top = undefined
|
||||
return
|
||||
}
|
||||
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"
|
||||
|
||||
// Spectrum icons
|
||||
import "@spectrum-css/icon/dist/index-vars.css"
|
||||
|
||||
// Constants
|
||||
export * from "./constants"
|
||||
|
||||
// Form components
|
||||
export { default as Input } from "./Form/Input.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 IconAvatar } from "./Icon/IconAvatar.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 ProgressCircle } from "./ProgressCircle/ProgressCircle.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 Accordion } from "./Accordion/Accordion.svelte"
|
||||
export { default as AbsTooltip } from "./Tooltip/AbsTooltip.svelte"
|
||||
export { TooltipPosition, TooltipType } from "./Tooltip/AbsTooltip.svelte"
|
||||
|
||||
// Renderers
|
||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||
|
|
|
@ -16,4 +16,4 @@
|
|||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.json", "**/*.spec.ts", "**/*.spec.js"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@
|
|||
"shortid": "2.2.15",
|
||||
"svelte-dnd-action": "^0.9.8",
|
||||
"svelte-loading-spinners": "^0.1.1",
|
||||
"svelte-portal": "1.0.0",
|
||||
"svelte-portal": "^2.2.1",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,20 +1,29 @@
|
|||
<script>
|
||||
import { ActionButton, List, ListItem, Button } from "@budibase/bbui"
|
||||
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||
import { screenStore, appStore } from "@/stores/builder"
|
||||
<script lang="ts">
|
||||
import { Button } from "@budibase/bbui"
|
||||
import ScreensPopover from "@/components/common/ScreensPopover.svelte"
|
||||
import { screenStore } from "@/stores/builder"
|
||||
import { getContext, createEventDispatcher } from "svelte"
|
||||
|
||||
const { datasource } = getContext("grid")
|
||||
import type { Screen, ScreenUsage } from "@budibase/types"
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let popover
|
||||
const { datasource }: { datasource: any } = getContext("grid")
|
||||
|
||||
let popover: any
|
||||
|
||||
$: ds = $datasource
|
||||
$: resourceId = ds?.type === "table" ? ds.tableId : ds?.id
|
||||
$: connectedScreens = findConnectedScreens($screenStore.screens, resourceId)
|
||||
$: screenCount = connectedScreens.length
|
||||
$: screenUsage = connectedScreens.map(
|
||||
(screen: Screen): ScreenUsage => ({
|
||||
url: screen.routing?.route,
|
||||
_id: screen._id!,
|
||||
})
|
||||
)
|
||||
|
||||
const findConnectedScreens = (screens, resourceId) => {
|
||||
const findConnectedScreens = (
|
||||
screens: Screen[],
|
||||
resourceId: string
|
||||
): Screen[] => {
|
||||
return screens.filter(screen => {
|
||||
return JSON.stringify(screen).includes(`"${resourceId}"`)
|
||||
})
|
||||
|
@ -26,34 +35,16 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<DetailPopover title="Screens" bind:this={popover}>
|
||||
<svelte:fragment slot="anchor" let:open>
|
||||
<ActionButton
|
||||
icon="WebPage"
|
||||
selected={open || screenCount}
|
||||
quiet
|
||||
accentColor="#364800"
|
||||
>
|
||||
Screens{screenCount ? `: ${screenCount}` : ""}
|
||||
</ActionButton>
|
||||
</svelte:fragment>
|
||||
{#if !connectedScreens.length}
|
||||
There aren't any screens connected to this data.
|
||||
{:else}
|
||||
The following screens are connected to this data.
|
||||
<List>
|
||||
{#each connectedScreens as screen}
|
||||
<ListItem
|
||||
title={screen.routing.route}
|
||||
url={`/builder/app/${$appStore.appId}/design/${screen._id}`}
|
||||
showArrow
|
||||
/>
|
||||
{/each}
|
||||
</List>
|
||||
{/if}
|
||||
<div>
|
||||
<ScreensPopover
|
||||
bind:this={popover}
|
||||
screens={screenUsage}
|
||||
icon="WebPage"
|
||||
accentColor="#364800"
|
||||
showCount
|
||||
>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary icon="WebPage" on:click={generateScreen}>
|
||||
Generate app screen
|
||||
</Button>
|
||||
</div>
|
||||
</DetailPopover>
|
||||
</svelte:fragment>
|
||||
</ScreensPopover>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import IntegrationIcon from "@/components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
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
|
||||
|
||||
|
@ -71,7 +71,10 @@
|
|||
{/if}
|
||||
</NavItem>
|
||||
<UpdateDatasourceModal {datasource} bind:this={editModal} />
|
||||
<DeleteConfirmationModal {datasource} bind:this={deleteConfirmationModal} />
|
||||
<DeleteDataConfirmModal
|
||||
source={datasource}
|
||||
bind:this={deleteConfirmationModal}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.datasource-icon {
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { datasources } from "@/stores/builder"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
|
||||
export let datasource
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
export const show = () => {
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
async function deleteDatasource() {
|
||||
try {
|
||||
const isSelected = datasource.selected || datasource.containsSelected
|
||||
await datasources.delete(datasource)
|
||||
notifications.success("Datasource deleted")
|
||||
if (isSelected) {
|
||||
$goto("./datasource")
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting datasource")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete Datasource"
|
||||
onOk={deleteDatasource}
|
||||
title="Confirm Deletion"
|
||||
>
|
||||
Are you sure you wish to delete the datasource
|
||||
<i>{datasource.name}</i>? This action cannot be undone.
|
||||
</ConfirmDialog>
|
|
@ -6,19 +6,18 @@
|
|||
} from "@/helpers/data/utils"
|
||||
import { goto as gotoStore, isActive } from "@roxi/routify"
|
||||
import {
|
||||
datasources,
|
||||
queries,
|
||||
userSelectedResourceMap,
|
||||
contextMenuStore,
|
||||
} from "@/stores/builder"
|
||||
import NavItem from "@/components/common/NavItem.svelte"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import DeleteDataConfirmModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte"
|
||||
import { notifications, Icon } from "@budibase/bbui"
|
||||
|
||||
export let datasource
|
||||
export let query
|
||||
|
||||
let confirmDeleteDialog
|
||||
let confirmDeleteModal
|
||||
|
||||
// goto won't work in the context menu callback if the store is called directly
|
||||
$: goto = $gotoStore
|
||||
|
@ -31,7 +30,7 @@
|
|||
keyBind: null,
|
||||
visible: true,
|
||||
disabled: false,
|
||||
callback: confirmDeleteDialog.show,
|
||||
callback: confirmDeleteModal.show,
|
||||
},
|
||||
{
|
||||
icon: "Duplicate",
|
||||
|
@ -51,20 +50,6 @@
|
|||
]
|
||||
}
|
||||
|
||||
async function deleteQuery() {
|
||||
try {
|
||||
// Go back to the datasource if we are deleting the active query
|
||||
if ($queries.selectedQueryId === query._id) {
|
||||
goto(`./datasource/${query.datasourceId}`)
|
||||
}
|
||||
await queries.delete(query)
|
||||
await datasources.fetch()
|
||||
notifications.success("Query deleted")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting query")
|
||||
}
|
||||
}
|
||||
|
||||
const openContextMenu = e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
@ -90,14 +75,7 @@
|
|||
<Icon size="S" hoverable name="MoreSmallList" on:click={openContextMenu} />
|
||||
</NavItem>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete Query"
|
||||
onOk={deleteQuery}
|
||||
title="Confirm Deletion"
|
||||
>
|
||||
Are you sure you wish to delete this query? This action cannot be undone.
|
||||
</ConfirmDialog>
|
||||
<DeleteDataConfirmModal source={query} bind:this={confirmDeleteModal} />
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
<script>
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { appStore, tables, datasources, screenStore } from "@/stores/builder"
|
||||
import { InlineAlert, Link, Input, notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||
|
||||
export let table
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
let screensPossiblyAffected = []
|
||||
let viewsMessage = ""
|
||||
let deleteTableName
|
||||
|
||||
const getViewsMessage = () => {
|
||||
const views = Object.values(table?.views ?? [])
|
||||
if (views.length < 1) {
|
||||
return ""
|
||||
}
|
||||
if (views.length === 1) {
|
||||
return ", including 1 view"
|
||||
}
|
||||
|
||||
return `, including ${views.length} views`
|
||||
}
|
||||
|
||||
export const show = () => {
|
||||
viewsMessage = getViewsMessage()
|
||||
screensPossiblyAffected = $screenStore.screens
|
||||
.filter(
|
||||
screen => screen.autoTableId === table._id && screen.routing?.route
|
||||
)
|
||||
.map(screen => ({
|
||||
text: screen.routing.route,
|
||||
url: `/builder/app/${$appStore.appId}/design/${screen._id}`,
|
||||
}))
|
||||
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
async function deleteTable() {
|
||||
const isSelected = $params.tableId === table._id
|
||||
try {
|
||||
await tables.delete(table)
|
||||
|
||||
if (table.sourceType === DB_TYPE_EXTERNAL) {
|
||||
await datasources.fetch()
|
||||
}
|
||||
notifications.success("Table deleted")
|
||||
if (isSelected) {
|
||||
$goto(`./datasource/${table.datasourceId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error(`Error deleting table - ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function hideDeleteDialog() {
|
||||
deleteTableName = ""
|
||||
}
|
||||
|
||||
const autofillTableName = () => {
|
||||
deleteTableName = table.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete Table"
|
||||
onOk={deleteTable}
|
||||
onCancel={hideDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
disabled={deleteTableName !== table.name}
|
||||
>
|
||||
<div class="content">
|
||||
<p class="firstWarning">
|
||||
Are you sure you wish to delete the table
|
||||
<span class="tableNameLine">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<b on:click={autofillTableName} class="tableName">{table.name}</b>
|
||||
<span>?</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="secondWarning">All table data will be deleted{viewsMessage}.</p>
|
||||
<p class="thirdWarning">This action <b>cannot be undone</b>.</p>
|
||||
|
||||
{#if screensPossiblyAffected.length > 0}
|
||||
<div class="affectedScreens">
|
||||
<InlineAlert
|
||||
header="The following screens were originally generated from this table and may no longer function as expected"
|
||||
>
|
||||
<ul class="affectedScreensList">
|
||||
{#each screensPossiblyAffected as item}
|
||||
<li>
|
||||
<Link quiet overBackground target="_blank" href={item.url}
|
||||
>{item.text}</Link
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</InlineAlert>
|
||||
</div>
|
||||
{/if}
|
||||
<p class="fourthWarning">Please enter the table name below to confirm.</p>
|
||||
<Input bind:value={deleteTableName} placeholder={table.name} />
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
margin-top: 0;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.firstWarning {
|
||||
margin: 0 0 12px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tableNameLine {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.tableName {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.secondWarning {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.thirdWarning {
|
||||
margin: 0 0 12px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.affectedScreens {
|
||||
margin: 18px 0;
|
||||
max-width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.affectedScreens :global(.spectrum-InLineAlert) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.affectedScreensList {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.affectedScreensList li {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.fourthWarning {
|
||||
margin: 12px 0 6px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,7 @@
|
|||
import NavItem from "@/components/common/NavItem.svelte"
|
||||
import { isActive } from "@roxi/routify"
|
||||
import EditModal from "./EditModal.svelte"
|
||||
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
|
||||
import DeleteConfirmationModal from "../../modals/DeleteDataConfirmationModal.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||
|
||||
|
@ -65,4 +65,4 @@
|
|||
{/if}
|
||||
</NavItem>
|
||||
<EditModal {table} bind:this={editModal} />
|
||||
<DeleteConfirmationModal {table} bind:this={deleteConfirmationModal} />
|
||||
<DeleteConfirmationModal source={table} bind:this={deleteConfirmationModal} />
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
<script lang="ts">
|
||||
import { Link, notifications } from "@budibase/bbui"
|
||||
import {
|
||||
appStore,
|
||||
datasources,
|
||||
queries,
|
||||
screenStore,
|
||||
tables,
|
||||
views,
|
||||
viewsV2,
|
||||
} from "@/stores/builder"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import { helpers, utils } from "@budibase/shared-core"
|
||||
import { SourceType } from "@budibase/types"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||
import { get } from "svelte/store"
|
||||
import type { Table, ViewV2, View, Datasource, Query } from "@budibase/types"
|
||||
|
||||
export let source: Table | ViewV2 | Datasource | Query | undefined
|
||||
|
||||
let confirmDeleteDialog: any
|
||||
let affectedScreens: { text: string; url: string }[] = []
|
||||
let sourceType: SourceType | undefined = undefined
|
||||
|
||||
const getDatasourceQueries = () => {
|
||||
if (sourceType !== SourceType.DATASOURCE) {
|
||||
return ""
|
||||
}
|
||||
const sourceId = getSourceID()
|
||||
const queryList = get(queries).list.filter(
|
||||
query => query.datasourceId === sourceId
|
||||
)
|
||||
return queryList
|
||||
}
|
||||
|
||||
function getSourceID(): string {
|
||||
if (!source) {
|
||||
throw new Error("No data source provided.")
|
||||
}
|
||||
if ("id" in source) {
|
||||
return source.id
|
||||
}
|
||||
return source._id!
|
||||
}
|
||||
|
||||
export const show = async () => {
|
||||
const usage = await screenStore.usageInScreens(getSourceID())
|
||||
affectedScreens = processScreens(usage.screens)
|
||||
sourceType = usage.sourceType
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
function processScreens(
|
||||
screens: { url: string; _id: string }[]
|
||||
): { text: string; url: string }[] {
|
||||
return screens.map(({ url, _id }) => ({
|
||||
text: url,
|
||||
url: `/builder/app/${$appStore.appId}/design/${_id}`,
|
||||
}))
|
||||
}
|
||||
|
||||
function hideDeleteDialog() {
|
||||
sourceType = undefined
|
||||
}
|
||||
|
||||
async function deleteTable(table: Table & { datasourceId?: string }) {
|
||||
const isSelected = $params.tableId === table._id
|
||||
try {
|
||||
await tables.delete({
|
||||
_id: table._id!,
|
||||
_rev: table._rev!,
|
||||
})
|
||||
|
||||
if (table.sourceType === DB_TYPE_EXTERNAL) {
|
||||
await datasources.fetch()
|
||||
}
|
||||
notifications.success("Table deleted")
|
||||
if (isSelected) {
|
||||
$goto(`./datasource/${table.datasourceId}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
notifications.error(`Error deleting table - ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteView(view: ViewV2 | View) {
|
||||
try {
|
||||
if (helpers.views.isV2(view)) {
|
||||
await viewsV2.delete(view as ViewV2)
|
||||
} else {
|
||||
await views.delete(view as View)
|
||||
}
|
||||
notifications.success("View deleted")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting view")
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDatasource(datasource: Datasource) {
|
||||
try {
|
||||
await datasources.delete(datasource)
|
||||
notifications.success("Datasource deleted")
|
||||
const isSelected =
|
||||
get(datasources).selectedDatasourceId === datasource._id
|
||||
if (isSelected) {
|
||||
$goto("./datasource")
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting datasource")
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteQuery(query: Query) {
|
||||
try {
|
||||
// Go back to the datasource if we are deleting the active query
|
||||
if ($queries.selectedQueryId === query._id) {
|
||||
$goto(`./datasource/${query.datasourceId}`)
|
||||
}
|
||||
await queries.delete(query)
|
||||
await datasources.fetch()
|
||||
notifications.success("Query deleted")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting query")
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSource() {
|
||||
if (!source || !sourceType) {
|
||||
throw new Error("Unable to delete - no data source found.")
|
||||
}
|
||||
|
||||
switch (sourceType) {
|
||||
case SourceType.TABLE:
|
||||
return await deleteTable(source as Table)
|
||||
case SourceType.VIEW:
|
||||
return await deleteView(source as ViewV2)
|
||||
case SourceType.QUERY:
|
||||
return await deleteQuery(source as Query)
|
||||
case SourceType.DATASOURCE:
|
||||
return await deleteDatasource(source as Datasource)
|
||||
default:
|
||||
utils.unreachable(sourceType)
|
||||
}
|
||||
}
|
||||
|
||||
function buildMessage(sourceType: string) {
|
||||
if (!source) {
|
||||
return ""
|
||||
}
|
||||
const screenCount = affectedScreens.length
|
||||
let message = `Removing ${source?.name} `
|
||||
let initialLength = message.length
|
||||
if (sourceType === SourceType.TABLE) {
|
||||
const views = "views" in source ? Object.values(source?.views ?? []) : []
|
||||
message += `will delete its data${
|
||||
views.length
|
||||
? `${screenCount ? "," : " and"} views (${views.length})`
|
||||
: ""
|
||||
}`
|
||||
} else if (sourceType === SourceType.DATASOURCE) {
|
||||
const queryList = getDatasourceQueries()
|
||||
if (queryList.length) {
|
||||
message += `will delete its queries (${queryList.length})`
|
||||
}
|
||||
}
|
||||
if (screenCount) {
|
||||
message +=
|
||||
initialLength !== message.length
|
||||
? ", and break connected screens:"
|
||||
: "will break connected screens:"
|
||||
} else {
|
||||
message += "."
|
||||
}
|
||||
return message.length !== initialLength ? message : ""
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
okText="Delete"
|
||||
onOk={deleteSource}
|
||||
onCancel={hideDeleteDialog}
|
||||
title={`Are you sure you want to delete this ${sourceType}?`}
|
||||
>
|
||||
<div class="content">
|
||||
{#if sourceType}
|
||||
<p class="warning">
|
||||
{buildMessage(sourceType)}
|
||||
{#if affectedScreens.length > 0}
|
||||
<span class="screens">
|
||||
{#each affectedScreens as item, idx}
|
||||
<Link overBackground target="_blank" href={item.url}
|
||||
>{item.text}{idx !== affectedScreens.length - 1
|
||||
? ","
|
||||
: ""}</Link
|
||||
>
|
||||
{/each}
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
<p class="warning">
|
||||
<b>This action cannot be undone.</b>
|
||||
</p>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
margin-top: 0;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.screens {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: var(--spacing-l);
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,7 @@
|
|||
export let onOk = undefined
|
||||
export let onCancel = undefined
|
||||
export let warning = true
|
||||
export let disabled
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
<script>
|
||||
import { Popover, Icon } from "@budibase/bbui"
|
||||
<script lang="ts">
|
||||
import {
|
||||
Popover,
|
||||
Icon,
|
||||
PopoverAlignment,
|
||||
type PopoverAPI,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
export let title
|
||||
export let align = "left"
|
||||
export let showPopover
|
||||
export let width
|
||||
export let title: string = ""
|
||||
export let subtitle: string | undefined = undefined
|
||||
export let align: PopoverAlignment = PopoverAlignment.Left
|
||||
export let showPopover: boolean = true
|
||||
export let width: number | undefined = undefined
|
||||
|
||||
let popover
|
||||
let anchor
|
||||
let open
|
||||
let popover: PopoverAPI | undefined
|
||||
let anchor: HTMLElement | undefined
|
||||
let open: boolean = false
|
||||
|
||||
export const show = () => popover?.show()
|
||||
export const hide = () => popover?.hide()
|
||||
|
@ -30,20 +36,24 @@
|
|||
{showPopover}
|
||||
on:open
|
||||
on:close
|
||||
customZindex={100}
|
||||
customZIndex={100}
|
||||
>
|
||||
<div class="detail-popover">
|
||||
<div class="detail-popover__header">
|
||||
<div class="detail-popover__title">
|
||||
{title}
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
hoverColor="var(--spectrum-global-color-gray-900)"
|
||||
on:click={hide}
|
||||
size="S"
|
||||
/>
|
||||
</div>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
hoverColor="var(--spectum-global-color-gray-900)"
|
||||
on:click={hide}
|
||||
/>
|
||||
{#if subtitle}
|
||||
<div class="detail-popover__subtitle">{subtitle}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="detail-popover__body">
|
||||
<slot />
|
||||
|
@ -56,14 +66,18 @@
|
|||
background-color: var(--spectrum-alias-background-color-primary);
|
||||
}
|
||||
.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;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
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-weight: 600;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,26 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="8" height="8">
|
||||
<circle cx="4" cy="4" r="4" stroke-width="0" fill="currentColor" />
|
||||
<script lang="ts">
|
||||
export let color = "currentColor"
|
||||
export let size: "S" | "M" = "M"
|
||||
|
||||
const sizes = {
|
||||
S: 6,
|
||||
M: 8,
|
||||
}
|
||||
|
||||
$: sizePx = sizes[size]
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox={`0 0 ${sizePx} ${sizePx}`}
|
||||
width={`${sizePx}`}
|
||||
height={`${sizePx}`}
|
||||
>
|
||||
<circle
|
||||
cx={sizePx / 2}
|
||||
cy={sizePx / 2}
|
||||
r={sizePx / 2}
|
||||
stroke-width="0"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 157 B After Width: | Height: | Size: 417 B |
|
@ -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>
|
|
@ -0,0 +1,58 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ActionButton,
|
||||
PopoverAlignment,
|
||||
} from "@budibase/bbui"
|
||||
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||
import { appStore } from "@/stores/builder"
|
||||
import type { ScreenUsage } from "@budibase/types"
|
||||
|
||||
export let screens: ScreenUsage[] = []
|
||||
export let icon = "DeviceDesktop"
|
||||
export let accentColor: string | null | undefined = null
|
||||
export let showCount = false
|
||||
export let align = PopoverAlignment.Left
|
||||
|
||||
let popover: any
|
||||
|
||||
export function show() {
|
||||
popover?.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
popover?.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetailPopover title="Screens" bind:this={popover} {align}>
|
||||
<svelte:fragment slot="anchor" let:open>
|
||||
<ActionButton
|
||||
{icon}
|
||||
quiet
|
||||
selected={open || !!(showCount && screens.length)}
|
||||
{accentColor}
|
||||
on:click={show}
|
||||
>
|
||||
Screens{showCount && screens.length ? `: ${screens.length}` : ""}
|
||||
</ActionButton>
|
||||
</svelte:fragment>
|
||||
|
||||
{#if !screens.length}
|
||||
There aren't any screens connected to this data.
|
||||
{:else}
|
||||
The following screens are connected to this data.
|
||||
<List>
|
||||
{#each screens as screen}
|
||||
<ListItem
|
||||
title={screen.url}
|
||||
url={`/builder/app/${$appStore.appId}/design/${screen._id}`}
|
||||
showArrow
|
||||
/>
|
||||
{/each}
|
||||
</List>
|
||||
{/if}
|
||||
|
||||
<slot name="footer" />
|
||||
</DetailPopover>
|
|
@ -25,7 +25,7 @@
|
|||
</div>
|
||||
|
||||
<Popover
|
||||
customZindex={998}
|
||||
customZIndex={998}
|
||||
bind:this={formPopover}
|
||||
align="center"
|
||||
anchor={formPopoverAnchor}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { Icon, Input, Drawer, Button } from "@budibase/bbui"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
|
@ -10,25 +10,25 @@
|
|||
import { builderStore } from "@/stores/builder"
|
||||
|
||||
export let panel = ClientBindingPanel
|
||||
export let value = ""
|
||||
export let bindings = []
|
||||
export let title
|
||||
export let placeholder
|
||||
export let label
|
||||
export let disabled = false
|
||||
export let allowHBS = true
|
||||
export let allowJS = true
|
||||
export let allowHelpers = true
|
||||
export let updateOnChange = true
|
||||
export let key
|
||||
export let disableBindings = false
|
||||
export let forceModal = false
|
||||
export let value: any = ""
|
||||
export let bindings: any[] = []
|
||||
export let title: string | undefined = undefined
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let label: string | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
export let allowHBS: boolean = true
|
||||
export let allowJS: boolean = true
|
||||
export let allowHelpers: boolean = true
|
||||
export let updateOnChange: boolean = true
|
||||
export let key: string | null = null
|
||||
export let disableBindings: boolean = false
|
||||
export let forceModal: boolean = false
|
||||
export let context = null
|
||||
export let autocomplete
|
||||
export let autocomplete: boolean | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let bindingDrawer
|
||||
let bindingDrawer: any
|
||||
let currentVal = value
|
||||
|
||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||
|
@ -38,7 +38,7 @@
|
|||
const saveBinding = () => {
|
||||
onChange(tempValue)
|
||||
onBlur()
|
||||
builderStore.propertyFocus()
|
||||
builderStore.propertyFocus(null)
|
||||
bindingDrawer.hide()
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@
|
|||
save: saveBinding,
|
||||
})
|
||||
|
||||
const onChange = value => {
|
||||
const onChange = (value: any) => {
|
||||
currentVal = readableToRuntimeBinding(bindings, value)
|
||||
dispatch("change", currentVal)
|
||||
}
|
||||
|
@ -55,8 +55,8 @@
|
|||
dispatch("blur", currentVal)
|
||||
}
|
||||
|
||||
const onDrawerHide = e => {
|
||||
builderStore.propertyFocus()
|
||||
const onDrawerHide = (e: any) => {
|
||||
builderStore.propertyFocus(null)
|
||||
dispatch("drawerHide", e.detail)
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
$: success = !error && !empty
|
||||
$: highlightedResult = highlight(expressionResult)
|
||||
$: highlightedLogs = expressionLogs.map(l => ({
|
||||
log: highlight(l.log.join(", ")),
|
||||
log: l.log.map(part => highlight(part)).join(", "),
|
||||
line: l.line,
|
||||
type: l.type,
|
||||
}))
|
||||
|
@ -95,7 +95,9 @@
|
|||
{#if empty}
|
||||
Your expression will be evaluated here
|
||||
{:else if error}
|
||||
{formatError(expressionError)}
|
||||
<div class="error-msg">
|
||||
{formatError(expressionError)}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="output-lines">
|
||||
{#each highlightedLogs as logLine}
|
||||
|
@ -118,13 +120,17 @@
|
|||
<span>{@html logLine.log}</span>
|
||||
</div>
|
||||
{#if logLine.line}
|
||||
<span style="color: var(--blue)">:{logLine.line}</span>
|
||||
<span style="color: var(--blue); overflow-wrap: normal;"
|
||||
>:{logLine.line}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<div class="line">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html highlightedResult}
|
||||
<div>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html highlightedResult}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -169,29 +175,33 @@
|
|||
.header.error::before {
|
||||
background: var(--error-bg);
|
||||
}
|
||||
.error-msg {
|
||||
padding-top: var(--spacing-m);
|
||||
}
|
||||
.body {
|
||||
flex: 1 1 auto;
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
font-family: var(--font-mono);
|
||||
margin: 0 var(--spacing-m);
|
||||
font-size: 12px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-wrap: anywhere;
|
||||
height: 0;
|
||||
}
|
||||
.output-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
.line {
|
||||
border-bottom: var(--border-light);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
padding: var(--spacing-s);
|
||||
padding: var(--spacing-m) 0;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
.line:not(:first-of-type) {
|
||||
border-top: var(--border-light);
|
||||
}
|
||||
.icon-log {
|
||||
display: flex;
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
anchor={primaryDisplayColumnAnchor}
|
||||
item={columns.primary}
|
||||
on:change={e => columns.update(e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
export let item
|
||||
export let anchor
|
||||
export let bindings
|
||||
|
||||
let draggableStore = writable({
|
||||
selected: null,
|
||||
|
@ -48,6 +49,7 @@
|
|||
componentInstance={item}
|
||||
{parseSettings}
|
||||
on:change
|
||||
{bindings}
|
||||
>
|
||||
<div slot="header" class="type-icon">
|
||||
<Icon name={icon} />
|
||||
|
|
|
@ -69,6 +69,7 @@ const toGridFormat = draggableListColumns => {
|
|||
active: entry.active,
|
||||
width: entry.width,
|
||||
conditions: entry.conditions,
|
||||
format: entry.format,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -85,6 +86,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
|
|||
columnType: column.columnType || schema[column.field].type,
|
||||
width: column.width,
|
||||
conditions: column.conditions,
|
||||
format: column.format,
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
runtimeToReadableBinding,
|
||||
} from "@/dataBinding"
|
||||
import { builderStore } from "@/stores/builder"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let label = ""
|
||||
export let labelHidden = false
|
||||
|
@ -26,16 +25,16 @@
|
|||
export let wide
|
||||
|
||||
let highlightType
|
||||
let domElement
|
||||
|
||||
$: highlightedProp = $builderStore.highlightedSetting
|
||||
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
||||
$: safeValue = getSafeValue(value, defaultValue, allBindings)
|
||||
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
||||
|
||||
$: if (!Array.isArray(value)) {
|
||||
highlightType =
|
||||
highlightedProp?.key === key ? `highlighted-${highlightedProp?.type}` : ""
|
||||
}
|
||||
$: isHighlighted = highlightedProp?.key === key
|
||||
|
||||
$: highlightType = isHighlighted ? `highlighted-${highlightedProp?.type}` : ""
|
||||
|
||||
const getAllBindings = (bindings, componentBindings, nested) => {
|
||||
if (!nested) {
|
||||
|
@ -76,14 +75,18 @@
|
|||
: enriched
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (highlightedProp) {
|
||||
builderStore.highlightSetting(null)
|
||||
}
|
||||
})
|
||||
function scrollToElement(element) {
|
||||
element?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
})
|
||||
}
|
||||
|
||||
$: highlightedProp && isHighlighted && scrollToElement(domElement)
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={domElement}
|
||||
id={`${key}-prop-control-wrap`}
|
||||
class={`property-control ${highlightType}`}
|
||||
class:wide={!label || labelHidden || wide === true}
|
||||
|
@ -150,10 +153,10 @@
|
|||
.property-control.highlighted {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-static-red-600);
|
||||
margin-top: -3.5px;
|
||||
margin-bottom: -3.5px;
|
||||
padding-bottom: 3.5px;
|
||||
padding-top: 3.5px;
|
||||
margin-top: -4px;
|
||||
margin-bottom: -4px;
|
||||
padding-bottom: 4px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.property-control.property-focus :global(input) {
|
||||
|
@ -172,7 +175,7 @@
|
|||
}
|
||||
.text {
|
||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||
color: var(--grey-6);
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
grid-column: 2 / 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
|
||||
import { screenStore } from "@/stores/builder"
|
||||
import ScreensPopover from "@/components/common/ScreensPopover.svelte"
|
||||
import type { ScreenUsage } from "@budibase/types"
|
||||
|
||||
export let sourceId: string
|
||||
|
||||
let screens: ScreenUsage[] = []
|
||||
let popover: any
|
||||
|
||||
export function show() {
|
||||
popover?.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
popover?.hide()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
let response = await screenStore.usageInScreens(sourceId)
|
||||
screens = response?.screens
|
||||
})
|
||||
</script>
|
||||
|
||||
<ScreensPopover
|
||||
bind:this={popover}
|
||||
{screens}
|
||||
icon="WebPage"
|
||||
accentColor="#364800"
|
||||
showCount
|
||||
/>
|
|
@ -23,6 +23,7 @@
|
|||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||
import QueryViewerSavePromptModal from "./QueryViewerSavePromptModal.svelte"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
||||
|
||||
export let query
|
||||
let queryHash
|
||||
|
@ -170,6 +171,7 @@
|
|||
</Body>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<ConnectedQueryScreens sourceId={query._id} />
|
||||
<Button disabled={loading} on:click={runQuery} overBackground>
|
||||
<Icon size="S" name="Play" />
|
||||
Run query</Button
|
||||
|
@ -384,6 +386,8 @@
|
|||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
runtimeToReadableMap,
|
||||
toBindingsArray,
|
||||
} from "@/dataBinding"
|
||||
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
||||
|
||||
export let queryId
|
||||
|
||||
|
@ -502,9 +503,12 @@
|
|||
on:change={() => (query.flags.urlName = false)}
|
||||
on:save={saveQuery}
|
||||
/>
|
||||
<div class="access">
|
||||
<Label>Access</Label>
|
||||
<AccessLevelSelect {query} {saveId} />
|
||||
<div class="controls">
|
||||
<ConnectedQueryScreens sourceId={query._id} />
|
||||
<div class="access">
|
||||
<Label>Access</Label>
|
||||
<AccessLevelSelect {query} {saveId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="url-block">
|
||||
|
@ -825,6 +829,12 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.access {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
|
|
|
@ -96,8 +96,8 @@
|
|||
maxWidth={300}
|
||||
dismissible={false}
|
||||
offset={12}
|
||||
handlePostionUpdate={tourStep?.positionHandler}
|
||||
customZindex={3}
|
||||
handlePositionUpdate={tourStep?.positionHandler}
|
||||
customZIndex={3}
|
||||
>
|
||||
<div class="tour-content">
|
||||
<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
|
||||
* in the app.
|
||||
*/
|
||||
export const getAllStateVariables = () => {
|
||||
// Find all button action settings in all components
|
||||
export const getAllStateVariables = screen => {
|
||||
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 = []
|
||||
getAllAssets().forEach(asset => {
|
||||
assets.forEach(asset => {
|
||||
findAllMatchingComponents(asset.props, component => {
|
||||
const settings = componentStore.getComponentSettings(component._component)
|
||||
const nestedTypes = [
|
||||
|
@ -1214,11 +1221,17 @@ export const getAllStateVariables = () => {
|
|||
})
|
||||
|
||||
// Add on load settings from screens
|
||||
get(screenStore).screens.forEach(screen => {
|
||||
if (screen) {
|
||||
if (screen.onLoad) {
|
||||
eventSettings.push(screen.onLoad)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
get(screenStore).screens.forEach(screen => {
|
||||
if (screen.onLoad) {
|
||||
eventSettings.push(screen.onLoad)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Extract all state keys from any "update state" actions in each setting
|
||||
let bindingSet = new Set()
|
||||
|
|
|
@ -76,13 +76,15 @@ export const getSequentialName = <T extends any>(
|
|||
{
|
||||
getName,
|
||||
numberFirstItem,
|
||||
separator = "",
|
||||
}: {
|
||||
getName?: (item: T) => string
|
||||
numberFirstItem?: boolean
|
||||
separator?: string
|
||||
} = {}
|
||||
) => {
|
||||
if (!prefix?.length) {
|
||||
return null
|
||||
return ""
|
||||
}
|
||||
const trimmedPrefix = prefix.trim()
|
||||
const firstName = numberFirstItem ? `${prefix}1` : trimmedPrefix
|
||||
|
@ -107,5 +109,5 @@ export const getSequentialName = <T extends any>(
|
|||
max = num
|
||||
}
|
||||
})
|
||||
return max === 0 ? firstName : `${prefix}${max + 1}`
|
||||
return max === 0 ? firstName : `${prefix}${separator}${max + 1}`
|
||||
}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
import { Component, Screen, ScreenProps } from "@budibase/types"
|
||||
import clientManifest from "@budibase/client/manifest.json"
|
||||
|
||||
export function findComponentsBySettingsType(
|
||||
screen: Screen,
|
||||
type: string | string[]
|
||||
) {
|
||||
const typesArray = Array.isArray(type) ? type : [type]
|
||||
|
||||
const result: {
|
||||
component: Component
|
||||
setting: {
|
||||
type: string
|
||||
key: string
|
||||
}
|
||||
}[] = []
|
||||
function recurseFieldComponentsInChildren(component: ScreenProps) {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
|
||||
const definition = getManifestDefinition(component)
|
||||
const setting =
|
||||
"settings" in definition &&
|
||||
definition.settings.find((s: any) => typesArray.includes(s.type))
|
||||
if (setting && "type" in setting) {
|
||||
result.push({
|
||||
component,
|
||||
setting: { type: setting.type!, key: setting.key! },
|
||||
})
|
||||
}
|
||||
component._children?.forEach(child => {
|
||||
recurseFieldComponentsInChildren(child)
|
||||
})
|
||||
}
|
||||
|
||||
recurseFieldComponentsInChildren(screen?.props)
|
||||
return result
|
||||
}
|
||||
|
||||
function getManifestDefinition(component: Component) {
|
||||
const componentType = component._component.split("/").slice(-1)[0]
|
||||
const definition =
|
||||
clientManifest[componentType as keyof typeof clientManifest]
|
||||
return definition
|
||||
}
|
|
@ -49,7 +49,7 @@ describe("getSequentialName", () => {
|
|||
|
||||
it("handles nullish prefix", async () => {
|
||||
const name = getSequentialName([], null)
|
||||
expect(name).toBe(null)
|
||||
expect(name).toBe("")
|
||||
})
|
||||
|
||||
it("handles just the prefix", async () => {
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<script>
|
||||
import { views, viewsV2 } from "@/stores/builder"
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export let view
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
export const show = () => {
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
||||
async function deleteView() {
|
||||
try {
|
||||
if (view.version === 2) {
|
||||
await viewsV2.delete(view)
|
||||
} else {
|
||||
await views.delete(view)
|
||||
}
|
||||
notifications.success("View deleted")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error deleting view")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
body={`Are you sure you wish to delete the view '${view.name}'? Your data will be deleted and this action cannot be undone.`}
|
||||
okText="Delete View"
|
||||
onOk={deleteView}
|
||||
title="Confirm Deletion"
|
||||
/>
|
|
@ -10,9 +10,8 @@
|
|||
import { Icon, ActionButton, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||
import { params, url } from "@roxi/routify"
|
||||
import EditViewModal from "./EditViewModal.svelte"
|
||||
import DeleteViewModal from "./DeleteViewModal.svelte"
|
||||
import EditTableModal from "@/components/backend/TableNavigator/TableNavItem/EditModal.svelte"
|
||||
import DeleteTableModal from "@/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte"
|
||||
import DeleteConfirmationModal from "@/components/backend/modals/DeleteDataConfirmationModal.svelte"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
import { DB_TYPE_EXTERNAL } from "@/constants/backend"
|
||||
import { TableNames } from "@/constants"
|
||||
|
@ -314,12 +313,12 @@
|
|||
|
||||
{#if table && tableEditable}
|
||||
<EditTableModal {table} bind:this={editTableModal} />
|
||||
<DeleteTableModal {table} bind:this={deleteTableModal} />
|
||||
<DeleteConfirmationModal source={table} bind:this={deleteTableModal} />
|
||||
{/if}
|
||||
|
||||
{#if editableView}
|
||||
<EditViewModal view={editableView} bind:this={editViewModal} />
|
||||
<DeleteViewModal view={editableView} bind:this={deleteViewModal} />
|
||||
<DeleteConfirmationModal source={editableView} bind:this={deleteViewModal} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
} from "@/dataBinding"
|
||||
import { ActionButton, notifications } from "@budibase/bbui"
|
||||
import { capitalise } from "@/helpers"
|
||||
import { builderStore } from "@/stores/builder"
|
||||
import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
|
||||
import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js"
|
||||
|
||||
|
@ -55,6 +56,17 @@
|
|||
$: id = $selectedComponent?._id
|
||||
$: id, (section = tabs[0])
|
||||
$: 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>
|
||||
|
||||
{#if $selectedComponent}
|
||||
|
@ -98,7 +110,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
</span>
|
||||
{#if section == "settings"}
|
||||
{#if section === "settings"}
|
||||
<TourWrap
|
||||
stepKeys={[
|
||||
BUILDER_FORM_CREATE_STEPS,
|
||||
|
@ -115,7 +127,7 @@
|
|||
/>
|
||||
</TourWrap>
|
||||
{/if}
|
||||
{#if section == "styles"}
|
||||
{#if section === "styles"}
|
||||
<DesignSection
|
||||
{componentInstance}
|
||||
{componentBindings}
|
||||
|
@ -130,7 +142,7 @@
|
|||
componentTitle={title}
|
||||
/>
|
||||
{/if}
|
||||
{#if section == "conditions"}
|
||||
{#if section === "conditions"}
|
||||
<ConditionalUISection
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
<Icon name="DragHandle" size="XL" />
|
||||
</div>
|
||||
<Select
|
||||
placeholder={null}
|
||||
placeholder={false}
|
||||
options={actionOptions}
|
||||
bind:value={condition.action}
|
||||
/>
|
||||
|
@ -227,7 +227,7 @@
|
|||
on:change={e => (condition.newValue = e.detail)}
|
||||
/>
|
||||
<Select
|
||||
placeholder={null}
|
||||
placeholder={false}
|
||||
options={getOperatorOptions(condition)}
|
||||
bind:value={condition.operator}
|
||||
on:change={e => onOperatorChange(condition, e.detail)}
|
||||
|
@ -236,7 +236,7 @@
|
|||
disabled={condition.noValue || condition.operator === "oneOf"}
|
||||
options={valueTypeOptions}
|
||||
bind:value={condition.valueType}
|
||||
placeholder={null}
|
||||
placeholder={false}
|
||||
on:change={e => onValueTypeChange(condition, e.detail)}
|
||||
/>
|
||||
{#if ["string", "number"].includes(condition.valueType)}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { componentStore } from "@/stores/builder"
|
||||
import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte"
|
||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||
import { builderStore } from "@/stores/builder"
|
||||
|
||||
export let componentInstance
|
||||
export let componentDefinition
|
||||
|
@ -18,6 +19,8 @@
|
|||
let tempValue
|
||||
let drawer
|
||||
|
||||
$: highlighted = $builderStore.highlightedSetting?.key === "_conditions"
|
||||
|
||||
const openDrawer = () => {
|
||||
tempValue = JSON.parse(JSON.stringify(componentInstance?._conditions ?? []))
|
||||
drawer.show()
|
||||
|
@ -52,7 +55,9 @@
|
|||
/>
|
||||
|
||||
<DetailSummary name={"Conditions"} collapsible={false}>
|
||||
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
|
||||
<div class:highlighted>
|
||||
<ActionButton fullWidth on:click={openDrawer}>{conditionText}</ActionButton>
|
||||
</div>
|
||||
</DetailSummary>
|
||||
<Drawer bind:this={drawer} title="Conditions">
|
||||
<svelte:fragment slot="description">
|
||||
|
@ -61,3 +66,13 @@
|
|||
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
|
||||
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
|
||||
</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,
|
||||
runtimeToReadableBinding,
|
||||
} from "@/dataBinding"
|
||||
import { builderStore } from "@/stores/builder"
|
||||
|
||||
export let componentInstance
|
||||
export let componentDefinition
|
||||
|
@ -32,6 +33,8 @@
|
|||
|
||||
$: icon = componentDefinition?.icon
|
||||
|
||||
$: highlighted = $builderStore.highlightedSetting?.key === "_styles"
|
||||
|
||||
const openDrawer = () => {
|
||||
tempValue = runtimeToReadableBinding(
|
||||
bindings,
|
||||
|
@ -55,7 +58,7 @@
|
|||
name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`}
|
||||
collapsible={false}
|
||||
>
|
||||
<div>
|
||||
<div class:highlighted>
|
||||
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton>
|
||||
</div>
|
||||
</DetailSummary>
|
||||
|
@ -97,4 +100,12 @@
|
|||
align-items: center;
|
||||
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>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<Layout gap="XS" paddingX="L" paddingY="XL">
|
||||
<Layout gap="XS" paddingX="XL" paddingY="XL">
|
||||
{#if activeTab === "theme"}
|
||||
<ThemePanel />
|
||||
{:else}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
if (id === `${$screenStore.selectedScreenId}-screen`) 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
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import AppPreview from "./AppPreview.svelte"
|
||||
import { screenStore, appStore } from "@/stores/builder"
|
||||
import UndoRedoControl from "@/components/common/UndoRedoControl.svelte"
|
||||
import ScreenErrorsButton from "./ScreenErrorsButton.svelte"
|
||||
import { Divider } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<div class="app-panel">
|
||||
|
@ -15,6 +17,8 @@
|
|||
{#if $appStore.clientFeatures.devicePreview}
|
||||
<DevicePreviewSelect />
|
||||
{/if}
|
||||
<Divider vertical />
|
||||
<ScreenErrorsButton />
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
@ -50,6 +54,9 @@
|
|||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
}
|
||||
.header-left :global(div) {
|
||||
border-right: none;
|
||||
}
|
||||
|
@ -59,7 +66,7 @@
|
|||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
|
|
|
@ -183,16 +183,6 @@
|
|||
toggleAddComponent()
|
||||
} else if (type === "highlight-setting") {
|
||||
builderStore.highlightSetting(data.setting, "error")
|
||||
|
||||
// Also scroll setting into view
|
||||
const selector = `#${data.setting}-prop-control`
|
||||
const element = document.querySelector(selector)?.parentElement
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
})
|
||||
}
|
||||
} else if (type === "eject-block") {
|
||||
const { id, definition } = data
|
||||
await componentStore.handleEjectBlock(id, definition)
|
||||
|
|
|
@ -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>
|
||||
import { notifications, Icon, Body } from "@budibase/bbui"
|
||||
import { isActive, goto } from "@roxi/routify"
|
||||
import { notifications, Icon } from "@budibase/bbui"
|
||||
import {
|
||||
selectedScreen,
|
||||
screenStore,
|
||||
|
@ -13,23 +12,12 @@
|
|||
import ComponentTree from "./ComponentTree.svelte"
|
||||
import { dndStore, DropPosition } from "./dndStore.js"
|
||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||
import getScreenContextMenuItems from "./getScreenContextMenuItems"
|
||||
|
||||
let scrolling = false
|
||||
|
||||
$: screenComponentId = `${$screenStore.selectedScreenId}-screen`
|
||||
$: navComponentId = `${$screenStore.selectedScreenId}-navigation`
|
||||
|
||||
const toNewComponentRoute = () => {
|
||||
if ($isActive(`./:componentId/new`)) {
|
||||
$goto(`./:componentId`)
|
||||
} else {
|
||||
$goto(`./:componentId/new`)
|
||||
}
|
||||
}
|
||||
|
||||
const onDrop = async () => {
|
||||
try {
|
||||
await dndStore.actions.drop()
|
||||
|
@ -39,10 +27,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
const handleScroll = e => {
|
||||
scrolling = e.target.scrollTop !== 0
|
||||
}
|
||||
|
||||
const hover = hoverStore.hover
|
||||
|
||||
// showCopy is used to hide the copy button when the user right-clicks the empty
|
||||
|
@ -72,17 +56,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<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">
|
||||
<ComponentScrollWrapper on:scroll={handleScroll}>
|
||||
<ComponentScrollWrapper>
|
||||
<ul
|
||||
class="componentTree"
|
||||
on:contextmenu={e => openScreenContextMenu(e, false)}
|
||||
|
@ -159,7 +135,6 @@
|
|||
</ul>
|
||||
</ComponentScrollWrapper>
|
||||
</div>
|
||||
<ComponentKeyHandler />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -168,35 +143,13 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.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);
|
||||
padding-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.components :global(.nav-item) {
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-left: auto;
|
||||
color: var(--grey-7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.addButton:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.list-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,25 +1,60 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import ScreenList from "./ScreenList/index.svelte"
|
||||
import ComponentList from "./ComponentList/index.svelte"
|
||||
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 Tabs = {
|
||||
Components: "Components",
|
||||
Bindings: "Bindings",
|
||||
State: "State",
|
||||
}
|
||||
|
||||
let activeTab = Tabs.Components
|
||||
</script>
|
||||
|
||||
<div class="panel" use:resizable>
|
||||
<div class="content">
|
||||
<ScreenList />
|
||||
<ComponentList />
|
||||
<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 />
|
||||
{:else if activeTab === Tabs.Bindings}
|
||||
<BindingsPanel />
|
||||
{:else if activeTab === Tabs.State}
|
||||
<div class="tab-content"><StatePanel /></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider">
|
||||
<div class="dividerClickExtender" role="separator" use:resizableHandle />
|
||||
</div>
|
||||
</div>
|
||||
<ComponentKeyHandler />
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
display: flex;
|
||||
min-width: 270px;
|
||||
min-width: 310px;
|
||||
width: 310px;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -34,6 +69,34 @@
|
|||
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 {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
@ -45,7 +108,6 @@
|
|||
background: var(--spectrum-global-color-gray-300);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.dividerClickExtender {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
<script lang="ts">
|
||||
import type { UIComponentError } from "@budibase/types"
|
||||
import {
|
||||
builderStore,
|
||||
componentStore,
|
||||
screenComponentErrorList,
|
||||
screenComponentsList,
|
||||
} from "@/stores/builder"
|
||||
import {
|
||||
AbsTooltip,
|
||||
ActionButton,
|
||||
Icon,
|
||||
Link,
|
||||
Popover,
|
||||
PopoverAlignment,
|
||||
TooltipPosition,
|
||||
} from "@budibase/bbui"
|
||||
import CircleIndicator from "@/components/common/Icons/CircleIndicator.svelte"
|
||||
|
||||
let button: any
|
||||
let popover: any
|
||||
|
||||
$: hasErrors = !!$screenComponentErrorList.length
|
||||
|
||||
function getErrorTitle(error: UIComponentError) {
|
||||
const titleParts = [
|
||||
$screenComponentsList.find(c => c._id === error.componentId)!
|
||||
._instanceName,
|
||||
]
|
||||
if (error.errorType === "setting" && error.cause === "invalid") {
|
||||
titleParts.push(error.label)
|
||||
}
|
||||
return titleParts.join(" - ")
|
||||
}
|
||||
|
||||
async function onErrorClick(error: UIComponentError) {
|
||||
componentStore.select(error.componentId)
|
||||
if (error.errorType === "setting") {
|
||||
builderStore.highlightSetting(error.key, "error")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={button} class="error-button">
|
||||
<AbsTooltip
|
||||
text={!hasErrors ? "No errors found!" : ""}
|
||||
position={TooltipPosition.Top}
|
||||
>
|
||||
<ActionButton
|
||||
quiet
|
||||
disabled={!hasErrors}
|
||||
on:click={() => popover.show()}
|
||||
size="M"
|
||||
icon="Alert"
|
||||
/>
|
||||
{#if hasErrors}
|
||||
<div class="error-indicator">
|
||||
<CircleIndicator
|
||||
size="S"
|
||||
color="var(--spectrum-global-color-static-red-600)"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</AbsTooltip>
|
||||
</div>
|
||||
<Popover
|
||||
bind:this={popover}
|
||||
anchor={button}
|
||||
align={PopoverAlignment.Right}
|
||||
maxWidth={400}
|
||||
showPopover={hasErrors}
|
||||
>
|
||||
<div class="error-popover">
|
||||
{#each $screenComponentErrorList as error}
|
||||
<div class="error">
|
||||
<Icon
|
||||
name="Alert"
|
||||
color="var(--spectrum-global-color-static-red-600)"
|
||||
size="S"
|
||||
/>
|
||||
<div>
|
||||
<Link overBackground on:click={() => onErrorClick(error)}>
|
||||
{getErrorTitle(error)}
|
||||
</Link>:
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html error.message}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.error-button {
|
||||
position: relative;
|
||||
}
|
||||
.error-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 8px;
|
||||
}
|
||||
.error-popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.error-popover .error {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
padding: var(--spacing-m);
|
||||
gap: var(--spacing-s);
|
||||
align-items: start;
|
||||
}
|
||||
.error-popover .error:not(:last-child) {
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
|
||||
.error-popover .error :global(mark) {
|
||||
background: unset;
|
||||
color: unset;
|
||||
}
|
||||
.error-popover .error :global(.spectrum-Link) {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
|
@ -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", () => {
|
||||
let instance = null
|
||||
|
||||
const setupDOM = () => {
|
||||
instance = render(AISettings, {})
|
||||
const modalContainer = document.createElement("div")
|
||||
modalContainer.classList.add("modal-container")
|
||||
instance.baseElement.appendChild(modalContainer)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("that the AISettings is rendered", () => {
|
||||
instance = render(AISettings, {})
|
||||
setupDOM()
|
||||
expect(instance).toBeDefined()
|
||||
})
|
||||
|
||||
describe("Licensing", () => {
|
||||
it("should show the premium label on self host for custom configs", async () => {
|
||||
setupEnv(Hosting.Self)
|
||||
instance = render(AISettings, {})
|
||||
setupDOM()
|
||||
const premiumTag = instance.queryByText("Premium")
|
||||
expect(premiumTag).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should show the enterprise label on cloud for custom configs", async () => {
|
||||
setupEnv(Hosting.Cloud)
|
||||
instance = render(AISettings, {})
|
||||
setupDOM()
|
||||
const enterpriseTag = instance.queryByText("Enterprise")
|
||||
expect(enterpriseTag).toBeInTheDocument()
|
||||
})
|
||||
|
@ -69,7 +76,7 @@ describe("AISettings", () => {
|
|||
let configModal
|
||||
|
||||
setupEnv(Hosting.Cloud)
|
||||
instance = render(AISettings)
|
||||
setupDOM()
|
||||
addConfigurationButton = instance.queryByText("Add configuration")
|
||||
expect(addConfigurationButton).toBeInTheDocument()
|
||||
await fireEvent.click(addConfigurationButton)
|
||||
|
@ -86,7 +93,7 @@ describe("AISettings", () => {
|
|||
{ customAIConfigsEnabled: true },
|
||||
{ AI_CUSTOM_CONFIGS: true }
|
||||
)
|
||||
instance = render(AISettings)
|
||||
setupDOM()
|
||||
addConfigurationButton = instance.queryByText("Add configuration")
|
||||
expect(addConfigurationButton).toBeInTheDocument()
|
||||
await fireEvent.click(addConfigurationButton)
|
||||
|
@ -103,7 +110,7 @@ describe("AISettings", () => {
|
|||
{ customAIConfigsEnabled: true },
|
||||
{ AI_CUSTOM_CONFIGS: true }
|
||||
)
|
||||
instance = render(AISettings)
|
||||
setupDOM()
|
||||
addConfigurationButton = instance.queryByText("Add configuration")
|
||||
expect(addConfigurationButton).toBeInTheDocument()
|
||||
await fireEvent.click(addConfigurationButton)
|
||||
|
|
|
@ -2,7 +2,7 @@ import { derived, get } from "svelte/store"
|
|||
import { API } from "@/api"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { generate } from "shortid"
|
||||
import { createHistoryStore } from "@/stores/builder/history"
|
||||
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { tables, appStore } from "@/stores/builder"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -1428,7 +1428,7 @@ const automationActions = (store: AutomationStore) => ({
|
|||
})
|
||||
|
||||
class AutomationStore extends BudiStore<AutomationState> {
|
||||
history: any
|
||||
history: HistoryStore<Automation>
|
||||
actions: ReturnType<typeof automationActions>
|
||||
|
||||
constructor() {
|
||||
|
@ -1437,8 +1437,6 @@ class AutomationStore extends BudiStore<AutomationState> {
|
|||
this.history = createHistoryStore({
|
||||
getDoc: this.actions.getDefinition.bind(this),
|
||||
selectDoc: this.actions.select.bind(this),
|
||||
beforeAction: () => {},
|
||||
afterAction: () => {},
|
||||
})
|
||||
|
||||
// Then wrap save and delete with history
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { get } from "svelte/store"
|
||||
import { selectedScreen as selectedScreenStore } from "./screens"
|
||||
import { findComponentPath } from "@/helpers/components"
|
||||
import { Screen, Component } from "@budibase/types"
|
||||
import { Component, Screen } from "@budibase/types"
|
||||
import { BudiStore, PersistenceType } from "@/stores/BudiStore"
|
||||
|
||||
interface OpenNodesState {
|
||||
|
|
|
@ -20,6 +20,8 @@ import {
|
|||
previewStore,
|
||||
tables,
|
||||
componentTreeNodesStore,
|
||||
builderStore,
|
||||
screenComponentsList,
|
||||
} from "@/stores/builder"
|
||||
import { buildFormSchema, getSchemaForDatasource } from "@/dataBinding"
|
||||
import {
|
||||
|
@ -30,8 +32,21 @@ import {
|
|||
} from "@/constants/backend"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import { Component, FieldType, Screen, Table } from "@budibase/types"
|
||||
import {
|
||||
ComponentDefinition,
|
||||
ComponentSetting,
|
||||
Component as ComponentType,
|
||||
ComponentCondition,
|
||||
FieldType,
|
||||
Screen,
|
||||
Table,
|
||||
} from "@budibase/types"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { getSequentialName } from "@/helpers/duplicate"
|
||||
|
||||
interface Component extends ComponentType {
|
||||
_id: string
|
||||
}
|
||||
|
||||
export interface ComponentState {
|
||||
components: Record<string, ComponentDefinition>
|
||||
|
@ -42,29 +57,6 @@ export interface ComponentState {
|
|||
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 = {
|
||||
components: {},
|
||||
customComponents: [],
|
||||
|
@ -254,7 +246,10 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
* @param {object} opts
|
||||
* @returns
|
||||
*/
|
||||
enrichEmptySettings(component: Component, opts: any) {
|
||||
enrichEmptySettings(
|
||||
component: Component,
|
||||
opts: { screen?: Screen; parent?: Component; useDefaultValues?: boolean }
|
||||
) {
|
||||
if (!component?._component) {
|
||||
return
|
||||
}
|
||||
|
@ -364,7 +359,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
getSchemaForDatasource(screen, dataSource, {})
|
||||
|
||||
// Finds fields by types from the schema of the configured datasource
|
||||
const findFieldTypes = (fieldTypes: any) => {
|
||||
const findFieldTypes = (fieldTypes: FieldType | FieldType[]) => {
|
||||
if (!Array.isArray(fieldTypes)) {
|
||||
fieldTypes = [fieldTypes]
|
||||
}
|
||||
|
@ -439,19 +434,32 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
* @param {object} parent
|
||||
* @returns
|
||||
*/
|
||||
createInstance(componentName: string, presetProps: any, parent: any) {
|
||||
createInstance(
|
||||
componentType: string,
|
||||
presetProps: any,
|
||||
parent: any
|
||||
): Component | null {
|
||||
const screen = get(selectedScreen)
|
||||
if (!screen) {
|
||||
throw "A valid screen must be selected"
|
||||
}
|
||||
|
||||
const definition = this.getDefinition(componentName)
|
||||
const definition = this.getDefinition(componentType)
|
||||
if (!definition) {
|
||||
return null
|
||||
}
|
||||
|
||||
const componentName = getSequentialName(
|
||||
get(screenComponentsList),
|
||||
`New ${definition.friendlyName || definition.name}`,
|
||||
{
|
||||
getName: c => c._instanceName,
|
||||
separator: " ",
|
||||
}
|
||||
)
|
||||
|
||||
// Generate basic component structure
|
||||
let instance = {
|
||||
let instance: Component = {
|
||||
_id: Helpers.uuid(),
|
||||
_component: definition.component,
|
||||
_styles: {
|
||||
|
@ -459,7 +467,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
hover: {},
|
||||
active: {},
|
||||
},
|
||||
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
||||
_instanceName: componentName,
|
||||
...presetProps,
|
||||
}
|
||||
|
||||
|
@ -478,16 +486,16 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
}
|
||||
|
||||
// Custom post processing for creation only
|
||||
let extras: any = {}
|
||||
let extras: Partial<Component> = {}
|
||||
if (definition.hasChildren) {
|
||||
extras._children = []
|
||||
}
|
||||
|
||||
// Add step name to form steps
|
||||
if (componentName.endsWith("/formstep")) {
|
||||
if (componentType.endsWith("/formstep")) {
|
||||
const parentForm = findClosestMatchingComponent(
|
||||
screen.props,
|
||||
get(selectedComponent)._id,
|
||||
get(selectedComponent)?._id,
|
||||
(component: Component) => component._component.endsWith("/form")
|
||||
)
|
||||
const formSteps = findAllMatchingComponents(
|
||||
|
@ -513,14 +521,14 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
* @returns
|
||||
*/
|
||||
async create(
|
||||
componentName: string,
|
||||
componentType: string,
|
||||
presetProps: any,
|
||||
parent: any,
|
||||
parent: Component,
|
||||
index: number
|
||||
) {
|
||||
const state = get(this.store)
|
||||
const componentInstance = this.createInstance(
|
||||
componentName,
|
||||
componentType,
|
||||
presetProps,
|
||||
parent
|
||||
)
|
||||
|
@ -716,14 +724,16 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} componentId
|
||||
*/
|
||||
select(componentId: string) {
|
||||
select(id: string) {
|
||||
this.update(state => {
|
||||
state.selectedComponentId = componentId
|
||||
return state
|
||||
// Only clear highlights if selecting a different component
|
||||
if (!id.includes(state.selectedComponentId!)) {
|
||||
builderStore.highlightSetting()
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
selectedComponentId: id,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -772,7 +782,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
if (!cut) {
|
||||
componentToPaste = makeComponentUnique(componentToPaste)
|
||||
}
|
||||
newComponentId = componentToPaste._id!
|
||||
newComponentId = componentToPaste._id
|
||||
|
||||
// Strip grid position metadata if pasting into a new screen, but keep
|
||||
// alignment metadata
|
||||
|
@ -915,7 +925,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
|
||||
// If we have children, select first child, and the node is not collapsed
|
||||
if (
|
||||
component._children?.length &&
|
||||
component?._children?.length &&
|
||||
(state.selectedComponentId === navComponentId ||
|
||||
componentTreeNodesStore.isNodeExpanded(component._id))
|
||||
) {
|
||||
|
@ -1105,7 +1115,7 @@ export class ComponentStore extends BudiStore<ComponentState> {
|
|||
})
|
||||
}
|
||||
|
||||
async updateConditions(conditions: Record<string, any>) {
|
||||
async updateConditions(conditions: ComponentCondition[]) {
|
||||
await this.patch((component: Component) => {
|
||||
component._conditions = conditions
|
||||
})
|
||||
|
@ -1339,12 +1349,15 @@ export const componentStore = new ComponentStore()
|
|||
|
||||
export const selectedComponent = derived(
|
||||
[componentStore, selectedScreen],
|
||||
([$store, $selectedScreen]) => {
|
||||
([$store, $selectedScreen]): Component | null => {
|
||||
if (
|
||||
$selectedScreen &&
|
||||
$store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`)
|
||||
) {
|
||||
return $selectedScreen?.props
|
||||
return {
|
||||
...$selectedScreen.props,
|
||||
_id: $selectedScreen.props._id!,
|
||||
}
|
||||
}
|
||||
if (!$selectedScreen || !$store.selectedComponentId) {
|
||||
return null
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
import * as jsonpatch from "fast-json-patch/index.mjs"
|
||||
import { writable, derived, get } from "svelte/store"
|
||||
import { Document } from "@budibase/types"
|
||||
import * as jsonpatch from "fast-json-patch"
|
||||
import { writable, derived, get, Readable } from "svelte/store"
|
||||
|
||||
export const Operations = {
|
||||
Add: "Add",
|
||||
Delete: "Delete",
|
||||
Change: "Change",
|
||||
export const enum Operations {
|
||||
Add = "Add",
|
||||
Delete = "Delete",
|
||||
Change = "Change",
|
||||
}
|
||||
|
||||
interface Operator<T extends Document> {
|
||||
id?: number
|
||||
type: Operations
|
||||
doc: T
|
||||
forwardPatch?: jsonpatch.Operation[]
|
||||
backwardsPatch?: jsonpatch.Operation[]
|
||||
}
|
||||
|
||||
interface HistoryState<T extends Document> {
|
||||
history: Operator<T>[]
|
||||
position: number
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export const initialState = {
|
||||
|
@ -13,14 +28,38 @@ export const initialState = {
|
|||
loading: false,
|
||||
}
|
||||
|
||||
export const createHistoryStore = ({
|
||||
export interface HistoryStore<T extends Document>
|
||||
extends Readable<
|
||||
HistoryState<T> & {
|
||||
canUndo: boolean
|
||||
canRedo: boolean
|
||||
}
|
||||
> {
|
||||
wrapSaveDoc: (
|
||||
fn: (doc: T) => Promise<T>
|
||||
) => (doc: T, operationId?: number) => Promise<T>
|
||||
wrapDeleteDoc: (
|
||||
fn: (doc: T) => Promise<void>
|
||||
) => (doc: T, operationId?: number) => Promise<void>
|
||||
|
||||
reset: () => void
|
||||
undo: () => Promise<void>
|
||||
redo: () => Promise<void>
|
||||
}
|
||||
|
||||
export const createHistoryStore = <T extends Document>({
|
||||
getDoc,
|
||||
selectDoc,
|
||||
beforeAction,
|
||||
afterAction,
|
||||
}) => {
|
||||
}: {
|
||||
getDoc: (id: string) => T | undefined
|
||||
selectDoc: (id: string) => void
|
||||
beforeAction?: (operation?: Operator<T>) => void
|
||||
afterAction?: (operation?: Operator<T>) => void
|
||||
}): HistoryStore<T> => {
|
||||
// Use a derived store to check if we are able to undo or redo any operations
|
||||
const store = writable(initialState)
|
||||
const store = writable<HistoryState<T>>(initialState)
|
||||
const derivedStore = derived(store, $store => {
|
||||
return {
|
||||
...$store,
|
||||
|
@ -31,8 +70,8 @@ export const createHistoryStore = ({
|
|||
|
||||
// Wrapped versions of essential functions which we call ourselves when using
|
||||
// undo and redo
|
||||
let saveFn
|
||||
let deleteFn
|
||||
let saveFn: (doc: T, operationId?: number) => Promise<T>
|
||||
let deleteFn: (doc: T, operationId?: number) => Promise<void>
|
||||
|
||||
/**
|
||||
* Internal util to set the loading flag
|
||||
|
@ -66,7 +105,7 @@ export const createHistoryStore = ({
|
|||
* For internal use only.
|
||||
* @param operation the operation to save
|
||||
*/
|
||||
const saveOperation = operation => {
|
||||
const saveOperation = (operation: Operator<T>) => {
|
||||
store.update(state => {
|
||||
// Update history
|
||||
let history = state.history
|
||||
|
@ -93,15 +132,15 @@ export const createHistoryStore = ({
|
|||
* @param fn the save function
|
||||
* @returns {function} a wrapped version of the save function
|
||||
*/
|
||||
const wrapSaveDoc = fn => {
|
||||
saveFn = async (doc, operationId) => {
|
||||
const wrapSaveDoc = (fn: (doc: T) => Promise<T>) => {
|
||||
saveFn = async (doc: T, operationId?: number) => {
|
||||
// Only works on a single doc at a time
|
||||
if (!doc || Array.isArray(doc)) {
|
||||
return
|
||||
}
|
||||
startLoading()
|
||||
try {
|
||||
const oldDoc = getDoc(doc._id)
|
||||
const oldDoc = getDoc(doc._id!)
|
||||
const newDoc = jsonpatch.deepClone(await fn(doc))
|
||||
|
||||
// Store the change
|
||||
|
@ -141,8 +180,8 @@ export const createHistoryStore = ({
|
|||
* @param fn the delete function
|
||||
* @returns {function} a wrapped version of the delete function
|
||||
*/
|
||||
const wrapDeleteDoc = fn => {
|
||||
deleteFn = async (doc, operationId) => {
|
||||
const wrapDeleteDoc = (fn: (doc: T) => Promise<void>) => {
|
||||
deleteFn = async (doc: T, operationId?: number) => {
|
||||
// Only works on a single doc at a time
|
||||
if (!doc || Array.isArray(doc)) {
|
||||
return
|
||||
|
@ -201,7 +240,7 @@ export const createHistoryStore = ({
|
|||
// Undo ADD
|
||||
if (operation.type === Operations.Add) {
|
||||
// Try to get the latest doc version to delete
|
||||
const latestDoc = getDoc(operation.doc._id)
|
||||
const latestDoc = getDoc(operation.doc._id!)
|
||||
const doc = latestDoc || operation.doc
|
||||
await deleteFn(doc, operation.id)
|
||||
}
|
||||
|
@ -219,7 +258,7 @@ export const createHistoryStore = ({
|
|||
// Undo CHANGE
|
||||
else {
|
||||
// Get the current doc and apply the backwards patch on top of it
|
||||
let doc = jsonpatch.deepClone(getDoc(operation.doc._id))
|
||||
let doc = jsonpatch.deepClone(getDoc(operation.doc._id!))
|
||||
if (doc) {
|
||||
jsonpatch.applyPatch(
|
||||
doc,
|
||||
|
@ -283,7 +322,7 @@ export const createHistoryStore = ({
|
|||
// Redo DELETE
|
||||
else if (operation.type === Operations.Delete) {
|
||||
// Try to get the latest doc version to delete
|
||||
const latestDoc = getDoc(operation.doc._id)
|
||||
const latestDoc = getDoc(operation.doc._id!)
|
||||
const doc = latestDoc || operation.doc
|
||||
await deleteFn(doc, operation.id)
|
||||
}
|
||||
|
@ -291,7 +330,7 @@ export const createHistoryStore = ({
|
|||
// Redo CHANGE
|
||||
else {
|
||||
// Get the current doc and apply the forwards patch on top of it
|
||||
let doc = jsonpatch.deepClone(getDoc(operation.doc._id))
|
||||
let doc = jsonpatch.deepClone(getDoc(operation.doc._id!))
|
||||
if (doc) {
|
||||
jsonpatch.applyPatch(doc, jsonpatch.deepClone(operation.forwardPatch))
|
||||
await saveFn(doc, operation.id)
|
|
@ -16,7 +16,11 @@ import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
|
|||
import { deploymentStore } from "./deployments.js"
|
||||
import { contextMenuStore } from "./contextMenu.js"
|
||||
import { snippets } from "./snippets"
|
||||
import { screenComponentErrors } from "./screenComponent"
|
||||
import {
|
||||
screenComponentsList,
|
||||
screenComponentErrors,
|
||||
screenComponentErrorList,
|
||||
} from "./screenComponent"
|
||||
|
||||
// Backend
|
||||
import { tables } from "./tables"
|
||||
|
@ -68,7 +72,9 @@ export {
|
|||
snippets,
|
||||
rowActions,
|
||||
appPublished,
|
||||
screenComponentsList,
|
||||
screenComponentErrors,
|
||||
screenComponentErrorList,
|
||||
}
|
||||
|
||||
export const reset = () => {
|
||||
|
|
|
@ -82,6 +82,10 @@ export class PreviewStore extends BudiStore<PreviewState> {
|
|||
}))
|
||||
}
|
||||
|
||||
updateState(data: Record<string, any>) {
|
||||
this.sendEvent("builder-state", data)
|
||||
}
|
||||
|
||||
requestComponentContext() {
|
||||
this.sendEvent("request-context")
|
||||
}
|
||||
|
|
|
@ -2,24 +2,29 @@ import { derived } from "svelte/store"
|
|||
import { tables } from "./tables"
|
||||
import { selectedScreen } from "./screens"
|
||||
import { viewsV2 } from "./viewsV2"
|
||||
import { findComponentsBySettingsType } from "@/helpers/screen"
|
||||
import { UIDatasourceType, Screen } from "@budibase/types"
|
||||
import {
|
||||
UIDatasourceType,
|
||||
Component,
|
||||
UIComponentError,
|
||||
ComponentDefinition,
|
||||
DependsOnComponentSetting,
|
||||
} from "@budibase/types"
|
||||
import { queries } from "./queries"
|
||||
import { views } from "./views"
|
||||
import { bindings, featureFlag } from "@/helpers"
|
||||
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>(
|
||||
key: TKey,
|
||||
list: TItem[]
|
||||
): Record<string, any> {
|
||||
return list.reduce(
|
||||
(result, item) => ({
|
||||
...result,
|
||||
[item[key] as string]: item,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
): Record<string, TItem> {
|
||||
return list.reduce<Record<string, TItem>>((result, item) => {
|
||||
result[item[key] as string] = item
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
const friendlyNameByType: Partial<Record<UIDatasourceType, string>> = {
|
||||
|
@ -37,63 +42,29 @@ const validationKeyByType: Record<UIDatasourceType, string | null> = {
|
|||
jsonarray: "value",
|
||||
}
|
||||
|
||||
export const screenComponentErrors = derived(
|
||||
[selectedScreen, tables, views, viewsV2, queries],
|
||||
([$selectedScreen, $tables, $views, $viewsV2, $queries]): Record<
|
||||
string,
|
||||
string[]
|
||||
> => {
|
||||
if (!featureFlag.isEnabled("CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS")) {
|
||||
return {}
|
||||
export const screenComponentsList = derived(
|
||||
[selectedScreen],
|
||||
([$selectedScreen]): Component[] => {
|
||||
if (!$selectedScreen) {
|
||||
return []
|
||||
}
|
||||
function getInvalidDatasources(
|
||||
screen: Screen,
|
||||
datasources: Record<string, any>
|
||||
) {
|
||||
const result: Record<string, string[]> = {}
|
||||
for (const { component, setting } of findComponentsBySettingsType(
|
||||
screen,
|
||||
["table", "dataSource"]
|
||||
)) {
|
||||
const componentSettings = component[setting.key]
|
||||
if (!componentSettings) {
|
||||
continue
|
||||
}
|
||||
const { label } = componentSettings
|
||||
const type = componentSettings.type as UIDatasourceType
|
||||
|
||||
const validationKey = validationKeyByType[type]
|
||||
if (!validationKey) {
|
||||
continue
|
||||
}
|
||||
return findAllComponents($selectedScreen.props)
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
export const screenComponentErrorList = derived(
|
||||
[selectedScreen, tables, views, viewsV2, queries, componentStore],
|
||||
([
|
||||
$selectedScreen,
|
||||
$tables,
|
||||
$views,
|
||||
$viewsV2,
|
||||
$queries,
|
||||
$componentStore,
|
||||
]): UIComponentError[] => {
|
||||
if (!$selectedScreen) {
|
||||
return []
|
||||
}
|
||||
|
||||
const datasources = {
|
||||
|
@ -103,11 +74,208 @@ export const screenComponentErrors = derived(
|
|||
...reduceBy("_id", $queries.list),
|
||||
}
|
||||
|
||||
if (!$selectedScreen) {
|
||||
// Skip validation if a screen is not selected.
|
||||
return {}
|
||||
const { components: definitions } = $componentStore
|
||||
|
||||
const errors: UIComponentError[] = []
|
||||
|
||||
function checkComponentErrors(component: Component, ancestors: string[]) {
|
||||
errors.push(...getInvalidDatasources(component, datasources, definitions))
|
||||
errors.push(...getMissingRequiredSettings(component, definitions))
|
||||
errors.push(...getMissingAncestors(component, definitions, ancestors))
|
||||
|
||||
for (const child of component._children || []) {
|
||||
checkComponentErrors(child, [...ancestors, component._component])
|
||||
}
|
||||
}
|
||||
|
||||
return getInvalidDatasources($selectedScreen, datasources)
|
||||
checkComponentErrors($selectedScreen?.props, [])
|
||||
|
||||
return errors
|
||||
}
|
||||
)
|
||||
|
||||
function getInvalidDatasources(
|
||||
component: Component,
|
||||
datasources: Record<string, any>,
|
||||
definitions: Record<string, ComponentDefinition>
|
||||
) {
|
||||
const result: UIComponentError[] = []
|
||||
|
||||
const datasourceTypes = ["table", "dataSource"]
|
||||
|
||||
const possibleSettings = definitions[component._component]?.settings?.filter(
|
||||
s => datasourceTypes.includes(s.type)
|
||||
)
|
||||
if (possibleSettings) {
|
||||
for (const setting of possibleSettings) {
|
||||
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.push({
|
||||
componentId: component._id!,
|
||||
key: setting.key,
|
||||
label: setting.label || setting.key,
|
||||
message: `The ${friendlyTypeName} named "${label}" could not be found`,
|
||||
|
||||
errorType: "setting",
|
||||
cause: "invalid",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function parseDependsOn(dependsOn: DependsOnComponentSetting | undefined): {
|
||||
key?: string
|
||||
value?: string
|
||||
} {
|
||||
if (dependsOn === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (typeof dependsOn === "string") {
|
||||
return { key: dependsOn }
|
||||
}
|
||||
|
||||
return { key: dependsOn.setting, value: dependsOn.value }
|
||||
}
|
||||
|
||||
function getMissingRequiredSettings(
|
||||
component: Component,
|
||||
definitions: Record<string, ComponentDefinition>
|
||||
) {
|
||||
const result: UIComponentError[] = []
|
||||
|
||||
const definition = definitions[component._component]
|
||||
|
||||
const settings = getSettingsDefinition(definition)
|
||||
|
||||
const missingRequiredSettings = settings.filter(setting => {
|
||||
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 { key: dependsOnKey, value: dependsOnValue } = parseDependsOn(
|
||||
setting.dependsOn
|
||||
)
|
||||
const realDependentValue =
|
||||
component[dependsOnKey as keyof typeof component]
|
||||
|
||||
const { key: sectionDependsOnKey, value: sectionDependsOnValue } =
|
||||
parseDependsOn(setting.sectionDependsOn)
|
||||
const sectionRealDependentValue =
|
||||
component[sectionDependsOnKey as keyof typeof component]
|
||||
|
||||
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.push(
|
||||
...missingRequiredSettings.map<UIComponentError>(s => ({
|
||||
componentId: component._id!,
|
||||
key: s.key,
|
||||
label: s.label || s.key,
|
||||
message: `Add the <mark>${s.label}</mark> setting to start using your component`,
|
||||
errorType: "setting",
|
||||
cause: "missing",
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const BudibasePrefix = "@budibase/standard-components/"
|
||||
function getMissingAncestors(
|
||||
component: Component,
|
||||
definitions: Record<string, ComponentDefinition>,
|
||||
ancestors: string[]
|
||||
): UIComponentError[] {
|
||||
const definition = definitions[component._component]
|
||||
|
||||
if (!definition?.requiredAncestors?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result: UIComponentError[] = []
|
||||
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.push(
|
||||
...missingAncestors.map<UIComponentError>(ancestor => {
|
||||
const ancestorDefinition = definitions[`${BudibasePrefix}${ancestor}`]
|
||||
return {
|
||||
componentId: component._id!,
|
||||
message: `${pluralise(definition.name)} need to be inside a
|
||||
<mark>${ancestorDefinition.name}</mark>`,
|
||||
errorType: "ancestor-setting",
|
||||
ancestor: {
|
||||
name: ancestorDefinition.name,
|
||||
fullType: `${BudibasePrefix}${ancestor}`,
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const screenComponentErrors = derived(
|
||||
[screenComponentErrorList],
|
||||
([$list]): Record<string, UIComponentError[]> => {
|
||||
return $list.reduce<Record<string, UIComponentError[]>>((obj, error) => {
|
||||
obj[error.componentId] ??= []
|
||||
obj[error.componentId].push(error)
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
navigationStore,
|
||||
selectedComponent,
|
||||
} from "@/stores/builder"
|
||||
import { createHistoryStore } from "@/stores/builder/history"
|
||||
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
||||
import { API } from "@/api"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import {
|
||||
|
@ -19,8 +19,8 @@ import {
|
|||
Screen,
|
||||
Component,
|
||||
SaveScreenResponse,
|
||||
ComponentDefinition,
|
||||
} from "@budibase/types"
|
||||
import { ComponentDefinition } from "./components"
|
||||
|
||||
interface ScreenState {
|
||||
screens: Screen[]
|
||||
|
@ -33,9 +33,9 @@ export const initialScreenState: ScreenState = {
|
|||
|
||||
// Review the nulls
|
||||
export class ScreenStore extends BudiStore<ScreenState> {
|
||||
history: any
|
||||
delete: any
|
||||
save: any
|
||||
history: HistoryStore<Screen>
|
||||
delete: (screens: Screen) => Promise<void>
|
||||
save: (screen: Screen) => Promise<Screen>
|
||||
|
||||
constructor() {
|
||||
super(initialScreenState)
|
||||
|
@ -58,13 +58,12 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
getDoc: (id: string) =>
|
||||
get(this.store).screens?.find(screen => screen._id === id),
|
||||
selectDoc: this.select,
|
||||
beforeAction: () => {},
|
||||
afterAction: () => {
|
||||
// Ensure a valid component is selected
|
||||
if (!get(selectedComponent)) {
|
||||
this.update(state => ({
|
||||
componentStore.update(state => ({
|
||||
...state,
|
||||
selectedComponentId: get(selectedScreen)?.props._id,
|
||||
selectedComponentId: get(selectedScreen)?._id,
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
@ -281,7 +280,10 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
* supports deeply mutating the current doc rather than just appending data.
|
||||
*/
|
||||
sequentialScreenPatch = Utils.sequential(
|
||||
async (patchFn: (screen: Screen) => any, screenId: string) => {
|
||||
async (
|
||||
patchFn: (screen: Screen) => boolean,
|
||||
screenId: string
|
||||
): Promise<Screen | void> => {
|
||||
const state = get(this.store)
|
||||
const screen = state.screens.find(screen => screen._id === screenId)
|
||||
if (!screen) {
|
||||
|
@ -362,10 +364,10 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
* Any deleted screens will then have their routes/links purged
|
||||
*
|
||||
* Wrapped by {@link delete}
|
||||
* @param {Screen | Screen[]} screens
|
||||
* @param {Screen } screens
|
||||
*/
|
||||
async deleteScreen(screens: Screen | Screen[]) {
|
||||
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||
async deleteScreen(screen: Screen) {
|
||||
const screensToDelete = [screen]
|
||||
// Build array of promises to speed up bulk deletions
|
||||
let promises: Promise<DeleteScreenResponse>[] = []
|
||||
let deleteUrls: string[] = []
|
||||
|
@ -498,6 +500,13 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a list of screens that are used by a given source ID (table, view, datasource, query)
|
||||
*/
|
||||
async usageInScreens(sourceId: string) {
|
||||
return API.usageInScreens(sourceId)
|
||||
}
|
||||
}
|
||||
|
||||
export const screenStore = new ScreenStore()
|
||||
|
|
|
@ -11,11 +11,8 @@
|
|||
<script>
|
||||
import { getContext, setContext, onMount } from "svelte"
|
||||
import { writable, get } from "svelte/store"
|
||||
import {
|
||||
enrichProps,
|
||||
propsAreSame,
|
||||
getSettingsDefinition,
|
||||
} from "utils/componentProps"
|
||||
import { enrichProps, propsAreSame } from "utils/componentProps"
|
||||
import { getSettingsDefinition } from "@budibase/frontend-core"
|
||||
import {
|
||||
builderStore,
|
||||
devToolsStore,
|
||||
|
@ -29,7 +26,6 @@
|
|||
import EmptyPlaceholder from "components/app/EmptyPlaceholder.svelte"
|
||||
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
|
||||
import ComponentErrorState from "components/error-states/ComponentErrorState.svelte"
|
||||
import { BudibasePrefix } from "../stores/components.js"
|
||||
import {
|
||||
decodeJSBinding,
|
||||
findHBSBlocks,
|
||||
|
@ -102,8 +98,6 @@
|
|||
let definition
|
||||
let settingsDefinition
|
||||
let settingsDefinitionMap
|
||||
let missingRequiredSettings = false
|
||||
let componentErrors = false
|
||||
|
||||
// Temporary styles which can be added in the app preview for things like
|
||||
// DND. We clear these whenever a new instance is received.
|
||||
|
@ -141,18 +135,11 @@
|
|||
$: componentErrors = instance?._meta?.errors
|
||||
$: hasChildren = !!definition?.hasChildren
|
||||
$: showEmptyState = definition?.showEmptyState !== false
|
||||
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
|
||||
$: hasMissingRequiredSettings = !!componentErrors?.find(
|
||||
e => e.errorType === "setting"
|
||||
)
|
||||
$: editable = !!definition?.editable && !hasMissingRequiredSettings
|
||||
$: 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
|
||||
// the builder preview
|
||||
|
@ -218,7 +205,7 @@
|
|||
styles: normalStyles,
|
||||
draggable,
|
||||
definition,
|
||||
errored: errorState,
|
||||
errored: hasComponentErrors,
|
||||
}
|
||||
|
||||
// When dragging and dropping, pad components to allow dropping between
|
||||
|
@ -251,9 +238,8 @@
|
|||
name,
|
||||
editing,
|
||||
type: instance._component,
|
||||
errorState,
|
||||
errorState: hasComponentErrors,
|
||||
parent: id,
|
||||
ancestors: [...($component?.ancestors ?? []), instance._component],
|
||||
path: [...($component?.path ?? []), id],
|
||||
darkMode,
|
||||
})
|
||||
|
@ -310,40 +296,6 @@
|
|||
staticSettings = instanceSettings.staticSettings
|
||||
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
|
||||
// before storing the reference stringified version
|
||||
const noChildren = JSON.stringify({ ...instance, _children: null })
|
||||
|
@ -686,7 +638,7 @@
|
|||
class:pad
|
||||
class:parent={hasChildren}
|
||||
class:block={isBlock}
|
||||
class:error={errorState}
|
||||
class:error={hasComponentErrors}
|
||||
class:root={isRoot}
|
||||
data-id={id}
|
||||
data-name={name}
|
||||
|
@ -694,12 +646,8 @@
|
|||
data-parent={$component.id}
|
||||
use:gridLayout={gridMetadata}
|
||||
>
|
||||
{#if errorState}
|
||||
<ComponentErrorState
|
||||
{missingRequiredSettings}
|
||||
{missingRequiredAncestors}
|
||||
{componentErrors}
|
||||
/>
|
||||
{#if hasComponentErrors}
|
||||
<ComponentErrorState {componentErrors} />
|
||||
{:else}
|
||||
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
||||
{#if children.length}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { get, derived, readable } from "svelte/store"
|
||||
import { featuresStore } from "stores"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
// import { processStringSync } from "@budibase/string-templates"
|
||||
|
||||
// table is actually any datasource, but called table for legacy compatibility
|
||||
export let table
|
||||
|
@ -42,6 +43,7 @@
|
|||
let gridContext
|
||||
let minHeight = 0
|
||||
|
||||
$: id = $component.id
|
||||
$: currentTheme = $context?.device?.theme
|
||||
$: darkMode = !currentTheme?.includes("light")
|
||||
$: parsedColumns = getParsedColumns(columns)
|
||||
|
@ -65,7 +67,6 @@
|
|||
const clean = gridContext?.rows.actions.cleanRow || (x => x)
|
||||
const cleaned = rows.map(clean)
|
||||
const goldenRow = generateGoldenSample(cleaned)
|
||||
const id = get(component).id
|
||||
return {
|
||||
// Not sure what this one is for...
|
||||
[id]: goldenRow,
|
||||
|
@ -104,6 +105,7 @@
|
|||
order: idx,
|
||||
conditions: column.conditions,
|
||||
visible: !!column.active,
|
||||
// format: createFormatter(column),
|
||||
}
|
||||
if (column.width) {
|
||||
overrides[column.field].width = column.width
|
||||
|
@ -112,6 +114,13 @@
|
|||
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 => {
|
||||
if (!buttons?.length) {
|
||||
return null
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { Layout, Toggle } from "@budibase/bbui"
|
||||
import { getSettingsDefinition } from "@budibase/frontend-core"
|
||||
import DevToolsStat from "./DevToolsStat.svelte"
|
||||
import { componentStore } from "stores/index.js"
|
||||
import { getSettingsDefinition } from "utils/componentProps.js"
|
||||
|
||||
let showEnrichedSettings = true
|
||||
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from "svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import MissingRequiredSetting from "./MissingRequiredSetting.svelte"
|
||||
import MissingRequiredAncestor from "./MissingRequiredAncestor.svelte"
|
||||
import { UIComponentError } from "@budibase/types"
|
||||
import ComponentErrorStateCta from "./ComponentErrorStateCTA.svelte"
|
||||
|
||||
export let missingRequiredSettings:
|
||||
| { key: string; label: string }[]
|
||||
| undefined
|
||||
export let missingRequiredAncestors: string[] | undefined
|
||||
export let componentErrors: string[] | undefined
|
||||
export let componentErrors: UIComponentError[] | undefined
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
|
||||
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
|
||||
$: requiredSetting = missingRequiredSettings?.[0]
|
||||
$: requiredAncestor = missingRequiredAncestors?.[0]
|
||||
$: errorMessage = componentErrors?.[0]
|
||||
</script>
|
||||
|
||||
|
@ -23,12 +17,10 @@
|
|||
{#if $component.errorState}
|
||||
<div class="component-placeholder" use:styleable={styles}>
|
||||
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
|
||||
{#if requiredAncestor}
|
||||
<MissingRequiredAncestor {requiredAncestor} />
|
||||
{:else if errorMessage}
|
||||
{errorMessage}
|
||||
{:else if requiredSetting}
|
||||
<MissingRequiredSetting {requiredSetting} />
|
||||
{#if errorMessage}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html errorMessage.message}
|
||||
<ComponentErrorStateCta error={errorMessage} />
|
||||
{/if}
|
||||
</div>
|
||||
{/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 ScreenslotType = "screenslot"
|
||||
export const ScreenslotID = "screenslot"
|
||||
export const GridRowHeight = 24
|
||||
export const GridColumns = 12
|
||||
export const GridSpacing = 4
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
dndStore,
|
||||
eventStore,
|
||||
hoverStore,
|
||||
stateStore,
|
||||
} from "./stores"
|
||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
||||
import { get } from "svelte/store"
|
||||
|
@ -87,8 +88,10 @@ const loadBudibase = async () => {
|
|||
dndStore.actions.reset()
|
||||
}
|
||||
} else if (type === "request-context") {
|
||||
const { selectedComponentInstance } = get(componentStore)
|
||||
const context = selectedComponentInstance?.getDataContext()
|
||||
const { selectedComponentInstance, screenslotInstance } =
|
||||
get(componentStore)
|
||||
const instance = selectedComponentInstance || screenslotInstance
|
||||
const context = instance?.getDataContext()
|
||||
let stringifiedContext = null
|
||||
try {
|
||||
stringifiedContext = JSON.stringify(context)
|
||||
|
@ -102,6 +105,9 @@ const loadBudibase = async () => {
|
|||
hoverStore.actions.hoverComponent(data, false)
|
||||
} else if (type === "builder-meta") {
|
||||
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
|
||||
builderStore: Readable<{
|
||||
inBuilder: boolean
|
||||
}>
|
||||
}> & {
|
||||
actions: {
|
||||
highlightSetting: (key: string) => void
|
||||
addParentComponent: (
|
||||
componentId: string,
|
||||
fullAncestorType: string
|
||||
) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Component = Readable<{
|
||||
|
|
|
@ -6,7 +6,7 @@ import { screenStore } from "./screens"
|
|||
import { builderStore } from "./builder"
|
||||
import Router from "../components/Router.svelte"
|
||||
import * as AppComponents from "../components/app/index.js"
|
||||
import { ScreenslotType } from "../constants"
|
||||
import { ScreenslotID, ScreenslotType } from "../constants"
|
||||
|
||||
export const BudibasePrefix = "@budibase/standard-components/"
|
||||
|
||||
|
@ -43,6 +43,7 @@ const createComponentStore = () => {
|
|||
selectedComponentDefinition: definition,
|
||||
selectedComponentPath: selectedPath?.map(component => component._id),
|
||||
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 { findComponentById, findComponentParent } from "../utils/components.js"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { DNDPlaceholderID } from "constants"
|
||||
import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "constants"
|
||||
|
||||
const createScreenStore = () => {
|
||||
const store = derived(
|
||||
|
@ -171,8 +171,8 @@ const createScreenStore = () => {
|
|||
_component: "@budibase/standard-components/layout",
|
||||
_children: [
|
||||
{
|
||||
_component: "screenslot",
|
||||
_id: "screenslot",
|
||||
_component: ScreenslotType,
|
||||
_id: ScreenslotID,
|
||||
_styles: {
|
||||
normal: {
|
||||
flex: "1 1 auto",
|
||||
|
|
|
@ -97,26 +97,3 @@ export const propsUseBinding = (props, bindingKey) => {
|
|||
}
|
||||
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,12 +2,14 @@ import {
|
|||
DeleteScreenResponse,
|
||||
SaveScreenRequest,
|
||||
SaveScreenResponse,
|
||||
UsageInScreensResponse,
|
||||
} from "@budibase/types"
|
||||
import { BaseAPIClient } from "./types"
|
||||
|
||||
export interface ScreenEndpoints {
|
||||
saveScreen: (screen: SaveScreenRequest) => Promise<SaveScreenResponse>
|
||||
deleteScreen: (id: string, rev: string) => Promise<DeleteScreenResponse>
|
||||
usageInScreens: (sourceId: string) => Promise<UsageInScreensResponse>
|
||||
}
|
||||
|
||||
export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
||||
|
@ -32,4 +34,10 @@ export const buildScreenEndpoints = (API: BaseAPIClient): ScreenEndpoints => ({
|
|||
url: `/api/screens/${id}/${rev}`,
|
||||
})
|
||||
},
|
||||
|
||||
usageInScreens: async sourceId => {
|
||||
return await API.post({
|
||||
url: `/api/screens/usage/${sourceId}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import GridCell from "./GridCell.svelte"
|
||||
import { getCellRenderer } from "../lib/renderers"
|
||||
import { derived, writable } from "svelte/store"
|
||||
import TextCell from "./TextCell.svelte"
|
||||
|
||||
const {
|
||||
rows,
|
||||
|
@ -36,11 +37,17 @@
|
|||
|
||||
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
|
||||
$: error = getErrorStore(rowFocused, cellId)
|
||||
|
||||
// Determine if the cell is editable
|
||||
$: readonly =
|
||||
hasCustomFormat ||
|
||||
columns.actions.isReadonly(column) ||
|
||||
(!$config.canEditRows && !row._isNewRow)
|
||||
|
||||
|
@ -69,7 +76,7 @@
|
|||
onKeyDown: (...params) => api?.onKeyDown?.(...params),
|
||||
isReadonly: () => readonly,
|
||||
getType: () => column.schema.type,
|
||||
getValue: () => row[column.name],
|
||||
getValue: () => value,
|
||||
setValue: (value, options = { apply: true }) => {
|
||||
validation.actions.setError(cellId, null)
|
||||
updateValue({
|
||||
|
@ -136,9 +143,9 @@
|
|||
}}
|
||||
>
|
||||
<svelte:component
|
||||
this={getCellRenderer(column)}
|
||||
this={renderer}
|
||||
bind:api
|
||||
value={row[column.name]}
|
||||
{value}
|
||||
schema={column.schema}
|
||||
onChange={cellAPI.setValue}
|
||||
{focused}
|
||||
|
|
|
@ -53,7 +53,6 @@ export const getCellRenderer = (column: UIColumn) => {
|
|||
if (column.calculationType) {
|
||||
return NumberCell
|
||||
}
|
||||
|
||||
return (
|
||||
getCellRendererByType(column.schema?.cellRenderType) ||
|
||||
getCellRendererByType(column.schema?.type) ||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue