diff --git a/.eslintrc.json b/.eslintrc.json index d475bba8d1..9dab2f1a88 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -55,7 +55,9 @@ } ], "no-redeclare": "off", - "@typescript-eslint/no-redeclare": "error" + "@typescript-eslint/no-redeclare": "error", + // have to turn this off to allow function overloading in typescript + "no-dupe-class-members": "off" } }, { @@ -88,7 +90,9 @@ "jest/expect-expect": "off", // We do this in some tests where the behaviour of internal tables // differs to external, but the API is broadly the same - "jest/no-conditional-expect": "off" + "jest/no-conditional-expect": "off", + // have to turn this off to allow function overloading in typescript + "no-dupe-class-members": "off" } }, { diff --git a/.github/AUTHORS.md b/.github/AUTHORS.md index df346f3325..d31bb64987 100644 --- a/.github/AUTHORS.md +++ b/.github/AUTHORS.md @@ -8,4 +8,5 @@ Contributors * Andrew Kingston - [@aptkingston](https://github.com/aptkingston) * Michael Drury - [@mike12345567](https://github.com/mike12345567) * Peter Clement - [@PClmnt](https://github.com/PClmnt) -* Rory Powell - [@Rory-Powell](https://github.com/Rory-Powell) \ No newline at end of file +* Rory Powell - [@Rory-Powell](https://github.com/Rory-Powell) +* Michaƫl St-Georges [@CSLTech](https://github.com/CSLTech) \ No newline at end of file diff --git a/lerna.json b/lerna.json index 1e49def198..cba15492eb 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.26.2", + "version": "2.26.4", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index f297d3089f..6694320978 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -1,4 +1,6 @@ import { IdentityContext, Snippet, VM } from "@budibase/types" +import { OAuth2Client } from "google-auth-library" +import { GoogleSpreadsheet } from "google-spreadsheet" // keep this out of Budibase types, don't want to expose context info export type ContextMap = { @@ -12,4 +14,8 @@ export type ContextMap = { vm?: VM cleanup?: (() => void | Promise)[] snippets?: Snippet[] + googleSheets?: { + oauthClient: OAuth2Client + clients: Record + } } diff --git a/packages/backend-core/src/db/Replication.ts b/packages/backend-core/src/db/Replication.ts index 735c2fa86e..617269df10 100644 --- a/packages/backend-core/src/db/Replication.ts +++ b/packages/backend-core/src/db/Replication.ts @@ -1,14 +1,31 @@ import PouchDB from "pouchdb" import { getPouchDB, closePouchDB } from "./couch" -import { DocumentType } from "../constants" +import { DocumentType } from "@budibase/types" + +enum ReplicationDirection { + TO_PRODUCTION = "toProduction", + TO_DEV = "toDev", +} class Replication { source: PouchDB.Database target: PouchDB.Database + direction: ReplicationDirection | undefined constructor({ source, target }: { source: string; target: string }) { this.source = getPouchDB(source) this.target = getPouchDB(target) + if ( + source.startsWith(DocumentType.APP_DEV) && + target.startsWith(DocumentType.APP) + ) { + this.direction = ReplicationDirection.TO_PRODUCTION + } else if ( + source.startsWith(DocumentType.APP) && + target.startsWith(DocumentType.APP_DEV) + ) { + this.direction = ReplicationDirection.TO_DEV + } } async close() { @@ -40,12 +57,18 @@ class Replication { } const filter = opts.filter + const direction = this.direction + const toDev = direction === ReplicationDirection.TO_DEV delete opts.filter return { ...opts, filter: (doc: any, params: any) => { - if (doc._id && doc._id.startsWith(DocumentType.AUTOMATION_LOG)) { + // don't sync design documents + if (toDev && doc._id?.startsWith("_design")) { + return false + } + if (doc._id?.startsWith(DocumentType.AUTOMATION_LOG)) { return false } if (doc._id === DocumentType.APP_METADATA) { diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index ef351f7d4d..8194d1aabf 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -12,6 +12,7 @@ import { isDocument, RowResponse, RowValue, + SQLiteDefinition, SqlQueryBinding, } from "@budibase/types" import { getCouchInfo } from "./connections" @@ -21,6 +22,8 @@ import { ReadStream, WriteStream } from "fs" import { newid } from "../../docIds/newid" import { SQLITE_DESIGN_DOC_ID } from "../../constants" import { DDInstrumentedDatabase } from "../instrumentation" +import { checkSlashesInUrl } from "../../helpers" +import env from "../../environment" const DATABASE_NOT_FOUND = "Database does not exist." @@ -281,25 +284,61 @@ export class DatabaseImpl implements Database { }) } + async _sqlQuery( + url: string, + method: "POST" | "GET", + body?: Record + ): Promise { + url = checkSlashesInUrl(`${this.couchInfo.sqlUrl}/${url}`) + const args: { url: string; method: string; cookie: string; body?: any } = { + url, + method, + cookie: this.couchInfo.cookie, + } + if (body) { + args.body = body + } + return this.performCall(() => { + return async () => { + const response = await directCouchUrlCall(args) + const json = await response.json() + if (response.status > 300) { + throw json + } + return json as T + } + }) + } + async sql( sql: string, parameters?: SqlQueryBinding ): Promise { const dbName = this.name const url = `/${dbName}/${SQLITE_DESIGN_DOC_ID}` - const response = await directCouchUrlCall({ - url: `${this.couchInfo.sqlUrl}/${url}`, - method: "POST", - cookie: this.couchInfo.cookie, - body: { - query: sql, - args: parameters, - }, + return await this._sqlQuery(url, "POST", { + query: sql, + args: parameters, }) - if (response.status > 300) { - throw new Error(await response.text()) + } + + // checks design document is accurate (cleans up tables) + // this will check the design document and remove anything from + // disk which is not supposed to be there + async sqlDiskCleanup(): Promise { + const dbName = this.name + const url = `/${dbName}/_cleanup` + return await this._sqlQuery(url, "POST") + } + + // removes a document from sqlite + async sqlPurgeDocument(docIds: string[] | string): Promise { + if (!Array.isArray(docIds)) { + docIds = [docIds] } - return (await response.json()) as T[] + const dbName = this.name + const url = `/${dbName}/_purge` + return await this._sqlQuery(url, "POST", { docs: docIds }) } async query( @@ -314,6 +353,17 @@ export class DatabaseImpl implements Database { async destroy() { try { + if (env.SQS_SEARCH_ENABLE) { + // delete the design document, then run the cleanup operation + try { + const definition = await this.get( + SQLITE_DESIGN_DOC_ID + ) + await this.remove(SQLITE_DESIGN_DOC_ID, definition._rev) + } finally { + await this.sqlDiskCleanup() + } + } return await this.nano().db.destroy(this.name) } catch (err: any) { // didn't exist, don't worry diff --git a/packages/backend-core/src/db/couch/utils.ts b/packages/backend-core/src/db/couch/utils.ts index 005b02a896..270d953320 100644 --- a/packages/backend-core/src/db/couch/utils.ts +++ b/packages/backend-core/src/db/couch/utils.ts @@ -21,7 +21,7 @@ export async function directCouchUrlCall({ url: string cookie: string method: string - body?: any + body?: Record }) { const params: any = { method: method, diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts index 32ba81ebd8..4e2b147ef3 100644 --- a/packages/backend-core/src/db/instrumentation.ts +++ b/packages/backend-core/src/db/instrumentation.ts @@ -56,12 +56,17 @@ export class DDInstrumentedDatabase implements Database { }) } + remove(idOrDoc: Document): Promise + remove(idOrDoc: string, rev?: string): Promise remove( - id: string | Document, - rev?: string | undefined + idOrDoc: string | Document, + rev?: string ): Promise { return tracer.trace("db.remove", span => { - span?.addTags({ db_name: this.name, doc_id: id }) + span?.addTags({ db_name: this.name, doc_id: idOrDoc }) + const isDocument = typeof idOrDoc === "object" + const id = isDocument ? idOrDoc._id! : idOrDoc + rev = isDocument ? idOrDoc._rev : rev return this.db.remove(id, rev) }) } @@ -160,4 +165,18 @@ export class DDInstrumentedDatabase implements Database { return this.db.sql(sql, parameters) }) } + + sqlPurgeDocument(docIds: string[] | string): Promise { + return tracer.trace("db.sqlPurgeDocument", span => { + span?.addTags({ db_name: this.name }) + return this.db.sqlPurgeDocument(docIds) + }) + } + + sqlDiskCleanup(): Promise { + return tracer.trace("db.sqlDiskCleanup", span => { + span?.addTags({ db_name: this.name }) + return this.db.sqlDiskCleanup() + }) + } } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 9ade81b9d7..20ff16739f 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -109,6 +109,7 @@ const environment = { API_ENCRYPTION_KEY: getAPIEncryptionKey(), COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || "http://localhost:4006", + SQS_SEARCH_ENABLE: process.env.SQS_SEARCH_ENABLE, COUCH_DB_USERNAME: process.env.COUCH_DB_USER, COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index f77c6385ba..37547573bd 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -492,7 +492,7 @@ export class UserDB { await platform.users.removeUser(dbUser) - await db.remove(userId, dbUser._rev) + await db.remove(userId, dbUser._rev!) const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0 await UserDB.quotas.removeUsers(1, creatorsToDelete) diff --git a/packages/bbui/src/Actions/click_outside.js b/packages/bbui/src/Actions/click_outside.js index 124f43ff04..526659cb7a 100644 --- a/packages/bbui/src/Actions/click_outside.js +++ b/packages/bbui/src/Actions/click_outside.js @@ -71,8 +71,8 @@ const handleMouseDown = e => { // Clear any previous listeners in case of multiple down events, and register // a single mouse up listener - document.removeEventListener("mouseup", handleMouseUp) - document.addEventListener("mouseup", handleMouseUp, true) + document.removeEventListener("click", handleMouseUp) + document.addEventListener("click", handleMouseUp, true) } // Global singleton listeners for our events diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index 6c4fcab757..21635592d2 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -155,6 +155,8 @@ export default function positionDropdown(element, opts) { applyXStrategy(Strategies.StartToEnd) } else if (align === "left-outside") { applyXStrategy(Strategies.EndToStart) + } else if (align === "center") { + applyXStrategy(Strategies.MidPoint) } else { applyXStrategy(Strategies.StartToStart) } diff --git a/packages/bbui/src/Form/Core/Signature.svelte b/packages/bbui/src/Form/Core/Signature.svelte new file mode 100644 index 0000000000..729d3ac5e2 --- /dev/null +++ b/packages/bbui/src/Form/Core/Signature.svelte @@ -0,0 +1,267 @@ + + +
+ {#if !disabled} +
+ {#if updated && saveIcon} + + { + dispatch("change", toDataUrl()) + }} + /> + + {/if} + {#if signatureFile?.url && !updated} + + { + if (editable) { + clearCanvas() + } + dispatch("clear") + }} + /> + + {/if} +
+ {/if} + {#if !editable && signatureFile?.url} + + {#if !urlFailed} + { + urlFailed = true + }} + /> + {:else} + Could not load signature + {/if} + {:else} +
+ + {#if editable} +
+
+ +
+
+
+ {/if} +
+ {/if} +
+ + diff --git a/packages/bbui/src/Form/Core/index.js b/packages/bbui/src/Form/Core/index.js index 7117b90081..6395fe2fac 100644 --- a/packages/bbui/src/Form/Core/index.js +++ b/packages/bbui/src/Form/Core/index.js @@ -16,3 +16,4 @@ export { default as CoreStepper } from "./Stepper.svelte" export { default as CoreRichTextField } from "./RichTextField.svelte" export { default as CoreSlider } from "./Slider.svelte" export { default as CoreFile } from "./File.svelte" +export { default as CoreSignature } from "./Signature.svelte" diff --git a/packages/bbui/src/Modal/Modal.svelte b/packages/bbui/src/Modal/Modal.svelte index be9c338892..4656be69d1 100644 --- a/packages/bbui/src/Modal/Modal.svelte +++ b/packages/bbui/src/Modal/Modal.svelte @@ -173,6 +173,7 @@ } .spectrum-Modal { + border: 2px solid var(--spectrum-global-color-gray-200); overflow: visible; max-height: none; margin: 40px 0; diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte index 189ef70c2b..61ceaeb00a 100644 --- a/packages/bbui/src/Modal/ModalContent.svelte +++ b/packages/bbui/src/Modal/ModalContent.svelte @@ -27,6 +27,7 @@ export let secondaryButtonText = undefined export let secondaryAction = undefined export let secondaryButtonWarning = false + export let custom = false const { hide, cancel } = getContext(Context.Modal) let loading = false @@ -63,12 +64,13 @@ class:spectrum-Dialog--medium={size === "M"} class:spectrum-Dialog--large={size === "L"} class:spectrum-Dialog--extraLarge={size === "XL"} + class:no-grid={custom} style="position: relative;" role="dialog" tabindex="-1" aria-modal="true" > -
+
+
+ +
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte index ac1c4f91cb..7898e13ec8 100644 --- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte @@ -61,6 +61,7 @@ selected={automation._id === selectedAutomationId} on:click={() => selectAutomation(automation._id)} selectedBy={$userSelectedResourceMap[automation._id]} + disabled={automation.disabled} > diff --git a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte b/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte index 1bc4b0f18e..9465374ae2 100644 --- a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte @@ -39,6 +39,15 @@ >Duplicate Edit + + {automation.disabled ? "Activate" : "Pause"} + Delete diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 879927343f..85ae1924d0 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -364,6 +364,7 @@ value.customType !== "cron" && value.customType !== "triggerSchema" && value.customType !== "automationFields" && + value.type !== "signature_single" && value.type !== "attachment" && value.type !== "attachment_single" ) @@ -456,7 +457,7 @@ value={inputData[key]} options={Object.keys(table?.schema || {})} /> - {:else if value.type === "attachment"} + {:else if value.type === "attachment" || value.type === "signature_single"}
diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte index ab020aad08..b5a54138ca 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte @@ -24,6 +24,11 @@ let table let schemaFields + let attachmentTypes = [ + FieldType.ATTACHMENTS, + FieldType.ATTACHMENT_SINGLE, + FieldType.SIGNATURE_SINGLE, + ] $: { table = $tables.list.find(table => table._id === value?.tableId) @@ -120,15 +125,9 @@ {#if schemaFields.length} {#each schemaFields as [field, schema]} {#if !schema.autocolumn} -
+
-
+
{#if isTestModal} onChange(e, field)} useLabel={false} /> -{:else if schema.type === FieldType.ATTACHMENTS || schema.type === FieldType.ATTACHMENT_SINGLE} +{:else if attachmentTypes.includes(schema.type)}
onChange( { detail: - schema.type === FieldType.ATTACHMENT_SINGLE + schema.type === FieldType.ATTACHMENT_SINGLE || + schema.type === FieldType.SIGNATURE_SINGLE ? e.detail.length > 0 - ? { url: e.detail[0].name, filename: e.detail[0].value } + ? { + url: e.detail[0].name, + filename: e.detail[0].value, + } : {} : e.detail.map(({ name, value }) => ({ url: name, @@ -125,7 +136,8 @@ customButtonText={"Add attachment"} keyPlaceholder={"URL"} valuePlaceholder={"Filename"} - actionButtonDisabled={schema.type === FieldType.ATTACHMENT_SINGLE && + actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE || + schema.type === FieldType.SIGNATURE) && Object.keys(value[field]).length >= 1} />
diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 1ec32cb3fd..1a6bb86113 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -1,4 +1,5 @@ + { + const signatureFile = sigCanvas.toFile() + + let attachRequest = new FormData() + attachRequest.append("file", signatureFile) + + try { + const uploadReq = await API.uploadBuilderAttachment(attachRequest) + const [signatureAttachment] = uploadReq + value = signatureAttachment + } catch (error) { + $notifications.error(error.message || "Failed to save signature") + value = [] + } + }} + title={meta.name} + {value} + bind:this={signatureModal} +/> + {#if type === "options" && meta.constraints.inclusion.length !== 0} {/if} + + diff --git a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte b/packages/builder/src/components/backend/DataTable/TableDataTable.svelte index 77229f3a17..e8e1008e3c 100644 --- a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/TableDataTable.svelte @@ -1,5 +1,6 @@ @@ -14,12 +15,16 @@
-
- -
-
+ {#if !disabled} +
+ +
+
+ +
+ {:else} -
+ {/if}
diff --git a/packages/builder/src/components/common/NavItem.svelte b/packages/builder/src/components/common/NavItem.svelte index 66b21e95a1..5cc6db65a0 100644 --- a/packages/builder/src/components/common/NavItem.svelte +++ b/packages/builder/src/components/common/NavItem.svelte @@ -25,6 +25,7 @@ export let selectedBy = null export let compact = false export let hovering = false + export let disabled = false const scrollApi = getContext("scroll") const dispatch = createEventDispatcher() @@ -74,6 +75,7 @@ class:scrollable class:highlighted class:selectedBy + class:disabled on:dragend on:dragstart on:dragover @@ -165,6 +167,9 @@ --avatars-background: var(--spectrum-global-color-gray-300); color: var(--ink); } + .nav-item.disabled span { + color: var(--spectrum-global-color-gray-700); + } .nav-item:hover, .hovering { background-color: var(--spectrum-global-color-gray-200); diff --git a/packages/builder/src/components/common/UpdateAppForm.svelte b/packages/builder/src/components/common/UpdateAppForm.svelte new file mode 100644 index 0000000000..0e07c5c918 --- /dev/null +++ b/packages/builder/src/components/common/UpdateAppForm.svelte @@ -0,0 +1,214 @@ + + +
+
+
+ + ($validation.touched.name = true)} + on:change={nameToUrl($values.name)} + disabled={appDeployed} + /> +
+
+ + ($validation.touched.url = true)} + on:change={tidyUrl($values.url)} + placeholder={$values.url + ? $values.url + : `/${resolveAppUrl(null, $values.name)}`} + disabled={appDeployed} + /> +
+
+ + +
+
+ {#if !appDeployed} + + {:else} +
+ Unpublish your app to edit name and URL +
+ {/if} +
+
+
+ + diff --git a/packages/builder/src/components/common/UpdateAppTopNav.svelte b/packages/builder/src/components/common/UpdateAppTopNav.svelte new file mode 100644 index 0000000000..f4a76c4576 --- /dev/null +++ b/packages/builder/src/components/common/UpdateAppTopNav.svelte @@ -0,0 +1,68 @@ + + +
+ + +
{ + formPopover.show() + }} + > + + + + +
+
+ + { + formPopoverOpen = false + }} + on:open={() => { + formPopoverOpen = true + }} +> + +
+ { + formPopover.hide() + }} + /> +
+
+
+ + diff --git a/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte b/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte index fb448cca8d..3a787a70cb 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableSlot.svelte @@ -28,6 +28,12 @@ let bindingDrawer let currentVal = value + let attachmentTypes = [ + FieldType.ATTACHMENT_SINGLE, + FieldType.ATTACHMENTS, + FieldType.SIGNATURE_SINGLE, + ] + $: readableValue = runtimeToReadableBinding(bindings, value) $: tempValue = readableValue $: isJS = isJSBinding(value) @@ -105,6 +111,7 @@ boolean: isValidBoolean, attachment: false, attachment_single: false, + signature_single: false, } const isValid = value => { @@ -126,6 +133,7 @@ "bigint", "barcodeqr", "attachment", + "signature_single", "attachment_single", ].includes(type) ) { @@ -169,7 +177,7 @@ {updateOnChange} /> {/if} - {#if !disabled && type !== "formula" && !disabled && type !== FieldType.ATTACHMENTS && !disabled && type !== FieldType.ATTACHMENT_SINGLE} + {#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)}
{ diff --git a/packages/builder/src/components/deploy/AppActions.svelte b/packages/builder/src/components/deploy/AppActions.svelte index 105d1ed958..bb950983a6 100644 --- a/packages/builder/src/components/deploy/AppActions.svelte +++ b/packages/builder/src/components/deploy/AppActions.svelte @@ -8,13 +8,11 @@ ActionButton, Icon, Link, - Modal, StatusLight, AbsTooltip, } from "@budibase/bbui" import RevertModal from "components/deploy/RevertModal.svelte" import VersionModal from "components/deploy/VersionModal.svelte" - import UpdateAppModal from "components/start/UpdateAppModal.svelte" import { processStringSync } from "@budibase/string-templates" import ConfirmDialog from "components/common/ConfirmDialog.svelte" import analytics, { Events, EventSource } from "analytics" @@ -26,7 +24,6 @@ isOnlyUser, appStore, deploymentStore, - initialise, sortedScreens, } from "stores/builder" import TourWrap from "components/portal/onboarding/TourWrap.svelte" @@ -37,7 +34,6 @@ export let loaded let unpublishModal - let updateAppModal let revertModal let versionModal let appActionPopover @@ -61,11 +57,6 @@ $: canPublish = !publishing && loaded && $sortedScreens.length > 0 $: lastDeployed = getLastDeployedString($deploymentStore, lastOpened) - const initialiseApp = async () => { - const applicationPkg = await API.fetchAppPackage($appStore.devId) - await initialise(applicationPkg) - } - const getLastDeployedString = deployments => { return deployments?.length ? processStringSync("Published {{ duration time 'millisecond' }} ago", { @@ -247,16 +238,12 @@ appActionPopover.hide() if (isPublished) { viewApp() - } else { - updateAppModal.show() } }} > {$appStore.url} {#if isPublished} - {:else} - {/if} @@ -330,20 +317,6 @@ Are you sure you want to unpublish the app {selectedApp?.name}? - - { - await initialiseApp() - }} - /> - - diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js index 2a5483d77c..e174a2e6f7 100644 --- a/packages/builder/src/components/design/settings/componentSettings.js +++ b/packages/builder/src/components/design/settings/componentSettings.js @@ -76,6 +76,7 @@ const componentMap = { "field/array": FormFieldSelect, "field/json": FormFieldSelect, "field/barcodeqr": FormFieldSelect, + "field/signature_single": FormFieldSelect, "field/bb_reference": FormFieldSelect, // Some validation types are the same as others, so not all types are // explicitly listed here. e.g. options uses string validation @@ -85,6 +86,8 @@ const componentMap = { "validation/boolean": ValidationEditor, "validation/datetime": ValidationEditor, "validation/attachment": ValidationEditor, + "validation/attachment_single": ValidationEditor, + "validation/signature_single": ValidationEditor, "validation/link": ValidationEditor, "validation/bb_reference": ValidationEditor, } diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte index 0a52a693c3..5cd5658063 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte @@ -24,7 +24,9 @@ parameters } $: automations = $automationStore.automations - .filter(a => a.definition.trigger?.stepId === TriggerStepID.APP) + .filter( + a => a.definition.trigger?.stepId === TriggerStepID.APP && !a.disabled + ) .map(automation => { const schema = Object.entries( automation.definition.trigger.inputs.fields || {} diff --git a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonSetting.svelte b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonSetting.svelte index 9a88875140..2387fda683 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonSetting.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonSetting.svelte @@ -1,5 +1,5 @@ - { - if (!open) { - popover.show() - open = true - } - }} -/> + { - drawers = [] - $draggable.actions.select(componentInstance._id) - }} - on:close={() => { - open = false - if ($draggable.selected === componentInstance._id) { - $draggable.actions.select() - } - }} + open={isOpen} + on:close={close} {anchor} align="left-outside" showPopover={drawers.length === 0} clickOutsideOverride={drawers.length > 0} maxHeight={600} offset={18} - handlePostionUpdate={customPositionHandler} > diff --git a/packages/builder/src/components/design/settings/controls/EditComponentPopover/index.js b/packages/builder/src/components/design/settings/controls/EditComponentPopover/index.js deleted file mode 100644 index 2dc3f60185..0000000000 --- a/packages/builder/src/components/design/settings/controls/EditComponentPopover/index.js +++ /dev/null @@ -1,18 +0,0 @@ -export const customPositionHandler = (anchorBounds, eleBounds, cfg) => { - let { left, top, offset } = cfg - let percentageOffset = 30 - // left-outside - left = anchorBounds.left - eleBounds.width - (offset || 5) - - // shift up from the anchor, if space allows - let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset - let defaultTop = anchorBounds.top - offsetPos - - if (window.innerHeight - defaultTop < eleBounds.height) { - top = window.innerHeight - eleBounds.height - 5 - } else { - top = anchorBounds.top - offsetPos - } - - return { ...cfg, left, top } -} diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte index 771bcf20e0..27590a9858 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte @@ -1,5 +1,5 @@ - - - ($validation.touched.name = true)} - on:change={nameToUrl($values.name)} - label="Name" - /> - - - - - ($validation.touched.url = true)} - on:change={tidyUrl($values.url)} - label="URL" - placeholder={$values.url - ? $values.url - : `/${resolveAppUrl(null, $values.name)}`} - /> - diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js index 75f6a053b5..78e90d6298 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.js @@ -33,7 +33,7 @@ export const FIELDS = { }, }, BARCODEQR: { - name: "Barcode/QR", + name: "Barcode / QR", type: FieldType.BARCODEQR, icon: TypeIconMap[FieldType.BARCODEQR], constraints: { @@ -43,7 +43,7 @@ export const FIELDS = { }, }, LONGFORM: { - name: "Long Form Text", + name: "Long form text", type: FieldType.LONGFORM, icon: TypeIconMap[FieldType.LONGFORM], constraints: { @@ -53,7 +53,7 @@ export const FIELDS = { }, }, OPTIONS: { - name: "Options", + name: "Single select", type: FieldType.OPTIONS, icon: TypeIconMap[FieldType.OPTIONS], constraints: { @@ -63,7 +63,7 @@ export const FIELDS = { }, }, ARRAY: { - name: "Multi-select", + name: "Multi select", type: FieldType.ARRAY, icon: TypeIconMap[FieldType.ARRAY], constraints: { @@ -83,7 +83,7 @@ export const FIELDS = { }, }, BIGINT: { - name: "BigInt", + name: "Big integer", type: FieldType.BIGINT, icon: TypeIconMap[FieldType.BIGINT], }, @@ -97,7 +97,7 @@ export const FIELDS = { }, }, DATETIME: { - name: "Date/Time", + name: "Date / time", type: FieldType.DATETIME, icon: TypeIconMap[FieldType.DATETIME], constraints: { @@ -111,7 +111,7 @@ export const FIELDS = { }, }, ATTACHMENT_SINGLE: { - name: "Attachment", + name: "Single attachment", type: FieldType.ATTACHMENT_SINGLE, icon: TypeIconMap[FieldType.ATTACHMENT_SINGLE], constraints: { @@ -119,7 +119,7 @@ export const FIELDS = { }, }, ATTACHMENTS: { - name: "Attachment List", + name: "Multi attachment", type: FieldType.ATTACHMENTS, icon: TypeIconMap[FieldType.ATTACHMENTS], constraints: { @@ -127,6 +127,14 @@ export const FIELDS = { presence: false, }, }, + SIGNATURE_SINGLE: { + name: "Signature", + type: FieldType.SIGNATURE_SINGLE, + icon: "AnnotatePen", + constraints: { + presence: false, + }, + }, LINK: { name: "Relationship", type: FieldType.LINK, @@ -137,7 +145,7 @@ export const FIELDS = { }, }, AUTO: { - name: "Auto Column", + name: "Auto column", type: FieldType.AUTO, icon: TypeIconMap[FieldType.AUTO], constraints: {}, @@ -158,7 +166,7 @@ export const FIELDS = { }, }, USER: { - name: "User", + name: "Single user", type: FieldType.BB_REFERENCE_SINGLE, subtype: BBReferenceFieldSubType.USER, icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][ @@ -166,7 +174,7 @@ export const FIELDS = { ], }, USERS: { - name: "User List", + name: "Multi user", type: FieldType.BB_REFERENCE, subtype: BBReferenceFieldSubType.USER, icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USER], diff --git a/packages/builder/src/dataBinding.js b/packages/builder/src/dataBinding.js index af229ce7e4..4e48c237ca 100644 --- a/packages/builder/src/dataBinding.js +++ b/packages/builder/src/dataBinding.js @@ -830,7 +830,7 @@ export const getActionBindings = (actions, actionId) => { * @return {{schema: Object, table: Object}} */ export const getSchemaForDatasourcePlus = (resourceId, options) => { - const isViewV2 = resourceId?.includes("view_") + const isViewV2 = resourceId?.startsWith("view_") const datasource = isViewV2 ? { type: "viewV2", diff --git a/packages/builder/src/helpers/validation/yup/app.js b/packages/builder/src/helpers/validation/yup/app.js index 1947844f63..3a00b7a49f 100644 --- a/packages/builder/src/helpers/validation/yup/app.js +++ b/packages/builder/src/helpers/validation/yup/app.js @@ -19,11 +19,10 @@ export const name = (validation, { apps, currentApp } = { apps: [] }) => { // exit early, above validator will fail return true } - if (currentApp) { - // filter out the current app if present - apps = apps.filter(app => app.appId !== currentApp.appId) - } return !apps + .filter(app => { + return app.appId !== currentApp?.appId + }) .map(app => app.name) .some(appName => appName.toLowerCase() === value.toLowerCase()) } diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index f100260343..6094c93a26 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -33,6 +33,7 @@ import { TOUR_KEYS } from "components/portal/onboarding/tours.js" import PreviewOverlay from "./_components/PreviewOverlay.svelte" import EnterpriseBasicTrialModal from "components/portal/onboarding/EnterpriseBasicTrialModal.svelte" + import UpdateAppTopNav from "components/common/UpdateAppTopNav.svelte" export let application @@ -104,10 +105,6 @@ } onMount(async () => { - document.fonts.onloadingdone = e => { - builderStore.loadFonts(e.fontfaces) - } - if (!hasSynced && application) { try { await API.syncApp(application) @@ -148,23 +145,25 @@ /> - {#key $builderStore?.fonts} - {#each $layout.children as { path, title }} - - - - {/each} - {/key} + {#each $layout.children as { path, title }} + + + + {/each}
- {$appStore.name} +
+ + {$appStore.name} + +
@@ -253,7 +252,6 @@ font-weight: 600; overflow: hidden; text-overflow: ellipsis; - padding: 0px var(--spacing-m); } .topleftnav { 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 0f85d2e3e3..ba6f403d81 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 @@ -71,6 +71,7 @@ "multifieldselect", "s3upload", "codescanner", + "signaturesinglefield", "bbreferencesinglefield", "bbreferencefield" ] diff --git a/packages/builder/src/pages/builder/app/[application]/settings/name-and-url.svelte b/packages/builder/src/pages/builder/app/[application]/settings/name-and-url.svelte index be580552c7..e91b8ac3a8 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/name-and-url.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/name-and-url.svelte @@ -1,30 +1,6 @@ @@ -33,61 +9,5 @@ Edit your app's name and URL - - - - {$appStore?.name} - - - - -
- -
-
- - - - {$appStore.url} - - -
- -
+ - - - { - await initialiseApp() - }} - /> - - - diff --git a/packages/builder/src/pages/builder/portal/_layout.svelte b/packages/builder/src/pages/builder/portal/_layout.svelte index a62233fad5..73152a1cd5 100644 --- a/packages/builder/src/pages/builder/portal/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/_layout.svelte @@ -1,7 +1,14 @@
- - onRowClick?.({ row: e.detail })} - /> - + onRowClick?.({ row: e.detail })} + />
@@ -183,14 +189,9 @@ border: 1px solid var(--spectrum-global-color-gray-300); border-radius: 4px; overflow: hidden; + height: 410px; } div.in-builder :global(*) { pointer-events: none; } - span { - display: contents; - } - span :global(.grid) { - height: var(--height); - } diff --git a/packages/client/src/components/app/blocks/FormBlockComponent.svelte b/packages/client/src/components/app/blocks/FormBlockComponent.svelte index 31610f79ac..396dfcf808 100644 --- a/packages/client/src/components/app/blocks/FormBlockComponent.svelte +++ b/packages/client/src/components/app/blocks/FormBlockComponent.svelte @@ -15,6 +15,7 @@ [FieldType.BOOLEAN]: "booleanfield", [FieldType.LONGFORM]: "longformfield", [FieldType.DATETIME]: "datetimefield", + [FieldType.SIGNATURE_SINGLE]: "signaturesinglefield", [FieldType.ATTACHMENTS]: "attachmentfield", [FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield", [FieldType.LINK]: "relationshipfield", diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte index 3489fd809c..27286a8666 100644 --- a/packages/client/src/components/app/forms/AttachmentField.svelte +++ b/packages/client/src/components/app/forms/AttachmentField.svelte @@ -25,7 +25,7 @@ let fieldState let fieldApi - const { API, notificationStore } = getContext("sdk") + const { API, notificationStore, environmentStore } = getContext("sdk") const formContext = getContext("form") const BYTES_IN_MB = 1000000 @@ -87,7 +87,7 @@ error={fieldState.error} on:change={handleChange} {processFiles} - {handleFileTooLarge} + handleFileTooLarge={$environmentStore.cloud ? handleFileTooLarge : null} {handleTooManyFiles} {maximum} {extensions} diff --git a/packages/client/src/components/app/forms/SignatureField.svelte b/packages/client/src/components/app/forms/SignatureField.svelte new file mode 100644 index 0000000000..bdae148368 --- /dev/null +++ b/packages/client/src/components/app/forms/SignatureField.svelte @@ -0,0 +1,129 @@ + + + + + + {#if fieldState} + {#if (Array.isArray(fieldState?.value) && !fieldState?.value?.length) || !fieldState?.value} + { + if (!$builderStore.inBuilder) { + modal.show() + } + }} + > + Add signature + + {:else} +
+ +
+ {/if} + {/if} +
+ + diff --git a/packages/client/src/components/app/forms/index.js b/packages/client/src/components/app/forms/index.js index 15966c3765..391b5fa19f 100644 --- a/packages/client/src/components/app/forms/index.js +++ b/packages/client/src/components/app/forms/index.js @@ -16,5 +16,6 @@ export { default as formstep } from "./FormStep.svelte" export { default as jsonfield } from "./JSONField.svelte" export { default as s3upload } from "./S3Upload.svelte" export { default as codescanner } from "./CodeScannerField.svelte" +export { default as signaturesinglefield } from "./SignatureField.svelte" export { default as bbreferencefield } from "./BBReferenceField.svelte" export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte" diff --git a/packages/client/src/components/app/forms/validation.js b/packages/client/src/components/app/forms/validation.js index bdd7213cb0..46a5330cf3 100644 --- a/packages/client/src/components/app/forms/validation.js +++ b/packages/client/src/components/app/forms/validation.js @@ -200,6 +200,17 @@ const parseType = (value, type) => { return value } + // Parse attachment/signature single, treating no key as null + if ( + type === FieldTypes.ATTACHMENT_SINGLE || + type === FieldTypes.SIGNATURE_SINGLE + ) { + if (!value?.key) { + return null + } + return value + } + // Parse links, treating no elements as null if (type === FieldTypes.LINK) { if (!Array.isArray(value) || !value.length) { @@ -246,10 +257,8 @@ const maxLengthHandler = (value, rule) => { // Evaluates a max file size (MB) constraint const maxFileSizeHandler = (value, rule) => { const limit = parseType(rule.value, "number") - return ( - value == null || - !value.some(attachment => attachment.size / 1000000 > limit) - ) + const check = attachment => attachment.size / 1000000 > limit + return value == null || !(value?.key ? check(value) : value.some(check)) } // Evaluates a max total upload size (MB) constraint @@ -257,8 +266,11 @@ const maxUploadSizeHandler = (value, rule) => { const limit = parseType(rule.value, "number") return ( value == null || - value.reduce((acc, currentItem) => acc + currentItem.size, 0) / 1000000 <= - limit + (value?.key + ? value.size / 1000000 <= limit + : value.reduce((acc, currentItem) => acc + currentItem.size, 0) / + 1000000 <= + limit) ) } diff --git a/packages/frontend-core/src/components/SignatureModal.svelte b/packages/frontend-core/src/components/SignatureModal.svelte new file mode 100644 index 0000000000..8132c5bced --- /dev/null +++ b/packages/frontend-core/src/components/SignatureModal.svelte @@ -0,0 +1,59 @@ + + + + { + onConfirm(canvas) + }} + > +
+ {title} +
+ +
+
+ + diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte index 1a2494987a..f485593c46 100644 --- a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte @@ -8,11 +8,10 @@ export let onChange export let readonly = false export let api - export let invertX = false export let schema export let maximum - const { API, notifications } = getContext("grid") + const { API, notifications, props } = getContext("grid") const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"] let isOpen = false @@ -92,13 +91,7 @@
{#if isOpen} - +
onChange(e.detail)} maximum={maximum || schema.constraints?.length?.maximum} {processFiles} - {handleFileTooLarge} + handleFileTooLarge={$props.isCloud ? handleFileTooLarge : null} />
diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte index d8cff26b9d..0156313d03 100644 --- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte @@ -18,8 +18,6 @@ export let row export let cellId export let updateValue = rows.actions.updateValue - export let invertX = false - export let invertY = false export let contentLines = 1 export let hidden = false @@ -93,8 +91,6 @@ onChange={cellAPI.setValue} {focused} {readonly} - {invertY} - {invertX} {contentLines} /> diff --git a/packages/frontend-core/src/components/grid/cells/DateCell.svelte b/packages/frontend-core/src/components/grid/cells/DateCell.svelte index 3c86fe3605..8922422b43 100644 --- a/packages/frontend-core/src/components/grid/cells/DateCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DateCell.svelte @@ -10,7 +10,6 @@ export let focused = false export let readonly = false export let api - export let invertX = false let isOpen let anchor @@ -111,7 +110,7 @@
{#if isOpen} - + { - columns.actions.changePrimaryDisplay(column.name) + datasource.actions.changePrimaryDisplay(column.name) open = false } const hideColumn = () => { - columns.update(state => { - const index = state.findIndex(col => col.name === column.name) - state[index].visible = false - return state.slice() - }) - columns.actions.saveChanges() + datasource.actions.addSchemaMutation(column.name, { visible: false }) + datasource.actions.saveSchemaMutations() open = false } @@ -386,7 +381,7 @@ > Hide column - {#if $config.canEditColumns && column.schema.type === "link" && column.schema.tableId === TableNames.USERS} + {#if $config.canEditColumns && column.schema.type === "link" && column.schema.tableId === TableNames.USERS && !column.schema.autocolumn} Migrate to user column diff --git a/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte b/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte index 6a05b4ae49..7829e5da7d 100644 --- a/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte @@ -8,7 +8,6 @@ export let onChange export let readonly = false export let api - export let invertX = false let textarea let isOpen = false @@ -67,7 +66,7 @@
{#if isOpen} - +