1
0
Fork 0
mirror of synced 2024-09-29 16:51:33 +13:00

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"}
./versionCommit.sh $BUMP_TYPE
new_version=$(./getCurrentVersion.sh)
cd ..
new_version=$(./scripts/getCurrentVersion.sh)
echo "version=$new_version" >> $GITHUB_OUTPUT
trigger-release:

View file

@ -2,16 +2,18 @@ server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
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;
error_log /dev/stderr warn;
access_log /dev/stdout main;
client_max_body_size 1000m;
ignore_invalid_headers off;
proxy_buffering 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/ {
default_type "text/plain";
root /var/www/html;
@ -47,6 +49,24 @@ server {
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/ {
# calls to the API are rate limited with bursting
limit_req zone=ratelimit burst=20 nodelay;
@ -70,18 +90,49 @@ server {
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 / {
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;
proxy_set_header Host $http_host;
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;
}
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_body_timeout 60;
keepalive_timeout 60;

View file

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

View file

@ -30,6 +30,7 @@ export * as timers from "./timers"
export { default as env } from "./environment"
export * as blacklist from "./blacklist"
export * as docUpdates from "./docUpdates"
export * from "./utils/Duration"
export { SearchParams } from "./db"
// Add context to tenancy for backwards compatibility
// 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
* 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._opts = opts
this._messages = []

View file

@ -2,11 +2,17 @@ import env from "../environment"
import { getRedisOptions } from "../redis/utils"
import { JobQueue } from "./constants"
import InMemoryQueue from "./inMemoryQueue"
import BullQueue from "bull"
import BullQueue, { QueueOptions } from "bull"
import { addListeners, StalledFn } from "./listeners"
import { Duration } from "../utils"
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 cleanupInterval: NodeJS.Timeout
@ -20,8 +26,15 @@ export function createQueue<T>(
jobQueue: JobQueue,
opts: { removeStalledCb?: StalledFn } = {}
): BullQueue.Queue<T> {
const { opts: redisOpts, redisProtocolUrl } = getRedisOptions()
const queueConfig: any = redisProtocolUrl || { redis: redisOpts }
const redisOpts = getRedisOptions()
const queueConfig: QueueOptions = {
redis: redisOpts,
settings: {
maxStalledCount: 0,
lockDuration: QUEUE_LOCK_MS,
lockRenewTime: QUEUE_LOCK_RENEW_INTERNAL_MS,
},
}
let queue: any
if (!env.isTest()) {
queue = new BullQueue(jobQueue, queueConfig)

View file

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

View file

@ -1,4 +1,5 @@
import env from "../environment"
import * as Redis from "ioredis"
const SLOT_REFRESH_MS = 2000
const CONNECT_TIMEOUT_MS = 10000
@ -42,7 +43,7 @@ export enum Databases {
export enum SelectableDatabase {
DEFAULT = 0,
SOCKET_IO = 1,
UNUSED_1 = 2,
RATE_LIMITING = 2,
UNUSED_2 = 3,
UNUSED_3 = 4,
UNUSED_4 = 5,
@ -58,7 +59,7 @@ export enum SelectableDatabase {
UNUSED_14 = 15,
}
export function getRedisOptions() {
export function getRedisConnectionDetails() {
let password = env.REDIS_PASSWORD
let url: string[] | string = env.REDIS_URL.split("//")
// get rid of the protocol
@ -74,28 +75,34 @@ export function getRedisOptions() {
}
const [host, port] = url.split(":")
let redisProtocolUrl
// fully qualified redis URL
if (/rediss?:\/\//.test(env.REDIS_URL)) {
redisProtocolUrl = env.REDIS_URL
return {
host,
password,
port: parseInt(port),
}
}
const opts: any = {
export function getRedisOptions() {
const { host, password, port } = getRedisConnectionDetails()
let redisOpts: Redis.RedisOptions = {
connectTimeout: CONNECT_TIMEOUT_MS,
port: port,
host,
password,
}
let opts: Redis.ClusterOptions | Redis.RedisOptions = redisOpts
if (env.REDIS_CLUSTERED) {
opts.redisOptions = {}
opts.redisOptions.tls = {}
opts.redisOptions.password = password
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
opts.dnsLookup = (address: string, callback: any) => callback(null, address)
} else {
opts.host = host
opts.port = port
opts.password = password
opts = {
connectTimeout: CONNECT_TIMEOUT_MS,
redisOptions: {
...redisOpts,
tls: {},
},
slotsRefreshTimeout: SLOT_REFRESH_MS,
dnsLookup: (address: string, callback: any) => callback(null, address),
} as Redis.ClusterOptions
}
return { opts, host, port: parseInt(port), redisProtocolUrl }
return opts
}
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 "./utils"
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 text = null
export let disabled = false
export let readonly = false
export let size
export let indeterminate = false
@ -24,6 +25,7 @@
class:is-invalid={!!error}
class:checked={value}
class:is-indeterminate={indeterminate}
class:readonly
>
<input
checked={value}
@ -68,4 +70,7 @@
.spectrum-Checkbox-input {
opacity: 0;
}
.readonly {
pointer-events: none;
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -44,6 +44,8 @@
const NUMBER_TYPE = FIELDS.NUMBER.type
const JSON_TYPE = FIELDS.JSON.type
const DATE_TYPE = FIELDS.DATETIME.type
const USER_TYPE = FIELDS.USER.subtype
const USERS_TYPE = FIELDS.USERS.subtype
const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
@ -287,6 +289,14 @@
if (saveColumn.type !== LINK_TYPE) {
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 {
await tables.saveField({
originalName,

View file

@ -20,7 +20,7 @@
let open = false
// Auto hide the component when another item is selected
$: if (open && $draggable.selected != componentInstance._id) {
$: if (open && $draggable.selected !== componentInstance._id) {
popover.hide()
}
@ -100,13 +100,13 @@
}}
on:close={() => {
open = false
if ($draggable.selected == componentInstance._id) {
if ($draggable.selected === componentInstance._id) {
$draggable.actions.select()
}
}}
{anchor}
align="left-outside"
showPopover={drawers.length == 0}
showPopover={drawers.length === 0}
clickOutsideOverride={drawers.length > 0}
maxHeight={600}
handlePostionUpdate={customPositionHandler}
@ -115,6 +115,7 @@
<Layout noPadding noGap>
<slot name="header" />
<ComponentSettingsSection
includeHidden
{componentInstance}
componentDefinition={parsedComponentDef}
isScreen={false}

View file

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

View file

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

View file

@ -2589,6 +2589,17 @@
"key": "disabled",
"defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "text",
"label": "Initial form step",
@ -2738,6 +2749,17 @@
"key": "disabled",
"defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "validation/string",
"label": "Validation",
@ -2776,6 +2798,35 @@
"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",
"defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "validation/number",
"label": "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",
"key": "disabled",
"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",
"label": "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",
"defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "select",
"label": "Options source",
@ -3110,6 +3270,35 @@
"type": "validation/string",
"label": "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",
"defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "select",
"label": "Type",
@ -3272,6 +3472,35 @@
"type": "validation/array",
"label": "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",
"defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "validation/boolean",
"label": "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",
"defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "validation/string",
"label": "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",
"defaultValue": false
},
{
"type": "boolean",
"label": "Read only",
"key": "readonly",
"defaultValue": false,
"dependsOn": {
"setting": "disabled",
"value": true,
"invert": true
}
},
{
"type": "validation/datetime",
"label": "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"
}
},
{
"type": "select",
"label": "Preferred camera",
"key": "preferredCamera",
"defaultValue": "environment",
"options": [
{
"label": "Front",
"value": "user"
},
{
"label": "Back",
"value": "environment"
}
]
},
{
"type": "event",
"label": "On change",
@ -3613,6 +3978,35 @@
"type": "validation/string",
"label": "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",
"label": "Disabled",
"label": "Read only",
"key": "disabled",
"defaultValue": false
},
@ -3789,6 +4183,35 @@
"type": "validation/attachment",
"label": "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",
"key": "disabled",
"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",
"key": "disabled",
"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",
"label": "Disabled",
"key": "disabled",
"defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
"defaultValue": false
}
]
},
@ -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",
"type": "select",
@ -5921,6 +6402,35 @@
"label": "Disabled",
"key": "disabled",
"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
$: inBuilder = $builderStore.inBuilder
$: instance = {
...props,
_component: getComponent(type),
_id: id,
_instanceName: getInstanceName(name, type),
_containsSlot: containsSlot,
_styles: {
...styles,
normal: styles?.normal || {},
},
_containsSlot: containsSlot,
...props,
}
// Register this block component if we're inside the builder so it can be

View file

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

View file

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

View file

@ -2,6 +2,7 @@
import BlockComponent from "components/BlockComponent.svelte"
import Placeholder from "components/app/Placeholder.svelte"
import { makePropSafe as safe } from "@budibase/string-templates"
import { getContext } from "svelte"
export let dataSource
export let actionUrl
@ -9,7 +10,6 @@
export let size
export let disabled
export let fields
export let labelPosition
export let title
export let description
export let saveButtonLabel
@ -33,6 +33,7 @@
barcodeqr: "codescanner",
bb_reference: "bbreferencefield",
}
const context = getContext("context")
let formId
@ -136,7 +137,8 @@
actionType: actionType === "Create" ? "Create" : "Update",
dataSource,
size,
disabled: disabled || actionType === "View",
disabled,
readonly: !disabled && actionType === "View",
}}
styles={{
normal: {
@ -226,16 +228,20 @@
<BlockComponent type="text" props={{ text: description }} order={1} />
{/if}
{#key fields}
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
{#each fields as field, idx}
{#if getComponentForField(field) && field.active}
<BlockComponent
type={getComponentForField(field)}
props={getPropsForField(field)}
order={idx}
/>
{/if}
{/each}
<BlockComponent type="container">
<div class="form-block fields" class:mobile={$context.device.mobile}>
{#each fields as field, idx}
{#if getComponentForField(field) && field.active}
<BlockComponent
type={getComponentForField(field)}
props={getPropsForField(field)}
order={idx}
interactive
name={field?.field}
/>
{/if}
{/each}
</div>
</BlockComponent>
{/key}
</BlockComponent>
@ -245,3 +251,14 @@
text="Choose your table and add some fields to your form to get started"
/>
{/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 label
export let disabled = false
export let readonly = false
export let compact = false
export let validation
export let extensions
export let onChange
export let maximum = undefined
export let span
let fieldState
let fieldApi
@ -71,33 +73,27 @@
{label}
{field}
{disabled}
{readonly}
{validation}
{span}
type="attachment"
bind:fieldState
bind:fieldApi
defaultValue={[]}
>
<div class="minHeightWrapper">
{#if fieldState}
<CoreDropzone
value={fieldState.value}
disabled={fieldState.disabled}
error={fieldState.error}
on:change={handleChange}
{processFiles}
{deleteAttachments}
{handleFileTooLarge}
{handleTooManyFiles}
{maximum}
{extensions}
{compact}
/>
{/if}
</div>
{#if fieldState}
<CoreDropzone
value={fieldState.value}
disabled={fieldState.disabled || fieldState.readonly}
error={fieldState.error}
on:change={handleChange}
{processFiles}
{deleteAttachments}
{handleFileTooLarge}
{handleTooManyFiles}
{maximum}
{extensions}
{compact}
/>
{/if}
</Field>
<style>
.minHeightWrapper {
min-height: 80px;
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
<script>
import Placeholder from "../Placeholder.svelte"
import FieldGroupFallback from "./FieldGroupFallback.svelte"
import { getContext, onDestroy } from "svelte"
export let label
@ -11,7 +10,9 @@
export let defaultValue
export let type
export let disabled = false
export let readonly = false
export let validation
export let span = 6
// Get contexts
const formContext = getContext("form")
@ -29,6 +30,7 @@
type,
defaultValue,
disabled,
readonly,
validation,
formStep
)
@ -62,40 +64,59 @@
})
</script>
<FieldGroupFallback>
<div class="spectrum-Form-item" use:styleable={$component.styles}>
{#key $component.editing}
<label
bind:this={labelNode}
contenteditable={$component.editing}
on:blur={$component.editing ? updateLabel : null}
class:hidden={!label}
for={fieldState?.fieldId}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
>
{label || " "}
</label>
{/key}
<div class="spectrum-Form-itemField">
{#if !formContext}
<Placeholder text="Form components need to be wrapped in a form" />
{:else if !fieldState}
<Placeholder />
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
<Placeholder
text="This Field setting is the wrong data type for this component"
/>
{:else}
<slot />
{#if fieldState.error}
<div class="error">{fieldState.error}</div>
{/if}
<div
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}
<label
bind:this={labelNode}
contenteditable={$component.editing}
on:blur={$component.editing ? updateLabel : null}
class:hidden={!label}
class:readonly
for={fieldState?.fieldId}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelClass}`}
>
{label || " "}
</label>
{/key}
<div class="spectrum-Form-itemField">
{#if !formContext}
<Placeholder text="Form components need to be wrapped in a form" />
{:else if !fieldState}
<Placeholder />
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
<Placeholder
text="This Field setting is the wrong data type for this component"
/>
{:else}
<slot />
{#if fieldState.error}
<div class="error">{fieldState.error}</div>
{/if}
</div>
{/if}
</div>
</FieldGroupFallback>
</div>
<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 {
white-space: nowrap;
}
@ -118,4 +139,7 @@
.spectrum-FieldLabel--left {
padding-right: var(--spectrum-global-dimension-size-200);
}
.readonly {
pointer-events: none;
}
</style>

View file

@ -8,6 +8,7 @@
export let theme
export let size
export let disabled = false
export let readonly = false
export let actionType = "Create"
export let initialFormStep = 1
@ -39,7 +40,7 @@
$: schemaKey = generateSchemaKey(schema)
$: initialValues = getInitialValues(actionType, dataSource, $context)
$: 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
@ -97,6 +98,7 @@
{theme}
{size}
{disabled}
{readonly}
{actionType}
{schema}
{table}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -33,7 +33,7 @@ export default class UserFetch extends DataFetch {
let finalQuery
// convert old format to new one - we now allow use of the lucene format
const { appId, paginated, ...rest } = query
if (!LuceneUtils.hasFilters(query) && rest.email) {
if (!LuceneUtils.hasFilters(query) && rest.email != null) {
finalQuery = { string: { email: rest.email } }
} else {
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 { rows, columns, query } = ctx.request.body
const { rows, columns, query, sort, sortOrder } = ctx.request.body
if (typeof format !== "string" || !exporters.isFormat(format)) {
ctx.throw(
400,
@ -272,6 +272,8 @@ export const exportRows = async (
rowIds: rows,
columns,
query,
sort,
sortOrder,
})
ctx.attachment(fileName)
return apiFileReturn(content)

View file

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

View file

@ -563,6 +563,56 @@ describe.each([
await assertRowUsage(rowUsage)
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", () => {

View file

@ -36,7 +36,7 @@ describe("Run through some parts of the automations system", () => {
it("should be able to init in builder", async () => {
const automation: Automation = {
...basicAutomation(),
appId: config.appId,
appId: config.appId!,
}
const fields: any = { a: 1, appId: config.appId }
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 env from "../../../environment"
let config: any
let config: TestConfig
export function getConfig() {
export function getConfig(): TestConfig {
if (!config) {
config = new TestConfig(false)
config = new TestConfig(true)
}
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 * as internal from "./search/internal"
import * as external from "./search/external"
@ -32,6 +38,8 @@ export interface ExportRowsParams {
rowIds?: string[]
columns?: string[]
query?: SearchFilters
sort?: string
sortOrder?: SortOrder
}
export interface ExportRowsResult {

View file

@ -98,12 +98,12 @@ export async function search(options: SearchParams) {
export async function exportRows(
options: ExportRowsParams
): Promise<ExportRowsResult> {
const { tableId, format, columns, rowIds } = options
const { tableId, format, columns, rowIds, query, sort, sortOrder } = options
const { datasourceId, tableName } = breakExternalTableId(tableId)
let query: SearchFilters = {}
let requestQuery: SearchFilters = {}
if (rowIds?.length) {
query = {
requestQuery = {
oneOf: {
_id: rowIds.map((row: string) => {
const ids = JSON.parse(
@ -119,6 +119,8 @@ export async function exportRows(
}),
},
}
} else {
requestQuery = query || {}
}
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)
}
let result = await search({ tableId, query })
let result = await search({ tableId, query: requestQuery, sort, sortOrder })
let rows: Row[] = []
// Filter data to only specified columns if required

View file

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

View file

@ -22,7 +22,15 @@ export class TableAPI extends TestAPI {
.send(data)
.set(this.config.defaultHeaders())
.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
}

View file

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

View file

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

View file

@ -31,10 +31,6 @@ import destroyable from "server-destroy"
import { initPro } from "./initPro"
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) {
console.warn(
"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())}`)
await initPro()
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 => {

View file

@ -1,7 +1,7 @@
## Description
_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>`
- ...more if required