diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 3cd037d0be..3efbb3988a 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -99,11 +99,6 @@ jobs: else yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro fi - - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - name: codecov-umbrella - verbose: true test-worker: runs-on: ubuntu-latest @@ -129,12 +124,6 @@ jobs: yarn test --scope=@budibase/worker fi - - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos - name: codecov-umbrella - verbose: true - test-server: runs-on: ubuntu-latest steps: @@ -159,12 +148,6 @@ jobs: yarn test --scope=@budibase/server fi - - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos - name: codecov-umbrella - verbose: true - test-pro: runs-on: ubuntu-latest if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 49e3473e63..8f3ab9c74c 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -10,6 +10,7 @@ jobs: steps: - uses: actions/stale@v8 with: + days-before-stale: 330 operations-per-run: 1 # stale rules for PRs days-before-pr-stale: 7 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..69d69ab7d0 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ +/packages/server @Budibase/backend +/packages/worker @Budibase/backend +/packages/backend-core @Budibase/backend diff --git a/charts/budibase/templates/alb-ingress.yaml b/charts/budibase/templates/alb-ingress.yaml index 6cd1cf2cba..fd38364ba6 100644 --- a/charts/budibase/templates/alb-ingress.yaml +++ b/charts/budibase/templates/alb-ingress.yaml @@ -7,8 +7,8 @@ metadata: kubernetes.io/ingress.class: alb alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/target-type: ip - alb.ingress.kubernetes.io/success-codes: 200,301 - alb.ingress.kubernetes.io/healthcheck-path: / + alb.ingress.kubernetes.io/success-codes: '200' + alb.ingress.kubernetes.io/healthcheck-path: '/health' {{- if .Values.ingress.certificateArn }} alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}' alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' diff --git a/hosting/couchdb/runner.sh b/hosting/couchdb/runner.sh index 2e4d26122f..b576c886c2 100644 --- a/hosting/couchdb/runner.sh +++ b/hosting/couchdb/runner.sh @@ -26,27 +26,48 @@ if [[ "${TARGETBUILD}" = "aas" ]]; then sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini elif [[ "${TARGETBUILD}" = "single" ]]; then + # In the single image build, the Dockerfile specifies /data as a volume + # mount, so we use that for all persistent data. sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then # In Kubernetes the directory /opt/couchdb/data has a persistent volume # mount for storing database data. sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini - sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/couchdb/etc/local.ini + + # We remove the database_dir and view_index_dir settings from the local.ini + # in Kubernetes because it will default to /opt/couchdb/data which is what + # our Helm chart was using prior to us switching to using our own CouchDB + # image. + sed -i "s#^database_dir.*\$##g" /opt/couchdb/etc/local.ini + sed -i "s#^view_index_dir.*\$##g" /opt/couchdb/etc/local.ini + + # We remove the -name setting from the vm.args file in Kubernetes because + # it will default to the pod FQDN, which is what's required for clustering + # to work. sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args else + # For all other builds, we use /data for persistent data. sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini fi +# Start Clouseau. Budibase won't function correctly without Clouseau running, it +# powers the search API endpoints which are used to do all sorts, including +# populating app grids. /opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & + +# Start CouchDB. /docker-entrypoint.sh /opt/couchdb/bin/couchdb & +# Wati for CouchDB to start up. while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do echo 'Waiting for CouchDB to start...'; sleep 5; done +# CouchDB needs the `_users` and `_replicator` databases to exist before it will +# function correctly, so we create them here. curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator sleep infinity \ No newline at end of file diff --git a/lerna.json b/lerna.json index 7d3b11bfa6..3540818872 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.17", + "version": "2.13.30", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 67a83c4075..6edeca3674 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -72,7 +72,7 @@ "@types/tar-fs": "2.0.1", "@types/uuid": "8.3.4", "chance": "1.1.8", - "ioredis-mock": "8.7.0", + "ioredis-mock": "8.9.0", "jest": "29.7.0", "jest-environment-node": "29.7.0", "jest-serial-runner": "1.2.1", diff --git a/packages/backend-core/src/objectStore/objectStore.ts b/packages/backend-core/src/objectStore/objectStore.ts index cdaf19fa55..1971c09e9d 100644 --- a/packages/backend-core/src/objectStore/objectStore.ts +++ b/packages/backend-core/src/objectStore/objectStore.ts @@ -260,12 +260,12 @@ export async function listAllObjects(bucketName: string, path: string) { } /** - * Generate a presigned url with a default TTL of 1 hour + * Generate a presigned url with a default TTL of 36 hours */ export function getPresignedUrl( bucketName: string, key: string, - durationSeconds: number = 3600 + durationSeconds: number = 129600 ) { const objectStore = ObjectStore(bucketName, { presigning: true }) const params = { diff --git a/packages/backend-core/src/redis/redlockImpl.ts b/packages/backend-core/src/redis/redlockImpl.ts index 266f1fe989..a7b2e2b4c6 100644 --- a/packages/backend-core/src/redis/redlockImpl.ts +++ b/packages/backend-core/src/redis/redlockImpl.ts @@ -2,8 +2,9 @@ import Redlock from "redlock" import { getLockClient } from "./init" import { LockOptions, LockType } from "@budibase/types" import * as context from "../context" -import env from "../environment" import { logWarn } from "../logging" +import { utils } from "@budibase/shared-core" +import { Duration } from "../utils" async function getClient( type: LockType, @@ -12,9 +13,7 @@ async function getClient( if (type === LockType.CUSTOM) { return newRedlock(opts) } - if (env.isTest() && type !== LockType.TRY_ONCE) { - return newRedlock(OPTIONS.TEST) - } + switch (type) { case LockType.TRY_ONCE: { return newRedlock(OPTIONS.TRY_ONCE) @@ -28,13 +27,16 @@ async function getClient( case LockType.DELAY_500: { return newRedlock(OPTIONS.DELAY_500) } + case LockType.AUTO_EXTEND: { + return newRedlock(OPTIONS.AUTO_EXTEND) + } default: { - throw new Error(`Could not get redlock client: ${type}`) + throw utils.unreachable(type) } } } -const OPTIONS = { +const OPTIONS: Record = { TRY_ONCE: { // immediately throws an error if the lock is already held retryCount: 0, @@ -42,11 +44,6 @@ const OPTIONS = { TRY_TWICE: { retryCount: 1, }, - TEST: { - // higher retry count in unit tests - // due to high contention. - retryCount: 100, - }, DEFAULT: { // the expected clock drift; for more details // see http://redis.io/topics/distlock @@ -67,10 +64,14 @@ const OPTIONS = { DELAY_500: { retryDelay: 500, }, + CUSTOM: {}, + AUTO_EXTEND: { + retryCount: -1, + }, } export async function newRedlock(opts: Redlock.Options = {}) { - let options = { ...OPTIONS.DEFAULT, ...opts } + const options = { ...OPTIONS.DEFAULT, ...opts } const redisWrapper = await getLockClient() const client = redisWrapper.getClient() return new Redlock([client], options) @@ -100,17 +101,36 @@ function getLockName(opts: LockOptions) { return name } +export const AUTO_EXTEND_POLLING_MS = Duration.fromSeconds(10).toMs() + export async function doWithLock( opts: LockOptions, task: () => Promise ): Promise> { const redlock = await getClient(opts.type, opts.customOptions) - let lock + let lock: Redlock.Lock | undefined + let timeout: NodeJS.Timeout | undefined try { const name = getLockName(opts) + const ttl = + opts.type === LockType.AUTO_EXTEND ? AUTO_EXTEND_POLLING_MS : opts.ttl + // create the lock - lock = await redlock.lock(name, opts.ttl) + lock = await redlock.lock(name, ttl) + + if (opts.type === LockType.AUTO_EXTEND) { + // We keep extending the lock while the task is running + const extendInIntervals = (): void => { + timeout = setTimeout(async () => { + lock = await lock!.extend(ttl, () => opts.onExtend && opts.onExtend()) + + extendInIntervals() + }, ttl / 2) + } + + extendInIntervals() + } // perform locked task // need to await to ensure completion before unlocking @@ -131,8 +151,7 @@ export async function doWithLock( throw e } } finally { - if (lock) { - await lock.unlock() - } + clearTimeout(timeout) + await lock?.unlock() } } diff --git a/packages/backend-core/src/redis/tests/redlockImpl.spec.ts b/packages/backend-core/src/redis/tests/redlockImpl.spec.ts new file mode 100644 index 0000000000..a1e83d8e6c --- /dev/null +++ b/packages/backend-core/src/redis/tests/redlockImpl.spec.ts @@ -0,0 +1,105 @@ +import { LockName, LockType, LockOptions } from "@budibase/types" +import { AUTO_EXTEND_POLLING_MS, doWithLock } from "../redlockImpl" +import { DBTestConfiguration, generator } from "../../../tests" + +describe("redlockImpl", () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + describe("doWithLock", () => { + const config = new DBTestConfiguration() + const lockTtl = AUTO_EXTEND_POLLING_MS + + function runLockWithExecutionTime({ + opts, + task, + executionTimeMs, + }: { + opts: LockOptions + task: () => Promise + executionTimeMs: number + }) { + return config.doInTenant(() => + doWithLock(opts, async () => { + // Run in multiple intervals until hitting the expected time + const interval = lockTtl / 10 + for (let i = executionTimeMs; i > 0; i -= interval) { + await jest.advanceTimersByTimeAsync(interval) + } + return task() + }) + ) + } + + it.each(Object.values(LockType))( + "should return the task value and release the lock", + async (lockType: LockType) => { + const expectedResult = generator.guid() + const mockTask = jest.fn().mockResolvedValue(expectedResult) + + const opts: LockOptions = { + name: LockName.PERSIST_WRITETHROUGH, + type: lockType, + ttl: lockTtl, + } + + const result = await runLockWithExecutionTime({ + opts, + task: mockTask, + executionTimeMs: 0, + }) + + expect(result.executed).toBe(true) + expect(result.executed && result.result).toBe(expectedResult) + expect(mockTask).toHaveBeenCalledTimes(1) + } + ) + + it("should extend when type is autoextend", async () => { + const expectedResult = generator.guid() + const mockTask = jest.fn().mockResolvedValue(expectedResult) + const mockOnExtend = jest.fn() + + const opts: LockOptions = { + name: LockName.PERSIST_WRITETHROUGH, + type: LockType.AUTO_EXTEND, + onExtend: mockOnExtend, + } + + const result = await runLockWithExecutionTime({ + opts, + task: mockTask, + executionTimeMs: lockTtl * 2.5, + }) + + expect(result.executed).toBe(true) + expect(result.executed && result.result).toBe(expectedResult) + expect(mockTask).toHaveBeenCalledTimes(1) + expect(mockOnExtend).toHaveBeenCalledTimes(5) + }) + + it.each(Object.values(LockType).filter(t => t !== LockType.AUTO_EXTEND))( + "should timeout when type is %s", + async (lockType: LockType) => { + const mockTask = jest.fn().mockResolvedValue("mockResult") + + const opts: LockOptions = { + name: LockName.PERSIST_WRITETHROUGH, + type: lockType, + ttl: lockTtl, + } + + await expect( + runLockWithExecutionTime({ + opts, + task: mockTask, + executionTimeMs: lockTtl * 2, + }) + ).rejects.toThrowError( + `Unable to fully release the lock on resource \"lock:${config.tenantId}_persist_writethrough\".` + ) + } + ) + }) +}) diff --git a/packages/bbui/src/Form/Core/Switch.svelte b/packages/bbui/src/Form/Core/Switch.svelte index ffbef6b27d..deffc19167 100644 --- a/packages/bbui/src/Form/Core/Switch.svelte +++ b/packages/bbui/src/Form/Core/Switch.svelte @@ -18,6 +18,7 @@ checked={value} {disabled} on:change={onChange} + on:click {id} type="checkbox" class="spectrum-Switch-input" diff --git a/packages/bbui/src/Form/Core/TextField.svelte b/packages/bbui/src/Form/Core/TextField.svelte index b10997c904..3335d3567b 100644 --- a/packages/bbui/src/Form/Core/TextField.svelte +++ b/packages/bbui/src/Form/Core/TextField.svelte @@ -20,7 +20,7 @@ let focus = false const updateValue = newValue => { - if (readonly) { + if (readonly || disabled) { return } if (type === "number") { @@ -31,14 +31,14 @@ } const onFocus = () => { - if (readonly) { + if (readonly || disabled) { return } focus = true } const onBlur = event => { - if (readonly) { + if (readonly || disabled) { return } focus = false @@ -46,14 +46,14 @@ } const onInput = event => { - if (readonly || !updateOnChange) { + if (readonly || !updateOnChange || disabled) { return } updateValue(event.target.value) } const updateValueOnEnter = event => { - if (readonly) { + if (readonly || disabled) { return } if (event.key === "Enter") { @@ -69,6 +69,7 @@ } onMount(() => { + if (disabled) return focus = autofocus if (focus) field.focus() }) @@ -108,4 +109,16 @@ .spectrum-Textfield { width: 100%; } + + input::placeholder { + color: var(--grey-7); + } + + input:hover::placeholder { + color: var(--grey-7) !important; + } + + input:focus::placeholder { + color: var(--grey-7) !important; + } diff --git a/packages/bbui/src/Form/Toggle.svelte b/packages/bbui/src/Form/Toggle.svelte index d452aa8f80..1fbad949ca 100644 --- a/packages/bbui/src/Form/Toggle.svelte +++ b/packages/bbui/src/Form/Toggle.svelte @@ -19,5 +19,5 @@ - + diff --git a/packages/builder/src/builderStore/componentUtils.js b/packages/builder/src/builderStore/componentUtils.js index 522dbae416..a7ee3ff351 100644 --- a/packages/builder/src/builderStore/componentUtils.js +++ b/packages/builder/src/builderStore/componentUtils.js @@ -1,4 +1,5 @@ import { store } from "./index" +import { get } from "svelte/store" import { Helpers } from "@budibase/bbui" import { decodeJSBinding, @@ -238,6 +239,10 @@ export const makeComponentUnique = component => { } export const getComponentText = component => { + if (component == null) { + return "" + } + if (component?._instanceName) { return component._instanceName } @@ -246,3 +251,16 @@ export const getComponentText = component => { "component" return capitalise(type) } + +export const getComponentName = component => { + if (component == null) { + return "" + } + + const components = get(store)?.components || {} + const componentDefinition = components[component._component] || {} + const name = + componentDefinition.friendlyName || componentDefinition.name || "" + + return name +} diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 85ac822006..d86e94aba2 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -29,6 +29,12 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g +const UpdateReferenceAction = { + ADD: "add", + DELETE: "delete", + MOVE: "move", +} + /** * Gets all bindable data context fields and instance fields. */ @@ -1226,3 +1232,81 @@ export const runtimeToReadableBinding = ( "readableBinding" ) } + +/** + * Used to update binding references for automation or action steps + * + * @param obj - The object to be updated + * @param originalIndex - The original index of the step being moved. Not applicable to add/delete. + * @param modifiedIndex - The new index of the step being modified + * @param action - Used to determine if a step is being added, deleted or moved + * @param label - The binding text that describes the steps + */ +export const updateReferencesInObject = ({ + obj, + modifiedIndex, + action, + label, + originalIndex, +}) => { + const stepIndexRegex = new RegExp(`{{\\s*${label}\\.(\\d+)\\.`, "g") + const updateActionStep = (str, index, replaceWith) => + str.replace(`{{ ${label}.${index}.`, `{{ ${label}.${replaceWith}.`) + for (const key in obj) { + if (typeof obj[key] === "string") { + let matches + while ((matches = stepIndexRegex.exec(obj[key])) !== null) { + const referencedStep = parseInt(matches[1]) + if ( + action === UpdateReferenceAction.ADD && + referencedStep >= modifiedIndex + ) { + obj[key] = updateActionStep( + obj[key], + referencedStep, + referencedStep + 1 + ) + } else if ( + action === UpdateReferenceAction.DELETE && + referencedStep > modifiedIndex + ) { + obj[key] = updateActionStep( + obj[key], + referencedStep, + referencedStep - 1 + ) + } else if (action === UpdateReferenceAction.MOVE) { + if (referencedStep === originalIndex) { + obj[key] = updateActionStep(obj[key], referencedStep, modifiedIndex) + } else if ( + modifiedIndex <= referencedStep && + modifiedIndex < originalIndex + ) { + obj[key] = updateActionStep( + obj[key], + referencedStep, + referencedStep + 1 + ) + } else if ( + modifiedIndex >= referencedStep && + modifiedIndex > originalIndex + ) { + obj[key] = updateActionStep( + obj[key], + referencedStep, + referencedStep - 1 + ) + } + } + } + } else if (typeof obj[key] === "object" && obj[key] !== null) { + updateReferencesInObject({ + obj: obj[key], + modifiedIndex, + action, + label, + originalIndex, + }) + } + } +} diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 38b7cdf7c2..ece17cb46f 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -4,7 +4,7 @@ import { getTemporalStore } from "./store/temporal" import { getThemeStore } from "./store/theme" import { getUserStore } from "./store/users" import { getDeploymentStore } from "./store/deployments" -import { derived, writable, get } from "svelte/store" +import { derived, get } from "svelte/store" import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" import { createHistoryStore } from "builderStore/store/history" @@ -146,5 +146,3 @@ export const userSelectedResourceMap = derived(userStore, $userStore => { export const isOnlyUser = derived(userStore, $userStore => { return $userStore.length < 2 }) - -export const screensHeight = writable("210px") diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index ba2458f414..af83f73dc6 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -4,6 +4,7 @@ import { cloneDeep } from "lodash/fp" import { generate } from "shortid" import { selectedAutomation } from "builderStore" import { notifications } from "@budibase/bbui" +import { updateReferencesInObject } from "builderStore/dataBinding" const initialAutomationState = { automations: [], @@ -22,34 +23,14 @@ export const getAutomationStore = () => { return store } -const updateReferencesInObject = (obj, modifiedIndex, action) => { - const regex = /{{\s*steps\.(\d+)\./g - for (const key in obj) { - if (typeof obj[key] === "string") { - let matches - while ((matches = regex.exec(obj[key])) !== null) { - const referencedStep = parseInt(matches[1]) - if (action === "add" && referencedStep >= modifiedIndex) { - obj[key] = obj[key].replace( - `{{ steps.${referencedStep}.`, - `{{ steps.${referencedStep + 1}.` - ) - } else if (action === "delete" && referencedStep > modifiedIndex) { - obj[key] = obj[key].replace( - `{{ steps.${referencedStep}.`, - `{{ steps.${referencedStep - 1}.` - ) - } - } - } else if (typeof obj[key] === "object" && obj[key] !== null) { - updateReferencesInObject(obj[key], modifiedIndex, action) - } - } -} - const updateStepReferences = (steps, modifiedIndex, action) => { steps.forEach(step => { - updateReferencesInObject(step.inputs, modifiedIndex, action) + updateReferencesInObject({ + obj: step.inputs, + modifiedIndex, + action, + label: "steps", + }) }) } diff --git a/packages/builder/src/builderStore/tests/dataBinding.test.js b/packages/builder/src/builderStore/tests/dataBinding.test.js index 47f6564749..039e33a94d 100644 --- a/packages/builder/src/builderStore/tests/dataBinding.test.js +++ b/packages/builder/src/builderStore/tests/dataBinding.test.js @@ -2,6 +2,7 @@ import { expect, describe, it, vi } from "vitest" import { runtimeToReadableBinding, readableToRuntimeBinding, + updateReferencesInObject, } from "../dataBinding" vi.mock("@budibase/frontend-core") @@ -84,3 +85,461 @@ describe("readableToRuntimeBinding", () => { ).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`) }) }) + +describe("updateReferencesInObject", () => { + it("should increment steps in sequence on 'add'", () => { + let obj = [ + { + id: "a0", + parameters: { + text: "Alpha", + }, + }, + { + id: "a1", + parameters: { + text: "Apple", + }, + }, + { + id: "b2", + parameters: { + text: "Banana {{ actions.1.row }}", + }, + }, + { + id: "c3", + parameters: { + text: "Carrot {{ actions.1.row }}", + }, + }, + { + id: "d4", + parameters: { + text: "Dog {{ actions.3.row }}", + }, + }, + { + id: "e5", + parameters: { + text: "Eagle {{ actions.4.row }}", + }, + }, + ] + updateReferencesInObject({ + obj, + modifiedIndex: 0, + action: "add", + label: "actions", + }) + + expect(obj).toEqual([ + { + id: "a0", + parameters: { + text: "Alpha", + }, + }, + { + id: "a1", + parameters: { + text: "Apple", + }, + }, + { + id: "b2", + parameters: { + text: "Banana {{ actions.2.row }}", + }, + }, + { + id: "c3", + parameters: { + text: "Carrot {{ actions.2.row }}", + }, + }, + { + id: "d4", + parameters: { + text: "Dog {{ actions.4.row }}", + }, + }, + { + id: "e5", + parameters: { + text: "Eagle {{ actions.5.row }}", + }, + }, + ]) + }) + + it("should decrement steps in sequence on 'delete'", () => { + let obj = [ + { + id: "a1", + parameters: { + text: "Apple", + }, + }, + { + id: "b2", + parameters: { + text: "Banana {{ actions.1.row }}", + }, + }, + { + id: "d4", + parameters: { + text: "Dog {{ actions.3.row }}", + }, + }, + { + id: "e5", + parameters: { + text: "Eagle {{ actions.4.row }}", + }, + }, + ] + updateReferencesInObject({ + obj, + modifiedIndex: 2, + action: "delete", + label: "actions", + }) + + expect(obj).toEqual([ + { + id: "a1", + parameters: { + text: "Apple", + }, + }, + { + id: "b2", + parameters: { + text: "Banana {{ actions.1.row }}", + }, + }, + { + id: "d4", + parameters: { + text: "Dog {{ actions.2.row }}", + }, + }, + { + id: "e5", + parameters: { + text: "Eagle {{ actions.3.row }}", + }, + }, + ]) + }) + + it("should handle on 'move' to a lower index", () => { + let obj = [ + { + id: "a1", + parameters: { + text: "Apple", + }, + }, + { + id: "b2", + parameters: { + text: "Banana {{ actions.0.row }}", + }, + }, + { + id: "e5", + parameters: { + text: "Eagle {{ actions.3.row }}", + }, + }, + { + id: "c3", + parameters: { + text: "Carrot {{ actions.0.row }}", + }, + }, + { + id: "d4", + parameters: { + text: "Dog {{ actions.2.row }}", + }, + }, + ] + updateReferencesInObject({ + obj, + modifiedIndex: 2, + action: "move", + label: "actions", + originalIndex: 4, + }) + + expect(obj).toEqual([ + { + id: "a1", + parameters: { + text: "Apple", + }, + }, + { + id: "b2", + parameters: { + text: "Banana {{ actions.0.row }}", + }, + }, + { + id: "e5", + parameters: { + text: "Eagle {{ actions.4.row }}", + }, + }, + { + id: "c3", + parameters: { + text: "Carrot {{ actions.0.row }}", + }, + }, + { + id: "d4", + parameters: { + text: "Dog {{ actions.3.row }}", + }, + }, + ]) + }) + + it("should handle on 'move' to a higher index", () => { + let obj = [ + { + id: "b2", + parameters: { + text: "Banana {{ actions.0.row }}", + }, + }, + { + id: "c3", + parameters: { + text: "Carrot {{ actions.0.row }}", + }, + }, + { + id: "a1", + parameters: { + text: "Apple", + }, + }, + { + id: "d4", + parameters: { + text: "Dog {{ actions.2.row }}", + }, + }, + { + id: "e5", + parameters: { + text: "Eagle {{ actions.3.row }}", + }, + }, + ] + updateReferencesInObject({ + obj, + modifiedIndex: 2, + action: "move", + label: "actions", + originalIndex: 0, + }) + + expect(obj).toEqual([ + { + id: "b2", + parameters: { + text: "Banana {{ actions.2.row }}", + }, + }, + { + id: "c3", + parameters: { + text: "Carrot {{ actions.2.row }}", + }, + }, + { + id: "a1", + parameters: { + text: "Apple", + }, + }, + { + id: "d4", + parameters: { + text: "Dog {{ actions.1.row }}", + }, + }, + { + id: "e5", + parameters: { + text: "Eagle {{ actions.3.row }}", + }, + }, + ]) + }) + + it("should handle on 'move' of action being referenced, dragged to a higher index", () => { + let obj = [ + { + "##eventHandlerType": "Validate Form", + id: "cCD0Dwcnq", + }, + { + "##eventHandlerType": "Close Screen Modal", + id: "3fbbIOfN0H", + }, + { + "##eventHandlerType": "Save Row", + parameters: { + tableId: "ta_bb_employee", + }, + id: "aehg5cTmhR", + }, + { + "##eventHandlerType": "Close Side Panel", + id: "mzkpf86cxo", + }, + { + "##eventHandlerType": "Navigate To", + id: "h0uDFeJa8A", + }, + { + parameters: { + autoDismiss: true, + type: "success", + message: "{{ actions.1.row }}", + }, + "##eventHandlerType": "Show Notification", + id: "JEI5lAyJZ", + }, + ] + updateReferencesInObject({ + obj, + modifiedIndex: 2, + action: "move", + label: "actions", + originalIndex: 1, + }) + + expect(obj).toEqual([ + { + "##eventHandlerType": "Validate Form", + id: "cCD0Dwcnq", + }, + { + "##eventHandlerType": "Close Screen Modal", + id: "3fbbIOfN0H", + }, + { + "##eventHandlerType": "Save Row", + parameters: { + tableId: "ta_bb_employee", + }, + id: "aehg5cTmhR", + }, + { + "##eventHandlerType": "Close Side Panel", + id: "mzkpf86cxo", + }, + { + "##eventHandlerType": "Navigate To", + id: "h0uDFeJa8A", + }, + { + parameters: { + autoDismiss: true, + type: "success", + message: "{{ actions.2.row }}", + }, + "##eventHandlerType": "Show Notification", + id: "JEI5lAyJZ", + }, + ]) + }) + + it("should handle on 'move' of action being referenced, dragged to a lower index", () => { + let obj = [ + { + "##eventHandlerType": "Save Row", + parameters: { + tableId: "ta_bb_employee", + }, + id: "aehg5cTmhR", + }, + { + "##eventHandlerType": "Validate Form", + id: "cCD0Dwcnq", + }, + { + "##eventHandlerType": "Close Screen Modal", + id: "3fbbIOfN0H", + }, + { + "##eventHandlerType": "Close Side Panel", + id: "mzkpf86cxo", + }, + { + "##eventHandlerType": "Navigate To", + id: "h0uDFeJa8A", + }, + { + parameters: { + autoDismiss: true, + type: "success", + message: "{{ actions.4.row }}", + }, + "##eventHandlerType": "Show Notification", + id: "JEI5lAyJZ", + }, + ] + updateReferencesInObject({ + obj, + modifiedIndex: 0, + action: "move", + label: "actions", + originalIndex: 4, + }) + + expect(obj).toEqual([ + { + "##eventHandlerType": "Save Row", + parameters: { + tableId: "ta_bb_employee", + }, + id: "aehg5cTmhR", + }, + { + "##eventHandlerType": "Validate Form", + id: "cCD0Dwcnq", + }, + { + "##eventHandlerType": "Close Screen Modal", + id: "3fbbIOfN0H", + }, + { + "##eventHandlerType": "Close Side Panel", + id: "mzkpf86cxo", + }, + { + "##eventHandlerType": "Navigate To", + id: "h0uDFeJa8A", + }, + { + parameters: { + autoDismiss: true, + type: "success", + message: "{{ actions.0.row }}", + }, + "##eventHandlerType": "Show Notification", + id: "JEI5lAyJZ", + }, + ]) + }) +}) diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte index 86235297ae..3fd2708186 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte @@ -64,7 +64,7 @@ {:else if schema.type === "link"} onChange(e, field)} useLabel={false} diff --git a/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte b/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte index 3434219384..1645ded66b 100644 --- a/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte @@ -22,7 +22,7 @@ { if (e.key.toLowerCase() === "enter") { e.target.blur() diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte index 09f97302fd..46eda34c2b 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte @@ -25,6 +25,7 @@ diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte index 337b91a033..e3e24d7b13 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte @@ -1,108 +1,50 @@ - -
+
$goto("../new")} />
@@ -110,6 +52,7 @@ {#if filteredScreens?.length} {#each filteredScreens as screen (screen._id)} @@ -148,14 +93,12 @@ min-height: 147px; max-height: calc(100% - 147px); position: relative; - transition: height 300ms ease-out; + transition: height 300ms ease-out, max-height 300ms ease-out; + height: 210px; } - .screens.search { - max-height: none; - } - .screens.resizing { - user-select: none; - cursor: row-resize; + .screens.searching { + max-height: 100%; + height: 100% !important; } .header { @@ -177,9 +120,6 @@ overflow: auto; flex-grow: 1; } - .screens.resizing .content { - pointer-events: none; - } .screens :global(.nav-item) { padding-right: 8px !important; @@ -217,4 +157,10 @@ .divider:hover:after { background: var(--spectrum-global-color-gray-300); } + .divider.disabled { + cursor: auto; + } + .divider.disabled:after { + background: var(--spectrum-global-color-gray-200); + } diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_layout.svelte index 0e630b4f39..ab29f2ea0d 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_layout.svelte @@ -40,6 +40,7 @@ } .content { + width: 100vw; display: flex; flex-direction: row; justify-content: flex-start; diff --git a/packages/builder/src/pages/builder/portal/apps/_layout.svelte b/packages/builder/src/pages/builder/portal/apps/_layout.svelte index 38c0274eca..8810edca9c 100644 --- a/packages/builder/src/pages/builder/portal/apps/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/apps/_layout.svelte @@ -14,7 +14,7 @@ import PortalSideBar from "./_components/PortalSideBar.svelte" // Don't block loading if we've already hydrated state - let loaded = $apps.length != null + let loaded = !!$apps?.length onMount(async () => { try { diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index ad0d3658ea..cf2c61b11d 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -1,5 +1,6 @@
diff --git a/packages/client/manifest.json b/packages/client/manifest.json index c7a207fc28..fdb0ad9db1 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -6056,18 +6056,6 @@ "options": ["Create", "Update", "View"], "defaultValue": "Create" }, - { - "type": "text", - "label": "Title", - "key": "title", - "nested": true - }, - { - "type": "text", - "label": "Description", - "key": "description", - "nested": true - }, { "section": true, "dependsOn": { @@ -6075,7 +6063,7 @@ "value": "Create", "invert": true }, - "name": "Row details", + "name": "Row ID", "info": "How to pass a row ID using bindings", "settings": [ { @@ -6095,8 +6083,20 @@ }, { "section": true, - "name": "Fields", + "name": "Details", "settings": [ + { + "type": "text", + "label": "Title", + "key": "title", + "nested": true + }, + { + "type": "text", + "label": "Description", + "key": "description", + "nested": true + }, { "type": "fieldConfiguration", "key": "fields", diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte index 41a071686e..fcdc93020e 100644 --- a/packages/client/src/components/app/forms/RelationshipField.svelte +++ b/packages/client/src/components/app/forms/RelationshipField.svelte @@ -1,6 +1,6 @@