diff --git a/lerna.json b/lerna.json index 030dde848c..b3a84c72d3 100644 --- a/lerna.json +++ b/lerna.json @@ -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": { diff --git a/packages/backend-core/src/cache/writethrough.ts b/packages/backend-core/src/cache/writethrough.ts index cd7409ca15..5a1d9f6a14 100644 --- a/packages/backend-core/src/cache/writethrough.ts +++ b/packages/backend-core/src/cache/writethrough.ts @@ -96,6 +96,24 @@ async function get(db: Database, id: string): Promise { return cacheItem.doc } +async function tryGet( + db: Database, + id: string +): Promise { + const cache = await getCache() + const cacheKey = makeCacheKey(db, id) + let cacheItem: CacheItem | null = await cache.get(cacheKey) + if (!cacheItem) { + const doc = await db.tryGet(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 { 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(id: string) { return get(this.db, id) } + async tryGet(id: string) { + return tryGet(this.db, id) + } + async remove(docOrId: any, rev?: any) { return remove(this.db, docOrId, rev) } diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index 3085b91ef1..28d389e6ba 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -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) diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 8e0c71ff18..e701f111aa 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -157,6 +157,33 @@ export async function doInTenant( 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( + tenantId: string, + task: () => T +): Promise { + 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( 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) } diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index 23598b951e..adee495e60 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -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 diff --git a/packages/backend-core/src/features/features.ts b/packages/backend-core/src/features/features.ts index 979f7f5aa7..7106777084 100644 --- a/packages/backend-core/src/features/features.ts +++ b/packages/backend-core/src/features/features.ts @@ -143,6 +143,7 @@ export class FlagSet { const personProperties: Record = { tenantId } const posthogFlags = await posthog.getAllFlags(userId, { personProperties, + onlyEvaluateLocally: true, }) for (const [name, value] of Object.entries(posthogFlags)) { diff --git a/packages/backend-core/src/features/tests/features.spec.ts b/packages/backend-core/src/features/tests/features.spec.ts index f918347eea..c01e01f095 100644 --- a/packages/backend-core/src/features/tests/features.spec.ts +++ b/packages/backend-core/src/features/tests/features.spec.ts @@ -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 environmentFlags?: string - posthogFlags?: PostHogFlags + posthogFlags?: Record expected?: Partial errorMessage?: string | RegExp } -interface PostHogFlags { - featureFlags?: Record - featureFlagPayloads?: Record +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): 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) { 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() + } }) }) }) diff --git a/packages/bbui/src/helpers.ts b/packages/bbui/src/helpers.ts index 330f381d53..10ccf4683d 100644 --- a/packages/bbui/src/helpers.ts +++ b/packages/bbui/src/helpers.ts @@ -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 "" } diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte index 6011e2753d..762a416cf6 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationSchemaLayout.svelte @@ -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") diff --git a/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte b/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte index a5ba264f60..c0cdba458e 100644 --- a/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte +++ b/packages/builder/src/components/automation/SetupPanel/ExecuteScriptV2.svelte @@ -27,6 +27,7 @@ allowHBS={false} updateOnChange={false} {context} + showComponent >
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 @@
-
{ 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 { diff --git a/packages/builder/src/components/design/ScreenDetailsModal.svelte b/packages/builder/src/components/design/ScreenDetailsModal.svelte index 3f8e08d031..410ddee8a9 100644 --- a/packages/builder/src/components/design/ScreenDetailsModal.svelte +++ b/packages/builder/src/components/design/ScreenDetailsModal.svelte @@ -80,5 +80,6 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + margin-top: 4px; } diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js index 1cf4b0211c..86014f152c 100644 --- a/packages/builder/src/components/design/settings/componentSettings.js +++ b/packages/builder/src/components/design/settings/componentSettings.js @@ -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, diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnDrawer.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnDrawer.svelte index 09734c2ca4..df7932c74d 100644 --- a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnDrawer.svelte @@ -145,7 +145,7 @@
- By default, all columns will automatically be shown. + The default column configuration will automatically be shown.
You can manually control which columns are included by adding them below. diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte index 5e27b591f8..fab905e8b7 100644 --- a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte @@ -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) { diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/TopLevelColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/TopLevelColumnEditor.svelte new file mode 100644 index 0000000000..69a80a85da --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/TopLevelColumnEditor.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/builder/src/constants/index.ts b/packages/builder/src/constants/index.ts index 3c3a6888ad..7068a1bb96 100644 --- a/packages/builder/src/constants/index.ts +++ b/packages/builder/src/constants/index.ts @@ -71,4 +71,5 @@ export const AutoScreenTypes = { BLANK: "blank", TABLE: "table", FORM: "form", + PDF: "pdf", } diff --git a/packages/builder/src/dataBinding.js b/packages/builder/src/dataBinding.js index 1011774ac5..ada1ee274d 100644 --- a/packages/builder/src/dataBinding.js +++ b/packages/builder/src/dataBinding.js @@ -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 diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte index 31479bc820..1da03377b5 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPanel.svelte @@ -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 => { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePanel.svelte index af693a872f..c1e68010ad 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePanel.svelte @@ -26,7 +26,9 @@
- These settings apply to all screens + + These settings apply to all screens. PDFs are always light theme. +
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte index 1654ff5e06..3dccd97701 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte @@ -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 } diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json index d809095dc0..c4ee0c9dd4 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json @@ -20,9 +20,11 @@ "name": "Data", "icon": "Data", "children": [ + "singlerowprovider", "dataprovider", "repeater", "gridblock", + "pdftable", "spreadsheet", "dynamicfilter", "daterangepicker" diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte index 9dd7aab640..be409eff09 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte @@ -1,10 +1,13 @@
@@ -14,10 +17,12 @@
- {#if $appStore.clientFeatures.devicePreview} - + {#if !isPDF} + {#if $appStore.clientFeatures.devicePreview} + + {/if} + {/if} -
diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte index edc502bbb4..12186a7055 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte @@ -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 @@ - + diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/images/pdf.svg b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/images/pdf.svg new file mode 100644 index 0000000000..16ac02e2d7 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/images/pdf.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte index bbd6fda256..416299e00d 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/index.svelte @@ -1,11 +1,14 @@ -
+
diff --git a/packages/client/src/components/app/SingleRowProvider.svelte b/packages/client/src/components/app/SingleRowProvider.svelte new file mode 100644 index 0000000000..b8a25a6005 --- /dev/null +++ b/packages/client/src/components/app/SingleRowProvider.svelte @@ -0,0 +1,46 @@ + + +
+ + + +
+ + diff --git a/packages/client/src/components/app/Text.svelte b/packages/client/src/components/app/Text.svelte index 17c12656af..c4937fec3d 100644 --- a/packages/client/src/components/app/Text.svelte +++ b/packages/client/src/components/app/Text.svelte @@ -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 = { "text-align": alignStyle, + "font-size": size || "14px", } if (colorStyle) { additions.color = colorStyle diff --git a/packages/client/src/components/app/container/GridContainer.svelte b/packages/client/src/components/app/container/GridContainer.svelte index 03598d53bd..66b7e14bac 100644 --- a/packages/client/src/components/app/container/GridContainer.svelte +++ b/packages/client/src/components/app/container/GridContainer.svelte @@ -135,12 +135,18 @@ use:styleable={$styles} data-cols={GridColumns} data-col-size={colSize} + data-required-rows={requiredRows} on:click={onClick} > {#if inBuilder} -
- {#each { length: GridColumns * rows } as _, idx} -
+
+ {#each { length: rows } as _} +
+ {/each} +
+
+ {#each { length: GridColumns } as _} +
{/each}
{/if} @@ -151,7 +157,8 @@ diff --git a/packages/client/src/components/app/pdf/PDFTable.svelte b/packages/client/src/components/app/pdf/PDFTable.svelte new file mode 100644 index 0000000000..e4d415fa01 --- /dev/null +++ b/packages/client/src/components/app/pdf/PDFTable.svelte @@ -0,0 +1,143 @@ + + +
+
+ {#if schema} + {#each Object.keys(schema) as col} +
{schema[col].displayName}
+ {/each} + {#each stringifiedRows as row} + {#each Object.keys(schema) as col} +
{row[col]}
+ {/each} + {/each} + {/if} +
+
+ + diff --git a/packages/client/src/components/app/pdf/index.ts b/packages/client/src/components/app/pdf/index.ts new file mode 100644 index 0000000000..ae100c894b --- /dev/null +++ b/packages/client/src/components/app/pdf/index.ts @@ -0,0 +1,2 @@ +export { default as pdf } from "./PDF.svelte" +export { default as pdftable } from "./PDFTable.svelte" diff --git a/packages/client/src/components/app/pdf/pdf.ts b/packages/client/src/components/app/pdf/pdf.ts new file mode 100644 index 0000000000..ed47dc9831 --- /dev/null +++ b/packages/client/src/components/app/pdf/pdf.ts @@ -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 = { + 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) + }) +} diff --git a/packages/client/src/components/preview/HoverIndicator.svelte b/packages/client/src/components/preview/HoverIndicator.svelte index 981e82bc2e..7424ccf86e 100644 --- a/packages/client/src/components/preview/HoverIndicator.svelte +++ b/packages/client/src/components/preview/HoverIndicator.svelte @@ -54,4 +54,4 @@ zIndex={selected ? 890 : 910} allowResizeAnchors /> -{/if}} +{/if} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 43c5d71d68..2685942921 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -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 diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index 1cc205b382..7340e6828c 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -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 + ) }) diff --git a/packages/client/src/utils/grid.ts b/packages/client/src/utils/grid.ts index 3412e734c6..08520ff781 100644 --- a/packages/client/src/utils/grid.ts +++ b/packages/client/src/utils/grid.ts @@ -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() diff --git a/packages/frontend-core/src/components/grid/cells/NumberCell.svelte b/packages/frontend-core/src/components/grid/cells/NumberCell.svelte index c8ae96ef21..5ac6e14b6e 100644 --- a/packages/frontend-core/src/components/grid/cells/NumberCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/NumberCell.svelte @@ -1,8 +1,5 @@ - - { + 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 +} diff --git a/packages/frontend-core/src/utils/index.ts b/packages/frontend-core/src/utils/index.ts index e1f66d348d..314353bfc8 100644 --- a/packages/frontend-core/src/utils/index.ts +++ b/packages/frontend-core/src/utils/index.ts @@ -15,3 +15,4 @@ export * from "./relatedColumns" export * from "./table" export * from "./components" export * from "./validation" +export * from "./formatting" diff --git a/packages/frontend-core/src/utils/schema.js b/packages/frontend-core/src/utils/schema.js index cd55d4983d..135dbd3e35 100644 --- a/packages/frontend-core/src/utils/schema.js +++ b/packages/frontend-core/src/utils/schema.js @@ -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 } +} diff --git a/packages/pro b/packages/pro index 4417bceb24..fc510aa4b7 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 4417bceb24eabdd9a8c1615fb83c4e6fe8c0c914 +Subproject commit fc510aa4b7cbda72bb40a1f3250571dc213b5ef5 diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index 8f4629a5b0..1d0271a726 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -144,9 +144,11 @@ export async function find(ctx: UserCtx) { 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 { diff --git a/packages/server/src/api/routes/tests/ai.spec.ts b/packages/server/src/api/routes/tests/ai.spec.ts index 288ab888fd..ad2ae7dc50 100644 --- a/packages/server/src/api/routes/tests/ai.spec.ts +++ b/packages/server/src/api/routes/tests/ai.spec.ts @@ -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): 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 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 + 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, + } + ) + }) + }) +}) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index bca7d16807..261cd097d9 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -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( { diff --git a/packages/server/src/constants/screens.ts b/packages/server/src/constants/screens.ts index 41c1e74874..3a7413633d 100644 --- a/packages/server/src/constants/screens.ts +++ b/packages/server/src/constants/screens.ts @@ -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: {}, diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index b3edc92e17..7f67e95e66 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -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 diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index 4c32e45a8c..e508c4e3c0 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -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, diff --git a/packages/types/src/documents/app/screen.ts b/packages/types/src/documents/app/screen.ts index f1a119083c..29bc5a97e3 100644 --- a/packages/types/src/documents/app/screen.ts +++ b/packages/types/src/documents/app/screen.ts @@ -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 { diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts index 86e15e4974..e3f9d3af3f 100644 --- a/packages/types/src/documents/app/table/schema.ts +++ b/packages/types/src/documents/app/table/schema.ts @@ -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 { diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts index 6ed2456ea6..b39d491fb7 100644 --- a/packages/types/src/sdk/licensing/feature.ts +++ b/packages/types/src/sdk/licensing/feature.ts @@ -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", diff --git a/packages/worker/src/api/controllers/global/configs.ts b/packages/worker/src/api/controllers/global/configs.ts index 2433e0a82e..9255b8edb1 100644 --- a/packages/worker/src/api/controllers/global/configs.ts +++ b/packages/worker/src/api/controllers/global/configs.ts @@ -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) { diff --git a/yarn.lock b/yarn.lock index 5d7fc76bf1..81eedc9591 100644 --- a/yarn.lock +++ b/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"