Merge branch 'master' of github.com:Budibase/budibase
This commit is contained in:
commit
71e023bdc3
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.8.1",
|
||||
"version": "3.8.5",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -96,6 +96,24 @@ async function get<T extends Document>(db: Database, id: string): Promise<T> {
|
|||
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> {
|
||||
const cache = await getCache()
|
||||
if (!docOrId) {
|
||||
|
@ -123,10 +141,17 @@ export class Writethrough {
|
|||
return put(this.db, doc, writeRateMs)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `tryGet` instead
|
||||
*/
|
||||
async get<T extends Document>(id: string) {
|
||||
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) {
|
||||
return remove(this.db, docOrId, rev)
|
||||
}
|
||||
|
|
|
@ -60,6 +60,11 @@ export const StaticDatabases = {
|
|||
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)
|
||||
|
|
|
@ -157,6 +157,33 @@ export async function doInTenant<T>(
|
|||
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>(
|
||||
appId: string,
|
||||
task: () => T
|
||||
|
@ -325,6 +352,11 @@ export function getGlobalDB(): Database {
|
|||
if (!context || (env.MULTI_TENANCY && !context.tenantId)) {
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -344,6 +376,11 @@ export function getAppDB(opts?: any): Database {
|
|||
if (!appId) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { GoogleSpreadsheet } from "google-spreadsheet"
|
|||
// keep this out of Budibase types, don't want to expose context info
|
||||
export type ContextMap = {
|
||||
tenantId?: string
|
||||
isSelfHostUsingCloud?: boolean
|
||||
appId?: string
|
||||
identity?: IdentityContext
|
||||
environmentVariables?: Record<string, string>
|
||||
|
|
|
@ -143,6 +143,7 @@ export class FlagSet<T extends { [name: string]: boolean }> {
|
|||
const personProperties: Record<string, string> = { tenantId }
|
||||
const posthogFlags = await posthog.getAllFlags(userId, {
|
||||
personProperties,
|
||||
onlyEvaluateLocally: true,
|
||||
})
|
||||
|
||||
for (const [name, value] of Object.entries(posthogFlags)) {
|
||||
|
|
|
@ -4,7 +4,6 @@ import * as context from "../../context"
|
|||
import environment, { withEnv } from "../../environment"
|
||||
import nodeFetch from "node-fetch"
|
||||
import nock from "nock"
|
||||
import * as crypto from "crypto"
|
||||
|
||||
const schema = {
|
||||
TEST_BOOLEAN: false,
|
||||
|
@ -16,26 +15,74 @@ interface TestCase {
|
|||
it: string
|
||||
identity?: Partial<IdentityContext>
|
||||
environmentFlags?: string
|
||||
posthogFlags?: PostHogFlags
|
||||
posthogFlags?: Record<string, boolean>
|
||||
expected?: Partial<typeof schema>
|
||||
errorMessage?: string | RegExp
|
||||
}
|
||||
|
||||
interface PostHogFlags {
|
||||
featureFlags?: Record<string, boolean>
|
||||
featureFlagPayloads?: Record<string, string>
|
||||
interface Property {
|
||||
key: string
|
||||
value: string
|
||||
operator: string
|
||||
type: string
|
||||
}
|
||||
|
||||
function mockPosthogFlags(
|
||||
flags: PostHogFlags,
|
||||
opts?: { token?: string; distinct_id?: string }
|
||||
) {
|
||||
const { token = "test", distinct_id = "us_1234" } = opts || {}
|
||||
interface Group {
|
||||
properties: Property[]
|
||||
rollout_percentage: number
|
||||
variant: string | null
|
||||
}
|
||||
|
||||
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")
|
||||
.post("/decide/?v=3", body => {
|
||||
return body.token === token && body.distinct_id === distinct_id
|
||||
})
|
||||
.reply(200, flags)
|
||||
.get("/api/feature_flag/local_evaluation?token=test&send_cohorts")
|
||||
.reply(200, posthogFlags(flags))
|
||||
.persist()
|
||||
}
|
||||
|
||||
|
@ -76,33 +123,27 @@ describe("feature flags", () => {
|
|||
},
|
||||
{
|
||||
it: "should be able to read boolean flags from PostHog",
|
||||
posthogFlags: {
|
||||
featureFlags: { TEST_BOOLEAN: true },
|
||||
},
|
||||
posthogFlags: { TEST_BOOLEAN: true },
|
||||
expected: { TEST_BOOLEAN: true },
|
||||
},
|
||||
{
|
||||
it: "should not be able to override a negative environment flag from PostHog",
|
||||
environmentFlags: "default:!TEST_BOOLEAN",
|
||||
posthogFlags: {
|
||||
featureFlags: { TEST_BOOLEAN: true },
|
||||
},
|
||||
posthogFlags: { TEST_BOOLEAN: true },
|
||||
expected: { TEST_BOOLEAN: false },
|
||||
},
|
||||
{
|
||||
it: "should not be able to override a positive environment flag from PostHog",
|
||||
environmentFlags: "default:TEST_BOOLEAN",
|
||||
posthogFlags: {
|
||||
featureFlags: {
|
||||
TEST_BOOLEAN: false,
|
||||
},
|
||||
TEST_BOOLEAN: false,
|
||||
},
|
||||
expected: { TEST_BOOLEAN: true },
|
||||
},
|
||||
{
|
||||
it: "should not error on unrecognised PostHog flag",
|
||||
posthogFlags: {
|
||||
featureFlags: { UNDEFINED: true },
|
||||
UNDEFINED: true,
|
||||
},
|
||||
expected: flags.defaults(),
|
||||
},
|
||||
|
@ -136,6 +177,8 @@ describe("feature flags", () => {
|
|||
// We need to pass in node-fetch here otherwise nock won't get used
|
||||
// because posthog-node uses axios under the hood.
|
||||
init({
|
||||
// Required for local evaluation rule polling to start
|
||||
personalApiKey: "test",
|
||||
fetch: (url, opts) => {
|
||||
return nodeFetch(url, opts)
|
||||
},
|
||||
|
@ -151,23 +194,25 @@ describe("feature flags", () => {
|
|||
...identity,
|
||||
}
|
||||
|
||||
await context.doInIdentityContext(fullIdentity, async () => {
|
||||
if (errorMessage) {
|
||||
await expect(flags.fetch()).rejects.toThrow(errorMessage)
|
||||
} else if (expected) {
|
||||
const values = await flags.fetch()
|
||||
expect(values).toMatchObject(expected)
|
||||
try {
|
||||
await context.doInIdentityContext(fullIdentity, async () => {
|
||||
if (errorMessage) {
|
||||
await expect(flags.fetch()).rejects.toThrow(errorMessage)
|
||||
} else if (expected) {
|
||||
const values = await flags.fetch()
|
||||
expect(values).toMatchObject(expected)
|
||||
|
||||
for (const [key, expectedValue] of Object.entries(expected)) {
|
||||
const value = await flags.isEnabled(key as keyof typeof schema)
|
||||
expect(value).toBe(expectedValue)
|
||||
for (const [key, expectedValue] of Object.entries(expected)) {
|
||||
const value = await flags.isEnabled(key as keyof typeof schema)
|
||||
expect(value).toBe(expectedValue)
|
||||
}
|
||||
} else {
|
||||
throw new Error("No expected value")
|
||||
}
|
||||
} else {
|
||||
throw new Error("No expected value")
|
||||
}
|
||||
})
|
||||
|
||||
shutdown()
|
||||
})
|
||||
} finally {
|
||||
shutdown()
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -185,26 +230,30 @@ describe("feature flags", () => {
|
|||
// We need to pass in node-fetch here otherwise nock won't get used
|
||||
// because posthog-node uses axios under the hood.
|
||||
init({
|
||||
// Required for local evaluation rule polling to start
|
||||
personalApiKey: "test",
|
||||
fetch: (url, opts) => {
|
||||
return nodeFetch(url, opts)
|
||||
},
|
||||
})
|
||||
|
||||
nock("https://us.i.posthog.com")
|
||||
.post("/decide/?v=3", body => {
|
||||
return body.token === "test" && body.distinct_id === "us_1234"
|
||||
})
|
||||
.get("/api/feature_flag/local_evaluation?token=test&send_cohorts")
|
||||
.reply(503)
|
||||
.persist()
|
||||
|
||||
await withEnv(
|
||||
{ POSTHOG_TOKEN: "test", POSTHOG_API_HOST: "https://us.i.posthog.com" },
|
||||
async () => {
|
||||
await context.doInIdentityContext(identity, async () => {
|
||||
await flags.fetch()
|
||||
})
|
||||
}
|
||||
)
|
||||
try {
|
||||
await withEnv(
|
||||
{ POSTHOG_TOKEN: "test", POSTHOG_API_HOST: "https://us.i.posthog.com" },
|
||||
async () => {
|
||||
await context.doInIdentityContext(identity, async () => {
|
||||
await flags.fetch()
|
||||
})
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
shutdown()
|
||||
}
|
||||
})
|
||||
|
||||
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 hashedIp = crypto.createHash("sha512").update(ip).digest("hex")
|
||||
|
||||
await withEnv(env, async () => {
|
||||
mockPosthogFlags(
|
||||
{
|
||||
featureFlags: { TEST_BOOLEAN: true },
|
||||
},
|
||||
{
|
||||
distinct_id: hashedIp,
|
||||
}
|
||||
)
|
||||
mockPosthogFlags({ TEST_BOOLEAN: true })
|
||||
|
||||
// We need to pass in node-fetch here otherwise nock won't get used
|
||||
// because posthog-node uses axios under the hood.
|
||||
init({
|
||||
// Required for local evaluation rule polling to start
|
||||
personalApiKey: "test",
|
||||
fetch: (url, opts) => {
|
||||
return nodeFetch(url, opts)
|
||||
},
|
||||
})
|
||||
|
||||
await context.doInIPContext(ip, async () => {
|
||||
await context.doInTenant("default", async () => {
|
||||
const result = await flags.fetch()
|
||||
expect(result.TEST_BOOLEAN).toBe(true)
|
||||
try {
|
||||
await context.doInIPContext(ip, async () => {
|
||||
await context.doInTenant("default", async () => {
|
||||
const result = await flags.fetch()
|
||||
expect(result.TEST_BOOLEAN).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
shutdown()
|
||||
} finally {
|
||||
shutdown()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -211,9 +211,12 @@ const localeDateFormat = new Intl.DateTimeFormat()
|
|||
|
||||
// Formats a dayjs date according to schema flags
|
||||
export const getDateDisplayValue = (
|
||||
value: dayjs.Dayjs | null,
|
||||
value: dayjs.Dayjs | string | null,
|
||||
{ enableTime = true, timeOnly = false } = {}
|
||||
): string => {
|
||||
if (typeof value === "string") {
|
||||
value = dayjs(value)
|
||||
}
|
||||
if (!value?.isValid()) {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -184,6 +184,8 @@
|
|||
},
|
||||
[SchemaFieldTypes.QUERY_PARAMS]: {
|
||||
comp: QueryParamSelector,
|
||||
fullWidth: true,
|
||||
title: "Query*",
|
||||
},
|
||||
[SchemaFieldTypes.CODE]: {
|
||||
comp: ExecuteScript,
|
||||
|
@ -281,7 +283,9 @@
|
|||
}
|
||||
const type = getFieldType(field, block)
|
||||
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 meta = getInputValue(inputData, "meta")
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
allowHBS={false}
|
||||
updateOnChange={false}
|
||||
{context}
|
||||
showComponent
|
||||
>
|
||||
<div class="field-wrap code-editor">
|
||||
<CodeEditorField
|
||||
|
@ -57,8 +58,8 @@
|
|||
|
||||
.scriptv2-wrapper :global(.icon.slot-icon),
|
||||
.scriptv2-wrapper :global(.text-area-slot-icon) {
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
right: 1px !important;
|
||||
top: 1px !important;
|
||||
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border-bottom-left-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border-right: 0px;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { queries } from "@/stores/builder"
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import PropField from "./PropField.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -28,7 +29,6 @@
|
|||
</script>
|
||||
|
||||
<div class="schema-field">
|
||||
<Label>Query</Label>
|
||||
<div class="field-width">
|
||||
<Select
|
||||
on:change={onChangeQuery}
|
||||
|
@ -42,26 +42,23 @@
|
|||
|
||||
{#if parameters.length}
|
||||
{#each parameters as field}
|
||||
<div class="schema-field">
|
||||
<Label>{field.name}</Label>
|
||||
<div class="field-width">
|
||||
<DrawerBindableInput
|
||||
panel={AutomationBindingPanel}
|
||||
extraThin
|
||||
value={value[field.name]}
|
||||
on:change={e => onChange(e, field)}
|
||||
type="string"
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PropField label={field.name} fullWidth>
|
||||
<DrawerBindableInput
|
||||
panel={AutomationBindingPanel}
|
||||
extraThin
|
||||
value={value[field.name]}
|
||||
on:change={e => onChange(e, field)}
|
||||
type="string"
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
</PropField>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.field-width {
|
||||
width: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.schema-field {
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
import {
|
||||
DrawerBindableSlot,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
} from "@/components/common/bindings"
|
||||
import { FIELDS } from "@/constants/backend"
|
||||
import { capitalise } from "@/helpers"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
|
@ -234,6 +238,17 @@
|
|||
)
|
||||
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>
|
||||
|
||||
{#each schemaFields || [] as [field, schema]}
|
||||
|
@ -243,20 +258,55 @@
|
|||
fullWidth={fullWidth || isFullWidth(schema.type)}
|
||||
{componentWidth}
|
||||
>
|
||||
<div class="prop-control-wrap">
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
{#if customDrawer.includes(schema.type) || isTestModal}
|
||||
<div class="prop-control-wrap">
|
||||
<RowSelectorTypes
|
||||
{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}
|
||||
bindings={parsedBindings}
|
||||
value={editableRow}
|
||||
meta={{
|
||||
fields: editableFields,
|
||||
}}
|
||||
{onChange}
|
||||
value={drawerValue(editableRow[field])}
|
||||
on:change={e =>
|
||||
onChange({
|
||||
row: {
|
||||
[field]: e.detail,
|
||||
},
|
||||
})}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
{context}
|
||||
/>
|
||||
</div>
|
||||
>
|
||||
<div class="prop-control-wrap">
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
{schema}
|
||||
bindings={parsedBindings}
|
||||
value={editableRow}
|
||||
meta={{
|
||||
fields: editableFields,
|
||||
}}
|
||||
{onChange}
|
||||
{context}
|
||||
/>
|
||||
</div>
|
||||
</DrawerBindableSlot>
|
||||
{/if}
|
||||
</PropField>
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
Multiselect,
|
||||
Button,
|
||||
} 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 { getContext } from "svelte"
|
||||
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||
|
@ -94,10 +100,15 @@
|
|||
if (fieldSchema.calculationType) {
|
||||
return false
|
||||
}
|
||||
// static numeric formulas will work
|
||||
if (isNumericStaticFormula(fieldSchema)) {
|
||||
return true
|
||||
}
|
||||
// Only allow numeric columns for most calculation types
|
||||
if (
|
||||
self.type !== CalculationType.COUNT &&
|
||||
!isNumeric(fieldSchema.type)
|
||||
!isNumeric(fieldSchema.type) &&
|
||||
fieldSchema.responseType !== FieldType.NUMBER
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let bindings = []
|
||||
export let context = {}
|
||||
export let height = 180
|
||||
export let dropdown
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
@ -32,6 +33,7 @@
|
|||
value = e.detail
|
||||
dispatch("change", value)
|
||||
}}
|
||||
showComponent
|
||||
>
|
||||
<div class="code-editor-wrapper">
|
||||
<CodeEditorField
|
||||
|
@ -39,6 +41,7 @@
|
|||
{bindings}
|
||||
{context}
|
||||
{height}
|
||||
{dropdown}
|
||||
allowHBS={false}
|
||||
allowJS
|
||||
placeholder={"Add bindings by typing $"}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
export let updateOnChange = true
|
||||
export let type = undefined
|
||||
export let schema = undefined
|
||||
export let showComponent = false
|
||||
|
||||
export let allowHBS = true
|
||||
export let context = {}
|
||||
|
@ -150,7 +151,7 @@
|
|||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="control" class:disabled>
|
||||
{#if !isValid(value) && !$$slots.default}
|
||||
{#if !isValid(value) && !showComponent}
|
||||
<Input
|
||||
{label}
|
||||
{disabled}
|
||||
|
@ -162,7 +163,7 @@
|
|||
{updateOnChange}
|
||||
/>
|
||||
<div
|
||||
class="icon"
|
||||
class="icon close"
|
||||
on:click={() => {
|
||||
if (!isJS) {
|
||||
dispatch("change", "")
|
||||
|
@ -212,22 +213,27 @@
|
|||
}
|
||||
|
||||
.slot-icon {
|
||||
right: 31px;
|
||||
right: 31px !important;
|
||||
border-right: 1px solid var(--spectrum-alias-border-color);
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-top-right-radius: 0px !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
}
|
||||
|
||||
.text-area-slot-icon {
|
||||
border-bottom: 1px solid var(--spectrum-alias-border-color);
|
||||
border-bottom-right-radius: 0px;
|
||||
top: 1px;
|
||||
.icon.close {
|
||||
right: 1px !important;
|
||||
border-right: none;
|
||||
border-top-right-radius: 4px !important;
|
||||
border-bottom-right-radius: 4px !important;
|
||||
}
|
||||
|
||||
.text-area-slot-icon,
|
||||
.json-slot-icon {
|
||||
right: 1px !important;
|
||||
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;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
|
|
@ -80,5 +80,6 @@
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -22,6 +22,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt
|
|||
import DrawerBindableInput from "@/components/common/bindings/DrawerBindableInput.svelte"
|
||||
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
||||
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
||||
import TopLevelColumnEditor from "./controls/ColumnEditor/TopLevelColumnEditor.svelte"
|
||||
import GridColumnEditor from "./controls/GridColumnConfiguration/GridColumnConfiguration.svelte"
|
||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||
|
@ -62,7 +63,10 @@ const componentMap = {
|
|||
stepConfiguration: FormStepConfiguration,
|
||||
formStepControls: FormStepControls,
|
||||
columns: ColumnEditor,
|
||||
// "Basic" actually includes nested JSON and relationship fields
|
||||
"columns/basic": BasicColumnEditor,
|
||||
// "Top level" is only the top level schema fields
|
||||
"columns/toplevel": TopLevelColumnEditor,
|
||||
"columns/grid": GridColumnEditor,
|
||||
tableConditions: TableConditionEditor,
|
||||
"field/sortable": SortableFieldSelect,
|
||||
|
|
|
@ -145,7 +145,7 @@
|
|||
<div class="column">
|
||||
<div class="wide">
|
||||
<Body size="S">
|
||||
By default, all columns will automatically be shown.
|
||||
The default column configuration will automatically be shown.
|
||||
<br />
|
||||
You can manually control which columns are included by adding them
|
||||
below.
|
||||
|
|
|
@ -10,10 +10,18 @@
|
|||
} from "@/dataBinding"
|
||||
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 allowCellEditing = true
|
||||
export let allowReorder = true
|
||||
export let getSchemaFields = getSearchableFields
|
||||
export let placeholder = "All columns"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -28,13 +36,7 @@
|
|||
: enrichedSchemaFields?.map(field => field.name)
|
||||
$: sanitisedValue = getValidColumns(value, options)
|
||||
$: updateBoundValue(sanitisedValue)
|
||||
$: enrichedSchemaFields = search.getFields(
|
||||
$tables.list,
|
||||
Object.values(schema || {}),
|
||||
{
|
||||
allowLinks: true,
|
||||
}
|
||||
)
|
||||
$: enrichedSchemaFields = getSchemaFields(schema, $tables.list)
|
||||
|
||||
$: {
|
||||
value = (value || []).filter(
|
||||
|
@ -44,7 +46,7 @@
|
|||
|
||||
const getText = value => {
|
||||
if (!value?.length) {
|
||||
return "All columns"
|
||||
return placeholder
|
||||
}
|
||||
let text = `${value.length} column`
|
||||
if (value.length !== 1) {
|
||||
|
|
|
@ -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}
|
||||
/>
|
|
@ -71,4 +71,5 @@ export const AutoScreenTypes = {
|
|||
BLANK: "blank",
|
||||
TABLE: "table",
|
||||
FORM: "form",
|
||||
PDF: "pdf",
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
getJsHelperList,
|
||||
} from "@budibase/string-templates"
|
||||
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 { environment, licensing } from "@/stores/portal"
|
||||
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
|
||||
if (schema) {
|
||||
let jsonAdditions = {}
|
||||
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 }
|
||||
schema = SchemaUtils.addNestedJSONSchemaFields(schema)
|
||||
}
|
||||
|
||||
// Determine if we should add ID and rev to the schema
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import { getBindableProperties } from "@/dataBinding"
|
||||
import BarButtonList from "@/components/design/settings/controls/BarButtonList.svelte"
|
||||
import URLVariableTestInput from "@/components/design/settings/controls/URLVariableTestInput.svelte"
|
||||
import { DrawerBindableInput } from "@/components/common/bindings"
|
||||
|
||||
$: bindings = getBindableProperties($selectedScreen, null)
|
||||
$: screenSettings = getScreenSettings($selectedScreen)
|
||||
|
@ -23,7 +24,59 @@
|
|||
let errors = {}
|
||||
|
||||
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",
|
||||
control: Checkbox,
|
||||
|
@ -66,34 +119,7 @@
|
|||
label: "On screen load",
|
||||
control: ButtonActionEditor,
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...screenComponentSettings,
|
||||
{
|
||||
key: "urlTest",
|
||||
control: URLVariableTestInput,
|
||||
|
@ -102,8 +128,6 @@
|
|||
},
|
||||
},
|
||||
]
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
const routeTaken = url => {
|
||||
|
|
|
@ -26,7 +26,9 @@
|
|||
|
||||
<div class="info">
|
||||
<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>
|
||||
<Layout noPadding gap="S">
|
||||
<Layout noPadding gap="XS">
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
// Get initial set of allowed components
|
||||
let allowedComponents = []
|
||||
const definition = componentStore.getDefinition(component?._component)
|
||||
if (definition.legalDirectChildren?.length) {
|
||||
if (definition?.legalDirectChildren?.length) {
|
||||
allowedComponents = definition.legalDirectChildren.map(x => {
|
||||
return `@budibase/standard-components/${x}`
|
||||
})
|
||||
|
@ -67,7 +67,7 @@
|
|||
}
|
||||
|
||||
// Build up list of illegal children from ancestors
|
||||
let illegalChildren = definition.illegalChildren || []
|
||||
let illegalChildren = definition?.illegalChildren || []
|
||||
path.forEach(ancestor => {
|
||||
// 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.
|
||||
|
@ -144,11 +144,6 @@
|
|||
}
|
||||
})
|
||||
|
||||
// Swap blocks and plugins
|
||||
let tmp = enrichedStructure[1]
|
||||
enrichedStructure[1] = enrichedStructure[0]
|
||||
enrichedStructure[0] = tmp
|
||||
|
||||
return enrichedStructure
|
||||
}
|
||||
|
||||
|
|
|
@ -20,9 +20,11 @@
|
|||
"name": "Data",
|
||||
"icon": "Data",
|
||||
"children": [
|
||||
"singlerowprovider",
|
||||
"dataprovider",
|
||||
"repeater",
|
||||
"gridblock",
|
||||
"pdftable",
|
||||
"spreadsheet",
|
||||
"dynamicfilter",
|
||||
"daterangepicker"
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<script>
|
||||
import DevicePreviewSelect from "./DevicePreviewSelect.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 ScreenErrorsButton from "./ScreenErrorsButton.svelte"
|
||||
import { Divider } from "@budibase/bbui"
|
||||
import { ScreenVariant } from "@budibase/types"
|
||||
|
||||
$: isPDF = $selectedScreen?.variant === ScreenVariant.PDF
|
||||
</script>
|
||||
|
||||
<div class="app-panel">
|
||||
|
@ -14,10 +17,12 @@
|
|||
<UndoRedoControl store={screenStore.history} />
|
||||
</div>
|
||||
<div class="header-right">
|
||||
{#if $appStore.clientFeatures.devicePreview}
|
||||
<DevicePreviewSelect />
|
||||
{#if !isPDF}
|
||||
{#if $appStore.clientFeatures.devicePreview}
|
||||
<DevicePreviewSelect />
|
||||
{/if}
|
||||
<Divider vertical />
|
||||
{/if}
|
||||
<Divider vertical />
|
||||
<ScreenErrorsButton />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
// Otherwise choose a datasource
|
||||
datasourceModal.show()
|
||||
}
|
||||
} else if (mode === AutoScreenTypes.BLANK) {
|
||||
} else if (mode === AutoScreenTypes.BLANK || mode === AutoScreenTypes.PDF) {
|
||||
screenDetailsModal.show()
|
||||
} else {
|
||||
throw new Error("Invalid mode provided")
|
||||
|
@ -101,8 +101,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
const createBlankScreen = async ({ route }) => {
|
||||
const screenTemplates = screenTemplating.blank({ route, screens })
|
||||
const createBasicScreen = async ({ route }) => {
|
||||
const screenTemplates =
|
||||
mode === AutoScreenTypes.BLANK
|
||||
? screenTemplating.blank({ route, screens })
|
||||
: screenTemplating.pdf({ route, screens })
|
||||
const newScreens = await createScreens(screenTemplates)
|
||||
loadNewScreen(newScreens[0])
|
||||
}
|
||||
|
@ -243,7 +246,7 @@
|
|||
</Modal>
|
||||
|
||||
<Modal bind:this={screenDetailsModal}>
|
||||
<ScreenDetailsModal onConfirm={createBlankScreen} />
|
||||
<ScreenDetailsModal onConfirm={createBasicScreen} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={formTypeModal}>
|
||||
|
|
|
@ -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 |
|
@ -1,11 +1,14 @@
|
|||
<script>
|
||||
import { Body } from "@budibase/bbui"
|
||||
import { Body, Tag, Tags } from "@budibase/bbui"
|
||||
import CreationPage from "@/components/common/CreationPage.svelte"
|
||||
import blank from "./images/blank.svg"
|
||||
import table from "./images/tableInline.svg"
|
||||
import form from "./images/formUpdate.svg"
|
||||
import pdf from "./images/pdf.svg"
|
||||
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||
import { screenStore } from "@/stores/builder"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { AutoScreenTypes } from "@/constants"
|
||||
|
||||
export let onClose = null
|
||||
|
||||
|
@ -27,35 +30,67 @@
|
|||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
||||
<div
|
||||
class="card"
|
||||
on:click={() => createScreenModal.show(AutoScreenTypes.BLANK)}
|
||||
>
|
||||
<div class="image">
|
||||
<img alt="A blank screen" src={blank} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Blank</Body>
|
||||
<Body size="M">Blank</Body>
|
||||
<Body size="XS">Add an empty blank screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||
<div
|
||||
class="card"
|
||||
on:click={() => createScreenModal.show(AutoScreenTypes.TABLE)}
|
||||
>
|
||||
<div class="image">
|
||||
<img alt="A table of data" src={table} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Table</Body>
|
||||
<Body size="M">Table</Body>
|
||||
<Body size="XS">List rows in a table</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("form")}>
|
||||
<div
|
||||
class="card"
|
||||
on:click={() => createScreenModal.show(AutoScreenTypes.FORM)}
|
||||
>
|
||||
<div class="image">
|
||||
<img alt="A form containing data" src={form} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Form</Body>
|
||||
<Body size="M">Form</Body>
|
||||
<Body size="XS">Capture data from your users</Body>
|
||||
</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>
|
||||
</CreationPage>
|
||||
</div>
|
||||
|
@ -86,7 +121,7 @@
|
|||
transition: filter 150ms;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
.card:not(.disabled):hover {
|
||||
filter: brightness(1.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -96,29 +131,38 @@
|
|||
width: 100%;
|
||||
max-height: 127px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card .image {
|
||||
min-width: 235px;
|
||||
height: 127px;
|
||||
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 {
|
||||
border: 1px solid var(--grey-4);
|
||||
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:nth-child(1)) {
|
||||
margin-bottom: 6px;
|
||||
.text :global(p:first-child) {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text :global(p:nth-child(2)) {
|
||||
color: var(--grey-6);
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,22 +4,24 @@ import { Helpers } from "@budibase/bbui"
|
|||
import { RoleUtils, Utils } from "@budibase/frontend-core"
|
||||
import { findAllMatchingComponents } from "@/helpers/components"
|
||||
import {
|
||||
layoutStore,
|
||||
appStore,
|
||||
componentStore,
|
||||
layoutStore,
|
||||
navigationStore,
|
||||
previewStore,
|
||||
selectedComponent,
|
||||
} from "@/stores/builder"
|
||||
import { createHistoryStore, HistoryStore } from "@/stores/builder/history"
|
||||
import { API } from "@/api"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
import {
|
||||
FetchAppPackageResponse,
|
||||
DeleteScreenResponse,
|
||||
Screen,
|
||||
Component,
|
||||
SaveScreenResponse,
|
||||
ComponentDefinition,
|
||||
DeleteScreenResponse,
|
||||
FetchAppPackageResponse,
|
||||
SaveScreenResponse,
|
||||
Screen,
|
||||
ScreenVariant,
|
||||
} from "@budibase/types"
|
||||
|
||||
interface ScreenState {
|
||||
|
@ -115,6 +117,14 @@ export class ScreenStore extends BudiStore<ScreenState> {
|
|||
state.selectedScreenId = screen._id
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -39,6 +39,7 @@ interface LicensingState {
|
|||
customAppScriptsEnabled: boolean
|
||||
syncAutomationsEnabled: boolean
|
||||
triggerAutomationRunEnabled: boolean
|
||||
pdfEnabled: boolean
|
||||
// the currently used quotas from the db
|
||||
quotaUsage?: QuotaUsage
|
||||
// derived quota metrics for percentages used
|
||||
|
@ -81,6 +82,7 @@ class LicensingStore extends BudiStore<LicensingState> {
|
|||
customAppScriptsEnabled: false,
|
||||
syncAutomationsEnabled: false,
|
||||
triggerAutomationRunEnabled: false,
|
||||
pdfEnabled: false,
|
||||
// the currently used quotas from the db
|
||||
quotaUsage: undefined,
|
||||
// derived quota metrics for percentages used
|
||||
|
@ -187,6 +189,7 @@ class LicensingStore extends BudiStore<LicensingState> {
|
|||
const customAppScriptsEnabled = features.includes(
|
||||
Constants.Features.CUSTOM_APP_SCRIPTS
|
||||
)
|
||||
const pdfEnabled = features.includes(Constants.Features.PDF)
|
||||
this.update(state => {
|
||||
return {
|
||||
...state,
|
||||
|
@ -208,6 +211,7 @@ class LicensingStore extends BudiStore<LicensingState> {
|
|||
triggerAutomationRunEnabled,
|
||||
perAppBuildersEnabled,
|
||||
customAppScriptsEnabled,
|
||||
pdfEnabled,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { BaseStructure } from "../BaseStructure"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { ScreenVariant } from "@budibase/types"
|
||||
|
||||
export class Screen extends BaseStructure {
|
||||
constructor() {
|
||||
|
@ -81,3 +82,25 @@ export class Screen extends BaseStructure {
|
|||
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default as blank } from "./blank"
|
||||
export { default as form } from "./form"
|
||||
export { default as table } from "./table"
|
||||
export { default as pdf } from "./pdf"
|
||||
|
|
|
@ -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
|
|
@ -149,6 +149,7 @@ export const customTypeToSchema: Record<string, SchemaFieldTypes> = {
|
|||
SchemaFieldTypes.AUTOMATION_FIELDS,
|
||||
[AutomationCustomIOType.WEBHOOK_URL]: SchemaFieldTypes.WEBHOOK_URL,
|
||||
[AutomationCustomIOType.QUERY_LIMIT]: SchemaFieldTypes.QUERY_LIMIT,
|
||||
[AutomationCustomIOType.QUERY_PARAMS]: SchemaFieldTypes.QUERY_PARAMS,
|
||||
["fields"]: SchemaFieldTypes.FIELDS,
|
||||
}
|
||||
|
||||
|
|
|
@ -8017,6 +8017,32 @@
|
|||
"key": "text",
|
||||
"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",
|
||||
"label": "Alignment",
|
||||
|
@ -8058,5 +8084,133 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"apexcharts": "^3.48.0",
|
||||
"dayjs": "^1.10.8",
|
||||
"downloadjs": "1.4.7",
|
||||
"html2pdf.js": "^0.9.3",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"leaflet": "^1.7.1",
|
||||
"sanitize-html": "^2.13.0",
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<script>
|
||||
import { getContext, onDestroy } from "svelte"
|
||||
import { generate } from "shortid"
|
||||
import { builderStore } from "../stores/builder"
|
||||
import { builderStore } from "@/stores/builder"
|
||||
import Component from "@/components/Component.svelte"
|
||||
|
||||
export let type
|
||||
export let props
|
||||
export let styles
|
||||
export let context
|
||||
export let name
|
||||
export let props = undefined
|
||||
export let styles = undefined
|
||||
export let context = undefined
|
||||
export let name = undefined
|
||||
export let order = 0
|
||||
export let containsSlot = false
|
||||
|
||||
// ID is only exposed as a prop so that it can be bound to from parent
|
||||
// block components
|
||||
export let id
|
||||
export let id = undefined
|
||||
|
||||
const component = getContext("component")
|
||||
const block = getContext("block")
|
||||
|
|
|
@ -196,8 +196,6 @@
|
|||
}
|
||||
|
||||
// Metadata to pass into grid action to apply CSS
|
||||
const checkGrid = x =>
|
||||
x?._component?.endsWith("/container") && x?.layout === "grid"
|
||||
$: insideGrid = checkGrid(parent)
|
||||
$: isGrid = checkGrid(instance)
|
||||
$: 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(() => {
|
||||
// Register this component instance for external access
|
||||
if ($appStore.isDevApp) {
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
<script>
|
||||
import { themeStore } from "@/stores"
|
||||
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>
|
||||
|
||||
<div style={$themeStore.customThemeCss} id="theme-root">
|
||||
<div style={$themeStore.customThemeCss} {id}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -5,12 +5,13 @@
|
|||
export let text: any = ""
|
||||
export let color: string | undefined = undefined
|
||||
export let align: "left" | "center" | "right" | "justify" = "left"
|
||||
export let size: string | undefined = "14px"
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable } = getContext("sdk")
|
||||
|
||||
// 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
|
||||
$: safeText = stringify(text)
|
||||
|
@ -18,10 +19,12 @@
|
|||
const enrichStyles = (
|
||||
styles: any,
|
||||
colorStyle: typeof color,
|
||||
alignStyle: typeof align
|
||||
alignStyle: typeof align,
|
||||
size: string | undefined
|
||||
) => {
|
||||
let additions: Record<string, string> = {
|
||||
"text-align": alignStyle,
|
||||
"font-size": size || "14px",
|
||||
}
|
||||
if (colorStyle) {
|
||||
additions.color = colorStyle
|
||||
|
|
|
@ -135,12 +135,18 @@
|
|||
use:styleable={$styles}
|
||||
data-cols={GridColumns}
|
||||
data-col-size={colSize}
|
||||
data-required-rows={requiredRows}
|
||||
on:click={onClick}
|
||||
>
|
||||
{#if inBuilder}
|
||||
<div class="underlay">
|
||||
{#each { length: GridColumns * rows } as _, idx}
|
||||
<div class="placeholder" class:first-col={idx % GridColumns === 0} />
|
||||
<div class="underlay-h">
|
||||
{#each { length: rows } as _}
|
||||
<div class="placeholder-h" />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="underlay-v">
|
||||
{#each { length: GridColumns } as _}
|
||||
<div class="placeholder-v" />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -151,7 +157,8 @@
|
|||
|
||||
<style>
|
||||
.grid,
|
||||
.underlay {
|
||||
.underlay-h,
|
||||
.underlay-v {
|
||||
height: var(--height) !important;
|
||||
min-height: var(--min-height) !important;
|
||||
max-height: none !important;
|
||||
|
@ -161,37 +168,45 @@
|
|||
grid-template-columns: repeat(var(--cols), calc(var(--col-size) * 1px));
|
||||
position: relative;
|
||||
}
|
||||
.underlay {
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Underlay grid lines */
|
||||
.underlay-h,
|
||||
.underlay-v {
|
||||
z-index: 0;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-top: 1px solid var(--spectrum-global-color-gray-900);
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.underlay {
|
||||
z-index: 0;
|
||||
}
|
||||
.placeholder {
|
||||
.placeholder-h {
|
||||
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);
|
||||
grid-row: 1 / -1;
|
||||
}
|
||||
.placeholder.first-col {
|
||||
.placeholder-v:first-child {
|
||||
border-left: 1px solid var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Highlight grid lines when resizing children */
|
||||
:global(.grid.highlight > .underlay) {
|
||||
:global(.grid.highlight > .underlay-h),
|
||||
:global(.grid.highlight > .underlay-v) {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
/* Highlight sibling borders when resizing childern */
|
||||
/* Highlight sibling borders when resizing children */
|
||||
:global(.grid.highlight > .component:not(.dragging)) {
|
||||
outline: 2px solid var(--spectrum-global-color-static-blue-200);
|
||||
pointer-events: none !important;
|
||||
|
|
|
@ -36,10 +36,12 @@ export { default as sidepanel } from "./SidePanel.svelte"
|
|||
export { default as modal } from "./Modal.svelte"
|
||||
export { default as gridblock } from "./GridBlock.svelte"
|
||||
export { default as textv2 } from "./Text.svelte"
|
||||
export { default as singlerowprovider } from "./SingleRowProvider.svelte"
|
||||
export * from "./charts"
|
||||
export * from "./forms"
|
||||
export * from "./blocks"
|
||||
export * from "./dynamic-filter"
|
||||
export * from "./pdf"
|
||||
|
||||
// Deprecated component left for compatibility in old apps
|
||||
export * from "./deprecated/table"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
export { default as pdf } from "./PDF.svelte"
|
||||
export { default as pdftable } from "./PDFTable.svelte"
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -54,4 +54,4 @@
|
|||
zIndex={selected ? 890 : 910}
|
||||
allowResizeAnchors
|
||||
/>
|
||||
{/if}}
|
||||
{/if}
|
||||
|
|
|
@ -31,6 +31,8 @@ import {
|
|||
} from "@budibase/types"
|
||||
import { ActionTypes } from "@/constants"
|
||||
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
|
||||
import * as svelte from "svelte"
|
||||
|
@ -89,6 +91,8 @@ export interface SDK {
|
|||
notificationStore: typeof notificationStore
|
||||
environmentStore: typeof environmentStore
|
||||
appStore: typeof appStore
|
||||
Block: typeof Block
|
||||
BlockComponent: typeof BlockComponent
|
||||
}
|
||||
|
||||
let app: ClientApp
|
||||
|
|
|
@ -8,6 +8,7 @@ import { RoleUtils } from "@budibase/frontend-core"
|
|||
import { findComponentById, findComponentParent } from "../utils/components.js"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "@/constants"
|
||||
import { ScreenVariant } from "@budibase/types"
|
||||
|
||||
const createScreenStore = () => {
|
||||
const store = derived(
|
||||
|
@ -193,5 +194,8 @@ const createScreenStore = () => {
|
|||
export const screenStore = createScreenStore()
|
||||
|
||||
export const isGridScreen = derived(screenStore, $screenStore => {
|
||||
return $screenStore.activeScreen?.props?.layout === "grid"
|
||||
return (
|
||||
$screenStore.activeScreen?.props?.layout === "grid" ||
|
||||
$screenStore.activeScreen?.variant === ScreenVariant.PDF
|
||||
)
|
||||
})
|
||||
|
|
|
@ -116,6 +116,9 @@ export const gridLayout = (node: HTMLDivElement, metadata: GridMetadata) => {
|
|||
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
|
||||
selectComponent = (e: Event) => {
|
||||
e.stopPropagation()
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<script context="module">
|
||||
const NumberFormatter = Intl.NumberFormat()
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { formatNumber } from "@budibase/frontend-core"
|
||||
import TextCell from "./TextCell.svelte"
|
||||
|
||||
export let api
|
||||
|
@ -13,18 +10,6 @@
|
|||
const newValue = isNaN(float) ? null : float
|
||||
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>
|
||||
|
||||
<TextCell
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -15,3 +15,4 @@ export * from "./relatedColumns"
|
|||
export * from "./table"
|
||||
export * from "./components"
|
||||
export * from "./validation"
|
||||
export * from "./formatting"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { helpers } from "@budibase/shared-core"
|
||||
import { TypeIconMap } from "../constants"
|
||||
import { convertJSONSchemaToTableSchema } from "./json"
|
||||
|
||||
export const getColumnIcon = column => {
|
||||
// For some reason we have remix icons saved under this property sometimes,
|
||||
|
@ -24,3 +25,25 @@ export const getColumnIcon = column => {
|
|||
|
||||
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
|
|
@ -144,9 +144,11 @@ export async function find(ctx: UserCtx<void, FindRowResponse>) {
|
|||
const { tableId, viewId } = utils.getSourceId(ctx)
|
||||
const sourceId = viewId || tableId
|
||||
const rowId = ctx.params.rowId
|
||||
|
||||
const response = await sdk.rows.find(sourceId, rowId)
|
||||
ctx.body = response
|
||||
try {
|
||||
ctx.body = await sdk.rows.find(sourceId, rowId)
|
||||
} catch (e) {
|
||||
ctx.throw(404, "That row couldn't be found")
|
||||
}
|
||||
}
|
||||
|
||||
function isDeleteRows(input: any): input is DeleteRows {
|
||||
|
|
|
@ -29,7 +29,6 @@ interface TestSetup {
|
|||
name: string
|
||||
setup: SetupFn
|
||||
mockLLMResponse: MockLLMResponseFn
|
||||
selfHostOnly?: boolean
|
||||
}
|
||||
|
||||
function budibaseAI(): SetupFn {
|
||||
|
@ -80,7 +79,7 @@ function customAIConfig(providerConfig: Partial<ProviderConfig>): SetupFn {
|
|||
}
|
||||
}
|
||||
|
||||
const providers: TestSetup[] = [
|
||||
const allProviders: TestSetup[] = [
|
||||
{
|
||||
name: "OpenAI API key",
|
||||
setup: async () => {
|
||||
|
@ -89,7 +88,6 @@ const providers: TestSetup[] = [
|
|||
})
|
||||
},
|
||||
mockLLMResponse: mockChatGPTResponse,
|
||||
selfHostOnly: true,
|
||||
},
|
||||
{
|
||||
name: "OpenAI API key with custom config",
|
||||
|
@ -126,9 +124,9 @@ describe("AI", () => {
|
|||
nock.cleanAll()
|
||||
})
|
||||
|
||||
describe.each(providers)(
|
||||
describe.each(allProviders)(
|
||||
"provider: $name",
|
||||
({ setup, mockLLMResponse, selfHostOnly }: TestSetup) => {
|
||||
({ setup, mockLLMResponse }: TestSetup) => {
|
||||
let cleanup: () => Promise<void> | void
|
||||
beforeAll(async () => {
|
||||
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,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
ViewV2,
|
||||
ViewV2Schema,
|
||||
ViewV2Type,
|
||||
FormulaType,
|
||||
} from "@budibase/types"
|
||||
import { generator, mocks } from "@budibase/backend-core/tests"
|
||||
import { datasourceDescribe } from "../../../integrations/tests/utils"
|
||||
|
@ -3865,6 +3866,48 @@ if (descriptions.length) {
|
|||
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 () => {
|
||||
await config.api.viewV2.create(
|
||||
{
|
||||
|
|
|
@ -365,7 +365,11 @@ export function createSampleDataTableScreen(): Screen {
|
|||
_component: "@budibase/standard-components/textv2",
|
||||
_styles: {
|
||||
normal: {
|
||||
"--grid-desktop-col-start": 1,
|
||||
"--grid-desktop-col-end": 3,
|
||||
"--grid-desktop-row-start": 1,
|
||||
"--grid-desktop-row-end": 3,
|
||||
"--grid-mobile-col-end": 7,
|
||||
},
|
||||
hover: {},
|
||||
active: {},
|
||||
|
@ -384,6 +388,7 @@ export function createSampleDataTableScreen(): Screen {
|
|||
"--grid-desktop-row-start": 1,
|
||||
"--grid-desktop-row-end": 3,
|
||||
"--grid-desktop-h-align": "end",
|
||||
"--grid-mobile-col-start": 7,
|
||||
},
|
||||
hover: {},
|
||||
active: {},
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
canGroupBy,
|
||||
FieldType,
|
||||
isNumeric,
|
||||
isNumericStaticFormula,
|
||||
PermissionLevel,
|
||||
RelationSchemaField,
|
||||
RenameColumn,
|
||||
|
@ -176,7 +177,11 @@ async function guardCalculationViewSchema(
|
|||
}
|
||||
|
||||
const isCount = schema.calculationType === CalculationType.COUNT
|
||||
if (!isCount && !isNumeric(targetSchema.type)) {
|
||||
if (
|
||||
!isCount &&
|
||||
!isNumeric(targetSchema.type) &&
|
||||
!isNumericStaticFormula(targetSchema)
|
||||
) {
|
||||
throw new HTTPError(
|
||||
`Calculation field "${name}" references field "${schema.field}" which is not a numeric field`,
|
||||
400
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Document } from "../document"
|
||||
import { FieldSchema, FormulaType } from "./table"
|
||||
|
||||
export enum FieldType {
|
||||
/**
|
||||
|
@ -147,6 +148,15 @@ export function isNumeric(type: FieldType) {
|
|||
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 = [
|
||||
FieldType.STRING,
|
||||
FieldType.LONGFORM,
|
||||
|
|
|
@ -15,6 +15,10 @@ export interface ScreenRouting {
|
|||
homeScreen?: boolean
|
||||
}
|
||||
|
||||
export enum ScreenVariant {
|
||||
PDF = "pdf",
|
||||
}
|
||||
|
||||
export interface Screen extends Document {
|
||||
layoutId?: string
|
||||
showNavigation?: boolean
|
||||
|
@ -24,6 +28,7 @@ export interface Screen extends Document {
|
|||
name?: string
|
||||
pluginAdded?: boolean
|
||||
onLoad?: EventHandler[]
|
||||
variant?: ScreenVariant
|
||||
}
|
||||
|
||||
export interface ScreenRoutesViewOutput extends Document {
|
||||
|
|
|
@ -207,6 +207,8 @@ export interface BaseFieldSchema extends UIFieldMetadata {
|
|||
autocolumn?: boolean
|
||||
autoReason?: AutoReason.FOREIGN_KEY
|
||||
subtype?: never
|
||||
// added when enriching nested JSON fields into schema
|
||||
nestedJSON?: boolean
|
||||
}
|
||||
|
||||
interface OtherFieldMetadata extends BaseFieldSchema {
|
||||
|
|
|
@ -14,6 +14,7 @@ export enum Feature {
|
|||
OFFLINE = "offline",
|
||||
EXPANDED_PUBLIC_API = "expandedPublicApi",
|
||||
CUSTOM_APP_SCRIPTS = "customAppScripts",
|
||||
PDF = "pdf",
|
||||
// deprecated - no longer licensed
|
||||
VIEW_PERMISSIONS = "viewPermissions",
|
||||
VIEW_READONLY_COLUMNS = "viewReadonlyColumns",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as email from "../../../utilities/email"
|
||||
import env from "../../../environment"
|
||||
import { googleCallbackUrl, oidcCallbackUrl } from "./auth"
|
||||
import * as auth from "./auth"
|
||||
import {
|
||||
cache,
|
||||
configs,
|
||||
|
@ -420,20 +420,58 @@ export async function publicSettings(
|
|||
) {
|
||||
try {
|
||||
// settings
|
||||
const configDoc = await configs.getSettingsConfigDoc()
|
||||
const [configDoc, googleConfig] = await Promise.all([
|
||||
configs.getSettingsConfigDoc(),
|
||||
configs.getGoogleConfig(),
|
||||
])
|
||||
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
|
||||
if (config.logoUrl && config.logoUrl !== "") {
|
||||
config.logoUrl = await objectStore.getGlobalFileUrl(
|
||||
"settings",
|
||||
"logoUrl",
|
||||
config.logoUrlEtag
|
||||
)
|
||||
const getLogoUrl = () => {
|
||||
// enrich the logo url - empty url means deleted
|
||||
if (config.logoUrl && config.logoUrl !== "") {
|
||||
return objectStore.getGlobalFileUrl(
|
||||
"settings",
|
||||
"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
|
||||
const faviconUrl =
|
||||
branding.faviconUrl && branding.faviconUrl !== ""
|
||||
|
@ -444,21 +482,11 @@ export async function publicSettings(
|
|||
)
|
||||
: 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 _oidcCallbackUrl = await oidcCallbackUrl()
|
||||
|
||||
// sso enforced
|
||||
const isSSOEnforced = await pro.features.isSSOEnforced({ config })
|
||||
const googleDatasourceConfigured = !!googleDatasource
|
||||
if (logoUrl) {
|
||||
config.logoUrl = logoUrl
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
type: ConfigType.SETTINGS,
|
||||
|
@ -472,8 +500,8 @@ export async function publicSettings(
|
|||
googleDatasourceConfigured,
|
||||
oidc,
|
||||
isSSOEnforced,
|
||||
oidcCallbackUrl: _oidcCallbackUrl,
|
||||
googleCallbackUrl: _googleCallbackUrl,
|
||||
oidcCallbackUrl,
|
||||
googleCallbackUrl,
|
||||
},
|
||||
}
|
||||
} catch (err: any) {
|
||||
|
|
216
yarn.lock
216
yarn.lock
|
@ -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"
|
||||
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:
|
||||
version "2.0.6"
|
||||
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"
|
||||
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:
|
||||
version "6.0.0"
|
||||
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:
|
||||
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:
|
||||
version "5.7.4"
|
||||
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"
|
||||
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:
|
||||
version "1.1.1"
|
||||
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"
|
||||
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:
|
||||
version "1.5.1"
|
||||
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"
|
||||
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:
|
||||
version "0.12.0"
|
||||
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"
|
||||
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:
|
||||
version "4.5.0"
|
||||
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"
|
||||
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:
|
||||
version "2.3.1"
|
||||
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"
|
||||
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:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
|
||||
integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
|
||||
|
||||
cssom@~0.3.6:
|
||||
version "0.3.8"
|
||||
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
|
||||
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
|
||||
"cssstyle@>= 0.2.34 < 0.3.0":
|
||||
version "0.2.37"
|
||||
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54"
|
||||
integrity sha512-FUpKc+1FNBsHUr9IsfSGCovr8VuGOiiuzlgCyppKBjJi2jYTOFLN3oiiNRMIvYqbFzF38mqKj4BgcevzU5/kIA==
|
||||
dependencies:
|
||||
cssom "0.3.x"
|
||||
|
||||
cssstyle@^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"
|
||||
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:
|
||||
version "1.14.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
|
||||
|
@ -11382,7 +11455,7 @@ esrecurse@^4.3.0:
|
|||
dependencies:
|
||||
estraverse "^5.2.0"
|
||||
|
||||
estraverse@^4.1.1:
|
||||
estraverse@^4.1.1, estraverse@^4.2.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
|
||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||
|
@ -11772,6 +11845,11 @@ file-entry-cache@^8.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "11.1.0"
|
||||
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"
|
||||
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:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d"
|
||||
|
@ -13129,7 +13224,7 @@ ical-generator@4.1.0:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
|
@ -14618,6 +14713,29 @@ jsdom@^24.1.1:
|
|||
ws "^8.18.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:
|
||||
version "3.0.2"
|
||||
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"
|
||||
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:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
|
||||
|
@ -16783,6 +16912,11 @@ nunjucks@^3.2.3:
|
|||
asap "^2.0.3"
|
||||
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:
|
||||
version "2.2.12"
|
||||
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"
|
||||
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:
|
||||
version "1.0.10"
|
||||
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"
|
||||
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:
|
||||
version "7.1.2"
|
||||
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"
|
||||
integrity sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww==
|
||||
|
||||
request@^2.88.0:
|
||||
request@^2.55.0, request@^2.88.0:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
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"
|
||||
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:
|
||||
version "3.0.2"
|
||||
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"
|
||||
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:
|
||||
version "5.0.1"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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:
|
||||
"@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"
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
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"
|
||||
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:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
|
@ -20986,7 +21157,7 @@ touch@^3.1.0:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf"
|
||||
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"
|
||||
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:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2"
|
||||
|
@ -21849,7 +22027,7 @@ webfinger@^0.4.2:
|
|||
step "0.0.x"
|
||||
xml2js "0.1.x"
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
webidl-conversions@^3.0.0, webidl-conversions@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
@ -21929,6 +22107,14 @@ whatwg-url@^14.0.0:
|
|||
tr46 "^5.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:
|
||||
version "5.0.0"
|
||||
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"
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
|
||||
|
|
Loading…
Reference in New Issue