merge master
This commit is contained in:
commit
04cf17cea7
|
@ -55,7 +55,9 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"no-redeclare": "off",
|
"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",
|
"jest/expect-expect": "off",
|
||||||
// We do this in some tests where the behaviour of internal tables
|
// We do this in some tests where the behaviour of internal tables
|
||||||
// differs to external, but the API is broadly the same
|
// 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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,4 +8,5 @@ Contributors
|
||||||
* Andrew Kingston - [@aptkingston](https://github.com/aptkingston)
|
* Andrew Kingston - [@aptkingston](https://github.com/aptkingston)
|
||||||
* Michael Drury - [@mike12345567](https://github.com/mike12345567)
|
* Michael Drury - [@mike12345567](https://github.com/mike12345567)
|
||||||
* Peter Clement - [@PClmnt](https://github.com/PClmnt)
|
* Peter Clement - [@PClmnt](https://github.com/PClmnt)
|
||||||
* Rory Powell - [@Rory-Powell](https://github.com/Rory-Powell)
|
* Rory Powell - [@Rory-Powell](https://github.com/Rory-Powell)
|
||||||
|
* Michaël St-Georges [@CSLTech](https://github.com/CSLTech)
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.26.2",
|
"version": "2.26.4",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { IdentityContext, Snippet, VM } from "@budibase/types"
|
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
|
// keep this out of Budibase types, don't want to expose context info
|
||||||
export type ContextMap = {
|
export type ContextMap = {
|
||||||
|
@ -12,4 +14,8 @@ export type ContextMap = {
|
||||||
vm?: VM
|
vm?: VM
|
||||||
cleanup?: (() => void | Promise<void>)[]
|
cleanup?: (() => void | Promise<void>)[]
|
||||||
snippets?: Snippet[]
|
snippets?: Snippet[]
|
||||||
|
googleSheets?: {
|
||||||
|
oauthClient: OAuth2Client
|
||||||
|
clients: Record<string, GoogleSpreadsheet>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,31 @@
|
||||||
import PouchDB from "pouchdb"
|
import PouchDB from "pouchdb"
|
||||||
import { getPouchDB, closePouchDB } from "./couch"
|
import { getPouchDB, closePouchDB } from "./couch"
|
||||||
import { DocumentType } from "../constants"
|
import { DocumentType } from "@budibase/types"
|
||||||
|
|
||||||
|
enum ReplicationDirection {
|
||||||
|
TO_PRODUCTION = "toProduction",
|
||||||
|
TO_DEV = "toDev",
|
||||||
|
}
|
||||||
|
|
||||||
class Replication {
|
class Replication {
|
||||||
source: PouchDB.Database
|
source: PouchDB.Database
|
||||||
target: PouchDB.Database
|
target: PouchDB.Database
|
||||||
|
direction: ReplicationDirection | undefined
|
||||||
|
|
||||||
constructor({ source, target }: { source: string; target: string }) {
|
constructor({ source, target }: { source: string; target: string }) {
|
||||||
this.source = getPouchDB(source)
|
this.source = getPouchDB(source)
|
||||||
this.target = getPouchDB(target)
|
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() {
|
async close() {
|
||||||
|
@ -40,12 +57,18 @@ class Replication {
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = opts.filter
|
const filter = opts.filter
|
||||||
|
const direction = this.direction
|
||||||
|
const toDev = direction === ReplicationDirection.TO_DEV
|
||||||
delete opts.filter
|
delete opts.filter
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...opts,
|
...opts,
|
||||||
filter: (doc: any, params: any) => {
|
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
|
return false
|
||||||
}
|
}
|
||||||
if (doc._id === DocumentType.APP_METADATA) {
|
if (doc._id === DocumentType.APP_METADATA) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
isDocument,
|
isDocument,
|
||||||
RowResponse,
|
RowResponse,
|
||||||
RowValue,
|
RowValue,
|
||||||
|
SQLiteDefinition,
|
||||||
SqlQueryBinding,
|
SqlQueryBinding,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getCouchInfo } from "./connections"
|
import { getCouchInfo } from "./connections"
|
||||||
|
@ -21,6 +22,8 @@ import { ReadStream, WriteStream } from "fs"
|
||||||
import { newid } from "../../docIds/newid"
|
import { newid } from "../../docIds/newid"
|
||||||
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
||||||
import { DDInstrumentedDatabase } from "../instrumentation"
|
import { DDInstrumentedDatabase } from "../instrumentation"
|
||||||
|
import { checkSlashesInUrl } from "../../helpers"
|
||||||
|
import env from "../../environment"
|
||||||
|
|
||||||
const DATABASE_NOT_FOUND = "Database does not exist."
|
const DATABASE_NOT_FOUND = "Database does not exist."
|
||||||
|
|
||||||
|
@ -281,25 +284,61 @@ export class DatabaseImpl implements Database {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _sqlQuery<T>(
|
||||||
|
url: string,
|
||||||
|
method: "POST" | "GET",
|
||||||
|
body?: Record<string, any>
|
||||||
|
): Promise<T> {
|
||||||
|
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<T extends Document>(
|
async sql<T extends Document>(
|
||||||
sql: string,
|
sql: string,
|
||||||
parameters?: SqlQueryBinding
|
parameters?: SqlQueryBinding
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const dbName = this.name
|
const dbName = this.name
|
||||||
const url = `/${dbName}/${SQLITE_DESIGN_DOC_ID}`
|
const url = `/${dbName}/${SQLITE_DESIGN_DOC_ID}`
|
||||||
const response = await directCouchUrlCall({
|
return await this._sqlQuery<T[]>(url, "POST", {
|
||||||
url: `${this.couchInfo.sqlUrl}/${url}`,
|
query: sql,
|
||||||
method: "POST",
|
args: parameters,
|
||||||
cookie: this.couchInfo.cookie,
|
|
||||||
body: {
|
|
||||||
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<void> {
|
||||||
|
const dbName = this.name
|
||||||
|
const url = `/${dbName}/_cleanup`
|
||||||
|
return await this._sqlQuery<void>(url, "POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes a document from sqlite
|
||||||
|
async sqlPurgeDocument(docIds: string[] | string): Promise<void> {
|
||||||
|
if (!Array.isArray(docIds)) {
|
||||||
|
docIds = [docIds]
|
||||||
}
|
}
|
||||||
return (await response.json()) as T[]
|
const dbName = this.name
|
||||||
|
const url = `/${dbName}/_purge`
|
||||||
|
return await this._sqlQuery<void>(url, "POST", { docs: docIds })
|
||||||
}
|
}
|
||||||
|
|
||||||
async query<T extends Document>(
|
async query<T extends Document>(
|
||||||
|
@ -314,6 +353,17 @@ export class DatabaseImpl implements Database {
|
||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
try {
|
try {
|
||||||
|
if (env.SQS_SEARCH_ENABLE) {
|
||||||
|
// delete the design document, then run the cleanup operation
|
||||||
|
try {
|
||||||
|
const definition = await this.get<SQLiteDefinition>(
|
||||||
|
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)
|
return await this.nano().db.destroy(this.name)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// didn't exist, don't worry
|
// didn't exist, don't worry
|
||||||
|
|
|
@ -21,7 +21,7 @@ export async function directCouchUrlCall({
|
||||||
url: string
|
url: string
|
||||||
cookie: string
|
cookie: string
|
||||||
method: string
|
method: string
|
||||||
body?: any
|
body?: Record<string, any>
|
||||||
}) {
|
}) {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
method: method,
|
method: method,
|
||||||
|
|
|
@ -56,12 +56,17 @@ export class DDInstrumentedDatabase implements Database {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove(idOrDoc: Document): Promise<DocumentDestroyResponse>
|
||||||
|
remove(idOrDoc: string, rev?: string): Promise<DocumentDestroyResponse>
|
||||||
remove(
|
remove(
|
||||||
id: string | Document,
|
idOrDoc: string | Document,
|
||||||
rev?: string | undefined
|
rev?: string
|
||||||
): Promise<DocumentDestroyResponse> {
|
): Promise<DocumentDestroyResponse> {
|
||||||
return tracer.trace("db.remove", span => {
|
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)
|
return this.db.remove(id, rev)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -160,4 +165,18 @@ export class DDInstrumentedDatabase implements Database {
|
||||||
return this.db.sql(sql, parameters)
|
return this.db.sql(sql, parameters)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sqlPurgeDocument(docIds: string[] | string): Promise<void> {
|
||||||
|
return tracer.trace("db.sqlPurgeDocument", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.sqlPurgeDocument(docIds)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDiskCleanup(): Promise<void> {
|
||||||
|
return tracer.trace("db.sqlDiskCleanup", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.sqlDiskCleanup()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,7 @@ const environment = {
|
||||||
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||||
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || "http://localhost:4006",
|
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_USERNAME: process.env.COUCH_DB_USER,
|
||||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
|
|
@ -492,7 +492,7 @@ export class UserDB {
|
||||||
|
|
||||||
await platform.users.removeUser(dbUser)
|
await platform.users.removeUser(dbUser)
|
||||||
|
|
||||||
await db.remove(userId, dbUser._rev)
|
await db.remove(userId, dbUser._rev!)
|
||||||
|
|
||||||
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
|
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
|
||||||
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
||||||
|
|
|
@ -71,8 +71,8 @@ const handleMouseDown = e => {
|
||||||
|
|
||||||
// Clear any previous listeners in case of multiple down events, and register
|
// Clear any previous listeners in case of multiple down events, and register
|
||||||
// a single mouse up listener
|
// a single mouse up listener
|
||||||
document.removeEventListener("mouseup", handleMouseUp)
|
document.removeEventListener("click", handleMouseUp)
|
||||||
document.addEventListener("mouseup", handleMouseUp, true)
|
document.addEventListener("click", handleMouseUp, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global singleton listeners for our events
|
// Global singleton listeners for our events
|
||||||
|
|
|
@ -155,6 +155,8 @@ export default function positionDropdown(element, opts) {
|
||||||
applyXStrategy(Strategies.StartToEnd)
|
applyXStrategy(Strategies.StartToEnd)
|
||||||
} else if (align === "left-outside") {
|
} else if (align === "left-outside") {
|
||||||
applyXStrategy(Strategies.EndToStart)
|
applyXStrategy(Strategies.EndToStart)
|
||||||
|
} else if (align === "center") {
|
||||||
|
applyXStrategy(Strategies.MidPoint)
|
||||||
} else {
|
} else {
|
||||||
applyXStrategy(Strategies.StartToStart)
|
applyXStrategy(Strategies.StartToStart)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,267 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, createEventDispatcher } from "svelte"
|
||||||
|
import Atrament from "atrament"
|
||||||
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let last
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let disabled = false
|
||||||
|
export let editable = true
|
||||||
|
export let width = 400
|
||||||
|
export let height = 220
|
||||||
|
export let saveIcon = false
|
||||||
|
export let darkMode
|
||||||
|
|
||||||
|
export function toDataUrl() {
|
||||||
|
// PNG to preserve transparency
|
||||||
|
return canvasRef.toDataURL("image/png")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFile() {
|
||||||
|
const data = canvasContext
|
||||||
|
.getImageData(0, 0, width, height)
|
||||||
|
.data.some(channel => channel !== 0)
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataURIParts = toDataUrl().split(",")
|
||||||
|
if (!dataURIParts.length) {
|
||||||
|
console.error("Could not retrieve signature data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull out the base64 encoded byte data
|
||||||
|
let binaryVal = atob(dataURIParts[1])
|
||||||
|
let blobArray = new Uint8Array(binaryVal.length)
|
||||||
|
let pos = 0
|
||||||
|
while (pos < binaryVal.length) {
|
||||||
|
blobArray[pos] = binaryVal.charCodeAt(pos)
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
const signatureBlob = new Blob([blobArray], {
|
||||||
|
type: "image/png",
|
||||||
|
})
|
||||||
|
|
||||||
|
return new File([signatureBlob], "signature.png", {
|
||||||
|
type: signatureBlob.type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCanvas() {
|
||||||
|
return canvasContext.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
let canvasRef
|
||||||
|
let canvasContext
|
||||||
|
let canvasWrap
|
||||||
|
let canvasWidth
|
||||||
|
let canvasHeight
|
||||||
|
let signature
|
||||||
|
|
||||||
|
let updated = false
|
||||||
|
let signatureFile
|
||||||
|
let urlFailed
|
||||||
|
|
||||||
|
$: if (value) {
|
||||||
|
signatureFile = value
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (signatureFile?.url) {
|
||||||
|
updated = false
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (last) {
|
||||||
|
dispatch("update")
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPos = e => {
|
||||||
|
var rect = canvasRef.getBoundingClientRect()
|
||||||
|
const canvasX = e.offsetX || e.targetTouches?.[0].pageX - rect.left
|
||||||
|
const canvasY = e.offsetY || e.targetTouches?.[0].pageY - rect.top
|
||||||
|
|
||||||
|
return { x: canvasX, y: canvasY }
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkUp = e => {
|
||||||
|
last = getPos(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasRef.addEventListener("pointerdown", e => {
|
||||||
|
const current = getPos(e)
|
||||||
|
//If the cursor didn't move at all, block the default pointerdown
|
||||||
|
if (last?.x === current?.x && last?.y === current?.y) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener("pointerup", checkUp)
|
||||||
|
|
||||||
|
signature = new Atrament(canvasRef, {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
color: "white",
|
||||||
|
})
|
||||||
|
|
||||||
|
signature.weight = 4
|
||||||
|
signature.smoothing = 2
|
||||||
|
|
||||||
|
canvasWrap.style.width = `${width}px`
|
||||||
|
canvasWrap.style.height = `${height}px`
|
||||||
|
|
||||||
|
const { width: wrapWidth, height: wrapHeight } =
|
||||||
|
canvasWrap.getBoundingClientRect()
|
||||||
|
|
||||||
|
canvasHeight = wrapHeight
|
||||||
|
canvasWidth = wrapWidth
|
||||||
|
|
||||||
|
canvasContext = canvasRef.getContext("2d")
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
signature.destroy()
|
||||||
|
document.removeEventListener("pointerup", checkUp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="signature" class:light={!darkMode} class:image-error={urlFailed}>
|
||||||
|
{#if !disabled}
|
||||||
|
<div class="overlay">
|
||||||
|
{#if updated && saveIcon}
|
||||||
|
<span class="save">
|
||||||
|
<Icon
|
||||||
|
name="Checkmark"
|
||||||
|
hoverable
|
||||||
|
tooltip={"Save"}
|
||||||
|
tooltipPosition={"top"}
|
||||||
|
tooltipType={"info"}
|
||||||
|
on:click={() => {
|
||||||
|
dispatch("change", toDataUrl())
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if signatureFile?.url && !updated}
|
||||||
|
<span class="delete">
|
||||||
|
<Icon
|
||||||
|
name="DeleteOutline"
|
||||||
|
hoverable
|
||||||
|
tooltip={"Delete"}
|
||||||
|
tooltipPosition={"top"}
|
||||||
|
tooltipType={"info"}
|
||||||
|
on:click={() => {
|
||||||
|
if (editable) {
|
||||||
|
clearCanvas()
|
||||||
|
}
|
||||||
|
dispatch("clear")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !editable && signatureFile?.url}
|
||||||
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
|
{#if !urlFailed}
|
||||||
|
<img
|
||||||
|
src={signatureFile?.url}
|
||||||
|
on:error={() => {
|
||||||
|
urlFailed = true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
Could not load signature
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div bind:this={canvasWrap} class="canvas-wrap">
|
||||||
|
<canvas
|
||||||
|
id="signature-canvas"
|
||||||
|
bind:this={canvasRef}
|
||||||
|
style="--max-sig-width: {width}px; --max-sig-height: {height}px"
|
||||||
|
/>
|
||||||
|
{#if editable}
|
||||||
|
<div class="indicator-overlay">
|
||||||
|
<div class="sign-here">
|
||||||
|
<Icon name="Close" />
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.indicator-overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: end;
|
||||||
|
padding: var(--spectrum-global-dimension-size-150);
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.sign-here {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spectrum-global-dimension-size-150);
|
||||||
|
}
|
||||||
|
.sign-here hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 2px solid var(--spectrum-global-color-gray-200);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.canvas-wrap {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.signature img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
#signature-canvas {
|
||||||
|
max-width: var(--max-sig-width);
|
||||||
|
max-height: var(--max-sig-height);
|
||||||
|
}
|
||||||
|
.signature.light img,
|
||||||
|
.signature.light #signature-canvas {
|
||||||
|
-webkit-filter: invert(100%);
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
|
.signature.image-error .overlay {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
.signature {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: var(--spectrum-global-dimension-size-150);
|
||||||
|
text-align: right;
|
||||||
|
z-index: 2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.save,
|
||||||
|
.delete {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -16,3 +16,4 @@ export { default as CoreStepper } from "./Stepper.svelte"
|
||||||
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
export { default as CoreRichTextField } from "./RichTextField.svelte"
|
||||||
export { default as CoreSlider } from "./Slider.svelte"
|
export { default as CoreSlider } from "./Slider.svelte"
|
||||||
export { default as CoreFile } from "./File.svelte"
|
export { default as CoreFile } from "./File.svelte"
|
||||||
|
export { default as CoreSignature } from "./Signature.svelte"
|
||||||
|
|
|
@ -173,6 +173,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-Modal {
|
.spectrum-Modal {
|
||||||
|
border: 2px solid var(--spectrum-global-color-gray-200);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
margin: 40px 0;
|
margin: 40px 0;
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
export let secondaryButtonText = undefined
|
export let secondaryButtonText = undefined
|
||||||
export let secondaryAction = undefined
|
export let secondaryAction = undefined
|
||||||
export let secondaryButtonWarning = false
|
export let secondaryButtonWarning = false
|
||||||
|
export let custom = false
|
||||||
|
|
||||||
const { hide, cancel } = getContext(Context.Modal)
|
const { hide, cancel } = getContext(Context.Modal)
|
||||||
let loading = false
|
let loading = false
|
||||||
|
@ -63,12 +64,13 @@
|
||||||
class:spectrum-Dialog--medium={size === "M"}
|
class:spectrum-Dialog--medium={size === "M"}
|
||||||
class:spectrum-Dialog--large={size === "L"}
|
class:spectrum-Dialog--large={size === "L"}
|
||||||
class:spectrum-Dialog--extraLarge={size === "XL"}
|
class:spectrum-Dialog--extraLarge={size === "XL"}
|
||||||
|
class:no-grid={custom}
|
||||||
style="position: relative;"
|
style="position: relative;"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
<div class="spectrum-Dialog-grid">
|
<div class="modal-core" class:spectrum-Dialog-grid={!custom}>
|
||||||
{#if title || $$slots.header}
|
{#if title || $$slots.header}
|
||||||
<h1
|
<h1
|
||||||
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
||||||
|
@ -153,6 +155,25 @@
|
||||||
.spectrum-Dialog-content {
|
.spectrum-Dialog-content {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-grid .spectrum-Dialog-content {
|
||||||
|
border-top: 2px solid var(--spectrum-global-color-gray-200);
|
||||||
|
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-grid .spectrum-Dialog-heading {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-Dialog.no-grid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-Dialog.no-grid .spectrum-Dialog-buttonGroup {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.spectrum-Dialog-heading {
|
.spectrum-Dialog-heading {
|
||||||
font-family: var(--font-accent);
|
font-family: var(--font-accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
@ -1,40 +1,28 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount, createEventDispatcher } from "svelte"
|
import { getContext, onDestroy } from "svelte"
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let id
|
export let id
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
let selected = getContext("tab")
|
let selected = getContext("tab")
|
||||||
let tab_internal
|
let observer
|
||||||
let tabInfo
|
let ref
|
||||||
|
|
||||||
const setTabInfo = () => {
|
$: isSelected = $selected.title === title
|
||||||
// If the tabs are being rendered inside a component which uses
|
$: {
|
||||||
// a svelte transition to enter, then this initial getBoundingClientRect
|
if (isSelected && ref) {
|
||||||
// will return an incorrect position.
|
observe()
|
||||||
// We just need to get this off the main thread to fix this, by using
|
} else {
|
||||||
// a 0ms timeout.
|
stopObserving()
|
||||||
setTimeout(() => {
|
}
|
||||||
tabInfo = tab_internal?.getBoundingClientRect()
|
|
||||||
if (tabInfo && $selected.title === title) {
|
|
||||||
$selected.info = tabInfo
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
const setTabInfo = () => {
|
||||||
setTabInfo()
|
const tabInfo = ref?.getBoundingClientRect()
|
||||||
})
|
if (tabInfo) {
|
||||||
|
$selected.info = tabInfo
|
||||||
//Ensure that the underline is in the correct location
|
|
||||||
$: {
|
|
||||||
if ($selected.title === title && tab_internal) {
|
|
||||||
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
|
|
||||||
setTabInfo()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,10 +30,25 @@
|
||||||
$selected = {
|
$selected = {
|
||||||
...$selected,
|
...$selected,
|
||||||
title,
|
title,
|
||||||
info: tab_internal.getBoundingClientRect(),
|
info: ref.getBoundingClientRect(),
|
||||||
}
|
}
|
||||||
dispatch("click")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const observe = () => {
|
||||||
|
if (!observer) {
|
||||||
|
observer = new ResizeObserver(setTabInfo)
|
||||||
|
observer.observe(ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopObserving = () => {
|
||||||
|
if (observer) {
|
||||||
|
observer.unobserve(ref)
|
||||||
|
observer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(stopObserving)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
@ -53,11 +56,12 @@
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
<div
|
<div
|
||||||
{id}
|
{id}
|
||||||
bind:this={tab_internal}
|
bind:this={ref}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
class:is-selected={$selected.title === title}
|
on:click
|
||||||
class="spectrum-Tabs-item"
|
class="spectrum-Tabs-item"
|
||||||
class:emphasized={$selected.title === title && $selected.emphasized}
|
class:is-selected={isSelected}
|
||||||
|
class:emphasized={isSelected && $selected.emphasized}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
|
@ -72,7 +76,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
<span class="spectrum-Tabs-itemLabel">{title}</span>
|
<span class="spectrum-Tabs-itemLabel">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if $selected.title === title}
|
|
||||||
|
{#if isSelected}
|
||||||
<Portal target=".spectrum-Tabs-content-{$selected.id}">
|
<Portal target=".spectrum-Tabs-content-{$selected.id}">
|
||||||
<slot />
|
<slot />
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import TestDataModal from "./TestDataModal.svelte"
|
import TestDataModal from "./TestDataModal.svelte"
|
||||||
import { flip } from "svelte/animate"
|
import { flip } from "svelte/animate"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
import { Icon, notifications, Modal } from "@budibase/bbui"
|
import { Icon, notifications, Modal, Toggle } from "@budibase/bbui"
|
||||||
import { ActionStepID } from "constants/backend/automations"
|
import { ActionStepID } from "constants/backend/automations"
|
||||||
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
import UndoRedoControl from "components/common/UndoRedoControl.svelte"
|
||||||
|
|
||||||
|
@ -73,6 +73,16 @@
|
||||||
Test details
|
Test details
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-spacing">
|
||||||
|
<Toggle
|
||||||
|
text={automation.disabled ? "Paused" : "Activated"}
|
||||||
|
on:change={automationStore.actions.toggleDisabled(
|
||||||
|
automation._id,
|
||||||
|
automation.disabled
|
||||||
|
)}
|
||||||
|
value={!automation.disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="canvas" on:scroll={handleScroll}>
|
<div class="canvas" on:scroll={handleScroll}>
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
selected={automation._id === selectedAutomationId}
|
selected={automation._id === selectedAutomationId}
|
||||||
on:click={() => selectAutomation(automation._id)}
|
on:click={() => selectAutomation(automation._id)}
|
||||||
selectedBy={$userSelectedResourceMap[automation._id]}
|
selectedBy={$userSelectedResourceMap[automation._id]}
|
||||||
|
disabled={automation.disabled}
|
||||||
>
|
>
|
||||||
<EditAutomationPopover {automation} />
|
<EditAutomationPopover {automation} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
|
@ -39,6 +39,15 @@
|
||||||
>Duplicate</MenuItem
|
>Duplicate</MenuItem
|
||||||
>
|
>
|
||||||
<MenuItem icon="Edit" on:click={updateAutomationDialog.show}>Edit</MenuItem>
|
<MenuItem icon="Edit" on:click={updateAutomationDialog.show}>Edit</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={automation.disabled ? "CheckmarkCircle" : "Cancel"}
|
||||||
|
on:click={automationStore.actions.toggleDisabled(
|
||||||
|
automation._id,
|
||||||
|
automation.disabled
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{automation.disabled ? "Activate" : "Pause"}
|
||||||
|
</MenuItem>
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
|
|
||||||
|
|
|
@ -364,6 +364,7 @@
|
||||||
value.customType !== "cron" &&
|
value.customType !== "cron" &&
|
||||||
value.customType !== "triggerSchema" &&
|
value.customType !== "triggerSchema" &&
|
||||||
value.customType !== "automationFields" &&
|
value.customType !== "automationFields" &&
|
||||||
|
value.type !== "signature_single" &&
|
||||||
value.type !== "attachment" &&
|
value.type !== "attachment" &&
|
||||||
value.type !== "attachment_single"
|
value.type !== "attachment_single"
|
||||||
)
|
)
|
||||||
|
@ -456,7 +457,7 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
options={Object.keys(table?.schema || {})}
|
options={Object.keys(table?.schema || {})}
|
||||||
/>
|
/>
|
||||||
{:else if value.type === "attachment"}
|
{:else if value.type === "attachment" || value.type === "signature_single"}
|
||||||
<div class="attachment-field-wrapper">
|
<div class="attachment-field-wrapper">
|
||||||
<div class="label-wrapper">
|
<div class="label-wrapper">
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
|
|
|
@ -24,6 +24,11 @@
|
||||||
|
|
||||||
let table
|
let table
|
||||||
let schemaFields
|
let schemaFields
|
||||||
|
let attachmentTypes = [
|
||||||
|
FieldType.ATTACHMENTS,
|
||||||
|
FieldType.ATTACHMENT_SINGLE,
|
||||||
|
FieldType.SIGNATURE_SINGLE,
|
||||||
|
]
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
table = $tables.list.find(table => table._id === value?.tableId)
|
table = $tables.list.find(table => table._id === value?.tableId)
|
||||||
|
@ -120,15 +125,9 @@
|
||||||
{#if schemaFields.length}
|
{#if schemaFields.length}
|
||||||
{#each schemaFields as [field, schema]}
|
{#each schemaFields as [field, schema]}
|
||||||
{#if !schema.autocolumn}
|
{#if !schema.autocolumn}
|
||||||
<div
|
<div class:schema-fields={!attachmentTypes.includes(schema.type)}>
|
||||||
class:schema-fields={schema.type !== FieldType.ATTACHMENTS &&
|
|
||||||
schema.type !== FieldType.ATTACHMENT_SINGLE}
|
|
||||||
>
|
|
||||||
<Label>{field}</Label>
|
<Label>{field}</Label>
|
||||||
<div
|
<div class:field-width={!attachmentTypes.includes(schema.type)}>
|
||||||
class:field-width={schema.type !== FieldType.ATTACHMENTS &&
|
|
||||||
schema.type !== FieldType.ATTACHMENT_SINGLE}
|
|
||||||
>
|
|
||||||
{#if isTestModal}
|
{#if isTestModal}
|
||||||
<RowSelectorTypes
|
<RowSelectorTypes
|
||||||
{isTestModal}
|
{isTestModal}
|
||||||
|
|
|
@ -21,6 +21,12 @@
|
||||||
return clone
|
return clone
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let attachmentTypes = [
|
||||||
|
FieldType.ATTACHMENTS,
|
||||||
|
FieldType.ATTACHMENT_SINGLE,
|
||||||
|
FieldType.SIGNATURE_SINGLE,
|
||||||
|
]
|
||||||
|
|
||||||
function schemaHasOptions(schema) {
|
function schemaHasOptions(schema) {
|
||||||
return !!schema.constraints?.inclusion?.length
|
return !!schema.constraints?.inclusion?.length
|
||||||
}
|
}
|
||||||
|
@ -29,7 +35,8 @@
|
||||||
let params = {}
|
let params = {}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
schema.type === FieldType.ATTACHMENT_SINGLE &&
|
(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
schema.type === FieldType.SIGNATURE_SINGLE) &&
|
||||||
Object.keys(keyValueObj).length === 0
|
Object.keys(keyValueObj).length === 0
|
||||||
) {
|
) {
|
||||||
return []
|
return []
|
||||||
|
@ -100,16 +107,20 @@
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
useLabel={false}
|
useLabel={false}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === FieldType.ATTACHMENTS || schema.type === FieldType.ATTACHMENT_SINGLE}
|
{:else if attachmentTypes.includes(schema.type)}
|
||||||
<div class="attachment-field-spacinng">
|
<div class="attachment-field-spacinng">
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
on:change={e =>
|
on:change={e =>
|
||||||
onChange(
|
onChange(
|
||||||
{
|
{
|
||||||
detail:
|
detail:
|
||||||
schema.type === FieldType.ATTACHMENT_SINGLE
|
schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
schema.type === FieldType.SIGNATURE_SINGLE
|
||||||
? e.detail.length > 0
|
? 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 }) => ({
|
: e.detail.map(({ name, value }) => ({
|
||||||
url: name,
|
url: name,
|
||||||
|
@ -125,7 +136,8 @@
|
||||||
customButtonText={"Add attachment"}
|
customButtonText={"Add attachment"}
|
||||||
keyPlaceholder={"URL"}
|
keyPlaceholder={"URL"}
|
||||||
valuePlaceholder={"Filename"}
|
valuePlaceholder={"Filename"}
|
||||||
actionButtonDisabled={schema.type === FieldType.ATTACHMENT_SINGLE &&
|
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||||
|
schema.type === FieldType.SIGNATURE) &&
|
||||||
Object.keys(value[field]).length >= 1}
|
Object.keys(value[field]).length >= 1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { API } from "api"
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
@ -8,15 +9,19 @@
|
||||||
Label,
|
Label,
|
||||||
RichTextField,
|
RichTextField,
|
||||||
TextArea,
|
TextArea,
|
||||||
|
CoreSignature,
|
||||||
|
ActionButton,
|
||||||
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import Dropzone from "components/common/Dropzone.svelte"
|
import Dropzone from "components/common/Dropzone.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
import Editor from "../../integration/QueryEditor.svelte"
|
import Editor from "../../integration/QueryEditor.svelte"
|
||||||
|
import { SignatureModal } from "@budibase/frontend-core/src/components"
|
||||||
|
import { themeStore } from "stores/portal"
|
||||||
|
|
||||||
export let defaultValue
|
|
||||||
export let meta
|
export let meta
|
||||||
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
export let value
|
||||||
export let readonly
|
export let readonly
|
||||||
export let error
|
export let error
|
||||||
|
|
||||||
|
@ -39,8 +44,35 @@
|
||||||
|
|
||||||
const timeStamp = resolveTimeStamp(value)
|
const timeStamp = resolveTimeStamp(value)
|
||||||
const isTimeStamp = !!timeStamp || meta?.timeOnly
|
const isTimeStamp = !!timeStamp || meta?.timeOnly
|
||||||
|
|
||||||
|
$: currentTheme = $themeStore?.theme
|
||||||
|
$: darkMode = !currentTheme.includes("light")
|
||||||
|
|
||||||
|
let signatureModal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<SignatureModal
|
||||||
|
{darkMode}
|
||||||
|
onConfirm={async sigCanvas => {
|
||||||
|
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 type === "options" && meta.constraints.inclusion.length !== 0}
|
||||||
<Select
|
<Select
|
||||||
{label}
|
{label}
|
||||||
|
@ -59,7 +91,51 @@
|
||||||
bind:value
|
bind:value
|
||||||
/>
|
/>
|
||||||
{:else if type === "attachment"}
|
{:else if type === "attachment"}
|
||||||
<Dropzone {label} {error} bind:value />
|
<Dropzone
|
||||||
|
compact
|
||||||
|
{label}
|
||||||
|
{error}
|
||||||
|
{value}
|
||||||
|
on:change={e => {
|
||||||
|
value = e.detail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if type === "attachment_single"}
|
||||||
|
<Dropzone
|
||||||
|
compact
|
||||||
|
{label}
|
||||||
|
{error}
|
||||||
|
value={value ? [value] : []}
|
||||||
|
on:change={e => {
|
||||||
|
value = e.detail?.[0]
|
||||||
|
}}
|
||||||
|
maximum={1}
|
||||||
|
/>
|
||||||
|
{:else if type === "signature_single"}
|
||||||
|
<div class="signature">
|
||||||
|
<Label>{label}</Label>
|
||||||
|
<div class="sig-wrap" class:display={value}>
|
||||||
|
{#if value}
|
||||||
|
<CoreSignature
|
||||||
|
{darkMode}
|
||||||
|
{value}
|
||||||
|
editable={false}
|
||||||
|
on:clear={() => {
|
||||||
|
value = null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<ActionButton
|
||||||
|
fullWidth
|
||||||
|
on:click={() => {
|
||||||
|
signatureModal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add signature
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else if type === "boolean"}
|
{:else if type === "boolean"}
|
||||||
<Toggle text={label} {error} bind:value />
|
<Toggle text={label} {error} bind:value />
|
||||||
{:else if type === "array" && meta.constraints.inclusion.length !== 0}
|
{:else if type === "array" && meta.constraints.inclusion.length !== 0}
|
||||||
|
@ -95,3 +171,22 @@
|
||||||
{:else}
|
{:else}
|
||||||
<Input {label} {type} {error} bind:value disabled={readonly} />
|
<Input {label} {type} {error} bind:value disabled={readonly} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.signature :global(label.spectrum-FieldLabel) {
|
||||||
|
padding-top: var(--spectrum-fieldlabel-padding-top);
|
||||||
|
padding-bottom: var(--spectrum-fieldlabel-padding-bottom);
|
||||||
|
}
|
||||||
|
.sig-wrap.display {
|
||||||
|
min-height: 50px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: var(--spectrum-alias-border-size-thin)
|
||||||
|
var(--spectrum-alias-border-color) solid;
|
||||||
|
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { datasources, tables, integrations, appStore } from "stores/builder"
|
import { datasources, tables, integrations, appStore } from "stores/builder"
|
||||||
|
import { themeStore, admin } from "stores/portal"
|
||||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
|
@ -37,6 +38,9 @@
|
||||||
})
|
})
|
||||||
$: relationshipsEnabled = relationshipSupport(tableDatasource)
|
$: relationshipsEnabled = relationshipSupport(tableDatasource)
|
||||||
|
|
||||||
|
$: currentTheme = $themeStore?.theme
|
||||||
|
$: darkMode = !currentTheme.includes("light")
|
||||||
|
|
||||||
const relationshipSupport = datasource => {
|
const relationshipSupport = datasource => {
|
||||||
const integration = $integrations[datasource?.source]
|
const integration = $integrations[datasource?.source]
|
||||||
return !isInternal && integration?.relationships !== false
|
return !isInternal && integration?.relationships !== false
|
||||||
|
@ -55,6 +59,7 @@
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<Grid
|
<Grid
|
||||||
{API}
|
{API}
|
||||||
|
{darkMode}
|
||||||
datasource={gridDatasource}
|
datasource={gridDatasource}
|
||||||
canAddRows={!isUsersTable}
|
canAddRows={!isUsersTable}
|
||||||
canDeleteRows={!isUsersTable}
|
canDeleteRows={!isUsersTable}
|
||||||
|
@ -63,6 +68,7 @@
|
||||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
on:updatedatasource={handleGridTableUpdate}
|
on:updatedatasource={handleGridTableUpdate}
|
||||||
|
isCloud={$admin.cloud}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="filter">
|
<svelte:fragment slot="filter">
|
||||||
{#if isUsersTable && $appStore.features.disableUserMetadata}
|
{#if isUsersTable && $appStore.features.disableUserMetadata}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { viewsV2 } from "stores/builder"
|
import { viewsV2 } from "stores/builder"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
allowDeleteRows
|
allowDeleteRows
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
on:updatedatasource={handleGridViewUpdate}
|
on:updatedatasource={handleGridViewUpdate}
|
||||||
|
isCloud={$admin.cloud}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="filter">
|
<svelte:fragment slot="filter">
|
||||||
<GridFilterButton />
|
<GridFilterButton />
|
||||||
|
|
|
@ -9,6 +9,7 @@ const MAX_DEPTH = 1
|
||||||
const TYPES_TO_SKIP = [
|
const TYPES_TO_SKIP = [
|
||||||
FieldType.FORMULA,
|
FieldType.FORMULA,
|
||||||
FieldType.LONGFORM,
|
FieldType.LONGFORM,
|
||||||
|
FieldType.SIGNATURE_SINGLE,
|
||||||
FieldType.ATTACHMENTS,
|
FieldType.ATTACHMENTS,
|
||||||
//https://github.com/Budibase/budibase/issues/3030
|
//https://github.com/Budibase/budibase/issues/3030
|
||||||
FieldType.INTERNAL,
|
FieldType.INTERNAL,
|
||||||
|
|
|
@ -398,44 +398,51 @@
|
||||||
if (!externalTable) {
|
if (!externalTable) {
|
||||||
return [
|
return [
|
||||||
FIELDS.STRING,
|
FIELDS.STRING,
|
||||||
FIELDS.BARCODEQR,
|
FIELDS.NUMBER,
|
||||||
FIELDS.LONGFORM,
|
|
||||||
FIELDS.OPTIONS,
|
FIELDS.OPTIONS,
|
||||||
FIELDS.ARRAY,
|
FIELDS.ARRAY,
|
||||||
FIELDS.NUMBER,
|
|
||||||
FIELDS.BIGINT,
|
|
||||||
FIELDS.BOOLEAN,
|
FIELDS.BOOLEAN,
|
||||||
FIELDS.DATETIME,
|
FIELDS.DATETIME,
|
||||||
FIELDS.ATTACHMENT_SINGLE,
|
|
||||||
FIELDS.ATTACHMENTS,
|
|
||||||
FIELDS.LINK,
|
FIELDS.LINK,
|
||||||
FIELDS.FORMULA,
|
FIELDS.LONGFORM,
|
||||||
FIELDS.JSON,
|
|
||||||
FIELDS.USER,
|
FIELDS.USER,
|
||||||
FIELDS.USERS,
|
FIELDS.USERS,
|
||||||
|
FIELDS.ATTACHMENT_SINGLE,
|
||||||
|
FIELDS.ATTACHMENTS,
|
||||||
|
FIELDS.FORMULA,
|
||||||
|
FIELDS.JSON,
|
||||||
|
FIELDS.BARCODEQR,
|
||||||
|
FIELDS.SIGNATURE_SINGLE,
|
||||||
|
FIELDS.BIGINT,
|
||||||
FIELDS.AUTO,
|
FIELDS.AUTO,
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
let fields = [
|
let fields = [
|
||||||
FIELDS.STRING,
|
FIELDS.STRING,
|
||||||
FIELDS.BARCODEQR,
|
|
||||||
FIELDS.LONGFORM,
|
|
||||||
FIELDS.OPTIONS,
|
|
||||||
FIELDS.DATETIME,
|
|
||||||
FIELDS.NUMBER,
|
FIELDS.NUMBER,
|
||||||
|
FIELDS.OPTIONS,
|
||||||
|
FIELDS.ARRAY,
|
||||||
FIELDS.BOOLEAN,
|
FIELDS.BOOLEAN,
|
||||||
FIELDS.FORMULA,
|
FIELDS.DATETIME,
|
||||||
FIELDS.BIGINT,
|
FIELDS.LINK,
|
||||||
|
FIELDS.LONGFORM,
|
||||||
FIELDS.USER,
|
FIELDS.USER,
|
||||||
|
FIELDS.USERS,
|
||||||
|
FIELDS.FORMULA,
|
||||||
|
FIELDS.BARCODEQR,
|
||||||
|
FIELDS.BIGINT,
|
||||||
]
|
]
|
||||||
|
|
||||||
if (datasource && datasource.source !== SourceName.GOOGLE_SHEETS) {
|
// Filter out multiple users for google sheets
|
||||||
fields.push(FIELDS.USERS)
|
if (datasource?.source === SourceName.GOOGLE_SHEETS) {
|
||||||
|
fields = fields.filter(x => x !== FIELDS.USERS)
|
||||||
}
|
}
|
||||||
// no-sql or a spreadsheet
|
|
||||||
if (!externalTable || table.sql) {
|
// Filter out SQL-specific types for non-SQL datasources
|
||||||
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
if (!table.sql) {
|
||||||
|
fields = fields.filter(x => x !== FIELDS.LINK && x !== FIELDS.ARRAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,8 +86,9 @@ export const createValidatedConfigStore = (integration, config) => {
|
||||||
([$configStore, $errorsStore, $selectedValidatorsStore]) => {
|
([$configStore, $errorsStore, $selectedValidatorsStore]) => {
|
||||||
const validatedConfig = []
|
const validatedConfig = []
|
||||||
|
|
||||||
|
const allowedRestKeys = ["rejectUnauthorized", "downloadImages"]
|
||||||
Object.entries(integration.datasource).forEach(([key, properties]) => {
|
Object.entries(integration.datasource).forEach(([key, properties]) => {
|
||||||
if (integration.name === "REST" && key !== "rejectUnauthorized") {
|
if (integration.name === "REST" && !allowedRestKeys.includes(key)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,10 @@
|
||||||
label: "Attachment",
|
label: "Attachment",
|
||||||
value: FieldType.ATTACHMENT_SINGLE,
|
value: FieldType.ATTACHMENT_SINGLE,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Signature",
|
||||||
|
value: FieldType.SIGNATURE_SINGLE,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Attachment list",
|
label: "Attachment list",
|
||||||
value: FieldType.ATTACHMENTS,
|
value: FieldType.ATTACHMENTS,
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { it, expect, describe, vi } from "vitest"
|
||||||
|
import Dropzone from "./Dropzone.svelte"
|
||||||
|
import { render, fireEvent } from "@testing-library/svelte"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
|
|
||||||
|
vi.spyOn(notifications, "error").mockImplementation(() => {})
|
||||||
|
|
||||||
|
describe("Dropzone", () => {
|
||||||
|
let instance = null
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("that the Dropzone is rendered", () => {
|
||||||
|
instance = render(Dropzone, {})
|
||||||
|
expect(instance).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Ensure the correct error message is shown when uploading the file in cloud", async () => {
|
||||||
|
admin.subscribe = vi.fn().mockImplementation(callback => {
|
||||||
|
callback({ cloud: true })
|
||||||
|
return () => {}
|
||||||
|
})
|
||||||
|
instance = render(Dropzone, { props: { fileSizeLimit: 1000000 } }) // 1MB
|
||||||
|
const fileInput = instance.getByLabelText("Select a file to upload")
|
||||||
|
const file = new File(["hello".repeat(2000000)], "hello.png", {
|
||||||
|
type: "image/png",
|
||||||
|
})
|
||||||
|
await fireEvent.change(fileInput, { target: { files: [file] } })
|
||||||
|
expect(notifications.error).toHaveBeenCalledWith(
|
||||||
|
"Files cannot exceed 1MB. Please try again with smaller files."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Ensure the file size error message is not shown when running on self host", async () => {
|
||||||
|
admin.subscribe = vi.fn().mockImplementation(callback => {
|
||||||
|
callback({ cloud: false })
|
||||||
|
return () => {}
|
||||||
|
})
|
||||||
|
instance = render(Dropzone, { props: { fileSizeLimit: 1000000 } }) // 1MB
|
||||||
|
const fileInput = instance.getByLabelText("Select a file to upload")
|
||||||
|
const file = new File(["hello".repeat(2000000)], "hello.png", {
|
||||||
|
type: "image/png",
|
||||||
|
})
|
||||||
|
await fireEvent.change(fileInput, { target: { files: [file] } })
|
||||||
|
expect(notifications.error).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,9 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Dropzone, notifications } from "@budibase/bbui"
|
import { Dropzone, notifications } from "@budibase/bbui"
|
||||||
|
import { admin } from "stores/portal"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
export let value = []
|
export let value = []
|
||||||
export let label
|
export let label
|
||||||
|
export let fileSizeLimit = undefined
|
||||||
|
|
||||||
const BYTES_IN_MB = 1000000
|
const BYTES_IN_MB = 1000000
|
||||||
|
|
||||||
|
@ -34,5 +36,6 @@
|
||||||
{label}
|
{label}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
handleFileTooLarge={$admin.cloud ? handleFileTooLarge : null}
|
||||||
|
{fileSizeLimit}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let app
|
export let app
|
||||||
export let color
|
export let color
|
||||||
export let autoSave = false
|
export let autoSave = false
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
@ -14,12 +15,16 @@
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="editable-icon">
|
<div class="editable-icon">
|
||||||
<div class="hover" on:click={modal.show}>
|
{#if !disabled}
|
||||||
<Icon name="Edit" {size} color="var(--spectrum-global-color-gray-600)" />
|
<div class="hover" on:click={modal.show}>
|
||||||
</div>
|
<Icon name="Edit" {size} color="var(--spectrum-global-color-gray-600)" />
|
||||||
<div class="normal">
|
</div>
|
||||||
|
<div class="normal">
|
||||||
|
<Icon name={name || "Apps"} {size} {color} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<Icon {name} {size} {color} />
|
<Icon {name} {size} {color} />
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
export let selectedBy = null
|
export let selectedBy = null
|
||||||
export let compact = false
|
export let compact = false
|
||||||
export let hovering = false
|
export let hovering = false
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -74,6 +75,7 @@
|
||||||
class:scrollable
|
class:scrollable
|
||||||
class:highlighted
|
class:highlighted
|
||||||
class:selectedBy
|
class:selectedBy
|
||||||
|
class:disabled
|
||||||
on:dragend
|
on:dragend
|
||||||
on:dragstart
|
on:dragstart
|
||||||
on:dragover
|
on:dragover
|
||||||
|
@ -165,6 +167,9 @@
|
||||||
--avatars-background: var(--spectrum-global-color-gray-300);
|
--avatars-background: var(--spectrum-global-color-gray-300);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
.nav-item.disabled span {
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
}
|
||||||
.nav-item:hover,
|
.nav-item:hover,
|
||||||
.hovering {
|
.hovering {
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Label, Icon, Input, notifications } from "@budibase/bbui"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import { appStore, initialise } from "stores/builder"
|
||||||
|
import { appsStore } from "stores/portal"
|
||||||
|
import { API } from "api"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
|
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||||
|
import { isEqual } from "lodash"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let alignActions = "left"
|
||||||
|
|
||||||
|
const values = writable({})
|
||||||
|
const validation = createValidationStore()
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let updating = false
|
||||||
|
let edited = false
|
||||||
|
let initialised = false
|
||||||
|
|
||||||
|
$: filteredApps = $appsStore.apps.filter(app => app.devId == $appStore.appId)
|
||||||
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
|
||||||
|
$: appName = $appStore.name
|
||||||
|
$: appURL = $appStore.url
|
||||||
|
$: appIconName = $appStore.icon?.name
|
||||||
|
$: appIconColor = $appStore.icon?.color
|
||||||
|
|
||||||
|
$: appMeta = {
|
||||||
|
name: appName,
|
||||||
|
url: appURL,
|
||||||
|
iconName: appIconName,
|
||||||
|
iconColor: appIconColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
const initForm = appMeta => {
|
||||||
|
edited = false
|
||||||
|
values.set({
|
||||||
|
...appMeta,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!initialised) {
|
||||||
|
setupValidation()
|
||||||
|
initialised = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = (vals, appMeta) => {
|
||||||
|
const { url } = vals || {}
|
||||||
|
validation.check({
|
||||||
|
...vals,
|
||||||
|
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
||||||
|
})
|
||||||
|
edited = !isEqual(vals, appMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// On app/apps update, reset the state.
|
||||||
|
$: initForm(appMeta)
|
||||||
|
$: validate($values, appMeta)
|
||||||
|
|
||||||
|
const resolveAppUrl = (template, name) => {
|
||||||
|
let parsedName
|
||||||
|
const resolvedName = resolveAppName(null, name)
|
||||||
|
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
|
||||||
|
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
|
||||||
|
return encodeURI(parsedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameToUrl = appName => {
|
||||||
|
let resolvedUrl = resolveAppUrl(null, appName)
|
||||||
|
tidyUrl(resolvedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveAppName = (template, name) => {
|
||||||
|
if (template && !name) {
|
||||||
|
return template.name
|
||||||
|
}
|
||||||
|
return name ? name.trim() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tidyUrl = url => {
|
||||||
|
if (url && !url.startsWith("/")) {
|
||||||
|
url = `/${url}`
|
||||||
|
}
|
||||||
|
$values.url = url === "" ? null : url
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateIcon = e => {
|
||||||
|
const { name, color } = e.detail
|
||||||
|
$values.iconColor = color
|
||||||
|
$values.iconName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupValidation = async () => {
|
||||||
|
appValidation.name(validation, {
|
||||||
|
apps: $appsStore.apps,
|
||||||
|
currentApp: app,
|
||||||
|
})
|
||||||
|
appValidation.url(validation, {
|
||||||
|
apps: $appsStore.apps,
|
||||||
|
currentApp: app,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateApp() {
|
||||||
|
try {
|
||||||
|
await appsStore.save($appStore.appId, {
|
||||||
|
name: $values.name?.trim(),
|
||||||
|
url: $values.url?.trim(),
|
||||||
|
icon: {
|
||||||
|
name: $values.iconName,
|
||||||
|
color: $values.iconColor,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await initialiseApp()
|
||||||
|
notifications.success("App update successful")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notifications.error("Error updating app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialiseApp = async () => {
|
||||||
|
const applicationPkg = await API.fetchAppPackage($appStore.appId)
|
||||||
|
await initialise(applicationPkg)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Name</Label>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.name}
|
||||||
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
|
on:change={nameToUrl($values.name)}
|
||||||
|
disabled={appDeployed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">URL</Label>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.url}
|
||||||
|
error={$validation.touched.url && $validation.errors.url}
|
||||||
|
on:blur={() => ($validation.touched.url = true)}
|
||||||
|
on:change={tidyUrl($values.url)}
|
||||||
|
placeholder={$values.url
|
||||||
|
? $values.url
|
||||||
|
: `/${resolveAppUrl(null, $values.name)}`}
|
||||||
|
disabled={appDeployed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Icon</Label>
|
||||||
|
<EditableIcon
|
||||||
|
{app}
|
||||||
|
size="XL"
|
||||||
|
name={$values.iconName}
|
||||||
|
color={$values.iconColor}
|
||||||
|
on:change={updateIcon}
|
||||||
|
disabled={appDeployed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="actions" class:right={alignActions === "right"}>
|
||||||
|
{#if !appDeployed}
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={async () => {
|
||||||
|
updating = true
|
||||||
|
await updateApp()
|
||||||
|
updating = false
|
||||||
|
dispatch("updated")
|
||||||
|
}}
|
||||||
|
disabled={appDeployed || updating || !edited || !$validation.valid}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<div class="edit-info">
|
||||||
|
<Icon size="S" name="Info" /> Unpublish your app to edit name and URL
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.actions.right {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
.fields {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 220px;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.edit-info {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script>
|
||||||
|
import { Popover, Layout, Icon } from "@budibase/bbui"
|
||||||
|
import UpdateAppForm from "./UpdateAppForm.svelte"
|
||||||
|
|
||||||
|
let formPopover
|
||||||
|
let formPopoverAnchor
|
||||||
|
let formPopoverOpen = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={formPopoverAnchor}>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<div
|
||||||
|
class="app-heading"
|
||||||
|
class:editing={formPopoverOpen}
|
||||||
|
on:click={() => {
|
||||||
|
formPopover.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<span class="edit-icon">
|
||||||
|
<Icon size="S" name="Edit" color={"var(--grey-7)"} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
customZindex={998}
|
||||||
|
bind:this={formPopover}
|
||||||
|
align="center"
|
||||||
|
anchor={formPopoverAnchor}
|
||||||
|
offset={20}
|
||||||
|
on:close={() => {
|
||||||
|
formPopoverOpen = false
|
||||||
|
}}
|
||||||
|
on:open={() => {
|
||||||
|
formPopoverOpen = true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layout noPadding gap="M">
|
||||||
|
<div class="popover-content">
|
||||||
|
<UpdateAppForm
|
||||||
|
on:updated={() => {
|
||||||
|
formPopover.hide()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.popover-content {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.app-heading {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.edit-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.app-heading:hover .edit-icon,
|
||||||
|
.app-heading.editing .edit-icon {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -28,6 +28,12 @@
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
let currentVal = value
|
let currentVal = value
|
||||||
|
|
||||||
|
let attachmentTypes = [
|
||||||
|
FieldType.ATTACHMENT_SINGLE,
|
||||||
|
FieldType.ATTACHMENTS,
|
||||||
|
FieldType.SIGNATURE_SINGLE,
|
||||||
|
]
|
||||||
|
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
$: tempValue = readableValue
|
$: tempValue = readableValue
|
||||||
$: isJS = isJSBinding(value)
|
$: isJS = isJSBinding(value)
|
||||||
|
@ -105,6 +111,7 @@
|
||||||
boolean: isValidBoolean,
|
boolean: isValidBoolean,
|
||||||
attachment: false,
|
attachment: false,
|
||||||
attachment_single: false,
|
attachment_single: false,
|
||||||
|
signature_single: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = value => {
|
const isValid = value => {
|
||||||
|
@ -126,6 +133,7 @@
|
||||||
"bigint",
|
"bigint",
|
||||||
"barcodeqr",
|
"barcodeqr",
|
||||||
"attachment",
|
"attachment",
|
||||||
|
"signature_single",
|
||||||
"attachment_single",
|
"attachment_single",
|
||||||
].includes(type)
|
].includes(type)
|
||||||
) {
|
) {
|
||||||
|
@ -169,7 +177,7 @@
|
||||||
{updateOnChange}
|
{updateOnChange}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !disabled && type !== "formula" && !disabled && type !== FieldType.ATTACHMENTS && !disabled && type !== FieldType.ATTACHMENT_SINGLE}
|
{#if !disabled && type !== "formula" && !disabled && !attachmentTypes.includes(type)}
|
||||||
<div
|
<div
|
||||||
class={`icon ${getIconClass(value, type)}`}
|
class={`icon ${getIconClass(value, type)}`}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
|
@ -8,13 +8,11 @@
|
||||||
ActionButton,
|
ActionButton,
|
||||||
Icon,
|
Icon,
|
||||||
Link,
|
Link,
|
||||||
Modal,
|
|
||||||
StatusLight,
|
StatusLight,
|
||||||
AbsTooltip,
|
AbsTooltip,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
|
@ -26,7 +24,6 @@
|
||||||
isOnlyUser,
|
isOnlyUser,
|
||||||
appStore,
|
appStore,
|
||||||
deploymentStore,
|
deploymentStore,
|
||||||
initialise,
|
|
||||||
sortedScreens,
|
sortedScreens,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
|
@ -37,7 +34,6 @@
|
||||||
export let loaded
|
export let loaded
|
||||||
|
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
let updateAppModal
|
|
||||||
let revertModal
|
let revertModal
|
||||||
let versionModal
|
let versionModal
|
||||||
let appActionPopover
|
let appActionPopover
|
||||||
|
@ -61,11 +57,6 @@
|
||||||
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
|
$: canPublish = !publishing && loaded && $sortedScreens.length > 0
|
||||||
$: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
|
$: lastDeployed = getLastDeployedString($deploymentStore, lastOpened)
|
||||||
|
|
||||||
const initialiseApp = async () => {
|
|
||||||
const applicationPkg = await API.fetchAppPackage($appStore.devId)
|
|
||||||
await initialise(applicationPkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLastDeployedString = deployments => {
|
const getLastDeployedString = deployments => {
|
||||||
return deployments?.length
|
return deployments?.length
|
||||||
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
||||||
|
@ -247,16 +238,12 @@
|
||||||
appActionPopover.hide()
|
appActionPopover.hide()
|
||||||
if (isPublished) {
|
if (isPublished) {
|
||||||
viewApp()
|
viewApp()
|
||||||
} else {
|
|
||||||
updateAppModal.show()
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$appStore.url}
|
{$appStore.url}
|
||||||
{#if isPublished}
|
{#if isPublished}
|
||||||
<Icon size="S" name="LinkOut" />
|
<Icon size="S" name="LinkOut" />
|
||||||
{:else}
|
|
||||||
<Icon size="S" name="Edit" />
|
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</Body>
|
</Body>
|
||||||
|
@ -330,20 +317,6 @@
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<Modal bind:this={updateAppModal} padding={false} width="600px">
|
|
||||||
<UpdateAppModal
|
|
||||||
app={{
|
|
||||||
name: $appStore.name,
|
|
||||||
url: $appStore.url,
|
|
||||||
icon: $appStore.icon,
|
|
||||||
appId: $appStore.appId,
|
|
||||||
}}
|
|
||||||
onUpdateComplete={async () => {
|
|
||||||
await initialiseApp()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<RevertModal bind:this={revertModal} />
|
<RevertModal bind:this={revertModal} />
|
||||||
<VersionModal hideIcon bind:this={versionModal} />
|
<VersionModal hideIcon bind:this={versionModal} />
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,7 @@ const componentMap = {
|
||||||
"field/array": FormFieldSelect,
|
"field/array": FormFieldSelect,
|
||||||
"field/json": FormFieldSelect,
|
"field/json": FormFieldSelect,
|
||||||
"field/barcodeqr": FormFieldSelect,
|
"field/barcodeqr": FormFieldSelect,
|
||||||
|
"field/signature_single": FormFieldSelect,
|
||||||
"field/bb_reference": FormFieldSelect,
|
"field/bb_reference": FormFieldSelect,
|
||||||
// Some validation types are the same as others, so not all types are
|
// Some validation types are the same as others, so not all types are
|
||||||
// explicitly listed here. e.g. options uses string validation
|
// explicitly listed here. e.g. options uses string validation
|
||||||
|
@ -85,6 +86,8 @@ const componentMap = {
|
||||||
"validation/boolean": ValidationEditor,
|
"validation/boolean": ValidationEditor,
|
||||||
"validation/datetime": ValidationEditor,
|
"validation/datetime": ValidationEditor,
|
||||||
"validation/attachment": ValidationEditor,
|
"validation/attachment": ValidationEditor,
|
||||||
|
"validation/attachment_single": ValidationEditor,
|
||||||
|
"validation/signature_single": ValidationEditor,
|
||||||
"validation/link": ValidationEditor,
|
"validation/link": ValidationEditor,
|
||||||
"validation/bb_reference": ValidationEditor,
|
"validation/bb_reference": ValidationEditor,
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,9 @@
|
||||||
parameters
|
parameters
|
||||||
}
|
}
|
||||||
$: automations = $automationStore.automations
|
$: automations = $automationStore.automations
|
||||||
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
|
.filter(
|
||||||
|
a => a.definition.trigger?.stepId === TriggerStepID.APP && !a.disabled
|
||||||
|
)
|
||||||
.map(automation => {
|
.map(automation => {
|
||||||
const schema = Object.entries(
|
const schema = Object.entries(
|
||||||
automation.definition.trigger.inputs.fields || {}
|
automation.definition.trigger.inputs.fields || {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { runtimeToReadableBinding } from "dataBinding"
|
import { runtimeToReadableBinding } from "dataBinding"
|
||||||
import { isJSBinding } from "@budibase/string-templates"
|
import { isJSBinding } from "@budibase/string-templates"
|
||||||
|
|
|
@ -100,9 +100,6 @@
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
get(store).actions.select(draggableItem.id)
|
get(store).actions.select(draggableItem.id)
|
||||||
}}
|
}}
|
||||||
on:mousedown={() => {
|
|
||||||
get(store).actions.select()
|
|
||||||
}}
|
|
||||||
bind:this={anchors[draggableItem.id]}
|
bind:this={anchors[draggableItem.id]}
|
||||||
class:highlighted={draggableItem.id === $store.selected}
|
class:highlighted={draggableItem.id === $store.selected}
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { componentStore } from "stores/builder"
|
import { componentStore } from "stores/builder"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import { customPositionHandler } from "."
|
|
||||||
import ComponentSettingsSection from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
import ComponentSettingsSection from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
||||||
|
|
||||||
export let anchor
|
export let anchor
|
||||||
|
@ -18,76 +17,74 @@
|
||||||
|
|
||||||
let popover
|
let popover
|
||||||
let drawers = []
|
let drawers = []
|
||||||
let open = false
|
let isOpen = false
|
||||||
|
|
||||||
// Auto hide the component when another item is selected
|
// Auto hide the component when another item is selected
|
||||||
$: if (open && $draggable.selected !== componentInstance._id) {
|
$: if (open && $draggable.selected !== componentInstance._id) {
|
||||||
popover.hide()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open automatically if the component is marked as selected
|
// Open automatically if the component is marked as selected
|
||||||
$: if (!open && $draggable.selected === componentInstance._id && popover) {
|
$: if (!open && $draggable.selected === componentInstance._id && popover) {
|
||||||
popover.show()
|
open()
|
||||||
open = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: componentDef = componentStore.getDefinition(componentInstance._component)
|
$: componentDef = componentStore.getDefinition(componentInstance._component)
|
||||||
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
isOpen = true
|
||||||
|
drawers = []
|
||||||
|
$draggable.actions.select(componentInstance._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
// Slight delay allows us to be able to properly toggle open/close state by
|
||||||
|
// clicking again on the settings icon
|
||||||
|
setTimeout(() => {
|
||||||
|
isOpen = false
|
||||||
|
if ($draggable.selected === componentInstance._id) {
|
||||||
|
$draggable.actions.select()
|
||||||
|
}
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleOpen = () => {
|
||||||
|
if (isOpen) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const processComponentDefinitionSettings = componentDef => {
|
const processComponentDefinitionSettings = componentDef => {
|
||||||
if (!componentDef) {
|
if (!componentDef) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
const clone = cloneDeep(componentDef)
|
const clone = cloneDeep(componentDef)
|
||||||
|
|
||||||
if (typeof parseSettings === "function") {
|
if (typeof parseSettings === "function") {
|
||||||
clone.settings = parseSettings(clone.settings)
|
clone.settings = parseSettings(clone.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSetting = async (setting, value) => {
|
const updateSetting = async (setting, value) => {
|
||||||
const nestedComponentInstance = cloneDeep(componentInstance)
|
const nestedComponentInstance = cloneDeep(componentInstance)
|
||||||
|
|
||||||
const patchFn = componentStore.updateComponentSetting(setting.key, value)
|
const patchFn = componentStore.updateComponentSetting(setting.key, value)
|
||||||
patchFn(nestedComponentInstance)
|
patchFn(nestedComponentInstance)
|
||||||
|
|
||||||
dispatch("change", nestedComponentInstance)
|
dispatch("change", nestedComponentInstance)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Icon
|
<Icon name="Settings" hoverable size="S" on:click={toggleOpen} />
|
||||||
name="Settings"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => {
|
|
||||||
if (!open) {
|
|
||||||
popover.show()
|
|
||||||
open = true
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
bind:this={popover}
|
open={isOpen}
|
||||||
on:open={() => {
|
on:close={close}
|
||||||
drawers = []
|
|
||||||
$draggable.actions.select(componentInstance._id)
|
|
||||||
}}
|
|
||||||
on:close={() => {
|
|
||||||
open = false
|
|
||||||
if ($draggable.selected === componentInstance._id) {
|
|
||||||
$draggable.actions.select()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{anchor}
|
{anchor}
|
||||||
align="left-outside"
|
align="left-outside"
|
||||||
showPopover={drawers.length === 0}
|
showPopover={drawers.length === 0}
|
||||||
clickOutsideOverride={drawers.length > 0}
|
clickOutsideOverride={drawers.length > 0}
|
||||||
maxHeight={600}
|
maxHeight={600}
|
||||||
offset={18}
|
offset={18}
|
||||||
handlePostionUpdate={customPositionHandler}
|
|
||||||
>
|
>
|
||||||
<span class="popover-wrap">
|
<span class="popover-wrap">
|
||||||
<Layout noPadding noGap>
|
<Layout noPadding noGap>
|
|
@ -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 }
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
import { Toggle, Icon } from "@budibase/bbui"
|
import { Toggle, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
|
@ -41,6 +41,7 @@ export const FieldTypeToComponentMap = {
|
||||||
[FieldType.BOOLEAN]: "booleanfield",
|
[FieldType.BOOLEAN]: "booleanfield",
|
||||||
[FieldType.LONGFORM]: "longformfield",
|
[FieldType.LONGFORM]: "longformfield",
|
||||||
[FieldType.DATETIME]: "datetimefield",
|
[FieldType.DATETIME]: "datetimefield",
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: "signaturesinglefield",
|
||||||
[FieldType.ATTACHMENTS]: "attachmentfield",
|
[FieldType.ATTACHMENTS]: "attachmentfield",
|
||||||
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
||||||
[FieldType.LINK]: "relationshipfield",
|
[FieldType.LINK]: "relationshipfield",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
import { Toggle, Icon } from "@budibase/bbui"
|
import { Toggle, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import EditComponentPopover from "../EditComponentPopover/EditComponentPopover.svelte"
|
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { setContext } from "svelte"
|
import { setContext } from "svelte"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
|
@ -67,6 +67,7 @@ const toGridFormat = draggableListColumns => {
|
||||||
label: entry.label,
|
label: entry.label,
|
||||||
field: entry.field,
|
field: entry.field,
|
||||||
active: entry.active,
|
active: entry.active,
|
||||||
|
width: entry.width,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +82,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
|
||||||
field: column.field,
|
field: column.field,
|
||||||
label: column.label,
|
label: column.label,
|
||||||
columnType: schema[column.field].type,
|
columnType: schema[column.field].type,
|
||||||
|
width: column.width,
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
export let value = null
|
export let value = null
|
||||||
|
|
||||||
$: dataSources = $datasources.list
|
$: dataSources = $datasources.list
|
||||||
.filter(ds => ds.source === "S3" && !ds.config?.endpoint)
|
.filter(ds => ds.source === "S3")
|
||||||
.map(ds => ({
|
.map(ds => ({
|
||||||
label: ds.name,
|
label: ds.name,
|
||||||
value: ds._id,
|
value: ds._id,
|
||||||
|
|
|
@ -108,6 +108,8 @@
|
||||||
Constraints.MaxFileSize,
|
Constraints.MaxFileSize,
|
||||||
Constraints.MaxUploadSize,
|
Constraints.MaxUploadSize,
|
||||||
],
|
],
|
||||||
|
["attachment_single"]: [Constraints.Required, Constraints.MaxUploadSize],
|
||||||
|
["signature_single"]: [Constraints.Required],
|
||||||
["link"]: [
|
["link"]: [
|
||||||
Constraints.Required,
|
Constraints.Required,
|
||||||
Constraints.Contains,
|
Constraints.Contains,
|
||||||
|
|
|
@ -2,21 +2,21 @@
|
||||||
import { Modal, ModalContent } from "@budibase/bbui"
|
import { Modal, ModalContent } from "@budibase/bbui"
|
||||||
import FreeTrial from "../../../../assets/FreeTrial.svelte"
|
import FreeTrial from "../../../../assets/FreeTrial.svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { auth, licensing } from "stores/portal"
|
import { auth, licensing, admin } from "stores/portal"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { PlanType } from "@budibase/types"
|
import { PlanType } from "@budibase/types"
|
||||||
import { sdk } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
let freeTrialModal
|
let freeTrialModal
|
||||||
|
|
||||||
$: planType = $licensing?.license?.plan?.type
|
$: planType = $licensing?.license?.plan?.type
|
||||||
$: showFreeTrialModal(planType, freeTrialModal)
|
$: showFreeTrialModal(planType, freeTrialModal)
|
||||||
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
const showFreeTrialModal = (planType, freeTrialModal) => {
|
const showFreeTrialModal = (planType, freeTrialModal) => {
|
||||||
if (
|
if (
|
||||||
planType === PlanType.ENTERPRISE_BASIC_TRIAL &&
|
planType === PlanType.ENTERPRISE_BASIC_TRIAL &&
|
||||||
!$auth.user?.freeTrialConfirmedAt &&
|
!$auth.user?.freeTrialConfirmedAt &&
|
||||||
sdk.users.isAdmin($auth.user)
|
isOwner
|
||||||
) {
|
) {
|
||||||
freeTrialModal?.show()
|
freeTrialModal?.show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
NewFormSteps,
|
NewFormSteps,
|
||||||
} from "./steps"
|
} from "./steps"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { customPositionHandler } from "components/design/settings/controls/EditComponentPopover"
|
|
||||||
|
|
||||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||||
|
|
||||||
|
@ -187,7 +186,6 @@ const getTours = () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS)
|
||||||
builderStore.highlightSetting("steps", "info")
|
builderStore.highlightSetting("steps", "info")
|
||||||
},
|
},
|
||||||
positionHandler: customPositionHandler,
|
|
||||||
align: "left-outside",
|
align: "left-outside",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -203,7 +201,6 @@ const getTours = () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID)
|
||||||
builderStore.highlightSetting("rowId", "info")
|
builderStore.highlightSetting("rowId", "info")
|
||||||
},
|
},
|
||||||
positionHandler: customPositionHandler,
|
|
||||||
align: "left-outside",
|
align: "left-outside",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -219,7 +216,6 @@ const getTours = () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS)
|
||||||
builderStore.highlightSetting("steps", "info")
|
builderStore.highlightSetting("steps", "info")
|
||||||
},
|
},
|
||||||
positionHandler: customPositionHandler,
|
|
||||||
align: "left-outside",
|
align: "left-outside",
|
||||||
scrollIntoView: true,
|
scrollIntoView: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
<script>
|
|
||||||
import { writable, get as svelteGet } from "svelte/store"
|
|
||||||
import {
|
|
||||||
notifications,
|
|
||||||
Input,
|
|
||||||
ModalContent,
|
|
||||||
Layout,
|
|
||||||
Label,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { appsStore } from "stores/portal"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { createValidationStore } from "helpers/validation/yup"
|
|
||||||
import * as appValidation from "helpers/validation/yup/app"
|
|
||||||
import EditableIcon from "../common/EditableIcon.svelte"
|
|
||||||
|
|
||||||
export let app
|
|
||||||
export let onUpdateComplete
|
|
||||||
|
|
||||||
$: appIdParts = app.appId ? app.appId?.split("_") : []
|
|
||||||
$: appId = appIdParts.slice(-1)[0]
|
|
||||||
|
|
||||||
const values = writable({
|
|
||||||
name: app.name,
|
|
||||||
url: app.url,
|
|
||||||
iconName: app.icon?.name,
|
|
||||||
iconColor: app.icon?.color,
|
|
||||||
})
|
|
||||||
const validation = createValidationStore()
|
|
||||||
|
|
||||||
$: {
|
|
||||||
const { url } = $values
|
|
||||||
|
|
||||||
validation.check({
|
|
||||||
...$values,
|
|
||||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupValidation = async () => {
|
|
||||||
const applications = svelteGet(appsStore).apps
|
|
||||||
appValidation.name(validation, {
|
|
||||||
apps: applications,
|
|
||||||
currentApp: {
|
|
||||||
...app,
|
|
||||||
appId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
appValidation.url(validation, {
|
|
||||||
apps: applications,
|
|
||||||
currentApp: {
|
|
||||||
...app,
|
|
||||||
appId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// init validation
|
|
||||||
const { url } = $values
|
|
||||||
validation.check({
|
|
||||||
...$values,
|
|
||||||
url: url?.[0] === "/" ? url.substring(1, url.length) : url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateApp() {
|
|
||||||
try {
|
|
||||||
await appsStore.save(app.appId, {
|
|
||||||
name: $values.name?.trim(),
|
|
||||||
url: $values.url?.trim(),
|
|
||||||
icon: {
|
|
||||||
name: $values.iconName,
|
|
||||||
color: $values.iconColor,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (typeof onUpdateComplete == "function") {
|
|
||||||
onUpdateComplete()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
notifications.error("Error updating app")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveAppUrl = (template, name) => {
|
|
||||||
let parsedName
|
|
||||||
const resolvedName = resolveAppName(null, name)
|
|
||||||
parsedName = resolvedName ? resolvedName.toLowerCase() : ""
|
|
||||||
const parsedUrl = parsedName ? parsedName.replace(/\s+/g, "-") : ""
|
|
||||||
return encodeURI(parsedUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveAppName = (template, name) => {
|
|
||||||
if (template && !name) {
|
|
||||||
return template.name
|
|
||||||
}
|
|
||||||
return name ? name.trim() : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const tidyUrl = url => {
|
|
||||||
if (url && !url.startsWith("/")) {
|
|
||||||
url = `/${url}`
|
|
||||||
}
|
|
||||||
$values.url = url === "" ? null : url
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameToUrl = appName => {
|
|
||||||
let resolvedUrl = resolveAppUrl(null, appName)
|
|
||||||
tidyUrl(resolvedUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateIcon = e => {
|
|
||||||
const { name, color } = e.detail
|
|
||||||
$values.iconColor = color
|
|
||||||
$values.iconName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(setupValidation)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
title="Edit name and URL"
|
|
||||||
confirmText="Save"
|
|
||||||
onConfirm={updateApp}
|
|
||||||
disabled={!$validation.valid}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
bind:value={$values.name}
|
|
||||||
error={$validation.touched.name && $validation.errors.name}
|
|
||||||
on:blur={() => ($validation.touched.name = true)}
|
|
||||||
on:change={nameToUrl($values.name)}
|
|
||||||
label="Name"
|
|
||||||
/>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label>Icon</Label>
|
|
||||||
<EditableIcon
|
|
||||||
{app}
|
|
||||||
size="XL"
|
|
||||||
name={$values.iconName}
|
|
||||||
color={$values.iconColor}
|
|
||||||
on:change={updateIcon}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
<Input
|
|
||||||
bind:value={$values.url}
|
|
||||||
error={$validation.touched.url && $validation.errors.url}
|
|
||||||
on:blur={() => ($validation.touched.url = true)}
|
|
||||||
on:change={tidyUrl($values.url)}
|
|
||||||
label="URL"
|
|
||||||
placeholder={$values.url
|
|
||||||
? $values.url
|
|
||||||
: `/${resolveAppUrl(null, $values.name)}`}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
|
@ -33,7 +33,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
BARCODEQR: {
|
BARCODEQR: {
|
||||||
name: "Barcode/QR",
|
name: "Barcode / QR",
|
||||||
type: FieldType.BARCODEQR,
|
type: FieldType.BARCODEQR,
|
||||||
icon: TypeIconMap[FieldType.BARCODEQR],
|
icon: TypeIconMap[FieldType.BARCODEQR],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -43,7 +43,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
LONGFORM: {
|
LONGFORM: {
|
||||||
name: "Long Form Text",
|
name: "Long form text",
|
||||||
type: FieldType.LONGFORM,
|
type: FieldType.LONGFORM,
|
||||||
icon: TypeIconMap[FieldType.LONGFORM],
|
icon: TypeIconMap[FieldType.LONGFORM],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -53,7 +53,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OPTIONS: {
|
OPTIONS: {
|
||||||
name: "Options",
|
name: "Single select",
|
||||||
type: FieldType.OPTIONS,
|
type: FieldType.OPTIONS,
|
||||||
icon: TypeIconMap[FieldType.OPTIONS],
|
icon: TypeIconMap[FieldType.OPTIONS],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -63,7 +63,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ARRAY: {
|
ARRAY: {
|
||||||
name: "Multi-select",
|
name: "Multi select",
|
||||||
type: FieldType.ARRAY,
|
type: FieldType.ARRAY,
|
||||||
icon: TypeIconMap[FieldType.ARRAY],
|
icon: TypeIconMap[FieldType.ARRAY],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -83,7 +83,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
BIGINT: {
|
BIGINT: {
|
||||||
name: "BigInt",
|
name: "Big integer",
|
||||||
type: FieldType.BIGINT,
|
type: FieldType.BIGINT,
|
||||||
icon: TypeIconMap[FieldType.BIGINT],
|
icon: TypeIconMap[FieldType.BIGINT],
|
||||||
},
|
},
|
||||||
|
@ -97,7 +97,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DATETIME: {
|
DATETIME: {
|
||||||
name: "Date/Time",
|
name: "Date / time",
|
||||||
type: FieldType.DATETIME,
|
type: FieldType.DATETIME,
|
||||||
icon: TypeIconMap[FieldType.DATETIME],
|
icon: TypeIconMap[FieldType.DATETIME],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -111,7 +111,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ATTACHMENT_SINGLE: {
|
ATTACHMENT_SINGLE: {
|
||||||
name: "Attachment",
|
name: "Single attachment",
|
||||||
type: FieldType.ATTACHMENT_SINGLE,
|
type: FieldType.ATTACHMENT_SINGLE,
|
||||||
icon: TypeIconMap[FieldType.ATTACHMENT_SINGLE],
|
icon: TypeIconMap[FieldType.ATTACHMENT_SINGLE],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -119,7 +119,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ATTACHMENTS: {
|
ATTACHMENTS: {
|
||||||
name: "Attachment List",
|
name: "Multi attachment",
|
||||||
type: FieldType.ATTACHMENTS,
|
type: FieldType.ATTACHMENTS,
|
||||||
icon: TypeIconMap[FieldType.ATTACHMENTS],
|
icon: TypeIconMap[FieldType.ATTACHMENTS],
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -127,6 +127,14 @@ export const FIELDS = {
|
||||||
presence: false,
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
SIGNATURE_SINGLE: {
|
||||||
|
name: "Signature",
|
||||||
|
type: FieldType.SIGNATURE_SINGLE,
|
||||||
|
icon: "AnnotatePen",
|
||||||
|
constraints: {
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
LINK: {
|
LINK: {
|
||||||
name: "Relationship",
|
name: "Relationship",
|
||||||
type: FieldType.LINK,
|
type: FieldType.LINK,
|
||||||
|
@ -137,7 +145,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AUTO: {
|
AUTO: {
|
||||||
name: "Auto Column",
|
name: "Auto column",
|
||||||
type: FieldType.AUTO,
|
type: FieldType.AUTO,
|
||||||
icon: TypeIconMap[FieldType.AUTO],
|
icon: TypeIconMap[FieldType.AUTO],
|
||||||
constraints: {},
|
constraints: {},
|
||||||
|
@ -158,7 +166,7 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
USER: {
|
USER: {
|
||||||
name: "User",
|
name: "Single user",
|
||||||
type: FieldType.BB_REFERENCE_SINGLE,
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
subtype: BBReferenceFieldSubType.USER,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][
|
icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][
|
||||||
|
@ -166,7 +174,7 @@ export const FIELDS = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
USERS: {
|
USERS: {
|
||||||
name: "User List",
|
name: "Multi user",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE,
|
||||||
subtype: BBReferenceFieldSubType.USER,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USER],
|
icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USER],
|
||||||
|
|
|
@ -830,7 +830,7 @@ export const getActionBindings = (actions, actionId) => {
|
||||||
* @return {{schema: Object, table: Object}}
|
* @return {{schema: Object, table: Object}}
|
||||||
*/
|
*/
|
||||||
export const getSchemaForDatasourcePlus = (resourceId, options) => {
|
export const getSchemaForDatasourcePlus = (resourceId, options) => {
|
||||||
const isViewV2 = resourceId?.includes("view_")
|
const isViewV2 = resourceId?.startsWith("view_")
|
||||||
const datasource = isViewV2
|
const datasource = isViewV2
|
||||||
? {
|
? {
|
||||||
type: "viewV2",
|
type: "viewV2",
|
||||||
|
|
|
@ -19,11 +19,10 @@ export const name = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||||
// exit early, above validator will fail
|
// exit early, above validator will fail
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (currentApp) {
|
|
||||||
// filter out the current app if present
|
|
||||||
apps = apps.filter(app => app.appId !== currentApp.appId)
|
|
||||||
}
|
|
||||||
return !apps
|
return !apps
|
||||||
|
.filter(app => {
|
||||||
|
return app.appId !== currentApp?.appId
|
||||||
|
})
|
||||||
.map(app => app.name)
|
.map(app => app.name)
|
||||||
.some(appName => appName.toLowerCase() === value.toLowerCase())
|
.some(appName => appName.toLowerCase() === value.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||||
import EnterpriseBasicTrialModal from "components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
|
import EnterpriseBasicTrialModal from "components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
|
||||||
|
import UpdateAppTopNav from "components/common/UpdateAppTopNav.svelte"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
|
@ -104,10 +105,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
document.fonts.onloadingdone = e => {
|
|
||||||
builderStore.loadFonts(e.fontfaces)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasSynced && application) {
|
if (!hasSynced && application) {
|
||||||
try {
|
try {
|
||||||
await API.syncApp(application)
|
await API.syncApp(application)
|
||||||
|
@ -148,23 +145,25 @@
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<Tabs {selected} size="M">
|
<Tabs {selected} size="M">
|
||||||
{#key $builderStore?.fonts}
|
{#each $layout.children as { path, title }}
|
||||||
{#each $layout.children as { path, title }}
|
<TourWrap stepKeys={[`builder-${title}-section`]}>
|
||||||
<TourWrap stepKeys={[`builder-${title}-section`]}>
|
<Tab
|
||||||
<Tab
|
quiet
|
||||||
quiet
|
selected={$isActive(path)}
|
||||||
selected={$isActive(path)}
|
on:click={topItemNavigate(path)}
|
||||||
on:click={topItemNavigate(path)}
|
title={capitalise(title)}
|
||||||
title={capitalise(title)}
|
id={`builder-${title}-tab`}
|
||||||
id={`builder-${title}-tab`}
|
/>
|
||||||
/>
|
</TourWrap>
|
||||||
</TourWrap>
|
{/each}
|
||||||
{/each}
|
|
||||||
{/key}
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div class="topcenternav">
|
<div class="topcenternav">
|
||||||
<Heading size="XS">{$appStore.name}</Heading>
|
<div class="app-name">
|
||||||
|
<UpdateAppTopNav {application}>
|
||||||
|
<Heading noPadding size="XS">{$appStore.name}</Heading>
|
||||||
|
</UpdateAppTopNav>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toprightnav">
|
<div class="toprightnav">
|
||||||
<span>
|
<span>
|
||||||
|
@ -253,7 +252,6 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding: 0px var(--spacing-m);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topleftnav {
|
.topleftnav {
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"multifieldselect",
|
"multifieldselect",
|
||||||
"s3upload",
|
"s3upload",
|
||||||
"codescanner",
|
"codescanner",
|
||||||
|
"signaturesinglefield",
|
||||||
"bbreferencesinglefield",
|
"bbreferencesinglefield",
|
||||||
"bbreferencefield"
|
"bbreferencefield"
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,30 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Layout, Divider, Heading, Body } from "@budibase/bbui"
|
||||||
Layout,
|
import UpdateAppForm from "components/common/UpdateAppForm.svelte"
|
||||||
Divider,
|
|
||||||
Heading,
|
|
||||||
Body,
|
|
||||||
Button,
|
|
||||||
Label,
|
|
||||||
Modal,
|
|
||||||
Icon,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { AppStatus } from "constants"
|
|
||||||
import { appStore, initialise } from "stores/builder"
|
|
||||||
import { appsStore } from "stores/portal"
|
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
|
||||||
import { API } from "api"
|
|
||||||
|
|
||||||
let updatingModal
|
|
||||||
|
|
||||||
$: filteredApps = $appsStore.apps.filter(app => app.devId == $appStore.appId)
|
|
||||||
$: app = filteredApps.length ? filteredApps[0] : {}
|
|
||||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
|
||||||
|
|
||||||
const initialiseApp = async () => {
|
|
||||||
const applicationPkg = await API.fetchAppPackage($appStore.appId)
|
|
||||||
await initialise(applicationPkg)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
|
@ -33,61 +9,5 @@
|
||||||
<Body>Edit your app's name and URL</Body>
|
<Body>Edit your app's name and URL</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<UpdateAppForm />
|
||||||
<Layout noPadding gap="XXS">
|
|
||||||
<Label size="L">Name</Label>
|
|
||||||
<Body>{$appStore?.name}</Body>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<Label size="L">Icon</Label>
|
|
||||||
<div class="icon">
|
|
||||||
<Icon
|
|
||||||
size="L"
|
|
||||||
name={$appStore?.icon?.name || "Apps"}
|
|
||||||
color={$appStore?.icon?.color}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Layout noPadding gap="XXS">
|
|
||||||
<Label size="L">URL</Label>
|
|
||||||
<Body>{$appStore.url}</Body>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
cta
|
|
||||||
on:click={() => {
|
|
||||||
updatingModal.show()
|
|
||||||
}}
|
|
||||||
disabled={appDeployed}
|
|
||||||
tooltip={appDeployed
|
|
||||||
? "You must unpublish your app to make changes"
|
|
||||||
: null}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={updatingModal} padding={false} width="600px">
|
|
||||||
<UpdateAppModal
|
|
||||||
app={{
|
|
||||||
name: $appStore.name,
|
|
||||||
url: $appStore.url,
|
|
||||||
icon: $appStore.icon,
|
|
||||||
appId: $appStore.appId,
|
|
||||||
}}
|
|
||||||
onUpdateComplete={async () => {
|
|
||||||
await initialiseApp()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.icon {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { isActive, redirect, goto, url } from "@roxi/routify"
|
import { isActive, redirect, goto, url } from "@roxi/routify"
|
||||||
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
||||||
import { organisation, auth, menu, appsStore, licensing } from "stores/portal"
|
import {
|
||||||
|
organisation,
|
||||||
|
auth,
|
||||||
|
menu,
|
||||||
|
appsStore,
|
||||||
|
licensing,
|
||||||
|
admin,
|
||||||
|
} from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
||||||
import MobileMenu from "./_components/MobileMenu.svelte"
|
import MobileMenu from "./_components/MobileMenu.svelte"
|
||||||
|
@ -20,6 +27,7 @@
|
||||||
$: $url(), updateActiveTab($menu)
|
$: $url(), updateActiveTab($menu)
|
||||||
$: isOnboarding =
|
$: isOnboarding =
|
||||||
!$appsStore.apps.length && sdk.users.hasBuilderPermissions($auth.user)
|
!$appsStore.apps.length && sdk.users.hasBuilderPermissions($auth.user)
|
||||||
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
const updateActiveTab = menu => {
|
const updateActiveTab = menu => {
|
||||||
for (let entry of menu) {
|
for (let entry of menu) {
|
||||||
|
@ -38,8 +46,7 @@
|
||||||
const showFreeTrialBanner = () => {
|
const showFreeTrialBanner = () => {
|
||||||
return (
|
return (
|
||||||
$licensing.license?.plan?.type ===
|
$licensing.license?.plan?.type ===
|
||||||
Constants.PlanType.ENTERPRISE_BASIC_TRIAL &&
|
Constants.PlanType.ENTERPRISE_BASIC_TRIAL && isOwner
|
||||||
sdk.users.isAdmin($auth.user)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ const automationActions = store => ({
|
||||||
steps: [],
|
steps: [],
|
||||||
trigger,
|
trigger,
|
||||||
},
|
},
|
||||||
|
disabled: false,
|
||||||
}
|
}
|
||||||
const response = await store.actions.save(automation)
|
const response = await store.actions.save(automation)
|
||||||
await store.actions.fetch()
|
await store.actions.fetch()
|
||||||
|
@ -134,6 +135,28 @@ const automationActions = store => ({
|
||||||
})
|
})
|
||||||
await store.actions.fetch()
|
await store.actions.fetch()
|
||||||
},
|
},
|
||||||
|
toggleDisabled: async automationId => {
|
||||||
|
let automation
|
||||||
|
try {
|
||||||
|
automation = store.actions.getDefinition(automationId)
|
||||||
|
if (!automation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
automation.disabled = !automation.disabled
|
||||||
|
await store.actions.save(automation)
|
||||||
|
notifications.success(
|
||||||
|
`Automation ${
|
||||||
|
automation.disabled ? "enabled" : "disabled"
|
||||||
|
} successfully`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(
|
||||||
|
`Error ${
|
||||||
|
automation && automation.disabled ? "enabling" : "disabling"
|
||||||
|
} automation`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
updateBlockInputs: async (block, data) => {
|
updateBlockInputs: async (block, data) => {
|
||||||
// Create new modified block
|
// Create new modified block
|
||||||
let newBlock = {
|
let newBlock = {
|
||||||
|
|
|
@ -14,7 +14,6 @@ export const INITIAL_BUILDER_STATE = {
|
||||||
tourKey: null,
|
tourKey: null,
|
||||||
tourStepKey: null,
|
tourStepKey: null,
|
||||||
hoveredComponentId: null,
|
hoveredComponentId: null,
|
||||||
fonts: null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BuilderStore extends BudiStore {
|
export class BuilderStore extends BudiStore {
|
||||||
|
@ -37,16 +36,6 @@ export class BuilderStore extends BudiStore {
|
||||||
this.websocket
|
this.websocket
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFonts(fontFaces) {
|
|
||||||
const ff = fontFaces.map(
|
|
||||||
fontFace => `${fontFace.family}-${fontFace.weight}`
|
|
||||||
)
|
|
||||||
this.update(state => ({
|
|
||||||
...state,
|
|
||||||
fonts: [...(state.fonts || []), ...ff],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
init(app) {
|
init(app) {
|
||||||
if (!app?.appId) {
|
if (!app?.appId) {
|
||||||
console.error("BuilderStore: No appId supplied for websocket")
|
console.error("BuilderStore: No appId supplied for websocket")
|
||||||
|
|
|
@ -131,7 +131,7 @@ export class AppsStore extends BudiStore {
|
||||||
if (updatedAppIndex !== -1) {
|
if (updatedAppIndex !== -1) {
|
||||||
let updatedApp = state.apps[updatedAppIndex]
|
let updatedApp = state.apps[updatedAppIndex]
|
||||||
updatedApp = { ...updatedApp, ...value }
|
updatedApp = { ...updatedApp, ...value }
|
||||||
state.apps = state.apps.splice(updatedAppIndex, 1, updatedApp)
|
state.apps.splice(updatedAppIndex, 1, updatedApp)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
process.env.DISABLE_PINO_LOGGER = "1"
|
||||||
process.env.NO_JS = "1"
|
process.env.NO_JS = "1"
|
||||||
process.env.JS_BCRYPT = "1"
|
process.env.JS_BCRYPT = "1"
|
||||||
process.env.DISABLE_JWT_WARNING = "1"
|
process.env.DISABLE_JWT_WARNING = "1"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
process.env.DISABLE_PINO_LOGGER = "1"
|
// have to import this before anything else
|
||||||
import "./environment"
|
import "./environment"
|
||||||
import { getCommands } from "./options"
|
import { getCommands } from "./options"
|
||||||
import { Command } from "commander"
|
import { Command } from "commander"
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
dir="$(dirname -- "$(readlink -f "${BASH_SOURCE}")")"
|
dir="$(dirname -- "$(readlink -f "${BASH_SOURCE}")")"
|
||||||
${dir}/node_modules/ts-node/dist/bin.js ${dir}/src/index.ts $@
|
${dir}/../../node_modules/ts-node/dist/bin.js ${dir}/src/index.ts $@
|
||||||
|
|
|
@ -2868,6 +2868,14 @@
|
||||||
"type": "plainText",
|
"type": "plainText",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"key": "label"
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Initial width",
|
||||||
|
"key": "width",
|
||||||
|
"placeholder": "Auto",
|
||||||
|
"min": 80,
|
||||||
|
"max": 9999
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -4229,6 +4237,55 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"signaturesinglefield": {
|
||||||
|
"name": "Signature",
|
||||||
|
"icon": "AnnotatePen",
|
||||||
|
"styles": ["size"],
|
||||||
|
"size": {
|
||||||
|
"width": 400,
|
||||||
|
"height": 50
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/signature_single",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Help text",
|
||||||
|
"key": "helpText"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Disabled",
|
||||||
|
"key": "disabled",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/signature_single",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"embeddedmap": {
|
"embeddedmap": {
|
||||||
"name": "Embedded Map",
|
"name": "Embedded Map",
|
||||||
"icon": "Location",
|
"icon": "Location",
|
||||||
|
@ -4494,7 +4551,7 @@
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "validation/attachment",
|
"type": "validation/attachment_single",
|
||||||
"label": "Validation",
|
"label": "Validation",
|
||||||
"key": "validation"
|
"key": "validation"
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,7 +33,8 @@
|
||||||
"sanitize-html": "^2.7.0",
|
"sanitize-html": "^2.7.0",
|
||||||
"screenfull": "^6.0.1",
|
"screenfull": "^6.0.1",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^4.0.1"
|
"svelte-spa-router": "^4.0.1",
|
||||||
|
"atrament": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-alias": "^5.1.0",
|
"@rollup/plugin-alias": "^5.1.0",
|
||||||
|
|
|
@ -193,6 +193,9 @@
|
||||||
$: pad = pad || (interactive && hasChildren && inDndPath)
|
$: pad = pad || (interactive && hasChildren && inDndPath)
|
||||||
$: $dndIsDragging, (pad = false)
|
$: $dndIsDragging, (pad = false)
|
||||||
|
|
||||||
|
$: currentTheme = $context?.device?.theme
|
||||||
|
$: darkMode = !currentTheme?.includes("light")
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: store.set({
|
$: store.set({
|
||||||
id,
|
id,
|
||||||
|
@ -222,6 +225,7 @@
|
||||||
parent: id,
|
parent: id,
|
||||||
ancestors: [...($component?.ancestors ?? []), instance._component],
|
ancestors: [...($component?.ancestors ?? []), instance._component],
|
||||||
path: [...($component?.path ?? []), id],
|
path: [...($component?.path ?? []), id],
|
||||||
|
darkMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialise = (instance, force = false) => {
|
const initialise = (instance, force = false) => {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
const { environmentStore } = getContext("sdk")
|
||||||
const {
|
const {
|
||||||
styleable,
|
styleable,
|
||||||
API,
|
API,
|
||||||
|
@ -36,12 +37,16 @@
|
||||||
|
|
||||||
let grid
|
let grid
|
||||||
let gridContext
|
let gridContext
|
||||||
|
let minHeight = 0
|
||||||
|
|
||||||
|
$: currentTheme = $context?.device?.theme
|
||||||
|
$: darkMode = !currentTheme?.includes("light")
|
||||||
$: parsedColumns = getParsedColumns(columns)
|
$: parsedColumns = getParsedColumns(columns)
|
||||||
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
|
$: columnWhitelist = parsedColumns.filter(x => x.active).map(x => x.field)
|
||||||
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
$: schemaOverrides = getSchemaOverrides(parsedColumns)
|
||||||
$: enrichedButtons = enrichButtons(buttons)
|
$: enrichedButtons = enrichButtons(buttons)
|
||||||
$: selectedRows = deriveSelectedRows(gridContext)
|
$: selectedRows = deriveSelectedRows(gridContext)
|
||||||
|
$: styles = patchStyles($component.styles, minHeight)
|
||||||
$: data = { selectedRows: $selectedRows }
|
$: data = { selectedRows: $selectedRows }
|
||||||
$: actions = [
|
$: actions = [
|
||||||
{
|
{
|
||||||
|
@ -50,8 +55,6 @@
|
||||||
metadata: { dataSource: table },
|
metadata: { dataSource: table },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
$: height = $component.styles?.normal?.height || "408px"
|
|
||||||
$: styles = getSanitisedStyles($component.styles)
|
|
||||||
|
|
||||||
// Provide additional data context for live binding eval
|
// Provide additional data context for live binding eval
|
||||||
export const getAdditionalDataContext = () => {
|
export const getAdditionalDataContext = () => {
|
||||||
|
@ -84,9 +87,11 @@
|
||||||
|
|
||||||
const getSchemaOverrides = columns => {
|
const getSchemaOverrides = columns => {
|
||||||
let overrides = {}
|
let overrides = {}
|
||||||
columns.forEach(column => {
|
columns.forEach((column, idx) => {
|
||||||
overrides[column.field] = {
|
overrides[column.field] = {
|
||||||
displayName: column.label,
|
displayName: column.label,
|
||||||
|
width: column.width,
|
||||||
|
order: idx,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return overrides
|
return overrides
|
||||||
|
@ -128,49 +133,50 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSanitisedStyles = styles => {
|
const patchStyles = (styles, minHeight) => {
|
||||||
return {
|
return {
|
||||||
...styles,
|
...styles,
|
||||||
normal: {
|
normal: {
|
||||||
...styles?.normal,
|
...styles?.normal,
|
||||||
height: undefined,
|
"min-height": `${minHeight}px`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
gridContext = grid.getContext()
|
gridContext = grid.getContext()
|
||||||
|
gridContext.minHeight.subscribe($height => (minHeight = $height))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={styles} class:in-builder={$builderStore.inBuilder}>
|
<div use:styleable={styles} class:in-builder={$builderStore.inBuilder}>
|
||||||
<span style="--height:{height};">
|
<Grid
|
||||||
<Grid
|
bind:this={grid}
|
||||||
bind:this={grid}
|
datasource={table}
|
||||||
datasource={table}
|
{API}
|
||||||
{API}
|
{stripeRows}
|
||||||
{stripeRows}
|
{quiet}
|
||||||
{quiet}
|
{darkMode}
|
||||||
{initialFilter}
|
{initialFilter}
|
||||||
{initialSortColumn}
|
{initialSortColumn}
|
||||||
{initialSortOrder}
|
{initialSortOrder}
|
||||||
{fixedRowHeight}
|
{fixedRowHeight}
|
||||||
{columnWhitelist}
|
{columnWhitelist}
|
||||||
{schemaOverrides}
|
{schemaOverrides}
|
||||||
canAddRows={allowAddRows}
|
canAddRows={allowAddRows}
|
||||||
canEditRows={allowEditRows}
|
canEditRows={allowEditRows}
|
||||||
canDeleteRows={allowDeleteRows}
|
canDeleteRows={allowDeleteRows}
|
||||||
canEditColumns={false}
|
canEditColumns={false}
|
||||||
canExpandRows={false}
|
canExpandRows={false}
|
||||||
canSaveSchema={false}
|
canSaveSchema={false}
|
||||||
canSelectRows={true}
|
canSelectRows={true}
|
||||||
showControls={false}
|
showControls={false}
|
||||||
notifySuccess={notificationStore.actions.success}
|
notifySuccess={notificationStore.actions.success}
|
||||||
notifyError={notificationStore.actions.error}
|
notifyError={notificationStore.actions.error}
|
||||||
buttons={enrichedButtons}
|
buttons={enrichedButtons}
|
||||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
isCloud={$environmentStore.cloud}
|
||||||
/>
|
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||||
</span>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Provider {data} {actions} />
|
<Provider {data} {actions} />
|
||||||
|
@ -183,14 +189,9 @@
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 410px;
|
||||||
}
|
}
|
||||||
div.in-builder :global(*) {
|
div.in-builder :global(*) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
span {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
span :global(.grid) {
|
|
||||||
height: var(--height);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
[FieldType.BOOLEAN]: "booleanfield",
|
[FieldType.BOOLEAN]: "booleanfield",
|
||||||
[FieldType.LONGFORM]: "longformfield",
|
[FieldType.LONGFORM]: "longformfield",
|
||||||
[FieldType.DATETIME]: "datetimefield",
|
[FieldType.DATETIME]: "datetimefield",
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: "signaturesinglefield",
|
||||||
[FieldType.ATTACHMENTS]: "attachmentfield",
|
[FieldType.ATTACHMENTS]: "attachmentfield",
|
||||||
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
[FieldType.ATTACHMENT_SINGLE]: "attachmentsinglefield",
|
||||||
[FieldType.LINK]: "relationshipfield",
|
[FieldType.LINK]: "relationshipfield",
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
|
||||||
const { API, notificationStore } = getContext("sdk")
|
const { API, notificationStore, environmentStore } = getContext("sdk")
|
||||||
const formContext = getContext("form")
|
const formContext = getContext("form")
|
||||||
const BYTES_IN_MB = 1000000
|
const BYTES_IN_MB = 1000000
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
handleFileTooLarge={$environmentStore.cloud ? handleFileTooLarge : null}
|
||||||
{handleTooManyFiles}
|
{handleTooManyFiles}
|
||||||
{maximum}
|
{maximum}
|
||||||
{extensions}
|
{extensions}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
<script>
|
||||||
|
import { CoreSignature, ActionButton } from "@budibase/bbui"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import { SignatureModal } from "@budibase/frontend-core/src/components"
|
||||||
|
|
||||||
|
export let field
|
||||||
|
export let label
|
||||||
|
export let disabled = false
|
||||||
|
export let readonly = false
|
||||||
|
export let validation
|
||||||
|
export let onChange
|
||||||
|
export let span
|
||||||
|
export let helpText = null
|
||||||
|
|
||||||
|
let fieldState
|
||||||
|
let fieldApi
|
||||||
|
let fieldSchema
|
||||||
|
let modal
|
||||||
|
|
||||||
|
const { API, notificationStore, builderStore } = getContext("sdk")
|
||||||
|
const context = getContext("context")
|
||||||
|
const formContext = getContext("form")
|
||||||
|
|
||||||
|
const saveSignature = async canvas => {
|
||||||
|
try {
|
||||||
|
const signatureFile = canvas.toFile()
|
||||||
|
let updateValue
|
||||||
|
|
||||||
|
if (signatureFile) {
|
||||||
|
let attachRequest = new FormData()
|
||||||
|
attachRequest.append("file", signatureFile)
|
||||||
|
|
||||||
|
const resp = await API.uploadAttachment({
|
||||||
|
data: attachRequest,
|
||||||
|
tableId: formContext?.dataSource?.tableId,
|
||||||
|
})
|
||||||
|
const [signatureAttachment] = resp
|
||||||
|
updateValue = signatureAttachment
|
||||||
|
} else {
|
||||||
|
updateValue = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed = fieldApi.setValue(updateValue)
|
||||||
|
if (onChange && changed) {
|
||||||
|
onChange({ value: updateValue })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notificationStore.actions.error(
|
||||||
|
`There was a problem saving your signature`
|
||||||
|
)
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSignature = async () => {
|
||||||
|
const changed = fieldApi.setValue(null)
|
||||||
|
if (onChange && changed) {
|
||||||
|
onChange({ value: null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: currentTheme = $context?.device?.theme
|
||||||
|
$: darkMode = !currentTheme?.includes("light")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SignatureModal
|
||||||
|
onConfirm={saveSignature}
|
||||||
|
title={label || fieldSchema?.name || ""}
|
||||||
|
value={fieldState?.value}
|
||||||
|
{darkMode}
|
||||||
|
bind:this={modal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
{label}
|
||||||
|
{field}
|
||||||
|
{disabled}
|
||||||
|
{readonly}
|
||||||
|
{validation}
|
||||||
|
{span}
|
||||||
|
{helpText}
|
||||||
|
type="signature_single"
|
||||||
|
bind:fieldState
|
||||||
|
bind:fieldApi
|
||||||
|
bind:fieldSchema
|
||||||
|
defaultValue={[]}
|
||||||
|
>
|
||||||
|
{#if fieldState}
|
||||||
|
{#if (Array.isArray(fieldState?.value) && !fieldState?.value?.length) || !fieldState?.value}
|
||||||
|
<ActionButton
|
||||||
|
fullWidth
|
||||||
|
disabled={fieldState.disabled}
|
||||||
|
on:click={() => {
|
||||||
|
if (!$builderStore.inBuilder) {
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add signature
|
||||||
|
</ActionButton>
|
||||||
|
{:else}
|
||||||
|
<div class="signature-field">
|
||||||
|
<CoreSignature
|
||||||
|
{darkMode}
|
||||||
|
disabled={$builderStore.inBuilder || fieldState.disabled}
|
||||||
|
editable={false}
|
||||||
|
value={fieldState?.value}
|
||||||
|
on:clear={deleteSignature}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.signature-field {
|
||||||
|
min-height: 50px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: var(--spectrum-alias-border-size-thin)
|
||||||
|
var(--spectrum-alias-border-color) solid;
|
||||||
|
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -16,5 +16,6 @@ export { default as formstep } from "./FormStep.svelte"
|
||||||
export { default as jsonfield } from "./JSONField.svelte"
|
export { default as jsonfield } from "./JSONField.svelte"
|
||||||
export { default as s3upload } from "./S3Upload.svelte"
|
export { default as s3upload } from "./S3Upload.svelte"
|
||||||
export { default as codescanner } from "./CodeScannerField.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 bbreferencefield } from "./BBReferenceField.svelte"
|
||||||
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
|
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
|
||||||
|
|
|
@ -200,6 +200,17 @@ const parseType = (value, type) => {
|
||||||
return value
|
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
|
// Parse links, treating no elements as null
|
||||||
if (type === FieldTypes.LINK) {
|
if (type === FieldTypes.LINK) {
|
||||||
if (!Array.isArray(value) || !value.length) {
|
if (!Array.isArray(value) || !value.length) {
|
||||||
|
@ -246,10 +257,8 @@ const maxLengthHandler = (value, rule) => {
|
||||||
// Evaluates a max file size (MB) constraint
|
// Evaluates a max file size (MB) constraint
|
||||||
const maxFileSizeHandler = (value, rule) => {
|
const maxFileSizeHandler = (value, rule) => {
|
||||||
const limit = parseType(rule.value, "number")
|
const limit = parseType(rule.value, "number")
|
||||||
return (
|
const check = attachment => attachment.size / 1000000 > limit
|
||||||
value == null ||
|
return value == null || !(value?.key ? check(value) : value.some(check))
|
||||||
!value.some(attachment => attachment.size / 1000000 > limit)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluates a max total upload size (MB) constraint
|
// Evaluates a max total upload size (MB) constraint
|
||||||
|
@ -257,8 +266,11 @@ const maxUploadSizeHandler = (value, rule) => {
|
||||||
const limit = parseType(rule.value, "number")
|
const limit = parseType(rule.value, "number")
|
||||||
return (
|
return (
|
||||||
value == null ||
|
value == null ||
|
||||||
value.reduce((acc, currentItem) => acc + currentItem.size, 0) / 1000000 <=
|
(value?.key
|
||||||
limit
|
? value.size / 1000000 <= limit
|
||||||
|
: value.reduce((acc, currentItem) => acc + currentItem.size, 0) /
|
||||||
|
1000000 <=
|
||||||
|
limit)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent, Body, CoreSignature } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let onConfirm = () => {}
|
||||||
|
export let value
|
||||||
|
export let title
|
||||||
|
export let darkMode
|
||||||
|
|
||||||
|
export const show = () => {
|
||||||
|
edited = false
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
let modal
|
||||||
|
let canvas
|
||||||
|
let edited = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<ModalContent
|
||||||
|
showConfirmButton
|
||||||
|
showCancelButton={false}
|
||||||
|
showCloseIcon={false}
|
||||||
|
custom
|
||||||
|
disabled={!edited}
|
||||||
|
showDivider={false}
|
||||||
|
onConfirm={() => {
|
||||||
|
onConfirm(canvas)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div slot="header">
|
||||||
|
<Body>{title}</Body>
|
||||||
|
</div>
|
||||||
|
<div class="signature-wrap modal">
|
||||||
|
<CoreSignature
|
||||||
|
{darkMode}
|
||||||
|
{value}
|
||||||
|
saveIcon={false}
|
||||||
|
bind:this={canvas}
|
||||||
|
on:update={() => {
|
||||||
|
edited = true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.signature-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -8,11 +8,10 @@
|
||||||
export let onChange
|
export let onChange
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX = false
|
|
||||||
export let schema
|
export let schema
|
||||||
export let maximum
|
export let maximum
|
||||||
|
|
||||||
const { API, notifications } = getContext("grid")
|
const { API, notifications, props } = getContext("grid")
|
||||||
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
@ -92,13 +91,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover
|
<GridPopover open={isOpen} {anchor} maxHeight={null} on:close={close}>
|
||||||
open={isOpen}
|
|
||||||
{anchor}
|
|
||||||
{invertX}
|
|
||||||
maxHeight={null}
|
|
||||||
on:close={close}
|
|
||||||
>
|
|
||||||
<div class="dropzone">
|
<div class="dropzone">
|
||||||
<Dropzone
|
<Dropzone
|
||||||
{value}
|
{value}
|
||||||
|
@ -106,7 +99,7 @@
|
||||||
on:change={e => onChange(e.detail)}
|
on:change={e => onChange(e.detail)}
|
||||||
maximum={maximum || schema.constraints?.length?.maximum}
|
maximum={maximum || schema.constraints?.length?.maximum}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
handleFileTooLarge={$props.isCloud ? handleFileTooLarge : null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</GridPopover>
|
</GridPopover>
|
||||||
|
|
|
@ -18,8 +18,6 @@
|
||||||
export let row
|
export let row
|
||||||
export let cellId
|
export let cellId
|
||||||
export let updateValue = rows.actions.updateValue
|
export let updateValue = rows.actions.updateValue
|
||||||
export let invertX = false
|
|
||||||
export let invertY = false
|
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
export let hidden = false
|
export let hidden = false
|
||||||
|
|
||||||
|
@ -93,8 +91,6 @@
|
||||||
onChange={cellAPI.setValue}
|
onChange={cellAPI.setValue}
|
||||||
{focused}
|
{focused}
|
||||||
{readonly}
|
{readonly}
|
||||||
{invertY}
|
|
||||||
{invertX}
|
|
||||||
{contentLines}
|
{contentLines}
|
||||||
/>
|
/>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
export let focused = false
|
export let focused = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX = false
|
|
||||||
|
|
||||||
let isOpen
|
let isOpen
|
||||||
let anchor
|
let anchor
|
||||||
|
@ -111,7 +110,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover {anchor} {invertX} maxHeight={null} on:close={close}>
|
<GridPopover {anchor} maxHeight={null} on:close={close}>
|
||||||
<CoreDatePickerPopoverContents
|
<CoreDatePickerPopoverContents
|
||||||
value={parsedValue}
|
value={parsedValue}
|
||||||
useKeyboardShortcuts={false}
|
useKeyboardShortcuts={false}
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
subscribe,
|
subscribe,
|
||||||
config,
|
config,
|
||||||
ui,
|
ui,
|
||||||
columns,
|
|
||||||
definition,
|
definition,
|
||||||
datasource,
|
datasource,
|
||||||
schema,
|
schema,
|
||||||
|
@ -158,17 +157,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeDisplayColumn = () => {
|
const makeDisplayColumn = () => {
|
||||||
columns.actions.changePrimaryDisplay(column.name)
|
datasource.actions.changePrimaryDisplay(column.name)
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideColumn = () => {
|
const hideColumn = () => {
|
||||||
columns.update(state => {
|
datasource.actions.addSchemaMutation(column.name, { visible: false })
|
||||||
const index = state.findIndex(col => col.name === column.name)
|
datasource.actions.saveSchemaMutations()
|
||||||
state[index].visible = false
|
|
||||||
return state.slice()
|
|
||||||
})
|
|
||||||
columns.actions.saveChanges()
|
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,7 +381,7 @@
|
||||||
>
|
>
|
||||||
Hide column
|
Hide column
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{#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}
|
||||||
<MenuItem icon="User" on:click={openMigrationModal}>
|
<MenuItem icon="User" on:click={openMigrationModal}>
|
||||||
Migrate to user column
|
Migrate to user column
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
export let onChange
|
export let onChange
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX = false
|
|
||||||
|
|
||||||
let textarea
|
let textarea
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
@ -67,7 +66,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover {anchor} {invertX} on:close={close}>
|
<GridPopover {anchor} on:close={close}>
|
||||||
<textarea
|
<textarea
|
||||||
bind:this={textarea}
|
bind:this={textarea}
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
export let multi = false
|
export let multi = false
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
export let invertX
|
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
|
@ -120,7 +119,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover {anchor} {invertX} on:close={close}>
|
<GridPopover {anchor} on:close={close}>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
{#each options as option, idx}
|
{#each options as option, idx}
|
||||||
{@const color = optionColors[option] || getOptionColor(option)}
|
{@const color = optionColors[option] || getOptionColor(option)}
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
export let focused
|
export let focused
|
||||||
export let schema
|
export let schema
|
||||||
export let onChange
|
export let onChange
|
||||||
export let invertX = false
|
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
export let searchFunction = API.searchTable
|
export let searchFunction = API.searchTable
|
||||||
export let primaryDisplay
|
export let primaryDisplay
|
||||||
|
@ -275,7 +274,7 @@
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<GridPopover open={isOpen} {anchor} {invertX} on:close={close}>
|
<GridPopover open={isOpen} {anchor} on:close={close}>
|
||||||
<div class="dropdown" on:wheel|stopPropagation>
|
<div class="dropdown" on:wheel|stopPropagation>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, getContext } from "svelte"
|
||||||
|
import { SignatureModal } from "@budibase/frontend-core/src/components"
|
||||||
|
import { CoreSignature, ActionButton } from "@budibase/bbui"
|
||||||
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
|
||||||
|
export let schema
|
||||||
|
export let value
|
||||||
|
export let focused = false
|
||||||
|
export let onChange
|
||||||
|
export let readonly = false
|
||||||
|
export let api
|
||||||
|
|
||||||
|
const { API, notifications, props } = getContext("grid")
|
||||||
|
|
||||||
|
let isOpen = false
|
||||||
|
let modal
|
||||||
|
let anchor
|
||||||
|
|
||||||
|
$: editable = focused && !readonly
|
||||||
|
$: {
|
||||||
|
if (!focused) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = () => {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
isOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
isOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSignature = async () => {
|
||||||
|
onChange(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSignature = async sigCanvas => {
|
||||||
|
const signatureFile = sigCanvas.toFile()
|
||||||
|
|
||||||
|
let attachRequest = new FormData()
|
||||||
|
attachRequest.append("file", signatureFile)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadReq = await API.uploadBuilderAttachment(attachRequest)
|
||||||
|
const [signatureAttachment] = uploadReq
|
||||||
|
onChange(signatureAttachment)
|
||||||
|
} catch (error) {
|
||||||
|
$notifications.error(error.message || "Failed to save signature")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
api = {
|
||||||
|
focus: () => open(),
|
||||||
|
blur: () => close(),
|
||||||
|
isActive: () => isOpen,
|
||||||
|
onKeyDown,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div
|
||||||
|
class="signature-cell"
|
||||||
|
class:light={!$props?.darkMode}
|
||||||
|
class:editable
|
||||||
|
bind:this={anchor}
|
||||||
|
on:click={editable ? open : null}
|
||||||
|
>
|
||||||
|
{#if value?.url}
|
||||||
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
|
<img src={value?.url} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SignatureModal
|
||||||
|
onConfirm={saveSignature}
|
||||||
|
title={schema?.name}
|
||||||
|
{value}
|
||||||
|
darkMode={$props.darkMode}
|
||||||
|
bind:this={modal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<GridPopover open={isOpen} {anchor} maxHeight={null} on:close={close}>
|
||||||
|
<div class="signature" class:empty={!value}>
|
||||||
|
{#if value?.key}
|
||||||
|
<div class="signature-wrap">
|
||||||
|
<CoreSignature
|
||||||
|
darkMode={$props.darkMode}
|
||||||
|
editable={false}
|
||||||
|
{value}
|
||||||
|
on:change={saveSignature}
|
||||||
|
on:clear={deleteSignature}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="add-signature">
|
||||||
|
<ActionButton
|
||||||
|
fullWidth
|
||||||
|
on:click={() => {
|
||||||
|
modal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add signature
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</GridPopover>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.signature {
|
||||||
|
min-width: 320px;
|
||||||
|
padding: var(--cell-padding);
|
||||||
|
background: var(--grid-background-alt);
|
||||||
|
border: var(--cell-border);
|
||||||
|
}
|
||||||
|
.signature.empty {
|
||||||
|
width: 100%;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
.signature-cell.light img {
|
||||||
|
-webkit-filter: invert(100%);
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
|
.signature-cell {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
max-width: 320px;
|
||||||
|
padding-left: var(--cell-padding);
|
||||||
|
padding-right: var(--cell-padding);
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-self: stretch;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.signature-cell.editable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.signature-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,7 +3,7 @@
|
||||||
import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
|
import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
|
||||||
import { getColumnIcon } from "../lib/utils"
|
import { getColumnIcon } from "../lib/utils"
|
||||||
|
|
||||||
const { columns, stickyColumn, dispatch } = getContext("grid")
|
const { columns, datasource, stickyColumn, dispatch } = getContext("grid")
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
let anchor
|
let anchor
|
||||||
|
@ -11,36 +11,20 @@
|
||||||
$: anyHidden = $columns.some(col => !col.visible)
|
$: anyHidden = $columns.some(col => !col.visible)
|
||||||
$: text = getText($columns)
|
$: text = getText($columns)
|
||||||
|
|
||||||
const toggleVisibility = async (column, visible) => {
|
const toggleColumn = async (column, visible) => {
|
||||||
columns.update(state => {
|
datasource.actions.addSchemaMutation(column.name, { visible })
|
||||||
const index = state.findIndex(col => col.name === column.name)
|
await datasource.actions.saveSchemaMutations()
|
||||||
state[index].visible = visible
|
|
||||||
return state.slice()
|
|
||||||
})
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
dispatch(visible ? "show-column" : "hide-column")
|
dispatch(visible ? "show-column" : "hide-column")
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAll = async () => {
|
const toggleAll = async visible => {
|
||||||
columns.update(state => {
|
let mutations = {}
|
||||||
return state.map(col => ({
|
$columns.forEach(column => {
|
||||||
...col,
|
mutations[column.name] = { visible }
|
||||||
visible: true,
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
await columns.actions.saveChanges()
|
datasource.actions.addSchemaMutations(mutations)
|
||||||
dispatch("show-column")
|
await datasource.actions.saveSchemaMutations()
|
||||||
}
|
dispatch(visible ? "show-column" : "hide-column")
|
||||||
|
|
||||||
const hideAll = async () => {
|
|
||||||
columns.update(state => {
|
|
||||||
return state.map(col => ({
|
|
||||||
...col,
|
|
||||||
visible: false,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
dispatch("hide-column")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getText = columns => {
|
const getText = columns => {
|
||||||
|
@ -80,14 +64,14 @@
|
||||||
<Toggle
|
<Toggle
|
||||||
size="S"
|
size="S"
|
||||||
value={column.visible}
|
value={column.visible}
|
||||||
on:change={e => toggleVisibility(column, e.detail)}
|
on:change={e => toggleColumn(column, e.detail)}
|
||||||
disabled={column.primaryDisplay}
|
disabled={column.primaryDisplay}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<ActionButton on:click={showAll}>Show all</ActionButton>
|
<ActionButton on:click={() => toggleAll(true)}>Show all</ActionButton>
|
||||||
<ActionButton on:click={hideAll}>Hide all</ActionButton>
|
<ActionButton on:click={() => toggleAll(false)}>Hide all</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { setContext, onMount } from "svelte"
|
import { setContext, onMount } from "svelte"
|
||||||
import { writable } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { clickOutside, ProgressCircle } from "@budibase/bbui"
|
import { clickOutside, ProgressCircle } from "@budibase/bbui"
|
||||||
import { createEventManagers } from "../lib/events"
|
import { createEventManagers } from "../lib/events"
|
||||||
|
@ -54,6 +54,8 @@
|
||||||
export let notifySuccess = null
|
export let notifySuccess = null
|
||||||
export let notifyError = null
|
export let notifyError = null
|
||||||
export let buttons = null
|
export let buttons = null
|
||||||
|
export let darkMode
|
||||||
|
export let isCloud = null
|
||||||
|
|
||||||
// Unique identifier for DOM nodes inside this instance
|
// Unique identifier for DOM nodes inside this instance
|
||||||
const gridID = `grid-${Math.random().toString().slice(2)}`
|
const gridID = `grid-${Math.random().toString().slice(2)}`
|
||||||
|
@ -108,9 +110,16 @@
|
||||||
notifySuccess,
|
notifySuccess,
|
||||||
notifyError,
|
notifyError,
|
||||||
buttons,
|
buttons,
|
||||||
|
darkMode,
|
||||||
|
isCloud,
|
||||||
})
|
})
|
||||||
$: minHeight =
|
|
||||||
Padding + SmallRowHeight + $rowHeight + (showControls ? ControlsHeight : 0)
|
// Derive min height and make available in context
|
||||||
|
const minHeight = derived(rowHeight, $height => {
|
||||||
|
const heightForControls = showControls ? ControlsHeight : 0
|
||||||
|
return Padding + SmallRowHeight + $height + heightForControls
|
||||||
|
})
|
||||||
|
context = { ...context, minHeight }
|
||||||
|
|
||||||
// Set context for children to consume
|
// Set context for children to consume
|
||||||
setContext("grid", context)
|
setContext("grid", context)
|
||||||
|
@ -136,7 +145,7 @@
|
||||||
class:quiet
|
class:quiet
|
||||||
on:mouseenter={() => gridFocused.set(true)}
|
on:mouseenter={() => gridFocused.set(true)}
|
||||||
on:mouseleave={() => gridFocused.set(false)}
|
on:mouseleave={() => gridFocused.set(false)}
|
||||||
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-overflow:{MaxCellRenderOverflow}px; --content-lines:{$contentLines}; --min-height:{minHeight}px; --controls-height:{ControlsHeight}px;"
|
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-overflow:{MaxCellRenderOverflow}px; --content-lines:{$contentLines}; --min-height:{$minHeight}px; --controls-height:{ControlsHeight}px;"
|
||||||
>
|
>
|
||||||
{#if showControls}
|
{#if showControls}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
bounds,
|
bounds,
|
||||||
renderedRows,
|
renderedRows,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
rowVerticalInversionIndex,
|
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
dispatch,
|
dispatch,
|
||||||
isDragging,
|
isDragging,
|
||||||
|
@ -41,11 +40,7 @@
|
||||||
<div bind:this={body} class="grid-body">
|
<div bind:this={body} class="grid-body">
|
||||||
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
|
<GridScrollWrapper scrollHorizontally scrollVertically attachHandlers>
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
<GridRow
|
<GridRow {row} top={idx === 0} />
|
||||||
{row}
|
|
||||||
top={idx === 0}
|
|
||||||
invertY={idx >= $rowVerticalInversionIndex}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
{#if $config.canAddRows}
|
{#if $config.canAddRows}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
export let top = false
|
export let top = false
|
||||||
export let invertY = false
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
focusedCellId,
|
focusedCellId,
|
||||||
|
@ -15,7 +14,6 @@
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
selectedCellMap,
|
selectedCellMap,
|
||||||
focusedRow,
|
focusedRow,
|
||||||
columnHorizontalInversionIndex,
|
|
||||||
contentLines,
|
contentLines,
|
||||||
isDragging,
|
isDragging,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
@ -38,15 +36,13 @@
|
||||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
||||||
>
|
>
|
||||||
{#each $visibleColumns as column, columnIdx}
|
{#each $visibleColumns as column}
|
||||||
{@const cellId = getCellID(row._id, column.name)}
|
{@const cellId = getCellID(row._id, column.name)}
|
||||||
<DataCell
|
<DataCell
|
||||||
{cellId}
|
{cellId}
|
||||||
{column}
|
{column}
|
||||||
{row}
|
{row}
|
||||||
{invertY}
|
|
||||||
{rowFocused}
|
{rowFocused}
|
||||||
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
|
||||||
highlighted={rowHovered || rowFocused || reorderSource === column.name}
|
highlighted={rowHovered || rowFocused || reorderSource === column.name}
|
||||||
selected={rowSelected}
|
selected={rowSelected}
|
||||||
rowIdx={row.__idx}
|
rowIdx={row.__idx}
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
rowHeight,
|
rowHeight,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
maxScrollTop,
|
maxScrollTop,
|
||||||
rowVerticalInversionIndex,
|
|
||||||
columnHorizontalInversionIndex,
|
|
||||||
selectedRows,
|
selectedRows,
|
||||||
loaded,
|
loaded,
|
||||||
refreshing,
|
refreshing,
|
||||||
|
@ -43,17 +41,9 @@
|
||||||
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
||||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||||
$: $datasource, (visible = false)
|
$: $datasource, (visible = false)
|
||||||
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
|
||||||
$: selectedRowCount = Object.values($selectedRows).length
|
$: selectedRowCount = Object.values($selectedRows).length
|
||||||
$: hasNoRows = !$rows.length
|
$: hasNoRows = !$rows.length
|
||||||
|
|
||||||
const shouldInvertY = (offset, inversionIndex, rows) => {
|
|
||||||
if (offset === 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return rows.length >= inversionIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
const addRow = async () => {
|
const addRow = async () => {
|
||||||
// Blur the active cell and tick to let final value updates propagate
|
// Blur the active cell and tick to let final value updates propagate
|
||||||
isAdding = true
|
isAdding = true
|
||||||
|
@ -205,7 +195,6 @@
|
||||||
width={$stickyColumn.width}
|
width={$stickyColumn.width}
|
||||||
{updateValue}
|
{updateValue}
|
||||||
topRow={offset === 0}
|
topRow={offset === 0}
|
||||||
{invertY}
|
|
||||||
>
|
>
|
||||||
{#if $stickyColumn?.schema?.autocolumn}
|
{#if $stickyColumn?.schema?.autocolumn}
|
||||||
<div class="readonly-overlay">Can't edit auto column</div>
|
<div class="readonly-overlay">Can't edit auto column</div>
|
||||||
|
@ -219,7 +208,7 @@
|
||||||
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
||||||
<GridScrollWrapper scrollHorizontally attachHandlers>
|
<GridScrollWrapper scrollHorizontally attachHandlers>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{#each $visibleColumns as column, columnIdx}
|
{#each $visibleColumns as column}
|
||||||
{@const cellId = `new-${column.name}`}
|
{@const cellId = `new-${column.name}`}
|
||||||
<DataCell
|
<DataCell
|
||||||
{cellId}
|
{cellId}
|
||||||
|
@ -230,8 +219,6 @@
|
||||||
focused={$focusedCellId === cellId}
|
focused={$focusedCellId === cellId}
|
||||||
width={column.width}
|
width={column.width}
|
||||||
topRow={offset === 0}
|
topRow={offset === 0}
|
||||||
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
|
||||||
{invertY}
|
|
||||||
hidden={!$columnRenderMap[column.name]}
|
hidden={!$columnRenderMap[column.name]}
|
||||||
>
|
>
|
||||||
{#if column?.schema?.autocolumn}
|
{#if column?.schema?.autocolumn}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import JSONCell from "../cells/JSONCell.svelte"
|
||||||
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
||||||
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
||||||
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
||||||
|
import SignatureCell from "../cells/SignatureCell.svelte"
|
||||||
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
|
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
|
||||||
|
|
||||||
const TypeComponentMap = {
|
const TypeComponentMap = {
|
||||||
|
@ -20,6 +21,7 @@ const TypeComponentMap = {
|
||||||
[FieldType.OPTIONS]: OptionsCell,
|
[FieldType.OPTIONS]: OptionsCell,
|
||||||
[FieldType.DATETIME]: DateCell,
|
[FieldType.DATETIME]: DateCell,
|
||||||
[FieldType.BARCODEQR]: TextCell,
|
[FieldType.BARCODEQR]: TextCell,
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: SignatureCell,
|
||||||
[FieldType.LONGFORM]: LongFormCell,
|
[FieldType.LONGFORM]: LongFormCell,
|
||||||
[FieldType.ARRAY]: MultiSelectCell,
|
[FieldType.ARRAY]: MultiSelectCell,
|
||||||
[FieldType.NUMBER]: NumberCell,
|
[FieldType.NUMBER]: NumberCell,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { derived, get, writable } from "svelte/store"
|
import { derived, get, writable } from "svelte/store"
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
import { GutterWidth, DefaultColumnWidth } from "../lib/constants"
|
import { GutterWidth, DefaultColumnWidth } from "../lib/constants"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
|
@ -75,72 +74,23 @@ export const deriveStores = context => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createActions = context => {
|
export const createActions = context => {
|
||||||
const { columns, stickyColumn, datasource, definition, schema } = context
|
const { columns, datasource, schema } = context
|
||||||
|
|
||||||
// Updates the datasources primary display column
|
|
||||||
const changePrimaryDisplay = async column => {
|
|
||||||
return await datasource.actions.saveDefinition({
|
|
||||||
...get(definition),
|
|
||||||
primaryDisplay: column,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates the width of all columns
|
// Updates the width of all columns
|
||||||
const changeAllColumnWidths = async width => {
|
const changeAllColumnWidths = async width => {
|
||||||
columns.update(state => {
|
const $schema = get(schema)
|
||||||
return state.map(col => ({
|
let mutations = {}
|
||||||
...col,
|
Object.keys($schema).forEach(field => {
|
||||||
width,
|
mutations[field] = { width }
|
||||||
}))
|
|
||||||
})
|
|
||||||
if (get(stickyColumn)) {
|
|
||||||
stickyColumn.update(state => ({
|
|
||||||
...state,
|
|
||||||
width,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
await saveChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persists column changes by saving metadata against datasource schema
|
|
||||||
const saveChanges = async () => {
|
|
||||||
const $columns = get(columns)
|
|
||||||
const $definition = get(definition)
|
|
||||||
const $stickyColumn = get(stickyColumn)
|
|
||||||
let newSchema = cloneDeep(get(schema)) || {}
|
|
||||||
|
|
||||||
// Build new updated datasource schema
|
|
||||||
Object.keys(newSchema).forEach(column => {
|
|
||||||
// Respect order specified by columns
|
|
||||||
const index = $columns.findIndex(x => x.name === column)
|
|
||||||
if (index !== -1) {
|
|
||||||
newSchema[column].order = index
|
|
||||||
} else {
|
|
||||||
delete newSchema[column].order
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy over metadata
|
|
||||||
if (column === $stickyColumn?.name) {
|
|
||||||
newSchema[column].visible = true
|
|
||||||
newSchema[column].width = $stickyColumn.width || DefaultColumnWidth
|
|
||||||
} else {
|
|
||||||
newSchema[column].visible = $columns[index]?.visible ?? true
|
|
||||||
newSchema[column].width = $columns[index]?.width || DefaultColumnWidth
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await datasource.actions.saveDefinition({
|
|
||||||
...$definition,
|
|
||||||
schema: newSchema,
|
|
||||||
})
|
})
|
||||||
|
datasource.actions.addSchemaMutations(mutations)
|
||||||
|
await datasource.actions.saveSchemaMutations()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columns: {
|
columns: {
|
||||||
...columns,
|
...columns,
|
||||||
actions: {
|
actions: {
|
||||||
saveChanges,
|
|
||||||
changePrimaryDisplay,
|
|
||||||
changeAllColumnWidths,
|
changeAllColumnWidths,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,15 +4,23 @@ import { memo } from "../../../utils"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const definition = memo(null)
|
const definition = memo(null)
|
||||||
|
const schemaMutations = memo({})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
definition,
|
definition,
|
||||||
|
schemaMutations,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deriveStores = context => {
|
export const deriveStores = context => {
|
||||||
const { API, definition, schemaOverrides, columnWhitelist, datasource } =
|
const {
|
||||||
context
|
API,
|
||||||
|
definition,
|
||||||
|
schemaOverrides,
|
||||||
|
columnWhitelist,
|
||||||
|
datasource,
|
||||||
|
schemaMutations,
|
||||||
|
} = context
|
||||||
|
|
||||||
const schema = derived(definition, $definition => {
|
const schema = derived(definition, $definition => {
|
||||||
let schema = getDatasourceSchema({
|
let schema = getDatasourceSchema({
|
||||||
|
@ -35,42 +43,26 @@ export const deriveStores = context => {
|
||||||
return schema
|
return schema
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Derives the total enriched schema, made up of the saved schema and any
|
||||||
|
// prop and user overrides
|
||||||
const enrichedSchema = derived(
|
const enrichedSchema = derived(
|
||||||
[schema, schemaOverrides, columnWhitelist],
|
[schema, schemaOverrides, schemaMutations, columnWhitelist],
|
||||||
([$schema, $schemaOverrides, $columnWhitelist]) => {
|
([$schema, $schemaOverrides, $schemaMutations, $columnWhitelist]) => {
|
||||||
if (!$schema) {
|
if (!$schema) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let enrichedSchema = { ...$schema }
|
let enrichedSchema = {}
|
||||||
|
Object.keys($schema).forEach(field => {
|
||||||
// Apply schema overrides
|
// Apply whitelist if provided
|
||||||
Object.keys($schemaOverrides || {}).forEach(field => {
|
if ($columnWhitelist?.length && !$columnWhitelist.includes(field)) {
|
||||||
if (enrichedSchema[field]) {
|
return
|
||||||
enrichedSchema[field] = {
|
}
|
||||||
...enrichedSchema[field],
|
enrichedSchema[field] = {
|
||||||
...$schemaOverrides[field],
|
...$schema[field],
|
||||||
}
|
...$schemaOverrides?.[field],
|
||||||
|
...$schemaMutations[field],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Apply whitelist if specified
|
|
||||||
if ($columnWhitelist?.length) {
|
|
||||||
const sortedColumns = {}
|
|
||||||
|
|
||||||
$columnWhitelist.forEach((columnKey, idx) => {
|
|
||||||
const enrichedColumn = enrichedSchema[columnKey]
|
|
||||||
if (enrichedColumn) {
|
|
||||||
sortedColumns[columnKey] = {
|
|
||||||
...enrichedColumn,
|
|
||||||
order: idx,
|
|
||||||
visible: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return sortedColumns
|
|
||||||
}
|
|
||||||
|
|
||||||
return enrichedSchema
|
return enrichedSchema
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -100,6 +92,8 @@ export const createActions = context => {
|
||||||
table,
|
table,
|
||||||
viewV2,
|
viewV2,
|
||||||
nonPlus,
|
nonPlus,
|
||||||
|
schemaMutations,
|
||||||
|
schema,
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
// Gets the appropriate API for the configured datasource type
|
// Gets the appropriate API for the configured datasource type
|
||||||
|
@ -136,11 +130,81 @@ export const createActions = context => {
|
||||||
// Update server
|
// Update server
|
||||||
if (get(config).canSaveSchema) {
|
if (get(config).canSaveSchema) {
|
||||||
await getAPI()?.actions.saveDefinition(newDefinition)
|
await getAPI()?.actions.saveDefinition(newDefinition)
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast change to external state can be updated, as this change
|
// Broadcast change so external state can be updated, as this change
|
||||||
// will not be received by the builder websocket because we caused it ourselves
|
// will not be received by the builder websocket because we caused it
|
||||||
dispatch("updatedatasource", newDefinition)
|
// ourselves
|
||||||
|
dispatch("updatedatasource", newDefinition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the datasources primary display column
|
||||||
|
const changePrimaryDisplay = async column => {
|
||||||
|
return await saveDefinition({
|
||||||
|
...get(definition),
|
||||||
|
primaryDisplay: column,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a schema mutation for a single field
|
||||||
|
const addSchemaMutation = (field, mutation) => {
|
||||||
|
if (!field || !mutation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schemaMutations.update($schemaMutations => {
|
||||||
|
return {
|
||||||
|
...$schemaMutations,
|
||||||
|
[field]: {
|
||||||
|
...$schemaMutations[field],
|
||||||
|
...mutation,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds schema mutations for multiple fields at once
|
||||||
|
const addSchemaMutations = mutations => {
|
||||||
|
const fields = Object.keys(mutations || {})
|
||||||
|
if (!fields.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schemaMutations.update($schemaMutations => {
|
||||||
|
let newSchemaMutations = { ...$schemaMutations }
|
||||||
|
fields.forEach(field => {
|
||||||
|
newSchemaMutations[field] = {
|
||||||
|
...newSchemaMutations[field],
|
||||||
|
...mutations[field],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return newSchemaMutations
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves schema changes to the server, if possible
|
||||||
|
const saveSchemaMutations = async () => {
|
||||||
|
// If we can't save schema changes then we just want to keep this in memory
|
||||||
|
if (!get(config).canSaveSchema) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const $definition = get(definition)
|
||||||
|
const $schemaMutations = get(schemaMutations)
|
||||||
|
const $schema = get(schema)
|
||||||
|
let newSchema = {}
|
||||||
|
|
||||||
|
// Build new updated datasource schema
|
||||||
|
Object.keys($schema).forEach(column => {
|
||||||
|
newSchema[column] = {
|
||||||
|
...$schema[column],
|
||||||
|
...$schemaMutations[column],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save the changes, then reset our local mutations
|
||||||
|
await saveDefinition({
|
||||||
|
...$definition,
|
||||||
|
schema: newSchema,
|
||||||
|
})
|
||||||
|
schemaMutations.set({})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a row to the datasource
|
// Adds a row to the datasource
|
||||||
|
@ -185,6 +249,10 @@ export const createActions = context => {
|
||||||
getRow,
|
getRow,
|
||||||
isDatasourceValid,
|
isDatasourceValid,
|
||||||
canUseColumn,
|
canUseColumn,
|
||||||
|
changePrimaryDisplay,
|
||||||
|
addSchemaMutation,
|
||||||
|
addSchemaMutations,
|
||||||
|
saveSchemaMutations,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const createActions = context => {
|
||||||
stickyColumn,
|
stickyColumn,
|
||||||
maxScrollLeft,
|
maxScrollLeft,
|
||||||
width,
|
width,
|
||||||
|
datasource,
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
let autoScrollInterval
|
let autoScrollInterval
|
||||||
|
@ -173,20 +174,17 @@ export const createActions = context => {
|
||||||
document.removeEventListener("touchend", stopReordering)
|
document.removeEventListener("touchend", stopReordering)
|
||||||
document.removeEventListener("touchcancel", stopReordering)
|
document.removeEventListener("touchcancel", stopReordering)
|
||||||
|
|
||||||
// Ensure there's actually a change
|
// Ensure there's actually a change before saving
|
||||||
let { sourceColumn, targetColumn } = get(reorder)
|
const { sourceColumn, targetColumn } = get(reorder)
|
||||||
if (sourceColumn !== targetColumn) {
|
|
||||||
moveColumn(sourceColumn, targetColumn)
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset state
|
|
||||||
reorder.set(reorderInitialState)
|
reorder.set(reorderInitialState)
|
||||||
|
if (sourceColumn !== targetColumn) {
|
||||||
|
await moveColumn(sourceColumn, targetColumn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moves a column after another columns.
|
// Moves a column after another columns.
|
||||||
// An undefined target column will move the source to index 0.
|
// An undefined target column will move the source to index 0.
|
||||||
const moveColumn = (sourceColumn, targetColumn) => {
|
const moveColumn = async (sourceColumn, targetColumn) => {
|
||||||
let $columns = get(columns)
|
let $columns = get(columns)
|
||||||
let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
|
let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
|
||||||
let targetIdx = $columns.findIndex(x => x.name === targetColumn)
|
let targetIdx = $columns.findIndex(x => x.name === targetColumn)
|
||||||
|
@ -198,14 +196,21 @@ export const createActions = context => {
|
||||||
}
|
}
|
||||||
return state.toSpliced(targetIdx, 0, removed[0])
|
return state.toSpliced(targetIdx, 0, removed[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Extract new orders as schema mutations
|
||||||
|
let mutations = {}
|
||||||
|
get(columns).forEach((column, idx) => {
|
||||||
|
mutations[column.name] = { order: idx }
|
||||||
|
})
|
||||||
|
datasource.actions.addSchemaMutations(mutations)
|
||||||
|
await datasource.actions.saveSchemaMutations()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moves a column one place left (as appears visually)
|
// Moves a column one place left (as appears visually)
|
||||||
const moveColumnLeft = async column => {
|
const moveColumnLeft = async column => {
|
||||||
const $visibleColumns = get(visibleColumns)
|
const $visibleColumns = get(visibleColumns)
|
||||||
const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
|
const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
|
||||||
moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
|
await moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moves a column one place right (as appears visually)
|
// Moves a column one place right (as appears visually)
|
||||||
|
@ -215,8 +220,7 @@ export const createActions = context => {
|
||||||
if (sourceIdx === $visibleColumns.length - 1) {
|
if (sourceIdx === $visibleColumns.length - 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
|
await moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,7 +6,6 @@ const initialState = {
|
||||||
initialMouseX: null,
|
initialMouseX: null,
|
||||||
initialWidth: null,
|
initialWidth: null,
|
||||||
column: null,
|
column: null,
|
||||||
columnIdx: null,
|
|
||||||
width: 0,
|
width: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
}
|
}
|
||||||
|
@ -21,7 +20,7 @@ export const createStores = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createActions = context => {
|
export const createActions = context => {
|
||||||
const { resize, columns, stickyColumn, ui } = context
|
const { resize, ui, datasource } = context
|
||||||
|
|
||||||
// Starts resizing a certain column
|
// Starts resizing a certain column
|
||||||
const startResizing = (column, e) => {
|
const startResizing = (column, e) => {
|
||||||
|
@ -32,12 +31,6 @@ export const createActions = context => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ui.actions.blur()
|
ui.actions.blur()
|
||||||
|
|
||||||
// Find and cache index
|
|
||||||
let columnIdx = get(columns).findIndex(col => col.name === column.name)
|
|
||||||
if (columnIdx === -1) {
|
|
||||||
columnIdx = "sticky"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial store state
|
// Set initial store state
|
||||||
resize.set({
|
resize.set({
|
||||||
width: column.width,
|
width: column.width,
|
||||||
|
@ -45,7 +38,6 @@ export const createActions = context => {
|
||||||
initialWidth: column.width,
|
initialWidth: column.width,
|
||||||
initialMouseX: x,
|
initialMouseX: x,
|
||||||
column: column.name,
|
column: column.name,
|
||||||
columnIdx,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add mouse event listeners to handle resizing
|
// Add mouse event listeners to handle resizing
|
||||||
|
@ -58,7 +50,7 @@ export const createActions = context => {
|
||||||
|
|
||||||
// Handler for moving the mouse to resize columns
|
// Handler for moving the mouse to resize columns
|
||||||
const onResizeMouseMove = e => {
|
const onResizeMouseMove = e => {
|
||||||
const { initialMouseX, initialWidth, width, columnIdx } = get(resize)
|
const { initialMouseX, initialWidth, width, column } = get(resize)
|
||||||
const { x } = parseEventLocation(e)
|
const { x } = parseEventLocation(e)
|
||||||
const dx = x - initialMouseX
|
const dx = x - initialMouseX
|
||||||
const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
|
const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
|
||||||
|
@ -69,17 +61,7 @@ export const createActions = context => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update column state
|
// Update column state
|
||||||
if (columnIdx === "sticky") {
|
datasource.actions.addSchemaMutation(column, { width })
|
||||||
stickyColumn.update(state => ({
|
|
||||||
...state,
|
|
||||||
width: newWidth,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
columns.update(state => {
|
|
||||||
state[columnIdx].width = newWidth
|
|
||||||
return [...state]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
resize.update(state => ({
|
resize.update(state => ({
|
||||||
|
@ -101,26 +83,16 @@ export const createActions = context => {
|
||||||
|
|
||||||
// Persist width if it changed
|
// Persist width if it changed
|
||||||
if ($resize.width !== $resize.initialWidth) {
|
if ($resize.width !== $resize.initialWidth) {
|
||||||
await columns.actions.saveChanges()
|
await datasource.actions.saveSchemaMutations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resets a column size back to default
|
// Resets a column size back to default
|
||||||
const resetSize = async column => {
|
const resetSize = async column => {
|
||||||
const $stickyColumn = get(stickyColumn)
|
datasource.actions.addSchemaMutation(column.name, {
|
||||||
if (column.name === $stickyColumn?.name) {
|
width: DefaultColumnWidth,
|
||||||
stickyColumn.update(state => ({
|
})
|
||||||
...state,
|
await datasource.actions.saveSchemaMutations()
|
||||||
width: DefaultColumnWidth,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
columns.update(state => {
|
|
||||||
const columnIdx = state.findIndex(x => x.name === column.name)
|
|
||||||
state[columnIdx].width = DefaultColumnWidth
|
|
||||||
return [...state]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import {
|
import { MinColumnWidth } from "../lib/constants"
|
||||||
MaxCellRenderOverflow,
|
|
||||||
MinColumnWidth,
|
|
||||||
ScrollBarSize,
|
|
||||||
} from "../lib/constants"
|
|
||||||
|
|
||||||
export const deriveStores = context => {
|
export const deriveStores = context => {
|
||||||
const {
|
const {
|
||||||
|
@ -85,51 +81,10 @@ export const deriveStores = context => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Determine the row index at which we should start vertically inverting cell
|
|
||||||
// dropdowns
|
|
||||||
const rowVerticalInversionIndex = derived(
|
|
||||||
[height, rowHeight, scrollTop],
|
|
||||||
([$height, $rowHeight, $scrollTop]) => {
|
|
||||||
const offset = $scrollTop % $rowHeight
|
|
||||||
|
|
||||||
// Compute the last row index with space to render popovers below it
|
|
||||||
const minBottom =
|
|
||||||
$height - ScrollBarSize * 3 - MaxCellRenderOverflow + offset
|
|
||||||
const lastIdx = Math.floor(minBottom / $rowHeight)
|
|
||||||
|
|
||||||
// Compute the first row index with space to render popovers above it
|
|
||||||
const minTop = MaxCellRenderOverflow + offset
|
|
||||||
const firstIdx = Math.ceil(minTop / $rowHeight)
|
|
||||||
|
|
||||||
// Use the greater of the two indices so that we prefer content below,
|
|
||||||
// unless there is room to render the entire popover above
|
|
||||||
return Math.max(lastIdx, firstIdx)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Determine the column index at which we should start horizontally inverting
|
|
||||||
// cell dropdowns
|
|
||||||
const columnHorizontalInversionIndex = derived(
|
|
||||||
[visibleColumns, scrollLeft, width],
|
|
||||||
([$visibleColumns, $scrollLeft, $width]) => {
|
|
||||||
const cutoff = $width + $scrollLeft - ScrollBarSize * 3
|
|
||||||
let inversionIdx = $visibleColumns.length
|
|
||||||
for (let i = $visibleColumns.length - 1; i >= 0; i--, inversionIdx--) {
|
|
||||||
const rightEdge = $visibleColumns[i].left + $visibleColumns[i].width
|
|
||||||
if (rightEdge + MaxCellRenderOverflow <= cutoff) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return inversionIdx
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrolledRowCount,
|
scrolledRowCount,
|
||||||
visualRowCapacity,
|
visualRowCapacity,
|
||||||
renderedRows,
|
renderedRows,
|
||||||
columnRenderMap,
|
columnRenderMap,
|
||||||
rowVerticalInversionIndex,
|
|
||||||
columnHorizontalInversionIndex,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export { default as SplitPage } from "./SplitPage.svelte"
|
export { default as SplitPage } from "./SplitPage.svelte"
|
||||||
export { default as TestimonialPage } from "./TestimonialPage.svelte"
|
export { default as TestimonialPage } from "./TestimonialPage.svelte"
|
||||||
|
export { default as SignatureModal } from "./SignatureModal.svelte"
|
||||||
export { default as Testimonial } from "./Testimonial.svelte"
|
export { default as Testimonial } from "./Testimonial.svelte"
|
||||||
export { default as UserAvatar } from "./UserAvatar.svelte"
|
export { default as UserAvatar } from "./UserAvatar.svelte"
|
||||||
export { default as UserAvatars } from "./UserAvatars.svelte"
|
export { default as UserAvatars } from "./UserAvatars.svelte"
|
||||||
|
|
|
@ -121,6 +121,7 @@ export const TypeIconMap = {
|
||||||
[FieldType.OPTIONS]: "Dropdown",
|
[FieldType.OPTIONS]: "Dropdown",
|
||||||
[FieldType.DATETIME]: "Calendar",
|
[FieldType.DATETIME]: "Calendar",
|
||||||
[FieldType.BARCODEQR]: "Camera",
|
[FieldType.BARCODEQR]: "Camera",
|
||||||
|
[FieldType.SIGNATURE_SINGLE]: "AnnotatePen",
|
||||||
[FieldType.LONGFORM]: "TextAlignLeft",
|
[FieldType.LONGFORM]: "TextAlignLeft",
|
||||||
[FieldType.ARRAY]: "Duplicate",
|
[FieldType.ARRAY]: "Duplicate",
|
||||||
[FieldType.NUMBER]: "123",
|
[FieldType.NUMBER]: "123",
|
||||||
|
|
|
@ -17,8 +17,10 @@ module FetchMock {
|
||||||
raw: () => {
|
raw: () => {
|
||||||
return { "content-type": ["application/json"] }
|
return { "content-type": ["application/json"] }
|
||||||
},
|
},
|
||||||
get: () => {
|
get: (name: string) => {
|
||||||
return ["application/json"]
|
if (name.toLowerCase() === "content-type") {
|
||||||
|
return ["application/json"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
json: async () => {
|
json: async () => {
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"global-agent": "3.0.0",
|
"global-agent": "3.0.0",
|
||||||
"google-spreadsheet": "4.1.2",
|
"google-spreadsheet": "npm:@budibase/google-spreadsheet@4.1.2",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5.3.2",
|
||||||
"isolated-vm": "^4.7.2",
|
"isolated-vm": "^4.7.2",
|
||||||
"jimp": "0.22.10",
|
"jimp": "0.22.10",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue