Merge branch 'master' into BUDI-7580/account_portal_submodule

This commit is contained in:
Adria Navarro 2023-11-07 19:06:33 +01:00
commit ef914882d4
61 changed files with 1231 additions and 237 deletions

View File

@ -45,8 +45,8 @@ jobs:
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"} BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
./versionCommit.sh $BUMP_TYPE ./versionCommit.sh $BUMP_TYPE
cd ..
new_version=$(./getCurrentVersion.sh) new_version=$(./scripts/getCurrentVersion.sh)
echo "version=$new_version" >> $GITHUB_OUTPUT echo "version=$new_version" >> $GITHUB_OUTPUT
trigger-release: trigger-release:

View File

@ -2,16 +2,18 @@ server {
listen 443 ssl default_server; listen 443 ssl default_server;
listen [::]:443 ssl default_server; listen [::]:443 ssl default_server;
server_name _; server_name _;
ssl_certificate /etc/letsencrypt/live/CUSTOM_DOMAIN/fullchain.pem; error_log /dev/stderr warn;
ssl_certificate_key /etc/letsencrypt/live/CUSTOM_DOMAIN/privkey.pem; access_log /dev/stdout main;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
client_max_body_size 1000m; client_max_body_size 1000m;
ignore_invalid_headers off; ignore_invalid_headers off;
proxy_buffering off; proxy_buffering off;
# port_in_redirect off; # port_in_redirect off;
ssl_certificate /etc/letsencrypt/live/CUSTOM_DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/CUSTOM_DOMAIN/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location ^~ /.well-known/acme-challenge/ { location ^~ /.well-known/acme-challenge/ {
default_type "text/plain"; default_type "text/plain";
root /var/www/html; root /var/www/html;
@ -47,6 +49,24 @@ server {
rewrite ^/worker/(.*)$ /$1 break; rewrite ^/worker/(.*)$ /$1 break;
} }
location /api/backups/ {
# calls to export apps are limited
limit_req zone=ratelimit burst=20 nodelay;
# 1800s timeout for app export requests
proxy_read_timeout 1800s;
proxy_connect_timeout 1800s;
proxy_send_timeout 1800s;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:4001;
}
location /api/ { location /api/ {
# calls to the API are rate limited with bursting # calls to the API are rate limited with bursting
limit_req zone=ratelimit burst=20 nodelay; limit_req zone=ratelimit burst=20 nodelay;
@ -70,18 +90,49 @@ server {
rewrite ^/db/(.*)$ /$1 break; rewrite ^/db/(.*)$ /$1 break;
} }
location /socket/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_pass http://127.0.0.1:4001;
}
location / { location / {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_connect_timeout 300; proxy_connect_timeout 300;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Connection ""; proxy_set_header Connection "";
chunked_transfer_encoding off; chunked_transfer_encoding off;
proxy_pass http://127.0.0.1:9000; proxy_pass http://127.0.0.1:9000;
} }
location /files/signed/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# IMPORTANT: Signed urls will inspect the host header of the request.
# Normally a signed url will need to be generated with a specified client host in mind.
# To support dynamic hosts, e.g. some unknown self-hosted installation url,
# use a predefined host header. The host 'minio-service' is also used at the time of url signing.
proxy_set_header Host minio-service;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://127.0.0.1:9000;
rewrite ^/files/signed/(.*)$ /$1 break;
}
client_header_timeout 60; client_header_timeout 60;
client_body_timeout 60; client_body_timeout 60;
keepalive_timeout 60; keepalive_timeout 60;

View File

@ -1,5 +1,5 @@
{ {
"version": "2.13.0", "version": "2.13.3",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -30,6 +30,7 @@ export * as timers from "./timers"
export { default as env } from "./environment" export { default as env } from "./environment"
export * as blacklist from "./blacklist" export * as blacklist from "./blacklist"
export * as docUpdates from "./docUpdates" export * as docUpdates from "./docUpdates"
export * from "./utils/Duration"
export { SearchParams } from "./db" export { SearchParams } from "./db"
// Add context to tenancy for backwards compatibility // Add context to tenancy for backwards compatibility
// only do this for external usages to prevent internal // only do this for external usages to prevent internal

View File

@ -36,7 +36,7 @@ class InMemoryQueue {
* @param opts This is not used by the in memory queue as there is no real use * @param opts This is not used by the in memory queue as there is no real use
* case when in memory, but is the same API as Bull * case when in memory, but is the same API as Bull
*/ */
constructor(name: string, opts = null) { constructor(name: string, opts?: any) {
this._name = name this._name = name
this._opts = opts this._opts = opts
this._messages = [] this._messages = []

View File

@ -2,11 +2,17 @@ import env from "../environment"
import { getRedisOptions } from "../redis/utils" import { getRedisOptions } from "../redis/utils"
import { JobQueue } from "./constants" import { JobQueue } from "./constants"
import InMemoryQueue from "./inMemoryQueue" import InMemoryQueue from "./inMemoryQueue"
import BullQueue from "bull" import BullQueue, { QueueOptions } from "bull"
import { addListeners, StalledFn } from "./listeners" import { addListeners, StalledFn } from "./listeners"
import { Duration } from "../utils"
import * as timers from "../timers" import * as timers from "../timers"
const CLEANUP_PERIOD_MS = 60 * 1000 // the queue lock is held for 5 minutes
const QUEUE_LOCK_MS = Duration.fromMinutes(5).toMs()
// queue lock is refreshed every 30 seconds
const QUEUE_LOCK_RENEW_INTERNAL_MS = Duration.fromSeconds(30).toMs()
// cleanup the queue every 60 seconds
const CLEANUP_PERIOD_MS = Duration.fromSeconds(60).toMs()
let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = [] let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
let cleanupInterval: NodeJS.Timeout let cleanupInterval: NodeJS.Timeout
@ -20,8 +26,15 @@ export function createQueue<T>(
jobQueue: JobQueue, jobQueue: JobQueue,
opts: { removeStalledCb?: StalledFn } = {} opts: { removeStalledCb?: StalledFn } = {}
): BullQueue.Queue<T> { ): BullQueue.Queue<T> {
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions() const redisOpts = getRedisOptions()
const queueConfig: any = redisProtocolUrl || { redis: redisOpts } const queueConfig: QueueOptions = {
redis: redisOpts,
settings: {
maxStalledCount: 0,
lockDuration: QUEUE_LOCK_MS,
lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS,
},
}
let queue: any let queue: any
if (!env.isTest()) { if (!env.isTest()) {
queue = new BullQueue(jobQueue, queueConfig) queue = new BullQueue(jobQueue, queueConfig)

View File

@ -16,6 +16,7 @@ import {
getRedisOptions, getRedisOptions,
SEPARATOR, SEPARATOR,
SelectableDatabase, SelectableDatabase,
getRedisConnectionDetails,
} from "./utils" } from "./utils"
import * as timers from "../timers" import * as timers from "../timers"
@ -91,12 +92,11 @@ function init(selectDb = DEFAULT_SELECT_DB) {
if (client) { if (client) {
client.disconnect() client.disconnect()
} }
const { redisProtocolUrl, opts, host, port } = getRedisOptions() const { host, port } = getRedisConnectionDetails()
const opts = getRedisOptions()
if (CLUSTERED) { if (CLUSTERED) {
client = new RedisCore.Cluster([{ host, port }], opts) client = new RedisCore.Cluster([{ host, port }], opts)
} else if (redisProtocolUrl) {
client = new RedisCore(redisProtocolUrl)
} else { } else {
client = new RedisCore(opts) client = new RedisCore(opts)
} }

View File

@ -1,4 +1,5 @@
import env from "../environment" import env from "../environment"
import * as Redis from "ioredis"
const SLOT_REFRESH_MS = 2000 const SLOT_REFRESH_MS = 2000
const CONNECT_TIMEOUT_MS = 10000 const CONNECT_TIMEOUT_MS = 10000
@ -42,7 +43,7 @@ export enum Databases {
export enum SelectableDatabase { export enum SelectableDatabase {
DEFAULT = 0, DEFAULT = 0,
SOCKET_IO = 1, SOCKET_IO = 1,
UNUSED_1 = 2, RATE_LIMITING = 2,
UNUSED_2 = 3, UNUSED_2 = 3,
UNUSED_3 = 4, UNUSED_3 = 4,
UNUSED_4 = 5, UNUSED_4 = 5,
@ -58,7 +59,7 @@ export enum SelectableDatabase {
UNUSED_14 = 15, UNUSED_14 = 15,
} }
export function getRedisOptions() { export function getRedisConnectionDetails() {
let password = env.REDIS_PASSWORD let password = env.REDIS_PASSWORD
let url: string[] | string = env.REDIS_URL.split("//") let url: string[] | string = env.REDIS_URL.split("//")
// get rid of the protocol // get rid of the protocol
@ -74,28 +75,34 @@ export function getRedisOptions() {
} }
const [host, port] = url.split(":") const [host, port] = url.split(":")
let redisProtocolUrl return {
host,
// fully qualified redis URL password,
if (/rediss?:\/\//.test(env.REDIS_URL)) { port: parseInt(port),
redisProtocolUrl = env.REDIS_URL
} }
}
const opts: any = { export function getRedisOptions() {
const { host, password, port } = getRedisConnectionDetails()
let redisOpts: Redis.RedisOptions = {
connectTimeout: CONNECT_TIMEOUT_MS, connectTimeout: CONNECT_TIMEOUT_MS,
port: port,
host,
password,
} }
let opts: Redis.ClusterOptions | Redis.RedisOptions = redisOpts
if (env.REDIS_CLUSTERED) { if (env.REDIS_CLUSTERED) {
opts.redisOptions = {} opts = {
opts.redisOptions.tls = {} connectTimeout: CONNECT_TIMEOUT_MS,
opts.redisOptions.password = password redisOptions: {
opts.slotsRefreshTimeout = SLOT_REFRESH_MS ...redisOpts,
opts.dnsLookup = (address: string, callback: any) => callback(null, address) tls: {},
} else { },
opts.host = host slotsRefreshTimeout: SLOT_REFRESH_MS,
opts.port = port dnsLookup: (address: string, callback: any) => callback(null, address),
opts.password = password } as Redis.ClusterOptions
} }
return { opts, host, port: parseInt(port), redisProtocolUrl } return opts
} }
export function addDbPrefix(db: string, key: string) { export function addDbPrefix(db: string, key: string) {

View File

@ -0,0 +1,49 @@
export enum DurationType {
MILLISECONDS = "milliseconds",
SECONDS = "seconds",
MINUTES = "minutes",
HOURS = "hours",
DAYS = "days",
}
const conversion: Record<DurationType, number> = {
milliseconds: 1,
seconds: 1000,
minutes: 60 * 1000,
hours: 60 * 60 * 1000,
days: 24 * 60 * 60 * 1000,
}
export class Duration {
static convert(from: DurationType, to: DurationType, duration: number) {
const milliseconds = duration * conversion[from]
return milliseconds / conversion[to]
}
static from(from: DurationType, duration: number) {
return {
to: (to: DurationType) => {
return Duration.convert(from, to, duration)
},
toMs: () => {
return Duration.convert(from, DurationType.MILLISECONDS, duration)
},
}
}
static fromSeconds(duration: number) {
return Duration.from(DurationType.SECONDS, duration)
}
static fromMinutes(duration: number) {
return Duration.from(DurationType.MINUTES, duration)
}
static fromHours(duration: number) {
return Duration.from(DurationType.HOURS, duration)
}
static fromDays(duration: number) {
return Duration.from(DurationType.DAYS, duration)
}
}

View File

@ -1,3 +1,4 @@
export * from "./hashing" export * from "./hashing"
export * from "./utils" export * from "./utils"
export * from "./stringUtils" export * from "./stringUtils"
export * from "./Duration"

View File

@ -0,0 +1,19 @@
import { Duration, DurationType } from "../Duration"
describe("duration", () => {
it("should convert minutes to milliseconds", () => {
expect(Duration.fromMinutes(5).toMs()).toBe(300000)
})
it("should convert seconds to milliseconds", () => {
expect(Duration.fromSeconds(30).toMs()).toBe(30000)
})
it("should convert days to milliseconds", () => {
expect(Duration.fromDays(1).toMs()).toBe(86400000)
})
it("should convert minutes to days", () => {
expect(Duration.fromMinutes(1440).to(DurationType.DAYS)).toBe(1)
})
})

View File

@ -8,6 +8,7 @@
export let id = null export let id = null
export let text = null export let text = null
export let disabled = false export let disabled = false
export let readonly = false
export let size export let size
export let indeterminate = false export let indeterminate = false
@ -24,6 +25,7 @@
class:is-invalid={!!error} class:is-invalid={!!error}
class:checked={value} class:checked={value}
class:is-indeterminate={indeterminate} class:is-indeterminate={indeterminate}
class:readonly
> >
<input <input
checked={value} checked={value}
@ -68,4 +70,7 @@
.spectrum-Checkbox-input { .spectrum-Checkbox-input {
opacity: 0; opacity: 0;
} }
.readonly {
pointer-events: none;
}
</style> </style>

View File

@ -8,6 +8,7 @@
export let options = [] export let options = []
export let error = null export let error = null
export let disabled = false export let disabled = false
export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
@ -34,6 +35,7 @@
title={getOptionLabel(option)} title={getOptionLabel(option)}
class="spectrum-Checkbox spectrum-FieldGroup-item" class="spectrum-Checkbox spectrum-FieldGroup-item"
class:is-invalid={!!error} class:is-invalid={!!error}
class:readonly
> >
<label <label
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item" class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item"
@ -66,4 +68,7 @@
.spectrum-Checkbox-input { .spectrum-Checkbox-input {
opacity: 0; opacity: 0;
} }
.readonly {
pointer-events: none;
}
</style> </style>

View File

@ -9,6 +9,7 @@
export let id = null export let id = null
export let disabled = false export let disabled = false
export let readonly = false
export let error = null export let error = null
export let enableTime = true export let enableTime = true
export let value = null export let value = null
@ -186,7 +187,7 @@
> >
<div <div
id={flatpickrId} id={flatpickrId}
class:is-disabled={disabled} class:is-disabled={disabled || readonly}
class:is-invalid={!!error} class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker" class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open} class:is-focused={open}
@ -211,6 +212,7 @@
{/if} {/if}
<input <input
{disabled} {disabled}
{readonly}
data-input data-input
type="text" type="text"
class="spectrum-Textfield-input spectrum-InputGroup-input" class="spectrum-Textfield-input spectrum-InputGroup-input"

View File

@ -386,7 +386,7 @@
} }
.compact .placeholder, .compact .placeholder,
.compact img { .compact img {
margin: 10px 16px; margin: 8px 16px;
} }
.compact img { .compact img {
height: 90px; height: 90px;
@ -456,6 +456,12 @@
color: var(--red); color: var(--red);
} }
.spectrum-Dropzone {
height: 220px;
}
.compact .spectrum-Dropzone {
height: 40px;
}
.spectrum-Dropzone.disabled { .spectrum-Dropzone.disabled {
pointer-events: none; pointer-events: none;
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-200);
@ -463,10 +469,6 @@
.disabled .spectrum-Heading--sizeL { .disabled .spectrum-Heading--sizeL {
color: var(--spectrum-alias-text-color-disabled); color: var(--spectrum-alias-text-color-disabled);
} }
.compact .spectrum-Dropzone {
padding-top: 8px;
padding-bottom: 8px;
}
.compact .spectrum-IllustratedMessage-description { .compact .spectrum-IllustratedMessage-description {
margin: 0; margin: 0;
} }
@ -477,7 +479,6 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
} }
.tag { .tag {
margin-top: 8px; margin-top: 8px;
} }

View File

@ -8,6 +8,7 @@
export let options = [] export let options = []
export let error = null export let error = null
export let disabled = false export let disabled = false
export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
export let getOptionTitle = option => option export let getOptionTitle = option => option
@ -40,6 +41,7 @@
title={getOptionTitle(option)} title={getOptionTitle(option)}
class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized" class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
class:is-invalid={!!error} class:is-invalid={!!error}
class:readonly
> >
<input <input
on:change={onChange} on:change={onChange}
@ -62,4 +64,7 @@
.spectrum-Radio-input { .spectrum-Radio-input {
opacity: 0; opacity: 0;
} }
.readonly {
pointer-events: none;
}
</style> </style>

View File

@ -4,6 +4,7 @@
export let value = "" export let value = ""
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false
export let error = null export let error = null
export let height = null export let height = null
export let id = null export let id = null
@ -20,6 +21,7 @@
{fullScreenOffset} {fullScreenOffset}
{disabled} {disabled}
{easyMDEOptions} {easyMDEOptions}
{readonly}
on:change on:change
/> />
</div> </div>

View File

@ -5,6 +5,7 @@
export let value = "" export let value = ""
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false
export let error = null export let error = null
export let id = null export let id = null
export let height = null export let height = null
@ -61,6 +62,7 @@
class="spectrum-Textfield-input" class="spectrum-Textfield-input"
style={align ? `text-align: ${align}` : ""} style={align ? `text-align: ${align}` : ""}
{disabled} {disabled}
{readonly}
{id} {id}
on:focus={() => (focus = true)} on:focus={() => (focus = true)}
on:blur={onChange} on:blur={onChange}

View File

@ -7,6 +7,7 @@
export let label = null export let label = null
export let labelPosition = "above" export let labelPosition = "above"
export let disabled = false export let disabled = false
export let readonly = false
export let error = null export let error = null
export let enableTime = true export let enableTime = true
export let timeOnly = false export let timeOnly = false
@ -33,6 +34,7 @@
<DatePicker <DatePicker
{error} {error}
{disabled} {disabled}
{readonly}
{value} {value}
{placeholder} {placeholder}
{enableTime} {enableTime}

View File

@ -8,6 +8,7 @@
export let id = null export let id = null
export let fullScreenOffset = 0 export let fullScreenOffset = 0
export let disabled = false export let disabled = false
export let readonly = false
export let easyMDEOptions export let easyMDEOptions
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -19,6 +20,9 @@
// control // control
$: checkValue(value) $: checkValue(value)
$: mde?.codemirror.on("change", debouncedUpdate) $: mde?.codemirror.on("change", debouncedUpdate)
$: if (readonly || disabled) {
mde?.togglePreview()
}
const checkValue = val => { const checkValue = val => {
if (mde && val !== latestValue) { if (mde && val !== latestValue) {
@ -54,6 +58,7 @@
easyMDEOptions={{ easyMDEOptions={{
initialValue: value, initialValue: value,
placeholder, placeholder,
toolbar: disabled || readonly ? false : undefined,
...easyMDEOptions, ...easyMDEOptions,
}} }}
/> />

View File

@ -44,6 +44,8 @@
const NUMBER_TYPE = FIELDS.NUMBER.type const NUMBER_TYPE = FIELDS.NUMBER.type
const JSON_TYPE = FIELDS.JSON.type const JSON_TYPE = FIELDS.JSON.type
const DATE_TYPE = FIELDS.DATETIME.type const DATE_TYPE = FIELDS.DATETIME.type
const USER_TYPE = FIELDS.USER.subtype
const USERS_TYPE = FIELDS.USERS.subtype
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"] const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
@ -287,6 +289,14 @@
if (saveColumn.type !== LINK_TYPE) { if (saveColumn.type !== LINK_TYPE) {
delete saveColumn.fieldName delete saveColumn.fieldName
} }
if (isUsersColumn(saveColumn)) {
if (saveColumn.subtype === USER_TYPE) {
saveColumn.relationshipType = RelationshipType.ONE_TO_MANY
} else if (saveColumn.subtype === USERS_TYPE) {
saveColumn.relationshipType = RelationshipType.MANY_TO_MANY
}
}
try { try {
await tables.saveField({ await tables.saveField({
originalName, originalName,

View File

@ -20,7 +20,7 @@
let open = false let open = 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() popover.hide()
} }
@ -100,13 +100,13 @@
}} }}
on:close={() => { on:close={() => {
open = false open = false
if ($draggable.selected == componentInstance._id) { if ($draggable.selected === componentInstance._id) {
$draggable.actions.select() $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}
handlePostionUpdate={customPositionHandler} handlePostionUpdate={customPositionHandler}
@ -115,6 +115,7 @@
<Layout noPadding noGap> <Layout noPadding noGap>
<slot name="header" /> <slot name="header" />
<ComponentSettingsSection <ComponentSettingsSection
includeHidden
{componentInstance} {componentInstance}
componentDefinition={parsedComponentDef} componentDefinition={parsedComponentDef}
isScreen={false} isScreen={false}

View File

@ -112,9 +112,9 @@
} }
await usersFetch.update({ await usersFetch.update({
query: { query: {
appId: query || !filterByAppAccess ? null : prodAppId, string: { email: query },
email: query,
}, },
appId: query || !filterByAppAccess ? null : prodAppId,
limit: 50, limit: 50,
paginate: query || !filterByAppAccess ? null : false, paginate: query || !filterByAppAccess ? null : false,
}) })

View File

@ -16,16 +16,18 @@
export let isScreen = false export let isScreen = false
export let onUpdateSetting export let onUpdateSetting
export let showSectionTitle = true export let showSectionTitle = true
export let includeHidden = false
export let tag export let tag
$: sections = getSections( $: sections = getSections(
componentInstance, componentInstance,
componentDefinition, componentDefinition,
isScreen, isScreen,
tag tag,
includeHidden
) )
const getSections = (instance, definition, isScreen, tag) => { const getSections = (instance, definition, isScreen, tag, includeHidden) => {
const settings = definition?.settings ?? [] const settings = definition?.settings ?? []
const generalSettings = settings.filter( const generalSettings = settings.filter(
setting => !setting.section && setting.tag === tag setting => !setting.section && setting.tag === tag
@ -52,7 +54,12 @@
return return
} }
section.settings.forEach(setting => { section.settings.forEach(setting => {
setting.visible = canRenderControl(instance, setting, isScreen) setting.visible = canRenderControl(
instance,
setting,
isScreen,
includeHidden
)
}) })
section.visible = section.visible =
section.name === "General" || section.name === "General" ||
@ -122,16 +129,20 @@
}) })
} }
const canRenderControl = (instance, setting, isScreen) => { const canRenderControl = (instance, setting, isScreen, includeHidden) => {
// Prevent rendering on click setting for screens // Prevent rendering on click setting for screens
if (setting?.type === "event" && isScreen) { if (setting?.type === "event" && isScreen) {
return false return false
} }
// Check we have a component to render for this setting
const control = getComponentForSetting(setting) const control = getComponentForSetting(setting)
if (!control) { if (!control) {
return false return false
} }
// Check if setting is hidden
if (setting.hidden && !includeHidden) {
return false
}
return shouldDisplay(instance, setting) return shouldDisplay(instance, setting)
} }
</script> </script>

View File

@ -2589,6 +2589,17 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "text", "type": "text",
"label": "Initial form step", "label": "Initial form step",
@ -2738,6 +2749,17 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
@ -2776,6 +2798,35 @@
"barTitle": "Justify text" "barTitle": "Justify text"
} }
] ]
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -2829,10 +2880,50 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/number", "type": "validation/number",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -2885,6 +2976,35 @@
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -2942,6 +3062,35 @@
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3049,6 +3198,17 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "select", "type": "select",
"label": "Options source", "label": "Options source",
@ -3110,6 +3270,35 @@
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3174,6 +3363,17 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "select", "type": "select",
"label": "Type", "label": "Type",
@ -3272,6 +3472,35 @@
"type": "validation/array", "type": "validation/array",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3348,10 +3577,50 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/boolean", "type": "validation/boolean",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3427,10 +3696,50 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3508,10 +3817,50 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{ {
"type": "validation/datetime", "type": "validation/datetime",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3598,6 +3947,22 @@
"value": "custom" "value": "custom"
} }
}, },
{
"type": "select",
"label": "Preferred camera",
"key": "preferredCamera",
"defaultValue": "environment",
"options": [
{
"label": "Front",
"value": "user"
},
{
"label": "Back",
"value": "environment"
}
]
},
{ {
"type": "event", "type": "event",
"label": "On change", "label": "On change",
@ -3613,6 +3978,35 @@
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3781,7 +4175,7 @@
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Read only",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
@ -3789,6 +4183,35 @@
"type": "validation/attachment", "type": "validation/attachment",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3857,6 +4280,46 @@
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -3909,6 +4372,46 @@
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
}, },
@ -5530,12 +6033,7 @@
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false, "defaultValue": false
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
} }
] ]
}, },
@ -5590,23 +6088,6 @@
} }
] ]
}, },
{
"tag": "style",
"type": "select",
"label": "Align labels",
"key": "labelPosition",
"defaultValue": "left",
"options": [
{
"label": "Left",
"value": "left"
},
{
"label": "Above",
"value": "above"
}
]
},
{ {
"tag": "style", "tag": "style",
"type": "select", "type": "select",
@ -5921,6 +6402,35 @@
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
},
{
"type": "select",
"label": "Layout",
"key": "span",
"defaultValue": 6,
"hidden": true,
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "1 column",
"value": 6,
"barIcon": "Stop",
"barTitle": "1 column"
},
{
"label": "2 columns",
"value": 3,
"barIcon": "ColumnTwoA",
"barTitle": "2 columns"
},
{
"label": "3 columns",
"value": 2,
"barIcon": "ViewColumn",
"barTitle": "3 columns"
}
]
} }
] ]
} }

View File

@ -26,15 +26,15 @@
$: parentId = $component?.id $: parentId = $component?.id
$: inBuilder = $builderStore.inBuilder $: inBuilder = $builderStore.inBuilder
$: instance = { $: instance = {
...props,
_component: getComponent(type), _component: getComponent(type),
_id: id, _id: id,
_instanceName: getInstanceName(name, type), _instanceName: getInstanceName(name, type),
_containsSlot: containsSlot,
_styles: { _styles: {
...styles, ...styles,
normal: styles?.normal || {}, normal: styles?.normal || {},
}, },
_containsSlot: containsSlot,
...props,
} }
// Register this block component if we're inside the builder so it can be // Register this block component if we're inside the builder so it can be

View File

@ -140,6 +140,7 @@
interactive && interactive &&
!isLayout && !isLayout &&
!isRoot && !isRoot &&
!isBlock &&
definition?.draggable !== false definition?.draggable !== false
$: droppable = interactive $: droppable = interactive
$: builderHidden = $: builderHidden =
@ -194,6 +195,7 @@
interactive, interactive,
draggable, draggable,
editable, editable,
isBlock,
}, },
empty: emptyState, empty: emptyState,
selected, selected,

View File

@ -10,7 +10,6 @@
export let size export let size
export let disabled export let disabled
export let fields export let fields
export let labelPosition
export let title export let title
export let description export let description
export let showDeleteButton export let showDeleteButton
@ -97,7 +96,6 @@
size, size,
disabled, disabled,
fields: fieldsOrDefault, fields: fieldsOrDefault,
labelPosition,
title, title,
description, description,
saveButtonLabel: saveLabel, saveButtonLabel: saveLabel,

View File

@ -2,6 +2,7 @@
import BlockComponent from "components/BlockComponent.svelte" import BlockComponent from "components/BlockComponent.svelte"
import Placeholder from "components/app/Placeholder.svelte" import Placeholder from "components/app/Placeholder.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { getContext } from "svelte"
export let dataSource export let dataSource
export let actionUrl export let actionUrl
@ -9,7 +10,6 @@
export let size export let size
export let disabled export let disabled
export let fields export let fields
export let labelPosition
export let title export let title
export let description export let description
export let saveButtonLabel export let saveButtonLabel
@ -33,6 +33,7 @@
barcodeqr: "codescanner", barcodeqr: "codescanner",
bb_reference: "bbreferencefield", bb_reference: "bbreferencefield",
} }
const context = getContext("context")
let formId let formId
@ -136,7 +137,8 @@
actionType: actionType === "Create" ? "Create" : "Update", actionType: actionType === "Create" ? "Create" : "Update",
dataSource, dataSource,
size, size,
disabled: disabled || actionType === "View", disabled,
readonly: !disabled && actionType === "View",
}} }}
styles={{ styles={{
normal: { normal: {
@ -226,16 +228,20 @@
<BlockComponent type="text" props={{ text: description }} order={1} /> <BlockComponent type="text" props={{ text: description }} order={1} />
{/if} {/if}
{#key fields} {#key fields}
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}> <BlockComponent type="container">
<div class="form-block fields" class:mobile={$context.device.mobile}>
{#each fields as field, idx} {#each fields as field, idx}
{#if getComponentForField(field) && field.active} {#if getComponentForField(field) && field.active}
<BlockComponent <BlockComponent
type={getComponentForField(field)} type={getComponentForField(field)}
props={getPropsForField(field)} props={getPropsForField(field)}
order={idx} order={idx}
interactive
name={field?.field}
/> />
{/if} {/if}
{/each} {/each}
</div>
</BlockComponent> </BlockComponent>
{/key} {/key}
</BlockComponent> </BlockComponent>
@ -245,3 +251,14 @@
text="Choose your table and add some fields to your form to get started" text="Choose your table and add some fields to your form to get started"
/> />
{/if} {/if}
<style>
.fields {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px 16px;
}
.fields.mobile :global(.spectrum-Form-item) {
grid-column: span 6 !important;
}
</style>

View File

@ -6,11 +6,13 @@
export let field export let field
export let label export let label
export let disabled = false export let disabled = false
export let readonly = false
export let compact = false export let compact = false
export let validation export let validation
export let extensions export let extensions
export let onChange export let onChange
export let maximum = undefined export let maximum = undefined
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -71,17 +73,18 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{span}
type="attachment" type="attachment"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
defaultValue={[]} defaultValue={[]}
> >
<div class="minHeightWrapper">
{#if fieldState} {#if fieldState}
<CoreDropzone <CoreDropzone
value={fieldState.value} value={fieldState.value}
disabled={fieldState.disabled} disabled={fieldState.disabled || fieldState.readonly}
error={fieldState.error} error={fieldState.error}
on:change={handleChange} on:change={handleChange}
{processFiles} {processFiles}
@ -93,11 +96,4 @@
{compact} {compact}
/> />
{/if} {/if}
</div>
</Field> </Field>
<style>
.minHeightWrapper {
min-height: 80px;
}
</style>

View File

@ -6,6 +6,7 @@
export let label export let label
export let text export let text
export let disabled = false export let disabled = false
export let readonly = false
export let size export let size
export let validation export let validation
export let defaultValue export let defaultValue
@ -39,6 +40,7 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
defaultValue={isTruthy(defaultValue)} defaultValue={isTruthy(defaultValue)}
type="boolean" type="boolean"
@ -49,6 +51,7 @@
<CoreCheckbox <CoreCheckbox
value={fieldState.value} value={fieldState.value}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{size} {size}

View File

@ -11,6 +11,7 @@
export let beepOnScan = false export let beepOnScan = false
export let beepFrequency = 2637 export let beepFrequency = 2637
export let customFrequency = 1046 export let customFrequency = 1046
export let preferredCamera = "environment"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -20,7 +21,7 @@
let cameraEnabled let cameraEnabled
let cameraStarted = false let cameraStarted = false
let html5QrCode let html5QrCode
let cameraSetting = { facingMode: "environment" } let cameraSetting = { facingMode: preferredCamera }
let cameraConfig = { let cameraConfig = {
fps: 25, fps: 25,
qrbox: { width: 250, height: 250 }, qrbox: { width: 250, height: 250 },

View File

@ -6,6 +6,7 @@
export let label export let label
export let type = "barcodeqr" export let type = "barcodeqr"
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let defaultValue = "" export let defaultValue = ""
export let onChange export let onChange
@ -14,6 +15,7 @@
export let beepOnScan export let beepOnScan
export let beepFrequency export let beepFrequency
export let customFrequency export let customFrequency
export let preferredCamera
let fieldState let fieldState
let fieldApi let fieldApi
@ -32,6 +34,7 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{type} {type}
@ -42,12 +45,13 @@
<CodeScanner <CodeScanner
value={fieldState.value} value={fieldState.value}
on:change={handleUpdate} on:change={handleUpdate}
disabled={fieldState.disabled} disabled={fieldState.disabled || fieldState.readonly}
{allowManualEntry} {allowManualEntry}
scanButtonText={scanText} scanButtonText={scanText}
{beepOnScan} {beepOnScan}
{beepFrequency} {beepFrequency}
{customFrequency} {customFrequency}
{preferredCamera}
/> />
{/if} {/if}
</Field> </Field>

View File

@ -6,6 +6,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let enableTime = true export let enableTime = true
export let timeOnly = false export let timeOnly = false
export let time24hr = false export let time24hr = false
@ -13,6 +14,7 @@
export let validation export let validation
export let defaultValue export let defaultValue
export let onChange export let onChange
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -29,8 +31,10 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{span}
type="datetime" type="datetime"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
@ -40,6 +44,7 @@
value={fieldState.value} value={fieldState.value}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
appendTo={document.getElementById("flatpickr-root")} appendTo={document.getElementById("flatpickr-root")}

View File

@ -1,6 +1,5 @@
<script> <script>
import Placeholder from "../Placeholder.svelte" import Placeholder from "../Placeholder.svelte"
import FieldGroupFallback from "./FieldGroupFallback.svelte"
import { getContext, onDestroy } from "svelte" import { getContext, onDestroy } from "svelte"
export let label export let label
@ -11,7 +10,9 @@
export let defaultValue export let defaultValue
export let type export let type
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let span = 6
// Get contexts // Get contexts
const formContext = getContext("form") const formContext = getContext("form")
@ -29,6 +30,7 @@
type, type,
defaultValue, defaultValue,
disabled, disabled,
readonly,
validation, validation,
formStep formStep
) )
@ -62,14 +64,21 @@
}) })
</script> </script>
<FieldGroupFallback> <div
<div class="spectrum-Form-item" use:styleable={$component.styles}> class="spectrum-Form-item"
class:span-2={span === 2}
class:span-3={span === 3}
class:span-6={span === 6 || !span}
use:styleable={$component.styles}
class:above={labelPos === "above"}
>
{#key $component.editing} {#key $component.editing}
<label <label
bind:this={labelNode} bind:this={labelNode}
contenteditable={$component.editing} contenteditable={$component.editing}
on:blur={$component.editing ? updateLabel : null} on:blur={$component.editing ? updateLabel : null}
class:hidden={!label} class:hidden={!label}
class:readonly
for={fieldState?.fieldId} for={fieldState?.fieldId}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`} class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
> >
@ -92,10 +101,22 @@
{/if} {/if}
{/if} {/if}
</div> </div>
</div> </div>
</FieldGroupFallback>
<style> <style>
:global(.form-block .spectrum-Form-item.span-2) {
grid-column: span 2;
}
:global(.form-block .spectrum-Form-item.span-3) {
grid-column: span 3;
}
:global(.form-block .spectrum-Form-item.span-6) {
grid-column: span 6;
}
.spectrum-Form-item.above {
display: flex;
flex-direction: column;
}
label { label {
white-space: nowrap; white-space: nowrap;
} }
@ -118,4 +139,7 @@
.spectrum-FieldLabel--left { .spectrum-FieldLabel--left {
padding-right: var(--spectrum-global-dimension-size-200); padding-right: var(--spectrum-global-dimension-size-200);
} }
.readonly {
pointer-events: none;
}
</style> </style>

View File

@ -8,6 +8,7 @@
export let theme export let theme
export let size export let size
export let disabled = false export let disabled = false
export let readonly = false
export let actionType = "Create" export let actionType = "Create"
export let initialFormStep = 1 export let initialFormStep = 1
@ -39,7 +40,7 @@
$: schemaKey = generateSchemaKey(schema) $: schemaKey = generateSchemaKey(schema)
$: initialValues = getInitialValues(actionType, dataSource, $context) $: initialValues = getInitialValues(actionType, dataSource, $context)
$: resetKey = Helpers.hashString( $: resetKey = Helpers.hashString(
schemaKey + JSON.stringify(initialValues) + disabled schemaKey + JSON.stringify(initialValues) + disabled + readonly
) )
// Returns the closes data context which isn't a built in context // Returns the closes data context which isn't a built in context
@ -97,6 +98,7 @@
{theme} {theme}
{size} {size}
{disabled} {disabled}
{readonly}
{actionType} {actionType}
{schema} {schema}
{table} {table}

View File

@ -6,6 +6,7 @@
export let dataSource export let dataSource
export let disabled = false export let disabled = false
export let readonly = false
export let initialValues export let initialValues
export let size export let size
export let schema export let schema
@ -148,6 +149,7 @@
type, type,
defaultValue = null, defaultValue = null,
fieldDisabled = false, fieldDisabled = false,
fieldReadOnly = false,
validationRules, validationRules,
step = 1 step = 1
) => { ) => {
@ -205,6 +207,7 @@
error: initialError, error: initialError,
disabled: disabled:
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns), disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
readonly: readonly || fieldReadOnly,
defaultValue, defaultValue,
validator, validator,
lastUpdate: Date.now(), lastUpdate: Date.now(),

View File

@ -7,6 +7,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let defaultValue = "" export let defaultValue = ""
export let onChange export let onChange
@ -48,6 +49,7 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
type="json" type="json"
@ -60,6 +62,7 @@
value={serialiseValue(fieldState.value)} value={serialiseValue(fieldState.value)}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}

View File

@ -8,6 +8,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let defaultValue = "" export let defaultValue = ""
export let format = "auto" export let format = "auto"
@ -58,6 +59,7 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
type="longform" type="longform"
@ -71,6 +73,7 @@
value={fieldState.value} value={fieldState.value}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}
@ -88,6 +91,7 @@
value={fieldState.value} value={fieldState.value}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}

View File

@ -6,6 +6,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let defaultValue export let defaultValue
export let optionsSource = "schema" export let optionsSource = "schema"
@ -17,6 +18,7 @@
export let onChange export let onChange
export let optionsType = "select" export let optionsType = "select"
export let direction = "vertical" export let direction = "vertical"
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -55,7 +57,9 @@
{field} {field}
{label} {label}
{disabled} {disabled}
{readonly}
{validation} {validation}
{span}
defaultValue={expandedDefaultValue} defaultValue={expandedDefaultValue}
type="array" type="array"
bind:fieldState bind:fieldState
@ -71,6 +75,7 @@
getOptionValue={flatOptions ? x => x : x => x.value} getOptionValue={flatOptions ? x => x : x => x.value}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
on:change={handleChange} on:change={handleChange}
{placeholder} {placeholder}
{options} {options}
@ -81,6 +86,7 @@
value={fieldState.value || []} value={fieldState.value || []}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
{options} {options}
{direction} {direction}

View File

@ -6,6 +6,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let optionsType = "select" export let optionsType = "select"
export let validation export let validation
export let defaultValue export let defaultValue
@ -18,6 +19,7 @@
export let direction = "vertical" export let direction = "vertical"
export let onChange export let onChange
export let sort = true export let sort = true
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -45,8 +47,10 @@
{field} {field}
{label} {label}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{span}
type="options" type="options"
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
@ -58,6 +62,7 @@
value={fieldState.value} value={fieldState.value}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
{options} {options}
{placeholder} {placeholder}
@ -72,6 +77,7 @@
value={fieldState.value} value={fieldState.value}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
{options} {options}
{direction} {direction}

View File

@ -11,6 +11,7 @@
export let label export let label
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let autocomplete = true export let autocomplete = true
export let defaultValue export let defaultValue
@ -18,6 +19,7 @@
export let filter export let filter
export let datasourceType = "table" export let datasourceType = "table"
export let primaryDisplay export let primaryDisplay
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -137,7 +139,9 @@
typeof value === "object" ? value._id : value typeof value === "object" ? value._id : value
) )
// Make sure field state is valid // Make sure field state is valid
if (values?.length > 0) {
fieldApi.setValue(values) fieldApi.setValue(values)
}
return values return values
} }
@ -183,9 +187,11 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
defaultValue={expandedDefaultValue} defaultValue={expandedDefaultValue}
{type} {type}
{span}
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
bind:fieldSchema bind:fieldSchema
@ -200,6 +206,7 @@
on:loadMore={loadMore} on:loadMore={loadMore}
id={fieldState.fieldId} id={fieldState.fieldId}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
getOptionLabel={getDisplayName} getOptionLabel={getDisplayName}
getOptionValue={option => option._id} getOptionValue={option => option._id}

View File

@ -7,10 +7,12 @@
export let placeholder export let placeholder
export let type = "text" export let type = "text"
export let disabled = false export let disabled = false
export let readonly = false
export let validation export let validation
export let defaultValue = "" export let defaultValue = ""
export let align export let align
export let onChange export let onChange
export let span
let fieldState let fieldState
let fieldApi let fieldApi
@ -27,8 +29,10 @@
{label} {label}
{field} {field}
{disabled} {disabled}
{readonly}
{validation} {validation}
{defaultValue} {defaultValue}
{span}
type={type === "number" ? "number" : "string"} type={type === "number" ? "number" : "string"}
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
@ -39,6 +43,7 @@
value={fieldState.value} value={fieldState.value}
on:change={handleChange} on:change={handleChange}
disabled={fieldState.disabled} disabled={fieldState.disabled}
readonly={fieldState.readonly}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}

View File

@ -40,6 +40,7 @@ export const styleable = (node, styles = {}) => {
const componentId = newStyles.id const componentId = newStyles.id
const customStyles = newStyles.custom || "" const customStyles = newStyles.custom || ""
const { isBlock } = newStyles
const normalStyles = { ...baseStyles, ...newStyles.normal } const normalStyles = { ...baseStyles, ...newStyles.normal }
const hoverStyles = { const hoverStyles = {
...normalStyles, ...normalStyles,
@ -76,6 +77,9 @@ export const styleable = (node, styles = {}) => {
// Handler to start editing a component (if applicable) when double // Handler to start editing a component (if applicable) when double
// clicking in the builder preview // clicking in the builder preview
editComponent = event => { editComponent = event => {
if (isBlock) {
return
}
if (newStyles.interactive && newStyles.editable) { if (newStyles.interactive && newStyles.editable) {
builderStore.actions.setEditMode(true) builderStore.actions.setEditMode(true)
} }

View File

@ -33,7 +33,7 @@ export default class UserFetch extends DataFetch {
let finalQuery let finalQuery
// convert old format to new one - we now allow use of the lucene format // convert old format to new one - we now allow use of the lucene format
const { appId, paginated, ...rest } = query const { appId, paginated, ...rest } = query
if (!LuceneUtils.hasFilters(query) && rest.email) { if (!LuceneUtils.hasFilters(query) && rest.email != null) {
finalQuery = { string: { email: rest.email } } finalQuery = { string: { email: rest.email } }
} else { } else {
finalQuery = rest finalQuery = rest

@ -1 +1 @@
Subproject commit bbbb7a1f9e4358ec8dc428f9a1034599c81f498d Subproject commit ac7785c832285255aee0b8f5309fed950f7b598c

View File

@ -254,7 +254,7 @@ export const exportRows = async (
const format = ctx.query.format const format = ctx.query.format
const { rows, columns, query } = ctx.request.body const { rows, columns, query, sort, sortOrder } = ctx.request.body
if (typeof format !== "string" || !exporters.isFormat(format)) { if (typeof format !== "string" || !exporters.isFormat(format)) {
ctx.throw( ctx.throw(
400, 400,
@ -272,6 +272,8 @@ export const exportRows = async (
rowIds: rows, rowIds: rows,
columns, columns,
query, query,
sort,
sortOrder,
}) })
ctx.attachment(fileName) ctx.attachment(fileName)
return apiFileReturn(content) return apiFileReturn(content)

View File

@ -15,6 +15,16 @@ import env from "../../../environment"
const Router = require("@koa/router") const Router = require("@koa/router")
const { RateLimit, Stores } = require("koa2-ratelimit") const { RateLimit, Stores } = require("koa2-ratelimit")
import { middleware, redis } from "@budibase/backend-core" import { middleware, redis } from "@budibase/backend-core"
import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils"
interface KoaRateLimitOptions {
socket: {
host: string
port: number
}
password?: string
database?: number
}
const PREFIX = "/api/public/v1" const PREFIX = "/api/public/v1"
// allow a lot more requests when in test // allow a lot more requests when in test
@ -29,32 +39,21 @@ function getApiLimitPerSecond(): number {
let rateLimitStore: any = null let rateLimitStore: any = null
if (!env.isTest()) { if (!env.isTest()) {
const REDIS_OPTS = redis.utils.getRedisOptions() const { password, host, port } = redis.utils.getRedisConnectionDetails()
let options let options: KoaRateLimitOptions = {
if (REDIS_OPTS.redisProtocolUrl) {
// fully qualified redis URL
options = {
url: REDIS_OPTS.redisProtocolUrl,
}
} else {
options = {
socket: { socket: {
host: REDIS_OPTS.host, host: host,
port: REDIS_OPTS.port, port: port,
}, },
} }
if (REDIS_OPTS.opts?.password || REDIS_OPTS.opts.redisOptions?.password) { if (password) {
// @ts-ignore options.password = password
options.password =
REDIS_OPTS.opts.password || REDIS_OPTS.opts.redisOptions.password
} }
if (!env.REDIS_CLUSTERED) { if (!env.REDIS_CLUSTERED) {
// @ts-ignore
// Can't set direct redis db in clustered env // Can't set direct redis db in clustered env
options.database = 1 options.database = SelectableDatabase.RATE_LIMITING
}
} }
rateLimitStore = new Stores.Redis(options) rateLimitStore = new Stores.Redis(options)
RateLimit.defaultOptions({ RateLimit.defaultOptions({

View File

@ -563,6 +563,56 @@ describe.each([
await assertRowUsage(rowUsage) await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage) await assertQueryUsage(queryUsage)
}) })
it("should not overwrite links if those links are not set", async () => {
let linkField: FieldSchema = {
type: FieldType.LINK,
name: "",
fieldName: "",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: InternalTable.USER_METADATA,
}
let table = await config.api.table.create({
name: "TestTable",
type: "table",
sourceType: TableSourceType.INTERNAL,
sourceId: INTERNAL_TABLE_SOURCE_ID,
schema: {
user1: { ...linkField, name: "user1", fieldName: "user1" },
user2: { ...linkField, name: "user2", fieldName: "user2" },
},
})
let user1 = await config.createUser()
let user2 = await config.createUser()
let row = await config.api.row.save(table._id!, {
user1: [{ _id: user1._id }],
user2: [{ _id: user2._id }],
})
let getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user1._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
let patchResp = await config.api.row.patch(table._id!, {
_id: row._id!,
_rev: row._rev!,
tableId: table._id!,
user1: [{ _id: user2._id }],
})
expect(patchResp.user1[0]._id).toEqual(user2._id)
expect(patchResp.user2[0]._id).toEqual(user2._id)
getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user2._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
})
}) })
describe("destroy", () => { describe("destroy", () => {

View File

@ -36,7 +36,7 @@ describe("Run through some parts of the automations system", () => {
it("should be able to init in builder", async () => { it("should be able to init in builder", async () => {
const automation: Automation = { const automation: Automation = {
...basicAutomation(), ...basicAutomation(),
appId: config.appId, appId: config.appId!,
} }
const fields: any = { a: 1, appId: config.appId } const fields: any = { a: 1, appId: config.appId }
await triggers.externalTrigger(automation, fields) await triggers.externalTrigger(automation, fields)

View File

@ -1,44 +0,0 @@
const setup = require("./utilities")
describe("test the update row action", () => {
let table, row, inputs
let config = setup.getConfig()
beforeAll(async () => {
await config.init()
table = await config.createTable()
row = await config.createRow()
inputs = {
rowId: row._id,
row: {
...row,
name: "Updated name",
// put a falsy option in to be removed
description: "",
}
}
})
afterAll(setup.afterAll)
it("should be able to run the action", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, inputs)
expect(res.success).toEqual(true)
const updatedRow = await config.getRow(table._id, res.id)
expect(updatedRow.name).toEqual("Updated name")
expect(updatedRow.description).not.toEqual("")
})
it("should check invalid inputs return an error", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {})
expect(res.success).toEqual(false)
})
it("should return an error when table doesn't exist", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
row: { _id: "invalid" },
rowId: "invalid",
})
expect(res.success).toEqual(false)
})
})

View File

@ -0,0 +1,169 @@
import {
FieldSchema,
FieldType,
INTERNAL_TABLE_SOURCE_ID,
InternalTable,
RelationshipType,
Row,
Table,
TableSourceType,
} from "@budibase/types"
import * as setup from "./utilities"
import * as uuid from "uuid"
describe("test the update row action", () => {
let table: Table, row: Row, inputs: any
let config = setup.getConfig()
beforeAll(async () => {
await config.init()
table = await config.createTable()
row = await config.createRow()
inputs = {
rowId: row._id,
row: {
...row,
name: "Updated name",
// put a falsy option in to be removed
description: "",
},
}
})
afterAll(setup.afterAll)
it("should be able to run the action", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, inputs)
expect(res.success).toEqual(true)
const updatedRow = await config.getRow(table._id!, res.id)
expect(updatedRow.name).toEqual("Updated name")
expect(updatedRow.description).not.toEqual("")
})
it("should check invalid inputs return an error", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {})
expect(res.success).toEqual(false)
})
it("should return an error when table doesn't exist", async () => {
const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
row: { _id: "invalid" },
rowId: "invalid",
})
expect(res.success).toEqual(false)
})
it("should not overwrite links if those links are not set", async () => {
let linkField: FieldSchema = {
type: FieldType.LINK,
name: "",
fieldName: "",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: InternalTable.USER_METADATA,
}
let table = await config.api.table.create({
name: uuid.v4(),
type: "table",
sourceType: TableSourceType.INTERNAL,
sourceId: INTERNAL_TABLE_SOURCE_ID,
schema: {
user1: { ...linkField, name: "user1", fieldName: uuid.v4() },
user2: { ...linkField, name: "user2", fieldName: uuid.v4() },
},
})
let user1 = await config.createUser()
let user2 = await config.createUser()
let row = await config.api.row.save(table._id!, {
user1: [{ _id: user1._id }],
user2: [{ _id: user2._id }],
})
let getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user1._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
let stepResp = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
rowId: row._id,
row: {
_id: row._id,
_rev: row._rev,
tableId: row.tableId,
user1: [user2._id],
user2: "",
},
})
expect(stepResp.success).toEqual(true)
getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user2._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
})
it("should overwrite links if those links are not set and we ask it do", async () => {
let linkField: FieldSchema = {
type: FieldType.LINK,
name: "",
fieldName: "",
constraints: {
type: "array",
presence: false,
},
relationshipType: RelationshipType.ONE_TO_MANY,
tableId: InternalTable.USER_METADATA,
}
let table = await config.api.table.create({
name: uuid.v4(),
type: "table",
sourceType: TableSourceType.INTERNAL,
sourceId: INTERNAL_TABLE_SOURCE_ID,
schema: {
user1: { ...linkField, name: "user1", fieldName: uuid.v4() },
user2: { ...linkField, name: "user2", fieldName: uuid.v4() },
},
})
let user1 = await config.createUser()
let user2 = await config.createUser()
let row = await config.api.row.save(table._id!, {
user1: [{ _id: user1._id }],
user2: [{ _id: user2._id }],
})
let getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user1._id)
expect(getResp.body.user2[0]._id).toEqual(user2._id)
let stepResp = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {
rowId: row._id,
row: {
_id: row._id,
_rev: row._rev,
tableId: row.tableId,
user1: [user2._id],
user2: "",
},
meta: {
fields: {
user2: {
clearRelationships: true,
},
},
},
})
expect(stepResp.success).toEqual(true)
getResp = await config.api.row.get(table._id!, row._id!)
expect(getResp.body.user1[0]._id).toEqual(user2._id)
expect(getResp.body.user2).toBeUndefined()
})
})

View File

@ -4,11 +4,11 @@ import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions"
import emitter from "../../../events/index" import emitter from "../../../events/index"
import env from "../../../environment" import env from "../../../environment"
let config: any let config: TestConfig
export function getConfig() { export function getConfig(): TestConfig {
if (!config) { if (!config) {
config = new TestConfig(false) config = new TestConfig(true)
} }
return config return config
} }

View File

@ -1,4 +1,10 @@
import { Row, SearchFilters, SearchParams } from "@budibase/types" import {
Row,
SearchFilters,
SearchParams,
SortOrder,
SortType,
} from "@budibase/types"
import { isExternalTableID } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
import * as internal from "./search/internal" import * as internal from "./search/internal"
import * as external from "./search/external" import * as external from "./search/external"
@ -32,6 +38,8 @@ export interface ExportRowsParams {
rowIds?: string[] rowIds?: string[]
columns?: string[] columns?: string[]
query?: SearchFilters query?: SearchFilters
sort?: string
sortOrder?: SortOrder
} }
export interface ExportRowsResult { export interface ExportRowsResult {

View File

@ -98,12 +98,12 @@ export async function search(options: SearchParams) {
export async function exportRows( export async function exportRows(
options: ExportRowsParams options: ExportRowsParams
): Promise<ExportRowsResult> { ): Promise<ExportRowsResult> {
const { tableId, format, columns, rowIds } = options const { tableId, format, columns, rowIds, query, sort, sortOrder } = options
const { datasourceId, tableName } = breakExternalTableId(tableId) const { datasourceId, tableName } = breakExternalTableId(tableId)
let query: SearchFilters = {} let requestQuery: SearchFilters = {}
if (rowIds?.length) { if (rowIds?.length) {
query = { requestQuery = {
oneOf: { oneOf: {
_id: rowIds.map((row: string) => { _id: rowIds.map((row: string) => {
const ids = JSON.parse( const ids = JSON.parse(
@ -119,6 +119,8 @@ export async function exportRows(
}), }),
}, },
} }
} else {
requestQuery = query || {}
} }
const datasource = await sdk.datasources.get(datasourceId!) const datasource = await sdk.datasources.get(datasourceId!)
@ -126,7 +128,7 @@ export async function exportRows(
throw new HTTPError("Datasource has not been configured for plus API.", 400) throw new HTTPError("Datasource has not been configured for plus API.", 400)
} }
let result = await search({ tableId, query }) let result = await search({ tableId, query: requestQuery, sort, sortOrder })
let rows: Row[] = [] let rows: Row[] = []
// Filter data to only specified columns if required // Filter data to only specified columns if required

View File

@ -84,7 +84,7 @@ export async function search(options: SearchParams) {
export async function exportRows( export async function exportRows(
options: ExportRowsParams options: ExportRowsParams
): Promise<ExportRowsResult> { ): Promise<ExportRowsResult> {
const { tableId, format, rowIds, columns, query } = options const { tableId, format, rowIds, columns, query, sort, sortOrder } = options
const db = context.getAppDB() const db = context.getAppDB()
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
@ -99,7 +99,12 @@ export async function exportRows(
result = await outputProcessing(table, response) result = await outputProcessing(table, response)
} else if (query) { } else if (query) {
let searchResponse = await search({ tableId, query }) let searchResponse = await search({
tableId,
query,
sort,
sortOrder,
})
result = searchResponse.rows result = searchResponse.rows
} }

View File

@ -22,7 +22,15 @@ export class TableAPI extends TestAPI {
.send(data) .send(data)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(expectStatus)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body return res.body
} }

View File

@ -1,5 +1,6 @@
import { SearchFilters, SearchParams } from "../../../sdk" import { SearchFilters, SearchParams } from "../../../sdk"
import { Row } from "../../../documents" import { Row } from "../../../documents"
import { SortOrder } from "../../../api"
import { ReadStream } from "fs" import { ReadStream } from "fs"
export interface SaveRowRequest extends Row {} export interface SaveRowRequest extends Row {}
@ -34,6 +35,8 @@ export interface ExportRowsRequest {
rows: string[] rows: string[]
columns?: string[] columns?: string[]
query?: SearchFilters query?: SearchFilters
sort?: string
sortOrder?: SortOrder
} }
export type ExportRowsResponse = ReadStream export type ExportRowsResponse = ReadStream

View File

@ -102,6 +102,7 @@ export interface BBReferenceFieldMetadata
extends Omit<BaseFieldSchema, "subtype"> { extends Omit<BaseFieldSchema, "subtype"> {
type: FieldType.BB_REFERENCE type: FieldType.BB_REFERENCE
subtype: FieldSubtype.USER | FieldSubtype.USERS subtype: FieldSubtype.USER | FieldSubtype.USERS
relationshipType?: RelationshipType
} }
export interface FieldConstraints { export interface FieldConstraints {

View File

@ -31,10 +31,6 @@ import destroyable from "server-destroy"
import { initPro } from "./initPro" import { initPro } from "./initPro"
import { handleScimBody } from "./middleware/handleScimBody" import { handleScimBody } from "./middleware/handleScimBody"
// configure events to use the pro audit log write
// can't integrate directly into backend-core due to cyclic issues
events.processors.init(proSdk.auditLogs.write)
if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) { if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) {
console.warn( console.warn(
"Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress" "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
@ -93,6 +89,9 @@ export default server.listen(parseInt(env.PORT || "4002"), async () => {
console.log(`Worker running on ${JSON.stringify(server.address())}`) console.log(`Worker running on ${JSON.stringify(server.address())}`)
await initPro() await initPro()
await redis.init() await redis.init()
// configure events to use the pro audit log write
// can't integrate directly into backend-core due to cyclic issues
await events.processors.init(proSdk.auditLogs.write)
}) })
process.on("uncaughtException", err => { process.on("uncaughtException", err => {

View File

@ -1,7 +1,7 @@
## Description ## Description
_Describe the problem or feature in addition to a link to the relevant github issues._ _Describe the problem or feature in addition to a link to the relevant github issues._
### Addresses: ## Addresses
- `<Enter the Link to the issue(s) this PR addresses>` - `<Enter the Link to the issue(s) this PR addresses>`
- ...more if required - ...more if required