diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 6bb2f8241f..b5f3acd0d5 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -232,3 +232,95 @@ jobs: # fi # curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} + + + try-tooljet-image-build: + runs-on: ubuntu-latest + needs: build-tooljet-image-for-ee-edtion + if: ${{ needs.build-tooljet-image-for-ee-edtion.result == 'success' }} + + steps: + - name: Checkout code to develop + if: "!contains(github.event.release.tag_name, 'ee-lts')" + uses: actions/checkout@v2 + with: + ref: refs/heads/main + + - name: Checkout code to lts-3.0 + if: contains(github.event.release.tag_name, '-ee-lts') + uses: actions/checkout@v2 + with: + ref: refs/heads/lts-3.0 + + # Create Docker Buildx builder with platform configuration + - name: Set up Docker Buildx + run: | + mkdir -p ~/.docker/cli-plugins + curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx + chmod a+x ~/.docker/cli-plugins/docker-buildx + docker buildx create --name mybuilder --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6 + docker buildx use mybuilder + + - name: Set DOCKER_CLI_EXPERIMENTAL + run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV + + - name: use mybuilder buildx + run: docker buildx use mybuilder + + - name: Docker Login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Check if Docker image is present + id: check-image-presence + run: | + response=$(curl -s "https://hub.docker.com/v2/repositories/tooljet/tooljet/tags/${{ github.event.release.tag_name }}") + if [[ $? -ne 0 ]]; then + echo "Failed to fetch JSON response. Stopping workflow execution." + exit 1 + fi + + if [[ $response == *"tag '${{ github.event.release.tag_name }}' not found"* ]]; then + echo "Docker image tag '${{ github.event.release.tag_name }}' not present." + exit 1 + else + echo "Docker image tag '${{ github.event.release.tag_name }}' is present." + fi + + - name: Build and Push Docker image for non-EE-LTS + if: "!contains(github.event.release.tag_name, '-ee-lts')" + uses: docker/build-push-action@v4 + with: + context: . + file: docker/ee/ee-try-tooljet.Dockerfile + push: true + tags: tooljet/try:${{ github.event.release.tag_name }},tooljet/try:ee-latest + platforms: linux/amd64 + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and Push Docker image for EE-LTS-3.0 + if: contains(github.event.release.tag_name, '-ee-lts') + uses: docker/build-push-action@v4 + with: + context: . + file: docker/ee/ee-try-tooljet-lts.Dockerfile + push: true + tags: tooljet/try:${{ github.event.release.tag_name }},tooljet/try:ee-lts-latest + platforms: linux/amd64 + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + + - name: Send Slack Notification + run: | + if [[ "${{ job.status }}" == "success" ]]; then + message="Try-ToolJet image published:\\n\`tooljet/try:${{ github.event.release.tag_name }}\`" + else + message="Job '${{ env.JOB_NAME }}' failed! tooljet/try:${{ github.event.release.tag_name }}" + fi + + curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/render-suspend-labeler.yml b/.github/workflows/render-suspend-labeler.yml index 7860ae3ade..e9dd2e8b9b 100644 --- a/.github/workflows/render-suspend-labeler.yml +++ b/.github/workflows/render-suspend-labeler.yml @@ -8,16 +8,16 @@ permissions: issues: write jobs: - label-stale-deploys: + label-stale-ce-deploys: runs-on: ubuntu-latest permissions: - pull-requests: write + pull-requests: write steps: - uses: akshaysasidrn/stale-label-fetch@v1.1 id: stale-label with: github-token: ${{ secrets.GITHUB_TOKEN }} - stale-label: 'active-review-app' + stale-label: 'active-ce-review-app' stale-time: '86400' type: 'pull_request' - name: Get stale numbers @@ -40,6 +40,42 @@ jobs: issue_number: prNumber, owner: context.repo.owner, repo: context.repo.repo, - labels: ['suspend-review-app'] + labels: ['suspend-ce-review-app'] + }) + } + + label-stale-ee-deploys: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: akshaysasidrn/stale-label-fetch@v1.1 + id: stale-label + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + stale-label: 'active-ee-review-app' + stale-time: '86400' + type: 'pull_request' + - name: Get stale numbers + run: echo "Matched PR numbers - ${{ steps.stale-label.outputs.stale-numbers }}" + - name: Add suspend label + uses: actions/github-script@v6 + env: + STALE_NUMBERS: ${{ steps.stale-label.outputs.stale-numbers }} + with: + github-token: ${{ secrets.TJ_BOT_PAT }} + script: | + if (!process.env.STALE_NUMBERS) return + + const prNumbers = process.env.STALE_NUMBERS.split(",") + + console.log(`Adding suspend labels for: ${prNumbers}`) + + for (const prNumber of prNumbers) { + github.rest.issues.addLabels({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['suspend-ee-review-app'] }) } diff --git a/.github/workflows/vulnerability-ci.yml b/.github/workflows/vulnerability-ci.yml index 15f8425a46..568ab6df31 100644 --- a/.github/workflows/vulnerability-ci.yml +++ b/.github/workflows/vulnerability-ci.yml @@ -11,7 +11,7 @@ on: # Schedule the workflow to run every two weeks once schedule: - - cron: '30 5 */14 * *' + - cron: '30 5 * * 1' jobs: PeriodicVulnerability-CheckOn-frontend-code: diff --git a/docker/ce-entrypoint.sh b/docker/ce-entrypoint.sh new file mode 100755 index 0000000000..4b63af2e45 --- /dev/null +++ b/docker/ce-entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +if [ -d "./server/dist" ]; then + SETUP_CMD='npm run db:setup:prod' +else + SETUP_CMD='npm run db:setup' +fi + +if [ -f "./.env" ]; then + declare $(grep -v '^#' ./.env | xargs) +fi + +if [ -z "$DATABASE_URL" ]; then + ./server/scripts/wait-for-it.sh $PG_HOST:${PG_PORT:-5432} --strict --timeout=300 -- $SETUP_CMD +else + PG_HOST=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $6}') + PG_PORT=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $7}') + + if [ -z "$DATABASE_PORT" ]; then + DATABASE_PORT="5432" + fi + + ./server/scripts/wait-for-it.sh "$PG_HOST:$PG_PORT" --strict --timeout=300 -- $SETUP_CMD +fi + +exec "$@" diff --git a/docker/ce-production.Dockerfile b/docker/ce-production.Dockerfile index 4e70ecb882..c77ebf128e 100644 --- a/docker/ce-production.Dockerfile +++ b/docker/ce-production.Dockerfile @@ -88,12 +88,13 @@ COPY --from=builder /app/frontend/build ./app/frontend/build # copy server build COPY --from=builder /app/server/package.json ./app/server/package.json COPY --from=builder /app/server/.version ./app/server/.version -COPY --from=builder /app/server/entrypoint.sh ./app/server/entrypoint.sh COPY --from=builder /app/server/node_modules ./app/server/node_modules COPY --from=builder /app/server/templates ./app/server/templates COPY --from=builder /app/server/scripts ./app/server/scripts COPY --from=builder /app/server/dist ./app/server/dist +COPY ./docker/ce-entrypoint.sh ./app/server/entrypoint.sh + # Define non-sudo user RUN useradd --create-home --home-dir /home/appuser appuser \ && chown -R appuser:0 /app \ @@ -111,5 +112,4 @@ WORKDIR /app # Dependencies for scripts outside nestjs RUN npm install dotenv@10.0.0 joi@17.4.1 - ENTRYPOINT ["./server/entrypoint.sh"] diff --git a/server/entrypoint.sh b/docker/ee/ee-entrypoint.sh similarity index 100% rename from server/entrypoint.sh rename to docker/ee/ee-entrypoint.sh diff --git a/docker/ee/ee-production.Dockerfile b/docker/ee/ee-production.Dockerfile index b69458daa1..e611643f30 100644 --- a/docker/ee/ee-production.Dockerfile +++ b/docker/ee/ee-production.Dockerfile @@ -145,12 +145,13 @@ COPY --from=builder /app/frontend/build ./app/frontend/build COPY --from=builder /app/server/package.json ./app/server/package.json COPY --from=builder /app/server/.version ./app/server/.version COPY --from=builder /app/server/ee/keys ./app/server/ee/keys -COPY --from=builder /app/server/entrypoint.sh ./app/server/entrypoint.sh COPY --from=builder /app/server/node_modules ./app/server/node_modules COPY --from=builder /app/server/templates ./app/server/templates COPY --from=builder /app/server/scripts ./app/server/scripts COPY --from=builder /app/server/dist ./app/server/dist +COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh + # Define non-sudo user RUN useradd --create-home --home-dir /home/appuser appuser \ && chown -R appuser:0 /app \ @@ -214,4 +215,4 @@ RUN npm install dotenv@10.0.0 joi@17.4.1 RUN npm cache clean --force -ENTRYPOINT ["./server/entrypoint.sh"] +ENTRYPOINT ["./server/ee-entrypoint.sh"] diff --git a/docker/ee/ee-try-entrypoint-lts.sh b/docker/ee/ee-try-entrypoint-lts.sh new file mode 100755 index 0000000000..27590534d0 --- /dev/null +++ b/docker/ee/ee-try-entrypoint-lts.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +# Start Redis +# service redis-server start +# redis-server /etc/redis/redis.conf + +# Start Postgres +service postgresql start + +# Export the PORT variable to be used by the application +export PORT=${PORT:-80} + +# Start Supervisor +exec supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/server/try-entrypoint.sh b/docker/ee/ee-try-entrypoint.sh old mode 100644 new mode 100755 similarity index 96% rename from server/try-entrypoint.sh rename to docker/ee/ee-try-entrypoint.sh index 5843b49ffd..5143e10e75 --- a/server/try-entrypoint.sh +++ b/docker/ee/ee-try-entrypoint.sh @@ -22,7 +22,7 @@ echo "Starting Temporal Server..." export PORT=${PORT:-80} # Start Supervisor -/usr/bin/supervisord -n & +exec supervisord -c /etc/supervisor/conf.d/supervisord.conf & # Wait for Temporal Server to be ready echo "Waiting for Temporal Server to be ready..." diff --git a/docker/try-tooljet.Dockerfile b/docker/ee/ee-try-tooljet-lts.Dockerfile similarity index 57% rename from docker/try-tooljet.Dockerfile rename to docker/ee/ee-try-tooljet-lts.Dockerfile index 695f17b913..5eb10b938a 100644 --- a/docker/try-tooljet.Dockerfile +++ b/docker/ee/ee-try-tooljet-lts.Dockerfile @@ -1,21 +1,31 @@ -FROM tooljet/tooljet-ce:latest +FROM tooljet/tooljet:ee-lts-latest -# copy postgrest executable -COPY --from=postgrest/postgrest:v10.1.1.20221215 /bin/postgrest /bin +# Copy PostgREST executable +COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin -# Install Postgres +# Install PostgreSQL USER root RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list -RUN echo "deb http://deb.debian.org/debian" RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor + USER postgres RUN service postgresql start && \ psql -c "create role tooljet with login superuser password 'postgres';" USER root +# Install Redis +RUN apt update && apt -y install redis + +# Create appuser home & ensure permission for supervisord and services +RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/lib/redis && \ + chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \ + chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql + +# Configure Supervisor to manage PostgREST, ToolJet, and Redis RUN echo "[supervisord] \n" \ "nodaemon=true \n" \ + "user=root \n" \ "\n" \ "[program:postgrest] \n" \ "command=/bin/postgrest \n" \ @@ -23,12 +33,23 @@ RUN echo "[supervisord] \n" \ "autorestart=true \n" \ "\n" \ "[program:tooljet] \n" \ + "user=appuser \n" \ "command=/bin/bash -c '/app/server/scripts/init-db-boot.sh' \n" \ "autostart=true \n" \ "autorestart=true \n" \ "stderr_logfile=/dev/stdout \n" \ "stderr_logfile_maxbytes=0 \n" \ "stdout_logfile=/dev/stdout \n" \ + "stdout_logfile_maxbytes=0 \n" \ + "\n" \ + "[program:redis] \n" \ + "user=appuser \n" \ + "command=/usr/bin/redis-server \n" \ + "autostart=true \n" \ + "autorestart=true \n" \ + "stderr_logfile=/dev/stdout \n" \ + "stderr_logfile_maxbytes=0 \n" \ + "stdout_logfile=/dev/stdout \n" \ "stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf # ENV defaults @@ -49,10 +70,17 @@ ENV TOOLJET_HOST=http://localhost \ PGRST_HOST=http://localhost:3000 \ PGRST_DB_URI=postgres://tooljet:postgres@localhost/tooljet_db \ PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \ + PGRST_DB_PRE_CONFIG=postgrest.pre_config \ ORM_LOGGING=true \ DEPLOYMENT_PLATFORM=docker:local \ HOME=/home/appuser \ + REDIS_HOST=localhost \ + REDIS_PORT=6379 \ + REDIS_USER=default \ + REDIS_PASS= \ TERM=xterm -# Prepare DB and start application -ENTRYPOINT service postgresql start 1> /dev/null && /usr/bin/supervisord +# Set the entrypoint +COPY ./docker/ee/ee-try-entrypoint-lts.sh /ee-try-entrypoint-lts.sh +RUN chmod +x /ee-try-entrypoint-lts +ENTRYPOINT ["/ee-try-entrypoint-lts.sh"] diff --git a/docker/ee/ee-try-tooljet.Dockerfile b/docker/ee/ee-try-tooljet.Dockerfile new file mode 100644 index 0000000000..11cbe88be3 --- /dev/null +++ b/docker/ee/ee-try-tooljet.Dockerfile @@ -0,0 +1,117 @@ +FROM tooljet/tooljet:ee-latest + +# Copy postgrest executable +COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin + +# Install Postgres +USER root +RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list +RUN echo "deb http://deb.debian.org/debian" +RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor +USER postgres +RUN service postgresql start && \ + psql -c "create role tooljet with login superuser password 'postgres';" +USER root + + +RUN apt update && apt -y install redis + +# Create appuser home & ensure permission for supervisord and services +RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/lib/redis && \ + chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \ + chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql + +# Install Temporal Server Binaries +RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.24.2/temporal_1.24.2_linux_amd64.tar.gz && \ + tar -xzf temporal_1.24.2_linux_amd64.tar.gz && \ + mv temporal-server /usr/bin/temporal-server && \ + chmod +x /usr/bin/temporal-server && \ + rm temporal_1.24.2_linux_amd64.tar.gz + +# Install Temporal UI Server Binaries +RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/ui-server_2.28.0_linux_amd64.tar.gz && \ + tar -xzf ui-server_2.28.0_linux_amd64.tar.gz && \ + mv ui-server /usr/bin/temporal-ui-server && \ + chmod +x /usr/bin/temporal-ui-server && \ + rm ui-server_2.28.0_linux_amd64.tar.gz + +# Copy Temporal configuration files +COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.yaml +COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml + +# Install grpcurl +RUN apt update && apt install -y curl \ + && curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl + +# Configure Supervisor to manage PostgREST, ToolJet, and Redis +RUN echo "[supervisord] \n" \ + "nodaemon=true \n" \ + "user=root \n" \ + "\n" \ + "[program:postgrest] \n" \ + "command=/bin/postgrest \n" \ + "autostart=true \n" \ + "autorestart=true \n" \ + "\n" \ + "[program:tooljet] \n" \ + "user=appuser \n" \ + "command=/bin/bash -c '/app/server/scripts/init-db-boot.sh' \n" \ + "autostart=true \n" \ + "autorestart=true \n" \ + "stderr_logfile=/dev/stdout \n" \ + "stderr_logfile_maxbytes=0 \n" \ + "stdout_logfile=/dev/stdout \n" \ + "stdout_logfile_maxbytes=0 \n" \ + "\n" \ + "[program:redis] \n" \ + "user=appuser \n" \ + "command=/usr/bin/redis-server \n" \ + "autostart=true \n" \ + "autorestart=true \n" \ + "stderr_logfile=/dev/stdout \n" \ + "stderr_logfile_maxbytes=0 \n" \ + "stdout_logfile=/dev/stdout \n" \ + "stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf + + +# ENV defaults +ENV TOOLJET_HOST=http://localhost \ + TOOLJET_SERVER_URL=http://localhost \ + PORT=80 \ + NODE_ENV=production \ + LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key \ + SECRET_KEY_BASE=replace_with_secret_key_base \ + PG_DB=tooljet_production \ + PG_USER=tooljet \ + PG_PASS=postgres \ + PG_HOST=localhost \ + ENABLE_TOOLJET_DB=true \ + TOOLJET_DB_HOST=localhost \ + TOOLJET_DB_USER=tooljet \ + TOOLJET_DB_PASS=postgres \ + TOOLJET_DB=tooljet_db \ + PGRST_HOST=http://localhost:3000 \ + PGRST_DB_URI=postgres://tooljet:postgres@localhost/tooljet_db \ + PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \ + PGRST_DB_PRE_CONFIG=postgrest.pre_config \ + ORM_LOGGING=true \ + DEPLOYMENT_PLATFORM=docker:local \ + HOME=/home/appuser \ + REDIS_HOST=localhost \ + REDIS_PORT=6379 \ + REDIS_USER=default \ + REDIS_PASS= \ + ENABLE_MARKETPLACE_FEATURE=true \ + TERM=xterm \ + ENABLE_WORKFLOW_SCHEDULING=true \ + TEMPORAL_SERVER_ADDRESS=localhost:7233 \ + TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS=tooljet-workflows \ + TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE=default \ + TEMPORAL_ADDRESS=localhost:7233 \ + TEMPORAL_CORS_ORIGINS=http://localhost:8080 + +# Set the entrypoint +COPY ./docker/ee/ee-try-entrypoint.sh /ee-try-entrypoint.sh +RUN chmod +x /ee-try-entrypoint.sh +ENTRYPOINT ["/ee-try-entrypoint.sh"] diff --git a/docker/ee/temporal-server.yaml b/docker/ee/temporal-server.yaml new file mode 100644 index 0000000000..bc17ed934f --- /dev/null +++ b/docker/ee/temporal-server.yaml @@ -0,0 +1,75 @@ +log: + stdout: true + level: info + +persistence: + defaultStore: sqlite-default + visibilityStore: sqlite-visibility + numHistoryShards: 4 + datastores: + sqlite-default: + sql: + pluginName: "sqlite" + databaseName: "/etc/temporal/default.db" + connectAddr: "localhost" + connectProtocol: "tcp" + connectAttributes: + cache: "private" + setup: true + + sqlite-visibility: + sql: + pluginName: "sqlite" + databaseName: "/etc/temporal/visibility.db" + connectAddr: "localhost" + connectProtocol: "tcp" + connectAttributes: + cache: "private" + setup: true + +global: + membership: + maxJoinDuration: 30s + broadcastAddress: "127.0.0.1" + pprof: + port: 7936 + +services: + frontend: + rpc: + grpcPort: 7233 + membershipPort: 6933 + bindOnLocalHost: true + httpPort: 7243 + + matching: + rpc: + grpcPort: 7235 + membershipPort: 6935 + bindOnLocalHost: true + + history: + rpc: + grpcPort: 7234 + membershipPort: 6934 + bindOnLocalHost: true + + worker: + rpc: + membershipPort: 6939 + +clusterMetadata: + enableGlobalNamespace: false + failoverVersionIncrement: 10 + masterClusterName: "active" + currentClusterName: "active" + clusterInformation: + active: + enabled: true + initialFailoverVersion: 1 + rpcName: "frontend" + rpcAddress: "localhost:7236" + httpAddress: "localhost:7243" + +dcRedirectionPolicy: + policy: "noop" diff --git a/docker/ee/temporal-ui-server.yaml b/docker/ee/temporal-ui-server.yaml new file mode 100644 index 0000000000..4daf530ae2 --- /dev/null +++ b/docker/ee/temporal-ui-server.yaml @@ -0,0 +1,8 @@ +temporalGrpcAddress: 127.0.0.1:7233 # Use the correct Temporal server address +host: 0.0.0.0 +port: 8080 +enableUi: true +cors: + allowOrigins: + - http://localhost:8080 +defaultNamespace: default diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index 7043c78774..5ece3710de 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -22,6 +22,8 @@ import { handleActivateTargets, handleDeactivateTargets, handleActivateNonDraggingComponents, + computeScrollDelta, + computeScrollDeltaOnDrag, } from './gridUtils'; import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd'; import useStore from '@/AppBuilder/_stores/store'; @@ -56,6 +58,7 @@ export default function Grid({ gridWidth, currentLayout }) { const canvasWidth = NO_OF_GRIDS * gridWidth; const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow); const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow); + const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow); const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS); const draggingComponentId = useStore((state) => state.draggingComponentId, shallow); const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); @@ -345,6 +348,7 @@ export default function Grid({ gridWidth, currentLayout }) { const handleDragEnd = useCallback( (boxPositions) => { let newParent = null; + let oldParent = null; const updatedLayouts = boxPositions.reduce((layouts, { id, x, y, parent }) => { const currentWidget = boxList.find((box) => box.id === id); const containerWidth = parent ? useGridStore.getState().subContainerWidths[parent] : gridWidth; @@ -389,7 +393,7 @@ export default function Grid({ gridWidth, currentLayout }) { } } newParent = parent ? parent : null; - + oldParent = currentWidget.component?.parent; layouts[id] = { width: _width, height: _height, @@ -400,6 +404,11 @@ export default function Grid({ gridWidth, currentLayout }) { return layouts; }, {}); setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: true }); + + // const currentWidget = boxList.find((box) => box.id === id); + updateContainerAutoHeight(newParent); + updateContainerAutoHeight(oldParent); + toggleCanvasUpdater(); }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -579,6 +588,11 @@ export default function Grid({ gridWidth, currentLayout }) { keepRatio={false} individualGroupableProps={individualGroupableProps} onResize={(e) => { + if(resizingComponentId !== e.target.id) { + useGridStore.getState().actions.setResizingComponentId(e.target.id); + showGridLines(); + } + const currentWidget = boxList.find(({ id }) => id === e.target.id); let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; if (currentWidget.component?.parent) { @@ -639,9 +653,7 @@ export default function Grid({ gridWidth, currentLayout }) { return false; } handleActivateNonDraggingComponents(); - useGridStore.getState().actions.setResizingComponentId(e.target.id); e.setMin([gridWidth, GRID_HEIGHT]); - showGridLines(); }} onResizeEnd={(e) => { try { @@ -867,20 +879,19 @@ export default function Grid({ gridWidth, currentLayout }) { const targetSlotId = target?.slotId; const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth; - - // const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || []; - // const draggedWidgetType = dragged.widgetType; const isParentChangeAllowed = dragContext.isDroppable; // Compute new position let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged); const isModalToCanvas = source.isModal && target.slotId === 'real-canvas'; + let scrollDelta = computeScrollDelta({ source }); if (isParentChangeAllowed && !isModalToCanvas) { - const parent = target.slotId === 'real-canvas' ? null : target.slotId; // Special case for Modal; If source widget is modal, prevent drops to canvas - handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]); + const parent = target.slotId === 'real-canvas' ? null : target.slotId; + + handleDragEnd([{ id: e.target.id, x: left, y: top + scrollDelta, parent }]); } else { const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth; @@ -889,9 +900,8 @@ export default function Grid({ gridWidth, currentLayout }) { !isModalToCanvas ?? toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`); } - // Apply transform for smooth transition - e.target.style.transform = `translate(${left}px, ${top}px)`; + e.target.style.transform = `translate(${left}px, ${top + scrollDelta}px)`; // Force reordering of conatiner if the parent has not changed const newParentId = target.slotId === 'real-canvas' ? 'canvas' : target.slotId; @@ -945,7 +955,7 @@ export default function Grid({ gridWidth, currentLayout }) { const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot; if (isParentModal) { - const modalContainer = e.target.closest('.tj-modal-widget-content'); + const modalContainer = e.target.closest('.tj-modal--container'); const mainCanvas = document.getElementById('real-canvas'); const mainRect = mainCanvas.getBoundingClientRect(); @@ -959,12 +969,6 @@ export default function Grid({ gridWidth, currentLayout }) { setCanvasBounds({ ...relativePosition }); } - e.target.style.transform = `translate(${left}px, ${top}px)`; - e.target.setAttribute( - 'widget-pos2', - `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` - ); - // This block is to show grid lines on the canvas when the dragged element is over a new canvas if (document.elementFromPoint(e.clientX, e.clientY)) { const targetElems = document.elementsFromPoint(e.clientX, e.clientY); @@ -992,6 +996,17 @@ export default function Grid({ gridWidth, currentLayout }) { handleActivateTargets(newParentId); } } + + // Build the drag context from the event + const source = { slotId: oldParentId }; + let scrollDelta = computeScrollDeltaOnDrag({ source }); + + e.target.style.transform = `translate(${left}px, ${top - scrollDelta}px)`; + e.target.setAttribute( + 'widget-pos2', + `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` + ); + // Postion ghost element exactly as same at dragged element if (document.getElementById(`moveable-drag-ghost`)) { document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`; @@ -1081,6 +1096,7 @@ export default function Grid({ gridWidth, currentLayout }) { } }} snapGridAll={true} + scrollable={true} /> ); diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index da179bc11d..f36f921be9 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -415,6 +415,20 @@ export function hideGridLines() { document.getElementById('real-canvas')?.classList.add('hide-grid'); } +export function showGridLinesOnSlot(slotId) { + var canvasElm = document.getElementById(`canvas-${slotId}`); + + canvasElm.classList.remove('hide-grid'); + canvasElm.classList.add('show-grid'); +} + +export function hideGridLinesOnSlot(slotId) { + var canvasElm = document.getElementById(`canvas-${slotId}`); + + canvasElm.classList.remove('show-grid'); + canvasElm.classList.add('hide-grid'); +} + // Track previously active elements for efficient cleanup let previousActiveWidgets = null; let previousActiveCanvas = null; @@ -488,3 +502,18 @@ export const handleDeactivateTargets = () => { component.classList.remove('non-dragging-component'); }); }; +export const computeScrollDelta = ({ source }) => { + // Only need to calculate scroll delta when moving from a sub-container + if (source.slotId !== 'real-canvas') { + const subContainerWrap = document + .querySelector(`#canvas-${source.slotId}`) + ?.closest('.sub-container-overflow-wrap'); + + return subContainerWrap?.scrollTop || 0; + } + + // Default case: No scroll adjustment needed + return 0; +}; + +export const computeScrollDeltaOnDrag = computeScrollDelta; diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js index a9405d043e..da5a8341bf 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js @@ -175,7 +175,6 @@ export class DragContext { const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot]; return !restrictedWidgets.includes(dragged.widgetType); - ß; } } diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index 9507370e13..6610ae5fb4 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -33,6 +33,7 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [ 'Divider', 'VerticalDivider', 'Link', + 'Form', ]; const RenderWidget = ({ diff --git a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx index ef5a5dbd7d..cbebcb0425 100644 --- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx @@ -22,6 +22,7 @@ import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { search, searchKeymap, searchPanelOpen } from '@codemirror/search'; import { handleSearchPanel, SearchBtn } from './SearchBox'; +import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; import { isInsideParent } from './utils'; const langSupport = Object.freeze({ @@ -73,6 +74,8 @@ const MultiLineCodeEditor = (props) => { const [editorView, setEditorView] = React.useState(null); + const { queryPanelKeybindings } = useQueryPanelKeyHooks(onChange, currentValueRef, 'multiline'); + const handleOnBlur = () => { if (!delayOnChange) return onChange(currentValueRef.current); setTimeout(() => { @@ -94,6 +97,7 @@ const MultiLineCodeEditor = (props) => { highlightActiveLine: false, autocompletion: hideSuggestion ?? true, highlightActiveLineGutter: false, + defaultKeymap: false, completionKeymap: true, searchKeymap: false, }; @@ -203,7 +207,12 @@ const MultiLineCodeEditor = (props) => { }; } - const customKeyMaps = [...defaultKeymap, ...completionKeymap, ...searchKeymap]; + const customKeyMaps = [ + ...defaultKeymap.filter((keyBinding) => keyBinding.key !== 'Mod-Enter'), // Remove default keybinding for Mod-Enter + ...completionKeymap, + ...searchKeymap, + ]; + const customTabKeymap = keymap.of([ { key: 'Tab', @@ -224,6 +233,7 @@ const MultiLineCodeEditor = (props) => { return true; }, }, + ...queryPanelKeybindings, ]); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx index 7c9ed53158..65e6f2eadd 100644 --- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx @@ -22,6 +22,7 @@ import CodeHinter from './CodeHinter'; import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => { const { initialValue, onChange, enablePreview = true, portalProps } = restProps; @@ -201,6 +202,8 @@ const EditorInput = ({ const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow); const getSuggestions = useStore((state) => state.getSuggestions, shallow); + const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline'); + const isInsideQueryManager = useMemo( () => isInsideParent(wrapperRef?.current, 'query-manager'), [wrapperRef.current] @@ -271,7 +274,10 @@ const EditorInput = ({ maxRenderedOptions: 10, }); - const customKeyMaps = [...defaultKeymap, ...completionKeymap]; + const customKeyMaps = [ + ...defaultKeymap.filter((keyBinding) => keyBinding.key !== 'Mod-Enter'), // Remove default keybinding for Mod-Enter + ...completionKeymap, + ]; const customTabKeymap = keymap.of([ { key: 'Tab', @@ -293,6 +299,7 @@ const EditorInput = ({ } }, }, + ...queryPanelKeybindings, ]); const handleOnChange = React.useCallback((val) => { @@ -442,7 +449,8 @@ const EditorInput = ({ bracketMatching: true, foldGutter: false, highlightActiveLine: false, - autocompletion: showSuggestions, + autocompletion: true, + defaultKeymap: false, completionKeymap: true, searchKeymap: false, }} diff --git a/frontend/src/AppBuilder/CodeEditor/styles.scss b/frontend/src/AppBuilder/CodeEditor/styles.scss index d7f715fb3b..d0a328bae0 100644 --- a/frontend/src/AppBuilder/CodeEditor/styles.scss +++ b/frontend/src/AppBuilder/CodeEditor/styles.scss @@ -220,6 +220,9 @@ .query-hinter{ flex-grow: 1; } + .cm-editor { + min-height: 150px !important; + } } .code-editor-query-panel{ &.show-line-numbers{ @@ -398,6 +401,12 @@ } } +.rest-api-body-codehinter { + .cm-editor { + min-height: 150px !important; + } +} + .border-danger { .cm-editor { border: 1px solid red !important; diff --git a/frontend/src/AppBuilder/CodeEditor/useQueryPanelKeyHooks.js b/frontend/src/AppBuilder/CodeEditor/useQueryPanelKeyHooks.js new file mode 100644 index 0000000000..1a41a7f19b --- /dev/null +++ b/frontend/src/AppBuilder/CodeEditor/useQueryPanelKeyHooks.js @@ -0,0 +1,58 @@ +import { useModuleId } from '@/AppBuilder/_contexts/ModuleContext'; +import useStore from '@/AppBuilder/_stores/store'; +import { useCallback, useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +export const useQueryPanelKeyHooks = (onChange, value, type) => { + const queryPanelHeight = useStore((state) => state.queryPanel.queryPanelHeight); + const runQueryOnShortcut = useStore((state) => state.queryPanel.runQueryOnShortcut); + const previewQueryOnShortcut = useStore((state) => state.queryPanel.previewQueryOnShortcut); + const moduleId = useModuleId(); + const location = useLocation(); + const { pathname } = location; + + const [queryPanelKeybindings, setQueryPanelKeybindings] = useState([]); + + const handleRunQuery = useCallback( + (view) => { + const isEditor = pathname.includes('/apps/'); + if (queryPanelHeight !== 0 && isEditor) { + onChange(type === 'multiline' ? value.current : value); + runQueryOnShortcut(); + } + return true; + }, + [queryPanelHeight, onChange, runQueryOnShortcut, value] + ); + + const handlePreviewQuery = useCallback( + (view) => { + const isEditor = pathname.includes('/apps/'); + if (queryPanelHeight !== 0 && isEditor) { + onChange(type === 'multiline' ? value.current : value); + previewQueryOnShortcut(moduleId); + } + return true; + }, + [queryPanelHeight, moduleId, onChange, previewQueryOnShortcut, value] + ); + + useEffect(() => { + setQueryPanelKeybindings([ + { + key: 'Mod-Enter', + preventDefault: true, + run: handleRunQuery, + }, + { + key: 'Mod-Shift-Enter', + preventDefault: true, + run: handlePreviewQuery, + }, + ]); + }, [handleRunQuery, handlePreviewQuery]); + + return { + queryPanelKeybindings, + }; +}; diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx index 9e6737f41c..244d8e53cf 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx @@ -381,7 +381,6 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () = {activeTab === 1 && renderQueryElement()} {activeTab === 2 && renderTransformation()} {activeTab === 3 && renderQueryOptions()} -
)} diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx index 013733494f..e7d90fde58 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx @@ -1,18 +1,17 @@ import React, { useState, forwardRef, useRef, useEffect, useCallback } from 'react'; import RenameIcon from '../Icons/RenameIcon'; -import Eye1 from '@/_ui/Icon/solidIcons/Eye1'; -import Play from '@/_ui/Icon/solidIcons/Play'; import cx from 'classnames'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { DATA_SOURCE_TYPE } from '@/_helpers/constants'; import { shallow } from 'zustand/shallow'; -import { Tooltip } from 'react-tooltip'; +import { ToolTip } from '@/_components'; import { Button } from 'react-bootstrap'; import { decodeEntities } from '@/_helpers/utils'; import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers'; import useStore from '@/AppBuilder/_stores/store'; import { useModuleId } from '@/AppBuilder/_contexts/ModuleContext'; +import { Button as ButtonComponent } from '@/components/ui/Button/Button'; import { debounce } from 'lodash'; export const QueryManagerHeader = forwardRef(({ darkMode, setActiveTab, activeTab }, ref) => { @@ -27,6 +26,7 @@ export const QueryManagerHeader = forwardRef(({ darkMode, setActiveTab, activeTa const setShowCreateQuery = useStore((state) => state.queryPanel.setShowCreateQuery); const queryName = selectedQuery?.name ?? ''; const shouldFreeze = useStore((state) => state.getShouldFreeze()); + useEffect(() => { if (selectedQuery?.name) { setShowCreateQuery(false); @@ -244,34 +244,26 @@ const RunButton = ({ buttonLoadingState }) => { const isLoading = useStore( (state) => state.resolvedStore.modules.canvas.exposedValues.queries[selectedQuery?.id]?.isLoading ?? false ); + const isMac = typeof navigator !== 'undefined' && navigator?.userAgent?.toLowerCase().includes('mac'); + const shortcutDisplay = isMac ? 'Run query ⌘↩' : 'Run query Ctrl+Enter'; return ( - - - {isInDraft && } + Run + {isMac ? '⌘↩' : 'Ctrl+Enter'} + + ); }; @@ -287,20 +279,22 @@ const PreviewButton = ({ buttonLoadingState, onClick }) => { : true; const isPreviewQueryLoading = useStore((state) => state.queryPanel.isPreviewQueryLoading); const { t } = useTranslation(); + const isMac = typeof navigator !== 'undefined' && navigator?.userAgent?.toLowerCase().includes('mac'); + const shortcutDisplay = `Preview query ${isMac ? '⌘⇧↩' : 'Ctrl+Shift+Enter'}`; return ( - + + + Preview + + ); }; diff --git a/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx b/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx index 2234fa8e50..e2c2ab56c8 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx @@ -204,7 +204,7 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
-
+
+
{ }, [props.options]); return ( - + +
{ + const runQueryOnShortcut = useStore((state) => state.queryPanel.runQueryOnShortcut); + const previewQueryOnShortcut = useStore((state) => state.queryPanel.previewQueryOnShortcut); + const moduleId = useModuleId(); + + useHotkeys( + ['mod+enter', 'mod+shift+enter'], + (event, handler) => { + if (handler.mod && handler.keys[0] === 'enter') { + if (handler.shift) { + previewQueryOnShortcut(moduleId); + } else runQueryOnShortcut(); + } + }, + { enabled: isExpanded, enableOnFormTags: ['input'] } + ); + + return
{children}
; +}; + +export default QueryKeyHooks; diff --git a/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx b/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx index ea8623b0c1..61e1f98ed0 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryPanel.jsx @@ -11,6 +11,7 @@ import useStore from '@/AppBuilder/_stores/store'; import SectionCollapse from '@/_ui/Icon/solidIcons/SectionCollapse'; import SectionExpand from '@/_ui/Icon/solidIcons/SectionExpand'; import { shallow } from 'zustand/shallow'; +import QueryKeyHooks from './QueryKeyHooks'; const MemoizedQueryDataPane = memo(QueryDataPane); const MemoizedQueryManager = memo(QueryManager); @@ -193,14 +194,14 @@ export const QueryPanel = ({ darkMode }) => { }} > {isExpanded && ( -
+
-
+ )}
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx index b39924854e..6b0bc05422 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx @@ -40,6 +40,7 @@ export const Form = ({ const { id } = component; const newOptions = [{ name: 'None', value: 'none' }]; + Object.entries(allComponents).forEach(([componentId, _component]) => { const validParent = _component.component.parent === id || @@ -52,6 +53,19 @@ export const Form = ({ tempComponentMeta.properties.buttonToSubmit.options = newOptions; + // Hide header footer if custom schema is turned on + + if (component.component.definition.properties.advanced.value === '{{true}}') { + component.component.properties.showHeader = { + ...component.component.properties.headerHeight, + isHidden: true, + }; + component.component.properties.showFooter = { + ...component.component.properties.headerHeight, + isHidden: true, + }; + } + const accordionItems = baseComponentProperties( properties, events, @@ -110,24 +124,6 @@ export const baseComponentProperties = ( }); } - items.push({ - title: 'Additional actions', - isOpen: true, - children: additionalActions?.map((property) => - renderElement( - component, - componentMeta, - paramUpdated, - dataQueries, - property, - 'properties', - currentState, - allComponents, - darkMode - ) - ), - }); - if (events.length > 0) { items.push({ title: `${i18next.t('widget.common.events', 'Events')}`, @@ -149,6 +145,24 @@ export const baseComponentProperties = ( }); } + items.push({ + title: 'Additional actions', + isOpen: true, + children: additionalActions?.map((property) => + renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + property, + 'properties', + currentState, + allComponents, + darkMode + ) + ), + }); + if (validations.length > 0) { items.push({ title: `${i18next.t('widget.common.validation', 'Validation')}`, @@ -168,25 +182,6 @@ export const baseComponentProperties = ( }); } - items.push({ - title: `${i18next.t('widget.common.general', 'General')}`, - isOpen: true, - children: ( - <> - {renderElement( - component, - componentMeta, - layoutPropertyChanged, - dataQueries, - 'tooltip', - 'general', - currentState, - allComponents - )} - - ), - }); - items.push({ title: `${i18next.t('widget.common.devices', 'Devices')}`, isOpen: true, diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx index d250a4342e..71d8f9ead8 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx @@ -539,6 +539,17 @@ export function Select({ componentMeta, darkMode, ...restProps }) { currentState, allComponents )} + {isMultiSelect && + renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + 'showAllSelectedLabel', + 'properties', + currentState, + allComponents + )} {isSortingEnabled && renderElement( component, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js index d4ab44b3ab..24690ebfca 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js @@ -131,6 +131,19 @@ export const buttonGroupConfig = { defaultValue: 'left', }, }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, }, exposedVariables: { selected: [1], @@ -162,6 +175,7 @@ export const buttonGroupConfig = { borderRadius: { value: '{{4}}' }, disabledState: { value: '{{false}}' }, selectedTextColor: { value: '#FFFFFF' }, + padding: { value: 'default' }, selectedBackgroundColor: { value: 'var(--primary-brand)' }, alignment: { value: 'left' }, }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/checkbox.js b/frontend/src/AppBuilder/WidgetManager/widgets/checkbox.js index ca509979cb..ca4e740885 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/checkbox.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/checkbox.js @@ -126,6 +126,20 @@ export const checkboxConfig = { ], accordian: 'label', }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + accordian: 'switch', + }, }, exposedVariables: { value: false, @@ -189,6 +203,7 @@ export const checkboxConfig = { handleColor: { value: '#FFFFFF' }, alignment: { value: 'right' }, boxShadow: { value: '0px 0px 0px 0px #00000090' }, + padding: { value: 'default' }, }, validation: { mandatory: { value: '{{false}}' }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/colorPicker.js b/frontend/src/AppBuilder/WidgetManager/widgets/colorPicker.js index 6ecdede1a8..6255f81202 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/colorPicker.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/colorPicker.js @@ -28,6 +28,19 @@ export const colorPickerConfig = { }, styles: { visibility: { type: 'toggle', displayName: 'Visibility' }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, }, exposedVariables: { selectedColorHex: '#000000', @@ -47,6 +60,7 @@ export const colorPickerConfig = { events: [], styles: { visibility: { value: '{{true}}' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/container.js b/frontend/src/AppBuilder/WidgetManager/widgets/container.js index 424b9a801d..6dc9a679a4 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/container.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/container.js @@ -3,7 +3,7 @@ export const containerConfig = { displayName: 'Container', description: 'Group components', defaultSize: { - width: 5, + width: 10, height: 200, }, component: 'Container', @@ -44,13 +44,19 @@ export const containerConfig = { displayName: 'Show header', validation: { schema: { type: 'boolean' }, - defaultValue: false, + defaultValue: true, }, }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, }, defaultChildren: [ { componentName: 'Text', + slotName: 'header', layout: { top: 20, left: 1, @@ -98,15 +104,6 @@ export const containerConfig = { }, accordian: 'container', }, - headerHeight: { - type: 'numberInput', - displayName: 'Height', - validation: { - schema: { type: 'number' }, - defaultValue: 80, - }, - accordian: 'header', - }, borderRadius: { type: 'numberInput', displayName: 'Border', @@ -154,10 +151,11 @@ export const containerConfig = { showOnMobile: { value: '{{false}}' }, }, properties: { - showHeader: { value: `{{false}}` }, + showHeader: { value: `{{true}}` }, loadingState: { value: `{{false}}` }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: `{{80}}` }, }, events: [], styles: { diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js index cb90554e6b..d7534b25a8 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js @@ -75,6 +75,18 @@ export const dropdownV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -314,6 +326,8 @@ export const dropdownV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select an option' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js index d299ec2a6f..129322cf73 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js @@ -4,7 +4,7 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 480, + height: 450, }, defaultChildren: [ { @@ -19,8 +19,8 @@ export const formConfig = { accessorKey: 'text', styles: ['fontWeight', 'textSize', 'textColor'], defaultValue: { - text: 'Form title', - textSize: 20, + text: 'Form', + textSize: 16, textColor: '#000', }, }, @@ -34,203 +34,68 @@ export const formConfig = { }, properties: ['text'], defaultValue: { - text: 'Button2', + text: 'Submit', padding: 'none', }, }, - { - componentName: 'Text', - layout: { - top: 40, - left: 10, - height: 30, - width: 17, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'User Details', - fontWeight: 'bold', - textSize: 18, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 90, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Name', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 160, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Age', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, { componentName: 'TextInput', layout: { - top: 120, - left: 10, - height: 30, - width: 25, + top: 20, + left: 5, + height: 40, + width: 31, }, properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Enter your name', - label: '', + label: 'Name', + width: '{{60}}', + direction: 'left', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { componentName: 'NumberInput', layout: { - top: 190, - left: 10, - height: 30, - width: 25, + top: 80, + left: 5, + height: 40, + width: 31, }, - properties: ['value', 'label'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { - value: 24, - label: '', + placeholder: 'Age', + label: 'Age', + width: '{{60}}', + direction: 'left', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { - componentName: 'Button', + componentName: 'TextInput', layout: { - top: 240, - left: 10, - height: 30, - width: 10, + top: 140, + left: 5, + height: 40, + width: 31, }, - properties: ['text'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { - text: 'Submit', + placeholder: 'Tomy', + label: 'Pet name', + width: '{{60}}', + alignment: 'side', + direction: 'left', + auto: '{{false}}', + padding: 'default', }, }, ], @@ -276,6 +141,24 @@ export const formConfig = { }, showHeader: { type: 'toggle', displayName: 'Header' }, showFooter: { type: 'toggle', displayName: 'Footer' }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + canvasHeight: { + type: 'numberInput', + displayName: 'Canvas height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + footerHeight: { + type: 'numberInput', + displayName: 'Footer height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, visibility: { type: 'toggle', displayName: 'Visibility', @@ -294,6 +177,13 @@ export const formConfig = { defaultValue: false, }, }, + tooltip: { + type: 'code', + displayName: 'Tooltip', + validation: { schema: { type: 'string' } }, + section: 'additionalActions', + placeholder: 'Enter tooltip text', + }, }, events: { onSubmit: { displayName: 'On submit' }, @@ -316,22 +206,6 @@ export const formConfig = { defaultValue: '#ffffffff', }, }, - headerHeight: { - type: 'code', - displayName: 'Header height', - validation: { - schema: { type: 'string' }, - defaultValue: '80px', - }, - }, - footerHeight: { - type: 'code', - displayName: 'Footer height', - validation: { - schema: { type: 'string' }, - defaultValue: '80px', - }, - }, backgroundColor: { type: 'colorSwatches', displayName: 'Background color', @@ -403,18 +277,18 @@ export const formConfig = { value: "{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}", }, - showHeader: { value: '{{false}}' }, - showFooter: { value: '{{false}}' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: 60 }, + footerHeight: { value: 60 }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - headerHeight: { value: '60px' }, - footerHeight: { value: '60px' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/icon.js b/frontend/src/AppBuilder/WidgetManager/widgets/icon.js index 761a2da425..aa22dbb86c 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/icon.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/icon.js @@ -78,6 +78,29 @@ export const iconConfig = { }, accordian: 'Icon', }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + accordian: 'Icon', + }, + boxShadow: { + type: 'boxShadow', + displayName: 'Box shadow', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: '0px 0px 0px 0px #00000040', + }, + accordian: 'Icon', + }, }, exposedVariables: {}, actions: [ @@ -116,6 +139,8 @@ export const iconConfig = { styles: { iconColor: { value: '#000' }, iconAlign: { value: 'center' }, + padding: { value: 'default' }, + boxShadow: { value: '0px 0px 0px 0px #00000040' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js index 4ab4af57ce..c9d89045aa 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js @@ -121,6 +121,12 @@ export const multiselectV2Config = { }, accordian: 'Options', }, + showAllSelectedLabel: { + type: 'toggle', + displayName: 'Show "All items are selected"', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + accordian: 'Options', + }, optionsLoadingState: { type: 'toggle', displayName: 'Options loading state', @@ -142,6 +148,18 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -327,6 +345,9 @@ export const multiselectV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select the options' }, + showAllSelectedLabel: { value: '{{true}}' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/rangeslider.js b/frontend/src/AppBuilder/WidgetManager/widgets/rangeslider.js index 320e7a6741..186ae7fbc7 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/rangeslider.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/rangeslider.js @@ -84,6 +84,19 @@ export const rangeSliderConfig = { defaultValue: true, }, }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, }, exposedVariables: { value: null, @@ -111,6 +124,7 @@ export const rangeSliderConfig = { handleColor: { value: '' }, trackColor: { value: 'var(--primary-brand)' }, visibility: { value: '{{true}}' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/starrating.js b/frontend/src/AppBuilder/WidgetManager/widgets/starrating.js index 8cb239133d..fd2dce59d9 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/starrating.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/starrating.js @@ -89,6 +89,19 @@ export const starratingConfig = { defaultValue: false, }, }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, }, exposedVariables: { value: 0, @@ -112,6 +125,7 @@ export const starratingConfig = { labelColor: { value: '' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/tags.js b/frontend/src/AppBuilder/WidgetManager/widgets/tags.js index 73cd44b550..494a34bc7b 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/tags.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/tags.js @@ -38,6 +38,19 @@ export const tagsConfig = { defaultValue: true, }, }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, alignment: { type: 'alignButtons', displayName: 'Alignment', @@ -62,6 +75,7 @@ export const tagsConfig = { events: [], styles: { visibility: { value: '{{true}}' }, + padding: { value: 'default' }, alignment: { value: 'left' }, }, }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitchv2.js b/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitchv2.js index 4bc21f3a86..4b31bc8f0c 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitchv2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitchv2.js @@ -126,6 +126,20 @@ export const toggleSwitchV2Config = { validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } }, accordian: 'switch', }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + accordian: 'switch', + }, }, exposedVariables: { value: false, @@ -187,6 +201,7 @@ export const toggleSwitchV2Config = { handleColor: { value: '#FFFFFF' }, alignment: { value: 'right' }, boxShadow: { value: '0px 0px 0px 0px #00000090' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/AppBuilder/Widgets/Container/Container.jsx b/frontend/src/AppBuilder/Widgets/Container/Container.jsx index 4978427370..a706d29069 100644 --- a/frontend/src/AppBuilder/Widgets/Container/Container.jsx +++ b/frontend/src/AppBuilder/Widgets/Container/Container.jsx @@ -33,7 +33,8 @@ export const Container = ({ shallow ); - const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles; + const { borderRadius, borderColor, boxShadow } = styles; + const { headerHeight = 80 } = properties; const contentBgColor = useMemo(() => { return { backgroundColor: diff --git a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx new file mode 100644 index 0000000000..888b91d3b7 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx @@ -0,0 +1,88 @@ +import React, { useEffect } from 'react'; +import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; +import { showGridLinesOnSlot, hideGridLinesOnSlot } from '@/AppBuilder/AppCanvas/Grid/gridUtils'; +import { useResizable } from '@/AppBuilder/_hooks/useMoveable'; + +export const HorizontalSlot = React.memo( + ({ + id, + height = 0, + width, + darkMode, + isDisabled, + isActive, + slotName = 'header', // 'header' or 'footer' + slotStyle = {}, + onResize, + isEditing, + maxHeight, + }) => { + const parsedHeight = parseInt(height, 10); + + const { getRootProps, getHandleProps, getResizeState } = useResizable({ + initialHeight: parsedHeight, + initialWidth: '100%', // Now respects parent's width + minHeight: 10, + maxHeight: maxHeight || 400, + maxWidth: '100%', + stepHeight: 10, // Height will change in steps of 10px + onResize: () => {}, + onDragEnd: (values) => { + onResize(values); + }, + isReverseVerticalDrag: slotName === 'footer', // Reverse dragging for Footer + }); + + const { height: resizedHeight, isDragging } = getResizeState(); + + useEffect(() => { + if (isDragging) { + showGridLinesOnSlot(id); + } else { + hideGridLinesOnSlot(id); + } + }, [isDragging, id]); + + const canvasHeight = parseInt(resizedHeight, 10) / 10; + + const resizeStyle = { + backgroundColor: darkMode ? '#1F2837' : '#fff', + }; + + return ( +
+
+ + {isEditing &&
} +
+ + {isDisabled && ( +
{}} + onDrop={(e) => e.stopPropagation()} + /> + )} +
+ ); + } +); diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index afeb4cf844..02b2573484 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect } from 'react'; import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; // eslint-disable-next-line import/no-unresolved import _, { debounce, omit } from 'lodash'; -import { generateUIComponents } from './FormUtils'; +import { generateUIComponents, getBodyHeight } from './FormUtils'; import { useMounted } from '@/_hooks/use-mount'; import { onComponentClick, removeFunctionObjects } from '@/_helpers/appUtils'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; @@ -14,12 +14,10 @@ import { CONTAINER_FORM_CANVAS_PADDING, SUBCONTAINER_CANVAS_BORDER_WIDTH, } from '@/AppBuilder/AppCanvas/appCanvasConstants'; -import './form.scss'; +import { HorizontalSlot } from './Components/HorizontalSlot'; +import { useActiveSlot } from '@/AppBuilder/_hooks/useActiveSlot'; -const getCanvasHeight = (height) => { - const parsedHeight = height.includes('px') ? parseInt(height, 10) : height; - return Math.ceil(parsedHeight); -}; +import './form.scss'; export const Form = function Form(props) { const { @@ -35,26 +33,19 @@ export const Form = function Form(props) { properties, resetComponent = () => {}, dataCy, + onComponentClick, } = props; const childComponents = useStore((state) => state.getChildComponents(id), shallow); - const { - borderRadius, - borderColor, - boxShadow, - headerHeight, - footerHeight, - footerBackgroundColor, - headerBackgroundColor, - } = styles; + const { borderRadius, borderColor, boxShadow, footerBackgroundColor, headerBackgroundColor } = styles; const { buttonToSubmit, - loadingState, advanced, JSONSchema, showHeader = false, showFooter = false, - visibility, - disabledState, + headerHeight = 80, + footerHeight = 80, + canvasHeight, } = properties; const { isDisabled, isVisible, isLoading } = useExposeState( properties.loadingState, @@ -65,6 +56,10 @@ export const Form = function Form(props) { ); const backgroundColor = ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor; + + const computedFormBodyHeight = getBodyHeight(height, showHeader, showFooter, headerHeight, footerHeight); + const computedBorderRadius = `${borderRadius ? parseFloat(borderRadius) : 0}px`; + const computedStyles = { backgroundColor, borderRadius: borderRadius ? parseFloat(borderRadius) : 0, @@ -74,16 +69,7 @@ export const Form = function Form(props) { position: 'relative', boxShadow, flexDirection: 'column', - }; - - const formHeader = { - flexShrink: 0, - paddingBottom: '3px', - paddingTop: '7px', - paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, - paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, - backgroundColor: - ['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor, + clipPath: `inset(0 round ${computedBorderRadius})`, }; const formContent = { @@ -96,13 +82,6 @@ export const Form = function Form(props) { paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, }; - const formFooter = { - flexShrink: 0, - padding: `${CONTAINER_FORM_CANVAS_PADDING}px`, - backgroundColor: - ['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor, - }; - const parentRef = useRef(null); const childDataRef = useRef({}); @@ -110,8 +89,6 @@ export const Form = function Form(props) { const [isValid, setValidation] = useState(true); const [uiComponents, setUIComponents] = useState([]); const mounted = useMounted(); - const canvasHeaderHeight = getCanvasHeight(headerHeight) / 10; - const canvasFooterHeight = getCanvasHeight(footerHeight) / 10; useEffect(() => { const exposedVariables = { @@ -287,9 +264,61 @@ export const Form = function Form(props) { setChildrenData(childDataRef.current); }; + const mode = useStore((state) => state.currentMode, shallow); + const isEditing = mode === 'edit'; + + const activeSlot = useActiveSlot(isEditing ? id : null); // Track the active slot for this widget + const setComponentProperty = useStore((state) => state.setComponentProperty, shallow); + // const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight); + + const updateHeaderSizeInStore = ({ newHeight }) => { + const _height = parseInt(newHeight, 10); + setComponentProperty(id, `headerHeight`, _height, 'properties', 'value', false); + }; + + const updateFooterSizeInStore = ({ newHeight }) => { + const _height = parseInt(newHeight, 10); + setComponentProperty(id, `footerHeight`, _height, 'properties', 'value', false); + }; + + const [canHeight, setCanHeight] = useState('100%'); + useEffect(() => { + // const newHeight = parseInt(height, 10) - 14; + + // const autoCanvasHeight = document.querySelector(`#canvas-${id}`)?.scrollHeight; + const wrapHeight = parseInt(computedFormBodyHeight, 10); + // Set height to the larger value between computed body height and canvas scroll height + const maxHeight = Math.max(wrapHeight, canvasHeight || 10); + + const roundedHeight = Math.round(maxHeight / 10) * 10; + setCanHeight(`${roundedHeight}px`); + }, [computedFormBodyHeight, canvasHeight]); + const headerMaxHeight = parseInt(height, 10) - parseInt(footerHeight, 10) - 100 - 10; + const footerMaxHeight = parseInt(height, 10) - parseInt(headerHeight, 10) - 100 - 10; + const formFooter = { + flexShrink: 0, + paddingTop: '3px', + paddingBottom: '7px', + paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, + paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, + maxHeight: `${footerMaxHeight}px`, + backgroundColor: + ['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor, + }; + const formHeader = { + flexShrink: 0, + paddingBottom: '3px', + paddingTop: '7px', + paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, + paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, + maxHeight: `${headerMaxHeight}px`, + backgroundColor: + ['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor, + }; + return (
- {showHeader && ( -
- - {isDisabled && ( -
{}} - onDrop={(e) => e.stopPropagation()} - /> - )} -
+ {!advanced && showHeader && ( + )} -
+ +
{isLoading ? (
@@ -332,14 +351,17 @@ export const Form = function Form(props) { ) : (
{!advanced && ( -
+
@@ -381,31 +403,19 @@ export const Form = function Form(props) {
)}
- {showFooter && ( -
- - {isDisabled && ( - + {!advanced && showFooter && ( + )} ); diff --git a/frontend/src/AppBuilder/Widgets/Form/FormUtils.js b/frontend/src/AppBuilder/Widgets/Form/FormUtils.js index 2dea50bdf9..228ca6f36e 100644 --- a/frontend/src/AppBuilder/Widgets/Form/FormUtils.js +++ b/frontend/src/AppBuilder/Widgets/Form/FormUtils.js @@ -533,3 +533,22 @@ const validBooleanChecker = (input) => { if (/^(true|false)$/i.test(input) == true) return JSON.parse(input); return true; }; + +export const getBodyHeight = (height, showHeader, showFooter, headerHeight = 60, footerHeight = 60) => { + let modalHeight = height ? parseInt(height, 10) : 0; + let parsedHeaderHeight = showHeader ? parseInt(headerHeight, 10) : 0; + let parsedFooterHeight = showFooter ? parseInt(footerHeight, 10) : 0; + + if (showHeader) { + // 10 is header padding + modalHeight = modalHeight - parsedHeaderHeight - 10; + } + if (showFooter) { + // 14 is footer padding + modalHeight = modalHeight - parsedFooterHeight - 14; + } + + const rounded = Math.ceil(modalHeight / 10) * 10; + + return `${Math.max(rounded - 20, 40)}px`; +}; diff --git a/frontend/src/AppBuilder/Widgets/Form/form.scss b/frontend/src/AppBuilder/Widgets/Form/form.scss index 530e837eb2..1e1776bef5 100644 --- a/frontend/src/AppBuilder/Widgets/Form/form.scss +++ b/frontend/src/AppBuilder/Widgets/Form/form.scss @@ -1,11 +1,17 @@ +.jet-form-widget { + display: flex; + flex-direction: column; + height: 100%; +} + .wj-form-header { position: relative; &::after { content: ""; position: absolute; bottom: 0; - left: -7px; - right: -7px; + left: -2px; + right: -2px; height: 1px; background-color: var(--border-weak); } @@ -17,8 +23,8 @@ content: ""; position: absolute; top: 0; - left: -7px; - right: -7px; + left: -2px; + right: -2px; height: 1px; background-color: var(--border-weak); } @@ -38,3 +44,77 @@ box-sizing: content-box; padding: 4px 0; } + +.resizable-slot { + position: relative; + height: auto; + box-shadow: 0 0 0 1px transparent; /* Acts as a border */ + transition: box-shadow 0.15s ease-in-out; + max-height: 100%; + + &.is-editing:hover { + box-shadow: 0 0 0 1px var(--border-weak); + } + + &.is-editing.active { + box-shadow: 0 0 0 1px var(--border-accent-weak); + } + + &.is-editing.dragging { + box-shadow: 0 0 0 1px var(--border-accent-strong); + } + + .resize-handle { + position: absolute; + bottom: -4px; + left: 50%; /* Center horizontally */ + transform: translateX(-50%); /* Ensure proper centering */ + width: 24px; + height: 8px; + border-radius: 4px; + background-color: initial; + border: 1px solid var(--background-accent-strong); + cursor: ns-resize; + z-index: 1; + visibility: hidden; + transition: visibility 0.15s ease-in-out; + } + + &.active .resize-handle { + visibility: visible; + } +} + +.only-bottom { +} + +.jet-form-header { + min-height: 10px; +} + +.jet-form-body { + min-height: 100px; + background-color: inherit; +} + +.jet-form-footer { + min-height: 10px; +} + +.jet-form-footer .resize-handle { + top: -4px; + bottom: unset; +} + +.jet-container.jet-container-json-form { + padding: 0px; + + .wj-form-header::after, + .wj-form-footer::after { + left: -3px; + right: -3px; + } + .jet-form-body fieldset { + padding: 20px; + } +} diff --git a/frontend/src/AppBuilder/Widgets/Modal.jsx b/frontend/src/AppBuilder/Widgets/Modal.jsx index c410cf4840..f3b758b7d1 100644 --- a/frontend/src/AppBuilder/Widgets/Modal.jsx +++ b/frontend/src/AppBuilder/Widgets/Modal.jsx @@ -277,7 +277,7 @@ export const Modal = function Modal({ { return ( { e.preventDefault(); diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/Header.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/Header.jsx index 9e9a6f656f..1331889870 100644 --- a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/Header.jsx +++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/Header.jsx @@ -117,7 +117,7 @@ export const Header = memo(
{showFilter && ( - + )} ); diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx index 905142fc59..7b1a3cdca6 100644 --- a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx +++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx @@ -6,7 +6,7 @@ import { FilterFooter } from './FilterFooter'; import { FilterHeader } from './FilterHeader'; import { debounce, isEqual } from 'lodash'; -export const Filter = memo(({ table, darkMode, setFilters, setShowFilter }) => { +export const Filter = memo(({ id, table, darkMode, setFilters, setShowFilter }) => { const { t } = useTranslation(); const [localFilters, setLocalFilters] = useState(table.getState().columnFilters); @@ -142,6 +142,7 @@ export const Filter = memo(({ table, darkMode, setFilters, setShowFilter }) => {
{localFilters.map((filter, index) => ( { + ({ id, filter, index, columns, darkMode, onColumnChange, onOperationChange, onValueChange, onRemove }) => { const { t } = useTranslation(); + const isDragging = useStore((state) => state.draggingComponentId === id); const selectStyles = (width) => { return { @@ -15,6 +18,10 @@ export const FilterRow = memo( menuList: (base) => ({ ...base, }), + menu: (base) => ({ + ...base, + display: isDragging ? 'none' : 'block', + }), }; }; @@ -29,11 +36,13 @@ export const FilterRow = memo( value={filter.id} search={true} onChange={(value) => onColumnChange(index, value)} + components={{ ValueContainer: CustomValueContainer }} placeholder={t('globals.select', 'Select') + '...'} className={`${darkMode ? 'select-search-dark' : 'select-search'} mb-0`} styles={selectStyles('100%')} useCustomStyles={true} darkMode={darkMode} + openMenuOnFocus={true} />
@@ -42,11 +51,13 @@ export const FilterRow = memo( value={filter.value.condition} search={true} onChange={(value) => onOperationChange(index, value)} + components={{ ValueContainer: CustomValueContainer }} className={`${darkMode ? 'select-search-dark' : 'select-search'}`} placeholder={t('globals.select', 'Select') + '...'} styles={selectStyles('100%')} useCustomStyles={true} darkMode={darkMode} + openMenuOnFocus={true} />
@@ -74,3 +85,15 @@ export const FilterRow = memo( ); } ); + +const CustomValueContainer = (props) => { + const handleClick = (e) => { + if (props.selectProps?.selectRef?.current) { + props.selectProps.selectRef.current.focus(); + } + if (props.innerProps?.onMouseDown) { + props.innerProps.onMouseDown(e); + } + }; + return ; +}; diff --git a/frontend/src/AppBuilder/_hooks/useActiveSlot.js b/frontend/src/AppBuilder/_hooks/useActiveSlot.js new file mode 100644 index 0000000000..14f80a366f --- /dev/null +++ b/frontend/src/AppBuilder/_hooks/useActiveSlot.js @@ -0,0 +1,84 @@ +import { useState, useEffect } from 'react'; +import useStore from '@/AppBuilder/_stores/store'; +import { shallow } from 'zustand/shallow'; + +const useIsWidgetSelected = (id) => { + // Get selected components from store using shallow comparison + const selectedComponents = useStore((state) => state.selectedComponents, shallow); + + // Check if the only selected component is the provided `id` + return selectedComponents.length === 1 && selectedComponents[0] === id; +}; + +export const useActiveSlot = (widgetId) => { + const [activeSlot, setActiveSlot] = useState(''); // Default to widget ID + const isSelected = useIsWidgetSelected(widgetId); // Check if widget is selected + + useEffect(() => { + if (!isSelected) { + setActiveSlot(''); + } + }, [isSelected]); + + useEffect(() => { + const handleDoubleClick = (event) => { + let target = event.target; + + if (!widgetId) { + setActiveSlot(null); + return; + } + + // Traverse up to find a slot with an id + while (target && target !== document.body) { + if (target.id && target.id.startsWith('canvas-')) { + const slotId = target.id.replace(/^canvas-/, ''); // ✅ Strip "canvas-" + setActiveSlot(slotId); + return; + } + target = target.parentElement; + } + + // If no slot is found, reset to widget ID + setActiveSlot(widgetId); + }; + const handleSingleClick = (event) => { + let target = event.target; + + if (!widgetId) { + setActiveSlot(null); + return; + } + + // Traverse up to find a valid main slot (not header/footer) + while (target && target !== document.body) { + if ( + target.id && + target.id.startsWith('canvas-') && + !target.id.endsWith('-header') && + !target.id.endsWith('-footer') + ) { + const slotId = target.id.replace(/^canvas-/, ''); // Strip "canvas-" + setActiveSlot(slotId); + return; + } + target = target.parentElement; + } + + // If no main slot is found, fallback to widget ID + setActiveSlot(widgetId); + }; + + // Attach single click if the widget is selected, otherwise listen for double-click + + document.addEventListener('dblclick', handleDoubleClick); + document.addEventListener('click', handleSingleClick); + + return () => { + document.removeEventListener('dblclick', handleDoubleClick); + document.removeEventListener('click', handleSingleClick); + }; + }, [widgetId]); // Re-run when widgetId or selection state changes + + return activeSlot; +}; diff --git a/frontend/src/AppBuilder/_hooks/useMoveable.js b/frontend/src/AppBuilder/_hooks/useMoveable.js new file mode 100644 index 0000000000..ea2687e473 --- /dev/null +++ b/frontend/src/AppBuilder/_hooks/useMoveable.js @@ -0,0 +1,135 @@ +import { useRef, useState } from 'react'; + +const defaultProps = { + minHeight: 50, + maxHeight: 600, + minWidth: 50, + maxWidth: 600, + lockHorizontal: false, + lockVertical: false, + stepHeight: 10, // Default step size for height + stepWidth: 10, // Default step size for width + onResize: null, + onDragStart: null, + onDragEnd: null, + isReverseVerticalDrag: false, +}; + +export const useResizable = (options = {}) => { + const props = { ...defaultProps, ...options }; + const parentRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); // ✅ Track dragging state + + const [height, setHeight] = useState( + typeof props.initialHeight === 'string' ? props.initialHeight : `${props.initialHeight || 200}px` + ); + const [width, setWidth] = useState( + typeof props.initialWidth === 'string' ? props.initialWidth : `${props.initialWidth || 200}px` + ); + + const getRootProps = () => ({ + ref: parentRef, + style: { height, width }, + }); + + const getResizeState = () => ({ + height, + width, + isDragging, + }); + + const getHandleProps = () => { + const handleMouseDown = (e) => { + // Prevent right-click drag activation (button === 2) + if (e.button === 2) return; + + if (!parentRef.current) return; + e.stopPropagation(); + e.preventDefault(); + const startHeight = parseInt(parentRef.current.clientHeight); + const startWidth = parseInt(parentRef.current.clientWidth); + const parentWidth = parentRef.current.parentElement ? parentRef.current.parentElement.clientWidth : startWidth; + const startY = e.clientY; + const startX = e.clientX; + const isPercentage = typeof props.initialWidth === 'string' && props.initialWidth.includes('%'); + + setIsDragging(true); // ✅ Set dragging state to true + + if (props.onDragStart) { + props.onDragStart({ newHeight: startHeight, newWidth: startWidth }); + } + + const handleMouseMove = (moveEvent) => { + moveEvent.stopPropagation(); + moveEvent.preventDefault(); + let newHeight = startHeight; + let newWidth = startWidth; + + if (!props.lockVertical) { + const deltaY = props.isReverseVerticalDrag ? startY - moveEvent.clientY : moveEvent.clientY - startY; + newHeight = startHeight + deltaY; + newHeight = Math.max(props.minHeight, Math.min(props.maxHeight, newHeight)); + newHeight = Math.round(newHeight / props.stepHeight) * props.stepHeight; // Snap to stepHeight + } + + if (!props.lockHorizontal) { + newWidth = startWidth + (moveEvent.clientX - startX); + newWidth = Math.max(props.minWidth, Math.min(props.maxWidth, newWidth)); + newWidth = Math.round(newWidth / props.stepWidth) * props.stepWidth; // Snap to stepWidth + + if (isPercentage) { + newWidth = (newWidth / parentWidth) * 100; // Convert to percentage + newWidth = `${newWidth.toFixed(2)}%`; + } else { + newWidth = `${newWidth}px`; + } + } + + setHeight(`${newHeight}px`); + setWidth(newWidth); + + if (parentRef.current) { + parentRef.current.style.height = `${newHeight}px`; + parentRef.current.style.width = newWidth; + } + + if (props.onResize) { + props.onResize({ + newHeight, + newWidth, + heightDiff: newHeight - startHeight, + widthDiff: isPercentage + ? parseInt(newWidth) - (startWidth / parentWidth) * 100 + : parseInt(newWidth) - startWidth, + }); + } + }; + + const handleMouseUp = () => { + setIsDragging(false); // ✅ Set dragging state to false + + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + + if (props.onDragEnd) { + // Get the updated height and width from the DOM instead of relying on state + const finalHeight = parentRef.current ? parseInt(parentRef.current.clientHeight) : parseInt(height); + const finalWidth = parentRef.current ? parseInt(parentRef.current.clientWidth) : parseInt(width); + + props.onDragEnd({ newHeight: finalHeight, newWidth: finalWidth }); + } + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + + return { + onMouseDown: handleMouseDown, + }; + }; + + return { rootRef: parentRef, getRootProps, getHandleProps, getResizeState }; +}; + +export default useResizable; diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index b746f1237b..b500a4d912 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -1896,4 +1896,28 @@ export const createComponentsSlice = (set, get) => ({ state.modalsOpenOnCanvas = newModalOpenOnCanvas; }); }, + updateContainerAutoHeight: (componentId) => { + if ( + !componentId || + componentId === 'canvas' || + componentId.includes('-header') || + componentId.includes('-footer') + ) { + return; + } + const { currentLayout, getCurrentPageComponents, setComponentProperty } = get(); + const allComponents = getCurrentPageComponents(); + + const childComponents = getAllChildComponents(allComponents, componentId); + const maxHeight = Object.values(childComponents).reduce((max, component) => { + const layout = component?.layouts?.[currentLayout]; + if (!layout) { + return max; + } + const sum = layout.top + layout.height; + return Math.max(max, sum); + }, 0); + + setComponentProperty(componentId, `canvasHeight`, maxHeight, 'properties', 'value', false); + }, }); diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index 811ebfc958..5ca9429693 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -1092,5 +1092,23 @@ export const createQueryPanelSlice = (set, get) => ({ isQuerySelected: (queryId) => { return get().queryPanel.selectedQuery?.id === queryId; }, + runQueryOnShortcut: () => { + const { queryPanel } = get(); + const { runQuery, selectedQuery } = queryPanel; + runQuery(selectedQuery?.id, selectedQuery?.name, undefined, 'edit', {}, true); + }, + previewQueryOnShortcut: (moduleId = 'canvas') => { + const { queryPanel } = get(); + const { previewQuery, selectedQuery, selectedDataSource } = queryPanel; + const query = { + data_source_id: selectedDataSource.id === 'null' ? null : selectedDataSource.id, + pluginId: selectedDataSource.pluginId, + options: { ...selectedQuery?.options }, + kind: selectedDataSource.kind, + name: selectedQuery?.name ?? '', + id: selectedQuery?.id, + }; + previewQuery(query, false, undefined, moduleId); + }, }, }); diff --git a/frontend/src/Editor/Components/ColorPicker.jsx b/frontend/src/Editor/Components/ColorPicker.jsx index 58282490f1..fb5be51f5e 100644 --- a/frontend/src/Editor/Components/ColorPicker.jsx +++ b/frontend/src/Editor/Components/ColorPicker.jsx @@ -161,8 +161,8 @@ export const ColorPicker = function ({ : { display: 'none' }; return ( -
-
+
+
setShowColorPicker(true)} diff --git a/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx b/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx index 848de076ae..6be9d65bc7 100644 --- a/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx +++ b/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx @@ -1,87 +1,118 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { components } from 'react-select'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import Loader from '@/ToolJetUI/Loader/Loader'; import './dropdownV2.scss'; -import { FormCheck } from 'react-bootstrap'; +// eslint-disable-next-line import/no-unresolved +import { useVirtualizer } from '@tanstack/react-virtual'; import cx from 'classnames'; const { MenuList } = components; // This Menulist also used in MultiselectV2 const CustomMenuList = ({ selectProps, ...props }) => { - const { - onInputChange, - onMenuInputFocus, - showAllOption, - isSelectAllSelected, - optionsLoadingState, - darkMode, - setSelected, - setIsSelectAllSelected, - fireEvent, - inputValue, - menuId, - } = selectProps; + const { onInputChange, onMenuInputFocus, optionsLoadingState, darkMode, inputValue, menuId, showSearchInput } = + selectProps; - const handleSelectAll = (e) => { - e.target.checked && fireEvent(); - if (e.target.checked) { - setSelected(props.options); - } else { - setSelected([]); + const parentRef = useRef(null); + const virtualizer = useVirtualizer({ + count: props?.children?.length || 0, + getScrollElement: () => parentRef.current, + estimateSize: () => 40, + overscan: 15, + }); + + useEffect(() => { + const searchInput = document.querySelector('.dropdown-multiselect-widget-search-box'); + if (searchInput) { + searchInput.focus(); } - setIsSelectAllSelected(e.target.checked); - }; + }, []); + return (
e.stopPropagation()} + onTouchEnd={(e) => e.stopPropagation()} > -
- - - - - onInputChange(e.currentTarget.value, { - action: 'input-change', - }) - } - onMouseDown={(e) => { - e.stopPropagation(); - e.target.focus(); - }} - onTouchEnd={(e) => { - e.stopPropagation(); - e.target.focus(); - }} - onFocus={onMenuInputFocus} - placeholder="Search" - className="dropdown-multiselect-widget-search-box" - /> -
- {showAllOption && !optionsLoadingState && ( - + {showSearchInput && ( +
+ + + + + onInputChange(e.currentTarget.value, { + action: 'input-change', + }) + } + onMouseDown={(e) => { + e.stopPropagation(); + e.target.focus(); + }} + onTouchEnd={(e) => { + e.stopPropagation(); + e.target.focus(); + }} + onFocus={onMenuInputFocus} + placeholder="Search" + className="dropdown-multiselect-widget-search-box" + /> +
)} - - {optionsLoadingState ? ( -
- + {!optionsLoadingState && ( +
+
+ {!virtualizer.getTotalSize() && props.children} + {virtualizer.getVirtualItems().map((virtualItem) => { + const option = props.options[virtualItem.index]; + const child = props.children[virtualItem.index]; + const isSelectAll = option?.value === 'multiselect-custom-menulist-select-all'; + return ( +
+ +
{child}
+
+
+ ); + })}
- ) : ( - props.children - )} - +
+ )} + {optionsLoadingState && ( +
+ +
+ )}
); }; diff --git a/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx b/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx index 73986ba274..d3e85030d6 100644 --- a/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx +++ b/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx @@ -6,7 +6,17 @@ import { highlightText } from './utils'; const CustomOption = (props) => { return ( - + { + e.preventDefault(); + e.stopPropagation(); + props.selectOption(props.data); + }, + }} + >
{props.isSelected && ( diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index 382ba010ed..1f3c4dc451 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -15,7 +15,6 @@ import Label from '@/_ui/Label'; import cx from 'classnames'; import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from './utils'; import { isMobileDevice } from '@/_helpers/appUtils'; -import useStore from '@/AppBuilder/_stores/store'; const { DropdownIndicator, ClearIndicator } = components; const INDICATOR_CONTAINER_WIDTH = 60; @@ -69,6 +68,8 @@ export const DropdownV2 = ({ disabledState, optionsLoadingState, sort, + showClearBtn, + showSearchInput, } = properties; const { selectedTextColor, @@ -104,8 +105,6 @@ export const DropdownV2 = ({ const [isDropdownDisabled, setIsDropdownDisabled] = useState(disabledState); const [searchInputValue, setSearchInputValue] = useState(''); const [userInteracted, setUserInteracted] = useState(false); - const currentMode = useStore((state) => state.currentMode); - const isEditor = currentMode === 'edit'; const _height = padding === 'default' ? `${height}px` : `${height + 4}px`; const labelRef = useRef(); @@ -173,12 +172,43 @@ export const DropdownV2 = ({ setExposedVariable('isValid', validationStatus?.isValid); }; - const handleClickInEditor = (e) => { - if (e.target.className.includes('clear-indicator') || isMenuOpen) return; - e.stopPropagation(); - selectRef.current?.onControlMouseDown(e); + const handleClickInsideSelect = () => { + if (isDropdownDisabled || isDropdownLoading) return; + if (isMenuOpen) { + setIsMenuOpen(false); + fireEvent('onBlur'); + setSearchInputValue(''); + } else { + setIsMenuOpen(true); + fireEvent('onFocus'); + if (!showSearchInput) { + selectRef.current.focus(); + } + } }; + const handleClickOutsideSelect = (event) => { + const menu = document.querySelector(`._tooljet-${componentName}`); + if ( + isMenuOpen && + menu && + dropdownRef.current && + !dropdownRef.current.contains(event.target) && + !menu.contains(event.target) + ) { + setIsMenuOpen(false); + fireEvent('onBlur'); + setSearchInputValue(''); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutsideSelect); + return () => { + document.removeEventListener('mousedown', handleClickOutsideSelect); + }; + }, [isMenuOpen, componentName]); + useEffect(() => { setInputValue(findDefaultItem(advanced ? schema : options)); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -331,8 +361,8 @@ export const DropdownV2 = ({ selectedTextColor !== '#1B1F24' ? selectedTextColor : isDropdownDisabled || isDropdownLoading - ? 'var(--text-disabled)' - : 'var(--text-primary)', + ? 'var(--text-disabled)' + : 'var(--text-primary)', maxWidth: ref?.current?.offsetWidth - (iconVisibility ? INDICATOR_CONTAINER_WIDTH + ICON_WIDTH : INDICATOR_CONTAINER_WIDTH), @@ -373,8 +403,8 @@ export const DropdownV2 = ({ selectedTextColor !== '#1B1F24' ? selectedTextColor : isDropdownDisabled || isDropdownLoading - ? 'var(--text-disabled)' - : 'var(--text-primary)', + ? 'var(--text-disabled)' + : 'var(--text-primary)', borderRadius: _state.isFocused && '8px', padding: '8px 6px 8px 38px', '&:hover': { @@ -386,7 +416,7 @@ export const DropdownV2 = ({ }), menuList: (provided) => ({ ...provided, - padding: '8px', + padding: '0 8px', borderRadius: '8px', // this is needed otherwise :active state doesn't look nice, gap is required display: 'flex', @@ -410,8 +440,8 @@ export const DropdownV2 = ({ data-cy={`label-${String(componentName).toLowerCase()} `} className={cx('dropdown-widget', 'd-flex', { [alignment === 'top' && - ((labelWidth != 0 && label?.length != 0) || - (labelAutoWidth && labelWidth == 0 && label && label?.length != 0)) + ((labelWidth != 0 && label?.length != 0) || + (labelAutoWidth && labelWidth == 0 && label && label?.length != 0)) ? 'flex-column' : 'align-items-center']: true, 'flex-row-reverse': direction === 'right' && alignment === 'side', @@ -446,7 +476,8 @@ export const DropdownV2 = ({
, - ClearIndicator: CustomClearIndicator, + ClearIndicator: showClearBtn ? CustomClearIndicator : () => null, DropdownIndicator: isMultiSelectLoading ? () => null : CustomDropdownIndicator, }} isClearable @@ -498,21 +529,15 @@ export const MultiselectV2 = ({ tabSelectsValue={false} controlShouldRenderValue={false} isSearchable={false} - onMenuOpen={() => { - setIsMultiselectOpen(true); - fireEvent('onFocus'); - }} - onMenuClose={() => { - setIsMultiselectOpen(false); - fireEvent('onBlur'); - }} onKeyDown={(e) => { - if (e.key === 'Enter' && !isMultiselectOpen) { + if (e.key === 'Enter' && !isMultiselectOpen && !isMultiSelectLoading) { setIsMultiselectOpen(true); + fireEvent('onFocus'); e.preventDefault(); } if (e.key === 'Escape' && isMultiselectOpen) { setIsMultiselectOpen(false); + fireEvent('onBlur'); e.preventDefault(); } }} @@ -520,21 +545,14 @@ export const MultiselectV2 = ({ icon={icon} doShowIcon={iconVisibility} containerRef={valueContainerRef} - showAllOption={showAllOption} - isSelectAllSelected={isSelectAllSelected} - setIsSelectAllSelected={(value) => { - setIsSelectAllSelected(value); - if (!value) { - fireEvent('onSelect'); - } - }} - setSelected={setInputValue} + showAllSelectedLabel={showAllSelectedLabel} iconColor={iconColor} optionsLoadingState={optionsLoadingState && advanced} darkMode={darkMode} - fireEvent={() => fireEvent('onSelect')} menuPlacement="auto" menuPortalTarget={document.body} + // This is not setting minheight, required to help calculate menuPlacement by providing fixed height upfront before rendering (required in the case of modal) + minMenuHeight={300} />
diff --git a/frontend/src/Editor/WidgetManager/configs/buttonGroup.js b/frontend/src/Editor/WidgetManager/configs/buttonGroup.js index 65b7e77807..4a1d5ff218 100644 --- a/frontend/src/Editor/WidgetManager/configs/buttonGroup.js +++ b/frontend/src/Editor/WidgetManager/configs/buttonGroup.js @@ -123,6 +123,19 @@ export const buttonGroupConfig = { defaultValue: '#007bff', }, }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, }, exposedVariables: { selected: [1], @@ -148,6 +161,7 @@ export const buttonGroupConfig = { disabledState: { value: '{{false}}' }, selectedTextColor: { value: '' }, selectedBackgroundColor: { value: '' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/Editor/WidgetManager/configs/checkbox.js b/frontend/src/Editor/WidgetManager/configs/checkbox.js index c9b6424020..9f991be251 100644 --- a/frontend/src/Editor/WidgetManager/configs/checkbox.js +++ b/frontend/src/Editor/WidgetManager/configs/checkbox.js @@ -126,6 +126,20 @@ export const checkboxConfig = { ], accordian: 'label', }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + accordian: 'switch', + }, }, exposedVariables: { value: false, @@ -189,6 +203,7 @@ export const checkboxConfig = { handleColor: { value: '#FFFFFF' }, alignment: { value: 'right' }, boxShadow: { value: '0px 0px 0px 0px #00000090' }, + padding: { value: 'default' }, }, validation: { mandatory: { value: '{{false}}' }, diff --git a/frontend/src/Editor/WidgetManager/configs/colorPicker.js b/frontend/src/Editor/WidgetManager/configs/colorPicker.js index b2fddd7e4c..6d93508891 100644 --- a/frontend/src/Editor/WidgetManager/configs/colorPicker.js +++ b/frontend/src/Editor/WidgetManager/configs/colorPicker.js @@ -26,6 +26,19 @@ export const colorPickerConfig = { }, styles: { visibility: { type: 'toggle', displayName: 'Visibility' }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, }, exposedVariables: { selectedColorHex: '#000000', @@ -45,6 +58,7 @@ export const colorPickerConfig = { events: [], styles: { visibility: { value: '{{true}}' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/Editor/WidgetManager/configs/container.js b/frontend/src/Editor/WidgetManager/configs/container.js index 424b9a801d..6dc9a679a4 100644 --- a/frontend/src/Editor/WidgetManager/configs/container.js +++ b/frontend/src/Editor/WidgetManager/configs/container.js @@ -3,7 +3,7 @@ export const containerConfig = { displayName: 'Container', description: 'Group components', defaultSize: { - width: 5, + width: 10, height: 200, }, component: 'Container', @@ -44,13 +44,19 @@ export const containerConfig = { displayName: 'Show header', validation: { schema: { type: 'boolean' }, - defaultValue: false, + defaultValue: true, }, }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, }, defaultChildren: [ { componentName: 'Text', + slotName: 'header', layout: { top: 20, left: 1, @@ -98,15 +104,6 @@ export const containerConfig = { }, accordian: 'container', }, - headerHeight: { - type: 'numberInput', - displayName: 'Height', - validation: { - schema: { type: 'number' }, - defaultValue: 80, - }, - accordian: 'header', - }, borderRadius: { type: 'numberInput', displayName: 'Border', @@ -154,10 +151,11 @@ export const containerConfig = { showOnMobile: { value: '{{false}}' }, }, properties: { - showHeader: { value: `{{false}}` }, + showHeader: { value: `{{true}}` }, loadingState: { value: `{{false}}` }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: `{{80}}` }, }, events: [], styles: { diff --git a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js index 308aff1f36..6b011fd082 100644 --- a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js +++ b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js @@ -75,6 +75,18 @@ export const dropdownV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -314,6 +326,8 @@ export const dropdownV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select an option' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/frontend/src/Editor/WidgetManager/configs/form.js b/frontend/src/Editor/WidgetManager/configs/form.js index 2d8eb7f0a8..129322cf73 100644 --- a/frontend/src/Editor/WidgetManager/configs/form.js +++ b/frontend/src/Editor/WidgetManager/configs/form.js @@ -4,7 +4,7 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 480, + height: 450, }, defaultChildren: [ { @@ -19,8 +19,8 @@ export const formConfig = { accessorKey: 'text', styles: ['fontWeight', 'textSize', 'textColor'], defaultValue: { - text: 'Form title', - textSize: 20, + text: 'Form', + textSize: 16, textColor: '#000', }, }, @@ -34,203 +34,68 @@ export const formConfig = { }, properties: ['text'], defaultValue: { - text: 'Button2', + text: 'Submit', padding: 'none', }, }, - { - componentName: 'Text', - layout: { - top: 40, - left: 10, - height: 30, - width: 17, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'User Details', - fontWeight: 'bold', - textSize: 18, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 90, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Name', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 160, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Age', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, { componentName: 'TextInput', layout: { - top: 120, - left: 10, - height: 30, - width: 25, + top: 20, + left: 5, + height: 40, + width: 31, }, properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Enter your name', - label: '', + label: 'Name', + width: '{{60}}', + direction: 'left', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { componentName: 'NumberInput', layout: { - top: 190, - left: 10, - height: 30, - width: 25, + top: 80, + left: 5, + height: 40, + width: 31, }, - properties: ['value', 'label'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { - value: 24, - label: '', + placeholder: 'Age', + label: 'Age', + width: '{{60}}', + direction: 'left', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { - componentName: 'Button', + componentName: 'TextInput', layout: { - top: 240, - left: 10, - height: 30, - width: 10, + top: 140, + left: 5, + height: 40, + width: 31, }, - properties: ['text'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { - text: 'Submit', + placeholder: 'Tomy', + label: 'Pet name', + width: '{{60}}', + alignment: 'side', + direction: 'left', + auto: '{{false}}', + padding: 'default', }, }, ], @@ -276,6 +141,24 @@ export const formConfig = { }, showHeader: { type: 'toggle', displayName: 'Header' }, showFooter: { type: 'toggle', displayName: 'Footer' }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + canvasHeight: { + type: 'numberInput', + displayName: 'Canvas height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + footerHeight: { + type: 'numberInput', + displayName: 'Footer height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, visibility: { type: 'toggle', displayName: 'Visibility', @@ -294,6 +177,13 @@ export const formConfig = { defaultValue: false, }, }, + tooltip: { + type: 'code', + displayName: 'Tooltip', + validation: { schema: { type: 'string' } }, + section: 'additionalActions', + placeholder: 'Enter tooltip text', + }, }, events: { onSubmit: { displayName: 'On submit' }, @@ -316,24 +206,8 @@ export const formConfig = { defaultValue: '#ffffffff', }, }, - headerHeight: { - type: 'code', - displayName: 'Header height', - validation: { - schema: { type: 'string' }, - defaultValue: '80px', - }, - }, - footerHeight: { - type: 'code', - displayName: 'Footer height', - validation: { - schema: { type: 'string' }, - defaultValue: '80px', - }, - }, backgroundColor: { - type: 'color', + type: 'colorSwatches', displayName: 'Background color', validation: { schema: { type: 'string' }, @@ -351,7 +225,7 @@ export const formConfig = { }, }, borderColor: { - type: 'color', + type: 'colorSwatches', displayName: 'Border color', validation: { schema: { type: 'string' }, @@ -403,18 +277,18 @@ export const formConfig = { value: "{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}", }, - showHeader: { value: '{{false}}' }, - showFooter: { value: '{{false}}' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: 60 }, + footerHeight: { value: 60 }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - headerHeight: { value: '60px' }, - footerHeight: { value: '60px' }, }, }, }; diff --git a/frontend/src/Editor/WidgetManager/configs/icon.js b/frontend/src/Editor/WidgetManager/configs/icon.js index aea06c976c..40dc8185dd 100644 --- a/frontend/src/Editor/WidgetManager/configs/icon.js +++ b/frontend/src/Editor/WidgetManager/configs/icon.js @@ -78,6 +78,29 @@ export const iconConfig = { }, accordian: 'Icon', }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + accordian: 'Icon', + }, + boxShadow: { + type: 'boxShadow', + displayName: 'Box shadow', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: '0px 0px 0px 0px #00000040', + }, + accordian: 'Icon', + }, }, exposedVariables: {}, actions: [ @@ -116,6 +139,8 @@ export const iconConfig = { styles: { iconColor: { value: '#000' }, iconAlign: { value: 'center' }, + padding: { value: 'default' }, + boxShadow: { value: '0px 0px 0px 0px #00000040' }, }, }, }; diff --git a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js index b603db9c4a..23618382df 100644 --- a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js +++ b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js @@ -121,6 +121,12 @@ export const multiselectV2Config = { }, accordian: 'Options', }, + showAllSelectedLabel: { + type: 'toggle', + displayName: 'Show "All items are selected"', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + accordian: 'Options', + }, optionsLoadingState: { type: 'toggle', displayName: 'Options loading state', @@ -142,6 +148,18 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -327,6 +345,9 @@ export const multiselectV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select the options' }, + showAllSelectedLabel: { value: '{{true}}' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/frontend/src/Editor/WidgetManager/configs/rangeslider.js b/frontend/src/Editor/WidgetManager/configs/rangeslider.js index 151dca3384..541ed95209 100644 --- a/frontend/src/Editor/WidgetManager/configs/rangeslider.js +++ b/frontend/src/Editor/WidgetManager/configs/rangeslider.js @@ -84,6 +84,19 @@ export const rangeSliderConfig = { defaultValue: true, }, }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, }, exposedVariables: { value: null, @@ -111,6 +124,7 @@ export const rangeSliderConfig = { handleColor: { value: '' }, trackColor: { value: '' }, visibility: { value: '{{true}}' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/Editor/WidgetManager/configs/starrating.js b/frontend/src/Editor/WidgetManager/configs/starrating.js index 01240d0369..d6caf8013c 100644 --- a/frontend/src/Editor/WidgetManager/configs/starrating.js +++ b/frontend/src/Editor/WidgetManager/configs/starrating.js @@ -89,6 +89,19 @@ export const starratingConfig = { defaultValue: false, }, }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, }, exposedVariables: { value: 0, @@ -112,6 +125,7 @@ export const starratingConfig = { labelColor: { value: '' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/Editor/WidgetManager/configs/tags.js b/frontend/src/Editor/WidgetManager/configs/tags.js index 8af289b23a..6479eeaad0 100644 --- a/frontend/src/Editor/WidgetManager/configs/tags.js +++ b/frontend/src/Editor/WidgetManager/configs/tags.js @@ -38,6 +38,19 @@ export const tagsConfig = { defaultValue: true, }, }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + }, }, exposedVariables: {}, definition: { @@ -54,6 +67,7 @@ export const tagsConfig = { events: [], styles: { visibility: { value: '{{true}}' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/Editor/WidgetManager/configs/toggleswitchv2.js b/frontend/src/Editor/WidgetManager/configs/toggleswitchv2.js index 6753fbb50d..6f61a7645d 100644 --- a/frontend/src/Editor/WidgetManager/configs/toggleswitchv2.js +++ b/frontend/src/Editor/WidgetManager/configs/toggleswitchv2.js @@ -126,6 +126,20 @@ export const toggleSwitchV2Config = { validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } }, accordian: 'switch', }, + padding: { + type: 'switch', + displayName: 'Padding', + validation: { + schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, + defaultValue: 'default', + }, + isFxNotRequired: true, + options: [ + { displayName: 'Default', value: 'default' }, + { displayName: 'None', value: 'none' }, + ], + accordian: 'switch', + }, }, exposedVariables: { value: false, @@ -187,6 +201,7 @@ export const toggleSwitchV2Config = { handleColor: { value: '#FFFFFF' }, alignment: { value: 'right' }, boxShadow: { value: '0px 0px 0px 0px #00000090' }, + padding: { value: 'default' }, }, }, }; diff --git a/frontend/src/_styles/queryManager.scss b/frontend/src/_styles/queryManager.scss index 1c63a111e9..1e879bb9c9 100644 --- a/frontend/src/_styles/queryManager.scss +++ b/frontend/src/_styles/queryManager.scss @@ -1250,6 +1250,7 @@ $border-radius: 4px; color: var(--slate12) !important; } } + &.data-source-exists { .cm-editor { border-radius: 0 4px 4px 0 !important; @@ -1834,6 +1835,7 @@ $border-radius: 4px; .cm-scroller { border-bottom-left-radius: 4px; + overscroll-behavior: auto !important; } } @@ -1853,22 +1855,23 @@ $border-radius: 4px; margin-left: 32px !important; } -.tjdb-codhinter-wrapper{ - .codehinter-input{ - .cm-editor{ +.tjdb-codhinter-wrapper { + .codehinter-input { + .cm-editor { // height: 30px !important; min-height: 30px !important; - max-height:100px !important; + max-height: 100px !important; border-radius: 0 !important; - border-right: 0 ; - } + border-right: 0; + } } } -.tjdb-limit-offset-codehinter{ - .cm-editor{ + +.tjdb-limit-offset-codehinter { + .cm-editor { // height: 30px !important; min-height: 30px !important; - max-height:100px !important; + max-height: 100px !important; } } @@ -1896,7 +1899,7 @@ $border-radius: 4px; margin-right: auto; p { - display: flex; + display: flex; align-items: center; span { @@ -1917,8 +1920,13 @@ $border-radius: 4px; } .restapi-key-value { - .code-hinter-wrapper, .code-editor-basic-wrapper, .codehinter-container, .cm-codehinter, .code-editor-query-panel{ - height:100%; + + .code-hinter-wrapper, + .code-editor-basic-wrapper, + .codehinter-container, + .cm-codehinter, + .code-editor-query-panel { + height: 100%; max-height: 100px; } } \ No newline at end of file diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 4fd1083987..f1ab4c8572 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -8280,6 +8280,10 @@ tbody { } } +.query-manager-btn-shortcut { + color: var(--text-disabled) !important; +} + .font-weight-500 { font-weight: 500; } diff --git a/frontend/src/_ui/Icon/solidIcons/Play01.jsx b/frontend/src/_ui/Icon/solidIcons/Play01.jsx new file mode 100644 index 0000000000..42d3c88835 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/Play01.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const Play01 = ({ fill = '#6A727C', width = '24', className = '', viewBox = '0 0 24 24' }) => ( + + + +); + +export default Play01; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index a64674b0bb..c6eff2e46b 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -87,6 +87,7 @@ import Pin from './Pin.jsx'; import Unpin from './Unpin.jsx'; import AlignRight from './AlignRight'; import Play from './Play.jsx'; +import Play01 from './Play01.jsx'; import Plus from './Plus.jsx'; import Plus01 from './Plus01.jsx'; import Reload from './Reload.jsx'; @@ -698,6 +699,8 @@ const Icon = (props) => { return ; case 'ai-crown': return ; + case 'play01': + return ; default: return ; } diff --git a/frontend/src/_ui/Select/SelectComponent.jsx b/frontend/src/_ui/Select/SelectComponent.jsx index 6675b090e3..2d62352000 100644 --- a/frontend/src/_ui/Select/SelectComponent.jsx +++ b/frontend/src/_ui/Select/SelectComponent.jsx @@ -4,6 +4,7 @@ import Select from 'react-select'; import defaultStyles from './styles'; export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSelect, darkMode, ...restProps }) => { + const selectRef = React.useRef(null); const isDarkMode = darkMode ?? localStorage.getItem('darkMode') === 'true'; const { isMulti = false, @@ -22,6 +23,7 @@ export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSele useCustomStyles = false, isDisabled = false, borderRadius, + openMenuOnFocus = false, } = restProps; const customStyles = useCustomStyles ? styles : defaultStyles(isDarkMode, width, height, styles, borderRadius); @@ -56,6 +58,8 @@ export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSele return (