merge base

This commit is contained in:
Vijaykant Yadav 2025-04-25 12:44:25 +05:30
commit 3e7f524403
100 changed files with 2411 additions and 1046 deletions

View file

@ -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 }}

View file

@ -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']
})
}

View file

@ -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:

27
docker/ce-entrypoint.sh Executable file
View file

@ -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 "$@"

View file

@ -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"]

View file

@ -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"]

View file

@ -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

View file

@ -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..."

View file

@ -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"]

View file

@ -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"]

View file

@ -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"

View file

@ -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

View file

@ -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}
/>
</>
);

View file

@ -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;

View file

@ -175,7 +175,6 @@ export class DragContext {
const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot];
return !restrictedWidgets.includes(dragged.widgetType);
ß;
}
}

View file

@ -33,6 +33,7 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [
'Divider',
'VerticalDivider',
'Link',
'Form',
];
const RenderWidget = ({

View file

@ -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

View file

@ -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,
}}

View file

@ -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;

View file

@ -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,
};
};

View file

@ -381,7 +381,6 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
{activeTab === 1 && renderQueryElement()}
{activeTab === 2 && renderTransformation()}
{activeTab === 3 && renderQueryOptions()}
<div className="pb-5" />
<Preview darkMode={darkMode} calculatePreviewHeight={calculatePreviewHeight} />
</>
)}

View file

@ -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 (
<span
{...(isInDraft && {
'data-tooltip-id': 'query-header-btn-run',
'data-tooltip-content': 'Connect a data source to run',
})}
>
<button
onClick={() => runQuery(selectedQuery?.id, selectedQuery?.name, undefined, 'edit', {}, true)}
className={`border-0 default-secondary-button ${buttonLoadingState(isLoading)}`}
data-cy="query-run-button"
disabled={isInDraft}
{...(isInDraft && {
'data-tooltip-id': 'query-header-btn-run',
'data-tooltip-content': 'Publish the query to run',
})}
>
<span
className={cx({
invisible: isLoading,
})}
<span>
<ToolTip message={shortcutDisplay} placement="bottom" trigger={['hover']} show={true} tooltipClassName="">
<ButtonComponent
size="medium"
variant="secondary"
onClick={() => runQuery(selectedQuery?.id, selectedQuery?.name, undefined, 'edit', {}, true)}
leadingIcon="play01"
disabled={isInDraft}
isLoading={isLoading}
className={isMac ? '!tw-w-[88px]' : '!tw-w-[120px]'}
data-cy="query-run-button"
>
<Play width={14} fill="var(--indigo9)" viewBox="0 0 14 14" />
</span>
<span className="query-manager-btn-name">{isLoading ? ' ' : 'Run'}</span>
</button>
{isInDraft && <Tooltip id="query-header-btn-run" className="tooltip" />}
Run
<span className="query-manager-btn-shortcut">{isMac ? '⌘↩' : 'Ctrl+Enter'}</span>
</ButtonComponent>
</ToolTip>
</span>
);
};
@ -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 (
<button
disabled={!hasPermissions}
onClick={onClick}
className={cx(`default-tertiary-button ${buttonLoadingState(isPreviewQueryLoading)} `, {
disabled: !hasPermissions,
})}
data-cy={'query-preview-button'}
>
<span className="query-preview-svg d-flex align-items-center query-icon-wrapper">
<Eye1 width={14} fill="var(--slate9)" />
</span>
<span>{t('editor.queryManager.preview', 'Preview')}</span>
</button>
<ToolTip message={shortcutDisplay} placement="bottom" trigger={['hover']} show={true} tooltipClassName="">
<ButtonComponent
size="medium"
variant="outline"
onClick={onClick}
// className="!tw-w-[100px]"
disabled={!hasPermissions}
isLoading={isPreviewQueryLoading}
data-cy={'query-preview-button'}
>
Preview
</ButtonComponent>
</ToolTip>
);
};

View file

@ -204,7 +204,7 @@ export const Transformation = ({ changeOption, options, darkMode, queryId, rende
<br />
<div className={`d-flex copilot-codehinter-wrap ${!enableTransformation && 'read-only-codehinter'}`}>
<div className="col flex-grow-1">
<div style={{ borderRadius: '6px', marginBottom: '20px', background: darkMode ? '#272822' : '#F8F9FA' }}>
<div style={{ borderRadius: '6px', background: darkMode ? '#272822' : '#F8F9FA' }}>
<div className="py-3 px-3 d-flex justify-content-between copilot-section-header">
<Tab.Container
activeKey={lang}

View file

@ -68,7 +68,7 @@ export default ({
);
})}
{bodyToggle && (
<div>
<div className="rest-api-body-codehinter">
<CodeHinter
type="extendedSingleLine"
initialValue={(rawBody || jsonBody) ?? ''} // If raw_body is not set, set initial value to legacy json_body if present

View file

@ -13,7 +13,7 @@ const Runjs = (props) => {
}, [props.options]);
return (
<Card className="runjs-editor mb-3">
<Card className="runjs-editor mb-3 !tw-mb-0">
<CodeHinter
renderCopilot={props.renderCopilot}
type="multiline"

View file

@ -16,7 +16,7 @@ export class Runpy extends React.Component {
render() {
return (
<div className="runps-editor mb-3">
<div className="runps-editor mb-3 !tw-mb-0">
<CodeHinter
renderCopilot={this.props.renderCopilot}
type="multiline"

View file

@ -0,0 +1,26 @@
import React from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { useHotkeys } from 'react-hotkeys-hook';
import { useModuleId } from '@/AppBuilder/_contexts/ModuleContext';
const QueryKeyHooks = ({ children, isExpanded }) => {
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 <div className="row main-row">{children}</div>;
};
export default QueryKeyHooks;

View file

@ -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 && (
<div className="row main-row">
<QueryKeyHooks isExpanded={isExpanded}>
<MemoizedQueryDataPane darkMode={darkMode} />
<div className="query-definition-pane-wrapper">
<div className="query-definition-pane">
<MemoizedQueryManager darkMode={darkMode} />
</div>
</div>
</div>
</QueryKeyHooks>
)}
</div>
<Tooltip id="tooltip-for-query-panel-footer-btn" className="tooltip" />

View file

@ -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,

View file

@ -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,

View file

@ -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' },
},

View file

@ -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}}' },

View file

@ -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' },
},
},
};

View file

@ -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: {

View file

@ -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}}' },

View file

@ -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' },
},
},
};

View file

@ -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' },
},
},
};

View file

@ -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}}' },

View file

@ -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' },
},
},
};

View file

@ -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' },
},
},
};

View file

@ -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' },
},
},

View file

@ -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' },
},
},
};

View file

@ -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:

View file

@ -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 (
<div className={`jet-form-${slotName} wj-form-${slotName}`} style={slotStyle}>
<div
className={`resizable-slot only-${slotName} ${isActive ? 'active' : ''} ${isEditing && 'is-editing'} ${
isDragging ? 'dragging' : ''
}`}
{...getRootProps()}
>
<SubContainer
id={id}
canvasHeight={canvasHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
margin: 0,
backgroundColor: 'transparent',
overflow: 'hidden',
}}
componentType="Form"
/>
{isEditing && <div className="resize-handle" {...getHandleProps()} style={resizeStyle} />}
</div>
{isDisabled && (
<div
id={`${id}-disabled`}
className="tj-form-disabled-overlay"
style={{ height: resizedHeight || '100%' }}
onClick={() => {}}
onDrop={(e) => e.stopPropagation()}
/>
)}
</div>
);
}
);

View file

@ -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 (
<form
className={`jet-container ${advanced && 'jet-container-json-form'}`}
className={`jet-container jet-form-widget ${advanced && 'jet-container-json-form'}`}
id={id}
data-cy={dataCy}
ref={parentRef}
@ -299,32 +328,22 @@ export const Form = function Form(props) {
if (e.target.className === 'real-canvas') onComponentClick(id, component);
}} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked
>
{showHeader && (
<div style={formHeader} className="wj-form-header">
<SubContainer
id={`${id}-header`}
canvasHeight={canvasHeaderHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
backgroundColor: 'transparent',
height: headerHeight,
}}
componentType="Form"
/>
{isDisabled && (
<div
id={`${id}-header-disabled`}
className="tj-form-disabled-overlay"
style={{ height: headerHeight || '100%' }}
onClick={() => {}}
onDrop={(e) => e.stopPropagation()}
/>
)}
</div>
{!advanced && showHeader && (
<HorizontalSlot
slotName="header"
slotStyle={formHeader}
isEditing={isEditing}
id={`${id}-header`}
height={headerHeight}
width={width}
darkMode={darkMode}
isDisabled={isDisabled}
isActive={activeSlot === `${id}-header`}
onResize={updateHeaderSizeInStore}
/>
)}
<div className="jet-form-body" style={formContent}>
<div className="jet-form-body sub-container-overflow-wrap" style={formContent}>
{isLoading ? (
<div className="p-2 tw-flex tw-items-center tw-justify-center" style={{ margin: '0px auto' }}>
<div className="spinner-border" role="status"></div>
@ -332,14 +351,17 @@ export const Form = function Form(props) {
) : (
<fieldset disabled={isDisabled} style={{ width: '100%' }}>
{!advanced && (
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: '100%' }}>
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: canHeight || '100%' }}>
<SubContainer
id={id}
canvasHeight={computedStyles.height}
canvasHeight={parseInt(computedFormBodyHeight, 10)}
canvasWidth={width}
onOptionChange={onOptionChange}
onOptionsChange={onOptionsChange}
styles={{ backgroundColor: computedStyles.backgroundColor }}
styles={{
backgroundColor: computedStyles.backgroundColor,
height: canHeight,
}}
darkMode={darkMode}
componentType="Form"
/>
@ -381,31 +403,19 @@ export const Form = function Form(props) {
</fieldset>
)}
</div>
{showFooter && (
<div className="jet-form-footer wj-form-footer" style={formFooter}>
<SubContainer
id={`${id}-footer`}
canvasHeight={canvasFooterHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
margin: 0,
backgroundColor: 'transparent',
height: footerHeight,
}}
componentType="Form"
/>
{isDisabled && (
<div
id={`${id}-footer-disabled`}
className="tj-form-disabled-overlay"
style={{ height: footerHeight || '100%' }}
onClick={() => {}}
onDrop={(e) => e.stopPropagation()}
/>
)}
</div>
{!advanced && showFooter && (
<HorizontalSlot
slotName="footer"
slotStyle={formFooter}
isEditing={isEditing}
id={`${id}-footer`}
height={footerHeight}
width={width}
darkMode={darkMode}
isDisabled={isDisabled}
onResize={updateFooterSizeInStore}
isActive={activeSlot === `${id}-footer`}
/>
)}
</form>
);

View file

@ -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`;
};

View file

@ -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;
}
}

View file

@ -277,7 +277,7 @@ export const Modal = function Modal({
<Modal.Component
show={showModal}
contentClassName="modal-component"
contentClassName="modal-component tj-modal--container"
container={document.getElementsByClassName('real-canvas')[0]}
size={size}
keyboard={true}

View file

@ -56,7 +56,7 @@ export const ModalWidget = ({ ...restProps }) => {
return (
<BootstrapModal
{...restProps}
contentClassName="modal-component tj-modal-widget-content"
contentClassName="modal-component tj-modal--container tj-modal-widget-content"
animation={true}
onEscapeKeyDown={(e) => {
e.preventDefault();

View file

@ -117,7 +117,7 @@ export const Header = memo(
</div>
</div>
{showFilter && (
<Filter table={table} darkMode={darkMode} setFilters={setFilters} setShowFilter={setShowFilter} />
<Filter id={id} table={table} darkMode={darkMode} setFilters={setFilters} setShowFilter={setShowFilter} />
)}
</>
);

View file

@ -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 }) => {
<div className="card-body">
{localFilters.map((filter, index) => (
<FilterRow
id={id}
key={index}
filter={filter}
index={index}

View file

@ -1,12 +1,15 @@
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import Select from '@/_ui/Select';
import { components } from 'react-select';
import defaultStyles from '@/_ui/Select/styles';
import { FILTER_OPTIONS } from './filterConstants';
import useStore from '@/AppBuilder/_stores/store';
export const FilterRow = memo(
({ filter, index, columns, darkMode, onColumnChange, onOperationChange, onValueChange, onRemove }) => {
({ 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}
/>
</div>
<div data-cy={`select-operation-dropdown-${index}`} className="col" style={{ maxWidth: '180px' }}>
@ -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}
/>
</div>
<div className="col">
@ -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 <components.ValueContainer {...props} innerProps={{ ...props.innerProps, onMouseDown: handleClick }} />;
};

View file

@ -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;
};

View file

@ -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;

View file

@ -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);
},
});

View file

@ -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);
},
},
});

View file

@ -161,8 +161,8 @@ export const ColorPicker = function ({
: { display: 'none' };
return (
<div>
<div style={{ baseStyle, boxShadow }} className="form-control" data-cy={dataCy}>
<div className="h-100">
<div style={{ baseStyle, boxShadow, height: '100%' }} className="form-control" data-cy={dataCy}>
<div
className="d-flex h-100 justify-content-between align-items-center"
onClick={() => setShowColorPicker(true)}

View file

@ -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 (
<div
id={`dropdown-multiselect-widget-custom-menu-list-${menuId}`}
className={cx('dropdown-multiselect-widget-custom-menu-list', { 'theme-dark dark-theme': darkMode })}
onClick={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
>
<div className="dropdown-multiselect-widget-search-box-wrapper">
<span>
<SolidIcon name="search01" width="14" />
</span>
<input
autoCorrect="off"
autoComplete="off"
spellCheck="false"
type="text"
value={inputValue}
onChange={(e) =>
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"
/>
</div>
{showAllOption && !optionsLoadingState && (
<label htmlFor="select-all-checkbox" className="multiselect-custom-menulist-select-all">
<FormCheck id="select-all-checkbox" checked={isSelectAllSelected} onChange={handleSelectAll} />
<span style={{ marginLeft: '4px' }}>Select all</span>
</label>
{showSearchInput && (
<div className="dropdown-multiselect-widget-search-box-wrapper">
<span>
<SolidIcon name="search01" width="14" />
</span>
<input
autoCorrect="off"
autoComplete="off"
spellCheck="false"
type="text"
value={inputValue}
onChange={(e) =>
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"
/>
</div>
)}
<MenuList {...props} selectProps={selectProps}>
{optionsLoadingState ? (
<div class="text-center py-4" style={{ minHeight: '188px' }}>
<Loader style={{ zIndex: 3, position: 'absolute' }} width="36" />
{!optionsLoadingState && (
<div
ref={parentRef}
className="dropdown-multiselect-widget-custom-menu-list-body"
style={{
maxHeight: selectProps.maxMenuHeight || 300,
}}
>
<div
style={{
height: `${virtualizer.getTotalSize() || 38}px`,
position: 'relative',
marginTop: '5px',
}}
>
{!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 (
<div
key={option.value}
style={{
position: isSelectAll ? 'sticky' : 'absolute',
width: '100%',
top: 0,
zIndex: isSelectAll && 10,
transform: `translateY(${virtualItem.start}px)`,
}}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
>
<MenuList {...props} selectProps={selectProps}>
<div>{child}</div>
</MenuList>
</div>
);
})}
</div>
) : (
props.children
)}
</MenuList>
</div>
)}
{optionsLoadingState && (
<div className="text-center py-4" style={{ minHeight: '188px' }}>
<Loader style={{ zIndex: 3, position: 'absolute' }} width="36" />
</div>
)}
</div>
);
};

View file

@ -6,7 +6,17 @@ import { highlightText } from './utils';
const CustomOption = (props) => {
return (
<components.Option {...props}>
<components.Option
{...props}
innerProps={{
...props.innerProps,
onTouchEnd: (e) => {
e.preventDefault();
e.stopPropagation();
props.selectOption(props.data);
},
}}
>
<div className="cursor-pointer">
{props.isSelected && (
<span style={{ maxHeight: '20px', marginRight: '8px', marginLeft: '-28px' }}>

View file

@ -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 = ({
<div
className="w-100 px-0 h-100 dropdownV2-widget"
ref={ref}
onMouseDownCapture={isEditor && handleClickInEditor}
onClick={handleClickInsideSelect}
onTouchEnd={handleClickInsideSelect}
>
<Select
ref={selectRef}
@ -456,17 +487,21 @@ export const DropdownV2 = ({
onChange={(selectedOption, actionProps) => {
if (actionProps.action === 'clear') {
setInputValue(null);
fireEvent('onSelect');
}
if (actionProps.action === 'select-option') {
setInputValue(selectedOption.value);
fireEvent('onSelect');
if (currentValue === selectedOption.value) {
setInputValue(null);
} else setInputValue(selectedOption.value);
}
fireEvent('onSelect');
setSearchInputValue('');
setIsMenuOpen(false);
setUserInteracted(true);
}}
options={selectOptions}
styles={customStyles}
isLoading={isDropdownLoading}
showSearchInput={showSearchInput}
onInputChange={onSearchTextChange}
inputValue={searchInputValue}
placeholder={placeholder}
@ -477,7 +512,7 @@ export const DropdownV2 = ({
Option: CustomOption,
LoadingIndicator: () => <Loader style={{ right: '11px', zIndex: 3, position: 'absolute' }} width="16" />,
DropdownIndicator: isDropdownLoading ? () => null : CustomDropdownIndicator,
ClearIndicator: CustomClearIndicator,
ClearIndicator: showClearBtn ? CustomClearIndicator : () => null,
}}
isClearable
tabSelectsValue={false}
@ -488,24 +523,20 @@ export const DropdownV2 = ({
darkMode={darkMode}
optionsLoadingState={optionsLoadingState && advanced}
menuPlacement="auto"
onMenuOpen={() => {
setIsMenuOpen(true);
fireEvent('onFocus');
}}
onMenuClose={() => {
setIsMenuOpen(false);
fireEvent('onBlur');
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isMenuOpen) {
if (e.key === 'Enter' && !isMenuOpen && !isDropdownLoading) {
setIsMenuOpen(true);
fireEvent('onFocus');
e.preventDefault();
}
if (e.key === 'Escape' && isMenuOpen) {
setIsMenuOpen(false);
fireEvent('onBlur');
e.preventDefault();
}
}}
// 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}
/>
</div>
</div>

View file

@ -0,0 +1,7 @@
.dropdown-multiselect-widget-custom-menu-list-body {
overflow-y: auto;
position: relative;
padding: 0 0 5px;
background-color: var(--surfaces-surface-01) !important;
border-radius: 0 0 8px 8px;
}

View file

@ -17,7 +17,7 @@ export const Icon = ({
}) => {
const isInitialRender = useRef(true);
const { icon, loadingState, disabledState } = properties;
const { iconAlign, iconColor } = styles;
const { iconAlign, iconColor, boxShadow } = styles;
// eslint-disable-next-line import/namespace
const IconElement = Icons[icon];
@ -84,10 +84,10 @@ export const Icon = ({
</div>
) : (
<div
className={cx('icon-widget', { 'd-none': !visibility }, { 'cursor-pointer': false })}
className={cx('icon-widget h-100', { 'd-none': !visibility }, { 'cursor-pointer': false })}
data-cy={dataCy}
data-disabled={isDisabled}
style={{ textAlign: iconAlign }}
style={{ textAlign: iconAlign, boxShadow }}
onMouseEnter={(event) => {
event.stopPropagation();
fireEvent('onHover');
@ -97,7 +97,7 @@ export const Icon = ({
color={color}
style={{
width: height < width ? 'auto' : width,
height: height < width ? height : 'auto',
height: height < width ? '100%' : 'auto',
color: iconColor,
}}
onClick={(event) => {

View file

@ -19,8 +19,7 @@ export const Link = ({ height, properties, styles, fireEvent, setExposedVariable
const computedStyles = {
display: 'flex',
alignItems: verticalAlignment === 'top' ? 'flex-start' : verticalAlignment === 'center' ? 'center' : 'flex-end',
justifyContent:
horizontalAlignment === 'left' ? 'flex-start' : horizontalAlignment === 'center' ? 'center' : 'flex-end',
textAlign: horizontalAlignment === 'left' ? 'left' : horizontalAlignment === 'center' ? 'center' : 'right',
height: '100%',
width: '100%',
boxShadow,
@ -113,10 +112,20 @@ export const Link = ({ height, properties, styles, fireEvent, setExposedVariable
onMouseOver={() => {
fireEvent('onHover');
}}
style={{ color: textColor, fontSize: textSize, cursor: isDisabled ? 'not-allowed' : 'pointer' }}
style={{ width: '100%' }}
ref={clickRef}
>
<span className="d-flex justify-content-center">
<span
className="d-flex"
style={{
fontSize: textSize,
cursor: 'auto',
justifyContent:
horizontalAlignment === 'left' ? 'flex-start' : horizontalAlignment === 'center' ? 'center' : 'flex-end',
color: textColor,
paddingBottom: verticalAlignment === 'bottom' ? '1px' : '0px',
}}
>
{iconVisibility && (
<IconElement
style={{
@ -130,7 +139,7 @@ export const Link = ({ height, properties, styles, fireEvent, setExposedVariable
stroke={1.5}
/>
)}
{linkTextState}
<span className="link-text">{linkTextState}</span>
</span>
</a>
</div>

View file

@ -1,8 +1,18 @@
.link-widget {
a {
text-underline-offset: 32%; // Adds space between text and underline
&:hover {
color: var(--link-hover-color) !important;
}
text-decoration: none !important;
pointer-events: none;
cursor: none !important;
.link-text {
pointer-events: all;
text-underline-offset: 32%; // Adds space between text and underline
cursor: pointer;
&:hover {
text-decoration: underline;
text-decoration-color: var(--link-hover-color) !important;
color: var(--link-hover-color) !important;
}
}
}
}

View file

@ -7,11 +7,23 @@ import { highlightText } from '../DropdownV2/utils';
const CustomOption = (props) => {
return (
<Option {...props}>
<Option
{...props}
innerProps={{
...props.innerProps,
onTouchEnd: (e) => {
e.preventDefault();
e.stopPropagation();
props.selectOption(props.data);
},
}}
>
<div className="d-flex multiselct-widget-option">
<FormCheck checked={props.isSelected} disabled={props?.isDisabled} />
<span style={{ marginLeft: '5px' }}>
{highlightText(props.label?.toString(), props.selectProps.inputValue)}
{props.label?.includes('Select all')
? 'Select all'
: highlightText(props.label?.toString(), props.selectProps.inputValue)}
</span>
</div>
</Option>

View file

@ -36,13 +36,13 @@ const CustomValueContainer = ({ children, ...props }) => {
</Placeholder>
) : (
<span className="text-truncate" {...props} id="options" style={{ maxWidth: valueContainerWidth }}>
{isAllOptionsSelected ? 'All items are selected.' : values.join(', ')}
{selectProps?.showAllSelectedLabel && isAllOptionsSelected ? 'All items are selected.' : values.join(', ')}
</span>
)}
{/* Rendering children except Placeholder component to preserve the default behavior of react-select like focus
handling */}
{React.Children.map(children, (child) => {
if (child.type !== Placeholder) {
if (child?.type !== Placeholder) {
return child;
}
})}

View file

@ -12,7 +12,6 @@ import Label from '@/_ui/Label';
const tinycolor = require('tinycolor2');
import { CustomDropdownIndicator, CustomClearIndicator } from '../DropdownV2/DropdownV2';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from '../DropdownV2/utils';
import useStore from '@/AppBuilder/_stores/store';
export const MultiselectV2 = ({
id,
@ -38,6 +37,9 @@ export const MultiselectV2 = ({
loadingState: multiSelectLoadingState,
optionsLoadingState,
sort,
showAllSelectedLabel,
showClearBtn,
showSearchInput,
} = properties;
const {
selectedTextColor,
@ -73,12 +75,9 @@ export const MultiselectV2 = ({
const [visibility, setVisibility] = useState(properties.visibility);
const [isMultiSelectLoading, setIsMultiSelectLoading] = useState(multiSelectLoadingState);
const [isMultiSelectDisabled, setIsMultiSelectDisabled] = useState(disabledState);
const [isSelectAllSelected, setIsSelectAllSelected] = useState(false);
const [searchInputValue, setSearchInputValue] = useState('');
const _height = padding === 'default' ? `${height}px` : `${height + 4}px`;
const [userInteracted, setUserInteracted] = useState(false);
const currentMode = useStore((state) => state.currentMode);
const isEditor = currentMode === 'edit';
const [isMultiselectOpen, setIsMultiselectOpen] = useState(false);
useEffect(() => {
@ -93,18 +92,29 @@ export const MultiselectV2 = ({
const _options = advanced ? schema : options;
let _selectOptions = Array.isArray(_options)
? _options
.filter((data) => data?.visible ?? true)
.map((data) => ({
...data,
label: data?.label,
value: data?.value,
isDisabled: data?.disable ?? false,
}))
.filter((data) => data?.visible ?? true)
.map((data) => ({
...data,
label: data?.label,
value: data?.value,
isDisabled: data?.disable ?? false,
}))
: [];
return sortArray(_selectOptions, sort);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, JSON.stringify(schema), JSON.stringify(options), sort]);
const modifiedSelectOptions = useMemo(() => {
// Adding select all option dynamically to the options
if (showAllOption && !optionsLoadingState) {
return [
// Appended search input value so that it is always visible
{ label: `Select all ${searchInputValue}`, value: 'multiselect-custom-menulist-select-all' },
...selectOptions,
];
} else return selectOptions;
}, [showAllOption, JSON.stringify(selectOptions), optionsLoadingState, searchInputValue]);
function findDefaultItem(value, isAdvanced, isDefault) {
if (isAdvanced) {
const foundItem = Array.isArray(schema) ? schema.filter((item) => item?.visible && item?.default) : [];
@ -129,8 +139,28 @@ export const MultiselectV2 = ({
}
return false;
}
const onChangeHandler = (items, action) => {
setInputValue(items);
const SELECT_ALL = 'multiselect-custom-menulist-select-all';
if (action.option?.value === SELECT_ALL) {
// Case 1 - If select all is selected
if (action.action === 'select-option') {
setInputValue(modifiedSelectOptions);
} else {
setInputValue([]);
}
} else if (items?.some((item) => item.value === SELECT_ALL)) {
// Case 2 - If select all is not selected but selected options include select all
setInputValue(items.filter((item) => item.value !== SELECT_ALL));
} else if (selectOptions?.length === items?.length) {
// Case 3 - If all options are selected except select all
setInputValue(modifiedSelectOptions);
} else {
// Case 4 - Normal selection
setInputValue(items);
}
fireEvent('onSelect');
setUserInteracted(true);
};
@ -270,26 +300,34 @@ export const MultiselectV2 = ({
fireEvent('onSearchTextChanged');
}
};
const handleClickOutside = (event) => {
const handleClickOutsideSelect = (event) => {
let menu = document.getElementById(`dropdown-multiselect-widget-custom-menu-list-${id}`);
if (
isMultiselectOpen &&
multiselectRef.current &&
!multiselectRef.current.contains(event.target) &&
menu &&
!menu.contains(event.target)
) {
if (isMultiselectOpen) {
fireEvent('onBlur');
setIsMultiselectOpen(false);
setSearchInputValue('');
}
setIsMultiselectOpen(false);
fireEvent('onBlur');
setSearchInputValue('');
}
};
const handleClickInEditor = (e) => {
if (e.target.className.includes('clear-indicator') || isMultiselectOpen) return;
e.stopPropagation();
selectRef.current?.onControlMouseDown(e);
const handleClickInsideSelect = () => {
if (isMultiSelectDisabled || isMultiSelectLoading) return;
if (isMultiselectOpen) {
setIsMultiselectOpen(false);
fireEvent('onBlur');
setSearchInputValue('');
} else {
setIsMultiselectOpen(true);
fireEvent('onFocus');
if (!showSearchInput) {
selectRef.current.focus();
}
}
};
const setInputValue = (values) => {
@ -304,20 +342,11 @@ export const MultiselectV2 = ({
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside, { capture: true });
document.addEventListener('mousedown', handleClickOutsideSelect, { capture: true });
return () => {
document.removeEventListener('mousedown', handleClickOutside, { capture: true });
document.removeEventListener('mousedown', handleClickOutsideSelect, { capture: true });
};
}, [isMultiselectOpen]);
// Handle Select all logic
useEffect(() => {
if (selectOptions?.length === selected?.length) {
setIsSelectAllSelected(true);
} else {
setIsSelectAllSelected(false);
}
}, [selectOptions, selected]);
}, [isMultiselectOpen, componentName]);
const customStyles = {
container: (base) => ({
@ -365,8 +394,8 @@ export const MultiselectV2 = ({
selectedTextColor !== '#1B1F24'
? selectedTextColor
: isMultiSelectLoading || isMultiSelectDisabled
? 'var(--text-disabled)'
: 'var(--text-primary)',
? 'var(--text-disabled)'
: 'var(--text-primary)',
}),
input: (provided, _state) => ({
@ -401,10 +430,10 @@ export const MultiselectV2 = ({
color: _state.isDisabled
? 'var(_--text-disbled)'
: selectedTextColor !== '#1B1F24'
? selectedTextColor
: isMultiSelectDisabled || isMultiSelectLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
? selectedTextColor
: isMultiSelectDisabled || isMultiSelectLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
borderRadius: _state.isFocused && '8px',
padding: '8px 6px 8px 12px',
'&:hover': {
@ -415,7 +444,7 @@ export const MultiselectV2 = ({
}),
menuList: (provided) => ({
...provided,
padding: '4px',
padding: '0 4px',
// this is needed otherwise :active state doesn't look nice, gap is required
display: 'flex',
flexDirection: 'column',
@ -426,6 +455,7 @@ export const MultiselectV2 = ({
menu: (provided) => ({
...provided,
marginTop: '5px',
borderRadius: '8px',
}),
};
const _width = (labelWidth / 100) * 70; // Max width which label can go is 70% for better UX calculate width based on this value
@ -436,7 +466,7 @@ export const MultiselectV2 = ({
data-cy={`label-${String(componentName).toLowerCase()} `}
className={cx('multiselect-widget', 'd-flex', {
[alignment === 'top' &&
((labelWidth != 0 && label?.length != 0) || (auto && labelWidth == 0 && label && label?.length != 0))
((labelWidth != 0 && label?.length != 0) || (auto && labelWidth == 0 && label && label?.length != 0))
? 'flex-column'
: 'align-items-center']: true,
'flex-row-reverse': direction === 'right' && alignment === 'side',
@ -468,17 +498,18 @@ export const MultiselectV2 = ({
_width={_width}
top={'1px'}
/>
<div className="w-100 px-0 h-100" onMouseDownCapture={isEditor && handleClickInEditor}>
<div className="w-100 px-0 h-100" onClick={handleClickInsideSelect} onTouchEnd={handleClickInsideSelect}>
<Select
ref={selectRef}
menuId={id}
isDisabled={isMultiSelectDisabled}
value={selected}
onChange={onChangeHandler}
options={selectOptions}
options={modifiedSelectOptions}
styles={customStyles}
// Only show loading when dynamic options are enabled
isLoading={isMultiSelectLoading}
showSearchInput={showSearchInput}
onInputChange={onSearchTextChange}
inputValue={searchInputValue}
menuIsOpen={isMultiselectOpen}
@ -488,7 +519,7 @@ export const MultiselectV2 = ({
ValueContainer: CustomValueContainer,
Option: CustomOption,
LoadingIndicator: () => <Loader style={{ right: '11px', zIndex: 3, position: 'absolute' }} width="16" />,
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}
/>
</div>
</div>

View file

@ -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' },
},
},
};

View file

@ -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}}' },

View file

@ -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' },
},
},
};

View file

@ -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: {

View file

@ -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}}' },

View file

@ -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' },
},
},
};

View file

@ -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' },
},
},
};

View file

@ -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}}' },

View file

@ -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' },
},
},
};

View file

@ -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' },
},
},
};

View file

@ -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' },
},
},
};

View file

@ -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' },
},
},
};

View file

@ -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;
}
}

View file

@ -8280,6 +8280,10 @@ tbody {
}
}
.query-manager-btn-shortcut {
color: var(--text-disabled) !important;
}
.font-weight-500 {
font-weight: 500;
}

View file

@ -0,0 +1,19 @@
import React from 'react';
const Play01 = ({ fill = '#6A727C', width = '24', className = '', viewBox = '0 0 24 24' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
className={className}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.5904 14.6487L7.43642 21.5939C5.40287 22.7559 2.87265 21.2875 2.87265 18.9454V5.05514C2.87265 2.71301 5.40287 1.24466 7.43642 2.40669L19.5904 9.35182C21.6397 10.5228 21.6397 13.4777 19.5904 14.6487Z"
fill={fill}
/>
</svg>
);
export default Play01;

View file

@ -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 <StudentIcon {...props} />;
case 'ai-crown':
return <AICrown {...props} />;
case 'play01':
return <Play01 {...props} />;
default:
return <Apps {...props} />;
}

View file

@ -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 (
<Select
{...restProps}
ref={selectRef}
selectRef={selectRef} // Exposed ref for custom components if needed
isLoading={isLoading}
isDisabled={isDisabled || isLoading}
options={selectOptions}
@ -64,6 +68,7 @@ export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSele
onChange={handleOnChange}
placeholder={placeholder}
styles={customStyles}
openMenuOnFocus={openMenuOnFocus}
formatOptionLabel={(option) => renderCustomOption(option)}
menuPlacement={menuPlacement}
maxMenuHeight={maxMenuHeight}

View file

@ -133,10 +133,20 @@ const Button = forwardRef(
const iconFillColor = !defaultButtonFillColour.includes(fill) && fill ? fill : getDefaultIconFillColor(variant);
const Comp = asChild ? Slot : 'Button';
const leadingIconElement = leadingIcon && (
<SolidIcon name={leadingIcon} height={getIconSize(size)} width={getIconSize(size)} fill={iconFillColor} />
<div
className="tw-flex tw-justify-center tw-items-center"
style={{ width: getIconSize(size), height: getIconSize(size) }}
>
<SolidIcon name={leadingIcon} fill={iconFillColor} />
</div>
);
const trailingIconElement = trailingIcon && (
<SolidIcon name={trailingIcon} height={getIconSize(size)} width={getIconSize(size)} fill={iconFillColor} />
<div
className="tw-flex tw-justify-center tw-items-center"
style={{ width: getIconSize(size), height: getIconSize(size) }}
>
<SolidIcon name={trailingIcon} fill={iconFillColor} />
</div>
);
return (

View file

@ -30,6 +30,7 @@ export class UpdateVisibilityFrIconComponent1737039401111 implements MigrationIn
const properties = component.properties;
const styles = component.styles;
const general = component.general;
const generalStyles = component.generalStyles;
if (styles.visibility) {
properties.visibility = styles.visibility;
@ -41,6 +42,11 @@ export class UpdateVisibilityFrIconComponent1737039401111 implements MigrationIn
delete general?.tooltip;
}
if (generalStyles?.boxShadow) {
styles.boxShadow = generalStyles?.boxShadow;
delete generalStyles?.boxShadow;
}
await entityManager.update(Component, component.id, {
properties,
styles,
@ -49,5 +55,5 @@ export class UpdateVisibilityFrIconComponent1737039401111 implements MigrationIn
}
}
public async down(queryRunner: QueryRunner): Promise<void> {}
public async down(queryRunner: QueryRunner): Promise<void> { }
}

View file

@ -0,0 +1,50 @@
import { Component } from 'src/entities/component.entity';
import { processDataInBatches } from '@helpers/migration.helper';
import { EntityManager, MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateContainerHeaderProperty1744097765065 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const componentTypes = ['Container'];
const batchSize = 100;
const entityManager = queryRunner.manager;
for (const componentType of componentTypes) {
await processDataInBatches(
entityManager,
async (entityManager: EntityManager) => {
return await entityManager.find(Component, {
where: { type: componentType },
order: { createdAt: 'ASC' },
});
},
async (entityManager: EntityManager, components: Component[]) => {
await this.processUpdates(entityManager, components);
},
batchSize
);
}
}
private async processUpdates(entityManager: EntityManager, components: Component[]) {
for (const component of components) {
const properties = component.properties;
const styles = component.styles;
const general = component.general;
// Update showHeader property to false for old instances
if (!properties.showHeader) {
properties.showHeader = { value: '{{false}}' };
}
// Update the modal component with the modified properties
await entityManager.update(Component, component.id, {
properties,
styles,
general,
});
}
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -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],
@ -164,6 +177,7 @@ export const buttonGroupConfig = {
disabledState: { value: '{{false}}' },
selectedTextColor: { value: '#FFFFFF' },
selectedBackgroundColor: { value: 'var(--primary-brand)' },
padding: { value: 'default' },
alignment: { value: 'left' },
},
},

View file

@ -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}}' },

View file

@ -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' },
},
},
};

View file

@ -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: {

View file

@ -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}}' },

View file

@ -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,19 +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'}}} }}",
},
buttonToSubmit: { value: '{{"none"}}' },
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' },
},
},
};

View file

@ -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,7 @@ export const iconConfig = {
styles: {
iconColor: { value: '#000' },
iconAlign: { value: 'center' },
},
padding: { value: 'default' },
boxShadow: { value: '0px 0px 0px 0px #00000040' },
},
};

View file

@ -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}}' },

View file

@ -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' },
},
},
};

View file

@ -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' },
},
},
};

View file

@ -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' },
},
},

View file

@ -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' },
},
},
};