Merge branch 'master' of github.com:Budibase/budibase

This commit is contained in:
Martin McKeaveney 2025-04-05 11:59:17 +01:00
commit 71e023bdc3
69 changed files with 1888 additions and 389 deletions

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.8.1", "version": "3.8.5",
"npmClient": "yarn", "npmClient": "yarn",
"concurrency": 20, "concurrency": 20,
"command": { "command": {

View File

@ -96,6 +96,24 @@ async function get<T extends Document>(db: Database, id: string): Promise<T> {
return cacheItem.doc return cacheItem.doc
} }
async function tryGet<T extends Document>(
db: Database,
id: string
): Promise<T | null> {
const cache = await getCache()
const cacheKey = makeCacheKey(db, id)
let cacheItem: CacheItem<T> | null = await cache.get(cacheKey)
if (!cacheItem) {
const doc = await db.tryGet<T>(id)
if (!doc) {
return null
}
cacheItem = makeCacheItem(doc)
await cache.store(cacheKey, cacheItem)
}
return cacheItem.doc
}
async function remove(db: Database, docOrId: any, rev?: any): Promise<void> { async function remove(db: Database, docOrId: any, rev?: any): Promise<void> {
const cache = await getCache() const cache = await getCache()
if (!docOrId) { if (!docOrId) {
@ -123,10 +141,17 @@ export class Writethrough {
return put(this.db, doc, writeRateMs) return put(this.db, doc, writeRateMs)
} }
/**
* @deprecated use `tryGet` instead
*/
async get<T extends Document>(id: string) { async get<T extends Document>(id: string) {
return get<T>(this.db, id) return get<T>(this.db, id)
} }
async tryGet<T extends Document>(id: string) {
return tryGet<T>(this.db, id)
}
async remove(docOrId: any, rev?: any) { async remove(docOrId: any, rev?: any) {
return remove(this.db, docOrId, rev) return remove(this.db, docOrId, rev)
} }

View File

@ -60,6 +60,11 @@ export const StaticDatabases = {
SCIM_LOGS: { SCIM_LOGS: {
name: "scim-logs", name: "scim-logs",
}, },
// Used by self-host users making use of Budicloud resources. Introduced when
// we started letting self-host users use Budibase AI in the cloud.
SELF_HOST_CLOUD: {
name: "self-host-cloud",
},
} }
export const APP_PREFIX = prefixed(DocumentType.APP) export const APP_PREFIX = prefixed(DocumentType.APP)

View File

@ -157,6 +157,33 @@ export async function doInTenant<T>(
return newContext(updates, task) return newContext(updates, task)
} }
// We allow self-host licensed users to make use of some Budicloud services
// (e.g. Budibase AI). When they do this, they use their license key as an API
// key. We use that license key to identify the tenant ID, and we set the
// context to be self-host using cloud. This affects things like where their
// quota documents get stored (because we want to avoid creating a new global
// DB for each self-host tenant).
export async function doInSelfHostTenantUsingCloud<T>(
tenantId: string,
task: () => T
): Promise<T> {
const updates = { tenantId, isSelfHostUsingCloud: true }
return newContext(updates, task)
}
export function isSelfHostUsingCloud() {
const context = Context.get()
return !!context?.isSelfHostUsingCloud
}
export function getSelfHostCloudDB() {
const context = Context.get()
if (!context || !context.isSelfHostUsingCloud) {
throw new Error("Self-host cloud DB not found")
}
return getDB(StaticDatabases.SELF_HOST_CLOUD.name)
}
export async function doInAppContext<T>( export async function doInAppContext<T>(
appId: string, appId: string,
task: () => T task: () => T
@ -325,6 +352,11 @@ export function getGlobalDB(): Database {
if (!context || (env.MULTI_TENANCY && !context.tenantId)) { if (!context || (env.MULTI_TENANCY && !context.tenantId)) {
throw new Error("Global DB not found") throw new Error("Global DB not found")
} }
if (context.isSelfHostUsingCloud) {
throw new Error(
"Global DB not found - self-host users using cloud don't have a global DB"
)
}
return getDB(baseGlobalDBName(context?.tenantId)) return getDB(baseGlobalDBName(context?.tenantId))
} }
@ -344,6 +376,11 @@ export function getAppDB(opts?: any): Database {
if (!appId) { if (!appId) {
throw new Error("Unable to retrieve app DB - no app ID.") throw new Error("Unable to retrieve app DB - no app ID.")
} }
if (isSelfHostUsingCloud()) {
throw new Error(
"App DB not found - self-host users using cloud don't have app DBs"
)
}
return getDB(appId, opts) return getDB(appId, opts)
} }

View File

@ -5,6 +5,7 @@ import { GoogleSpreadsheet } from "google-spreadsheet"
// keep this out of Budibase types, don't want to expose context info // keep this out of Budibase types, don't want to expose context info
export type ContextMap = { export type ContextMap = {
tenantId?: string tenantId?: string
isSelfHostUsingCloud?: boolean
appId?: string appId?: string
identity?: IdentityContext identity?: IdentityContext
environmentVariables?: Record<string, string> environmentVariables?: Record<string, string>

View File

@ -143,6 +143,7 @@ export class FlagSet<T extends { [name: string]: boolean }> {
const personProperties: Record<string, string> = { tenantId } const personProperties: Record<string, string> = { tenantId }
const posthogFlags = await posthog.getAllFlags(userId, { const posthogFlags = await posthog.getAllFlags(userId, {
personProperties, personProperties,
onlyEvaluateLocally: true,
}) })
for (const [name, value] of Object.entries(posthogFlags)) { for (const [name, value] of Object.entries(posthogFlags)) {

View File

@ -4,7 +4,6 @@ import * as context from "../../context"
import environment, { withEnv } from "../../environment" import environment, { withEnv } from "../../environment"
import nodeFetch from "node-fetch" import nodeFetch from "node-fetch"
import nock from "nock" import nock from "nock"
import * as crypto from "crypto"
const schema = { const schema = {
TEST_BOOLEAN: false, TEST_BOOLEAN: false,
@ -16,26 +15,74 @@ interface TestCase {
it: string it: string
identity?: Partial<IdentityContext> identity?: Partial<IdentityContext>
environmentFlags?: string environmentFlags?: string
posthogFlags?: PostHogFlags posthogFlags?: Record<string, boolean>
expected?: Partial<typeof schema> expected?: Partial<typeof schema>
errorMessage?: string | RegExp errorMessage?: string | RegExp
} }
interface PostHogFlags { interface Property {
featureFlags?: Record<string, boolean> key: string
featureFlagPayloads?: Record<string, string> value: string
operator: string
type: string
} }
function mockPosthogFlags( interface Group {
flags: PostHogFlags, properties: Property[]
opts?: { token?: string; distinct_id?: string } rollout_percentage: number
) { variant: string | null
const { token = "test", distinct_id = "us_1234" } = opts || {} }
interface Filters {
groups: Group[]
}
interface FlagRules {
active: boolean
deleted: boolean
ensure_experience_continuity: boolean
filters: Filters
has_encrypted_payloads: boolean
id: string
key: string
name: string
team_id: number
version: number
}
interface LocalEvaluationResponse {
flags: FlagRules[]
}
function posthogFlags(flags: Record<string, boolean>): LocalEvaluationResponse {
return {
flags: Object.entries(flags).map(([name, value]) => ({
active: value,
deleted: false,
ensure_experience_continuity: false,
filters: {
groups: [
{
properties: [],
rollout_percentage: 100,
variant: null,
},
],
},
version: 2,
has_encrypted_payloads: false,
id: name,
name,
team_id: 1,
key: name,
})),
}
}
function mockPosthogFlags(flags: Record<string, boolean>) {
nock("https://us.i.posthog.com") nock("https://us.i.posthog.com")
.post("/decide/?v=3", body => { .get("/api/feature_flag/local_evaluation?token=test&send_cohorts")
return body.token === token && body.distinct_id === distinct_id .reply(200, posthogFlags(flags))
})
.reply(200, flags)
.persist() .persist()
} }
@ -76,33 +123,27 @@ describe("feature flags", () => {
}, },
{ {
it: "should be able to read boolean flags from PostHog", it: "should be able to read boolean flags from PostHog",
posthogFlags: { posthogFlags: { TEST_BOOLEAN: true },
featureFlags: { TEST_BOOLEAN: true },
},
expected: { TEST_BOOLEAN: true }, expected: { TEST_BOOLEAN: true },
}, },
{ {
it: "should not be able to override a negative environment flag from PostHog", it: "should not be able to override a negative environment flag from PostHog",
environmentFlags: "default:!TEST_BOOLEAN", environmentFlags: "default:!TEST_BOOLEAN",
posthogFlags: { posthogFlags: { TEST_BOOLEAN: true },
featureFlags: { TEST_BOOLEAN: true },
},
expected: { TEST_BOOLEAN: false }, expected: { TEST_BOOLEAN: false },
}, },
{ {
it: "should not be able to override a positive environment flag from PostHog", it: "should not be able to override a positive environment flag from PostHog",
environmentFlags: "default:TEST_BOOLEAN", environmentFlags: "default:TEST_BOOLEAN",
posthogFlags: { posthogFlags: {
featureFlags: { TEST_BOOLEAN: false,
TEST_BOOLEAN: false,
},
}, },
expected: { TEST_BOOLEAN: true }, expected: { TEST_BOOLEAN: true },
}, },
{ {
it: "should not error on unrecognised PostHog flag", it: "should not error on unrecognised PostHog flag",
posthogFlags: { posthogFlags: {
featureFlags: { UNDEFINED: true }, UNDEFINED: true,
}, },
expected: flags.defaults(), expected: flags.defaults(),
}, },
@ -136,6 +177,8 @@ describe("feature flags", () => {
// We need to pass in node-fetch here otherwise nock won't get used // We need to pass in node-fetch here otherwise nock won't get used
// because posthog-node uses axios under the hood. // because posthog-node uses axios under the hood.
init({ init({
// Required for local evaluation rule polling to start
personalApiKey: "test",
fetch: (url, opts) => { fetch: (url, opts) => {
return nodeFetch(url, opts) return nodeFetch(url, opts)
}, },
@ -151,23 +194,25 @@ describe("feature flags", () => {
...identity, ...identity,
} }
await context.doInIdentityContext(fullIdentity, async () => { try {
if (errorMessage) { await context.doInIdentityContext(fullIdentity, async () => {
await expect(flags.fetch()).rejects.toThrow(errorMessage) if (errorMessage) {
} else if (expected) { await expect(flags.fetch()).rejects.toThrow(errorMessage)
const values = await flags.fetch() } else if (expected) {
expect(values).toMatchObject(expected) const values = await flags.fetch()
expect(values).toMatchObject(expected)
for (const [key, expectedValue] of Object.entries(expected)) { for (const [key, expectedValue] of Object.entries(expected)) {
const value = await flags.isEnabled(key as keyof typeof schema) const value = await flags.isEnabled(key as keyof typeof schema)
expect(value).toBe(expectedValue) expect(value).toBe(expectedValue)
}
} else {
throw new Error("No expected value")
} }
} else { })
throw new Error("No expected value") } finally {
} shutdown()
}) }
shutdown()
}) })
} }
) )
@ -185,26 +230,30 @@ describe("feature flags", () => {
// We need to pass in node-fetch here otherwise nock won't get used // We need to pass in node-fetch here otherwise nock won't get used
// because posthog-node uses axios under the hood. // because posthog-node uses axios under the hood.
init({ init({
// Required for local evaluation rule polling to start
personalApiKey: "test",
fetch: (url, opts) => { fetch: (url, opts) => {
return nodeFetch(url, opts) return nodeFetch(url, opts)
}, },
}) })
nock("https://us.i.posthog.com") nock("https://us.i.posthog.com")
.post("/decide/?v=3", body => { .get("/api/feature_flag/local_evaluation?token=test&send_cohorts")
return body.token === "test" && body.distinct_id === "us_1234"
})
.reply(503) .reply(503)
.persist() .persist()
await withEnv( try {
{ POSTHOG_TOKEN: "test", POSTHOG_API_HOST: "https://us.i.posthog.com" }, await withEnv(
async () => { { POSTHOG_TOKEN: "test", POSTHOG_API_HOST: "https://us.i.posthog.com" },
await context.doInIdentityContext(identity, async () => { async () => {
await flags.fetch() await context.doInIdentityContext(identity, async () => {
}) await flags.fetch()
} })
) }
)
} finally {
shutdown()
}
}) })
it("should still get flags when user is logged out", async () => { it("should still get flags when user is logged out", async () => {
@ -216,34 +265,30 @@ describe("feature flags", () => {
} }
const ip = "127.0.0.1" const ip = "127.0.0.1"
const hashedIp = crypto.createHash("sha512").update(ip).digest("hex")
await withEnv(env, async () => { await withEnv(env, async () => {
mockPosthogFlags( mockPosthogFlags({ TEST_BOOLEAN: true })
{
featureFlags: { TEST_BOOLEAN: true },
},
{
distinct_id: hashedIp,
}
)
// We need to pass in node-fetch here otherwise nock won't get used // We need to pass in node-fetch here otherwise nock won't get used
// because posthog-node uses axios under the hood. // because posthog-node uses axios under the hood.
init({ init({
// Required for local evaluation rule polling to start
personalApiKey: "test",
fetch: (url, opts) => { fetch: (url, opts) => {
return nodeFetch(url, opts) return nodeFetch(url, opts)
}, },
}) })
await context.doInIPContext(ip, async () => { try {
await context.doInTenant("default", async () => { await context.doInIPContext(ip, async () => {
const result = await flags.fetch() await context.doInTenant("default", async () => {
expect(result.TEST_BOOLEAN).toBe(true) const result = await flags.fetch()
expect(result.TEST_BOOLEAN).toBe(true)
})
}) })
}) } finally {
shutdown()
shutdown() }
}) })
}) })
}) })

View File

@ -211,9 +211,12 @@ const localeDateFormat = new Intl.DateTimeFormat()
// Formats a dayjs date according to schema flags // Formats a dayjs date according to schema flags
export const getDateDisplayValue = ( export const getDateDisplayValue = (
value: dayjs.Dayjs | null, value: dayjs.Dayjs | string | null,
{ enableTime = true, timeOnly = false } = {} { enableTime = true, timeOnly = false } = {}
): string => { ): string => {
if (typeof value === "string") {
value = dayjs(value)
}
if (!value?.isValid()) { if (!value?.isValid()) {
return "" return ""
} }

View File

@ -184,6 +184,8 @@
}, },
[SchemaFieldTypes.QUERY_PARAMS]: { [SchemaFieldTypes.QUERY_PARAMS]: {
comp: QueryParamSelector, comp: QueryParamSelector,
fullWidth: true,
title: "Query*",
}, },
[SchemaFieldTypes.CODE]: { [SchemaFieldTypes.CODE]: {
comp: ExecuteScript, comp: ExecuteScript,
@ -281,7 +283,9 @@
} }
const type = getFieldType(field, block) const type = getFieldType(field, block)
const config = type ? SchemaTypes[type] : null const config = type ? SchemaTypes[type] : null
const title = getFieldLabel(key, field, requiredProperties?.includes(key)) const title =
config?.title ||
getFieldLabel(key, field, requiredProperties?.includes(key))
const value = getInputValue(inputData, key) const value = getInputValue(inputData, key)
const meta = getInputValue(inputData, "meta") const meta = getInputValue(inputData, "meta")

View File

@ -27,6 +27,7 @@
allowHBS={false} allowHBS={false}
updateOnChange={false} updateOnChange={false}
{context} {context}
showComponent
> >
<div class="field-wrap code-editor"> <div class="field-wrap code-editor">
<CodeEditorField <CodeEditorField
@ -57,8 +58,8 @@
.scriptv2-wrapper :global(.icon.slot-icon), .scriptv2-wrapper :global(.icon.slot-icon),
.scriptv2-wrapper :global(.text-area-slot-icon) { .scriptv2-wrapper :global(.text-area-slot-icon) {
right: 1px; right: 1px !important;
top: 1px; top: 1px !important;
border-top-right-radius: var(--spectrum-alias-border-radius-regular); border-top-right-radius: var(--spectrum-alias-border-radius-regular);
border-bottom-left-radius: var(--spectrum-alias-border-radius-regular); border-bottom-left-radius: var(--spectrum-alias-border-radius-regular);
border-right: 0px; border-right: 0px;

View File

@ -1,9 +1,10 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { queries } from "@/stores/builder" import { queries } from "@/stores/builder"
import { Select, Label } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import PropField from "./PropField.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -28,7 +29,6 @@
</script> </script>
<div class="schema-field"> <div class="schema-field">
<Label>Query</Label>
<div class="field-width"> <div class="field-width">
<Select <Select
on:change={onChangeQuery} on:change={onChangeQuery}
@ -42,26 +42,23 @@
{#if parameters.length} {#if parameters.length}
{#each parameters as field} {#each parameters as field}
<div class="schema-field"> <PropField label={field.name} fullWidth>
<Label>{field.name}</Label> <DrawerBindableInput
<div class="field-width"> panel={AutomationBindingPanel}
<DrawerBindableInput extraThin
panel={AutomationBindingPanel} value={value[field.name]}
extraThin on:change={e => onChange(e, field)}
value={value[field.name]} type="string"
on:change={e => onChange(e, field)} {bindings}
type="string" updateOnChange={false}
{bindings} />
updateOnChange={false} </PropField>
/>
</div>
</div>
{/each} {/each}
{/if} {/if}
<style> <style>
.field-width { .field-width {
width: 320px; width: 100%;
} }
.schema-field { .schema-field {

View File

@ -11,6 +11,10 @@
import { FieldType } from "@budibase/types" import { FieldType } from "@budibase/types"
import RowSelectorTypes from "./RowSelectorTypes.svelte" import RowSelectorTypes from "./RowSelectorTypes.svelte"
import {
DrawerBindableSlot,
ServerBindingPanel as AutomationBindingPanel,
} from "@/components/common/bindings"
import { FIELDS } from "@/constants/backend" import { FIELDS } from "@/constants/backend"
import { capitalise } from "@/helpers" import { capitalise } from "@/helpers"
import { memo } from "@budibase/frontend-core" import { memo } from "@budibase/frontend-core"
@ -234,6 +238,17 @@
) )
dispatch("change", result) dispatch("change", result)
} }
/**
* Converts arrays into strings. The CodeEditor expects a string or encoded JS
* @param{object} fieldValue
*/
const drawerValue = fieldValue => {
return Array.isArray(fieldValue) ? fieldValue.join(",") : fieldValue
}
// The element controls their own binding drawer
const customDrawer = ["string", "number", "barcodeqr", "bigint"]
</script> </script>
{#each schemaFields || [] as [field, schema]} {#each schemaFields || [] as [field, schema]}
@ -243,20 +258,55 @@
fullWidth={fullWidth || isFullWidth(schema.type)} fullWidth={fullWidth || isFullWidth(schema.type)}
{componentWidth} {componentWidth}
> >
<div class="prop-control-wrap"> {#if customDrawer.includes(schema.type) || isTestModal}
<RowSelectorTypes <div class="prop-control-wrap">
{isTestModal} <RowSelectorTypes
{field} {isTestModal}
{field}
{schema}
bindings={parsedBindings}
value={editableRow}
meta={{
fields: editableFields,
}}
{onChange}
{context}
/>
</div>
{:else}
<DrawerBindableSlot
title={$memoStore?.row?.title || field}
panel={AutomationBindingPanel}
type={schema.type}
{schema} {schema}
bindings={parsedBindings} value={drawerValue(editableRow[field])}
value={editableRow} on:change={e =>
meta={{ onChange({
fields: editableFields, row: {
}} [field]: e.detail,
{onChange} },
})}
{bindings}
allowJS={true}
updateOnChange={false}
{context} {context}
/> >
</div> <div class="prop-control-wrap">
<RowSelectorTypes
{isTestModal}
{field}
{schema}
bindings={parsedBindings}
value={editableRow}
meta={{
fields: editableFields,
}}
{onChange}
{context}
/>
</div>
</DrawerBindableSlot>
{/if}
</PropField> </PropField>
{/if} {/if}
{/each} {/each}

View File

@ -6,7 +6,13 @@
Multiselect, Multiselect,
Button, Button,
} from "@budibase/bbui" } from "@budibase/bbui"
import { CalculationType, canGroupBy, isNumeric } from "@budibase/types" import {
CalculationType,
canGroupBy,
FieldType,
isNumeric,
isNumericStaticFormula,
} from "@budibase/types"
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte" import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
import DetailPopover from "@/components/common/DetailPopover.svelte" import DetailPopover from "@/components/common/DetailPopover.svelte"
@ -94,10 +100,15 @@
if (fieldSchema.calculationType) { if (fieldSchema.calculationType) {
return false return false
} }
// static numeric formulas will work
if (isNumericStaticFormula(fieldSchema)) {
return true
}
// Only allow numeric columns for most calculation types // Only allow numeric columns for most calculation types
if ( if (
self.type !== CalculationType.COUNT && self.type !== CalculationType.COUNT &&
!isNumeric(fieldSchema.type) !isNumeric(fieldSchema.type) &&
fieldSchema.responseType !== FieldType.NUMBER
) { ) {
return false return false
} }

View File

@ -12,6 +12,7 @@
export let bindings = [] export let bindings = []
export let context = {} export let context = {}
export let height = 180 export let height = 180
export let dropdown
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
</script> </script>
@ -32,6 +33,7 @@
value = e.detail value = e.detail
dispatch("change", value) dispatch("change", value)
}} }}
showComponent
> >
<div class="code-editor-wrapper"> <div class="code-editor-wrapper">
<CodeEditorField <CodeEditorField
@ -39,6 +41,7 @@
{bindings} {bindings}
{context} {context}
{height} {height}
{dropdown}
allowHBS={false} allowHBS={false}
allowJS allowJS
placeholder={"Add bindings by typing $"} placeholder={"Add bindings by typing $"}

View File

@ -22,6 +22,7 @@
export let updateOnChange = true export let updateOnChange = true
export let type = undefined export let type = undefined
export let schema = undefined export let schema = undefined
export let showComponent = false
export let allowHBS = true export let allowHBS = true
export let context = {} export let context = {}
@ -150,7 +151,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="control" class:disabled> <div class="control" class:disabled>
{#if !isValid(value) && !$$slots.default} {#if !isValid(value) && !showComponent}
<Input <Input
{label} {label}
{disabled} {disabled}
@ -162,7 +163,7 @@
{updateOnChange} {updateOnChange}
/> />
<div <div
class="icon" class="icon close"
on:click={() => { on:click={() => {
if (!isJS) { if (!isJS) {
dispatch("change", "") dispatch("change", "")
@ -212,22 +213,27 @@
} }
.slot-icon { .slot-icon {
right: 31px; right: 31px !important;
border-right: 1px solid var(--spectrum-alias-border-color); border-right: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: 0px; border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px; border-bottom-right-radius: 0px !important;
} }
.text-area-slot-icon { .icon.close {
border-bottom: 1px solid var(--spectrum-alias-border-color); right: 1px !important;
border-bottom-right-radius: 0px; border-right: none;
top: 1px; border-top-right-radius: 4px !important;
border-bottom-right-radius: 4px !important;
} }
.text-area-slot-icon,
.json-slot-icon { .json-slot-icon {
right: 1px !important;
border-bottom: 1px solid var(--spectrum-alias-border-color); border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px; border-top-right-radius: 4px !important;
border-bottom-right-radius: 0px !important;
border-bottom-left-radius: 4px !important;
top: 1px; top: 1px;
right: 0px;
} }
.icon { .icon {

View File

@ -80,5 +80,6 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-top: 4px;
} }
</style> </style>

View File

@ -22,6 +22,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte" import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte" import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
import TopLevelColumnEditor from "./controls/ColumnEditor/TopLevelColumnEditor.svelte"
import GridColumnEditor from "./controls/GridColumnConfiguration/GridColumnConfiguration.svelte" import GridColumnEditor from "./controls/GridColumnConfiguration/GridColumnConfiguration.svelte"
import BarButtonList from "./controls/BarButtonList.svelte" import BarButtonList from "./controls/BarButtonList.svelte"
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte" import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
@ -62,7 +63,10 @@ const componentMap = {
stepConfiguration: FormStepConfiguration, stepConfiguration: FormStepConfiguration,
formStepControls: FormStepControls, formStepControls: FormStepControls,
columns: ColumnEditor, columns: ColumnEditor,
// "Basic" actually includes nested JSON and relationship fields
"columns/basic": BasicColumnEditor, "columns/basic": BasicColumnEditor,
// "Top level" is only the top level schema fields
"columns/toplevel": TopLevelColumnEditor,
"columns/grid": GridColumnEditor, "columns/grid": GridColumnEditor,
tableConditions: TableConditionEditor, tableConditions: TableConditionEditor,
"field/sortable": SortableFieldSelect, "field/sortable": SortableFieldSelect,

View File

@ -145,7 +145,7 @@
<div class="column"> <div class="column">
<div class="wide"> <div class="wide">
<Body size="S"> <Body size="S">
By default, all columns will automatically be shown. The default column configuration will automatically be shown.
<br /> <br />
You can manually control which columns are included by adding them You can manually control which columns are included by adding them
below. below.

View File

@ -10,10 +10,18 @@
} from "@/dataBinding" } from "@/dataBinding"
import { selectedScreen, tables } from "@/stores/builder" import { selectedScreen, tables } from "@/stores/builder"
export let componentInstance const getSearchableFields = (schema, tableList) => {
return search.getFields(tableList, Object.values(schema || {}), {
allowLinks: true,
})
}
export let componentInstance = undefined
export let value = [] export let value = []
export let allowCellEditing = true export let allowCellEditing = true
export let allowReorder = true export let allowReorder = true
export let getSchemaFields = getSearchableFields
export let placeholder = "All columns"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -28,13 +36,7 @@
: enrichedSchemaFields?.map(field => field.name) : enrichedSchemaFields?.map(field => field.name)
$: sanitisedValue = getValidColumns(value, options) $: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue) $: updateBoundValue(sanitisedValue)
$: enrichedSchemaFields = search.getFields( $: enrichedSchemaFields = getSchemaFields(schema, $tables.list)
$tables.list,
Object.values(schema || {}),
{
allowLinks: true,
}
)
$: { $: {
value = (value || []).filter( value = (value || []).filter(
@ -44,7 +46,7 @@
const getText = value => { const getText = value => {
if (!value?.length) { if (!value?.length) {
return "All columns" return placeholder
} }
let text = `${value.length} column` let text = `${value.length} column`
if (value.length !== 1) { if (value.length !== 1) {

View File

@ -0,0 +1,15 @@
<script lang="ts">
import ColumnEditor from "./ColumnEditor.svelte"
import type { TableSchema } from "@budibase/types"
const getTopLevelSchemaFields = (schema: TableSchema) => {
return Object.values(schema).filter(fieldSchema => !fieldSchema.nestedJSON)
}
</script>
<ColumnEditor
{...$$props}
on:change
allowCellEditing={false}
getSchemaFields={getTopLevelSchemaFields}
/>

View File

@ -71,4 +71,5 @@ export const AutoScreenTypes = {
BLANK: "blank", BLANK: "blank",
TABLE: "table", TABLE: "table",
FORM: "form", FORM: "form",
PDF: "pdf",
} }

View File

@ -26,7 +26,7 @@ import {
getJsHelperList, getJsHelperList,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import { TableNames } from "./constants" import { TableNames } from "./constants"
import { JSONUtils, Constants } from "@budibase/frontend-core" import { JSONUtils, Constants, SchemaUtils } from "@budibase/frontend-core"
import ActionDefinitions from "@/components/design/settings/controls/ButtonActionEditor/manifest.json" import ActionDefinitions from "@/components/design/settings/controls/ButtonActionEditor/manifest.json"
import { environment, licensing } from "@/stores/portal" import { environment, licensing } from "@/stores/portal"
import { convertOldFieldFormat } from "@/components/design/settings/controls/FieldConfiguration/utils" import { convertOldFieldFormat } from "@/components/design/settings/controls/FieldConfiguration/utils"
@ -1026,25 +1026,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
// Check for any JSON fields so we can add any top level properties // Check for any JSON fields so we can add any top level properties
if (schema) { if (schema) {
let jsonAdditions = {} schema = SchemaUtils.addNestedJSONSchemaFields(schema)
Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") {
const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema(
fieldSchema,
{
squashObjects: true,
}
)
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,
nestedJSON: true,
}
})
}
})
schema = { ...schema, ...jsonAdditions }
} }
// Determine if we should add ID and rev to the schema // Determine if we should add ID and rev to the schema

View File

@ -16,6 +16,7 @@
import { getBindableProperties } from "@/dataBinding" import { getBindableProperties } from "@/dataBinding"
import BarButtonList from "@/components/design/settings/controls/BarButtonList.svelte" import BarButtonList from "@/components/design/settings/controls/BarButtonList.svelte"
import URLVariableTestInput from "@/components/design/settings/controls/URLVariableTestInput.svelte" import URLVariableTestInput from "@/components/design/settings/controls/URLVariableTestInput.svelte"
import { DrawerBindableInput } from "@/components/common/bindings"
$: bindings = getBindableProperties($selectedScreen, null) $: bindings = getBindableProperties($selectedScreen, null)
$: screenSettings = getScreenSettings($selectedScreen) $: screenSettings = getScreenSettings($selectedScreen)
@ -23,7 +24,59 @@
let errors = {} let errors = {}
const getScreenSettings = screen => { const getScreenSettings = screen => {
let settings = [ // Determine correct screen settings for the top level component
let screenComponentSettings = []
switch ($selectedScreen.props._component) {
case "@budibase/standard-components/pdf":
screenComponentSettings = [
{
key: "props.fileName",
label: "PDF title",
defaultValue: "Report",
control: DrawerBindableInput,
},
{
key: "props.buttonText",
label: "Button text",
defaultValue: "Download PDF",
control: DrawerBindableInput,
},
]
break
default:
screenComponentSettings = [
{
key: "width",
label: "Width",
control: Select,
props: {
options: ["Extra small", "Small", "Medium", "Large", "Max"],
placeholder: "Default",
disabled: !!screen.layoutId,
},
},
{
key: "props.layout",
label: "Layout",
defaultValue: "flex",
control: BarButtonList,
props: {
options: [
{
barIcon: "ModernGridView",
value: "flex",
},
{
barIcon: "ViewGrid",
value: "grid",
},
],
},
},
]
}
return [
{ {
key: "routing.homeScreen", key: "routing.homeScreen",
control: Checkbox, control: Checkbox,
@ -66,34 +119,7 @@
label: "On screen load", label: "On screen load",
control: ButtonActionEditor, control: ButtonActionEditor,
}, },
{ ...screenComponentSettings,
key: "width",
label: "Width",
control: Select,
props: {
options: ["Extra small", "Small", "Medium", "Large", "Max"],
placeholder: "Default",
disabled: !!screen.layoutId,
},
},
{
key: "props.layout",
label: "Layout",
defaultValue: "flex",
control: BarButtonList,
props: {
options: [
{
barIcon: "ModernGridView",
value: "flex",
},
{
barIcon: "ViewGrid",
value: "grid",
},
],
},
},
{ {
key: "urlTest", key: "urlTest",
control: URLVariableTestInput, control: URLVariableTestInput,
@ -102,8 +128,6 @@
}, },
}, },
] ]
return settings
} }
const routeTaken = url => { const routeTaken = url => {

View File

@ -26,7 +26,9 @@
<div class="info"> <div class="info">
<Icon name="InfoOutline" size="S" /> <Icon name="InfoOutline" size="S" />
<Body size="S">These settings apply to all screens</Body> <Body size="S">
These settings apply to all screens. PDFs are always light theme.
</Body>
</div> </div>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">

View File

@ -58,7 +58,7 @@
// Get initial set of allowed components // Get initial set of allowed components
let allowedComponents = [] let allowedComponents = []
const definition = componentStore.getDefinition(component?._component) const definition = componentStore.getDefinition(component?._component)
if (definition.legalDirectChildren?.length) { if (definition?.legalDirectChildren?.length) {
allowedComponents = definition.legalDirectChildren.map(x => { allowedComponents = definition.legalDirectChildren.map(x => {
return `@budibase/standard-components/${x}` return `@budibase/standard-components/${x}`
}) })
@ -67,7 +67,7 @@
} }
// Build up list of illegal children from ancestors // Build up list of illegal children from ancestors
let illegalChildren = definition.illegalChildren || [] let illegalChildren = definition?.illegalChildren || []
path.forEach(ancestor => { path.forEach(ancestor => {
// Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level. // Sidepanels and modals can be nested anywhere in the component tree, but really they are always rendered at the top level.
// Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here. // Because of this, it doesn't make sense to carry over any parent illegal children to them, so the array is reset here.
@ -144,11 +144,6 @@
} }
}) })
// Swap blocks and plugins
let tmp = enrichedStructure[1]
enrichedStructure[1] = enrichedStructure[0]
enrichedStructure[0] = tmp
return enrichedStructure return enrichedStructure
} }

View File

@ -20,9 +20,11 @@
"name": "Data", "name": "Data",
"icon": "Data", "icon": "Data",
"children": [ "children": [
"singlerowprovider",
"dataprovider", "dataprovider",
"repeater", "repeater",
"gridblock", "gridblock",
"pdftable",
"spreadsheet", "spreadsheet",
"dynamicfilter", "dynamicfilter",
"daterangepicker" "daterangepicker"

View File

@ -1,10 +1,13 @@
<script> <script>
import DevicePreviewSelect from "./DevicePreviewSelect.svelte" import DevicePreviewSelect from "./DevicePreviewSelect.svelte"
import AppPreview from "./AppPreview.svelte" import AppPreview from "./AppPreview.svelte"
import { screenStore, appStore } from "@/stores/builder" import { screenStore, appStore, selectedScreen } from "@/stores/builder"
import UndoRedoControl from "@/components/common/UndoRedoControl.svelte" import UndoRedoControl from "@/components/common/UndoRedoControl.svelte"
import ScreenErrorsButton from "./ScreenErrorsButton.svelte" import ScreenErrorsButton from "./ScreenErrorsButton.svelte"
import { Divider } from "@budibase/bbui" import { Divider } from "@budibase/bbui"
import { ScreenVariant } from "@budibase/types"
$: isPDF = $selectedScreen?.variant === ScreenVariant.PDF
</script> </script>
<div class="app-panel"> <div class="app-panel">
@ -14,10 +17,12 @@
<UndoRedoControl store={screenStore.history} /> <UndoRedoControl store={screenStore.history} />
</div> </div>
<div class="header-right"> <div class="header-right">
{#if $appStore.clientFeatures.devicePreview} {#if !isPDF}
<DevicePreviewSelect /> {#if $appStore.clientFeatures.devicePreview}
<DevicePreviewSelect />
{/if}
<Divider vertical />
{/if} {/if}
<Divider vertical />
<ScreenErrorsButton /> <ScreenErrorsButton />
</div> </div>
</div> </div>

View File

@ -53,7 +53,7 @@
// Otherwise choose a datasource // Otherwise choose a datasource
datasourceModal.show() datasourceModal.show()
} }
} else if (mode === AutoScreenTypes.BLANK) { } else if (mode === AutoScreenTypes.BLANK || mode === AutoScreenTypes.PDF) {
screenDetailsModal.show() screenDetailsModal.show()
} else { } else {
throw new Error("Invalid mode provided") throw new Error("Invalid mode provided")
@ -101,8 +101,11 @@
} }
} }
const createBlankScreen = async ({ route }) => { const createBasicScreen = async ({ route }) => {
const screenTemplates = screenTemplating.blank({ route, screens }) const screenTemplates =
mode === AutoScreenTypes.BLANK
? screenTemplating.blank({ route, screens })
: screenTemplating.pdf({ route, screens })
const newScreens = await createScreens(screenTemplates) const newScreens = await createScreens(screenTemplates)
loadNewScreen(newScreens[0]) loadNewScreen(newScreens[0])
} }
@ -243,7 +246,7 @@
</Modal> </Modal>
<Modal bind:this={screenDetailsModal}> <Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal onConfirm={createBlankScreen} /> <ScreenDetailsModal onConfirm={createBasicScreen} />
</Modal> </Modal>
<Modal bind:this={formTypeModal}> <Modal bind:this={formTypeModal}>

View File

@ -0,0 +1,38 @@
<svg width="118" height="65" viewBox="0 0 118 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1099_17726)">
<g clip-path="url(#clip1_1099_17726)" filter="url(#filter0_d_1099_17726)">
<rect width="118" height="65" fill="#D8B500"/>
<mask id="mask0_1099_17726" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="118" height="65">
<rect width="118" height="65" fill="white"/>
</mask>
<g mask="url(#mask0_1099_17726)">
</g>
</g>
<rect x="38.0762" y="22.3271" width="41.6766" height="42.6718" fill="white" fill-opacity="0.5"/>
<rect x="23.1543" y="11.4121" width="71.3021" height="53.5878" fill="url(#paint0_linear_1099_17726)"/>
<rect x="23.6543" y="11.9121" width="70.3021" height="52.5878" stroke="white" stroke-opacity="0.2"/>
<path d="M44.1365 45.7637V34.0637H48.4205C49.2725 34.0637 50.0585 34.1837 50.7785 34.4237C51.4985 34.6637 52.0745 35.0657 52.5065 35.6297C52.9505 36.1937 53.1725 36.9677 53.1725 37.9517C53.1725 38.8997 52.9505 39.6797 52.5065 40.2917C52.0745 40.8917 51.4985 41.3357 50.7785 41.6237C50.0705 41.9117 49.3085 42.0557 48.4925 42.0557H47.2325V45.7637H44.1365ZM47.2325 39.6077H48.3485C48.9605 39.6077 49.4105 39.4637 49.6985 39.1757C49.9985 38.8757 50.1485 38.4677 50.1485 37.9517C50.1485 37.4237 49.9865 37.0517 49.6625 36.8357C49.3385 36.6197 48.8765 36.5117 48.2765 36.5117H47.2325V39.6077ZM55.0877 45.7637V34.0637H58.5437C59.7317 34.0637 60.7757 34.2617 61.6757 34.6577C62.5877 35.0417 63.2957 35.6597 63.7997 36.5117C64.3037 37.3637 64.5557 38.4797 64.5557 39.8597C64.5557 41.2397 64.3037 42.3677 63.7997 43.2437C63.2957 44.1077 62.6057 44.7437 61.7297 45.1517C60.8537 45.5597 59.8517 45.7637 58.7237 45.7637H55.0877ZM58.1837 43.2797H58.3637C58.9277 43.2797 59.4377 43.1837 59.8937 42.9917C60.3497 42.7997 60.7097 42.4577 60.9737 41.9657C61.2497 41.4737 61.3877 40.7717 61.3877 39.8597C61.3877 38.9477 61.2497 38.2577 60.9737 37.7897C60.7097 37.3097 60.3497 36.9857 59.8937 36.8177C59.4377 36.6377 58.9277 36.5477 58.3637 36.5477H58.1837V43.2797ZM66.6365 45.7637V34.0637H74.2685V36.6557H69.7325V38.8877H73.6205V41.4797H69.7325V45.7637H66.6365Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_1099_17726" x="-10" y="-6" width="138" height="85" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1099_17726"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1099_17726" result="shape"/>
</filter>
<linearGradient id="paint0_linear_1099_17726" x1="23.1543" y1="11.4121" x2="94.0187" y2="65.5726" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.6"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_1099_17726">
<rect width="118" height="65" fill="white"/>
</clipPath>
<clipPath id="clip1_1099_17726">
<rect width="118" height="65" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -1,11 +1,14 @@
<script> <script>
import { Body } from "@budibase/bbui" import { Body, Tag, Tags } from "@budibase/bbui"
import CreationPage from "@/components/common/CreationPage.svelte" import CreationPage from "@/components/common/CreationPage.svelte"
import blank from "./images/blank.svg" import blank from "./images/blank.svg"
import table from "./images/tableInline.svg" import table from "./images/tableInline.svg"
import form from "./images/formUpdate.svg" import form from "./images/formUpdate.svg"
import pdf from "./images/pdf.svg"
import CreateScreenModal from "./CreateScreenModal.svelte" import CreateScreenModal from "./CreateScreenModal.svelte"
import { screenStore } from "@/stores/builder" import { screenStore } from "@/stores/builder"
import { licensing } from "@/stores/portal"
import { AutoScreenTypes } from "@/constants"
export let onClose = null export let onClose = null
@ -27,35 +30,67 @@
</div> </div>
<div class="cards"> <div class="cards">
<div class="card" on:click={() => createScreenModal.show("blank")}> <div
class="card"
on:click={() => createScreenModal.show(AutoScreenTypes.BLANK)}
>
<div class="image"> <div class="image">
<img alt="A blank screen" src={blank} /> <img alt="A blank screen" src={blank} />
</div> </div>
<div class="text"> <div class="text">
<Body size="S">Blank</Body> <Body size="M">Blank</Body>
<Body size="XS">Add an empty blank screen</Body> <Body size="XS">Add an empty blank screen</Body>
</div> </div>
</div> </div>
<div class="card" on:click={() => createScreenModal.show("table")}> <div
class="card"
on:click={() => createScreenModal.show(AutoScreenTypes.TABLE)}
>
<div class="image"> <div class="image">
<img alt="A table of data" src={table} /> <img alt="A table of data" src={table} />
</div> </div>
<div class="text"> <div class="text">
<Body size="S">Table</Body> <Body size="M">Table</Body>
<Body size="XS">List rows in a table</Body> <Body size="XS">List rows in a table</Body>
</div> </div>
</div> </div>
<div class="card" on:click={() => createScreenModal.show("form")}> <div
class="card"
on:click={() => createScreenModal.show(AutoScreenTypes.FORM)}
>
<div class="image"> <div class="image">
<img alt="A form containing data" src={form} /> <img alt="A form containing data" src={form} />
</div> </div>
<div class="text"> <div class="text">
<Body size="S">Form</Body> <Body size="M">Form</Body>
<Body size="XS">Capture data from your users</Body> <Body size="XS">Capture data from your users</Body>
</div> </div>
</div> </div>
<div
class="card"
class:disabled={!$licensing.pdfEnabled}
on:click={$licensing.pdfEnabled
? () => createScreenModal.show(AutoScreenTypes.PDF)
: null}
>
<div class="image">
<img alt="A PDF document" src={pdf} width="185" />
</div>
<div class="text">
<Body size="M">
PDF
{#if !$licensing.pdfEnabled}
<Tags>
<Tag icon="LockClosed">Premium</Tag>
</Tags>
{/if}
</Body>
<Body size="XS">Create, edit and export your PDF</Body>
</div>
</div>
</div> </div>
</CreationPage> </CreationPage>
</div> </div>
@ -86,7 +121,7 @@
transition: filter 150ms; transition: filter 150ms;
} }
.card:hover { .card:not(.disabled):hover {
filter: brightness(1.1); filter: brightness(1.1);
cursor: pointer; cursor: pointer;
} }
@ -96,29 +131,38 @@
width: 100%; width: 100%;
max-height: 127px; max-height: 127px;
overflow: hidden; overflow: hidden;
}
.image img {
width: 100%;
}
.card .image {
min-width: 235px; min-width: 235px;
height: 127px; height: 127px;
background-color: var(--grey-2); background-color: var(--grey-2);
position: relative;
}
.card.disabled .image:after {
content: "";
height: 100%;
width: 100%;
top: 0;
left: 0;
position: absolute;
background: rgba(0, 0, 0, 0.5);
}
.image img {
width: 100%;
} }
.text { .text {
border: 1px solid var(--grey-4); border: 1px solid var(--grey-4);
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
padding: 8px 16px 13px 16px; padding: 12px 16px 12px 16px;
display: flex;
flex-direction: column;
gap: 2px;
} }
.text :global(p:first-child) {
.text :global(p:nth-child(1)) { display: flex;
margin-bottom: 6px; gap: var(--spacing-m);
align-items: center;
} }
.text :global(p:nth-child(2)) { .text :global(p:nth-child(2)) {
color: var(--grey-6); color: var(--spectrum-global-color-gray-600);
} }
</style> </style>

View File

@ -4,22 +4,24 @@ import { Helpers } from "@budibase/bbui"
import { RoleUtils, Utils } from "@budibase/frontend-core" import { RoleUtils, Utils } from "@budibase/frontend-core"
import { findAllMatchingComponents } from "@/helpers/components" import { findAllMatchingComponents } from "@/helpers/components"
import { import {
layoutStore,
appStore, appStore,
componentStore, componentStore,
layoutStore,
navigationStore, navigationStore,
previewStore,
selectedComponent, selectedComponent,
} from "@/stores/builder" } from "@/stores/builder"
import { createHistoryStore, HistoryStore } from "@/stores/builder/history" import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
import { API } from "@/api" import { API } from "@/api"
import { BudiStore } from "../BudiStore" import { BudiStore } from "../BudiStore"
import { import {
FetchAppPackageResponse,
DeleteScreenResponse,
Screen,
Component, Component,
SaveScreenResponse,
ComponentDefinition, ComponentDefinition,
DeleteScreenResponse,
FetchAppPackageResponse,
SaveScreenResponse,
Screen,
ScreenVariant,
} from "@budibase/types" } from "@budibase/types"
interface ScreenState { interface ScreenState {
@ -115,6 +117,14 @@ export class ScreenStore extends BudiStore<ScreenState> {
state.selectedScreenId = screen._id state.selectedScreenId = screen._id
return state return state
}) })
// If this is a PDF screen, ensure we're on desktop
if (
screen.variant === ScreenVariant.PDF &&
get(previewStore).previewDevice !== "desktop"
) {
previewStore.setDevice("desktop")
}
} }
/** /**

View File

@ -39,6 +39,7 @@ interface LicensingState {
customAppScriptsEnabled: boolean customAppScriptsEnabled: boolean
syncAutomationsEnabled: boolean syncAutomationsEnabled: boolean
triggerAutomationRunEnabled: boolean triggerAutomationRunEnabled: boolean
pdfEnabled: boolean
// the currently used quotas from the db // the currently used quotas from the db
quotaUsage?: QuotaUsage quotaUsage?: QuotaUsage
// derived quota metrics for percentages used // derived quota metrics for percentages used
@ -81,6 +82,7 @@ class LicensingStore extends BudiStore<LicensingState> {
customAppScriptsEnabled: false, customAppScriptsEnabled: false,
syncAutomationsEnabled: false, syncAutomationsEnabled: false,
triggerAutomationRunEnabled: false, triggerAutomationRunEnabled: false,
pdfEnabled: false,
// the currently used quotas from the db // the currently used quotas from the db
quotaUsage: undefined, quotaUsage: undefined,
// derived quota metrics for percentages used // derived quota metrics for percentages used
@ -187,6 +189,7 @@ class LicensingStore extends BudiStore<LicensingState> {
const customAppScriptsEnabled = features.includes( const customAppScriptsEnabled = features.includes(
Constants.Features.CUSTOM_APP_SCRIPTS Constants.Features.CUSTOM_APP_SCRIPTS
) )
const pdfEnabled = features.includes(Constants.Features.PDF)
this.update(state => { this.update(state => {
return { return {
...state, ...state,
@ -208,6 +211,7 @@ class LicensingStore extends BudiStore<LicensingState> {
triggerAutomationRunEnabled, triggerAutomationRunEnabled,
perAppBuildersEnabled, perAppBuildersEnabled,
customAppScriptsEnabled, customAppScriptsEnabled,
pdfEnabled,
} }
}) })
} }

View File

@ -1,5 +1,6 @@
import { BaseStructure } from "../BaseStructure" import { BaseStructure } from "../BaseStructure"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { ScreenVariant } from "@budibase/types"
export class Screen extends BaseStructure { export class Screen extends BaseStructure {
constructor() { constructor() {
@ -81,3 +82,25 @@ export class Screen extends BaseStructure {
return this return this
} }
} }
export class PDFScreen extends Screen {
constructor() {
super()
this._json.variant = ScreenVariant.PDF
this._json.width = "Max"
this._json.showNavigation = false
this._json.props = {
_id: Helpers.uuid(),
_component: "@budibase/standard-components/pdf",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "PDF",
title: "PDF",
}
}
}

View File

@ -1,3 +1,4 @@
export { default as blank } from "./blank" export { default as blank } from "./blank"
export { default as form } from "./form" export { default as form } from "./form"
export { default as table } from "./table" export { default as table } from "./table"
export { default as pdf } from "./pdf"

View File

@ -0,0 +1,20 @@
import { PDFScreen } from "./Screen"
import { capitalise } from "@/helpers"
import getValidRoute from "./getValidRoute"
import { Roles } from "@/constants/backend"
const pdf = ({ route, screens }) => {
const validRoute = getValidRoute(screens, route, Roles.BASIC)
const template = new PDFScreen().role(Roles.BASIC).route(validRoute).json()
return [
{
data: template,
navigationLinkLabel:
validRoute === "/" ? null : capitalise(validRoute.split("/")[1]),
},
]
}
export default pdf

View File

@ -149,6 +149,7 @@ export const customTypeToSchema: Record<string, SchemaFieldTypes> = {
SchemaFieldTypes.AUTOMATION_FIELDS, SchemaFieldTypes.AUTOMATION_FIELDS,
[AutomationCustomIOType.WEBHOOK_URL]: SchemaFieldTypes.WEBHOOK_URL, [AutomationCustomIOType.WEBHOOK_URL]: SchemaFieldTypes.WEBHOOK_URL,
[AutomationCustomIOType.QUERY_LIMIT]: SchemaFieldTypes.QUERY_LIMIT, [AutomationCustomIOType.QUERY_LIMIT]: SchemaFieldTypes.QUERY_LIMIT,
[AutomationCustomIOType.QUERY_PARAMS]: SchemaFieldTypes.QUERY_PARAMS,
["fields"]: SchemaFieldTypes.FIELDS, ["fields"]: SchemaFieldTypes.FIELDS,
} }

View File

@ -8017,6 +8017,32 @@
"key": "text", "key": "text",
"wide": true "wide": true
}, },
{
"type": "select",
"label": "Size",
"key": "size",
"defaultValue": "14px",
"showInBar": true,
"placeholder": "Default",
"options": [
{
"label": "Small",
"value": "12px"
},
{
"label": "Medium",
"value": "14px"
},
{
"label": "Large",
"value": "18px"
},
{
"label": "Extra large",
"value": "24px"
}
]
},
{ {
"type": "select", "type": "select",
"label": "Alignment", "label": "Alignment",
@ -8058,5 +8084,133 @@
"showInBar": true "showInBar": true
} }
] ]
},
"pdf": {
"name": "PDF Generator",
"icon": "Document",
"hasChildren": true,
"showEmptyState": false,
"illegalChildren": ["sidepanel", "modal", "gridblock"],
"grid": {
"hAlign": "center",
"vAlign": "start"
},
"size": {
"width": 800,
"height": 1200
},
"description": "A component to render PDFs from other Budibase components",
"settings": [
{
"type": "text",
"label": "PDF title",
"key": "fileName",
"defaultValue": "Report"
},
{
"type": "text",
"label": "Button text",
"key": "buttonText",
"defaultValue": "Download PDF"
}
]
},
"pdftable": {
"name": "PDF Table",
"icon": "Table",
"styles": ["size"],
"size": {
"width": 600,
"height": 304
},
"grid": {
"hAlign": "stretch",
"vAlign": "stretch"
},
"settings": [
{
"type": "dataSource",
"label": "Data",
"key": "datasource",
"required": true
},
{
"type": "filter",
"label": "Filtering",
"key": "filter",
"resetOn": "datasource",
"dependsOn": {
"setting": "datasource.type",
"value": "custom",
"invert": true
}
},
{
"type": "field/sortable",
"label": "Sort column",
"key": "sortColumn",
"placeholder": "Default",
"resetOn": "datasource",
"dependsOn": {
"setting": "datasource.type",
"value": "custom",
"invert": true
}
},
{
"type": "select",
"label": "Sort order",
"key": "sortOrder",
"resetOn": "datasource",
"options": ["Ascending", "Descending"],
"defaultValue": "Ascending",
"dependsOn": "sortColumn"
},
{
"type": "columns/toplevel",
"label": "Columns",
"key": "columns",
"resetOn": "datasource",
"placeholder": "First 3 columns"
},
{
"type": "number",
"label": "Limit",
"key": "limit",
"defaultValue": 10
}
]
},
"singlerowprovider": {
"name": "Single Row Provider",
"icon": "SQLQuery",
"hasChildren": true,
"actions": ["RefreshDatasource"],
"size": {
"width": 500,
"height": 200
},
"grid": {
"hAlign": "stretch",
"vAlign": "stretch"
},
"settings": [
{
"type": "table",
"label": "Datasource",
"key": "datasource",
"required": true
},
{
"type": "text",
"label": "Row ID",
"key": "rowId",
"required": true,
"resetOn": "datasource"
}
],
"context": {
"type": "schema"
}
} }
} }

View File

@ -29,6 +29,7 @@
"apexcharts": "^3.48.0", "apexcharts": "^3.48.0",
"dayjs": "^1.10.8", "dayjs": "^1.10.8",
"downloadjs": "1.4.7", "downloadjs": "1.4.7",
"html2pdf.js": "^0.9.3",
"html5-qrcode": "^2.3.8", "html5-qrcode": "^2.3.8",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"sanitize-html": "^2.13.0", "sanitize-html": "^2.13.0",

View File

@ -1,20 +1,20 @@
<script> <script>
import { getContext, onDestroy } from "svelte" import { getContext, onDestroy } from "svelte"
import { generate } from "shortid" import { generate } from "shortid"
import { builderStore } from "../stores/builder" import { builderStore } from "@/stores/builder"
import Component from "@/components/Component.svelte" import Component from "@/components/Component.svelte"
export let type export let type
export let props export let props = undefined
export let styles export let styles = undefined
export let context export let context = undefined
export let name export let name = undefined
export let order = 0 export let order = 0
export let containsSlot = false export let containsSlot = false
// ID is only exposed as a prop so that it can be bound to from parent // ID is only exposed as a prop so that it can be bound to from parent
// block components // block components
export let id export let id = undefined
const component = getContext("component") const component = getContext("component")
const block = getContext("block") const block = getContext("block")

View File

@ -196,8 +196,6 @@
} }
// Metadata to pass into grid action to apply CSS // Metadata to pass into grid action to apply CSS
const checkGrid = x =>
x?._component?.endsWith("/container") && x?.layout === "grid"
$: insideGrid = checkGrid(parent) $: insideGrid = checkGrid(parent)
$: isGrid = checkGrid(instance) $: isGrid = checkGrid(instance)
$: gridMetadata = { $: gridMetadata = {
@ -601,6 +599,18 @@
} }
} }
const checkGrid = x => {
// Check for a grid container
if (x?._component?.endsWith("/container") && x?.layout === "grid") {
return true
}
// Check for a PDF (always grid)
if (x?._component?.endsWith("/pdf")) {
return true
}
return false
}
onMount(() => { onMount(() => {
// Register this component instance for external access // Register this component instance for external access
if ($appStore.isDevApp) { if ($appStore.isDevApp) {

View File

@ -1,12 +1,18 @@
<script> <script>
import { themeStore } from "@/stores" import { themeStore } from "@/stores"
import { setContext } from "svelte" import { setContext } from "svelte"
import { Context } from "@budibase/bbui" import { Context, Helpers } from "@budibase/bbui"
setContext(Context.PopoverRoot, "#theme-root") export let popoverRoot = true
const id = Helpers.uuid()
if (popoverRoot) {
setContext(Context.PopoverRoot, `#${id}`)
}
</script> </script>
<div style={$themeStore.customThemeCss} id="theme-root"> <div style={$themeStore.customThemeCss} {id}>
<slot /> <slot />
</div> </div>

View File

@ -0,0 +1,46 @@
<script lang="ts">
import { getContext } from "svelte"
import type { Row, TableDatasource, ViewDatasource } from "@budibase/types"
export let datasource: TableDatasource | ViewDatasource
export let rowId: string
const component = getContext("component")
const { styleable, API, Provider, ActionTypes } = getContext("sdk")
let row: Row | undefined
$: datasourceId =
datasource.type === "table" ? datasource.tableId : datasource.id
$: fetchRow(datasourceId, rowId)
$: actions = [
{
type: ActionTypes.RefreshDatasource,
callback: () => fetchRow(datasourceId, rowId),
metadata: { dataSource: datasource },
},
]
const fetchRow = async (datasourceId: string, rowId: string) => {
try {
row = await API.fetchRow(datasourceId, rowId)
} catch (e) {
row = undefined
}
}
</script>
<div use:styleable={$component.styles}>
<Provider {actions} data={row ?? null}>
<slot />
</Provider>
</div>
<style>
div {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
</style>

View File

@ -5,12 +5,13 @@
export let text: any = "" export let text: any = ""
export let color: string | undefined = undefined export let color: string | undefined = undefined
export let align: "left" | "center" | "right" | "justify" = "left" export let align: "left" | "center" | "right" | "justify" = "left"
export let size: string | undefined = "14px"
const component = getContext("component") const component = getContext("component")
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
// Add in certain settings to styles // Add in certain settings to styles
$: styles = enrichStyles($component.styles, color, align) $: styles = enrichStyles($component.styles, color, align, size)
// Ensure we're always passing in a string value to the markdown editor // Ensure we're always passing in a string value to the markdown editor
$: safeText = stringify(text) $: safeText = stringify(text)
@ -18,10 +19,12 @@
const enrichStyles = ( const enrichStyles = (
styles: any, styles: any,
colorStyle: typeof color, colorStyle: typeof color,
alignStyle: typeof align alignStyle: typeof align,
size: string | undefined
) => { ) => {
let additions: Record<string, string> = { let additions: Record<string, string> = {
"text-align": alignStyle, "text-align": alignStyle,
"font-size": size || "14px",
} }
if (colorStyle) { if (colorStyle) {
additions.color = colorStyle additions.color = colorStyle

View File

@ -135,12 +135,18 @@
use:styleable={$styles} use:styleable={$styles}
data-cols={GridColumns} data-cols={GridColumns}
data-col-size={colSize} data-col-size={colSize}
data-required-rows={requiredRows}
on:click={onClick} on:click={onClick}
> >
{#if inBuilder} {#if inBuilder}
<div class="underlay"> <div class="underlay-h">
{#each { length: GridColumns * rows } as _, idx} {#each { length: rows } as _}
<div class="placeholder" class:first-col={idx % GridColumns === 0} /> <div class="placeholder-h" />
{/each}
</div>
<div class="underlay-v">
{#each { length: GridColumns } as _}
<div class="placeholder-v" />
{/each} {/each}
</div> </div>
{/if} {/if}
@ -151,7 +157,8 @@
<style> <style>
.grid, .grid,
.underlay { .underlay-h,
.underlay-v {
height: var(--height) !important; height: var(--height) !important;
min-height: var(--min-height) !important; min-height: var(--min-height) !important;
max-height: none !important; max-height: none !important;
@ -161,37 +168,45 @@
grid-template-columns: repeat(var(--cols), calc(var(--col-size) * 1px)); grid-template-columns: repeat(var(--cols), calc(var(--col-size) * 1px));
position: relative; position: relative;
} }
.underlay { .clickable {
cursor: pointer;
}
/* Underlay grid lines */
.underlay-h,
.underlay-v {
z-index: 0;
display: none; display: none;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-top: 1px solid var(--spectrum-global-color-gray-900);
opacity: 0.1; opacity: 0.1;
pointer-events: none; pointer-events: none;
} }
.underlay { .placeholder-h {
z-index: 0;
}
.placeholder {
border-bottom: 1px solid var(--spectrum-global-color-gray-900); border-bottom: 1px solid var(--spectrum-global-color-gray-900);
grid-column: 1 / -1;
}
.placeholder-h:first-child {
border-top: 1px solid var(--spectrum-global-color-gray-900);
}
.placeholder-v {
border-right: 1px solid var(--spectrum-global-color-gray-900); border-right: 1px solid var(--spectrum-global-color-gray-900);
grid-row: 1 / -1;
} }
.placeholder.first-col { .placeholder-v:first-child {
border-left: 1px solid var(--spectrum-global-color-gray-900); border-left: 1px solid var(--spectrum-global-color-gray-900);
} }
.clickable {
cursor: pointer;
}
/* Highlight grid lines when resizing children */ /* Highlight grid lines when resizing children */
:global(.grid.highlight > .underlay) { :global(.grid.highlight > .underlay-h),
:global(.grid.highlight > .underlay-v) {
display: grid; display: grid;
} }
/* Highlight sibling borders when resizing childern */ /* Highlight sibling borders when resizing children */
:global(.grid.highlight > .component:not(.dragging)) { :global(.grid.highlight > .component:not(.dragging)) {
outline: 2px solid var(--spectrum-global-color-static-blue-200); outline: 2px solid var(--spectrum-global-color-static-blue-200);
pointer-events: none !important; pointer-events: none !important;

View File

@ -36,10 +36,12 @@ export { default as sidepanel } from "./SidePanel.svelte"
export { default as modal } from "./Modal.svelte" export { default as modal } from "./Modal.svelte"
export { default as gridblock } from "./GridBlock.svelte" export { default as gridblock } from "./GridBlock.svelte"
export { default as textv2 } from "./Text.svelte" export { default as textv2 } from "./Text.svelte"
export { default as singlerowprovider } from "./SingleRowProvider.svelte"
export * from "./charts" export * from "./charts"
export * from "./forms" export * from "./forms"
export * from "./blocks" export * from "./blocks"
export * from "./dynamic-filter" export * from "./dynamic-filter"
export * from "./pdf"
// Deprecated component left for compatibility in old apps // Deprecated component left for compatibility in old apps
export * from "./deprecated/table" export * from "./deprecated/table"

View File

@ -0,0 +1,191 @@
<script lang="ts">
import { getContext, onMount, tick } from "svelte"
import { Heading, Button } from "@budibase/bbui"
import { htmlToPdf, pxToPt, A4HeightPx, type PDFOptions } from "./pdf"
import { GridRowHeight } from "@/constants"
import CustomThemeWrapper from "@/components/CustomThemeWrapper.svelte"
const component = getContext("component")
const { styleable, Block, BlockComponent } = getContext("sdk")
export let fileName: string | undefined
export let buttonText: string | undefined
// Derive dimension calculations
const DesiredRows = 40
const innerPageHeightPx = GridRowHeight * DesiredRows
const doubleMarginPx = A4HeightPx - innerPageHeightPx
const marginPt = pxToPt(doubleMarginPx / 2)
let rendering = false
let pageCount = 1
let ref: HTMLElement
let gridRef: HTMLElement
$: safeName = fileName || "Report"
$: safeButtonText = buttonText || "Download PDF"
$: heightPx = pageCount * innerPageHeightPx + doubleMarginPx
$: pageStyle = `--height:${heightPx}px; --margin:${marginPt}pt;`
$: gridMinHeight = pageCount * DesiredRows * GridRowHeight
const generatePDF = async () => {
rendering = true
await tick()
preprocessCSS()
try {
const opts: PDFOptions = {
fileName: safeName,
marginPt,
footer: true,
}
await htmlToPdf(ref, opts)
} catch (error) {
console.error("Error rendering PDF", error)
}
rendering = false
}
const preprocessCSS = () => {
const els = document.getElementsByClassName("grid-child")
for (let el of els) {
if (!(el instanceof HTMLElement)) {
return
}
// Get the computed values and assign them back to the style, simplifying
// the CSS that gets handled by HTML2PDF
const styles = window.getComputedStyle(el)
el.style.setProperty("grid-column-end", styles.gridColumnEnd, "important")
}
}
const getDividerStyle = (idx: number) => {
const top = (idx + 1) * innerPageHeightPx + doubleMarginPx / 2
return `--idx:"${idx + 1}"; --top:${top}px;`
}
const handleGridMutation = () => {
const rows = parseInt(gridRef.dataset.requiredRows || "1")
const nextPageCount = Math.max(1, Math.ceil(rows / DesiredRows))
if (nextPageCount > pageCount || !gridRef.classList.contains("highlight")) {
pageCount = nextPageCount
}
}
onMount(() => {
// Observe required content rows and use this to determine required pages
const gridDOMID = `${$component.id}-grid-dom`
gridRef = document.getElementsByClassName(gridDOMID)[0] as HTMLElement
const mutationObserver = new MutationObserver(handleGridMutation)
mutationObserver.observe(gridRef, {
attributes: true,
attributeFilter: ["data-required-rows", "class"],
})
return () => {
mutationObserver.disconnect()
}
})
</script>
<Block>
<div class="wrapper" style="--margin:{marginPt}pt;">
<div class="container" use:styleable={$component.styles}>
<div class="title">
<Heading size="M">{safeName}</Heading>
<Button disabled={rendering} cta on:click={generatePDF}>
{safeButtonText}
</Button>
</div>
<div class="page" style={pageStyle}>
{#if pageCount > 1}
{#each { length: pageCount } as _, idx}
<div
class="divider"
class:last={idx === pageCount - 1}
style={getDividerStyle(idx)}
/>
{/each}
{/if}
<div
class="spectrum spectrum--medium spectrum--light pageContent"
bind:this={ref}
>
<CustomThemeWrapper popoverRoot={false}>
<BlockComponent
type="container"
props={{ layout: "grid" }}
styles={{
normal: {
height: `${gridMinHeight}px`,
},
}}
context="grid"
>
<slot />
</BlockComponent>
</CustomThemeWrapper>
</div>
</div>
</div>
</div>
</Block>
<style>
.wrapper {
width: 100%;
height: 100%;
padding: 64px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
}
.container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
width: 595.28pt;
gap: var(--spacing-xl);
align-self: center;
}
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.page {
width: 595.28pt;
min-height: var(--height);
padding: var(--margin);
background-color: white;
flex: 0 0 auto;
display: flex;
justify-content: flex-start;
align-items: stretch;
box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, 0.1);
flex-direction: column;
margin: 0 auto;
position: relative;
}
.pageContent {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
background: white;
}
.divider {
width: 100%;
height: 2px;
background: var(--spectrum-global-color-static-gray-400);
position: absolute;
left: 0;
top: var(--top);
transform: translateY(-50%);
}
.divider.last {
top: calc(var(--top) + var(--margin));
background: transparent;
}
</style>

View File

@ -0,0 +1,143 @@
<script lang="ts">
import type {
DataFetchDatasource,
FieldSchema,
GroupUserDatasource,
SortOrder,
TableSchema,
UISearchFilter,
UserDatasource,
} from "@budibase/types"
import { fetchData, QueryUtils, stringifyRow } from "@budibase/frontend-core"
import { getContext } from "svelte"
type ProviderDatasource = Exclude<
DataFetchDatasource,
UserDatasource | GroupUserDatasource
>
type ChosenColumns = Array<{ name: string; displayName?: string }> | undefined
type Schema = { [key: string]: FieldSchema & { displayName: string } }
export let datasource: ProviderDatasource
export let filter: UISearchFilter | undefined = undefined
export let sortColumn: string | undefined = undefined
export let sortOrder: SortOrder | undefined = undefined
export let columns: ChosenColumns = undefined
export let limit: number = 20
const component = getContext("component")
const { styleable, API } = getContext("sdk")
$: query = QueryUtils.buildQuery(filter)
$: fetch = createFetch(datasource)
$: fetch.update({
query,
sortColumn,
sortOrder,
limit,
})
$: schema = sanitizeSchema($fetch.schema, columns)
$: columnCount = Object.keys(schema).length
$: rowCount = $fetch.rows?.length || 0
$: stringifiedRows = ($fetch?.rows || []).map(row =>
stringifyRow(row, schema)
)
const createFetch = (datasource: ProviderDatasource) => {
return fetchData({
API,
datasource,
options: {
query,
sortColumn,
sortOrder,
limit,
paginate: false,
},
})
}
const sanitizeSchema = (
schema: TableSchema | null,
columns: ChosenColumns
): Schema => {
if (!schema) {
return {}
}
let sanitized: Schema = {}
// Clean out hidden fields and ensure we have
Object.entries(schema).forEach(([field, fieldSchema]) => {
if (fieldSchema.visible !== false) {
sanitized[field] = {
...fieldSchema,
displayName: field,
}
}
})
// Clean out unselected columns.
// Default to first 3 columns if none specified, as we are width contrained.
if (!columns?.length) {
columns = Object.values(sanitized).slice(0, 3)
}
let pruned: Schema = {}
for (let col of columns) {
if (sanitized[col.name]) {
pruned[col.name] = {
...sanitized[col.name],
displayName: col.displayName || sanitized[col.name].displayName,
}
}
}
sanitized = pruned
return sanitized
}
</script>
<div class="vars" style="--cols:{columnCount}; --rows:{rowCount};">
<div class="table" class:valid={!!schema} use:styleable={$component.styles}>
{#if schema}
{#each Object.keys(schema) as col}
<div class="cell header">{schema[col].displayName}</div>
{/each}
{#each stringifiedRows as row}
{#each Object.keys(schema) as col}
<div class="cell">{row[col]}</div>
{/each}
{/each}
{/if}
</div>
</div>
<style>
.vars {
display: contents;
--border-color: var(--spectrum-global-color-gray-300);
}
.table {
display: grid;
grid-template-columns: repeat(var(--cols), minmax(40px, auto));
grid-template-rows: repeat(var(--rows), max-content);
overflow: hidden;
background: var(--spectrum-global-color-gray-50);
}
.table.valid {
border-left: 1px solid var(--border-color);
border-top: 1px solid var(--border-color);
}
.cell {
border-right: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-xs) var(--spacing-s);
overflow: hidden;
word-break: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.cell.header {
font-weight: 600;
}
</style>

View File

@ -0,0 +1,2 @@
export { default as pdf } from "./PDF.svelte"
export { default as pdftable } from "./PDFTable.svelte"

View File

@ -0,0 +1,78 @@
// @ts-ignore
import html2pdf from "html2pdf.js"
export const pxToPt = (px: number) => (px / 4) * 3
export const ptToPx = (pt: number) => (pt / 3) * 4
export const A4HeightPx = ptToPx(841.92) + 1
export interface PDFOptions {
fileName?: string
marginPt?: number
orientation?: "portrait" | "landscape"
htmlScale?: number
footer?: boolean
}
export async function htmlToPdf(el: HTMLElement, opts: PDFOptions = {}) {
const userOpts: Required<PDFOptions> = {
fileName: "file.pdf",
marginPt: 60,
orientation: "portrait",
htmlScale: 1,
footer: true,
...opts,
}
return new Promise(resolve => {
// Sanity check title
let fileName = userOpts.fileName
if (!fileName.endsWith(".pdf")) {
fileName += ".pdf"
}
// Config
const options = {
margin: userOpts.marginPt,
filename: fileName,
image: { type: "jpeg", quality: 0.95 },
html2canvas: { dpi: 192, scale: 2, useCORS: true },
jsPDF: {
orientation: userOpts.orientation,
unit: "pt",
format: "a4",
},
pagebreak: { avoid: ".no-break" },
// Custom params
htmlScale: userOpts.htmlScale,
}
let worker = html2pdf().set(options).from(el).toPdf()
// Add footer if required
if (opts.footer) {
worker = worker.get("pdf").then((pdf: any) => {
const totalPages = pdf.internal.getNumberOfPages()
for (let i = 1; i <= totalPages; i++) {
pdf.setPage(i)
pdf.setFontSize(10)
pdf.setTextColor(200)
pdf.text(
`Page ${i} of ${totalPages}`,
pdf.internal.pageSize.getWidth() - options.margin,
pdf.internal.pageSize.getHeight() - options.margin / 2,
"right"
)
pdf.text(
options.filename.replace(".pdf", ""),
options.margin,
pdf.internal.pageSize.getHeight() - options.margin / 2
)
}
})
}
worker.save().then(resolve)
})
}

View File

@ -54,4 +54,4 @@
zIndex={selected ? 890 : 910} zIndex={selected ? 890 : 910}
allowResizeAnchors allowResizeAnchors
/> />
{/if}} {/if}

View File

@ -31,6 +31,8 @@ import {
} from "@budibase/types" } from "@budibase/types"
import { ActionTypes } from "@/constants" import { ActionTypes } from "@/constants"
import { APIClient } from "@budibase/frontend-core" import { APIClient } from "@budibase/frontend-core"
import BlockComponent from "./components/BlockComponent.svelte"
import Block from "./components/Block.svelte"
// Provide svelte and svelte/internal as globals for custom components // Provide svelte and svelte/internal as globals for custom components
import * as svelte from "svelte" import * as svelte from "svelte"
@ -89,6 +91,8 @@ export interface SDK {
notificationStore: typeof notificationStore notificationStore: typeof notificationStore
environmentStore: typeof environmentStore environmentStore: typeof environmentStore
appStore: typeof appStore appStore: typeof appStore
Block: typeof Block
BlockComponent: typeof BlockComponent
} }
let app: ClientApp let app: ClientApp

View File

@ -8,6 +8,7 @@ import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js" import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "@/constants" import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "@/constants"
import { ScreenVariant } from "@budibase/types"
const createScreenStore = () => { const createScreenStore = () => {
const store = derived( const store = derived(
@ -193,5 +194,8 @@ const createScreenStore = () => {
export const screenStore = createScreenStore() export const screenStore = createScreenStore()
export const isGridScreen = derived(screenStore, $screenStore => { export const isGridScreen = derived(screenStore, $screenStore => {
return $screenStore.activeScreen?.props?.layout === "grid" return (
$screenStore.activeScreen?.props?.layout === "grid" ||
$screenStore.activeScreen?.variant === ScreenVariant.PDF
)
}) })

View File

@ -116,6 +116,9 @@ export const gridLayout = (node: HTMLDivElement, metadata: GridMetadata) => {
return return
} }
// Add a unique class to elements we mutate so we can easily find them later
node.classList.add("grid-child")
// Callback to select the component when clicking on the wrapper // Callback to select the component when clicking on the wrapper
selectComponent = (e: Event) => { selectComponent = (e: Event) => {
e.stopPropagation() e.stopPropagation()

View File

@ -1,8 +1,5 @@
<script context="module">
const NumberFormatter = Intl.NumberFormat()
</script>
<script> <script>
import { formatNumber } from "@budibase/frontend-core"
import TextCell from "./TextCell.svelte" import TextCell from "./TextCell.svelte"
export let api export let api
@ -13,18 +10,6 @@
const newValue = isNaN(float) ? null : float const newValue = isNaN(float) ? null : float
onChange(newValue) onChange(newValue)
} }
const formatNumber = value => {
const type = typeof value
if (type !== "string" && type !== "number") {
return ""
}
if (type === "string" && !value.trim().length) {
return ""
}
const res = NumberFormatter.format(value)
return res === "NaN" ? value : res
}
</script> </script>
<TextCell <TextCell

View File

@ -0,0 +1,149 @@
import {
BBReferenceFieldSubType,
FieldSchema,
FieldType,
Row,
TableSchema,
} from "@budibase/types"
import { Helpers } from "@budibase/bbui"
// Singleton formatter to save us creating one every time
const NumberFormatter = Intl.NumberFormat()
export type StringifiedRow = { [key: string]: string }
// Formats a number according to the locale
export const formatNumber = (value: any): string => {
const type = typeof value
if (type !== "string" && type !== "number") {
return ""
}
if (type === "string" && !value.trim().length) {
return ""
}
const res = NumberFormatter.format(value)
return res === "NaN" ? stringifyValue(value) : res
}
// Attempts to stringify any type of value
const stringifyValue = (value: any): string => {
if (value == null) {
return ""
}
if (typeof value === "string") {
return value
}
if (typeof value.toString === "function") {
return stringifyValue(value.toString())
}
try {
return JSON.stringify(value)
} catch (e) {
return ""
}
}
const stringifyField = (value: any, schema: FieldSchema): string => {
switch (schema.type) {
// Auto should not exist as it should always be typed by its underlying
// real type, like date or user
case FieldType.AUTO:
return ""
// Just state whether signatures exist or not
case FieldType.SIGNATURE_SINGLE:
return value ? "Yes" : "No"
// Extract attachment names
case FieldType.ATTACHMENT_SINGLE:
case FieldType.ATTACHMENTS: {
if (!value) {
return ""
}
const arrayValue = Array.isArray(value) ? value : [value]
return arrayValue
.map(x => x.name)
.filter(x => !!x)
.join(", ")
}
// Extract primary displays from relationships
case FieldType.LINK: {
if (!value) {
return ""
}
const arrayValue = Array.isArray(value) ? value : [value]
return arrayValue
.map(x => x.primaryDisplay)
.filter(x => !!x)
.join(", ")
}
// Stringify JSON blobs
case FieldType.JSON:
return value ? JSON.stringify(value) : ""
// User is the only BB reference subtype right now
case FieldType.BB_REFERENCE:
case FieldType.BB_REFERENCE_SINGLE: {
if (
schema.subtype !== BBReferenceFieldSubType.USERS &&
schema.subtype !== BBReferenceFieldSubType.USER
) {
return ""
}
if (!value) {
return ""
}
const arrayVal = Array.isArray(value) ? value : [value]
return arrayVal?.map((user: any) => user.primaryDisplay).join(", ") || ""
}
// Join arrays with commas
case FieldType.ARRAY:
return value?.join(", ") || ""
// Just capitalise booleans
case FieldType.BOOLEAN:
return Helpers.capitalise(value?.toString() || "false")
// Format dates into something readable
case FieldType.DATETIME: {
return Helpers.getDateDisplayValue(value, {
enableTime: !schema.dateOnly,
timeOnly: schema.timeOnly,
})
}
// Format numbers using a locale string
case FieldType.NUMBER:
return formatNumber(value)
// Simple string types
case FieldType.STRING:
case FieldType.LONGFORM:
case FieldType.BIGINT:
case FieldType.OPTIONS:
case FieldType.AI:
case FieldType.BARCODEQR:
return value || ""
// Fallback for unknown types or future column types that we forget to add
case FieldType.FORMULA:
default:
return stringifyValue(value)
}
}
// Stringifies every property of a row, ensuring they are all human-readable
// strings for display
export const stringifyRow = (row: Row, schema: TableSchema): StringifiedRow => {
let stringified: StringifiedRow = {}
Object.entries(schema).forEach(([field, fieldSchema]) => {
stringified[field] = stringifyField(
Helpers.deepGet(row, field),
fieldSchema
)
})
return stringified
}

View File

@ -15,3 +15,4 @@ export * from "./relatedColumns"
export * from "./table" export * from "./table"
export * from "./components" export * from "./components"
export * from "./validation" export * from "./validation"
export * from "./formatting"

View File

@ -1,5 +1,6 @@
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
import { TypeIconMap } from "../constants" import { TypeIconMap } from "../constants"
import { convertJSONSchemaToTableSchema } from "./json"
export const getColumnIcon = column => { export const getColumnIcon = column => {
// For some reason we have remix icons saved under this property sometimes, // For some reason we have remix icons saved under this property sometimes,
@ -24,3 +25,25 @@ export const getColumnIcon = column => {
return result || "Text" return result || "Text"
} }
export const addNestedJSONSchemaFields = schema => {
if (!schema) {
return schema
}
let jsonAdditions = {}
Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
squashObjects: true,
})
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,
nestedJSON: true,
}
})
}
})
return { ...schema, ...jsonAdditions }
}

@ -1 +1 @@
Subproject commit 4417bceb24eabdd9a8c1615fb83c4e6fe8c0c914 Subproject commit fc510aa4b7cbda72bb40a1f3250571dc213b5ef5

View File

@ -144,9 +144,11 @@ export async function find(ctx: UserCtx<void, FindRowResponse>) {
const { tableId, viewId } = utils.getSourceId(ctx) const { tableId, viewId } = utils.getSourceId(ctx)
const sourceId = viewId || tableId const sourceId = viewId || tableId
const rowId = ctx.params.rowId const rowId = ctx.params.rowId
try {
const response = await sdk.rows.find(sourceId, rowId) ctx.body = await sdk.rows.find(sourceId, rowId)
ctx.body = response } catch (e) {
ctx.throw(404, "That row couldn't be found")
}
} }
function isDeleteRows(input: any): input is DeleteRows { function isDeleteRows(input: any): input is DeleteRows {

View File

@ -29,7 +29,6 @@ interface TestSetup {
name: string name: string
setup: SetupFn setup: SetupFn
mockLLMResponse: MockLLMResponseFn mockLLMResponse: MockLLMResponseFn
selfHostOnly?: boolean
} }
function budibaseAI(): SetupFn { function budibaseAI(): SetupFn {
@ -80,7 +79,7 @@ function customAIConfig(providerConfig: Partial<ProviderConfig>): SetupFn {
} }
} }
const providers: TestSetup[] = [ const allProviders: TestSetup[] = [
{ {
name: "OpenAI API key", name: "OpenAI API key",
setup: async () => { setup: async () => {
@ -89,7 +88,6 @@ const providers: TestSetup[] = [
}) })
}, },
mockLLMResponse: mockChatGPTResponse, mockLLMResponse: mockChatGPTResponse,
selfHostOnly: true,
}, },
{ {
name: "OpenAI API key with custom config", name: "OpenAI API key with custom config",
@ -126,9 +124,9 @@ describe("AI", () => {
nock.cleanAll() nock.cleanAll()
}) })
describe.each(providers)( describe.each(allProviders)(
"provider: $name", "provider: $name",
({ setup, mockLLMResponse, selfHostOnly }: TestSetup) => { ({ setup, mockLLMResponse }: TestSetup) => {
let cleanup: () => Promise<void> | void let cleanup: () => Promise<void> | void
beforeAll(async () => { beforeAll(async () => {
cleanup = await setup(config) cleanup = await setup(config)
@ -243,86 +241,104 @@ describe("AI", () => {
) )
}) })
}) })
!selfHostOnly &&
describe("POST /api/ai/chat", () => {
let envCleanup: () => void
let featureCleanup: () => void
beforeAll(() => {
envCleanup = setEnv({ SELF_HOSTED: false })
featureCleanup = features.testutils.setFeatureFlags("*", {
AI_JS_GENERATION: true,
})
})
afterAll(() => {
featureCleanup()
envCleanup()
})
beforeEach(() => {
const license: License = {
plan: {
type: PlanType.FREE,
model: PlanModel.PER_USER,
usesInvoicing: false,
},
features: [],
quotas: {} as any,
tenantId: config.tenantId,
}
nock(env.ACCOUNT_PORTAL_URL).get("/api/license").reply(200, license)
})
it("handles correct chat response", async () => {
mockLLMResponse("Hi there!")
const { message } = await config.api.ai.chat({
messages: [{ role: "user", content: "Hello!" }],
licenseKey: "test-key",
})
expect(message).toBe("Hi there!")
})
it("handles chat response error", async () => {
mockLLMResponse(() => {
throw new Error("LLM error")
})
await config.api.ai.chat(
{
messages: [{ role: "user", content: "Hello!" }],
licenseKey: "test-key",
},
{ status: 500 }
)
})
it("handles no license", async () => {
nock.cleanAll()
nock(env.ACCOUNT_PORTAL_URL).get("/api/license").reply(404)
await config.api.ai.chat(
{
messages: [{ role: "user", content: "Hello!" }],
licenseKey: "test-key",
},
{
status: 403,
}
)
})
it("handles no license key", async () => {
await config.api.ai.chat(
{
messages: [{ role: "user", content: "Hello!" }],
// @ts-expect-error - intentionally wrong
licenseKey: undefined,
},
{
status: 403,
}
)
})
})
} }
) )
}) })
describe("BudibaseAI", () => {
const config = new TestConfiguration()
let cleanup: () => void | Promise<void>
beforeAll(async () => {
await config.init()
cleanup = await budibaseAI()(config)
})
afterAll(async () => {
if ("then" in cleanup) {
await cleanup()
} else {
cleanup()
}
config.end()
})
describe("POST /api/ai/chat", () => {
let envCleanup: () => void
let featureCleanup: () => void
beforeAll(() => {
envCleanup = setEnv({ SELF_HOSTED: false })
featureCleanup = features.testutils.setFeatureFlags("*", {
AI_JS_GENERATION: true,
})
})
afterAll(() => {
featureCleanup()
envCleanup()
})
beforeEach(() => {
nock.cleanAll()
const license: License = {
plan: {
type: PlanType.FREE,
model: PlanModel.PER_USER,
usesInvoicing: false,
},
features: [],
quotas: {} as any,
tenantId: config.tenantId,
}
nock(env.ACCOUNT_PORTAL_URL).get("/api/license").reply(200, license)
})
it("handles correct chat response", async () => {
mockChatGPTResponse("Hi there!")
const { message } = await config.api.ai.chat({
messages: [{ role: "user", content: "Hello!" }],
licenseKey: "test-key",
})
expect(message).toBe("Hi there!")
})
it("handles chat response error", async () => {
mockChatGPTResponse(() => {
throw new Error("LLM error")
})
await config.api.ai.chat(
{
messages: [{ role: "user", content: "Hello!" }],
licenseKey: "test-key",
},
{ status: 500 }
)
})
it("handles no license", async () => {
nock.cleanAll()
nock(env.ACCOUNT_PORTAL_URL).get("/api/license").reply(404)
await config.api.ai.chat(
{
messages: [{ role: "user", content: "Hello!" }],
licenseKey: "test-key",
},
{
status: 403,
}
)
})
it("handles no license key", async () => {
await config.api.ai.chat(
{
messages: [{ role: "user", content: "Hello!" }],
// @ts-expect-error - intentionally wrong
licenseKey: undefined,
},
{
status: 403,
}
)
})
})
})

View File

@ -35,6 +35,7 @@ import {
ViewV2, ViewV2,
ViewV2Schema, ViewV2Schema,
ViewV2Type, ViewV2Type,
FormulaType,
} from "@budibase/types" } from "@budibase/types"
import { generator, mocks } from "@budibase/backend-core/tests" import { generator, mocks } from "@budibase/backend-core/tests"
import { datasourceDescribe } from "../../../integrations/tests/utils" import { datasourceDescribe } from "../../../integrations/tests/utils"
@ -3865,6 +3866,48 @@ if (descriptions.length) {
expect(rows[0].count).toEqual(2) expect(rows[0].count).toEqual(2)
}) })
isInternal &&
it("should be able to max a static formula field", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
string: {
type: FieldType.STRING,
name: "string",
},
formula: {
type: FieldType.FORMULA,
name: "formula",
formulaType: FormulaType.STATIC,
responseType: FieldType.NUMBER,
formula: "{{ string }}",
},
},
})
)
await config.api.row.save(table._id!, {
string: "1",
})
await config.api.row.save(table._id!, {
string: "2",
})
const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: {
maxFormula: {
visible: true,
calculationType: CalculationType.MAX,
field: "formula",
},
},
})
const { rows } = await config.api.row.search(view.id)
expect(rows.length).toEqual(1)
expect(rows[0].maxFormula).toEqual(2)
})
it("should not be able to COUNT(DISTINCT ...) against a non-existent field", async () => { it("should not be able to COUNT(DISTINCT ...) against a non-existent field", async () => {
await config.api.viewV2.create( await config.api.viewV2.create(
{ {

View File

@ -365,7 +365,11 @@ export function createSampleDataTableScreen(): Screen {
_component: "@budibase/standard-components/textv2", _component: "@budibase/standard-components/textv2",
_styles: { _styles: {
normal: { normal: {
"--grid-desktop-col-start": 1,
"--grid-desktop-col-end": 3, "--grid-desktop-col-end": 3,
"--grid-desktop-row-start": 1,
"--grid-desktop-row-end": 3,
"--grid-mobile-col-end": 7,
}, },
hover: {}, hover: {},
active: {}, active: {},
@ -384,6 +388,7 @@ export function createSampleDataTableScreen(): Screen {
"--grid-desktop-row-start": 1, "--grid-desktop-row-start": 1,
"--grid-desktop-row-end": 3, "--grid-desktop-row-end": 3,
"--grid-desktop-h-align": "end", "--grid-desktop-h-align": "end",
"--grid-mobile-col-start": 7,
}, },
hover: {}, hover: {},
active: {}, active: {},

View File

@ -4,6 +4,7 @@ import {
canGroupBy, canGroupBy,
FieldType, FieldType,
isNumeric, isNumeric,
isNumericStaticFormula,
PermissionLevel, PermissionLevel,
RelationSchemaField, RelationSchemaField,
RenameColumn, RenameColumn,
@ -176,7 +177,11 @@ async function guardCalculationViewSchema(
} }
const isCount = schema.calculationType === CalculationType.COUNT const isCount = schema.calculationType === CalculationType.COUNT
if (!isCount && !isNumeric(targetSchema.type)) { if (
!isCount &&
!isNumeric(targetSchema.type) &&
!isNumericStaticFormula(targetSchema)
) {
throw new HTTPError( throw new HTTPError(
`Calculation field "${name}" references field "${schema.field}" which is not a numeric field`, `Calculation field "${name}" references field "${schema.field}" which is not a numeric field`,
400 400

View File

@ -1,4 +1,5 @@
import { Document } from "../document" import { Document } from "../document"
import { FieldSchema, FormulaType } from "./table"
export enum FieldType { export enum FieldType {
/** /**
@ -147,6 +148,15 @@ export function isNumeric(type: FieldType) {
return NumericTypes.includes(type) return NumericTypes.includes(type)
} }
export function isNumericStaticFormula(schema: FieldSchema) {
return (
schema.type === FieldType.FORMULA &&
schema.formulaType === FormulaType.STATIC &&
schema.responseType &&
isNumeric(schema.responseType)
)
}
export const GroupByTypes = [ export const GroupByTypes = [
FieldType.STRING, FieldType.STRING,
FieldType.LONGFORM, FieldType.LONGFORM,

View File

@ -15,6 +15,10 @@ export interface ScreenRouting {
homeScreen?: boolean homeScreen?: boolean
} }
export enum ScreenVariant {
PDF = "pdf",
}
export interface Screen extends Document { export interface Screen extends Document {
layoutId?: string layoutId?: string
showNavigation?: boolean showNavigation?: boolean
@ -24,6 +28,7 @@ export interface Screen extends Document {
name?: string name?: string
pluginAdded?: boolean pluginAdded?: boolean
onLoad?: EventHandler[] onLoad?: EventHandler[]
variant?: ScreenVariant
} }
export interface ScreenRoutesViewOutput extends Document { export interface ScreenRoutesViewOutput extends Document {

View File

@ -207,6 +207,8 @@ export interface BaseFieldSchema extends UIFieldMetadata {
autocolumn?: boolean autocolumn?: boolean
autoReason?: AutoReason.FOREIGN_KEY autoReason?: AutoReason.FOREIGN_KEY
subtype?: never subtype?: never
// added when enriching nested JSON fields into schema
nestedJSON?: boolean
} }
interface OtherFieldMetadata extends BaseFieldSchema { interface OtherFieldMetadata extends BaseFieldSchema {

View File

@ -14,6 +14,7 @@ export enum Feature {
OFFLINE = "offline", OFFLINE = "offline",
EXPANDED_PUBLIC_API = "expandedPublicApi", EXPANDED_PUBLIC_API = "expandedPublicApi",
CUSTOM_APP_SCRIPTS = "customAppScripts", CUSTOM_APP_SCRIPTS = "customAppScripts",
PDF = "pdf",
// deprecated - no longer licensed // deprecated - no longer licensed
VIEW_PERMISSIONS = "viewPermissions", VIEW_PERMISSIONS = "viewPermissions",
VIEW_READONLY_COLUMNS = "viewReadonlyColumns", VIEW_READONLY_COLUMNS = "viewReadonlyColumns",

View File

@ -1,6 +1,6 @@
import * as email from "../../../utilities/email" import * as email from "../../../utilities/email"
import env from "../../../environment" import env from "../../../environment"
import { googleCallbackUrl, oidcCallbackUrl } from "./auth" import * as auth from "./auth"
import { import {
cache, cache,
configs, configs,
@ -420,20 +420,58 @@ export async function publicSettings(
) { ) {
try { try {
// settings // settings
const configDoc = await configs.getSettingsConfigDoc() const [configDoc, googleConfig] = await Promise.all([
configs.getSettingsConfigDoc(),
configs.getGoogleConfig(),
])
const config = configDoc.config const config = configDoc.config
const branding = await pro.branding.getBrandingConfig(config) const brandingPromise = pro.branding.getBrandingConfig(config)
// enrich the logo url - empty url means deleted const getLogoUrl = () => {
if (config.logoUrl && config.logoUrl !== "") { // enrich the logo url - empty url means deleted
config.logoUrl = await objectStore.getGlobalFileUrl( if (config.logoUrl && config.logoUrl !== "") {
"settings", return objectStore.getGlobalFileUrl(
"logoUrl", "settings",
config.logoUrlEtag "logoUrl",
) config.logoUrlEtag
)
}
} }
// google
const googleDatasourcePromise = configs.getGoogleDatasourceConfig()
const preActivated = googleConfig && googleConfig.activated == null
const google = preActivated || !!googleConfig?.activated
const googleCallbackUrlPromise = auth.googleCallbackUrl(googleConfig)
// oidc
const oidcConfigPromise = configs.getOIDCConfig()
const oidcCallbackUrlPromise = auth.oidcCallbackUrl()
// sso enforced
const isSSOEnforcedPromise = pro.features.isSSOEnforced({ config })
// performance all async work at same time, there is no need for all of these
// operations to occur in sync, slowing the endpoint down significantly
const [
branding,
googleDatasource,
googleCallbackUrl,
oidcConfig,
oidcCallbackUrl,
isSSOEnforced,
logoUrl,
] = await Promise.all([
brandingPromise,
googleDatasourcePromise,
googleCallbackUrlPromise,
oidcConfigPromise,
oidcCallbackUrlPromise,
isSSOEnforcedPromise,
getLogoUrl(),
])
// enrich the favicon url - empty url means deleted // enrich the favicon url - empty url means deleted
const faviconUrl = const faviconUrl =
branding.faviconUrl && branding.faviconUrl !== "" branding.faviconUrl && branding.faviconUrl !== ""
@ -444,21 +482,11 @@ export async function publicSettings(
) )
: undefined : undefined
// google
const googleConfig = await configs.getGoogleConfig()
const googleDatasourceConfigured =
!!(await configs.getGoogleDatasourceConfig())
const preActivated = googleConfig && googleConfig.activated == null
const google = preActivated || !!googleConfig?.activated
const _googleCallbackUrl = await googleCallbackUrl(googleConfig)
// oidc
const oidcConfig = await configs.getOIDCConfig()
const oidc = oidcConfig?.activated || false const oidc = oidcConfig?.activated || false
const _oidcCallbackUrl = await oidcCallbackUrl() const googleDatasourceConfigured = !!googleDatasource
if (logoUrl) {
// sso enforced config.logoUrl = logoUrl
const isSSOEnforced = await pro.features.isSSOEnforced({ config }) }
ctx.body = { ctx.body = {
type: ConfigType.SETTINGS, type: ConfigType.SETTINGS,
@ -472,8 +500,8 @@ export async function publicSettings(
googleDatasourceConfigured, googleDatasourceConfigured,
oidc, oidc,
isSSOEnforced, isSSOEnforced,
oidcCallbackUrl: _oidcCallbackUrl, oidcCallbackUrl,
googleCallbackUrl: _googleCallbackUrl, googleCallbackUrl,
}, },
} }
} catch (err: any) { } catch (err: any) {

216
yarn.lock
View File

@ -7527,6 +7527,11 @@ a-sync-waterfall@^1.0.0:
resolved "https://registry.yarnpkg.com/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7" resolved "https://registry.yarnpkg.com/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7"
integrity sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA== integrity sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==
abab@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
integrity sha512-I+Wi+qiE2kUXyrRhNsWv6XsjUTBJjSoVSctKNBfLG5zG/Xe7Rjbxf13+vqYHNTwHaFU+FtSlVxOCTiMEVtPv0A==
abab@^2.0.3, abab@^2.0.5, abab@^2.0.6: abab@^2.0.3, abab@^2.0.5, abab@^2.0.6:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
@ -7598,6 +7603,13 @@ accepts@^1.3.5, accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.8:
mime-types "~2.1.34" mime-types "~2.1.34"
negotiator "0.6.3" negotiator "0.6.3"
acorn-globals@^1.0.4:
version "1.0.9"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-1.0.9.tgz#55bb5e98691507b74579d0513413217c380c54cf"
integrity sha512-j3/4pkfih8W4NK22gxVSXcEonTpAHOHh0hu5BoZrKcOsW/4oBPxTi4Yk3SAj+FhC1f3+bRTkXdm4019gw1vg9g==
dependencies:
acorn "^2.1.0"
acorn-globals@^6.0.0: acorn-globals@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
@ -7636,6 +7648,11 @@ acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0:
dependencies: dependencies:
acorn "^8.11.0" acorn "^8.11.0"
acorn@^2.1.0, acorn@^2.4.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7"
integrity sha512-pXK8ez/pVjqFdAgBkF1YPVRacuLQ9EXBKaKWaeh58WNfMkCmZhOZzu+NtKSPD5PHmCCHheQ5cD29qM1K4QTxIg==
acorn@^5.2.1: acorn@^5.2.1:
version "5.7.4" version "5.7.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
@ -7951,6 +7968,11 @@ array-differ@^3.0.0:
resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b"
integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==
array-equal@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.2.tgz#a8572e64e822358271250b9156d20d96ef5dec04"
integrity sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==
array-flatten@1.1.1: array-flatten@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@ -8371,6 +8393,11 @@ base62@^1.1.0:
resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428" resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428"
integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA== integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA==
base64-arraybuffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1: base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -8853,6 +8880,16 @@ caniuse-lite@^1.0.30001449:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz#31d2e26f0a2309860ed3eff154e03890d9d851a7" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz#31d2e26f0a2309860ed3eff154e03890d9d851a7"
integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==
canvg@^1.0:
version "1.5.3"
resolved "https://registry.yarnpkg.com/canvg/-/canvg-1.5.3.tgz#aad17915f33368bf8eb80b25d129e3ae922ddc5f"
integrity sha512-7Gn2IuQzvUQWPIuZuFHrzsTM0gkPz2RRT9OcbdmA03jeKk8kltrD8gqUzNX15ghY/4PV5bbe5lmD6yDLDY6Ybg==
dependencies:
jsdom "^8.1.0"
rgbcolor "^1.0.1"
stackblur-canvas "^1.4.1"
xmldom "^0.1.22"
caseless@~0.12.0: caseless@~0.12.0:
version "0.12.0" version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@ -8863,6 +8900,11 @@ catering@^2.0.0, catering@^2.1.0:
resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510"
integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==
cf-blob.js@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/cf-blob.js/-/cf-blob.js-0.0.1.tgz#f5ab7e12e798caf08ccf828c69aba0f063d83f99"
integrity sha512-KkUmNT/rgVK+KehG7cSvbLwMb+OS5Qby6ADB4LP12jtx6rfVvHCdyqFUjAeQnDpGpQNNwvpi0R/tluT2J6P99Q==
chai@^4.3.7: chai@^4.3.7:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8"
@ -9701,6 +9743,13 @@ crypto-randomuuid@^1.0.0:
resolved "https://registry.yarnpkg.com/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz#acf583e5e085e867ae23e107ff70279024f9e9e7" resolved "https://registry.yarnpkg.com/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz#acf583e5e085e867ae23e107ff70279024f9e9e7"
integrity sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA== integrity sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==
css-line-break@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
dependencies:
utrie "^1.0.2"
css-tree@^2.3.1: css-tree@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20"
@ -9719,15 +9768,22 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", cssom@~0.3.6:
version "0.3.8"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
cssom@^0.4.4: cssom@^0.4.4:
version "0.4.4" version "0.4.4"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
cssom@~0.3.6: "cssstyle@>= 0.2.34 < 0.3.0":
version "0.3.8" version "0.2.37"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54"
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== integrity sha512-FUpKc+1FNBsHUr9IsfSGCovr8VuGOiiuzlgCyppKBjJi2jYTOFLN3oiiNRMIvYqbFzF38mqKj4BgcevzU5/kIA==
dependencies:
cssom "0.3.x"
cssstyle@^2.3.0: cssstyle@^2.3.0:
version "2.3.0" version "2.3.0"
@ -11039,6 +11095,11 @@ es6-error@^4.0.1, es6-error@^4.1.1:
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
es6-promise@^4.2.5:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
esbuild-node-externals@^1.14.0: esbuild-node-externals@^1.14.0:
version "1.14.0" version "1.14.0"
resolved "https://registry.yarnpkg.com/esbuild-node-externals/-/esbuild-node-externals-1.14.0.tgz#fc2950c67a068dc2b538fd1381ad7d8e20a6f54d" resolved "https://registry.yarnpkg.com/esbuild-node-externals/-/esbuild-node-externals-1.14.0.tgz#fc2950c67a068dc2b538fd1381ad7d8e20a6f54d"
@ -11105,6 +11166,18 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
escodegen@^1.6.1:
version "1.14.3"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
dependencies:
esprima "^4.0.1"
estraverse "^4.2.0"
esutils "^2.0.2"
optionator "^0.8.1"
optionalDependencies:
source-map "~0.6.1"
escodegen@^2.0.0: escodegen@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
@ -11382,7 +11455,7 @@ esrecurse@^4.3.0:
dependencies: dependencies:
estraverse "^5.2.0" estraverse "^5.2.0"
estraverse@^4.1.1: estraverse@^4.1.1, estraverse@^4.2.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
@ -11772,6 +11845,11 @@ file-entry-cache@^8.0.0:
dependencies: dependencies:
flat-cache "^4.0.0" flat-cache "^4.0.0"
file-saver@1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8"
integrity sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg==
file-type@^11.1.0: file-type@^11.1.0:
version "11.1.0" version "11.1.0"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-11.1.0.tgz#93780f3fed98b599755d846b99a1617a2ad063b8" resolved "https://registry.yarnpkg.com/file-type/-/file-type-11.1.0.tgz#93780f3fed98b599755d846b99a1617a2ad063b8"
@ -12979,6 +13057,23 @@ html-tag@^2.0.0:
is-self-closing "^1.0.1" is-self-closing "^1.0.1"
kind-of "^6.0.0" kind-of "^6.0.0"
html2canvas@^1.0.0-alpha.12:
version "1.4.1"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
dependencies:
css-line-break "^2.1.0"
text-segmentation "^1.0.3"
html2pdf.js@^0.9.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/html2pdf.js/-/html2pdf.js-0.9.3.tgz#e7fc6143f748ce253670eaae403987342b66b15c"
integrity sha512-M254g3Z+ZsjtQFDxJlU6E8Zgb8xOpCBQQM1lFPn4Lq+myAdWoYtMFnwlVo/eOI9R1cG75+YmMSDQofkugwOV/Q==
dependencies:
es6-promise "^4.2.5"
html2canvas "^1.0.0-alpha.12"
jspdf "1.4.1"
html5-qrcode@^2.3.8: html5-qrcode@^2.3.8:
version "2.3.8" version "2.3.8"
resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d" resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d"
@ -13129,7 +13224,7 @@ ical-generator@4.1.0:
dependencies: dependencies:
uuid-random "^1.3.2" uuid-random "^1.3.2"
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.5: iconv-lite@0.4.24, iconv-lite@^0.4.13, iconv-lite@^0.4.24, iconv-lite@^0.4.5:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -14618,6 +14713,29 @@ jsdom@^24.1.1:
ws "^8.18.0" ws "^8.18.0"
xml-name-validator "^5.0.0" xml-name-validator "^5.0.0"
jsdom@^8.1.0:
version "8.5.0"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-8.5.0.tgz#d4d8f5dbf2768635b62a62823b947cf7071ebc98"
integrity sha512-rvWfcn2O8SrXPaX5fTYIfPVwvnbU8DnZkjAXK305wfP67csyaJBhgg0F2aU6imqJ+lZmj9EmrBAXy6rWHf2/9Q==
dependencies:
abab "^1.0.0"
acorn "^2.4.0"
acorn-globals "^1.0.4"
array-equal "^1.0.0"
cssom ">= 0.3.0 < 0.4.0"
cssstyle ">= 0.2.34 < 0.3.0"
escodegen "^1.6.1"
iconv-lite "^0.4.13"
nwmatcher ">= 1.3.7 < 2.0.0"
parse5 "^1.5.1"
request "^2.55.0"
sax "^1.1.4"
symbol-tree ">= 3.1.0 < 4.0.0"
tough-cookie "^2.2.0"
webidl-conversions "^3.0.1"
whatwg-url "^2.0.1"
xml-name-validator ">= 2.0.1 < 3.0.0"
jsesc@^3.0.2: jsesc@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e"
@ -14737,6 +14855,17 @@ jsonwebtoken@9.0.2, jsonwebtoken@^9.0.0:
ms "^2.1.1" ms "^2.1.1"
semver "^7.5.4" semver "^7.5.4"
jspdf@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-1.4.1.tgz#8dbd437986346d65efe20ede5361927666b8e4ca"
integrity sha512-2vYVdrvrQUdKKPyWHw81t1jEYYAJ6uFJ/HtTcGbI4qXIQEdl18dLEuL2wTeSv2GzeQLSgUvEvwsXsszuHK+PTw==
dependencies:
canvg "^1.0"
cf-blob.js "0.0.1"
file-saver "1.3.8"
omggif "1.0.7"
stackblur "^1.0.0"
jsprim@^1.2.2: jsprim@^1.2.2:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
@ -16783,6 +16912,11 @@ nunjucks@^3.2.3:
asap "^2.0.3" asap "^2.0.3"
commander "^5.1.0" commander "^5.1.0"
"nwmatcher@>= 1.3.7 < 2.0.0":
version "1.4.4"
resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e"
integrity sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==
nwsapi@^2.2.0, nwsapi@^2.2.4: nwsapi@^2.2.0, nwsapi@^2.2.4:
version "2.2.12" version "2.2.12"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8"
@ -16964,6 +17098,11 @@ obliterator@^1.6.1:
resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3" resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3"
integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig== integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==
omggif@1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d"
integrity sha512-KVVUF85EHKUB9kxxT2D8CksGgfayZKxWtH/+i34zbyDdxFHvsqQs+O756usW7uri2YBD8jE/8GgAsA6wVA1tjg==
omggif@^1.0.10: omggif@^1.0.10:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19"
@ -17434,6 +17573,11 @@ parse5@6.0.1:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parse5@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
integrity sha512-w2jx/0tJzvgKwZa58sj2vAYq/S/K1QJfIB3cWYea/Iu1scFPDQQ3IQiVZTHWtRBwAjv2Yd7S/xeZf3XqLDb3bA==
parse5@^7.1.2: parse5@^7.1.2:
version "7.1.2" version "7.1.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
@ -18997,7 +19141,7 @@ remixicon@2.5.0:
resolved "https://registry.yarnpkg.com/remixicon/-/remixicon-2.5.0.tgz#b5e245894a1550aa23793f95daceadbf96ad1a41" resolved "https://registry.yarnpkg.com/remixicon/-/remixicon-2.5.0.tgz#b5e245894a1550aa23793f95daceadbf96ad1a41"
integrity sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww== integrity sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww==
request@^2.88.0: request@^2.55.0, request@^2.88.0:
version "2.88.2" version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@ -19162,6 +19306,11 @@ rfdc@^1.3.0, rfdc@^1.3.1:
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rgbcolor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==
rimraf@3.0.2, rimraf@^3.0.2: rimraf@3.0.2, rimraf@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@ -19387,6 +19536,11 @@ sax@>=0.6.0:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
sax@^1.1.4:
version "1.4.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f"
integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==
saxes@^5.0.1: saxes@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
@ -20049,6 +20203,16 @@ stackback@0.0.2:
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
stackblur-canvas@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-1.4.1.tgz#849aa6f94b272ff26f6471fa4130ed1f7e47955b"
integrity sha512-TfbTympL5C1K+F/RizDkMBqH18EkUKU8V+4PphIXR+fWhZwwRi3bekP04gy2TOwOT3R6rJQJXAXFrbcZde7wow==
stackblur@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stackblur/-/stackblur-1.0.0.tgz#b407a7e05c93b08d66883bb808d7cba3a503f12f"
integrity sha512-K92JX8alrs0pTox5U2arVBqB8tJmak9dh9i4Xausy94TnnGMdLfTn7P2Dp/NOzlmxvEs7lDzeryo8YqOy0BHRQ==
standard-as-callback@^2.1.0: standard-as-callback@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
@ -20608,7 +20772,7 @@ swagger-parser@10.0.2:
dependencies: dependencies:
"@apidevtools/swagger-parser" "10.0.2" "@apidevtools/swagger-parser" "10.0.2"
symbol-tree@^3.2.4: "symbol-tree@>= 3.1.0 < 4.0.0", symbol-tree@^3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
@ -20793,6 +20957,13 @@ text-hex@1.0.x:
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
text-segmentation@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
dependencies:
utrie "^1.0.2"
text-table@^0.2.0: text-table@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -20986,7 +21157,7 @@ touch@^3.1.0:
dependencies: dependencies:
nopt "~1.0.10" nopt "~1.0.10"
tough-cookie@4.1.3, "tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.0.0, tough-cookie@^4.1.2, tough-cookie@^4.1.4, tough-cookie@~2.5.0: tough-cookie@4.1.3, tough-cookie@^2.2.0, "tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.0.0, tough-cookie@^4.1.2, tough-cookie@^4.1.4, tough-cookie@~2.5.0:
version "4.1.3" version "4.1.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf"
integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==
@ -21598,6 +21769,13 @@ utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
utrie@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
dependencies:
base64-arraybuffer "^1.0.2"
uue@3.1.2: uue@3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2" resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2"
@ -21849,7 +22027,7 @@ webfinger@^0.4.2:
step "0.0.x" step "0.0.x"
xml2js "0.1.x" xml2js "0.1.x"
webidl-conversions@^3.0.0: webidl-conversions@^3.0.0, webidl-conversions@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
@ -21929,6 +22107,14 @@ whatwg-url@^14.0.0:
tr46 "^5.0.0" tr46 "^5.0.0"
webidl-conversions "^7.0.0" webidl-conversions "^7.0.0"
whatwg-url@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-2.0.1.tgz#5396b2043f020ee6f704d9c45ea8519e724de659"
integrity sha512-sX+FT4N6iR0ZiqGqyDEKklyfMGR99zvxZD+LQ8IGae5uVGswQ7DOeLPB5KgJY8FzkwSzwqOXLQeVQvtOTSQU9Q==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^5.0.0: whatwg-url@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
@ -22218,6 +22404,11 @@ xhr@^2.4.1:
parse-headers "^2.0.0" parse-headers "^2.0.0"
xtend "^4.0.0" xtend "^4.0.0"
"xml-name-validator@>= 2.0.1 < 3.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
integrity sha512-jRKe/iQYMyVJpzPH+3HL97Lgu5HrCfii+qSo+TfjKHtOnvbnvdVfMYrn9Q34YV81M2e5sviJlI6Ko9y+nByzvA==
xml-name-validator@^3.0.0: xml-name-validator@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
@ -22256,6 +22447,11 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmldom@^0.1.22:
version "0.1.31"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
xmlhttprequest-ssl@~2.0.0: xmlhttprequest-ssl@~2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"