diff --git a/cypress-tests/cypress/e2e/happyPath/appbuilder/commonTestcases/newSuits/appTitle.cy.js b/cypress-tests/cypress/e2e/happyPath/appbuilder/commonTestcases/newSuits/appTitle.cy.js index b9bce11f9a..e98190bc81 100644 --- a/cypress-tests/cypress/e2e/happyPath/appbuilder/commonTestcases/newSuits/appTitle.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/appbuilder/commonTestcases/newSuits/appTitle.cy.js @@ -1,34 +1,5 @@ import { fake } from "Fixtures/fake"; -import { textInputText } from "Texts/textInput"; -import { commonWidgetText, widgetValue, customValidation } from "Texts/common"; -import { commonSelectors, commonWidgetSelector } from "Selectors/common"; -import { buttonText } from "Texts/button"; -import { - verifyControlComponentAction, - randomString, -} from "Support/utils/editor/textInput"; -import { - openAccordion, - verifyAndModifyParameter, - openEditorSidebar, - verifyAndModifyToggleFx, - addDefaultEventHandler, - verifyComponentValueFromInspector, - selectColourFromColourPicker, - verifyBoxShadowCss, - verifyLayout, - verifyTooltip, - editAndVerifyWidgetName, - verifyPropertiesGeneralAccordion, - verifyStylesGeneralAccordion, - randomNumber, - closeAccordions, -} from "Support/utils/commonWidget"; -import { - selectCSA, - selectEvent, - addSupportCSAData, -} from "Support/utils/events"; +import { commonWidgetSelector } from "Selectors/common"; describe("Editor title", () => { const data = {}; @@ -44,8 +15,8 @@ describe("Editor title", () => { }); it("should verify titles", () => { cy.url().should("include", "/tooljets-workspace"); - // cy.title().should("eq", "Dashboard | ToolJet"); - cy.title().should("eq", "ToolJet"); + cy.title().should("eq", "Dashboard | ToolJet"); + // cy.title().should("eq", "ToolJet"); cy.log(data.appName); @@ -57,7 +28,7 @@ describe("Editor title", () => { cy.url().should("include", `/applications/${Cypress.env("appId")}`); // cy.title().should("eq", `${data.appName} | ToolJet`); - // cy.title().should("eq", `Preview - ${data.appName} | ToolJet`); + cy.title().should("eq", `Preview - ${data.appName} | ToolJet`); cy.go("back"); cy.releaseApp(); diff --git a/docker/cloud/cloud-entrypoint.sh b/docker/cloud/cloud-entrypoint.sh new file mode 100644 index 0000000000..26df38cb5c --- /dev/null +++ b/docker/cloud/cloud-entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +npm cache clean --force + +if [ -d "./server/dist" ]; then + SETUP_CMD='npm run cloud:setup:prod' +else + SETUP_CMD='npm run cloud:setup' +fi + +npm cache clean --force + +if [ -f "./.env" ]; then + declare $(grep -v '^#' ./.env | xargs) +fi + +if [ -z "$DATABASE_URL" ]; then + ./server/scripts/wait-for-it.sh $PG_HOST:${PG_PORT:-5432} --strict --timeout=300 -- $SETUP_CMD +else + PG_HOST=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $6}') + PG_PORT=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $7}') + + if [ -z "$DATABASE_PORT" ]; then + DATABASE_PORT="5432" + fi + + ./server/scripts/wait-for-it.sh "$PG_HOST:$PG_PORT" --strict --timeout=300 -- $SETUP_CMD +fi + +exec "$@" diff --git a/docker/cloud/cloud-production.Dockerfile b/docker/cloud/cloud-production.Dockerfile new file mode 100644 index 0000000000..a2e36bedb3 --- /dev/null +++ b/docker/cloud/cloud-production.Dockerfile @@ -0,0 +1,121 @@ +FROM node:18.18.2-buster AS builder + +# Fix for JS heap limit allocation issue +ENV NODE_OPTIONS="--max-old-space-size=4096" + +RUN npm i -g npm@9.8.1 +RUN mkdir -p /app +# RUN npm cache clean --force + +WORKDIR /app + +# Scripts for building +COPY ./package.json ./package.json + +# Build plugins +COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/ +RUN npm --prefix plugins install +COPY ./plugins/ ./plugins/ +RUN NODE_ENV=production npm --prefix plugins run build +RUN npm --prefix plugins prune --production + +# Build frontend +COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/ +RUN npm --prefix frontend install +COPY ./frontend/ ./frontend/ +RUN npm --prefix frontend run build --production +RUN npm --prefix frontend prune --production + +ENV NODE_ENV=production + +# Build server +COPY ./server/package.json ./server/package-lock.json ./server/ +RUN npm --prefix server install +COPY ./server/ ./server/ +RUN npm install -g @nestjs/cli +RUN npm --prefix server run build + +FROM debian:11 + +RUN apt-get update -yq \ + && apt-get install curl gnupg zip -yq \ + && apt-get install -yq build-essential \ + && apt-get clean -y + + +RUN curl -O https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz \ + && tar -xf node-v18.18.2-linux-x64.tar.xz \ + && mv node-v18.18.2-linux-x64 /usr/local/lib/nodejs \ + && echo 'export PATH="/usr/local/lib/nodejs/bin:$PATH"' >> /etc/profile.d/nodejs.sh \ + && /bin/bash -c "source /etc/profile.d/nodejs.sh" \ + && rm node-v18.18.2-linux-x64.tar.xz +ENV PATH=/usr/local/lib/nodejs/bin:$PATH + +ENV NODE_ENV=production +ENV NODE_OPTIONS="--max-old-space-size=4096" +RUN apt-get update && \ + apt-get install -y postgresql-client freetds-dev libaio1 wget && \ + apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes && \ + apt-get -y autoremove && \ + apt-get -y autoclean + +# Install Instantclient Basic Light Oracle and Dependencies +WORKDIR /opt/oracle + +RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linuxx64.zip && \ + wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \ + unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \ + unzip instantclient-basiclite-linux.x64-11.2.0.4.0.zip && rm -f instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \ + cd /opt/oracle/instantclient_21_10 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \ + cd /opt/oracle/instantclient_11_2 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \ + echo /opt/oracle/instantclient* > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig +# Set the Instant Client library paths +ENV LD_LIBRARY_PATH="/opt/oracle/instantclient_11_2:/opt/oracle/instantclient_21_10:${LD_LIBRARY_PATH}" + + +WORKDIR / + +RUN mkdir -p /app + +# copy npm scripts +COPY --from=builder /app/package.json ./app/package.json + +# copy plugins dependencies +COPY --from=builder /app/plugins/dist ./app/plugins/dist +COPY --from=builder /app/plugins/client.js ./app/plugins/client.js +COPY --from=builder /app/plugins/node_modules ./app/plugins/node_modules +COPY --from=builder /app/plugins/packages/common ./app/plugins/packages/common +COPY --from=builder /app/plugins/package.json ./app/plugins/package.json + +# 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/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/cloud/cloud-entrypoint.sh ./app/server/cloud-entrypoint.sh + +# Define non-sudo user +RUN useradd --create-home --home-dir /home/appuser appuser \ + && chown -R appuser:0 /app \ + && chown -R appuser:0 /home/appuser \ + && chmod u+x /app \ + && chmod -R g=u /app + +# Set npm cache directory +ENV npm_config_cache /home/appuser/.npm + +ENV HOME=/home/appuser + +# Installing git for simple git commands +RUN apt-get update && apt-get install -y git && apt-get clean + +USER appuser + +WORKDIR /app +# Dependencies for scripts outside nestjs +RUN npm install dotenv@10.0.0 joi@17.4.1 + +ENTRYPOINT ["./server/cloud-entrypoint.sh"] diff --git a/docker/cloud/cloud-server.Dockerfile b/docker/cloud/cloud-server.Dockerfile index cc9fd4fce3..96a9fe0f01 100644 --- a/docker/cloud/cloud-server.Dockerfile +++ b/docker/cloud/cloud-server.Dockerfile @@ -91,12 +91,13 @@ COPY --from=builder /app/plugins/package.json ./app/plugins/package.json # 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/cloud/cloud-entrypoint.sh ./app/server/cloud-entrypoint.sh + # Define non-sudo user RUN useradd --create-home --home-dir /home/appuser appuser \ && chown -R appuser:0 /app \ @@ -108,10 +109,14 @@ RUN useradd --create-home --home-dir /home/appuser appuser \ ENV npm_config_cache /home/appuser/.npm ENV HOME=/home/appuser + +# Installing git for simple git commands +RUN apt-get update && apt-get install -y git && apt-get clean + USER appuser WORKDIR /app # Dependencies for scripts outside nestjs RUN npm install dotenv@10.0.0 joi@17.4.1 -ENTRYPOINT ["./server/entrypoint.sh"] +ENTRYPOINT ["./server/cloud-entrypoint.sh"] diff --git a/docker/ee/ee-entrypoint.sh b/docker/ee/ee-entrypoint.sh index ac4b0bafd2..f9319e16be 100755 --- a/docker/ee/ee-entrypoint.sh +++ b/docker/ee/ee-entrypoint.sh @@ -42,6 +42,115 @@ else echo "Using external PostgREST at $PGRST_HOST." fi +# Neo4j configuration +# ---------------------------------- +# Default Neo4j environment values +# ---------------------------------- +export NEO4J_USER=${NEO4J_USER:-"neo4j"} +export NEO4J_PASSWORD=${NEO4J_PASSWORD:-"appaqvyvRLbeukhFE"} +export NEO4J_AUTH=${NEO4J_AUTH:-"neo4j/appaqvyvRLbeukhFE"} +export NEO4J_URI=${NEO4J_URI:-"bolt://localhost:7687"} +export NEO4J_PLUGINS=${NEO4J_PLUGINS:-'["apoc"]'} +export NEO4J_AUTH + +# Extract username and password from NEO4J_AUTH if set +if [ -n "$NEO4J_AUTH" ]; then + # Extract username and password from NEO4J_AUTH (format: username/password) + NEO4J_USERNAME=$(echo "$NEO4J_AUTH" | cut -d'/' -f1) + NEO4J_PASSWORD=$(echo "$NEO4J_AUTH" | cut -d'/' -f2) + + # Export these for application use + export NEO4J_USERNAME + export NEO4J_PASSWORD + + echo "Neo4j authentication configured with username: $NEO4J_USERNAME" +else + echo "NEO4J_AUTH not set, using default authentication" +fi + +# Check if Neo4j is already initialized and set password if necessary +if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then + echo "Setting Neo4j initial password..." + + # Ensure Neo4j is not running before setting the initial password + neo4j stop || true + + # Set the initial password using the correct command format for Neo4j 5.x + NEO4J_ADMIN_CMD=$(which neo4j-admin) + NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1) + echo "Detected Neo4j version: $NEO4J_VERSION" + + # Use version-specific command format + MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1) + if [ "$MAJOR_VERSION" -ge "5" ]; then + # For Neo4j 5.x and higher + echo "Using Neo4j 5.x+ password command format" + $NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || { + echo "Warning: Could not set Neo4j password, it may already be set" + } + else + # For Neo4j 4.x and lower + echo "Using Neo4j 4.x password command format" >/dev/null 2>&1 + $NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || { + echo "Warning: Could not set Neo4j password, it may already be set" + } + fi +fi + +# Update Neo4j configuration +echo "Configuring Neo4j..." +cat > /etc/neo4j/neo4j.conf << EOF +# Neo4j configuration +dbms.security.auth_enabled=true +server.bolt.enabled=true +server.bolt.listen_address=0.0.0.0:7687 +server.directories.data=/var/lib/neo4j/data +server.directories.logs=/var/log/neo4j +initial.dbms.default_database=neo4j +server.directories.plugins=/var/lib/neo4j/plugins +server.directories.import=/var/lib/neo4j/import + +# APOC Settings +dbms.security.procedures.unrestricted=apoc.* +dbms.security.procedures.allowlist=apoc.*,algo.*,gds.* +EOF + +if [ -w "$NEO4J_LOG_DIR" ]; then + chmod -R 770 "$NEO4J_LOG_DIR" || echo "Warning: Could not set log directory permissions" >/dev/null 2>&1 +fi + +# Start Neo4j +echo "Starting Neo4j service..." +neo4j console >/dev/null 2>&1 & + +# Add a wait for Neo4j to be ready with more robust checking +echo "Waiting for Neo4j to be ready..." +NEO4J_READY=false +for i in {1..60}; do + # First try standard status check + if neo4j status >/dev/null 2>&1; then + echo "Neo4j is ready (via status check)" + NEO4J_READY=true + break + fi + + # Also try connecting to the bolt port as a fallback + if command -v nc >/dev/null 2>&1; then + if nc -z localhost 7687 >/dev/null 2>&1; then + echo "Neo4j is ready (port 7687 is open)" + NEO4J_READY=true + break + fi + fi + + echo "Waiting for Neo4j to start... ($i/60)" + sleep 2 +done + +if [ "$NEO4J_READY" = false ]; then + echo "WARNING: Neo4j may not be fully started yet, but continuing..." +fi + # Check WORKLOW_WORKER and skip SETUP_CMD if true if [ "${WORKFLOW_WORKER}" == "true" ]; then echo "WORKFLOW_WORKER is set to true. Running worker process." diff --git a/docker/ee/ee-production.Dockerfile b/docker/ee/ee-production.Dockerfile index e611643f30..e7405f0994 100644 --- a/docker/ee/ee-production.Dockerfile +++ b/docker/ee/ee-production.Dockerfile @@ -60,7 +60,7 @@ RUN npm --prefix server run build FROM debian:11 RUN apt-get update -yq \ - && apt-get install curl gnupg zip -yq \ + && apt-get install curl wget gnupg zip -yq \ && apt-get install -yq build-essential \ && apt -y install redis \ && apt-get clean -y @@ -80,13 +80,29 @@ RUN echo "[supervisord]\n" \ "nodaemon=true\n" \ "\n" \ "[program:postgrest]\n" \ - "command=/bin/postgrest \n" \ + "command=/bin/postgrest\n" \ "autostart=true\n" \ "autorestart=true\n" \ "stdout_logfile=/dev/stdout\n" \ "stderr_logfile=/dev/stderr\n" \ "stdout_logfile_maxbytes=0\n" \ "stderr_logfile_maxbytes=0\n" \ + "\n" \ + "[program:neo4j]\n" \ + "command=neo4j console\n" \ + "autostart=true\n" \ + "autorestart=unexpected\n" \ + "startsecs=30\n" \ + "startretries=999\n" \ + "priority=90\n" \ + "exitcodes=0,1,2\n" \ + "stopsignal=SIGTERM\n" \ + "stopasgroup=true\n" \ + "killasgroup=true\n" \ + "redirect_stderr=true\n" \ + "stdout_logfile=/var/log/neo4j/neo4j.log\n" \ + "stdout_logfile_backups=10\n" \ + "stderr_capture_maxbytes=20MB\n" \ "\n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf # Create a wrapper for PostgREST to prefix its logs @@ -114,6 +130,48 @@ RUN apt-get update && \ apt-get -y autoremove && \ apt-get -y autoclean +# Install Neo4j +RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \ + echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \ + apt-get update && \ + apt-get install -y neo4j=1:5.26.6 && \ + apt-mark hold neo4j && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Set the necessary Neo4j environment variables +ENV NEO4J_HOME=/opt/neo4j +ENV NEO4J_CONF=/etc/neo4j +ENV NEO4J_DATA=/var/lib/neo4j/data +ENV NEO4J_LOG=/var/log/neo4j +ENV NEO4J_PLUGIN=/var/lib/neo4j/plugins +ENV NEO4J_IMPORT=/var/lib/neo4j/import + +# Create the necessary directories for Neo4j +RUN mkdir -p /data/db /data/logs /data/plugins +RUN mkdir -p /opt/neo4j/plugins + +# Configure APOC plugin for Neo4j +ENV NEO4J_dbms_active_plugins=apoc + +# Download and install APOC plugin for Neo4j 5.x (BEFORE creating user) +RUN mkdir -p /var/lib/neo4j/plugins && \ + wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \ + # Try to download extended version + (wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-extended.jar || \ + wget -P /var/lib/neo4j/plugins https://neo4j-contrib.github.io/neo4j-apoc-procedures/5.26.6/apoc-5.26.6-extended.jar || \ + echo "Extended JAR not available, continuing with core only") + +# Configure Neo4j with APOC +RUN echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \ + echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf + +# Configure Neo4j to use authentication +RUN if [ -f "/etc/neo4j/neo4j.conf" ]; then \ + sed -i '/dbms.security.auth_enabled/d' /etc/neo4j/neo4j.conf && \ + echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf; \ +fi + # Install Instantclient Basic Light Oracle and Dependencies WORKDIR /opt/oracle @@ -149,6 +207,7 @@ 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 --from=builder /app/server/src/assets ./app/server/src/assets COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh @@ -161,14 +220,21 @@ RUN useradd --create-home --home-dir /home/appuser appuser \ && chmod -R g=u /app \ && chmod -R g=u /home -# Create directory /home/appuser and set ownership to appuser (Refer doc for understanding the changes https://app.clickup.com/37484951/v/dc/13qycq-4081) +RUN mkdir -p /var/lib/neo4j/data/databases /var/lib/neo4j/data/transactions /var/log/neo4j /opt/neo4j/run && \ + chown -R appuser:0 /var/lib/neo4j /var/log/neo4j /etc/neo4j /opt/neo4j/run && \ + chmod -R 770 /var/lib/neo4j /var/log/neo4j /etc/neo4j /opt/neo4j/run && \ + chmod -R 644 /var/lib/neo4j/plugins/*.jar && \ + chown -R appuser:0 /var/lib/neo4j/plugins && \ + chmod 755 /var/lib/neo4j/plugins + +# Create directory /home/appuser and set ownership to appuser RUN mkdir -p /home/appuser \ && chown -R appuser:0 /home/appuser \ && chmod g+s /home/appuser \ && chmod -R g=u /home/appuser \ && npm cache clean --force -# Create directory /tmp/.npm/npm-cache/ and set ownership to appuser (Refer doc for understanding the changes https://app.clickup.com/37484951/v/dc/13qycq-4081) +# Create directory /tmp/.npm/npm-cache/ and set ownership to appuser RUN mkdir -p /tmp/.npm/npm-cache/ \ && chown -R appuser:0 /tmp/.npm/npm-cache/ \ && chmod g+s /tmp/.npm/npm-cache/ \ @@ -206,6 +272,9 @@ RUN mkdir -p /var/lib/postgrest /var/log/postgrest /etc/postgrest \ ENV HOME=/home/appuser +# Installing git for simple git commands +RUN apt-get update && apt-get install -y git && apt-get clean + # Switch back to appuser USER appuser diff --git a/frontend/assets/images/icons/editor/file-code.svg b/frontend/assets/images/icons/editor/file-code.svg new file mode 100644 index 0000000000..4dc470055c --- /dev/null +++ b/frontend/assets/images/icons/editor/file-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/images/icons/module-editor.svg b/frontend/assets/images/icons/module-editor.svg new file mode 100644 index 0000000000..e0b55223c3 --- /dev/null +++ b/frontend/assets/images/icons/module-editor.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/assets/images/modules/blank-module-list-icon-1.svg b/frontend/assets/images/modules/blank-module-list-icon-1.svg new file mode 100644 index 0000000000..1f9c188e27 --- /dev/null +++ b/frontend/assets/images/modules/blank-module-list-icon-1.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/assets/images/modules/blank-module-list-icon-2.svg b/frontend/assets/images/modules/blank-module-list-icon-2.svg new file mode 100644 index 0000000000..4ca21df25a --- /dev/null +++ b/frontend/assets/images/modules/blank-module-list-icon-2.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/assets/images/modules/blank-module.svg b/frontend/assets/images/modules/blank-module.svg new file mode 100644 index 0000000000..35207b8ca1 --- /dev/null +++ b/frontend/assets/images/modules/blank-module.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/ee b/frontend/ee index aa3c4f603f..064c4c149a 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit aa3c4f603f549337fc88a772a6a31e18eaf38701 +Subproject commit 064c4c149ab095ea9e6b6f992b45ae5a3f190673 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2db11e6493..4f841128fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -89,6 +89,7 @@ "query-string": "^8.1.0", "rc-slider": "^10.1.1", "react": "^18.2.0", + "react-accessible-treeview": "^2.11.1", "react-beautiful-dnd": "^13.1.1", "react-big-calendar": "^1.6.5", "react-bootstrap": "^2.7.2", @@ -105,6 +106,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-highlight-words": "^0.21.0", "react-hot-toast": "^2.4.0", "react-hotkeys-hook": "^4.3.5", "react-i18next": "^12.1.5", @@ -34032,6 +34034,11 @@ "hermes-estree": "0.23.1" } }, + "node_modules/highlight-words-core": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/highlight-words-core/-/highlight-words-core-1.2.3.tgz", + "integrity": "sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "license": "BSD-3-Clause", @@ -41074,6 +41081,17 @@ "node": ">=0.10.0" } }, + "node_modules/react-accessible-treeview": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/react-accessible-treeview/-/react-accessible-treeview-2.11.1.tgz", + "integrity": "sha512-lFegHjFJp2OvtoHMtbIqjby7N3MGDRASlbJsMLqElxQHwZ97xIYho2S4QvXKK7l3/nII0IKDQFJXZNBj6ecG3g==", + "peerDependencies": { + "classnames": "^2.2.6", + "prop-types": "^15.7.2", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-base16-styling": { "version": "0.9.1", "license": "MIT", @@ -41616,6 +41634,23 @@ } } }, + "node_modules/react-highlight-words": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/react-highlight-words/-/react-highlight-words-0.21.0.tgz", + "integrity": "sha512-SdWEeU9fIINArEPO1rO5OxPyuhdEKZQhHzZZP1ie6UeXQf+CjycT1kWaB+9bwGcVbR0NowuHK3RqgqNg6bgBDQ==", + "dependencies": { + "highlight-words-core": "^1.2.0", + "memoize-one": "^4.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/react-highlight-words/node_modules/memoize-one": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.3.tgz", + "integrity": "sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==" + }, "node_modules/react-hot-toast": { "version": "2.4.1", "license": "MIT", diff --git a/frontend/package.json b/frontend/package.json index 3821a370f5..22f34ae313 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -85,6 +85,7 @@ "query-string": "^8.1.0", "rc-slider": "^10.1.1", "react": "^18.2.0", + "react-accessible-treeview": "^2.11.1", "react-beautiful-dnd": "^13.1.1", "react-big-calendar": "^1.6.5", "react-bootstrap": "^2.7.2", @@ -101,6 +102,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-highlight-words": "^0.21.0", "react-hot-toast": "^2.4.0", "react-hotkeys-hook": "^4.3.5", "react-i18next": "^12.1.5", @@ -264,4 +266,4 @@ "jsx" ] } -} \ No newline at end of file +} diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index 3d58259c5f..e2967e892d 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -2,7 +2,7 @@ import React, { Suspense } from 'react'; // eslint-disable-next-line no-unused-vars import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { authorizeWorkspace, updateCurrentSession } from '@/_helpers/authorizeWorkspace'; -import { authenticationService, tooljetService } from '@/_services'; +import { authenticationService, tooljetService, licenseService } from '@/_services'; import { withRouter } from '@/_hoc/withRouter'; import { PrivateRoute, AdminRoute, AppsRoute, SwitchWorkspaceRoute } from '@/Routes'; import { HomePage } from '@/HomePage'; @@ -42,7 +42,6 @@ import { shallow } from 'zustand/shallow'; import useStore from '@/AppBuilder/_stores/store'; import { checkIfToolJetCloud } from '@/_helpers/utils'; import { BasicPlanMigrationBanner } from '@/HomePage/BasicPlanMigrationBanner/BasicPlanMigrationBanner'; -import { licenseService } from '@/_services'; const AppWrapper = (props) => { const { isAppDarkMode } = useAppDarkMode(); @@ -295,6 +294,15 @@ class AppComponent extends React.Component { > }> }> + + + + } + /> {getAuditLogsRoutes(this.props)} import('@/AppBuilder/Header')); // const LeftSidebar = lazy(() => import('@/AppBuilder/LeftSidebar')); @@ -22,12 +23,13 @@ import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext'; // const QueryPanel = lazy(() => import('@/AppBuilder/QueryPanel')); // TODO: split Loader into separate component and remove editor loading state from Editor -export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMode }) => { +export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMode, appType = 'front-end' }) => { useAppData(appId, moduleId, darkMode); - const isEditorLoading = useStore((state) => state.isEditorLoading); - const currentMode = useStore((state) => state.currentMode); + const isEditorLoading = useStore((state) => state.loaderStore.modules[moduleId].isEditorLoading, shallow); + const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); + const isModuleEditor = appType === 'module'; - const updateIsTJDarkMode = useStore((state) => state.updateIsTJDarkMode); + const updateIsTJDarkMode = useStore((state) => state.updateIsTJDarkMode, shallow); const changeToDarkMode = (newMode) => { updateIsTJDarkMode(newMode); @@ -45,19 +47,19 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod return (
- Loading...
}> - - - - {window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && } - - - + + Loading...}> + + + + {window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && } + + - - - + + + ); diff --git a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx index 365c3bbc59..10ab1fa054 100644 --- a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx +++ b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Container } from './Container'; import Grid from './Grid'; import { EditorSelecto } from './Selecto'; -import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import { HotkeyProvider } from './HotkeyProvider'; import './appCanvas.scss'; import useStore from '@/AppBuilder/_stores/store'; @@ -18,15 +18,18 @@ import useAppCanvasMaxWidth from './useAppCanvasMaxWidth'; import { DeleteWidgetConfirmation } from './DeleteWidgetConfirmation'; import useSidebarMargin from './useSidebarMargin'; -export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => { +export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) => { + const { moduleId, isModuleMode, appType } = useModuleContext(); const canvasContainerRef = useRef(); const handleCanvasContainerMouseUp = useStore((state) => state.handleCanvasContainerMouseUp, shallow); - const canvasHeight = useStore((state) => state.canvasHeight); - const creationMode = useStore((state) => state.app.creationMode); - const environmentLoadingState = useStore((state) => state.environmentLoadingState || state.isEditorLoading); - const [canvasWidth, setCanvasWidth] = useState(getCanvasWidth()); + const canvasHeight = useStore((state) => state.appStore.modules[moduleId].canvasHeight); + const creationMode = useStore((state) => state.appStore.modules[moduleId].app.creationMode); + const environmentLoadingState = useStore( + (state) => state.environmentLoadingState || state.loaderStore.modules[moduleId].isEditorLoading + ); + const [canvasWidth, setCanvasWidth] = useState(getCanvasWidth(moduleId)); const gridWidth = canvasWidth / NO_OF_GRIDS; - const currentMode = useStore((state) => state.currentMode, shallow); + const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); const pageSidebarStyle = useStore((state) => state?.pageSettings?.definition?.properties?.style, shallow); const currentLayout = useStore((state) => state.currentLayout, shallow); const queryPanelHeight = useStore((state) => state?.queryPanel?.queryPanelHeight || 0); @@ -42,23 +45,79 @@ export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => { const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow); const getPageId = useStore((state) => state.getCurrentPageId, shallow); + const hideSidebar = isModuleMode || isPagesSidebarHidden || appType === 'module'; + useEffect(() => { // Need to remove this if we shift setExposedVariable Logic outside of components // Currently present to run onLoadQueries after the component is mounted - setIsComponentLayoutReady(true); - return () => setIsComponentLayoutReady(false); + setIsComponentLayoutReady(true, moduleId); + return () => setIsComponentLayoutReady(false, moduleId); }, []); useEffect(() => { function handleResize() { - const _canvasWidth = document.getElementById('real-canvas')?.getBoundingClientRect()?.width; + const _canvasWidth = + moduleId === 'canvas' + ? document.getElementById('real-canvas')?.getBoundingClientRect()?.width + : document.getElementById(moduleId)?.getBoundingClientRect()?.width; if (_canvasWidth !== 0) setCanvasWidth(_canvasWidth); } - window.addEventListener('resize', handleResize); + + if (moduleId === 'canvas') { + window.addEventListener('resize', handleResize); + } else { + const elem = document.getElementById(moduleId); + const resizeObserver = new ResizeObserver(handleResize); + if (elem) resizeObserver.observe(elem); + + return () => { + if (elem) resizeObserver.unobserve(elem); + resizeObserver.disconnect(); + }; + } handleResize(); return () => window.removeEventListener('resize', handleResize); - }, [currentLayout, canvasMaxWidth, isViewerSidebarPinned]); + }, [currentLayout, canvasMaxWidth, isViewerSidebarPinned, moduleId]); + + const styles = useMemo(() => { + const canvasBgColor = + currentMode === 'view' + ? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor) + : !isAppDarkMode + ? '#EBEBEF' + : '#2F3C4C'; + + if (isModuleMode) { + return { + borderLeft: 'none', + height: '100%', + background: canvasBgColor, + }; + } + + return { + borderLeft: currentMode === 'edit' && editorMarginLeft + 'px solid', + height: currentMode === 'edit' ? canvasContainerHeight : '100%', + background: canvasBgColor, + marginLeft: + isViewerSidebarPinned && !hideSidebar && currentLayout !== 'mobile' && currentMode !== 'edit' + ? pageSidebarStyle === 'icon' + ? '65px' + : '210px' + : 'auto', + }; + }, [ + currentMode, + isAppDarkMode, + isModuleMode, + editorMarginLeft, + canvasContainerHeight, + isViewerSidebarPinned, + hideSidebar, + currentLayout, + pageSidebarStyle, + ]); return (
{ className={cx( 'canvas-container align-items-center page-container', { 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned }, - { 'overflow-x-auto': (currentMode === 'edit' && isSidebarOpen) || currentMode === 'view' } + { 'overflow-x-auto': (currentMode === 'edit' && isSidebarOpen) || currentMode === 'view' }, + { 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode )} - style={{ - // transform: `scale(1)`, - borderLeft: currentMode === 'edit' && editorMarginLeft + 'px solid', - height: currentMode === 'edit' ? canvasContainerHeight : '100%', - background: - currentMode === 'view' - ? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor) - : !isAppDarkMode - ? '#EBEBEF' - : '#2F3C4C', - marginLeft: - isViewerSidebarPinned && !isPagesSidebarHidden && currentLayout !== 'mobile' && currentMode !== 'edit' - ? pageSidebarStyle === 'icon' - ? '65px' - : '210px' - : 'auto', - }} + style={styles} >
@@ -107,7 +151,7 @@ export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => { {environmentLoadingState !== 'loading' && (
{ canvasMaxWidth={canvasMaxWidth} isViewerSidebarPinned={isViewerSidebarPinned} pageSidebarStyle={pageSidebarStyle} + appType={appType} /> -
+ {appType !== 'module' &&
}
)} diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index 45835c39de..22a410c674 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -4,6 +4,8 @@ import './configHandle.scss'; import useStore from '@/AppBuilder/_stores/store'; import { findHighestLevelofSelection } from '../Grid/gridUtils'; import SolidIcon from '@/_ui/Icon/solidIcons/index'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; +import { DROPPABLE_PARENTS } from '../appCanvasConstants'; const CONFIG_HANDLE_HEIGHT = 20; const BUFFER_HEIGHT = 1; @@ -18,10 +20,12 @@ export const ConfigHandle = ({ showHandle, componentType, visibility, + isModuleContainer, subContainerIndex, }) => { + const { moduleId } = useModuleContext(); const shouldFreeze = useStore((state) => state.getShouldFreeze()); - const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow); + const componentName = useStore((state) => state.getComponentDefinition(id, moduleId)?.component?.name || '', shallow); const isMultipleComponentsSelected = useStore( (state) => (findHighestLevelofSelection(state?.selectedComponents)?.length > 1 ? true : false), shallow @@ -43,6 +47,7 @@ export const ConfigHandle = ({ return ( (subContainerIndex === 0 || subContainerIndex === null) && (isWidgetHovered || + isModuleContainer || (showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) && !anyComponentHovered)) ); }, shallow); @@ -67,7 +72,9 @@ export const ConfigHandle = ({ if (componentType === 'Tabs') { setFocusedParentId(`${id}-${currentTab}`); } else { - setFocusedParentId(id); + if (DROPPABLE_PARENTS.has(componentType)) { + setFocusedParentId(id); + } } }} > @@ -125,20 +132,22 @@ export const ConfigHandle = ({ data-cy={`${componentName.toLowerCase()}-inspect-button`} className="config-handle-inspect" /> - { - deleteComponents([id]); - }} - data-cy={`${componentName.toLowerCase()}-delete-button`} - > - - + {!isModuleContainer && ( + { + deleteComponents([id]); + }} + data-cy={`${componentName.toLowerCase()}-delete-button`} + > + + + )}
)} diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index 56129afcd9..2997b84943 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -25,7 +25,10 @@ import NoComponentCanvasContainer from './NoComponentCanvasContainer'; import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants'; import { isPDFSupported } from '@/_helpers/appUtils'; import toast from 'react-hot-toast'; +import { ModuleContainerBlank } from '@/modules/Modules/components'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import useSortedComponents from '../_hooks/useSortedComponents'; +import { noop } from 'lodash'; //TODO: Revisit the logic of height (dropRef) @@ -50,9 +53,12 @@ export const Container = React.memo( isViewerSidebarPinned, pageSidebarStyle, componentType, + appType, }) => { + const { moduleId } = useModuleContext(); const realCanvasRef = useRef(null); - const components = useStore((state) => state.getContainerChildrenMapping(id), shallow); + const components = useStore((state) => state.getContainerChildrenMapping(id, moduleId), shallow); + const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow); const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow); const setLastCanvasClickPosition = useStore((state) => state.setLastCanvasClickPosition, shallow); @@ -61,12 +67,14 @@ export const Container = React.memo( shallow ); const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow); - const currentMode = useStore((state) => state.currentMode, shallow); + const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); const currentLayout = useStore((state) => state.currentLayout, shallow); const setFocusedParentId = useStore((state) => state.setFocusedParentId, shallow); + const setShowModuleBorder = useStore((state) => state.setShowModuleBorder, shallow) || noop; + const isContainerReadOnly = useMemo(() => { return (index !== 0 && (componentType === 'Listview' || componentType === 'Kanban')) || currentMode === 'view'; - }, [componentType, index, currentMode]); + }, [index, componentType, currentMode]); const [{ isOverCurrent }, drop] = useDrop({ accept: 'box', @@ -75,7 +83,9 @@ export const Container = React.memo( item.canvasId = id; item.canvasWidth = getContainerCanvasWidth(); }, - drop: async ({ componentType }, monitor) => { + drop: async ({ componentType, component }, monitor) => { + setShowModuleBorder(false); // Hide the module border when dropping + if (currentMode === 'view' || (appType === 'module' && componentType !== 'ModuleContainer')) return; const didDrop = monitor.didDrop(); if (didDrop) return; if (componentType === 'PDF' && !isPDFSupported()) { @@ -84,15 +94,41 @@ export const Container = React.memo( ); return; } + + // IMPORTANT: This logic needs to be changed when we implement the module versioning + const moduleInfo = component?.moduleId + ? { + moduleId: component.moduleId, + versionId: component.versionId, + environmentId: component.environmentId, + moduleName: component.displayName, + moduleContainer: component.moduleContainer, + } + : undefined; + if (WIDGETS_WITH_DEFAULT_CHILDREN.includes(componentType)) { - const parentComponent = addNewWidgetToTheEditor(componentType, monitor, currentLayout, realCanvasRef, id); + const parentComponent = addNewWidgetToTheEditor( + componentType, + monitor, + currentLayout, + realCanvasRef, + id, + moduleInfo + ); const childComponents = addChildrenWidgetsToParent(componentType, parentComponent?.id, currentLayout); const newComponents = [parentComponent, ...childComponents]; await addComponentToCurrentPage(newComponents); // setSelectedComponents([parentComponent?.id]); setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION); } else { - const newComponent = addNewWidgetToTheEditor(componentType, monitor, currentLayout, realCanvasRef, id); + const newComponent = addNewWidgetToTheEditor( + componentType, + monitor, + currentLayout, + realCanvasRef, + id, + moduleInfo + ); await addComponentToCurrentPage([newComponent]); // setSelectedComponents([newComponent?.id]); setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION); @@ -103,7 +139,11 @@ export const Container = React.memo( }), }); - const showEmptyContainer = currentMode === 'edit' && id === 'canvas' && components.length === 0 && !isOverCurrent; + const showEmptyContainer = + currentMode === 'edit' && + (id === 'canvas' || componentType === 'ModuleContainer') && + components.length === 0 && + !isOverCurrent; function getContainerCanvasWidth() { if (canvasWidth !== undefined) { @@ -116,7 +156,7 @@ export const Container = React.memo( const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS; useEffect(() => { - useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS); + useGridStore.getState().actions.setSubContainerWidths(id, gridWidth); // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasWidth, listViewMode, columns]); @@ -126,7 +166,8 @@ export const Container = React.memo( !isPagesSidebarHidden && isViewerSidebarPinned && currentLayout !== 'mobile' && - currentMode !== 'edit' + currentMode !== 'edit' && + appType !== 'module' ) { return `calc(100% - ${pageSidebarStyle === 'icon' ? '65px' : '210px'})`; } @@ -148,6 +189,23 @@ export const Container = React.memo( [setLastCanvasClickPosition] ); + /* Due to some reason react-dnd does not identify the dragover element if this element is dynamically removed on drag. + Hence display is set to none on dragover and removed only when the component is added */ + + const renderEmptyContainer = () => { + if (components && components?.length !== 0) return; + + const styles = { + display: showEmptyContainer ? 'block' : 'none', + ...(componentType === 'ModuleContainer' ? { height: '100%', width: '100%' } : {}), + }; + + return ( +
+ {componentType === 'ModuleContainer' ? : } +
+ ); + }; const sortedComponents = useSortedComponents(components, currentLayout, id); return ( @@ -183,7 +241,7 @@ export const Container = React.memo( }} className={cx('real-canvas', { 'sub-canvas': id !== 'canvas', - 'show-grid': isOverCurrent && (index === 0 || index === null), + 'show-grid': isOverCurrent && (index === 0 || index === null) && currentMode === 'edit', })} id={id === 'canvas' ? 'real-canvas' : `canvas-${id}`} data-cy="real-canvas" @@ -217,14 +275,7 @@ export const Container = React.memo( /> ))}
- - {/* Due to some reason react-dnd does not identify the dragover element if this element is dynamically removed on drag. - Hence display is set to none on dragover and removed only when the component is added */} - {(!components || components?.length === 0) && ( -
- -
- )} + {renderEmptyContainer()}
); } diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index 5ece3710de..d92efe63fb 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -28,8 +28,8 @@ import { import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd'; import useStore from '@/AppBuilder/_stores/store'; import './Grid.css'; -import { NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants'; - +import { DROPPABLE_PARENTS, NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, position: 'css' }; const RESIZABLE_CONFIG = { edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'], @@ -38,17 +38,19 @@ const RESIZABLE_CONFIG = { export const GRID_HEIGHT = 10; export default function Grid({ gridWidth, currentLayout }) { + const { moduleId, isModuleEditor } = useModuleContext(); const lastDraggedEventsRef = useRef(null); const updateCanvasBottomHeight = useStore((state) => state.updateCanvasBottomHeight, shallow); const setComponentLayout = useStore((state) => state.setComponentLayout, shallow); - const mode = useStore((state) => state.currentMode, shallow); + const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); const [boxList, setBoxList] = useState([]); - const currentPageComponents = useStore((state) => state.getCurrentPageComponents(), shallow); + const currentPageComponents = useStore((state) => state.getCurrentPageComponents(moduleId), shallow); const selectedComponents = useStore((state) => state.selectedComponents, shallow); const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow); const getComponentTypeFromId = useStore((state) => state.getComponentTypeFromId, shallow); const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); const isGroupHandleHoverd = useIsGroupHandleHoverd(); + const openModalWidgetId = useOpenModalWidgetId(); const moveableRef = useRef(null); const triggerCanvasUpdater = useStore((state) => state.triggerCanvasUpdater, shallow); @@ -132,7 +134,7 @@ export default function Grid({ gridWidth, currentLayout }) { const noOfBoxs = Object.values(boxList || []).length; useEffect(() => { - updateCanvasBottomHeight(boxList); + updateCanvasBottomHeight(boxList, moduleId); noOfBoxs != 0; // eslint-disable-next-line react-hooks/exhaustive-deps }, [noOfBoxs, triggerCanvasUpdater]); @@ -333,8 +335,8 @@ export default function Grid({ gridWidth, currentLayout }) { }; const isComponentVisible = (id) => { - const component = getResolvedComponent(id); - const componentExposedVisibility = getExposedValueOfComponent(id)?.isVisible; + const component = getResolvedComponent(id, null, moduleId); + const componentExposedVisibility = getExposedValueOfComponent(id, moduleId)?.isVisible; if (componentExposedVisibility === false) return false; let visibility; if (isArray(component)) { @@ -422,6 +424,10 @@ export default function Grid({ gridWidth, currentLayout }) { const moveableBox = document.querySelector(`.moveable-control-box`); const showConfigHandle = (e) => { const targetId = e.target.offsetParent.getAttribute('target-id'); + const componentType = getComponentTypeFromId(targetId); + if (componentType === 'ModuleContainer') { + return; + } useStore.getState().setHoveredComponentBoundaryId(targetId); }; const hideConfigHandle = () => { @@ -461,9 +467,7 @@ export default function Grid({ gridWidth, currentLayout }) { widgetId = widgetId.split('-').slice(0, -1).join('-'); widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component; } - if ( - !['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes(widgetType) - ) { + if (!DROPPABLE_PARENTS.has(widgetType)) { isDroppable = false; } } @@ -477,10 +481,15 @@ export default function Grid({ gridWidth, currentLayout }) { .map(({ component }) => component.component); const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId; const parentWidgetType = getComponentTypeFromId(parentId); - const restrictedWidgetsTobeDropped = + let restrictedWidgetsTobeDropped = RESTRICTED_WIDGETS_CONFIG?.[parentWidgetType]?.filter((widgetType) => widgetsTypeToBeDropped.includes(widgetType) ) || []; + + if (isModuleEditor && parentId === undefined) { + restrictedWidgetsTobeDropped = widgetsTypeToBeDropped; + // useGridStore.getState().actions.setIsGroupHandleHoverd(false); + } const isParentChangeAllowed = isEmpty(restrictedWidgetsTobeDropped); if (!isParentChangeAllowed) { @@ -505,7 +514,12 @@ export default function Grid({ gridWidth, currentLayout }) { }); // Show error message - toast.error(`${restrictedWidgetsTobeDropped} is not compatible as a child component of ${parentWidgetType}`); + if (isModuleEditor) { + // Added this to hide configHandle when multiple components were dragged using the configHandle and placed outside the module + setSelectedComponents([]); + } else { + toast.error(`${restrictedWidgetsTobeDropped} is not compatible as a child component of ${parentWidgetType}`); + } } const parentElm = draggedOverElem || document.getElementById('real-canvas'); @@ -588,11 +602,11 @@ export default function Grid({ gridWidth, currentLayout }) { keepRatio={false} individualGroupableProps={individualGroupableProps} onResize={(e) => { - if(resizingComponentId !== e.target.id) { + 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) { @@ -874,7 +888,7 @@ export default function Grid({ gridWidth, currentLayout }) { if (!e.lastEvent) return; // Build the drag context from the event - const dragContext = dragContextBuilder({ event: e, widgets: boxList }); + const dragContext = dragContextBuilder({ event: e, widgets: boxList, isModuleEditor }); const { target, source, dragged } = dragContext; const targetSlotId = target?.slotId; @@ -967,6 +981,19 @@ export default function Grid({ gridWidth, currentLayout }) { left: modalRect.left - mainRect.left, }; setCanvasBounds({ ...relativePosition }); + } else if (isModuleEditor) { + const moduleContainer = e.target.closest('.module-container-canvas'); + const mainCanvas = document.getElementById('real-canvas'); + + const mainRect = mainCanvas.getBoundingClientRect(); + const modalRect = moduleContainer.getBoundingClientRect(); + const relativePosition = { + top: modalRect.top - mainRect.top, + right: mainRect.right - modalRect.right + moduleContainer.offsetWidth, + bottom: modalRect.height + (modalRect.top - mainRect.top), + left: modalRect.left - mainRect.left, + }; + setCanvasBounds({ ...relativePosition }); } // This block is to show grid lines on the canvas when the dragged element is over a new canvas diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js index da5a8341bf..0c78f8730a 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js @@ -54,6 +54,7 @@ import { RESTRICTED_WIDGETS_CONFIG, RESTRICTED_WIDGET_SLOTS_CONFIG, } from '@/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig'; +import { DROPPABLE_PARENTS } from '../../appCanvasConstants'; const CANVAS_ID = 'canvas'; const REAL_CANVAS_ID = 'real-canvas'; @@ -84,8 +85,6 @@ export class DragEntity { * This class helps determine if a slot is valid and handles various properties like modals. */ export class DropAreaEntity { - static dropAreaWidgets = ['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'ModalV2', 'Listview', 'Container', 'Table']; - constructor(widget, slotId) { this.widget = widget; // The widget that owns this slot this.id = widget?.id || CANVAS_ID; // ID of the widget @@ -119,7 +118,7 @@ export class DropAreaEntity { // Determines if the slot is a valid drop target get isDroppable() { - return DropAreaEntity.dropAreaWidgets.includes(this.widgetType); + return DROPPABLE_PARENTS.has(this.widgetType); } // Returns the type of slot (header, footer, body, etc.) @@ -143,7 +142,7 @@ export class DropAreaEntity { * - Any restrictions based on parent-child relationships */ export class DragContext { - constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets }) { + constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets, isModuleEditor = false }) { const sourceWidgetId = sourceSlotId?.slice(0, 36); const sourceWidget = getWidgetById(widgets, sourceWidgetId); @@ -156,6 +155,7 @@ export class DragContext { this.target = new DropAreaEntity(targetWidget, targetSlotId); this.dragged = new DragEntity(draggedWidget); this.widgets = widgets; + this.isModuleEditor = isModuleEditor; } /** @@ -168,7 +168,13 @@ export class DragContext { } get isDroppable() { - const { dragged, target } = this; + const { dragged, target, isModuleEditor } = this; + + // If the target is the canvas and we are in module editor, + // then we don't want to drop the widget outside the module + if (isModuleEditor && target.id === 'canvas') { + return false; + } const restrictedWidgetsOnTarget = RESTRICTED_WIDGETS_CONFIG?.[target.widgetType] || []; const restrictedWidgetsOnTargetSlot = RESTRICTED_WIDGET_SLOTS_CONFIG?.[target.slotType] || []; @@ -181,13 +187,19 @@ export class DragContext { /** * Constructs the **dragging context** by gathering all relevant details from the event. */ -export function dragContextBuilder({ event, widgets }) { +export function dragContextBuilder({ event, widgets, isModuleEditor = false }) { const draggedWidgetId = event.target.id; const draggedWidget = getWidgetById(widgets, draggedWidgetId); const sourceSlotId = draggedWidget.parent; // Initialize drag context - const context = new DragContext({ widgets, draggedWidgetId, sourceSlotId, targetSlotId: sourceSlotId }); + const context = new DragContext({ + widgets, + draggedWidgetId, + sourceSlotId, + targetSlotId: sourceSlotId, + isModuleEditor, + }); // Determine the potential drop target const targetSlotId = getDroppableSlotIdOnScreen(event, widgets); @@ -209,7 +221,7 @@ export const getDroppableSlotIdOnScreen = (event, widgets) => { .map((ele) => extractSlotId(ele)) .filter((slotId) => { const widgetType = getWidgetById(widgets, slotId.slice(0, 36))?.component?.component || CANVAS_ID; - return DropAreaEntity.dropAreaWidgets.includes(widgetType); + return DROPPABLE_PARENTS.has(widgetType); }); return slotId; diff --git a/frontend/src/AppBuilder/AppCanvas/HotkeyProvider.jsx b/frontend/src/AppBuilder/AppCanvas/HotkeyProvider.jsx index 1aa54dfc7b..018383c61f 100644 --- a/frontend/src/AppBuilder/AppCanvas/HotkeyProvider.jsx +++ b/frontend/src/AppBuilder/AppCanvas/HotkeyProvider.jsx @@ -4,8 +4,10 @@ import useStore from '@/AppBuilder/_stores/store'; import { pasteComponents, copyComponents } from './appCanvasUtils'; import useKeyHooks from '@/_hooks/useKeyHooks'; import { shallow } from 'zustand/shallow'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }) => { + const { isModuleEditor } = useModuleContext(); const canvasRef = useRef(null); const focusedParentId = useStore((state) => state.focusedParentId, shallow); const handleUndo = useStore((state) => state.handleUndo); @@ -18,10 +20,13 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth } const getSelectedComponents = useStore((state) => state.getSelectedComponents, shallow); const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow); const containerChildrenMapping = useStore((state) => state.containerChildrenMapping, shallow); + const getComponentTypeFromId = useStore((state) => state.getComponentTypeFromId, shallow); + useHotkeys('meta+z, control+z', handleUndo, { enabled: mode === 'edit' }); useHotkeys('meta+shift+z, control+shift+z', handleRedo, { enabled: mode === 'edit' }); const paste = async () => { + if (isModuleEditor && !focusedParentId) return; if (navigator.clipboard && typeof navigator.clipboard.readText === 'function') { try { const cliptext = await navigator.clipboard.readText(); @@ -61,6 +66,24 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth } enableReleasedVersionPopupState(); return; } + + // Disable cut, copy, paste, delete shortcuts in module editor + // or when a ModuleContainer is selected + if (isModuleEditor) { + const selectedComponents = getSelectedComponents(); + if ( + selectedComponents.length > 0 && + selectedComponents.some((id) => { + const componentType = getComponentTypeFromId(id, 'canvas'); + return componentType === 'ModuleContainer'; + }) + ) { + if (['KeyC', 'KeyX', 'KeyV', 'KeyD', 'Backspace'].includes(key)) { + return; + } + } + } + switch (key) { case 'Escape': handleEscapeKeyPress(); // clears the selected components diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index 6610ae5fb4..b893c80552 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -7,6 +7,7 @@ import { renderTooltip } from '@/_helpers/appUtils'; import { useTranslation } from 'react-i18next'; import ErrorBoundary from '@/_ui/ErrorBoundary'; import { BOX_PADDING } from './appCanvasConstants'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [ 'Table', @@ -47,23 +48,27 @@ const RenderWidget = ({ inCanvas = false, darkMode, }) => { - const componentDefinition = useStore((state) => state.getComponentDefinition(id), shallow); + const { moduleId } = useModuleContext(); + const componentDefinition = useStore((state) => state.getComponentDefinition(id, moduleId), shallow); const getDefaultStyles = useStore((state) => state.debugger.getDefaultStyles, shallow); const component = componentDefinition?.component; const componentName = component?.name; const [key, setKey] = useState(Math.random()); const resolvedProperties = useStore( - (state) => state.getResolvedComponent(id, subContainerIndex)?.properties, + (state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.properties, + shallow + ); + const resolvedStyles = useStore( + (state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.styles, shallow ); - const resolvedStyles = useStore((state) => state.getResolvedComponent(id, subContainerIndex)?.styles, shallow); const fireEvent = useStore((state) => state.eventsSlice.fireEvent, shallow); const resolvedGeneralProperties = useStore( - (state) => state.getResolvedComponent(id, subContainerIndex)?.general, + (state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.general, shallow ); const resolvedGeneralStyles = useStore( - (state) => state.getResolvedComponent(id, subContainerIndex)?.generalStyles, + (state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.generalStyles, shallow ); const unResolvedValidation = componentDefinition?.component?.definition?.validation || {}; @@ -73,10 +78,13 @@ const RenderWidget = ({ const setExposedValue = useStore((state) => state.setExposedValue, shallow); const setExposedValues = useStore((state) => state.setExposedValues, shallow); const setDefaultExposedValues = useStore((state) => state.setDefaultExposedValues, shallow); - const resolvedValidation = useStore((state) => state.getResolvedComponent(id)?.validation, shallow); + const resolvedValidation = useStore( + (state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.validation, + shallow + ); const parentId = component?.parent; const customResolvables = useStore( - (state) => state.resolvedStore.modules.canvas?.customResolvables?.[parentId], + (state) => state.resolvedStore.modules[moduleId]?.customResolvables?.[parentId], shallow ); const { t } = useTranslation(); @@ -110,31 +118,31 @@ const RenderWidget = ({ (key, value) => { // Check if the component is inside the subcontainer and it has its own onOptionChange(setExposedValue) function if (onOptionChange === null) { - setExposedValue(id, key, value); + setExposedValue(id, key, value, moduleId); // Trigger an update when the child components is directly linked to any component - updateDependencyValues(`components.${id}.${key}`); + updateDependencyValues(`components.${id}.${key}`, moduleId); } else { onOptionChange(key, value, id, subContainerIndex); } }, - [id, setExposedValue, updateDependencyValues, subContainerIndex, onOptionChange] + [id, setExposedValue, updateDependencyValues, subContainerIndex, onOptionChange, moduleId] ); const setExposedVariables = useCallback( (exposedValues) => { if (onOptionsChange === null) { - setExposedValues(id, 'components', exposedValues); + setExposedValues(id, 'components', exposedValues, moduleId); } else { onOptionsChange(exposedValues, id, subContainerIndex); } }, - [id, setExposedValues, onOptionsChange] + [id, setExposedValues, onOptionsChange, moduleId] ); const fireEventWrapper = useCallback( (eventName, options) => { - fireEvent(eventName, id, 'canvas', customResolvables?.[subContainerIndex] ?? {}, options); + fireEvent(eventName, id, moduleId, customResolvables?.[subContainerIndex] ?? {}, options); return Promise.resolve(); }, - [fireEvent, id, customResolvables, subContainerIndex] + [fireEvent, id, customResolvables, subContainerIndex, moduleId] ); const onComponentClick = useStore((state) => state.eventsSlice.onComponentClickEvent); @@ -155,17 +163,18 @@ const RenderWidget = ({ ? null : ['hover', 'focus'] : !resolvedGeneralProperties?.tooltip?.toString().trim() - ? null - : ['hover', 'focus'] + ? null + : ['hover', 'focus'] } overlay={(props) => renderTooltip({ props, text: inCanvas - ? `${SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY.includes(component?.component) - ? resolvedProperties?.tooltip - : resolvedGeneralProperties?.tooltip - }` + ? `${ + SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY.includes(component?.component) + ? resolvedProperties?.tooltip + : resolvedGeneralProperties?.tooltip + }` : `${t(`widget.${component?.name}.description`, component?.description)}`, }) } diff --git a/frontend/src/AppBuilder/AppCanvas/Selecto.jsx b/frontend/src/AppBuilder/AppCanvas/Selecto.jsx index c92cf75a1e..dfa27d3638 100644 --- a/frontend/src/AppBuilder/AppCanvas/Selecto.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Selecto.jsx @@ -5,8 +5,10 @@ import './selecto.scss'; import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants'; import { shallow } from 'zustand/shallow'; import { findHighestLevelofSelection } from './Grid/gridUtils'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const EditorSelecto = () => { + const { moduleId } = useModuleContext(); const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab); const setSelectedComponents = useStore((state) => state.setSelectedComponents); const getSelectedComponents = useStore((state) => state.getSelectedComponents, shallow); @@ -16,7 +18,7 @@ export const EditorSelecto = () => { const filterSelectedComponentsByHighestLevel = (selectedIds) => { const highestLevelComponents = findHighestLevelofSelection( selectedIds.map((id) => { - const component = getComponentDefinition(id); + const component = getComponentDefinition(id, moduleId); return { ...component, id, diff --git a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx index df78a8afa4..9f91c2d2c5 100644 --- a/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx +++ b/frontend/src/AppBuilder/AppCanvas/WidgetWrapper.jsx @@ -6,6 +6,8 @@ import { ConfigHandle } from './ConfigHandle/ConfigHandle'; import { useGridStore } from '@/_stores/gridStore'; import cx from 'classnames'; import RenderWidget from './RenderWidget'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; +import { NO_OF_GRIDS } from './appCanvasConstants'; const WidgetWrapper = memo( ({ @@ -20,24 +22,31 @@ const WidgetWrapper = memo( mode, darkMode, }) => { + const { moduleId } = useModuleContext(); const calculateMoveableBoxHeightWithId = useStore((state) => state.calculateMoveableBoxHeightWithId, shallow); const stylesDefinition = useStore( - (state) => state.getComponentDefinition(id)?.component?.definition?.styles, + (state) => state.getComponentDefinition(id, moduleId)?.component?.definition?.styles, + shallow + ); + const layoutData = useStore( + (state) => state.getComponentDefinition(id, moduleId)?.layouts?.[currentLayout], shallow ); - const layoutData = useStore((state) => state.getComponentDefinition(id)?.layouts?.[currentLayout], shallow); const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow); const isDragging = useStore((state) => state.draggingComponentId === id); const isResizing = useGridStore((state) => state.resizingComponentId === id); - const componentType = useStore((state) => state.getComponentDefinition(id)?.component?.component, shallow); + const componentType = useStore( + (state) => state.getComponentDefinition(id, moduleId)?.component?.component, + shallow + ); const setHoveredComponentForGrid = useStore((state) => state.setHoveredComponentForGrid, shallow); const canShowInCurrentLayout = useStore((state) => { - const others = state.getResolvedComponent(id, subContainerIndex)?.others; + const others = state.getResolvedComponent(id, subContainerIndex, moduleId)?.others; return others?.[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop']; }); const visibility = useStore((state) => { - const component = state.getResolvedComponent(id, subContainerIndex); - const componentExposedVisibility = state.getExposedValueOfComponent(id)?.isVisible; + const component = state.getResolvedComponent(id, subContainerIndex, moduleId); + const componentExposedVisibility = state.getExposedValueOfComponent(id, moduleId)?.isVisible; if (componentExposedVisibility === false) return false; if (component?.properties?.visibility === false || component?.styles?.visibility === false) return false; return true; @@ -47,16 +56,24 @@ const WidgetWrapper = memo( return null; } - const width = gridWidth * layoutData?.width; + let newLayoutData = layoutData; + + if (componentType === 'ModuleContainer' && mode === 'view') { + newLayoutData = { ...layoutData, top: 0, left: 0, width: NO_OF_GRIDS }; + } + + const width = gridWidth * newLayoutData?.width; const height = calculateMoveableBoxHeightWithId(id, currentLayout, stylesDefinition); const styles = { width: width + 'px', height: visibility === false ? '10px' : `${height}px`, - transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`, + transform: `translate(${newLayoutData.left * gridWidth}px, ${newLayoutData.top}px)`, WebkitFontSmoothing: 'antialiased', border: visibility === false && mode === 'edit' ? `1px solid var(--border-default)` : 'none', }; + const isModuleContainer = componentType === 'ModuleContainer'; + if (!componentType) return null; return ( <> @@ -67,6 +84,7 @@ const WidgetWrapper = memo( 'position-absolute': readOnly, 'active-target': isWidgetActive, 'opacity-0': isDragging || isResizing, + 'module-container': isModuleContainer, })} data-id={`${id}`} id={id} @@ -76,30 +94,32 @@ const WidgetWrapper = memo( // zIndex: mode === 'view' && widget.component.component == 'Datepicker' ? 2 : null, ...styles, }} - onMouseEnter={(e) => { - if (isDragging) return; + onMouseEnter={() => { + if (isDragging || isModuleContainer) return; setHoveredComponentForGrid(id); }} onMouseLeave={() => { - if (isDragging) return; + if (isDragging || isModuleContainer) return; setHoveredComponentForGrid(''); }} > {mode == 'edit' && ( )} { +export const addNewWidgetToTheEditor = ( + componentType, + eventMonitorObject, + currentLayout, + realCanvasRef, + parentId, + moduleInfo = undefined +) => { const canvasBoundingRect = realCanvasRef?.current?.getBoundingClientRect(); const componentMeta = componentTypes.find((component) => component.component === componentType); const componentName = computeComponentName(componentType, useStore.getState().getCurrentPageComponents()); @@ -52,6 +59,24 @@ export const addNewWidgetToTheEditor = (componentType, eventMonitorObject, curre const mainCanvasWidth = useGridStore.getState().subContainerWidths['canvas']; let width = Math.round((defaultWidth * mainCanvasWidth) / gridWidth); + let customLayouts = undefined; + + if (moduleInfo) { + componentData.definition.properties.moduleAppId = { value: moduleInfo.moduleId }; + componentData.definition.properties.moduleVersionId = { value: moduleInfo.versionId }; + componentData.definition.properties.moduleEnvironmentId = { value: moduleInfo.environmentId }; + componentData.definition.properties.visibility = { value: true }; + customLayouts = moduleInfo.moduleContainer.layouts; + + const inputItems = Object.values( + moduleInfo.moduleContainer.component.definition.properties?.input_items?.value ?? {} + ); + + for (const { name, default_value } of inputItems) { + componentData.definition.properties[name] = { value: default_value }; + } + } + // Ensure minimum width width = Math.max(width, 1); @@ -77,14 +102,14 @@ export const addNewWidgetToTheEditor = (componentType, eventMonitorObject, curre [currentLayout]: { top: top, left: left, - width, - height: defaultHeight, + width: customLayouts ? customLayouts[currentLayout].width : width, + height: customLayouts ? customLayouts[currentLayout].height : defaultHeight, }, [nonActiveLayout]: { top: top, left: left, - width, - height: defaultHeight, + width: customLayouts ? customLayouts[nonActiveLayout].width : width, + height: customLayouts ? customLayouts[nonActiveLayout].height : defaultHeight, }, }, withDefaultChildren: WIDGETS_WITH_DEFAULT_CHILDREN.includes(componentData.component), @@ -171,6 +196,7 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou component: { ...componentData, parent: _parent, + name: widgetName, }, layouts: { [currentLayout]: { @@ -683,10 +709,14 @@ export function pasteComponents(targetParentId, copiedComponentObj) { toast.success(`Component${filteredComponentsCount > 1 ? 's' : ''} pasted successfully`); } -export const getCanvasWidth = (currentLayout) => { - if (currentLayout === 'mobile') { - return CANVAS_WIDTHS.deviceWindowWidth; +export const getCanvasWidth = (moduleId = 'canvas') => { + if (moduleId !== 'canvas') { + return '100%'; } + + // if (currentLayout === 'mobile') { + // return CANVAS_WIDTHS.deviceWindowWidth; + // } const windowWidth = window.innerWidth; const widthInPx = windowWidth - (CANVAS_WIDTHS.leftSideBarWidth + CANVAS_WIDTHS.rightSideBarWidth); const canvasMaxWidth = useStore.getState().globalSettings.canvasMaxWidth; diff --git a/frontend/src/AppBuilder/AppCanvas/selecto.scss b/frontend/src/AppBuilder/AppCanvas/selecto.scss index 7366e2fd06..0962255be8 100644 --- a/frontend/src/AppBuilder/AppCanvas/selecto.scss +++ b/frontend/src/AppBuilder/AppCanvas/selecto.scss @@ -1,20 +1,21 @@ .active-target { outline: 1px solid #4af !important; -} +} -.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover { +.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover { outline: 1px solid #4af; z-index: 4 !important; } -.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { +.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { // outline: 1px solid #4af; z-index: 4 !important; } +.main-editor-canvas .widget-target.module-container { + outline: dotted 2px #CCD1D5 !important; +} + // .main-editor-canvas .widget-target:hover { // outline: 1px solid #4af; -// } - - - +// } \ No newline at end of file diff --git a/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js b/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js index 969e60f398..d80cbd76cc 100644 --- a/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js +++ b/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js @@ -3,11 +3,13 @@ import { isEmpty } from 'lodash'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { LEFT_SIDEBAR_WIDTH } from './appCanvasConstants'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const useSidebarMargin = (canvasContainerRef) => { + const { moduleId } = useModuleContext(); const [editorMarginLeft, setEditorMarginLeft] = useState(0); const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow); - const mode = useStore((state) => state.currentMode, shallow); + const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); useEffect(() => { if (mode !== 'view') setEditorMarginLeft(isSidebarOpen ? LEFT_SIDEBAR_WIDTH : 0); diff --git a/frontend/src/AppBuilder/CodeBuilder/Elements/Query.jsx b/frontend/src/AppBuilder/CodeBuilder/Elements/Query.jsx new file mode 100644 index 0000000000..f182a5cbb9 --- /dev/null +++ b/frontend/src/AppBuilder/CodeBuilder/Elements/Query.jsx @@ -0,0 +1,100 @@ +import React, { useCallback, useMemo } from 'react'; +import SelectComponent from '@/_ui/Select'; +import { components } from 'react-select'; +import Check from '@/_ui/Icon/solidIcons/Check'; +import useStore from '@/AppBuilder/_stores/store'; + +const Option = (props) => { + return ( + +
+ {props.label} + {props.isSelected && ( + + + + )} +
+
+ ); +}; + +const selectCustomStyles = { + control: (base, state) => { + return { + ...base, + border: state.isFocused ? '1px solid #3E63DD' : '1px solid #cccccc', + boxShadow: state.isFocused ? '0px 0px 6px #3E63DD' : 'none', + backgroundColor: state.isFocused ? 'var(--indigo2)' : 'var(--base)', + '&:hover': { + border: '1px solid #3E63DD !important', + boxShadow: '0px 0px 6px #3E63DD', + }, + borderRadius: '6px', + width: '144px', + minHeight: '32px', + }; + }, + + dropdownIndicator: (base) => ({ + ...base, + padding: '4px', + }), + menuList: (base) => ({ + ...base, + padding: '4px', + }), + option: (base, state) => ({ + ...base, + backgroundColor: state.isFocused ? '#F0F4FF !important' : 'white', + color: '#11181C', + borderRadius: '6px', + }), +}; + +export const Query = ({ value, onChange, meta }) => { + const dataQueries = useStore((state) => state.dataQuery.getCurrentModuleQueries('canvas')); + const options = dataQueries + .filter((query) => !(meta?.skipKinds ?? []).includes(query.kind)) + .map((query) => ({ name: query.name, value: query.id })); + + const onValueChange = useCallback( + (value) => { + console.log('value--- ', value, options); + onChange(value); + }, + [onChange, options] + ); + + // const cleanedValue = useMemo(() => { + // if (initialValue) { + // return initialValue.replace('{{queries.', '').replace('}}', ''); + // } + // return ''; + // }, [initialValue]); + + return ( +
+
e.stopPropagation()}> + null, + Option, + }} + /> +
+
+ ); +}; diff --git a/frontend/src/AppBuilder/CodeBuilder/TypeMapping.js b/frontend/src/AppBuilder/CodeBuilder/TypeMapping.js index eb01df1241..a249f52676 100644 --- a/frontend/src/AppBuilder/CodeBuilder/TypeMapping.js +++ b/frontend/src/AppBuilder/CodeBuilder/TypeMapping.js @@ -20,4 +20,5 @@ export const TypeMapping = { visibility: 'Visibility', numberInput: 'NumberInput', tableRowHeightInput: 'TableRowHeightInput', + query: 'Query', }; diff --git a/frontend/src/AppBuilder/CodeEditor/DynamicFxTypeRenderer.jsx b/frontend/src/AppBuilder/CodeEditor/DynamicFxTypeRenderer.jsx index 9a204b9cf2..99d80a6050 100644 --- a/frontend/src/AppBuilder/CodeEditor/DynamicFxTypeRenderer.jsx +++ b/frontend/src/AppBuilder/CodeEditor/DynamicFxTypeRenderer.jsx @@ -18,6 +18,7 @@ import { NumberInput } from '../CodeBuilder/Elements/NumberInput'; import { Datepicker } from '../CodeBuilder/Elements/Datepicker'; import TableRowHeightInput from '../CodeBuilder/Elements/TableRowHeightInput'; import { TimePicker } from '../CodeBuilder/Elements/TimePicker'; +import { Query } from '../CodeBuilder/Elements/Query'; import { ColorSwatches } from '@/modules/Appbuilder/components'; const AllElements = { @@ -40,6 +41,7 @@ const AllElements = { TableRowHeightInput, Datepicker, TimePicker, + Query, }; export const DynamicFxTypeRenderer = ({ paramType, ...restProps }) => { diff --git a/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx b/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx index c123cb2b9c..5c422b1eb3 100644 --- a/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx +++ b/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx @@ -13,6 +13,7 @@ import { reservedKeywordReplacer } from '@/_lib/reserved-keyword-replacer'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { Overlay } from 'react-bootstrap'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const sanitizeLargeDataset = (data, callback) => { const SIZE_LIMIT_KB = 5 * 1024; // 5 KB in bytes @@ -90,11 +91,12 @@ export const PreviewBox = ({ isWorkspaceVariable, validationFn, }) => { + const { moduleId } = useModuleContext(); const [resolvedValue, setResolvedValue] = useState(''); const [error, setError] = useState(null); const [coersionData, setCoersionData] = useState(null); const [largeDataset, setLargeDataset] = useState(false); - const globals = useStore((state) => state.getAllExposedValues().constants || {}, shallow); + const globals = useStore((state) => state.getAllExposedValues(moduleId).constants || {}, shallow); const secrets = useStore((state) => state.getSecrets(), shallow); const globalServerConstantsRegex = /^\{\{.*globals\.server.*\}\}$/; diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx index 16514ca409..7177dc4202 100644 --- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx @@ -28,11 +28,13 @@ import CodeHinter from './CodeHinter'; import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext'; import { createReferencesLookup } from '@/_stores/utils'; import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => { + const { moduleId } = useModuleContext(); const { initialValue, onChange, enablePreview = true, portalProps } = restProps; const { validation = {} } = fieldMeta; const [showPreview, setShowPreview] = useState(false); @@ -42,7 +44,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r const [cursorInsidePreview, setCursorInsidePreview] = useState(false); const [showSuggestions, setShowSuggestions] = useState(true); const validationFn = restProps?.validationFn; - const componentDefinition = useStore((state) => state.getComponentDefinition(componentId), shallow); + const componentDefinition = useStore((state) => state.getComponentDefinition(componentId, moduleId), shallow); const parentId = componentDefinition?.component?.parent; const customResolvables = useStore((state) => state.resolvedStore.modules.canvas?.customResolvables, shallow); @@ -522,6 +524,32 @@ const DynamicEditorBridge = (props) => { setForceCodeBox(fxActive); }, [component, fxActive]); + const renderFx = () => { + if (paramType === 'query' || (paramLabel !== 'Type' && isFxNotRequired === undefined)) { + return null; + } + return ( +
+ { + if (codeShow) { + setForceCodeBox(false); + onFxPress(false); + } else { + setForceCodeBox(true); + onFxPress(true); + } + }} + dataCy={cyLabel} + /> +
+ ); + }; + const fxClass = isEventManagerParam ? 'justify-content-start' : 'justify-content-end'; return (
@@ -538,26 +566,7 @@ const DynamicEditorBridge = (props) => { )}
- {paramLabel !== 'Type' && isFxNotRequired === undefined && ( -
- { - if (codeShow) { - setForceCodeBox(false); - onFxPress(false); - } else { - setForceCodeBox(true); - onFxPress(true); - } - }} - dataCy={cyLabel} - /> -
- )} + {renderFx()}
{!codeShow && ( diff --git a/frontend/src/AppBuilder/CodeEditor/utils.js b/frontend/src/AppBuilder/CodeEditor/utils.js index 11d6eb3c90..1b5fe0aafb 100644 --- a/frontend/src/AppBuilder/CodeEditor/utils.js +++ b/frontend/src/AppBuilder/CodeEditor/utils.js @@ -154,6 +154,7 @@ function resolveCode(code, customObjects = {}, withError = false, reservedKeywor 'queries', 'globals', 'page', + 'input', 'constants', 'moment', '_', @@ -168,6 +169,7 @@ function resolveCode(code, customObjects = {}, withError = false, reservedKeywor isJsCode ? state?.queries : undefined, isJsCode ? state?.globals : undefined, isJsCode ? state?.page : undefined, + isJsCode ? state?.input : undefined, state?.constants, // Passing constants as an argument allows the evaluated code to access and utilize the constants value correctly. moment, _, @@ -365,6 +367,7 @@ export const FxParamTypeMapping = Object.freeze({ visibility: 'Visibility', numberInput: 'NumberInput', tableRowHeightInput: 'TableRowHeightInput', + query: 'Query', }); export function computeCoercion(oldValue, newValue) { diff --git a/frontend/src/AppBuilder/Header/AppVersionsManager.jsx b/frontend/src/AppBuilder/Header/AppVersionsManager.jsx index 05009fd531..56070684c0 100644 --- a/frontend/src/AppBuilder/Header/AppVersionsManager.jsx +++ b/frontend/src/AppBuilder/Header/AppVersionsManager.jsx @@ -6,6 +6,7 @@ import { shallow } from 'zustand/shallow'; import { ToolTip } from '@/_components/ToolTip'; import { decodeEntities } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const appVersionLoadingStatus = Object.freeze({ loading: 'loading', @@ -14,6 +15,7 @@ const appVersionLoadingStatus = Object.freeze({ }); export const AppVersionsManager = function ({ darkMode }) { + const { moduleId } = useModuleContext(); const [appVersionStatus, setGetAppVersionStatus] = useState(appVersionLoadingStatus.loading); const [deleteVersion, setDeleteVersion] = useState({ @@ -54,15 +56,15 @@ export const AppVersionsManager = function ({ darkMode }) { deleteVersionAction: state.deleteVersionAction, selectedEnvironment: state.selectedEnvironment, currentLayout: state.currentLayout, - appId: state.app.appId, + appId: state.appStore.modules[moduleId].app.appId, releasedVersionId: state.releasedVersionId, editingVersion: state.editingVersion, setCurrentVersionId: state.setCurrentVersionId, currentVersionId: state.currentVersionId, - creationMode: state.app.creationMode, - isPublic: state.app.isPublic, - isViewer: state.isViewer, - currentMode: state.currentMode, + creationMode: state.appStore.modules[moduleId].app.creationMode, + isPublic: state.appStore.modules[moduleId].app.isPublic, + isViewer: state.appStore.modules[moduleId].isViewer, + currentMode: state.modeStore.modules[moduleId].currentMode, }), shallow ); diff --git a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx index 3add0e6074..e23fc51e9a 100644 --- a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx +++ b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'; import Select from '@/_ui/Select'; import { shallow } from 'zustand/shallow'; import useStore from '@/AppBuilder/_stores/store'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const CreateVersionModal = ({ showCreateAppVersion, @@ -17,6 +18,7 @@ const CreateVersionModal = ({ fetchingOrgGit, handleCommitOnVersionCreation = () => { }, }) => { + const { moduleId } = useModuleContext(); const [isCreatingVersion, setIsCreatingVersion] = useState(false); const [versionName, setVersionName] = useState(''); @@ -35,7 +37,7 @@ const CreateVersionModal = ({ developmentVersions: state.developmentVersions, featureAccess: state.license.featureAccess, editingVersion: state.currentVersionId, - appId: state.app.appId, + appId: state.appStore.modules[moduleId].app.appId, currentVersionId: state.currentVersionId, setCurrentVersionId: state.setCurrentVersionId, selectedVersion: state.selectedVersion, diff --git a/frontend/src/AppBuilder/Header/EditAppName.jsx b/frontend/src/AppBuilder/Header/EditAppName.jsx index 7211bb085b..e9dff6074a 100644 --- a/frontend/src/AppBuilder/Header/EditAppName.jsx +++ b/frontend/src/AppBuilder/Header/EditAppName.jsx @@ -8,10 +8,17 @@ import { InfoOrErrorBox } from './InfoOrErrorBox'; import { toast } from 'react-hot-toast'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; function EditAppName() { + const { moduleId } = useModuleContext(); const [appId, appName, setAppName, appCreationMode] = useStore( - (state) => [state.app.appId, state.app.appName, state.setAppName, state.app.creationMode], + (state) => [ + state.appStore.modules[moduleId].app.appId, + state.appStore.modules[moduleId].app.appName, + state.setAppName, + state.appStore.modules[moduleId].app.creationMode, + ], shallow ); diff --git a/frontend/src/AppBuilder/Header/EditVersionModal.jsx b/frontend/src/AppBuilder/Header/EditVersionModal.jsx index 3a126c6285..c22dd74991 100644 --- a/frontend/src/AppBuilder/Header/EditVersionModal.jsx +++ b/frontend/src/AppBuilder/Header/EditVersionModal.jsx @@ -4,8 +4,10 @@ import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { shallow } from 'zustand/shallow'; import useStore from '@/AppBuilder/_stores/store'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const EditVersionModal = ({ setShowEditAppVersion, showEditAppVersion }) => { + const { moduleId } = useModuleContext(); const [isEditingVersion, setIsEditingVersion] = useState(false); const { updateVersionNameAction, @@ -15,7 +17,7 @@ export const EditVersionModal = ({ setShowEditAppVersion, showEditAppVersion }) (state) => ({ updateVersionNameAction: state.updateVersionNameAction, selectedVersion: state.selectedVersion, - appId: state.app.appId, + appId: state.appStore.modules[moduleId].app.appId, }), shallow ); diff --git a/frontend/src/AppBuilder/Header/EditorHeader.jsx b/frontend/src/AppBuilder/Header/EditorHeader.jsx index fb779c6269..5e1e639819 100644 --- a/frontend/src/AppBuilder/Header/EditorHeader.jsx +++ b/frontend/src/AppBuilder/Header/EditorHeader.jsx @@ -12,12 +12,15 @@ import RightTopHeaderButtons from './RightTopHeaderButtons/RightTopHeaderButtons import BuildSuggestions from './BuildSuggestions'; import GitSyncManager from './GitSyncManager'; import UpdatePresenceMultiPlayer from './UpdatePresenceMultiPlayer'; +import { ModuleEditorBanner } from '@/modules/Modules/components'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const EditorHeader = ({ darkMode }) => { + const { moduleId, isModuleEditor } = useModuleContext(); const { isSaving, saveError, isVersionReleased } = useStore( (state) => ({ - isSaving: state.app.isSaving, - saveError: state.app.saveError, + isSaving: state.appStore.modules[moduleId].app.isSaving, + saveError: state.appStore.modules[moduleId].app.saveError, isVersionReleased: state.isVersionReleased, }), shallow @@ -72,7 +75,10 @@ export const EditorHeader = ({ darkMode }) => { }} >
- +
+ {isModuleEditor && } + +
@@ -96,20 +102,21 @@ export const EditorHeader = ({ darkMode }) => { {shouldEnableMultiplayer && }
-
- {/*
*/} + {!isModuleEditor &&
}
- {/* {editingVersion && ( */} - - {/* )} */} - - + {!isModuleEditor && ( + <> + + + + + )}
- + diff --git a/frontend/src/AppBuilder/Header/RightTopHeaderButtons/ReleaseVersionButton.jsx b/frontend/src/AppBuilder/Header/RightTopHeaderButtons/ReleaseVersionButton.jsx index 494eaa52e9..48b77cc238 100644 --- a/frontend/src/AppBuilder/Header/RightTopHeaderButtons/ReleaseVersionButton.jsx +++ b/frontend/src/AppBuilder/Header/RightTopHeaderButtons/ReleaseVersionButton.jsx @@ -8,8 +8,10 @@ import { shallow } from 'zustand/shallow'; import '@/_styles/versions.scss'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import useStore from '@/AppBuilder/_stores/store'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const ReleaseVersionButton = function DeployVersionButton() { + const { moduleId } = useModuleContext(); const [isReleasing, setIsReleasing] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const { isVersionReleased, editingVersion, updateReleasedVersionId, appId, versionToBeReleased, name } = useStore( @@ -19,7 +21,7 @@ const ReleaseVersionButton = function DeployVersionButton() { editingVersion: state.editingVersion, isEditorFreezed: state.isEditorFreezed, updateReleasedVersionId: state.updateReleasedVersionId, - appId: state.app.appId, + appId: state.appStore.modules[moduleId].app.appId, versionToBeReleased: state.currentVersionId, // selectedVersionId: state.selectedVersion.id, }), diff --git a/frontend/src/AppBuilder/Header/RightTopHeaderButtons/RightTopHeaderButtons.jsx b/frontend/src/AppBuilder/Header/RightTopHeaderButtons/RightTopHeaderButtons.jsx index d22a2978c8..9b144bdd88 100644 --- a/frontend/src/AppBuilder/Header/RightTopHeaderButtons/RightTopHeaderButtons.jsx +++ b/frontend/src/AppBuilder/Header/RightTopHeaderButtons/RightTopHeaderButtons.jsx @@ -8,19 +8,21 @@ import { isEmpty } from 'lodash'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import useStore from '@/AppBuilder/_stores/store'; import { PromoteReleaseButton } from '@/modules/Appbuilder/components'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; -const RightTopHeaderButtons = () => { +const RightTopHeaderButtons = ({ isModuleEditor }) => { return (
- + {!isModuleEditor && }
); }; const PreviewAndShareIcons = () => { + const { moduleId } = useModuleContext(); const { featureAccess, currentPageHandle, @@ -36,14 +38,14 @@ const PreviewAndShareIcons = () => { } = useStore( (state) => ({ featureAccess: state.license?.featureAccess, - currentPageHandle: state?.currentPageHandle, + currentPageHandle: state?.modules[moduleId].currentPageHandle, selectedEnvironment: state.selectedEnvironment, isVersionReleased: state.releasedVersionId === state.selectedVersion?.id, editingVersion: state.editingVersion, - appId: state.app.appId, - app: state.app.app, - slug: state.app.slug, - isPublic: state.app.isPublic, + appId: state.appStore.modules[moduleId].app.appId, + app: state.appStore.modules[moduleId].app.app, + slug: state.appStore.modules[moduleId].app.slug, + isPublic: state.appStore.modules[moduleId].app.isPublic, currentVersionId: state.currentVersionId, selectedVersion: state.selectedVersion, }), diff --git a/frontend/src/AppBuilder/LeftSidebar/Debugger/Debugger.jsx b/frontend/src/AppBuilder/LeftSidebar/Debugger/Debugger.jsx index 7b775e2b08..3823e2bfff 100644 --- a/frontend/src/AppBuilder/LeftSidebar/Debugger/Debugger.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/Debugger/Debugger.jsx @@ -10,7 +10,7 @@ function Debugger({ pinned, setPinned }) { shallow ); - const currentPageId = useStore((state) => state.currentPageId); + const currentPageId = useStore((state) => state.modules.canvas.currentPageId); const logsToBeShown = logs.filter((log) => log.page === currentPageId); diff --git a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/AppExport.jsx b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/AppExport.jsx index a70799ea54..0bce2fb999 100644 --- a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/AppExport.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/AppExport.jsx @@ -3,12 +3,14 @@ import { Button } from '@/components/ui/Button/Button'; import ExportAppModal from '@/HomePage/ExportAppModal'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import cx from 'classnames'; const AppExport = ({ darkMode }) => { + const { moduleId } = useModuleContext(); const { app } = useStore( (state) => ({ - app: state.app, + app: state.appStore.modules[moduleId].app, }), shallow ); diff --git a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/CanvasSettings.jsx b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/CanvasSettings.jsx index 9f021cd070..209677b747 100644 --- a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/CanvasSettings.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/CanvasSettings.jsx @@ -11,12 +11,14 @@ import FxButton from '@/Editor/CodeBuilder/Elements/FxButton'; import { useTranslation } from 'react-i18next'; import { Confirm } from '@/Editor/Viewer/Confirm'; import { shallow } from 'zustand/shallow'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const CanvasSettings = ({ darkMode }) => { + const { moduleId } = useModuleContext(); const { globalSettings, globalSettingsChanged, resolveOthers, getCanvasBackgroundColor } = useStore( (state) => ({ globalSettings: state.globalSettings, - updateGlobalSettings: state.updateGlobalSettings, + isMaintenanceOn: state.appStore.modules[moduleId].app.isMaintenanceOn, globalSettingsChanged: state.globalSettingsChanged, resolveOthers: state.resolveOthers, getCanvasBackgroundColor: state.getCanvasBackgroundColor, diff --git a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/MaintenanceMode.jsx b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/MaintenanceMode.jsx index c2b969146f..8b1b1cece0 100644 --- a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/MaintenanceMode.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/MaintenanceMode.jsx @@ -3,12 +3,14 @@ import useStore from '@/AppBuilder/_stores/store'; import SwitchComponent from '@/components/ui/Switch/Index'; import { shallow } from 'zustand/shallow'; import { Confirm } from '@/Editor/Viewer/Confirm'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const MaintenanceMode = ({ darkMode }) => { + const { moduleId } = useModuleContext(); const [showConfirmation, setConfirmationShow] = useState(false); const { isMaintenanceOn, toggleAppMaintenance } = useStore( (state) => ({ - isMaintenanceOn: state.app.isMaintenanceOn, + isMaintenanceOn: state.appStore.modules[moduleId].app.isMaintenanceOn, toggleAppMaintenance: state.toggleAppMaintenance, }), shallow diff --git a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/SlugInput.jsx b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/SlugInput.jsx index c996b09005..e7f255d697 100644 --- a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/SlugInput.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/SlugInput.jsx @@ -8,9 +8,11 @@ import { getHostURL, replaceEditorURL } from '@/_helpers/routes'; import useStore from '@/AppBuilder/_stores/store'; import { useTranslation } from 'react-i18next'; import { shallow } from 'zustand/shallow'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; // import { useStore } from '@/store'; const SlugInput = () => { + const { moduleId } = useModuleContext(); const { slug: oldSlug, appId, @@ -20,11 +22,11 @@ const SlugInput = () => { } = useStore( (state) => ({ globalSettings: state.globalSettings, - slug: state.app.slug, - appId: state.app.appId, - app: state.app, + slug: state.appStore.modules[moduleId].app.slug, + appId: state.appStore.modules[moduleId].app.appId, + app: state.appStore.modules[moduleId].app, setApp: state.setApp, - currentPage: state.modules.canvas.pages[state.currentPageId], + currentPage: state.modules[moduleId].pages[state.modules[moduleId].currentPageIndex], }), shallow ); diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx index aaf12714b9..e2b5947206 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx @@ -10,6 +10,7 @@ import LeftSidebarInspector from './LeftSidebarInspector/LeftSidebarInspector'; import GlobalSettings from './GlobalSettings'; import '../../_styles/left-sidebar.scss'; import Debugger from './Debugger/Debugger'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; // TODO: remove passing refs to LeftSidebarItem and use state @@ -23,6 +24,7 @@ export const BaseLeftSidebar = ({ renderAISideBarTrigger = () => null, renderAIChat = () => null, }) => { + const { moduleId, isModuleEditor, appType } = useModuleContext(); const [ pinned, selectedSidebarItem, @@ -41,7 +43,7 @@ export const BaseLeftSidebar = ({ state.selectedSidebarItem, state.setIsLeftSideBarPinned, state.setSelectedSidebarItem, - state.currentMode, + state.modeStore.modules[moduleId].currentMode, state.queryPanel.queryPanelHeight, state.debugger.unreadErrorCount, state.debugger.resetUnreadErrorCount, @@ -104,6 +106,8 @@ export const BaseLeftSidebar = ({ // popoverContentHeight={popoverContentHeight} setPinned={setPinned} pinned={pinned} + moduleId={moduleId} + appType={appType} /> ); case 'tooljetai': @@ -148,6 +152,7 @@ export const BaseLeftSidebar = ({ // globalSettingsChanged={globalSettingsChanged} // globalSettings={appDefinition.globalSettings} darkMode={darkMode} + isModuleEditor={isModuleEditor} // toggleAppMaintenance={toggleAppMaintenance} // isMaintenanceOn={isMaintenanceOn} // app={app} @@ -162,72 +167,79 @@ export const BaseLeftSidebar = ({ return null; } + const renderCommonItems = () => { + return ( + <> + handleSelectedSidebarItem('inspect')} + darkMode={darkMode} + icon="inspect" + className={`left-sidebar-item left-sidebar-layout left-sidebar-inspector`} + tip="Inspector" + ref={setSideBarBtnRefs('inspect')} + /> + + handleSelectedSidebarItem('debugger')} + className={`left-sidebar-item left-sidebar-layout`} + badge={true} + count={unreadErrorCount} + tip="Debugger" + ref={setSideBarBtnRefs('debugger')} + /> + + ); + }; + + const renderLeftSidebarItems = () => { + if (isModuleEditor) { + return renderCommonItems(); + } + return ( + <> + {renderAISideBarTrigger({ + selectedSidebarItem: selectedSidebarItem, + onClick: () => handleSelectedSidebarItem('tooljetai'), + darkMode: darkMode, + icon: 'tooljetai', + className: `left-sidebar-item left-sidebar-layout left-sidebar-page-selector`, + tip: 'Build with AI', + ref: setSideBarBtnRefs('tooljetai'), + })} + handleSelectedSidebarItem('page')} + darkMode={darkMode} + icon="page" + className={`left-sidebar-item left-sidebar-layout left-sidebar-page-selector`} + tip="Pages" + ref={setSideBarBtnRefs('page')} + /> + {renderCommonItems()} + handleSelectedSidebarItem('settings')} + className={`left-sidebar-item left-sidebar-layout`} + badge={true} + tip="Settings" + ref={setSideBarBtnRefs('settings')} + isModuleEditor={isModuleEditor} + /> + + ); + }; + return (
- {renderAISideBarTrigger({ - selectedSidebarItem: selectedSidebarItem, - onClick: () => handleSelectedSidebarItem('tooljetai'), - darkMode: darkMode, - icon: 'tooljetai', - className: `left-sidebar-item left-sidebar-layout left-sidebar-page-selector`, - tip: 'Build with AI', - ref: setSideBarBtnRefs('tooljetai'), - })} - handleSelectedSidebarItem('page')} - darkMode={darkMode} - icon="page" - className={`left-sidebar-item left-sidebar-layout left-sidebar-page-selector`} - tip="Pages" - ref={setSideBarBtnRefs('page')} - /> - - handleSelectedSidebarItem('inspect')} - darkMode={darkMode} - icon="inspect" - className={`left-sidebar-item left-sidebar-layout left-sidebar-inspector`} - tip="Inspector" - ref={setSideBarBtnRefs('inspect')} - /> - - handleSelectedSidebarItem('debugger')} - className={`left-sidebar-item left-sidebar-layout`} - badge={true} - count={unreadErrorCount} - tip="Debugger" - ref={setSideBarBtnRefs('debugger')} - /> - handleSelectedSidebarItem('settings')} - className={`left-sidebar-item left-sidebar-layout`} - badge={true} - tip="Settings" - ref={setSideBarBtnRefs('settings')} - /> - - {/* {dataSources?.length > 0 && ( - handleSelectedSidebarItem('datasource')} - icon="datasource" - className={`left-sidebar-item left-sidebar-layout sidebar-datasources`} - tip="Sources" - ref={setSideBarBtnRefs('datasource')} - /> - )} */} - + {renderLeftSidebarItems()} { // if tooljetai is open don't close @@ -238,7 +250,7 @@ export const BaseLeftSidebar = ({ toggleLeftSidebar(false); }} open={isSidebarOpen} - popoverContentClassName={`p-0 sidebar-h-100-popover ${selectedSidebarItem}`} + popoverContentClassName={`p-0 left-sidebar-scrollbar sidebar-h-100-popover ${selectedSidebarItem}`} side="right" popoverContent={renderPopoverContent()} popoverContentHeight={popoverContentHeight} diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/ArrayNode.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/ArrayNode.jsx new file mode 100644 index 0000000000..5c5d673c83 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/ArrayNode.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import OverflowTooltip from '@/_components/OverflowTooltip'; + +const ArrayNode = ({ value }) => { + return ( +
+ {`[${value.length}]`} +
+ ); +}; + +export default ArrayNode; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/BooleanNode.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/BooleanNode.jsx new file mode 100644 index 0000000000..65616c4883 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/BooleanNode.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import OverflowTooltip from '@/_components/OverflowTooltip'; + +const BooleanNode = ({ value }) => { + return ( +
+ + {value.toString()} + +
+ ); +}; + +export default BooleanNode; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/FunctionNode.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/FunctionNode.jsx new file mode 100644 index 0000000000..858aaa4d8f --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/FunctionNode.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import OverflowTooltip from '@/_components/OverflowTooltip'; + +const FunctionNode = () => { + return ( +
+ + function + +
+ ); +}; + +export default FunctionNode; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/NullNode.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/NullNode.jsx new file mode 100644 index 0000000000..40ff32737a --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/NullNode.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import OverflowTooltip from '@/_components/OverflowTooltip'; + +const NullNode = ({ value }) => { + return ( +
+ {value === null ? 'null' : 'undefined'} +
+ ); +}; + +export default NullNode; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/NumberNode.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/NumberNode.jsx new file mode 100644 index 0000000000..18643844e6 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/NumberNode.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import OverflowTooltip from '@/_components/OverflowTooltip'; + +const NumberNode = ({ value }) => { + return ( +
+ + {value} + +
+ ); +}; + +export default NumberNode; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/ObjectNode.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/ObjectNode.jsx new file mode 100644 index 0000000000..9977d6f108 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/ObjectNode.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import OverflowTooltip from '@/_components/OverflowTooltip'; +const ObjectNode = ({ value }) => { + return ( +
+ {`{${Object.keys(value).length}}`} +
+ ); +}; + +export default ObjectNode; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/Row.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/Row.jsx new file mode 100644 index 0000000000..94bf363706 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/Row.jsx @@ -0,0 +1,155 @@ +import React, { useState } from 'react'; +import StringNode from './StringNode'; +import FunctionNode from './FunctionNode'; +import NumberNode from './NumberNode'; +import BooleanNode from './BooleanNode'; +import NullNode from './NullNode'; +import ArrayNode from './ArrayNode'; +import ObjectNode from './ObjectNode'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import OverflowTooltip from '@/_components/OverflowTooltip'; +import { ToolTip } from '@/_components/ToolTip'; +import { DefaultCopyIcon } from '../../DefaultCopyIcon'; +import { copyToClipboard, extractComponentName } from '../../utils'; +import WidgetIcon from '@/../assets/images/icons/widgets'; +import { generateCypressDataCy } from '@/modules/common/helpers/cypressHelpers'; + +const renderNodeIcons = (node, iconsList, darkMode) => { + const icon = iconsList.filter((icon) => icon?.iconName === node)[0]; + + if (icon && icon.jsx) { + if (icon?.tooltipMessage) { + return ( + +
{icon.jsx({ height: 14, width: 14 })}
+
+ ); + } + return icon.jsx({ height: 14, width: 14 }); + } + return null; +}; + +const Row = ({ label, value, level = 1, absolutePath, iconsList, darkMode }) => { + const [isExpanded, setIsExpanded] = useState(false); + const Node = () => { + if (typeof value === 'string') { + return ; + } else if (typeof value === 'undefined' || value === null) { + return ; + } else if (typeof value === 'number') { + return ; + } else if (typeof value === 'boolean') { + return ; + } else if (Array.isArray(value)) { + return ; + } else if (typeof value === 'object') { + return ; + } else if (typeof value === 'function') { + return ; + } + }; + + const isObject = typeof value === 'object' && !Array.isArray(value) && value !== null; + const isArray = Array.isArray(value); + + return ( +
+
+
setIsExpanded((prev) => !prev)}> +
+ {(isArray || isObject) && + (isExpanded ? ( + + ) : ( + + ))} +
+
+ + {renderNodeIcons(label, iconsList, darkMode)} + {label} + +
+
+ +
+
+ + { + copyToClipboard(`{{${absolutePath}}}`, false); + }} + className="copy-to-clipboard json-viewer-action-icon" + > + + + + + { + copyToClipboard(value); + }} + className="json-viewer-action-icon" + > + + + +
+
+
+ {isExpanded && isObject && ( +
+ {Object.entries(value).map(([key, val]) => ( + + ))} +
+ )} + {isExpanded && isArray && ( +
+ {value.map((item, index) => { + return ( + + ); + })} +
+ )} +
+ ); +}; + +export default Row; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/StringNode.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/StringNode.jsx new file mode 100644 index 0000000000..60e3d42d20 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/Components/StringNode.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import OverflowTooltip from '@/_components/OverflowTooltip'; + +const StringNode = ({ value }) => { + return ( +
+ {`"${value}"`} +
+ ); +}; + +export default StringNode; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/CustomJSONViewer.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/CustomJSONViewer.jsx new file mode 100644 index 0000000000..d37aff84aa --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/CustomJSONViewer.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import Row from './Components/Row'; +import './styles.scss'; + +const CustomJSONViewer = ({ data, absolutePath, iconsList }) => { + let modifiedData = data; + if (typeof data !== 'object') modifiedData = { '': data }; + return ( +
+ {Object.entries(modifiedData).map(([key, value], index) => { + return ( + + ); + })} +
+ ); +}; + +export default CustomJSONViewer; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/styles.scss b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/styles.scss new file mode 100644 index 0000000000..eea3660631 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/CustomJSONViewer/styles.scss @@ -0,0 +1,93 @@ +.custom-json-viewer { + margin-left: 16px; + margin-right: 16px; + font-family: "IBM Plex Sans"; + font-size: 12px; + color: var(--text-default, #1B1F24); + overflow-x: auto; + min-width: 0; + + // Hide scrollbar for Chrome, Safari and Opera + &::-webkit-scrollbar { + display: none; + } + + // Hide scrollbar for IE, Edge and Firefox + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + + .json-viewer-row-container { + min-width: max-content; + width: 100%; + position: relative; + &:hover { + background-color: var(--interactive-overlays-fill-hover); + width: 100%; + + .json-viewer-actions-container { + display: flex; + } + } + } + + .json-viewer-row { + width: 100%; + display: flex; + height: 20px; + align-items: center; + overflow: hidden; + cursor: pointer; + + .json-viewer-expand-icon { + width: 12px; + height: 12px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .json-viewer-label-container { + margin-right: 4px; + flex-shrink: 0; /* don't shrink */ + white-space: nowrap; + } + + .json-viewer-value-container { + flex: 1; /* take available space */ + min-width: 0; /* allow shrinkage */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 70px; /* Add padding to prevent text from going under actions */ + } + + .json-viewer-actions-container { + display: none; + margin-left: auto; + height: 12px; + width: 40px; + align-items: center; + flex-shrink: 0; + position: absolute; + right: 16px; /* Align with the parent container's margin */ + background: var(--bg-default); /* Add background to ensure text doesn't show through */ + z-index: 1; /* Ensure actions stay on top */ + + .json-viewer-action-icon { + display: flex; + align-items: center; + justify-content: center; + height: 20px; + width: 20px; + + &:hover { + cursor: pointer; + background-color: var(--button-outline-hover); + border-radius: 4px; + } + } + } + + } +} \ No newline at end of file diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/DefaultCopyIcon.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/DefaultCopyIcon.jsx new file mode 100644 index 0000000000..c71b8ad5c3 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/DefaultCopyIcon.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +export const DefaultCopyIcon = ({ height = 12, width = 12, fill = '#6A727C' }) => ( + + + +); diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/HiddenOptions.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/HiddenOptions.jsx new file mode 100644 index 0000000000..05c4333549 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/HiddenOptions.jsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from 'react'; +import { ToolTip } from '@/_components/ToolTip'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; +import useStore from '@/AppBuilder/_stores/store'; +import { shallow } from 'zustand/shallow'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import cx from 'classnames'; +import { DefaultCopyIcon } from './DefaultCopyIcon'; +import { generateCypressDataCy } from '@/modules/common/helpers/cypressHelpers'; + +export const HiddenOptions = (props) => { + const { nodeSpecificFilteredActions, generalActionsFiltered, darkMode, setActionClicked, data } = props; + const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); + const [showMenu, setShowMenu] = useState(false); + const closeMenu = () => { + setShowMenu(false); + }; + + const copyPath = () => { + generalActionsFiltered[0].dispatchAction(`{{${data?.selectedNodePath}}}`, false); + }; + + const copyValue = () => { + const value = getResolvedValue(`{{${data?.selectedNodePath}}}`); + generalActionsFiltered[0].dispatchAction(value); + }; + + useEffect(() => { + const handleClickOutside = (event) => { + if (event.target.closest('.copy-menu-options') === null) { + closeMenu(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // This is to ensure that the actionClicked state is updated when the menu is shown or deleted on the next render to avoid misplacing the Popover + useEffect(() => { + setTimeout(() => setActionClicked(showMenu), 0); + }, [showMenu]); + + const renderOptions = () => { + return nodeSpecificFilteredActions?.map((actionOption, index) => { + const { name, icon, src, iconName, dispatchAction, width = 12, height = 12 } = actionOption; + if (icon) { + return ( +
+ + {/* ${name === 'Go to component' ? '' : currentNode} */} + { + event.stopPropagation(); + dispatchAction(data); + }} + > + + + +
+ ); + } + }); + }; + + return ( +
+ {renderOptions()} + e.stopPropagation()}> + +
+
{ + event.stopPropagation(); + copyPath(); + closeMenu(); + }} + className="option" + data-cy="inspector-copy-path" + > + + Copy path +
+
{ + event.stopPropagation(); + copyValue(); + closeMenu(); + }} + className="option" + data-cy="inspector-copy-value" + > + + Copy value +
+
+
+ + } + > +
{ + event.stopPropagation(); + setShowMenu((prev) => !prev); + }} + className="node-action-icon" + style={{ + outline: 'none', + ...(showMenu && { backgroundColor: 'var(--button-outline-pressed, rgba(136, 144, 153, 0.18)' }), + }} + > + +
+
+
+ ); +}; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/JSONTreeViewerV2.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/JSONTreeViewerV2.jsx new file mode 100644 index 0000000000..3829f2187a --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/JSONTreeViewerV2.jsx @@ -0,0 +1,238 @@ +import React, { useMemo } from 'react'; +import TreeView, { flattenTree } from 'react-accessible-treeview'; +import useStore from '@/AppBuilder/_stores/store'; +import { shallow } from 'zustand/shallow'; +import Fuse from 'fuse.js'; +import JSONViewer from './JSONViewer'; +import { Node } from './Node'; +import { v4 as uuidv4 } from 'uuid'; +import InputComponent from '@/components/ui/Input/Index'; +import { isEmpty } from 'lodash'; + +const ensureUniqueIds = (node, parentId = '') => { + if (!node) return node; + + const seenIds = new Set(); + const processNode = (currentNode, currentParentId = '') => { + if (!currentNode) return currentNode; + + const newChildren = currentNode.children?.map((child, index) => { + const baseId = child.id; + let uniqueId = baseId; + let counter = 1; + + while (seenIds.has(uniqueId)) { + uniqueId = `${baseId}_${counter}`; + counter++; + } + + seenIds.add(uniqueId); + return processNode({ ...child, id: uniqueId }, uniqueId); + }); + + return { + ...currentNode, + children: newChildren, + }; + }; + + return processNode(node, parentId); +}; + +const JSONTreeViewerV2 = ({ data = {}, iconsList = [], darkMode, searchablePaths = new Set() }) => { + const searchValue = useStore((state) => state.inspectorSearchValue, shallow); + const getComponentIdFromName = useStore((state) => state.getComponentIdFromName, shallow); + const getComponentDefinition = useStore((state) => state.getComponentDefinition, shallow); + const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); + const setSearchValue = useStore((state) => state.setInspectorSearchValue, shallow); + const selectedNodePath = useStore((state) => state.selectedNodePath, shallow); + const setSelectedNodePath = useStore((state) => state.setSelectedNodePath, shallow); + + const selectedNodes = useStore((state) => state.selectedNodes, shallow); + + function fuzzySearch(query, searchablePaths) { + const list = Array.from(searchablePaths); + const fuse = new Fuse(list, { + threshold: 0.2, + minMatchCharLength: 2, + includeScore: true, + distance: 1000, + tokenize: true, + matchAllTokens: true, + }); + return fuse.search(query).map((result) => result.item); + } + + const [searchedSet, pathSet] = useMemo(() => { + const result = fuzzySearch(searchValue, searchablePaths); + const expandedIdSet = new Set(); + result.forEach((id) => { + const pathArray = id.split('.'); + for (let i = pathArray.length - 1; i > 0; i--) { + const parentPath = pathArray.slice(0, i).join('.'); + if (!expandedIdSet.has(parentPath)) { + expandedIdSet.add(parentPath); + } + } + }); + return [new Set(result), expandedIdSet]; + }, [searchValue, JSON.stringify(searchablePaths)]); + + // Do not remove this code, once we have the data in the correct format, we can use this function to filter the data + // const recursiveFn = (obj) => { + // if (!obj || typeof obj !== 'object') return []; + // let isCompletelyExposed = false; + // obj?.children?.forEach((child) => { + // const { id } = child; + // if (searchedSet.has(id)) { + // isCompletelyExposed = true; + // } + // }); + // const newChildren = + // obj?.children + // ?.filter((child) => { + // return isCompletelyExposed || pathSet.has(child.id); + // }) + // ?.map((child) => { + // return recursiveFn(child); + // }) || []; + + // return { + // ...obj, + // children: newChildren, + // }; + // }; + + // const formattedData = useMemo(() => { + // return searchValue ? recursiveFn(data) : data; + // }, [data, searchValue]); + + const key = useMemo(() => { + return uuidv4(); + }, [JSON.stringify(data), selectedNodePath]); + + const processedData = useMemo(() => ensureUniqueIds(data), [data]); + const flattendedData = flattenTree(processedData); + + const backFn = () => { + setSelectedNodePath(null); + }; + + const selectedData = (() => { + if (selectedNodePath?.startsWith('components.')) { + // Split the selectedNode path using . and grab the second element if it exists + const pathArray = selectedNodePath.split('.'); + const componentName = pathArray?.[1]; + const componentId = getComponentIdFromName(componentName); + const component = getComponentDefinition(componentId); + const parent = component?.component?.parent; + if (parent) { + const parentComponent = getComponentDefinition(parent); + const parentType = parentComponent?.component?.component; + if (parentType === 'Form') { + return { + id: componentId, + }; + } + } + } + return selectedNodePath ? getResolvedValue(`{{${selectedNodePath}}}`) : {}; + })(); + + const expandedIds = [...Array.from(pathSet), ...selectedNodes]; + + const filteredIds = useMemo(() => { + const expandedIdsSet = new Set(expandedIds); + const filtered = flattendedData.filter((item) => { + const { metadata } = item || {}; + const { actualPath, path } = metadata || {}; + return expandedIdsSet.has(actualPath || path); + }); + + return filtered + .map((item) => item.id) + .filter((path) => { + const pathArray = path.split('.'); + // One by one combine and check if the path is in expandedIds or not + for (let i = pathArray.length - 1; i > 0; i--) { + const parentPath = pathArray.slice(0, i).join('.'); + if (!expandedIdsSet.has(parentPath)) { + return false; + } + } + return true; + }); + }, [flattendedData, expandedIds]); + + return ( + <> + {!selectedNodePath || (typeof selectedData == 'object' && isEmpty(selectedData)) ? ( +
+
+ {/* setSearchValue(e.target.value)} + onClearCallback={() => setSearchValue('')} + placeholder={`Search`} + customClass={`tj-inspector-search-input tj-text-xsm`} + showClearButton={false} + width={300} + /> */} + + setSearchValue(e.target.value)} + onClear={() => setSearchValue('')} + size="medium" + placeholder="Search" + value={searchValue} + {...(searchValue && { trailingAction: 'clear' })} + data-cy="inspector-search-input" + /> +
+
+ { + const { element } = props; + const { metadata } = element || {}; + const { path } = metadata || {}; + const data = { + nodeName: element.name, + selectedNodePath: path, + }; + + return ( + + ); + }} + /> +
+
+ ) : ( + + )} + + ); +}; + +export default JSONTreeViewerV2; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/JSONViewer.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/JSONViewer.jsx new file mode 100644 index 0000000000..e1391adc2e --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/JSONViewer.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { TreeViewHeader } from './TreeViewHeader'; +import useCallbackActions from './useCallbackActions'; +import CustomJSONViewer from './CustomJSONViewer/CustomJSONViewer'; + +export const JSONViewer = (props) => { + const { data, path, darkMode, backFn, iconsList } = props; + + const callbackActions = useCallbackActions() || []; + const type = path.startsWith('components') ? 'components' : path.startsWith('queries') ? 'queries' : 'actions'; + const nodeSpecificActions = callbackActions.filter((action) => [type].includes(action.for))?.[0]?.actions; + const optionsData = { + nodeName: path?.split('.')?.slice(-1)?.[0] || '', + selectedNodePath: path, + }; + + const generalActions = callbackActions.filter((action) => action.for === 'all')?.[0]?.actions || []; + + return ( +
+ + +
+ ); +}; + +export default JSONViewer; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx index 3adca4be98..6b8e52ce4b 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx @@ -1,108 +1,153 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { HeaderSection } from '@/_ui/LeftSidebar'; import JSONTreeViewer from '@/_ui/JSONTreeViewer'; +import JSONTreeViewerV2 from './JSONTreeViewerV2'; import _ from 'lodash'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import useIconList from './useIconList'; -import useCallbackActions from './useCallbackActions'; +import { Button as ButtonComponent } from '@/components/ui/Button/Button'; +import { formatInspectorDataMisc, formatInspectorQueryData } from './utils'; +import ErrorBoundary from '@/_ui/ErrorBoundary'; -const sortAndReduce = (obj) => { - return Object.entries(obj) - .sort((a, b) => a[0].localeCompare(b[0], undefined, { sensitivity: 'base' })) - .reduce((acc, [name, value]) => { - acc[name] = value; - return acc; - }, {}); -}; +import './styles.scss'; -const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => { +const LeftSidebarInspector = ({ darkMode, pinned, setPinned, moduleId, appType }) => { const exposedComponentsVariables = useStore((state) => state.getAllExposedValues().components, shallow); const exposedQueries = useStore((state) => state.getAllExposedValues().queries || {}, shallow); const exposedVariables = useStore((state) => state.getAllExposedValues().variables || {}, shallow); const exposedConstants = useStore((state) => state.getAllExposedValues().constants || {}, shallow); const exposedPageVariables = useStore((state) => state.getAllExposedValues().page || {}, shallow); const exposedGlobalVariables = useStore((state) => state.getAllExposedValues().globals || {}, shallow); + const exposedModuleInputs = useStore((state) => state.getAllExposedValues(moduleId).input || {}, shallow); const componentIdNameMapping = useStore((state) => state.getComponentIdNameMapping(), shallow); + const formatInspectorComponentData = useStore((state) => state.formatInspectorComponentData, shallow); const queryNameIdMapping = useStore((state) => state.getQueryNameIdMapping(), shallow); - const pathToBeInspected = useStore((state) => state.pathToBeInspected); + const searchablePaths = useRef(new Set(['queries', 'components', 'globals', 'variables', 'page', 'constants'])); + const iconsList = useIconList({ exposedComponentsVariables, componentIdNameMapping, exposedQueries, }); - const callbackActions = useCallbackActions(); const sortedComponents = useMemo(() => { - return Object.entries(componentIdNameMapping) - .map(([key, name]) => ({ - key, - name: name || key, - value: exposedComponentsVariables[key] ?? { id: key }, - })) - .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) - .reduce((acc, { key, name, value }) => { - acc[name] = { ...value, id: key }; - return acc; - }, {}); + return formatInspectorComponentData(componentIdNameMapping, exposedComponentsVariables, searchablePaths.current); }, [exposedComponentsVariables, componentIdNameMapping]); const sortedQueries = useMemo(() => { - // Create a reverse mapping for faster lookups - const reverseMapping = Object.fromEntries(Object.entries(queryNameIdMapping).map(([name, id]) => [id, name])); - - const _sortedQueries = Object.entries(exposedQueries) - .map(([key, value]) => ({ - key, - name: reverseMapping[key] || key, - value, - })) - .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) - .reduce((acc, { name, value }) => { - acc[name] = value; - return acc; - }, {}); - return _sortedQueries; + return formatInspectorQueryData(queryNameIdMapping, exposedQueries, searchablePaths.current); }, [exposedQueries, queryNameIdMapping]); - const sortedVariables = useMemo(() => sortAndReduce(exposedVariables), [exposedVariables]); + const sortedVariables = useMemo( + () => formatInspectorDataMisc(exposedVariables, 'variables', searchablePaths.current), + [exposedVariables] + ); - const sortedConstants = useMemo(() => sortAndReduce(exposedConstants), [exposedConstants]); + const sortedConstants = useMemo( + () => formatInspectorDataMisc(exposedConstants, 'constants', searchablePaths.current), + [exposedConstants] + ); - const sortedPageVariables = useMemo(() => sortAndReduce(exposedPageVariables), [exposedPageVariables]); + const sortedPageVariables = useMemo( + () => formatInspectorDataMisc(exposedPageVariables, 'page', searchablePaths.current), + [exposedPageVariables] + ); - const sortedGlobalVariables = useMemo(() => sortAndReduce(exposedGlobalVariables), [exposedGlobalVariables]); + const sortedGlobalVariables = useMemo( + () => formatInspectorDataMisc(exposedGlobalVariables, 'globals', searchablePaths.current), + [exposedGlobalVariables] + ); + + const sortedModuleInputs = useMemo( + () => formatInspectorDataMisc(exposedModuleInputs, 'input', searchablePaths.current), + [exposedModuleInputs] + ); const memoizedJSONData = React.useMemo(() => { - const jsontreeData = {}; + const jsontreeData = { + name: '', + children: [ + { + id: 'queries', + name: 'Queries', - jsontreeData['queries'] = sortedQueries; - jsontreeData['components'] = sortedComponents; - jsontreeData['globals'] = sortedGlobalVariables; - jsontreeData['variables'] = sortedVariables; - jsontreeData['page'] = sortedPageVariables; - jsontreeData['constants'] = sortedConstants; + children: sortedQueries, + metadata: { type: 'queries', path: 'queries' }, + }, + { + id: 'components', + name: 'Components', + + children: sortedComponents, + metadata: { type: 'components', path: 'components' }, + }, + { + id: 'globals', + name: 'Globals', + + children: sortedGlobalVariables, + metadata: { type: 'globals', path: 'globals' }, + }, + { + id: 'variables', + name: 'Variables', + + children: sortedVariables, + metadata: { type: 'variables', path: 'variables' }, + }, + { + id: 'page', + name: 'Page', + + children: sortedPageVariables, + metadata: { type: 'page', path: 'page' }, + }, + { + id: 'constants', + name: 'Constants', + children: sortedConstants, + metadata: { type: 'constants', path: 'constants' }, + }, + ], + }; + + if (appType === 'module') { + jsontreeData.children.push({ + id: 'input', + name: 'Input', + children: sortedModuleInputs, + metadata: { path: 'input' }, + }); + } + + const addNoDataChild = (data) => { + const types = data.children; + types.forEach((type) => { + if (type.children.length === 0) { + type.children.push({ + id: `empty-${type.metadata.type}`, + name: `No ${type.metadata.type} found`, + children: [], + metadata: { noData: true }, + }); + } + }); + }; + + addNoDataChild(jsontreeData); return jsontreeData; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]); - - const handleNodeExpansion = (path, data, currentNode) => { - if (pathToBeInspected && path?.length > 0) { - const shouldExpand = pathToBeInspected.includes(path[path.length - 1]); - - // Scroll to the component in the inspector - if (path?.length === 2 && path?.[0] === 'components' && shouldExpand) { - const target = document.getElementById(`inspector-node-${String(currentNode).toLowerCase()}`); - if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - } - - return shouldExpand; - } else return false; - }; + }, [ + sortedComponents, + sortedQueries, + sortedVariables, + sortedConstants, + sortedPageVariables, + sortedGlobalVariables, + sortedModuleInputs, + ]); return (
{ style={{ resize: 'horizontal', minWidth: 288 }} > - +
- setPinned(!pinned)} - darkMode={darkMode} - styles={{ width: '28px', padding: 0 }} - data-cy={`left-sidebar-inspector`} - variant="tertiary" - className="left-sidebar-header-btn" - leftIcon={pinned ? 'unpin' : 'pin'} - iconWidth="14" - fill={`var(--slate12)`} - > + variant="ghost" + fill="var(--icon-strong,#6A727C)" + size="medium" + data-cy="left-sidebar-pin-button" + />
+
- + + +
); diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/Node.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/Node.jsx new file mode 100644 index 0000000000..081737f3f5 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/Node.jsx @@ -0,0 +1,163 @@ +import React, { useState } from 'react'; +import WidgetIcon from '@/../assets/images/icons/widgets'; +import { extractComponentName } from './utils'; +import { ToolTip } from '@/_components/ToolTip'; +import Highlighter from 'react-highlight-words'; +import cx from 'classnames'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import OverflowTooltip from '@/_components/OverflowTooltip'; +import { HiddenOptions } from './HiddenOptions'; +import useCallbackActions from './useCallbackActions'; +import useStore from '@/AppBuilder/_stores/store'; +import { Button as ButtonComponent } from '@/components/ui/Button/Button'; +import { shallow } from 'zustand/shallow'; + +const renderNodeIcons = (node, iconsList, darkMode) => { + const icon = iconsList.filter((icon) => icon?.iconName === node && !icon?.isInfoIcon)[0]; + if (icon && icon?.iconPath) { + return ( + + ); + } + + if (icon && icon.jsx) { + if (icon?.tooltipMessage) { + return ( + +
{icon.jsx({ height: 14, width: 14 })}
+
+ ); + } + return icon.jsx({ height: 14, width: 14 }); + } +}; + +export const Node = (props) => { + const { + element, + getNodeProps, + level, + handleSelect, + handleExpand, + isExpanded, + isDisabled, + isBranch, + darkMode, + setSelectedNodePath, + searchValue, + iconsList, + data, + } = props; + + const [actionClicked, setActionClicked] = useState(false); + const setSelectedNodes = useStore((state) => state.setSelectedNodes, shallow); + const callbackActions = useCallbackActions() || []; + const nodeIcon = renderNodeIcons(element.name, iconsList, darkMode); + const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); + const metadata = element.metadata || {}; + const { type, path } = metadata; + const nodeSpecificActions = callbackActions.filter((action) => [type].includes(action.for)); + const onExpand = (node) => { + const { element } = node || {}; + const { metadata } = element || {}; + const { path, actualPath } = metadata || {}; + setSelectedNodes(actualPath || path); + }; + + const onSelect = (node) => { + const { isBranch, element } = node || {}; + const { metadata } = element || {}; + const { path, type } = metadata || {}; + if (type && level !== 1) { + setSelectedNodePath(path); + } + }; + + const nodeSpecificFilteredActions = + nodeSpecificActions?.[0]?.actions?.filter((action) => { + return action.enableInspectorTreeView; + }) || []; + + const generalActions = callbackActions.filter((action) => action.for === 'all'); + const generalActionsFiltered = generalActions?.[0]?.actions?.filter((action) => { + return action.enableInspectorTreeView; + }); + + return ( + //
+
1 ? 12 : 0, + // paddingLeft: '16px', + opacity: isDisabled ? 0.5 : 1, + height: '24px', + display: 'flex', + alignItems: 'center', + color: level === 1 ? 'var(--text-placeholder, #6A727C)' : 'var(--text-default, #1B1F24)', + fontWeight: level === 1 ? 500 : 400, + marginTop: level === 1 ? 4 : 0, + marginBottom: level === 1 ? 4 : 0, + // borderLeft: level > 1 ? '1px solid var(--slate6, #D7DBDF)' : 'none', + }} + > + {/* {!['queries', 'globals', 'variables'].includes(type) && ( */} +
+ {(isBranch || level === 1 || path === 'page.variables') && ( + onExpand(props)} + variant="ghost" + fill="var(--icon-default,#ACB2B9)" + size="small" + data-cy={`inspector-${type}-expand-button`} + /> + )} +
+ {/* )} */} + +
onSelect(props)} + className={cx('node-content', { + 'node-content-hoverable': level !== 1 && !metadata.noData, + 'node-content-active': actionClicked, + })} + > + {nodeIcon &&
{nodeIcon}
} +
+ {metadata.noData && ( +
+ + {element.name} +
+ )} + {!metadata.noData && ( + + + + )} +
+
+ +
+
+
+ //
+ ); +}; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/TreeViewHeader.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/TreeViewHeader.jsx new file mode 100644 index 0000000000..3ef719de00 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/TreeViewHeader.jsx @@ -0,0 +1,157 @@ +import React, { useEffect, useState } from 'react'; +import useStore from '@/AppBuilder/_stores/store'; +import { shallow } from 'zustand/shallow'; +import ArrowLeft from '@/_ui/Icon/bulkIcons/Arrowleft'; +import CheveronRight from '@/_ui/Icon/bulkIcons/CheveronRight'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; +import cx from 'classnames'; +import { ToolTip } from '@/_components/ToolTip'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { DefaultCopyIcon } from './DefaultCopyIcon'; +import { generateCypressDataCy } from '@/modules/common/helpers/cypressHelpers'; + +export const TreeViewHeader = (props) => { + const { path, backFn, darkMode, data, nodeSpecificActions, type, generalActions } = props; + const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); + const [showMenu, setShowMenu] = useState(false); + const pathArray = path.split('.'); + const parentNode = pathArray[0]; + + const closeMenu = () => { + setShowMenu(false); + }; + + const copyPath = () => { + generalActions[0].dispatchAction(`{{${data?.selectedNodePath}}}`, false); + }; + + const copyValue = () => { + const value = getResolvedValue(`{{${data?.selectedNodePath}}}`); + generalActions[0].dispatchAction(value); + }; + + useEffect(() => { + const handleClickOutside = (event) => { + if (event.target.closest('.copy-menu-options') === null) { + closeMenu(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const renderOptions = () => { + return ( + <> +
+
{ + event.stopPropagation(); + copyPath(); + closeMenu(); + }} + className="option" + data-cy="inspector-copy-path" + > + + Copy path +
+
{ + event.stopPropagation(); + copyValue(); + closeMenu(); + }} + className="option" + data-cy="inspector-copy-value" + > + + Copy value +
+
+ + {nodeSpecificActions?.map((actionOption, index) => { + const { name, icon, src, iconName, dispatchAction, width = 16, height = 16 } = actionOption; + if (icon) { + return ( +
+ { + event.stopPropagation(); + dispatchAction(data); + setShowMenu(false); + }} + className="option" + > + + {name} + +
+ ); + } + })} + + ); + }; + + return ( +
+ {/*
+ +
*/} +
+ {parentNode.charAt(0).toUpperCase() + parentNode.slice(1)} + + {pathArray.length > 1 && + pathArray.slice(1).map((item, index) => ( + <> + + + {item.charAt(0).toUpperCase() + item.slice(1)} + + + ))} +
+ + + {renderOptions()} + + } + > +
{ + event.stopPropagation(); + setShowMenu((prev) => !prev); + }} + className="copy-menu-options-icon json-viewer-options-btn" + style={{ + outline: 'none', + border: 'none', + boxShadow: 'none', + }} + data-cy="inspector-menu-icon" + > + +
+
+
+ ); +}; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/styles.scss b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/styles.scss new file mode 100644 index 0000000000..6a2c981600 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/styles.scss @@ -0,0 +1,288 @@ +.json-tree-view { + // ul { + // margin-left:16px !important; + // border-left: 1px solid var(--slate6, #D7DBDF); + // } + + ul[role="tree"] { + margin-left: 16px !important; + border-left: none !important; + + ul { + margin-left: 16px !important; + border-left: none !important; + + ul { + margin-left: 10px !important; + padding-left: 16px !important; + border-left: 1px solid var(--slate6, #D7DBDF) !important; + } + } + + } +} + +.basic.tree { + list-style: none; + margin: 0; + padding: 0px; + padding-right: 20px; + padding-top: 0px; + +} + +.basic .tree-node, +.basic .tree-node-group { + list-style: none; + margin: 0; + padding: 0; +} + +.basic .tree-branch-wrapper, +.basic .tree-node__leaf { + outline: none; +} + +// .basic .tree-node--focused { +// outline-color: rgb(77, 144, 254); +// outline-style: auto; +// outline-width: 2px; +// display: block; +// } + +.basic .tree-node__branch { + display: block; +} + +.basic .tree-node { + cursor: pointer; +} + +.node-expansion-icon { + width: 20px; + height: 20px; + margin-right: 4px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.node-icon { + display: flex; + align-items: flex-end; + justify-content: center; + margin-right: 6px; + height: 14px; + width: 14px; +} + +.node-label { + display: flex; + align-items: center; + font-size: 12px; + flex: 1; + min-width: 0px; +} + + +.tj-inspector-search-input { + width: 300px; + height: 32px; + border-radius: 6px; + background-color: var(--base) !important; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; +} + +.node-content { + display: flex; + align-items: center; + height: 100%; + width: 100%; + padding-left: 4px; + padding-right: 4px; + +} + +.node-content-active { + .copy-menu-options-icon { + background-color: var(--interactive-overlays-fill-hover); + } +} + +.node-content-hoverable:hover, +.node-content-active { + background-color: var(--interactive-overlays-fill-hover); + cursor: pointer; + + .node-actions { + display: flex; + } + +} + +.json-viewer { + font-size: 11px; + + ul { + background-color: transparent !important; + padding-left: 6px !important; + padding-right: 10px !important; + // li{ + // text-indent: 0px !important; + // padding-top: 0px !important; + // height:18.46px; + // } + // ul > li:hover { + // background-color: var(--interactive-overlays-fill-hover); // or any color you prefer + // } + } + + + +} + +.json-viewer-node-value { + font-weight: 500; +} + +.json-viewer-header { + font-size: 12px; + font-weight: 500; + display: flex; + flex-direction: row; + margin-left: 16px; + margin-bottom: 12px; + margin-right: 18px; + margin-top: 8px; + height: 28px; + align-items: center; + +} + +.json-viewer-back-btn { + display: flex; + align-items: center; + + &:hover { + cursor: pointer; + background-color: var(--interactive-overlays-fill-hover); + border-radius: 4px; + } + +} + +// .json-viewer-options-btn { +// display: flex; +// align-items: center; +// margin-left: auto; + +// &:hover { +// cursor: pointer; +// background-color:var(--interactive-overlays-fill-hover); +// border-radius: 4px; +// } +// } + +.json-viewer-options-btn { + margin-left: auto; + + + align-items: center; + +} + + +.node-highlight { + background-color: #FFD43B; + padding: 0px; +} + +.node-actions { + justify-content: center; + align-items: center; + margin-left: auto; + display: none; + gap: 4px; +} + +.node-action-icon { + display: flex; + justify-content: center; + align-items: center; + height: 20px; + width: 20px; + border-radius: 4px; + flex-shrink: 0; + + &:hover { + background-color: var(--button-outline-hover); + } + +} + +.copy-menu-options-icon { + border: 1px solid var(--border-weak, #E4E7EB); + display: flex; + justify-content: center; + align-items: center; + width: 20px; + height: 20px; + border-radius: 4px; + box-shadow: var(--elevation-100-box-shadow); + background-color: var(--base); + + &:hover { + background-color: var(--button-outline-hover); + } +} + +.copy-menu-options { + width: 144px; + border: none; + background-color: var(--background-surface-layer-01); + border-radius: 10px; + top: -5px !important; + + &.dark-theme { + .popover-body { + box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.9), 0px 8px 16px 0px #000000; + } + } + + .popover-body { + width: 100%; + padding: 8px; + border-radius: 10px; + background: var(--background-surface-layer-01); + box-shadow: 0px 0px 1px 0px rgba(48, 50, 51, 0.05), 0px 8px 16px 0px rgba(48, 50, 51, 0.1); + + .menu-options { + .option { + display: flex; + padding: 6px 8px; + align-items: center; + gap: 6px; + height: 30px; + align-self: stretch; + border-radius: 6px; + cursor: pointer; + + &:hover { + background: rgba(136, 144, 153, 0.08); + } + } + } + } +} + +.node-label-no-data { + display: flex; + align-items: center; + gap: 4px; + color: var(--text-placeholder, #858C94); + +} \ No newline at end of file diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js index 937fe45b2c..1c4685b585 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useCallbackActions.js @@ -1,7 +1,7 @@ import { toast } from 'react-hot-toast'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; -// import { runQuery } from '@/AppBuilder/_utils/queryPanel'; +import { copyToClipboard } from './utils'; const useCallbackActions = () => { const deleteComponents = useStore((state) => state.deleteComponents, shallow); @@ -10,29 +10,39 @@ const useCallbackActions = () => { const shouldFreeze = useStore((state) => state.getShouldFreeze()); const runQuery = useStore((state) => state.queryPanel.runQuery); const getComponentIdToAutoScroll = useStore((state) => state.getComponentIdToAutoScroll); + const setSelectedQuery = useStore((state) => state.queryPanel.setSelectedQuery, shallow); + const getComponentIdFromName = useStore((state) => state.getComponentIdFromName, shallow); + const getQueryIdFromName = useStore((state) => state.getQueryIdFromName, shallow); const handleRemoveComponent = (component) => { - deleteComponents([component.id]); + const { nodeName } = component; + const componentId = getComponentIdFromName(nodeName); + deleteComponents([componentId]); }; const handleSelectComponentOnEditor = (component) => { - if (currentPageComponents?.[component.id]) { - setSelectedComponents([component.id]); + const { nodeName } = component; + const componentId = getComponentIdFromName(nodeName); + if (currentPageComponents?.[componentId]) { + setSelectedComponents([componentId]); } }; - const handleRunQuery = (query, currentNode) => { - runQuery(query.id, currentNode, undefined, 'edit', {}, true); + const handleRunQuery = (data) => { + const { nodeName } = data; + const queryId = getQueryIdFromName(nodeName); + runQuery(queryId, nodeName, undefined, 'edit', {}, true); }; - const copyToClipboard = (data) => { - const stringified = JSON.stringify(data, null, 2).replace(/\\/g, ''); - navigator.clipboard.writeText(stringified); - return toast.success('Copied to the clipboard', { position: 'top-center' }); + const selectQuery = (data) => { + const { nodeName } = data; + const id = getQueryIdFromName(nodeName); + setSelectedQuery(id); }; const handleAutoScrollToComponent = (data) => { - const { isAccessible, computedComponentId, isOnCanvas } = getComponentIdToAutoScroll(data.id); + const componentId = getComponentIdFromName(data.nodeName); + const { isAccessible, computedComponentId, isOnCanvas } = getComponentIdToAutoScroll(componentId); if (!isAccessible) { if (isOnCanvas) { toast.success( @@ -57,9 +67,19 @@ const useCallbackActions = () => { name: 'Run Query', dispatchAction: handleRunQuery, icon: true, - src: 'assets/images/icons/editor/play.svg', + iconName: 'play01', width: 8, height: 8, + enableInspectorTreeView: false, + }, + { + name: 'View query', + dispatchAction: selectQuery, + icon: true, + iconName: 'file-code', + width: 14, + height: 14, + enableInspectorTreeView: true, }, ], enableForAllChildren: false, @@ -68,10 +88,30 @@ const useCallbackActions = () => { { for: 'components', actions: [ - { name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true }, - { name: 'Go to component', dispatchAction: handleAutoScrollToComponent, icon: true, iconName: 'select' }, + { + name: 'Select Widget', + dispatchAction: handleSelectComponentOnEditor, + icon: false, + onSelect: true, + enableInspectorTreeView: false, + }, + { + name: 'Go to component', + dispatchAction: handleAutoScrollToComponent, + icon: true, + iconName: 'corners', + enableInspectorTreeView: true, + }, ...(!shouldFreeze - ? [{ name: 'Delete Component', dispatchAction: handleRemoveComponent, icon: true, iconName: 'trash' }] + ? [ + { + name: 'Delete Component', + dispatchAction: handleRemoveComponent, + icon: true, + iconName: 'trash', + enableInspectorTreeView: false, + }, + ] : []), ], enableForAllChildren: false, @@ -79,7 +119,10 @@ const useCallbackActions = () => { }, { for: 'all', - actions: [{ name: 'Copy value', dispatchAction: copyToClipboard, icon: false }], + actions: [ + { name: 'Copy value', dispatchAction: copyToClipboard, icon: false, enableInspectorTreeView: true }, + { name: 'Copy path', dispatchAction: copyToClipboard, icon: false, enableInspectorTreeView: true }, + ], }, ]; return callbackActions; diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useIconList.js b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useIconList.js index e93c18d959..ab818d9786 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useIconList.js +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/useIconList.js @@ -10,7 +10,10 @@ const useIconList = ({ exposedComponentsVariables, componentIdNameMapping, expos const queryIcons = Object.keys(exposedQueries).map((queryId) => { const query = dataQueries.find((dataQuery) => dataQuery.id === queryId); if (!isEmpty(query)) { - return { iconName: query?.name, jsx: () => }; + return { + iconName: query?.name, + jsx: ({ height = 16, width = 16 }) => , + }; } }); @@ -37,7 +40,9 @@ const useIconList = ({ exposedComponentsVariables, componentIdNameMapping, expos if (componentExposedVariables.disable) { icons.push({ iconName: 'disable', - jsx: () => , + jsx: ({ height = 16, width = 16 }) => ( + + ), className: 'component-icon', tooltipMessage: 'This function will be deprecated soon, You can use setDisable as an alternative', isInfoIcon: true, @@ -47,7 +52,9 @@ const useIconList = ({ exposedComponentsVariables, componentIdNameMapping, expos if (componentExposedVariables.visibility) { icons.push({ iconName: 'visibility', - jsx: () => , + jsx: ({ height = 16, width = 16 }) => ( + + ), className: 'component-icon', tooltipMessage: 'This function will be deprecated soon, You can use setVisibility as an alternative', isInfoIcon: true, @@ -62,7 +69,9 @@ const useIconList = ({ exposedComponentsVariables, componentIdNameMapping, expos if (componentExposedVariables.setChecked) { icons.push({ iconName: 'setChecked', - jsx: () => , + jsx: ({ height = 16, width = 16 }) => ( + + ), className: 'component-icon', tooltipMessage: 'This function will be deprecated soon, You can use setValue as an alternative', isInfoIcon: true, @@ -78,7 +87,9 @@ const useIconList = ({ exposedComponentsVariables, componentIdNameMapping, expos if (componentExposedVariables.disable) { icons.push({ iconName: 'disable', - jsx: () => , + jsx: ({ height = 16, width = 16 }) => ( + + ), className: 'component-icon', tooltipMessage: 'This function will be deprecated soon, You can use setDisable as an alternative', isInfoIcon: true, @@ -88,7 +99,9 @@ const useIconList = ({ exposedComponentsVariables, componentIdNameMapping, expos if (componentExposedVariables.visibility) { icons.push({ iconName: 'visibility', - jsx: () => , + jsx: ({ height = 16, width = 16 }) => ( + + ), className: 'component-icon', tooltipMessage: 'This function will be deprecated soon, You can use setVisibility as an alternative', isInfoIcon: true, @@ -98,7 +111,9 @@ const useIconList = ({ exposedComponentsVariables, componentIdNameMapping, expos if (componentExposedVariables.loading) { icons.push({ iconName: 'loading', - jsx: () => , + jsx: ({ height = 16, width = 16 }) => ( + + ), className: 'component-icon', tooltipMessage: 'This function will be deprecated soon, You can use setLoading as an alternative', isInfoIcon: true, @@ -112,7 +127,9 @@ const useIconList = ({ exposedComponentsVariables, componentIdNameMapping, expos return [ { iconName: 'visibility', - jsx: () => , + jsx: ({ height = 16, width = 16 }) => ( + + ), className: 'component-icon', tooltipMessage: 'This function will be deprecated soon, You can use setVisibility as an alternative', isInfoIcon: true, @@ -124,7 +141,6 @@ const useIconList = ({ exposedComponentsVariables, componentIdNameMapping, expos }) .flat() .filter((value) => value !== undefined); // Remove undefined values - const iconsList = useMemo( () => [...queryIcons, ...componentIcons, ...deprecatedIcons], [queryIcons, componentIcons, deprecatedIcons] diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/utils.js b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/utils.js new file mode 100644 index 0000000000..87ee5306a2 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/utils.js @@ -0,0 +1,92 @@ +import { toast } from 'react-hot-toast'; + +export const formatInspectorDataMisc = (obj, type, searchablePaths = new Set()) => { + if (typeof obj !== 'object' || obj === null) return []; + const data = Object.entries(obj).sort((a, b) => a[0].localeCompare(b[0], undefined, { sensitivity: 'base' })); + const reduceData = (obj, path, level = 1) => { + let data = obj; + if (!obj || typeof obj !== 'object' || (path === 'page.variables' ? level > 2 : level > 1)) return []; + else if (!Array.isArray(obj)) { + data = Object.entries(obj); + } + return data.reduce((acc, [name, value]) => { + const currentPath = path + `.${name}`; + searchablePaths.add(currentPath); + return [ + ...acc, + { + id: currentPath, + name, + children: reduceData(value, currentPath, level + 1), + metadata: { + type: type, + path: currentPath, + ...((path === 'page.variables' ? level === 2 : level === 1) && { + data: typeof value === 'object' ? JSON.stringify(value) : value, + }), + }, + }, + ]; + }, []); + }; + + return reduceData(data, type); +}; + +export const formatInspectorQueryData = (queryNameIdMapping, exposedQueries, searchablePaths = new Set()) => { + const reverseMapping = Object.fromEntries(Object.entries(queryNameIdMapping).map(([name, id]) => [id, name])); + const _sortedQueries = Object.entries(exposedQueries) + .map(([key, value]) => ({ + key, + name: reverseMapping[key] || key, + value, + })) + .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); + + const reduceData = (obj, path = 'queries', level = 1) => { + let data = obj; + if (!obj || typeof obj !== 'object' || level > 1) return []; + else if (!Array.isArray(obj)) { + data = Object.entries(obj); + } + return data + .filter((item) => item.name) + .reduce((acc, { id, name, value }) => { + const currentPath = path + `.${name}`; + searchablePaths.add(currentPath); + return [ + ...acc, + { + id: currentPath, + name, + children: reduceData(value, currentPath, level + 1), + metadata: { + type: 'queries', + path: currentPath, + ...(level === 1 && { data: typeof value === 'object' ? JSON.stringify(value) : value }), + }, + }, + ]; + }, []); + }; + + return reduceData(_sortedQueries); +}; + +export const extractComponentName = (path) => { + // Match the last part of the URL before ".svg" using a regular expression + const match = path.match(/\/([^/]+)\.svg$/); + + if (match && match[1]) { + return match[1]; // Return the matched component name + } else { + return null; // Return null if the pattern doesn't match + } +}; + +export const copyToClipboard = (data, includeQuotes = true) => { + const stringified = JSON.stringify(data, null, 2).replace(/\\/g, ''); + const finalText = includeQuotes ? stringified : stringified.slice(1, -1); + navigator.clipboard.writeText(finalText); + return toast.success('Copied to the clipboard', { position: 'top-center' }); +}; diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx index 1c5c3124f0..d57e82c0c6 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx @@ -2,11 +2,13 @@ import React from 'react'; import { Overlay, Popover } from 'react-bootstrap'; import { Button } from '@/_ui/LeftSidebar'; import useStore from '@/AppBuilder/_stores/store'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import { shallow } from 'zustand/shallow'; import { ToolTip } from '@/_components/ToolTip'; import SolidIcon from '@/_ui/Icon/SolidIcons'; export const PageHandlerMenu = ({ darkMode }) => { + const { moduleId } = useModuleContext(); const setShowEditingPopover = useStore((state) => state.setShowEditingPopover); const setShowPageEventsModal = useStore((state) => state.setShowPageEventsModal); @@ -31,7 +33,7 @@ export const PageHandlerMenu = ({ darkMode }) => { closePageEditPopover(); }; - const homePageId = useStore((state) => state.app.homePageId); + const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId); const page = editingPage; const isHomePage = page?.id === homePageId; const showMenu = showEditingPopover; @@ -105,7 +107,7 @@ export const PageHandlerMenu = ({ darkMode }) => { text="Mark home" iconSrc={'assets/images/icons/home.svg'} closeMenu={() => {}} - callback={() => markAsHomePage(editingPage.id)} + callback={() => markAsHomePage(editingPage.id, moduleId)} /> )} {!isDisabled && ( @@ -164,7 +166,7 @@ export const PageHandlerMenu = ({ darkMode }) => { >
Page permission
- {!licenseValid && } + {!licenseValid && }
); diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx index a113251b61..3f8b3c09d3 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx @@ -16,13 +16,16 @@ import { RenameInput } from './RenameInput'; import IconSelector from './IconSelector'; import { withRouter } from '@/_hoc/withRouter'; import OverflowTooltip from '@/_components/OverflowTooltip'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import { shallow } from 'zustand/shallow'; +import { ToolTip } from '@/_components/ToolTip'; export const PageMenuItem = withRouter( memo(({ darkMode, page, navigate }) => { - const homePageId = useStore((state) => state.app.homePageId); + const { moduleId } = useModuleContext(); + const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId); const isHomePage = page.id === homePageId; - const currentPageId = useStore((state) => state.currentPageId); + const currentPageId = useStore((state) => state.modules[moduleId].currentPageId); const isSelected = page.id === currentPageId; const isHidden = page?.hidden ?? false; const isDisabled = page?.disabled ?? false; @@ -139,9 +142,9 @@ export const PageMenuItem = withRouter( if (currentPageId === page.id) { return; } - switchPage(page.id, page.handle); - setCurrentPageHandle(page.handle); - }, [currentPageId, page.id, page.handle, switchPage, setCurrentPageHandle]); + switchPage(page.id, page.handle, [], moduleId); + setCurrentPageHandle(page.handle, moduleId); + }, [currentPageId, page.id, page.handle, switchPage, setCurrentPageHandle, moduleId]); const handlePageMenuSettings = useCallback( (event) => { @@ -151,6 +154,36 @@ export const PageMenuItem = withRouter( [popoverRef.current, page] ); + function getTooltip() { + const permission = page?.permissions?.length ? page?.permissions[0] : null; + if (!permission) return ''; + const users = permission.users || []; + const isSingle = permission.type === 'SINGLE'; + const isGroup = permission.type === 'GROUP'; + + if (users.length === 0) return null; + + if (isSingle) { + if (users.length === 1) { + const email = users[0].user.email; + return `Access restricted to ${email}`; + } else { + return `Access restricted to ${users.length} users`; + } + } + + if (isGroup) { + if (users.length === 1) { + const groupName = users[0].permissionGroup?.name ?? 'Group'; + return `Access restricted to ${groupName} group`; + } else { + return `Access restricted to ${users.length} groups`; + } + } + + return ''; + } + return (
setIsHovered(true)} @@ -200,7 +233,13 @@ export const PageMenuItem = withRouter(
- {licenseValid && restricted && } + {licenseValid && restricted && ( + +
+ +
+
+ )}
{!shouldFreeze && ( diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx index 6a4a1c516a..160a941ebe 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx @@ -8,6 +8,7 @@ import { appPermissionService } from '@/_services'; import { ConfirmDialog } from '@/_components'; import toast from 'react-hot-toast'; import Spinner from '@/_ui/Spinner'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const PERMISSION_TYPES = { single: 'SINGLE', @@ -16,10 +17,11 @@ const PERMISSION_TYPES = { }; export default function PagePermission({ darkMode }) { + const { moduleId } = useModuleContext(); const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); const editingPage = useStore((state) => state.editingPage); - const appId = useStore((state) => state.app.appId); + const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); const selectedUserGroups = useStore((state) => state.selectedUserGroups); const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); const selectedUsers = useStore((state) => state.selectedUsers); @@ -34,7 +36,6 @@ export default function PagePermission({ darkMode }) { const [showConfirmDelete, setShowConfirmDelete] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isPermissionsLoading, setPermissionsLoading] = useState(true); - const [pageToDelete, setPageToDelete] = useState(null); const [initialSelectedGroups, setInitialSelectedGroups] = useState([]); const [initialSelectedUsers, setInitialSelectedUsers] = useState([]); const [initalPagePermissionType, setInitialPagePermissionType] = useState('all'); @@ -42,7 +43,7 @@ export default function PagePermission({ darkMode }) { useEffect(() => { if (!showPagePermissionModal) return; const fetchPagePermission = () => { - appPermissionService.getPagePermission(appId, editingPage?.id || pageToDelete).then((data) => { + appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => { if (data) { if (data[0] && data[0]?.type === PERMISSION_TYPES.group) { const groups = @@ -55,7 +56,6 @@ export default function PagePermission({ darkMode }) { setInitialPagePermissionType(data[0]?.type?.toLowerCase()); setPagePermission(data); toggleUserGroupSelect(true); - setPageToDelete(null); setInitialSelectedGroups(groups); data?.length && setSelectedUserGroups(groups); } else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) { @@ -74,7 +74,6 @@ export default function PagePermission({ darkMode }) { setInitialPagePermissionType(data[0]?.type?.toLowerCase()); setPagePermission(data); toggleUsersSelect(true); - setPageToDelete(null); setInitialSelectedUsers(users); data?.length && setSelectedUsers(users); } @@ -83,7 +82,7 @@ export default function PagePermission({ darkMode }) { }); }; fetchPagePermission(); - }, [showPagePermissionModal, pageToDelete]); + }, [showPagePermissionModal]); const isSelectionUnchanged = useMemo(() => { if (pagePermissionType === 'group') { @@ -237,13 +236,12 @@ export default function PagePermission({ darkMode }) { const deletePagePermission = () => { setIsLoading(true); appPermissionService - .deletePagePermission(appId, pageToDelete) + .deletePagePermission(appId, editingPage?.id) .then((data) => { toast.success('Permission successfully deleted!', { className: 'text-nowrap w-auto mw-100', }); - updatePageWithPermissions(pageToDelete, []); - setPageToDelete(null); + updatePageWithPermissions(editingPage?.id, []); }) .catch(() => { toast.error('Permission could not be deleted. Please try again!', { @@ -284,25 +282,18 @@ export default function PagePermission({ darkMode }) { isLoading={isLoading} handleClose={handlePagePermissionModalClose} confirmBtnProps={{ - title: pagePermission ? 'Update' : pagePermissionType === 'all' ? 'Default permission' : 'Create permission', + title: pagePermission + ? 'Save changes' + : pagePermissionType === 'all' + ? 'Default permission' + : 'Create permission', disabled: isPermissionsLoading || isSelectionUnchanged, tooltipMessage: '', + leftIcon: pagePermission && 'save', + className: 'action-btn-page-permission', }} darkMode={darkMode} className="page-permissions-modal" - headerAction={() => - pagePermission && ( - { - setPageToDelete(editingPage?.id); - togglePagePermissionModal(false); - setShowConfirmDelete(true); - }} - > - - - ) - } >
{isPermissionsLoading ? ( @@ -360,7 +351,8 @@ export default function PagePermission({ darkMode }) { } const UserGroupSelect = () => { - const appId = useStore((state) => state.app.appId); + const { moduleId } = useModuleContext(); + const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); const selectedUserGroups = useStore((state) => state.selectedUserGroups); const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); const [userGroups, setUserGroups] = useState([]); @@ -420,7 +412,8 @@ const UserGroupSelect = () => { }; const UserSelect = () => { - const appId = useStore((state) => state.app.appId); + const { moduleId } = useModuleContext(); + const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); const editingPage = useStore((state) => state.editingPage); const selectedUsers = useStore((state) => state.selectedUsers); const setSelectedUsers = useStore((state) => state.setSelectedUsers); diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss index 968218b106..a3adc9ec20 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss @@ -374,4 +374,8 @@ .spinner-center { min-height: 250px; } +} + +.modal-base .modal-footer .action-btn-page-permission svg path { + fill: var(--indigo1) !important; } \ No newline at end of file diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx index 88a0e2664a..e903078dc2 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerHeader.jsx @@ -10,12 +10,12 @@ 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 { useModuleContext } 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) => { - const moduleId = useModuleId(); + const { moduleId } = useModuleContext(); const updateQuerySuggestions = useStore((state) => state.queryPanel.updateQuerySuggestions); const previewQuery = useStore((state) => state.queryPanel.previewQuery); const renameQuery = useStore((state) => state.dataQuery.renameQuery); @@ -152,8 +152,8 @@ const NameInput = ({ onInput, value, darkMode, isDiabled, selectedQuery }) => { const hasPermissions = selectedDataSourceScope === 'global' ? canUpdateDataSource(selectedQuery?.data_source_id) || - canReadDataSource(selectedQuery?.data_source_id) || - canDeleteDataSource() + canReadDataSource(selectedQuery?.data_source_id) || + canDeleteDataSource() : true; const inputRef = useRef(); @@ -275,8 +275,8 @@ const PreviewButton = ({ buttonLoadingState, onClick }) => { const hasPermissions = selectedDataSource?.scope === 'global' && selectedDataSource?.type !== DATA_SOURCE_TYPE.SAMPLE ? canUpdateDataSource(selectedQuery?.data_source_id) || - canReadDataSource(selectedQuery?.data_source_id) || - canDeleteDataSource() + canReadDataSource(selectedQuery?.data_source_id) || + canDeleteDataSource() : true; const isPreviewQueryLoading = useStore((state) => state.queryPanel.isPreviewQueryLoading); const { t } = useTranslation(); diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Workflows.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Workflows.jsx index dcbf7b46ce..60210d4d07 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Workflows.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Workflows.jsx @@ -5,15 +5,17 @@ import CodeHinter from '@/AppBuilder/CodeEditor'; import './workflows-query.scss'; import { v4 as uuidv4 } from 'uuid'; import useStore from '@/AppBuilder/_stores/store'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver'; export function Workflows({ options, optionsChanged, currentState }) { + const { moduleId } = useModuleContext(); const [workflowOptions, setWorkflowOptions] = useState([]); const [isMenuOpen, setIsMenuOpen] = useState(false); const [_selectedWorkflowId, setSelectedWorkflowId] = useState(undefined); const [params, setParams] = useState([...(options.params ?? [{ key: '', value: '' }])]); - const appId = useStore((state) => state.app.appId); + const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); usePopoverObserver( document.getElementsByClassName('query-details')[0], diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx index fbad517a81..dc6f075d33 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx @@ -13,9 +13,11 @@ import useStore from '@/AppBuilder/_stores/store'; import { Confirm } from '@/Editor/Viewer/Confirm'; // TODO: enable delete query confirmation popup import { debounce } from 'lodash'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => { - const appId = useStore((state) => state.app.appId); + const { moduleId } = useModuleContext(); + const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); const isQuerySelected = useStore((state) => state.queryPanel.isQuerySelected(dataQuery.id), shallow); const setSelectedQuery = useStore((state) => state.queryPanel.setSelectedQuery); diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentConfigurationTab/ComponentConfigurationTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentConfigurationTab/ComponentConfigurationTab.jsx index e0ddb28922..2f4b785b6a 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentConfigurationTab/ComponentConfigurationTab.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentConfigurationTab/ComponentConfigurationTab.jsx @@ -5,7 +5,7 @@ import useStore from '@/AppBuilder/_stores/store'; import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants'; import { shallow } from 'zustand/shallow'; -export const ComponentConfigurationTab = ({ darkMode }) => { +export const ComponentConfigurationTab = ({ darkMode, isModuleEditor }) => { const selectedComponentId = useStore((state) => state.selectedComponents?.[0], shallow); const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab); if (!selectedComponentId) { @@ -17,6 +17,7 @@ export const ComponentConfigurationTab = ({ darkMode }) => { darkMode={darkMode} selectedComponentId={selectedComponentId} pages={[]} + isModuleEditor={isModuleEditor} /> ); }; diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/ComponentModuleTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/ComponentModuleTab.jsx new file mode 100644 index 0000000000..f1126ab4d5 --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/ComponentModuleTab.jsx @@ -0,0 +1,32 @@ +import React, { useState } from 'react'; +import './styles.scss'; + +export const ComponentModuleTab = ({ onChangeTab }) => { + const [activeTab, setActiveTab] = useState(1); + + const handleChangeTab = (tab) => { + setActiveTab(tab); + onChangeTab(tab); + }; + + return ( +
+
+ + +
+
+ ); +}; diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/index.js b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/index.js new file mode 100644 index 0000000000..a9bf63add0 --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/index.js @@ -0,0 +1 @@ +export { ComponentModuleTab as default } from './ComponentModuleTab'; diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/styles.scss b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/styles.scss new file mode 100644 index 0000000000..51fdce0d61 --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentModuleTab/styles.scss @@ -0,0 +1,19 @@ +.tj-tabs-container-outer { + padding-top: 0px; + gap: 10px; + height: 36px; + margin-bottom: 8px; + margin-top: 16px; +} + +.tj-tabs-container { + padding: 2px; + gap: 2px; + display: flex; + + width: 100%; + height: 100%; + background: var(--slate4); + border-radius: 6px; + +} \ No newline at end of file diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentsManagerTab.jsx similarity index 78% rename from frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx rename to frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentsManagerTab.jsx index ae5aadfac6..7968e72e89 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/ComponentsManagerTab.jsx @@ -1,24 +1,30 @@ import React, { useCallback, useMemo, useState } from 'react'; import { isEmpty, debounce } from 'lodash'; import { useTranslation } from 'react-i18next'; -import { LEGACY_ITEMS } from './constants'; +import { LEGACY_ITEMS, IGNORED_ITEMS } from './constants'; import { componentTypes, componentTypeDefinitionMap } from '@/AppBuilder/WidgetManager'; import Fuse from 'fuse.js'; import { SearchBox } from '@/_components'; import { DragLayer } from './DragLayer'; import useStore from '@/AppBuilder/_stores/store'; +import ComponentModuleTab from './ComponentModuleTab'; +import { ModuleManager } from '@/modules/Modules/components'; // TODO: Hardcode all the component-section mapping in a constant file and just loop over it // TODO: styling // TODO: scrolling // TODO: searching -export const ComponentsManagerTab = ({ darkMode }) => { +export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => { const componentList = useMemo(() => { - return componentTypes.map((component) => component.component); + return componentTypes + .map((component) => component.component) + .filter((component) => !IGNORED_ITEMS.includes(component)); }, [componentTypes]); const [filteredComponents, setFilteredComponents] = useState(componentList); + const [searchQuery, setSearchQuery] = useState(''); + const [activeTab, setActiveTab] = useState(1); const _shouldFreeze = useStore((state) => state.getShouldFreeze()); const isAutoMobileLayout = useStore((state) => state.currentLayout === 'mobile' && state.getIsAutoMobileLayout()); const shouldFreeze = _shouldFreeze || isAutoMobileLayout; @@ -26,9 +32,14 @@ export const ComponentsManagerTab = ({ darkMode }) => { const handleSearchQueryChange = useCallback( debounce((e) => { const { value } = e.target; - filterComponents(value); + setSearchQuery(value); + + if (activeTab === 1) { + filterComponents(value); + } + // No need to filter modules here as we pass searchQuery to ModuleManager }, 125), - [] + [activeTab] ); const filterComponents = useCallback((value) => { @@ -160,24 +171,48 @@ export const ComponentsManagerTab = ({ darkMode }) => { } } + const handleChangeTab = (tab) => { + setActiveTab(tab); + // When changing tabs, we don't need to reset the search + // The search query will be applied to the new tab + }; + + const renderSection = () => { + if (activeTab === 1) { + return
{segregateSections()}
; + } + return ; + }; + return (
-

Components

+ {isModuleEditor ? ( +

Components

+ ) : ( + + )}
handleSearchQueryChange(e)} onClearCallback={() => { - filterComponents(''); + setSearchQuery(''); + if (activeTab === 1) { + filterComponents(''); + } }} - placeholder={t('globals.searchComponents', 'Search widgets')} - customClass={`tj-widgets-search-input tj-text-xsm`} + placeholder={ + activeTab === 1 + ? t('globals.searchComponents', 'Search widgets') + : t('globals.searchModules', 'Search modules') + } + customClass={`tj-widgets-search-input tj-text-xsm`} showClearButton={false} width={266} />
-
{segregateSections()}
+ {renderSection()}
); }; diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/DragLayer.jsx similarity index 70% rename from frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx rename to frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/DragLayer.jsx index 9589ac145b..0c6f7327af 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/DragLayer.jsx @@ -1,11 +1,18 @@ import React, { useEffect } from 'react'; import { WidgetBox } from '../WidgetBox'; +import { ModuleWidgetBox } from '@/modules/Modules/components'; import { useDrag, useDragLayer } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; import { snapToGrid } from '@/AppBuilder/AppCanvas/appCanvasUtils'; import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants'; +import useStore from '@/AppBuilder/_stores/store'; +import { shallow } from 'zustand/shallow'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; +import { noop } from 'lodash'; -export const DragLayer = ({ index, component }) => { +export const DragLayer = ({ index, component, isModuleTab = false }) => { + const { isModuleEditor } = useModuleContext(); + const setShowModuleBorder = useStore((state) => state.setShowModuleBorder, shallow) || noop; const [{ isDragging }, drag, preview] = useDrag( () => ({ type: 'box', @@ -19,12 +26,25 @@ export const DragLayer = ({ index, component }) => { preview(getEmptyImage(), { captureDraggingState: true }); }, []); + useEffect(() => { + if (isDragging && !isModuleEditor) { + setShowModuleBorder(true); + } else { + setShowModuleBorder(false); + } + }, [isDragging, setShowModuleBorder, isModuleEditor]); + + // const size = isModuleTab + // ? component.module_container.layouts[currentLayout] + // : component.defaultSize || { width: 30, height: 40 }; + const size = component.defaultSize || { width: 30, height: 40 }; + return ( <> {isDragging && } -
- +
+ {isModuleTab ? : }
); diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/constants.js b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/constants.js similarity index 67% rename from frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/constants.js rename to frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/constants.js index 19cfdb7032..778d2b5c66 100644 --- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/constants.js +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/constants.js @@ -7,3 +7,5 @@ export const LEGACY_ITEMS = [ 'Modal', 'TextArea', ]; + +export const IGNORED_ITEMS = ['ModuleContainer', 'ModuleViewer']; diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/index.js b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/index.js new file mode 100644 index 0000000000..6f24ff7948 --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/ComponentManagerTab/index.js @@ -0,0 +1 @@ +export { ComponentsManagerTab as default } from './ComponentsManagerTab'; diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/index.js b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/index.js deleted file mode 100644 index ddb70f8220..0000000000 --- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './ComponentsManagerTab'; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DatetimePickerV2.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DatetimePickerV2.jsx index 8a24e5eb92..665f7b6755 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DatetimePickerV2.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DatetimePickerV2.jsx @@ -9,6 +9,7 @@ import cx from 'classnames'; import useStore from '@/AppBuilder/_stores/store'; import styles from '@/_ui/Select/styles'; import moment from 'moment-timezone'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const DATE_FORMAT_OPTIONS = [ { @@ -101,12 +102,13 @@ const DatetimePickerV2 = ({ componentMeta, componentName, darkMode, ...restProps allComponents, pages, } = restProps; + const { moduleId } = useModuleContext(); const items = []; const additionalActions = []; const properties = []; const formatting = []; const validations = Object.keys(componentMeta.validation || {}); - const resolvedProperties = useStore((state) => state.getResolvedComponent(component.id)?.properties); + const resolvedProperties = useStore((state) => state.getResolvedComponent(component.id, null, moduleId)?.properties); const isDateFormatFxOn = componentMeta?.definition?.properties?.dateFormat?.fxActive || false; const isTimeFormatFxOn = componentMeta?.definition?.properties?.timeFormat?.fxActive || false; const dateFormat = resolvedProperties?.dateFormat ?? resolvedProperties?.format; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx index 346d0bfc17..57da7ec318 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx @@ -6,7 +6,7 @@ import { renderElement } from '../Utils'; import i18next from 'i18next'; import { resolveReferences } from '@/_helpers/utils'; // import { AllComponents } from '@/Editor/Box'; -import { AllComponents } from '@/_helpers/editorHelpers'; +import { AllComponents } from '@/AppBuilder/_helpers/editorHelpers'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx index c74e023c0b..f2b0ff7594 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Elements/Code.jsx @@ -20,14 +20,23 @@ export const Code = ({ placeholder, validationFn, isHidden = false, + customMeta, }) => { const currentState = useCurrentState(); - let initialValue = !_.isEmpty(definition) - ? definition.value - : getDefinitionInitialValue(paramType, param.name, component, currentState, definition.value); + function getInitialValue() { + if (customMeta && customMeta.defaultValue) { + return customMeta.defaultValue; + } + return !_.isEmpty(definition) + ? definition.value + : getDefinitionInitialValue(paramType, param.name, component, currentState, definition.value); + } - const paramMeta = accordian ? componentMeta[paramType]?.[param.name] : componentMeta[paramType][param.name]; + let initialValue = getInitialValue(); + const paramMeta = accordian + ? customMeta ?? componentMeta[paramType]?.[param.name] + : customMeta ?? componentMeta[paramType][param.name]; const displayName = paramMeta.displayName || param.name; function handleCodeChanged(value) { diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx index e1c6bcf5c8..a5f5bb2202 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx @@ -30,6 +30,7 @@ import { appService } from '@/_services'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; import useStore from '@/AppBuilder/_stores/store'; import { useEventActions, useEvents } from '@/AppBuilder/_stores/slices/eventsSlice'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup'; import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem'; import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver'; @@ -48,10 +49,13 @@ export const EventManager = ({ customEventRefs = undefined, component, }) => { + const { moduleId, isModuleEditor } = useModuleContext(); const components = useStore((state) => state.getCurrentPageComponents()); const pages = useStore((state) => _.get(state, 'modules.canvas.pages', []), shallow).filter( (page) => !page.disabled && !page.isPageGroup ); + const moduleInputDummyQueries = useStore((state) => state?.getModuleInputDummyQueries?.(), shallow) || {}; + const dataQueries = useStore((state) => { const queries = state.dataQuery?.queries?.modules?.canvas || []; if (callerQueryId) { @@ -62,7 +66,7 @@ export const EventManager = ({ const allAppEvents = useEvents(); const { createAppVersionEventHandlers, deleteAppVersionEventHandler, updateAppVersionEventHandlers } = useEventActions(); - const appId = useStore((state) => state.app.appId); + const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); const eventsUpdatedLoader = useStore((state) => state.eventsSlice.getEventsUpdatedLoader(), shallow); const eventsCreatedLoader = useStore((state) => state.eventsSlice.getEventsCreatedLoader(), shallow); @@ -102,9 +106,9 @@ export const EventManager = ({ return a.index - b.index; }); - setEvents(sortedEvents || []); + setEvents(sortedEvents || [], moduleId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(currentEvents)]); + }, [JSON.stringify(currentEvents), moduleId]); let groupedOptions = ActionTypes.reduce((acc, action) => { const groupName = action.group; @@ -454,6 +458,11 @@ export const EventManager = ({ return defaultValue; }; + const constructDataQueryOptions = () => { + const queries = dataQueries.filter((qry) => isQueryRunnable(qry)).map((qry) => ({ name: qry.name, value: qry.id })); + const moduleInputs = Object.entries(moduleInputDummyQueries).map(([key, value]) => ({ name: value, value: key })); + return [...moduleInputs, ...queries]; + }; const formatGroupLabel = (data) => { if (data.label === 'run-action') return; return ( @@ -687,27 +696,34 @@ export const EventManager = ({
setNewComponentName(e.target.value)} + type="text" + onBlur={() => handleComponentNameChange(newComponentName)} + className="w-100 inspector-edit-widget-name" + value={newComponentName} + ref={inputRef} + data-cy="edit-widget-name" + /> +
+ ); + }; + + const renderTabs = () => ( + + ); + return ( -
+
clearSelectedComponents()}> @@ -468,68 +502,50 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
-
-
- setNewComponentName(e.target.value)} - type="text" - onBlur={() => handleComponentNameChange(newComponentName)} - className="w-100 inspector-edit-widget-name" - value={newComponentName} - ref={inputRef} - data-cy="edit-widget-name" - /> -
-
-
- - - {INSPECTOR_HEADER_OPTIONS.map((option) => ( -
{ - e.stopPropagation(); - handleInspectorHeaderActions(option.value); - }} - > -
{option.icon}
+
{renderAppNameInput()}
+ {!isModuleContainer && ( +
+ + + {INSPECTOR_HEADER_OPTIONS.map((option) => (
{ + e.stopPropagation(); + handleInspectorHeaderActions(option.value); + }} > - {option?.label} +
{option.icon}
+
+ {option?.label} +
-
- ))} - - - } - > - setShowHeaderActionsMenu(true)}> - - - -
-
-
- - - {propertiesTab} - - - {stylesTab} - - + ))} + + + } + > + setShowHeaderActionsMenu(true)}> + + + +
+ )}
+ +
{renderTabs()}
@@ -749,6 +765,12 @@ const GetAccordion = React.memo( case 'CurrencyInput': return ; + case 'ModuleContainer': + return ; + + case 'ModuleViewer': + return ; + default: { return ; } diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js index 7e47f7f6c0..a9b981eb1a 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js @@ -28,13 +28,14 @@ export function renderCustomStyles( components = {}, accordian, darkMode = false, - placeholder = '' + placeholder = '', + customMeta ) { const componentConfig = component.component; const componentDefinition = componentConfig.definition; const paramTypeDefinition = componentDefinition[paramType] || {}; const definition = paramTypeDefinition[param] || {}; - const meta = componentMeta[paramType]?.[accordian]?.[param]; + const meta = customMeta ?? componentMeta[paramType]?.[accordian]?.[param]; if ( componentConfig.component == 'DropDown' || @@ -95,7 +96,7 @@ export function renderCustomStyles( return ( <> ); @@ -128,7 +130,8 @@ export function renderElement( components = {}, darkMode = false, placeholder = '', - validationFn + validationFn, + customMeta ) { const componentConfig = component.component; const componentDefinition = componentConfig.definition; @@ -158,7 +161,7 @@ export function renderElement( return ( ); } diff --git a/frontend/src/AppBuilder/RightSideBar/RightSideBar.jsx b/frontend/src/AppBuilder/RightSideBar/RightSideBar.jsx index 88ea0f477a..3322b9e143 100644 --- a/frontend/src/AppBuilder/RightSideBar/RightSideBar.jsx +++ b/frontend/src/AppBuilder/RightSideBar/RightSideBar.jsx @@ -1,11 +1,13 @@ import React from 'react'; import useStore from '@/AppBuilder/_stores/store'; import { ComponentConfigurationTab } from './ComponentConfigurationTab'; -import { ComponentsManagerTab } from './ComponentsManagerTab'; +import ComponentsManagerTab from './ComponentManagerTab'; import cx from 'classnames'; import { PageSettings } from './PageSettingsTab'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const RightSideBar = ({ darkMode }) => { + const { isModuleEditor } = useModuleContext(); const activeTab = useStore((state) => state.activeRightSideBarTab); const pageSettingSelected = useStore((state) => state.pageSettingSelected); @@ -15,9 +17,9 @@ export const RightSideBar = ({ darkMode }) => {
{pageSettingSelected && } {activeTab === 'components' ? ( - + ) : ( - + )}
diff --git a/frontend/src/AppBuilder/Viewer/MobileNavigationMenu.jsx b/frontend/src/AppBuilder/Viewer/MobileNavigationMenu.jsx index bf4b6b50b4..0862634c77 100644 --- a/frontend/src/AppBuilder/Viewer/MobileNavigationMenu.jsx +++ b/frontend/src/AppBuilder/Viewer/MobileNavigationMenu.jsx @@ -8,11 +8,13 @@ import Cross from '@/_ui/Icon/solidIcons/Cross'; import useStore from '@/AppBuilder/_stores/store'; import { buildTree } from '../LeftSidebar/PageMenu/Tree/utilities'; import * as Icons from '@tabler/icons-react'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const RenderGroup = ({ pages, pageGroup, currentPage, darkMode, handlepageSwitch, currentPageId, icon }) => { + const { moduleId } = useModuleContext(); const [isExpanded, setIsExpanded] = useState(true); const groupActive = currentPage.pageGroupId === pageGroup?.id; - const homePageId = useStore((state) => state.app.homePageId); + const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId); const handleToggle = () => { setIsExpanded(!isExpanded); }; @@ -78,8 +80,9 @@ const RenderGroup = ({ pages, pageGroup, currentPage, darkMode, handlepageSwitch }; const RenderPageGroups = ({ pages, handlepageSwitch, darkMode, currentPageId, currentPage }) => { + const { moduleId } = useModuleContext(); const tree = buildTree(pages); - const homePageId = useStore((state) => state.app.homePageId); + const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId); return (
@@ -123,6 +126,7 @@ const RenderPageGroups = ({ pages, handlepageSwitch, darkMode, currentPageId, cu }; const MobileNavigationMenu = ({ pages, switchPage, currentPageId, darkMode, changeToDarkMode, showDarkModeToggle }) => { + const { moduleId } = useModuleContext(); const selectedVersionName = useStore((state) => state.selectedVersion?.name); const selectedEnvironmentName = useStore((state) => state.selectedEnvironment?.name); const license = useStore((state) => state.license); @@ -134,7 +138,7 @@ const MobileNavigationMenu = ({ pages, switchPage, currentPageId, darkMode, chan version: selectedVersionName, env: selectedEnvironmentName, }; - switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams)); + switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams), moduleId); }; var styles = { bmBurgerButton: { @@ -189,7 +193,7 @@ const MobileNavigationMenu = ({ pages, switchPage, currentPageId, darkMode, chan const isLicensed = !_.get(license, 'featureAccess.licenseStatus.isExpired', true) && _.get(license, 'featureAccess.licenseStatus.isLicenseValid', false); - const homePageId = useStore((state) => state.app.homePageId); + const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId); return ( <> diff --git a/frontend/src/AppBuilder/Viewer/PageGroup.jsx b/frontend/src/AppBuilder/Viewer/PageGroup.jsx index 0311115b09..9948651c43 100644 --- a/frontend/src/AppBuilder/Viewer/PageGroup.jsx +++ b/frontend/src/AppBuilder/Viewer/PageGroup.jsx @@ -8,11 +8,10 @@ import useStore from '@/AppBuilder/_stores/store'; import { buildTree } from '../LeftSidebar/PageMenu/Tree/utilities'; import OverflowTooltip from '@/_components/OverflowTooltip'; import cx from 'classnames'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const RenderPage = ({ page, currentPageId, switchPageWrapper, labelStyle, computeStyles, darkMode, homePageId }) => { const isHomePage = page.id === homePageId; - console.log({ page, homePageId }); - console.log({ isHomePage }); const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon; const IconElement = Icons?.[iconName] ?? Icons?.['IconFileDescription']; return (page.hidden || page.disabled) && page?.restricted ? null : ( @@ -141,11 +140,12 @@ const RenderPageGroup = ({ export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkMode, switchPageWrapper }) => { // Don't render empty folders if displaying only icons + const { moduleId } = useModuleContext(); const tree = buildTree(pages, !!labelStyle?.label?.hidden); const filteredPages = tree.filter((page) => (!page?.isPageGroup || page.children?.length > 0) && !page?.restricted); const currentPageId = useStore((state) => state.currentPageId); const currentPage = pages.find((page) => page.id === currentPageId); - const homePageId = useStore((state) => state.app.homePageId); + const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId); return (
{/* page.id)}> */} diff --git a/frontend/src/AppBuilder/Viewer/PreviewSettings.jsx b/frontend/src/AppBuilder/Viewer/PreviewSettings.jsx index 633a6dcf0e..e914a9bee4 100644 --- a/frontend/src/AppBuilder/Viewer/PreviewSettings.jsx +++ b/frontend/src/AppBuilder/Viewer/PreviewSettings.jsx @@ -9,8 +9,10 @@ import HeaderActions from '@/AppBuilder/Header/HeaderActions'; import { AppEnvironments } from '@/modules/Appbuilder/components'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { useAppType } from '@/AppBuilder/_contexts/ModuleContext'; const PreviewSettings = ({ isMobileLayout, showHeader, darkMode }) => { + const appType = useAppType(); const { setShowUndoRedoBtn, editingVersion } = useStore( (state) => ({ setShowUndoRedoBtn: state?.setShowUndoRedoBtn, @@ -32,14 +34,14 @@ const PreviewSettings = ({ isMobileLayout, showHeader, darkMode }) => { Preview settings - {editingVersion && ( + {editingVersion && appType !== 'module' && ( <>
)} - +
@@ -85,13 +87,17 @@ const PreviewSettings = ({ isMobileLayout, showHeader, darkMode }) => { {previewNavbar && ( - - - -
- - - + {appType !== 'module' && ( + <> + + + +
+ + + + + )}
{ +export const Viewer = ({ + id: appId, + darkMode, + moduleId = 'canvas', + switchDarkMode, + environmentId, + versionId, + moduleMode = false, +} = {}) => { const DEFAULT_CANVAS_WIDTH = 1292; const { t } = useTranslation(); const [isSidebarPinned, setIsSidebarPinned] = useState(localStorage.getItem('isPagesSidebarPinned') !== 'false'); - useAppData(appId, moduleId, darkMode, 'view', { environmentId, versionId }); + const appType = useAppData(appId, moduleId, darkMode, 'view', { environmentId, versionId }, moduleMode); const { isEditorLoading, @@ -41,46 +50,49 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod toggleCurrentLayout, } = useStore( (state) => ({ - isEditorLoading: state.isEditorLoading, - currentMode: state.currentMode, + isEditorLoading: state.loaderStore.modules[moduleId].isEditorLoading, + currentMode: state.modeStore.modules[moduleId].currentMode, currentLayout: state.currentLayout, editingVersion: state.editingVersion, selectedVersion: state.selectedVersion, currentCanvasWidth: state.currentCanvasWidth, - appName: state.app.appName, - homePageId: state?.app.homepageId, - currentPageId: state.currentPageId, + appName: state.appStore.modules[moduleId].app.appName, + homePageId: state.appStore.modules[moduleId].app.homepageId, + currentPageId: state.modules[moduleId].currentPageId, globalSettings: state.globalSettings, - pages: state.modules.canvas.pages, + pages: state.modules[moduleId].pages, modules: state.modules, globalSettingsChanged: state.globalSettingsChanged, pageSettings: state.pageSettings, updateCanvasHeight: state.updateCanvasBottomHeight, - isMaintenanceOn: state.app.isMaintenanceOn, + isMaintenanceOn: state.appStore.modules[moduleId].app.isMaintenanceOn, setIsViewer: state.setIsViewer, toggleCurrentLayout: state.toggleCurrentLayout, }), shallow ); - const getCurrentPageComponents = useStore((state) => state.getCurrentPageComponents(), shallow); + + const getCurrentPageComponents = useStore((state) => state.getCurrentPageComponents(moduleId), shallow); const currentPageComponents = useMemo(() => getCurrentPageComponents, [getCurrentPageComponents]); const changeDarkMode = useStore((state) => state.changeDarkMode); const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow); const canvasBgColor = useStore((state) => state.getCanvasBackgroundColor('canvas', darkMode), shallow); const deviceWindowWidth = window.screen.width - 5; + const hideSidebar = moduleMode || isPagesSidebarHidden || appType === 'module'; + const computeCanvasMaxWidth = useCallback(() => { if (globalSettings?.maxCanvasWidth) { return globalSettings.maxCanvasWidth; } if (globalSettings?.canvasMaxWidthType === 'px') { - return (+globalSettings?.canvasMaxWidth || DEFAULT_CANVAS_WIDTH) - (!isPagesSidebarHidden ? 200 : 0); + return (+globalSettings?.canvasMaxWidth || DEFAULT_CANVAS_WIDTH) - (!hideSidebar ? 200 : 0); } if (globalSettings?.canvasMaxWidthType === '%') { return +globalSettings?.canvasMaxWidth + '%'; } return DEFAULT_CANVAS_WIDTH; - }, [globalSettings, isPagesSidebarHidden]); + }, [globalSettings, hideSidebar]); const toggleSidebarPinned = useCallback(() => { const newValue = !isSidebarPinned; @@ -104,8 +116,8 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod }, []); useEffect(() => { - updateCanvasHeight(currentPageComponents); - }, [currentPageComponents, updateCanvasHeight]); + updateCanvasHeight(currentPageComponents, moduleId); + }, [currentPageComponents, moduleId, updateCanvasHeight]); const changeToDarkMode = (newMode) => { switchDarkMode(newMode); @@ -113,16 +125,56 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod useEffect(() => { const isMobileDevice = deviceWindowWidth < 600; toggleCurrentLayout(isMobileDevice ? 'mobile' : 'desktop'); - setIsViewer(true); + setIsViewer(true, moduleId); return () => { - setIsViewer(false); + setIsViewer(false, moduleId); }; }, []); + const renderHeader = () => { + if (moduleMode) { + return null; + } + + if (currentLayout !== 'mobile') { + return ( + + ); + } + + return ( + <> + {currentLayout === 'mobile' && !isMobilePreviewMode && ( + + )} + + ); + }; + if (isEditorLoading) { return ( -
- +
+ {moduleMode ? : }
); } else if (isMaintenanceOn) { @@ -144,43 +196,18 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod className={cx('viewer wrapper', { 'mobile-layout': currentLayout, 'theme-dark dark-theme': darkMode })} > - - {currentLayout !== 'mobile' && ( - - )} - {currentLayout === 'mobile' && !isMobilePreviewMode && ( - - )} + + {renderHeader()}
- {currentLayout !== 'mobile' && !isPagesSidebarHidden && ( + {currentLayout !== 'mobile' && !hideSidebar && !moduleMode && (
- {currentLayout === 'mobile' && isMobilePreviewMode && ( + {currentLayout === 'mobile' && isMobilePreviewMode && !moduleMode && ( )} - +
- {/* {!licenseValid && isAppLoaded && } */} {isMobilePreviewMode &&
} {isMobilePreviewMode &&
}
@@ -238,11 +270,11 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
-
- +
+ - -
+ +
); } }; diff --git a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx index 5312d60a8a..6a64c6766b 100644 --- a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx +++ b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx @@ -9,6 +9,7 @@ import useStore from '@/AppBuilder/_stores/store'; import { APP_HEADER_HEIGHT } from '../AppCanvas/appCanvasConstants'; import OverflowTooltip from '@/_components/OverflowTooltip'; import { RenderPageAndPageGroup } from './PageGroup'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const ViewerSidebarNavigation = ({ isMobileDevice, @@ -20,10 +21,11 @@ export const ViewerSidebarNavigation = ({ isSidebarPinned, toggleSidebarPinned, }) => { + const { moduleId } = useModuleContext(); const { definition: { styles = {}, properties = {} } = {} } = useStore((state) => state.pageSettings) || {}; const selectedVersionName = useStore((state) => state.selectedVersion?.name); const selectedEnvironmentName = useStore((state) => state.selectedEnvironment?.name); - const homePageId = useStore((state) => state.app.homePageId); + const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId); const license = useStore((state) => state.license); if (isMobileDevice) { @@ -95,7 +97,7 @@ export const ViewerSidebarNavigation = ({ version: selectedVersionName, env: selectedEnvironmentName, }; - switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams)); + switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams), moduleId); }; const isLicensed = diff --git a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js index 136ef942c7..21c68480ea 100644 --- a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js +++ b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js @@ -58,6 +58,8 @@ import { linkConfig, iconConfig, boundedBoxConfig, + moduleContainerConfig, + moduleViewerConfig, emailinputConfig, phoneinputConfig, currencyinputConfig, @@ -126,4 +128,6 @@ export const widgets = [ linkConfig, iconConfig, boundedBoxConfig, + moduleContainerConfig, + moduleViewerConfig, ]; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/index.js b/frontend/src/AppBuilder/WidgetManager/widgets/index.js index e79bee68c2..d387e6230d 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/index.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/index.js @@ -58,6 +58,8 @@ import { kanbanBoardConfig } from './kanbanBoard'; import { datetimePickerV2Config } from './datetimepickerV2'; import { datePickerV2Config } from './datepickerV2'; import { timePickerConfig } from './timepicker'; +import { moduleContainerConfig } from './moduleContainer'; +import { moduleViewerConfig } from './moduleViewer'; import { emailinputConfig } from './emailinput'; import { phoneinputConfig } from './phoneinput'; import { currencyinputConfig } from './currencyinput'; @@ -126,4 +128,6 @@ export { linkConfig, iconConfig, boundedBoxConfig, + moduleContainerConfig, + moduleViewerConfig, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/moduleContainer.js b/frontend/src/AppBuilder/WidgetManager/widgets/moduleContainer.js new file mode 100644 index 0000000000..bf488095a6 --- /dev/null +++ b/frontend/src/AppBuilder/WidgetManager/widgets/moduleContainer.js @@ -0,0 +1,36 @@ +export const moduleContainerConfig = { + name: 'ModuleContainer', + displayName: 'Module Container', + description: 'Module Container', + component: 'ModuleContainer', + defaultSize: { + width: 10, + height: 400, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: { + inputItems: { type: 'array', displayName: 'Input' }, + outputItems: { type: 'array', displayName: 'Output' }, + }, + events: {}, + styles: {}, + exposedVariables: {}, + actions: [], + definition: { + others: { + showOnDesktop: { value: '{{true}}' }, + showOnMobile: { value: '{{false}}' }, + }, + properties: { + inputItems: { value: [] }, + outputItems: { value: [] }, + }, + events: [], + styles: { + backgroundColor: { value: '#fff' }, + }, + }, +}; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/moduleViewer.js b/frontend/src/AppBuilder/WidgetManager/widgets/moduleViewer.js new file mode 100644 index 0000000000..b5d2962cde --- /dev/null +++ b/frontend/src/AppBuilder/WidgetManager/widgets/moduleViewer.js @@ -0,0 +1,30 @@ +export const moduleViewerConfig = { + name: 'ModuleViewer', + displayName: 'Module', + description: 'Module', + component: 'ModuleViewer', + defaultSize: { + width: 10, + height: 400, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: {}, + events: {}, + styles: {}, + exposedVariables: {}, + actions: [], + definition: { + others: { + showOnDesktop: { value: '{{true}}' }, + showOnMobile: { value: '{{false}}' }, + }, + properties: {}, + events: [], + styles: { + backgroundColor: { value: '#fff' }, + }, + }, +}; diff --git a/frontend/src/AppBuilder/Widgets/Kanban/Components/Modal.jsx b/frontend/src/AppBuilder/Widgets/Kanban/Components/Modal.jsx index 3837051886..0acefe2053 100644 --- a/frontend/src/AppBuilder/Widgets/Kanban/Components/Modal.jsx +++ b/frontend/src/AppBuilder/Widgets/Kanban/Components/Modal.jsx @@ -6,8 +6,10 @@ import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; // eslint-disable-next-line import/no-unresolved import { diff } from 'deep-object-diff'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const Modal = function Modal({ darkMode, showModal, setShowModal, kanbanProps, lastSelectedCard }) { + const { moduleId } = useModuleContext(); const updateCustomResolvables = useStore((state) => state.updateCustomResolvables, shallow); const parentRef = useRef(null); const { id, containerProps, component } = kanbanProps; @@ -17,7 +19,7 @@ export const Modal = function Modal({ darkMode, showModal, setShowModal, kanbanP if (Object.keys(diff(lastSelectedCard, prevLastSelectedCard.current)).length > 0) { prevLastSelectedCard.current = lastSelectedCard; // Update the customResolvables with the lastSelectedCard - updateCustomResolvables(`${id}-modal`, [{ cardData: lastSelectedCard }], 'cardData'); + updateCustomResolvables(`${id}-modal`, [{ cardData: lastSelectedCard }], 'cardData', moduleId); } const renderCloseButton = () => { diff --git a/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx b/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx index 8aa7b578ac..90fd261622 100644 --- a/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx +++ b/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx @@ -26,6 +26,7 @@ import cx from 'classnames'; import { useGridStore } from '@/_stores/gridStore'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const dropAnimation = { sideEffects: defaultDropAnimationSideEffects({ @@ -40,11 +41,12 @@ const dropAnimation = { const TRASH_ID = 'void'; export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) { + const { moduleId } = useModuleContext(); const updateCustomResolvables = useStore((state) => state.updateCustomResolvables, shallow); const { properties, fireEvent, setExposedVariable, setExposedVariables, styles } = kanbanProps; const { columnData, cardData, cardWidth, cardHeight, showDeleteButton, enableAddCard } = properties; const { accentColor } = styles; - const mode = useStore((state) => state.currentMode, shallow); + const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); const [lastSelectedCard, setLastSelectedCard] = useState({}); // eslint-disable-next-line react-hooks/exhaustive-deps const columnDataAsObj = useMemo(() => convertArrayToObj(columnData), [JSON.stringify(columnData)]); @@ -78,7 +80,8 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) { updateCustomResolvables( id, flatCardData.map((d) => ({ cardData: d })), - 'cardData' + 'cardData', + moduleId ); }, []); diff --git a/frontend/src/AppBuilder/Widgets/Listview.jsx b/frontend/src/AppBuilder/Widgets/Listview.jsx index 807e62ecb2..e08735eb2c 100644 --- a/frontend/src/AppBuilder/Widgets/Listview.jsx +++ b/frontend/src/AppBuilder/Widgets/Listview.jsx @@ -9,6 +9,7 @@ import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; import { diff } from 'deep-object-diff'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const Listview = function Listview({ id, @@ -21,8 +22,9 @@ export const Listview = function Listview({ darkMode, dataCy, }) { + const { moduleId } = useModuleContext(); const getComponentNameFromId = useStore((state) => state.getComponentNameFromId, shallow); - const childComponents = useStore((state) => state.getChildComponents(id), shallow); + const childComponents = useStore((state) => state.getChildComponents(id, moduleId), shallow); const updateCustomResolvables = useStore((state) => state.updateCustomResolvables, shallow); const fallbackProperties = { height: 100, showBorder: false, data: [] }; const fallbackStyles = { visibility: true, disabledState: false }; @@ -71,7 +73,7 @@ export const Listview = function Listview({ const onOptionChange = useCallback( (optionName, value, componentId, index) => { setChildrenData((prevData) => { - const componentName = getComponentNameFromId(componentId); + const componentName = getComponentNameFromId(componentId, moduleId); const changedData = { [componentName]: { [optionName]: value } }; const existingDataAtIndex = prevData[index] ?? {}; const newDataAtIndex = { @@ -86,13 +88,13 @@ export const Listview = function Listview({ return { ...prevData, ...newChildrenData }; }); }, - [getComponentNameFromId, setChildrenData] + [getComponentNameFromId, setChildrenData, moduleId] ); const onOptionsChange = useCallback( (exposedVariables, componentId, index) => { setChildrenData((prevData) => { - const componentName = getComponentNameFromId(componentId); + const componentName = getComponentNameFromId(componentId, moduleId); const existingDataAtIndex = prevData[index] ?? {}; const changedData = {}; Object.keys(exposedVariables).forEach((key) => { @@ -110,7 +112,7 @@ export const Listview = function Listview({ return { ...prevData, ...newChildrenData }; }); }, - [getComponentNameFromId, setChildrenData] + [getComponentNameFromId, setChildrenData, moduleId] ); function onRecordOrRowClicked(index) { @@ -233,7 +235,7 @@ export const Listview = function Listview({ }; }); // Update the customResolvables with the new listItems - if (listItems.length > 0) updateCustomResolvables(id, listItems, 'listItem'); + if (listItems.length > 0) updateCustomResolvables(id, listItems, 'listItem', moduleId); } return (
state.currentMode, shallow); + const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas); /**** Start - Logic to reset the zIndex of modal control box ****/ diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx b/frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx index bfe1431ad0..01c07cf49f 100644 --- a/frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx +++ b/frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx @@ -16,6 +16,7 @@ import { createModalStyles } from '@/AppBuilder/Widgets/ModalV2/helpers/stylesFa import { onShowSideEffects, onHideSideEffects } from '@/AppBuilder/Widgets/ModalV2/helpers/sideEffects'; import '@/AppBuilder/Widgets/ModalV2/style.scss'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const ModalV2 = function Modal({ id, @@ -29,6 +30,7 @@ export const ModalV2 = function Modal({ dataCy, height, }) { + const { moduleId } = useModuleContext(); const [showModal, setShowModal] = useState(false); const { closeOnClickingOutside = false, @@ -56,7 +58,7 @@ export const ModalV2 = function Modal({ const titleAlignment = properties.titleAlignment ?? 'left'; const size = properties.size ?? 'lg'; const setSelectedComponentAsModal = useStore((state) => state.setSelectedComponentAsModal, shallow); - const mode = useStore((state) => state.currentMode, shallow); + const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); const computedModalBodyHeight = getModalBodyHeight(modalHeight, showHeader, showFooter, headerHeight, footerHeight); const headerHeightPx = getModalHeaderHeight(showHeader, headerHeight); diff --git a/frontend/src/AppBuilder/Widgets/NewTable/Table.jsx b/frontend/src/AppBuilder/Widgets/NewTable/Table.jsx index c5f1328aee..7615f9f585 100644 --- a/frontend/src/AppBuilder/Widgets/NewTable/Table.jsx +++ b/frontend/src/AppBuilder/Widgets/NewTable/Table.jsx @@ -9,9 +9,11 @@ import useTableStore from './_stores/tableStore'; import TableContainer from './_components/TableContainer'; import { transformTableData } from './_utils/transformTableData'; import { usePrevious } from '@dnd-kit/utilities'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; export const Table = memo( ({ id, componentName, width, height, properties, styles, darkMode, fireEvent, setExposedVariables }) => { + const { moduleId } = useModuleContext(); // get table store functions const initializeComponent = useTableStore((state) => state.initializeComponent, shallow); const removeComponent = useTableStore((state) => state.removeComponent, shallow); @@ -83,7 +85,8 @@ export const Table = memo( firstRowOfTable, autogenerateColumns, columnDeletionHistory, - shouldAutogenerateColumns.current + shouldAutogenerateColumns.current, + moduleId ); shouldAutogenerateColumns.current = false; }, [ @@ -94,6 +97,7 @@ export const Table = memo( setColumnDetails, firstRowOfTable, autogenerateColumns, + moduleId, columnDeletionHistory, ]); diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/TableExposedVariables.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/TableExposedVariables.jsx index 6ec4f66994..93f0721366 100644 --- a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/TableExposedVariables.jsx +++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/TableExposedVariables.jsx @@ -7,6 +7,7 @@ import { filterFunctions } from '../Header/_components/Filter/filterUtils'; import { isArray, debounce } from 'lodash'; import { useMounted } from '@/_hooks/use-mount'; import { usePrevious } from '@dnd-kit/utilities'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; // Component to expose variables & fire events from the table // It might miss some variables which are tightly coupled with the component state export const TableExposedVariables = ({ @@ -19,6 +20,7 @@ export const TableExposedVariables = ({ pageIndex = 1, lastClickedRow, }) => { + const { moduleId } = useModuleContext(); const editedRows = useTableStore((state) => state.getAllEditedRows(id), shallow); const editedFields = useTableStore((state) => state.getAllEditedFields(id), shallow); const addNewRowDetails = useTableStore((state) => state.getAllAddNewRowDetails(id), shallow); @@ -314,7 +316,7 @@ export const TableExposedVariables = ({ // Create debounced function using useRef to persist between renders const debouncedSetProperty = useRef( debounce((sizing) => { - setComponentProperty(id, 'columnSizes', sizing, 'properties'); + setComponentProperty(id, 'columnSizes', sizing, 'properties', 'value', false, moduleId); }, 300) ).current; diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/columnSlice.js b/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/columnSlice.js index 6663f4f2f9..2ea3a69bc5 100644 --- a/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/columnSlice.js +++ b/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/columnSlice.js @@ -14,7 +14,8 @@ export const createColumnSlice = (set, get) => ({ firstRowOfTable, autogenerateColumnsFlag, columnDeletionHistory, - shouldAutogenerateColumns + shouldAutogenerateColumns, + moduleId = 'canvas' ) => { set( (state) => { @@ -33,7 +34,8 @@ export const createColumnSlice = (set, get) => ({ isDynamicColumnSelected, autogenerateColumnsFlag, columnDeletionHistory, - columnData + columnData, + moduleId ); state.components[id].columnDetails.columnProperties = columnProperties; state.components[id].columnDetails.transformations = get().generateColumnTransformations( @@ -60,7 +62,8 @@ export const createColumnSlice = (set, get) => ({ isDynamicColumnSelected, autogenerateColumnsFlag, columnDeletionHistory, - columnData + columnData, + moduleId ) => { if (autogenerateColumnsFlag) { const setComponentProperty = useStore.getState().setComponentProperty; @@ -75,7 +78,7 @@ export const createColumnSlice = (set, get) => ({ ); if (!isDynamicColumnSelected && !isEqual(existingGeneratedColumn, generatedColumns)) { - setComponentProperty(id, 'columns', generatedColumns, 'properties', 'value', false, 'canvas', { + setComponentProperty(id, 'columns', generatedColumns, 'properties', 'value', false, moduleId, { skipUndoRedo: true, saveAfterAction: true, }); diff --git a/frontend/src/AppBuilder/Widgets/Table/Table.jsx b/frontend/src/AppBuilder/Widgets/Table/Table.jsx index 7d378e7d70..5921fd0e34 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Table.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Table.jsx @@ -49,6 +49,7 @@ import { EmptyState } from './Components/EmptyState'; import { LoadingState } from './Components/LoadingState'; // eslint-disable-next-line import/no-unresolved import { useVirtualizer } from '@tanstack/react-virtual'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; // utilityForNestedNewRow function is used to construct nested object while adding or updating new row when '.' is present in column key for adding new row const utilityForNestedNewRow = (row) => { @@ -87,11 +88,12 @@ export const Table = React.memo( // events, // setProperty, }) => { - const component = useStore((state) => state.getComponentDefinition(id), shallow); + const { moduleId } = useModuleContext(); + const component = useStore((state) => state.getComponentDefinition(id, moduleId), shallow); const exposedNewRows = useStore((state) => state.getExposedValueOfComponent(id)?.newRows || [], shallow); const validateWidget = useStore((state) => state.validateWidget, shallow); const validateDates = useStore((state) => state.validateDates, shallow); - const mode = useStore((state) => state.currentMode); + const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode); const { color, diff --git a/frontend/src/AppBuilder/_contexts/ModuleContext.jsx b/frontend/src/AppBuilder/_contexts/ModuleContext.jsx index 1c8e64a843..d0f2b96c31 100644 --- a/frontend/src/AppBuilder/_contexts/ModuleContext.jsx +++ b/frontend/src/AppBuilder/_contexts/ModuleContext.jsx @@ -2,16 +2,51 @@ import React, { createContext, useContext } from 'react'; export const ModuleContext = createContext(); -export const ModuleProvider = ({ moduleId, children }) => { - return {children}; +export const ModuleProvider = ({ moduleId, isModuleMode, appType, isModuleEditor, children }) => { + return ( + + {children} + + ); +}; + +export const useModuleContext = () => { + const context = useContext(ModuleContext); + if (!context) { + throw new Error('useModuleContext must be used within a ModuleProvider'); + } + return context; }; export const useModuleId = () => { const context = useContext(ModuleContext); - if (!context) { throw new Error('useModuleId must be used within a ModuleProvider'); } - return context; + return context.moduleId; +}; + +export const useIsModuleMode = () => { + const context = useContext(ModuleContext); + if (!context) { + throw new Error('useIsModuleMode must be used within a ModuleProvider'); + } + return context.isModuleMode; +}; + +export const useAppType = () => { + const context = useContext(ModuleContext); + if (!context) { + throw new Error('useAppType must be used within a ModuleProvider'); + } + return context.appType; +}; + +export const useIsModuleEditor = () => { + const context = useContext(ModuleContext); + if (!context) { + throw new Error('useIsModuleEditor must be used within a ModuleProvider'); + } + return context.isModuleEditor; }; diff --git a/frontend/src/AppBuilder/_helpers/editorHelpers.js b/frontend/src/AppBuilder/_helpers/editorHelpers.js index 183c5c1cec..f6c00e60a0 100644 --- a/frontend/src/AppBuilder/_helpers/editorHelpers.js +++ b/frontend/src/AppBuilder/_helpers/editorHelpers.js @@ -71,6 +71,9 @@ import { Form } from '@/AppBuilder/Widgets/Form/Form'; import { Modal } from '@/AppBuilder/Widgets/Modal'; import { ModalV2 } from '@/AppBuilder/Widgets/ModalV2/ModalV2'; import { Calendar } from '@/AppBuilder/Widgets/Calendar/Calendar'; + +import { ModuleContainer, ModuleViewer } from '@/modules/Modules/components'; + // import './requestIdleCallbackPolyfill'; export function memoizeFunction(func) { @@ -152,6 +155,8 @@ export const AllComponents = { Form, BoundedBox, ToggleSwitchV2, + ModuleContainer, + ModuleViewer, }; if (isPDFSupported()) { AllComponents.PDF = await import('@/Editor/Components/PDF').then((module) => module.PDF); diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index 433a677dc7..f1331e5768 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { appEnvironmentService, appService, + appsService, appVersionService, dataqueryService, datasourceService, @@ -27,6 +28,7 @@ import { distinctUntilChanged } from 'rxjs'; import { baseTheme, convertAllKeysToSnakeCase } from '../_stores/utils'; import { getPreviewQueryParams } from '@/_helpers/routes'; import { useLocation, useMatch, useParams } from 'react-router-dom'; +import { useMounted } from '@/_hooks/use-mount'; import useThemeAccess from './useThemeAccess'; import { handleError } from '@/_helpers/handleAppAccess'; import toast from 'react-hot-toast'; @@ -56,12 +58,22 @@ const normalizeQueryTransformationOptions = (query) => { return query; }; -const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, versionId } = {}) => { +const useAppData = ( + appId, + moduleId, + darkMode, + mode = 'edit', + { environmentId, versionId } = {}, + moduleMode = false +) => { + const mounted = useMounted(); + const initModules = useStore((state) => state.initModules, shallow); + moduleMode && !mounted && initModules(moduleId); const { state } = useLocation(); const [currentSession, setCurrentSession] = useState(); const setEditorLoading = useStore((state) => state.setEditorLoading); const setApp = useStore((state) => state.setApp); - const app = useStore((state) => state.app); + const app = useStore((state) => state.appStore.modules[moduleId].app); const user = useStore((state) => state.user); const setCurrentVersionId = useStore((state) => state.setCurrentVersionId); const currentVersionId = useStore((state) => state.currentVersionId); @@ -79,9 +91,9 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v // const fetchDataSources = useStore((state) => state.fetchDataSources); const fetchGlobalDataSources = useStore((state) => state.fetchGlobalDataSources); const previousVersion = usePrevious(currentVersionId); - const events = useStore((state) => state.eventsSlice.module[moduleId].events); - const pages = useStore((state) => state.modules[moduleId].pages); - const currentPageId = useStore((state) => state.currentPageId); + const events = useStore((state) => state.eventsSlice.module[moduleId]?.events || []); + const pages = useStore((state) => state.modules[moduleId]?.pages || []); + const currentPageId = useStore((state) => state.modules[moduleId].currentPageId); const setResolvedConstants = useStore((state) => state.setResolvedConstants); const setSecrets = useStore((state) => state.setSecrets); const setQueryMapping = useStore((state) => state.setQueryMapping); @@ -106,11 +118,18 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v const appMode = useStore((state) => state.globalSettings.appMode); const selectedTheme = useStore((state) => state.globalSettings.theme); const previousEnvironmentId = usePrevious(selectedEnvironment?.id); - const isComponentLayoutReady = useStore((state) => state.isComponentLayoutReady, shallow); + const isComponentLayoutReady = useStore((state) => state.appStore.modules[moduleId].isComponentLayoutReady, shallow); const pageSwitchInProgress = useStore((state) => state.pageSwitchInProgress); const setPageSwitchInProgress = useStore((state) => state.setPageSwitchInProgress); const selectedVersion = useStore((state) => state.selectedVersion); const setIsPublicAccess = useStore((state) => state.setIsPublicAccess); + + const setModulesIsLoading = useStore((state) => state.setModulesIsLoading, shallow); + const setModulesList = useStore((state) => state.setModulesList, shallow); + const setModuleDefinition = useStore((state) => state.setModuleDefinition); + const getModuleDefinition = useStore((state) => state.getModuleDefinition); + const deleteModuleDefinition = useStore((state) => state.deleteModuleDefinition); + const themeAccess = useThemeAccess(); const setConversation = useStore((state) => state.ai?.setConversation); @@ -121,7 +140,8 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v const setSelectedSidebarItem = useStore((state) => state.setSelectedSidebarItem); const toggleLeftSidebar = useStore((state) => state.toggleLeftSidebar); const pathParams = useParams(); - const slug = pathParams?.slug; + const slug = moduleMode ? '' : pathParams?.slug; + const licenseStatus = useStore((state) => state.isLicenseValid()); const match = useMatch('/applications/:slug/:pageHandle'); @@ -129,6 +149,14 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v const initialLoadRef = useRef(true); + const appTypeRef = useRef(null); + const { isReleasedVersionId } = useStore( + (state) => ({ + isReleasedVersionId: state?.releasedVersionId == state.currentVersionId || state.isVersionReleased, + }), + shallow + ); + const fetchAndInjectCustomStyles = async (isPublicAccess = false) => { try { const head = document.head || document.getElementsByTagName('head')[0]; @@ -153,14 +181,14 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v }; useEffect(() => { - if (pageSwitchInProgress) { + if (pageSwitchInProgress && !moduleMode) { const currentPageEvents = events.filter((event) => event.target === 'page' && event.sourceId === currentPageId); setPageSwitchInProgress(false); setTimeout(() => { handleEvent('onPageLoad', currentPageEvents, {}); }, 0); } - }, [pageSwitchInProgress, currentPageId]); + }, [pageSwitchInProgress, currentPageId, moduleMode]); useEffect(() => { const subscription = authenticationService.currentSession @@ -190,268 +218,317 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v return () => { subscription && subscription.unsubscribe(); }; - }, []); + }, [moduleMode]); useEffect(() => { const exposedTheme = appMode && appMode !== 'auto' ? appMode : localStorage.getItem('darkMode') === 'true' ? 'dark' : 'light'; - setResolvedGlobals('theme', { name: exposedTheme }); - }, [appMode, darkMode]); + setResolvedGlobals('theme', { name: exposedTheme }, moduleId); + }, [appMode, darkMode, moduleId]); useEffect(() => { if (!currentSession) { return; } - const queryParams = getPreviewQueryParams(); - const isPublicAccess = - (currentSession?.load_app && currentSession?.authentication_failed) || (!queryParams.version && mode !== 'edit'); - const isPreviewForVersion = (mode !== 'edit' && queryParams.version) || isPublicAccess; let appDataPromise; - if (isPublicAccess) { - appDataPromise = appService.fetchAppBySlug(slug); + const queryParams = moduleMode ? {} : getPreviewQueryParams(); + const isPublicAccess = moduleMode + ? false + : (currentSession?.load_app && currentSession?.authentication_failed) || + (!queryParams.version && mode !== 'edit'); + const isPreviewForVersion = (mode !== 'edit' && queryParams.version) || isPublicAccess; + + if (moduleMode) { + const moduleDefinition = getModuleDefinition(appId); + if (moduleDefinition) { + // clean up the module definition from the store + deleteModuleDefinition(appId); + appDataPromise = Promise.resolve(moduleDefinition); + } else { + appDataPromise = appService.fetchApp(appId); + } } else { - appDataPromise = isPreviewForVersion - ? appVersionService.getAppVersionData(appId, versionId) - : appService.fetchApp(appId); + if (isPublicAccess) { + appDataPromise = appService.fetchAppBySlug(slug); + } else { + appDataPromise = isPreviewForVersion + ? appVersionService.getAppVersionData(appId, versionId) + : appService.fetchApp(appId); + } } // const appDataPromise = appService.fetchApp(appId); - appDataPromise.then(async (result) => { - let appData = { ...result }; - let editorEnvironment = result.editorEnvironment; - if (isPreviewForVersion) { - const rawDataQueries = appData?.data_queries; - const rawEditingVersionDataQueries = appData?.editing_version?.data_queries; - appData = convertAllKeysToSnakeCase(appData); + appDataPromise + .then(async (result) => { + let appData = { ...result }; + let editorEnvironment = result.editorEnvironment; + if (isPreviewForVersion) { + const rawDataQueries = appData?.data_queries; + const rawEditingVersionDataQueries = appData?.editing_version?.data_queries; + appData = convertAllKeysToSnakeCase(appData); - appData.data_queries = rawDataQueries; - if (appData.editing_version && rawEditingVersionDataQueries) { - appData.editing_version.data_queries = rawEditingVersionDataQueries; - } + appData.data_queries = rawDataQueries; + if (appData.editing_version && rawEditingVersionDataQueries) { + appData.editing_version.data_queries = rawEditingVersionDataQueries; + } - editorEnvironment = { - id: environmentId, - name: queryParams.env, - }; - } - - let constantsResp; - if (mode !== 'edit') { - try { - const queryParams = { slug: slug }; - const viewerEnvironment = await appEnvironmentService.getEnvironment(environmentId, queryParams); editorEnvironment = { - id: viewerEnvironment?.environment?.id, - name: viewerEnvironment?.environment?.name, + id: environmentId, + name: queryParams.env, }; - constantsResp = - isPublicAccess && appData.is_public - ? await orgEnvironmentConstantService.getConstantsFromPublicApp(slug, viewerEnvironment?.environment?.id) - : await orgEnvironmentConstantService.getConstantsFromApp(slug, viewerEnvironment?.environment?.id); - } catch (error) { - console.error('Error fetching viewer environment:', error); } - } - if (mode === 'edit') { - constantsResp = await orgEnvironmentConstantService.getConstantsFromEnvironment(editorEnvironment?.id); - } - // get the constants for specific environment - constantsResp.constants = extractEnvironmentConstantsFromConstantsList( - constantsResp?.constants, - editorEnvironment?.name - ); - - setIsPublicAccess(isPublicAccess && mode !== 'edit' && appData.is_public); - - fetchAndInjectCustomStyles(isPublicAccess && mode !== 'edit' && appData.is_public); - - const pages = appData.pages.map((page) => { - return page; - }); - const conversation = appData.ai_conversation; - const docsConversation = appData.ai_conversation_learn; - if (setConversation && setDocsConversation) { - setConversation(conversation); - setDocsConversation(docsConversation); - // important to control ai inputs - getCreditBalance(); - } - - let showWalkthrough = true; - // if app was created from propmt, and no earlier messages are present in the conversation, send the prompt message - - // handles the getappdataby slug api call. Gets the homePageId from the appData. - const homePageId = - appData.editing_version?.homePageId || appData.editing_version?.home_page_id || appData.home_page_id; - - setApp({ - appName: appData.name, - appId: appData.id, - slug: appData.slug, - currentAppEnvironmentId: editorEnvironment.id, - isMaintenanceOn: - 'is_maintenance_on' in result - ? result.is_maintenance_on - : 'isMaintenanceOn' in result - ? result.isMaintenanceOn - : false, - organizationId: appData.organizationId || appData.organization_id, - homePageId: homePageId, - isPublic: appData.is_public, - creationMode: appData.creation_mode, - }); - setIsEditorFreezed(appData.should_freeze_editor); - const global_settings = mapKeys( - appData.editing_version?.global_settings || appData.global_settings, - (value, key) => camelCase(key) - ); - if (!global_settings?.theme) { - global_settings.theme = baseTheme; - } - setGlobalSettings(global_settings); - setPages(pages, moduleId); - setPageSettings( - computePageSettings(deepCamelCase(appData?.editing_version?.page_settings ?? appData?.page_settings)) - ); - - // set starting page as homepage initially - let startingPage = appData.pages.find((page) => page.id === homePageId); - - //no access to homepage, set to the next available page - if (startingPage?.restricted) { - startingPage = appData.pages.find((page) => !page?.restricted); - } - - if (initialLoadRef.current) { - // if initial load, check if the path has a page handle and set that as the starting page - const initialLoadPath = location.pathname.split('/').pop(); - - const page = appData.pages.find((page) => page.handle === initialLoadPath && !page.isPageGroup); - if (page) { - // if page is disabled, and not editing redirect to home page - const shouldRedirect = page?.restricted || (mode !== 'edit' && page?.disabled); - - if (shouldRedirect) { - const newUrl = window.location.href.replace(initialLoadPath, startingPage.handle); - window.history.replaceState(null, null, newUrl); - - if (page?.restricted) { - toast.error('Access to this page is restricted. Contact admin to know more.', { - className: 'text-nowrap w-auto mw-100', - }); - } - } else { - startingPage = page; + let constantsResp; + if (mode !== 'edit') { + try { + const queryParams = { slug: slug }; + const viewerEnvironment = await appEnvironmentService.getEnvironment(environmentId, queryParams); + editorEnvironment = { + id: viewerEnvironment?.environment?.id, + name: viewerEnvironment?.environment?.name, + }; + constantsResp = + isPublicAccess && appData.is_public + ? await orgEnvironmentConstantService.getConstantsFromPublicApp( + slug, + viewerEnvironment?.environment?.id + ) + : await orgEnvironmentConstantService.getConstantsFromApp(slug, viewerEnvironment?.environment?.id); + } catch (error) { + console.error('Error fetching viewer environment:', error); } } - // navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`); - } - - // Add page id and handle to the state on initial load - const currentState = window.history.state || {}; - const pageInfo = { - id: startingPage.id, - handle: startingPage.handle, - }; - const newState = { ...currentState, ...pageInfo }; - window.history.replaceState(newState, '', window.location.href); - - setCurrentPageHandle(startingPage.handle); - updateFeatureAccess(); - setCurrentPageId(startingPage.id, moduleId); - setResolvedPageConstants({ - id: startingPage?.id, - handle: startingPage?.handle, - name: startingPage?.name, - }); - setComponentNameIdMapping(moduleId); - updateEventsField('events', appData.events); - setCurrentVersionId(appData.editing_version?.id || appData.current_version_id); - setAppHomePageId(homePageId); - - const queryData = - isPublicAccess || (mode !== 'edit' && appData.is_public) - ? appData - : await dataqueryService.getAll(appData.editing_version?.id || appData.current_version_id); - const dataQueries = queryData.data_queries || queryData?.editing_version?.data_queries; - dataQueries.forEach((query) => normalizeQueryTransformationOptions(query)); - setQueries(dataQueries); - if (dataQueries?.length > 0) { - setSelectedQuery(dataQueries[0]?.id); - initialiseResolvedQuery(dataQueries.map((query) => query.id)); - } - const constants = constantsResp?.constants; - - if (constants) { - const orgConstants = {}; - const orgSecrets = {}; - constants.map((constant) => { - if (constant.type !== 'Secret') { - orgConstants[constant.name] = constant.value; - } else { - orgSecrets[constant.name] = constant.value; - } - }); - setResolvedConstants(orgConstants); - setSecrets(orgSecrets); - } - setQueryMapping(moduleId); - - setResolvedGlobals('environment', editorEnvironment); - setResolvedGlobals('mode', { value: mode }); - setResolvedGlobals('currentUser', { - ...user, - groups: currentSession?.groups, - role: currentSession?.role?.name, - ssoUserInfo: currentSession?.ssoUserInfo, - ...(currentSession?.currentUser?.metadata && !isEmpty(currentSession?.currentUser?.metadata) - ? { metadata: currentSession?.currentUser?.metadata } - : {}), - }); - setResolvedGlobals('urlparams', JSON.parse(JSON.stringify(queryString.parse(location?.search)))); - initDependencyGraph(moduleId); - setCurrentMode(mode); // TODO: set mode based on the slug/appDef - if ( - state.ai && - state?.prompt && - initialLoadRef.current && - (conversation?.aiConversationMessages || []).length === 0 - ) { - setSelectedSidebarItem('tooljetai'); - toggleLeftSidebar('true'); - sendMessage(state.prompt); - setConversationZeroState(true); - showWalkthrough = false; - } - // fetchDataSources(appData.editing_version.id, editorEnvironment.id); - if (!isPublicAccess) { - const envFromQueryParams = mode === 'view' && new URLSearchParams(location?.search)?.get('env'); - useStore.getState().init(appData.editing_version?.id || appData.current_version_id, envFromQueryParams); - fetchGlobalDataSources( - appData.organization_id, - appData.editing_version?.id || appData.current_version_id, - editorEnvironment.id + if (mode === 'edit') { + constantsResp = await orgEnvironmentConstantService.getConstantsFromEnvironment(editorEnvironment?.id); + } + // get the constants for specific environment + constantsResp.constants = extractEnvironmentConstantsFromConstantsList( + constantsResp?.constants, + editorEnvironment?.name ); - } - useStore.getState().updateEditingVersion(appData.editing_version?.id || appData.current_version_id); //check if this is needed - updateReleasedVersionId(appData.current_version_id); - setEditorLoading(false); - initialLoadRef.current = false; - // only show if app is not created from prompt - if (showWalkthrough) initEditorWalkThrough(); - checkAndSetTrueBuildSuggestionsFlag(); - return () => { - document.title = retrieveWhiteLabelText(); - }; - }); + !moduleMode && setIsPublicAccess(isPublicAccess && mode !== 'edit' && appData.is_public); + + fetchAndInjectCustomStyles(isPublicAccess && mode !== 'edit' && appData.is_public); + + const pages = appData.pages.map((page) => { + return page; + }); + const conversation = appData.ai_conversation; + const docsConversation = appData.ai_conversation_learn; + if (setConversation && setDocsConversation) { + setConversation(conversation); + setDocsConversation(docsConversation); + // important to control ai inputs + getCreditBalance(); + } + + let showWalkthrough = true; + // if app was created from propmt, and no earlier messages are present in the conversation, send the prompt message + + // handles the getappdataby slug api call. Gets the homePageId from the appData. + const homePageId = + appData.editing_version?.homePageId || appData.editing_version?.home_page_id || appData.home_page_id; + + appTypeRef.current = appData.type; + + setApp( + { + appName: appData.name, + appId: appData.id, + slug: appData.slug, + currentAppEnvironmentId: editorEnvironment.id, + isMaintenanceOn: + 'is_maintenance_on' in result + ? result.is_maintenance_on + : 'isMaintenanceOn' in result + ? result.isMaintenanceOn + : false, + organizationId: appData.organizationId || appData.organization_id, + homePageId: homePageId, + isPublic: appData.is_public, + creationMode: appData.creation_mode, + }, + moduleId + ); + if (!moduleMode) { + setIsEditorFreezed(appData.should_freeze_editor); + const global_settings = mapKeys( + appData.editing_version?.global_settings || appData.global_settings, + (value, key) => camelCase(key) + ); + if (!global_settings?.theme) { + global_settings.theme = baseTheme; + } + setGlobalSettings(global_settings); + } + setPages(pages, moduleId); + setPageSettings( + computePageSettings(deepCamelCase(appData?.editing_version?.page_settings ?? appData?.page_settings)) + ); + + // set starting page as homepage initially + let startingPage = appData.pages.find((page) => page.id === homePageId); + + //no access to homepage, set to the next available page + if (startingPage?.restricted) { + startingPage = appData.pages.find((page) => !page?.restricted); + } + + if (initialLoadRef.current && !moduleMode) { + // if initial load, check if the path has a page handle and set that as the starting page + const initialLoadPath = location.pathname.split('/').pop(); + + const page = appData.pages.find((page) => page.handle === initialLoadPath && !page.isPageGroup); + if (page) { + // if page is disabled, and not editing redirect to home page + const shouldRedirect = page?.restricted || (mode !== 'edit' && page?.disabled); + + if (shouldRedirect) { + const newUrl = window.location.href.replace(initialLoadPath, startingPage.handle); + window.history.replaceState(null, null, newUrl); + + if (page?.restricted) { + toast.error('Access to this page is restricted. Contact admin to know more.', { + className: 'text-nowrap w-auto mw-100', + }); + } + } else { + startingPage = page; + } + } + + // navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`); + } + + // Add page id and handle to the state on initial load + const currentState = window.history.state || {}; + const pageInfo = { + id: startingPage.id, + handle: startingPage.handle, + }; + const newState = { ...currentState, ...pageInfo }; + window.history.replaceState(newState, '', window.location.href); + + setCurrentPageHandle(startingPage.handle, moduleId); + setCurrentPageId(startingPage.id, moduleId); + setResolvedPageConstants( + { + id: startingPage?.id, + handle: startingPage?.handle, + name: startingPage?.name, + }, + moduleId + ); + setComponentNameIdMapping(moduleId); + updateEventsField('events', appData.events, moduleId); + if (!moduleMode) { + updateFeatureAccess(); + setCurrentVersionId(appData.editing_version?.id || appData.current_version_id); + } + setAppHomePageId(homePageId, moduleId); + if (!moduleMode && appData.modules) { + setModuleDefinition(appData.modules); + } + + const queryData = + isPublicAccess || (mode !== 'edit' && appData.is_public) + ? appData + : await dataqueryService.getAll(appData.editing_version?.id || appData.current_version_id); + const dataQueries = queryData.data_queries || queryData?.editing_version?.data_queries; + dataQueries.forEach((query) => normalizeQueryTransformationOptions(query)); + setQueries(dataQueries, moduleId); + if (dataQueries?.length > 0) { + !moduleMode && setSelectedQuery(dataQueries[0]?.id); + initialiseResolvedQuery( + dataQueries.map((query) => query.id), + moduleId + ); + } + const constants = constantsResp?.constants; + + if (constants) { + const orgConstants = {}; + const orgSecrets = {}; + constants.map((constant) => { + if (constant.type !== 'Secret') { + orgConstants[constant.name] = constant.value; + } else { + orgSecrets[constant.name] = constant.value; + } + }); + setResolvedConstants(orgConstants, moduleId); + setSecrets(orgSecrets, moduleId); + } + setQueryMapping(moduleId); + + setResolvedGlobals('environment', editorEnvironment, moduleId); + setResolvedGlobals('mode', { value: mode }, moduleId); + setResolvedGlobals( + 'currentUser', + { + ...user, + groups: currentSession?.groups, + role: currentSession?.role?.name, + ssoUserInfo: currentSession?.ssoUserInfo, + ...(currentSession?.currentUser?.metadata && !isEmpty(currentSession?.currentUser?.metadata) + ? { metadata: currentSession?.currentUser?.metadata } + : {}), + }, + moduleId + ); + setResolvedGlobals('urlparams', JSON.parse(JSON.stringify(queryString.parse(location?.search))), moduleId); + initDependencyGraph(moduleId); + setCurrentMode(mode, moduleId); // TODO: set mode based on the slug/appDef + if ( + !moduleMode && + state.ai && + state?.prompt && + initialLoadRef.current && + (conversation?.aiConversationMessages || []).length === 0 + ) { + setSelectedSidebarItem('tooljetai'); + toggleLeftSidebar('true'); + sendMessage(state.prompt); + setConversationZeroState(true); + showWalkthrough = false; + } + // fetchDataSources(appData.editing_version.id, editorEnvironment.id); + if (!isPublicAccess && !moduleMode) { + const envFromQueryParams = mode === 'view' && new URLSearchParams(location?.search)?.get('env'); + useStore.getState().init(appData.editing_version?.id || appData.current_version_id, envFromQueryParams); + fetchGlobalDataSources( + appData.organization_id, + appData.editing_version?.id || appData.current_version_id, + editorEnvironment.id + ); + } + if (!moduleMode) { + useStore.getState().updateEditingVersion(appData.editing_version?.id || appData.current_version_id); //check if this is needed + updateReleasedVersionId(appData.current_version_id); + } + + setEditorLoading(false, moduleId); + initialLoadRef.current = false; + // only show if app is not created from prompt + if (showWalkthrough && !moduleMode) initEditorWalkThrough(); + !moduleMode && checkAndSetTrueBuildSuggestionsFlag(); + return () => { + document.title = retrieveWhiteLabelText(); + }; + }) + .catch((error) => { + if (moduleMode) { + setEditorLoading(false, moduleId); + toast.error('Error fetching module data'); + } + }); }, [setApp, setEditorLoading, currentSession]); useEffect(() => { if (isComponentLayoutReady) { - runOnLoadQueries().then(() => { + runOnLoadQueries(moduleId).then(() => { let startingPage = pages.find((page) => page.id === currentPageId); const currentPageEvents = events.filter( (event) => event.target === 'page' && event.sourceId === startingPage.id @@ -459,11 +536,18 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v handleEvent('onPageLoad', currentPageEvents, {}); }); } - }, [isComponentLayoutReady]); + }, [isComponentLayoutReady, moduleId]); useEffect(() => { - fetchAndSetWindowTitle({ page: pageTitles.EDITOR, appName: app.appName }); - }, [app.appName]); + if (moduleId) return; + fetchAndSetWindowTitle({ + page: pageTitles.EDITOR, + appName: app.appName, + mode: mode, + isReleased: isReleasedVersionId, + licenseStatus: licenseStatus, + }); + }, [app.appName, isReleasedVersionId, licenseStatus, mode, moduleId]); useEffect(() => { if (!themeAccess) return; @@ -476,6 +560,7 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v }, [darkMode, selectedTheme, themeAccess]); useEffect(() => { + if (moduleMode) return; const exposedTheme = appMode && appMode !== 'auto' ? appMode : localStorage.getItem('darkMode') === 'true' ? 'dark' : 'light'; const isEnvChanged = @@ -483,13 +568,13 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v const isVersionChanged = currentVersionId && previousVersion && currentVersionId != previousVersion; if (isEnvChanged || isVersionChanged) { - setEditorLoading(true); + setEditorLoading(true, moduleId); clearSelectedComponents(); if (isEnvChanged) { setEnvironmentLoadingState('loading'); } appVersionService.getAppVersionData(appId, selectedVersion?.id).then(async (appData) => { - cleanUpStore(); + cleanUpStore(false); const { should_freeze_editor } = appData; setIsEditorFreezed(should_freeze_editor); @@ -524,7 +609,7 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v ); setCurrentPageId(startingPage.id, moduleId); setComponentNameIdMapping(moduleId); - updateEventsField('events', appData.events); + updateEventsField('events', appData.events, moduleId); // const queryData = await dataqueryService.getAll(currentVersionId); if (isEnvChanged) { @@ -550,7 +635,7 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v const queryData = await dataqueryService.getAll(currentVersionId); const dataQueries = queryData.data_queries; dataQueries.forEach((query) => normalizeQueryTransformationOptions(query)); - setQueries(dataQueries); + setQueries(dataQueries, moduleId); if (dataQueries?.length > 0) { setSelectedQuery(dataQueries[0]?.id); initialiseResolvedQuery(dataQueries.map((query) => query.id)); @@ -579,10 +664,27 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v setQueryMapping(moduleId); initDependencyGraph(moduleId); - setEditorLoading(false); + setEditorLoading(false, false); }); } - }, [selectedEnvironment?.id, currentVersionId]); + }, [selectedEnvironment?.id, currentVersionId, moduleMode, moduleId]); + + useEffect(() => { + if (moduleMode) return; + if (mode === 'edit') { + requestIdleCallback( + () => { + appsService.getAll(0, '', '', 'module').then((data) => { + setModulesIsLoading(false); + setModulesList(data.apps); + }); + }, + { timeout: 2000 } + ); // Adding a timeout of 2 seconds as fallback + } + }, [setModulesIsLoading, setModulesList, mode, moduleMode]); + + return appTypeRef.current; }; export default useAppData; diff --git a/frontend/src/AppBuilder/_stores/ast.js b/frontend/src/AppBuilder/_stores/ast.js index 106298588f..8412c3b7b2 100644 --- a/frontend/src/AppBuilder/_stores/ast.js +++ b/frontend/src/AppBuilder/_stores/ast.js @@ -32,7 +32,7 @@ function findExpression(input) { export function extractAndReplaceReferencesFromString(input, componentIdNameMapping = {}, queryIdNameMapping = {}) { // Quick check for relevant keywords const regexForQuickCheck = - /\b(components|queries|globals|variables|page|parameters|secrets|constants)(?:\[\S*|\.\S*|\?\.\S*)/; + /\b(components|queries|globals|variables|page|parameters|secrets|constants|input)(?:\[\S*|\.\S*|\?\.\S*)/; if (!regexForQuickCheck.test(input)) { return { allRefs: [], @@ -41,7 +41,7 @@ export function extractAndReplaceReferencesFromString(input, componentIdNameMapp }; } - const relevantKeywords = /\b(components|queries|globals|variables|page|parameters|secrets|constants)\b/; + const relevantKeywords = /\b(components|queries|globals|variables|page|parameters|secrets|constants|input)\b/; const expressionRegex = /{{(.*?)}}/gs; const results = []; let lastIndex = 0; @@ -312,6 +312,7 @@ function parseExpression(expression, componentIdNameMapping, queryIdNameMapping, variables: true, globals: true, page: true, + input: true, }; walk.simple(ast, { @@ -359,7 +360,7 @@ function parseExpression(expression, componentIdNameMapping, queryIdNameMapping, if ( (rootObject && (rootObject === 'queries' || rootObject === 'components') && path.length >= 3) || - ((rootObject === 'variables' || rootObject === 'globals') && path.length === 2) || + ((rootObject === 'variables' || rootObject === 'globals' || rootObject === 'input') && path.length === 2) || (rootObject === 'page' && path.length === 3) ) { return createReferenceObject(rootObject, path, uuidMappings, componentIdNameMapping, queryIdNameMapping); @@ -386,7 +387,7 @@ function createReferenceObject(entityType, path, uuidMappings, componentIdNameMa const mapping = entityType === 'components' ? componentIdNameMapping : queryIdNameMapping; entityNameOrId = mapping[entityNameOrId] || entityNameOrId; } - } else if (entityType === 'variables' || entityType === 'globals') { + } else if (entityType === 'variables' || entityType === 'globals' || entityType === 'input') { entityKey = path[1]; } else if (entityType === 'page') { entityNameOrId = path[1]; diff --git a/frontend/src/AppBuilder/_stores/slices/DependencyClass.js b/frontend/src/AppBuilder/_stores/slices/DependencyClass.js index 730cb41e12..e263532310 100644 --- a/frontend/src/AppBuilder/_stores/slices/DependencyClass.js +++ b/frontend/src/AppBuilder/_stores/slices/DependencyClass.js @@ -14,7 +14,7 @@ class DependencyGraph { } addDependency(fromPath, toPath, nodeData = '') { - if (fromPath.startsWith('variables.')) { + if (fromPath.startsWith('variables.') || fromPath.startsWith('input.')) { if (!this.hasNode(fromPath)) { const parts = fromPath.split('.'); fromPath = parts.slice(0, 2).join('.'); diff --git a/frontend/src/AppBuilder/_stores/slices/appSlice.js b/frontend/src/AppBuilder/_stores/slices/appSlice.js index 4b0ded7023..6962c6752d 100644 --- a/frontend/src/AppBuilder/_stores/slices/appSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/appSlice.js @@ -1,4 +1,3 @@ -import { updateCanvasBackground } from '@/_helpers/editorHelpers'; import { appsService, appVersionService } from '@/_services'; import { decimalToHex } from '@/Editor/editorConstants'; import toast from 'react-hot-toast'; @@ -10,8 +9,6 @@ import { replaceEntityReferencesWithIds, baseTheme } from '../utils'; import _ from 'lodash'; const initialState = { - app: {}, - canvasHeight: null, isSaving: false, globalSettings: { theme: baseTheme, @@ -19,20 +16,80 @@ const initialState = { pageSwitchInProgress: false, isTJDarkMode: localStorage.getItem('darkMode') === 'true', isViewer: false, - isComponentLayoutReady: false, + appStore: { + modules: { + canvas: { + canvasHeight: null, + app: {}, + isViewer: false, + isComponentLayoutReady: false, + }, + }, + }, }; export const createAppSlice = (set, get) => ({ ...initialState, - setIsViewer: (isViewer) => set(() => ({ isViewer }), false, 'setIsViewer'), - setApp: (app) => set(() => ({ app }), false, 'setApp'), - setAppName: (name) => set((state) => ({ app: { ...state.app, appName: name } }), false, 'setAppName'), - setAppHomePageId: (homePageId) => set((state) => ({ app: { ...state.app, homePageId } }), false, 'setAppHomePageId'), - setIsComponentLayoutReady: (isReady) => - set(() => ({ isComponentLayoutReady: isReady }), false, 'setIsComponentLayoutReady'), - setCanvasHeight: (canvasHeight) => set({ canvasHeight }, false, 'setCanvasHeight'), - updateCanvasBottomHeight: (components) => { - const { currentLayout, currentMode, setCanvasHeight } = get(); + initializeAppSlice: (moduleId) => { + set( + (state) => { + state.appStore.modules[moduleId] = { ...initialState.appStore.modules.canvas }; + }, + false, + 'initializeAppSlice' + ); + }, + setIsViewer: (isViewer, moduleId = 'canvas') => + set( + (state) => { + state.appStore.modules[moduleId].isViewer = isViewer; + }, + false, + 'setIsViewer' + ), + setApp: (app, moduleId = 'canvas') => + set( + (state) => { + state.appStore.modules[moduleId].app = app; + }, + false, + 'setApp' + ), + setAppName: (name, moduleId = 'canvas') => + set( + (state) => { + state.appStore.modules[moduleId].app.appName = name; + }, + false, + 'setAppName' + ), + setAppHomePageId: (homePageId, moduleId = 'canvas') => + set( + (state) => { + state.appStore.modules[moduleId].app.homePageId = homePageId; + }, + false, + 'setAppHomePageId' + ), + setIsComponentLayoutReady: (isReady, moduleId = 'canvas') => + set( + (state) => { + state.appStore.modules[moduleId].isComponentLayoutReady = isReady; + }, + false, + 'setIsComponentLayoutReady' + ), + setCanvasHeight: (canvasHeight, moduleId = 'canvas') => + set( + (state) => { + state.appStore.modules[moduleId].canvasHeight = canvasHeight; + }, + false, + 'setCanvasHeight' + ), + updateCanvasBottomHeight: (components, moduleId = 'canvas') => { + const { currentLayout, getCurrentMode, setCanvasHeight } = get(); + const currentMode = getCurrentMode(moduleId); const maxHeight = Object.values(components).reduce((max, component) => { const layout = component?.layouts?.[currentLayout]; if (!layout) { @@ -43,23 +100,25 @@ export const createAppSlice = (set, get) => ({ }, 0); const bottomPadding = currentMode === 'view' ? 100 : 300; const frameHeight = currentMode === 'view' ? 45 : 85; - setCanvasHeight(`max(100vh - ${frameHeight}px, ${maxHeight + bottomPadding}px)`); + setCanvasHeight(`max(100vh - ${frameHeight}px, ${maxHeight + bottomPadding}px)`, moduleId); }, - setIsAppSaving: (isSaving) => { + setIsAppSaving: (isSaving, moduleId = 'canvas') => { set( (state) => { - state.app.isSaving = isSaving; + state.appStore.modules[moduleId].app.isSaving = isSaving; }, false, 'setIsAppSaving' ); }, setGlobalSettings: (globalSettings) => set(() => ({ globalSettings }), false, 'setGlobalSettings'), - toggleAppMaintenance: () => { - const { isMaintenanceOn, appId } = get().app; + toggleAppMaintenance: (moduleId = 'canvas') => { + const { isMaintenanceOn, appId } = get().appStore.modules[moduleId].app; appsService.setMaintenance(appId, !isMaintenanceOn).then(() => { - set((state) => ({ app: { ...state.app, isMaintenanceOn: !isMaintenanceOn } })); + set((state) => { + state.appStore.modules[moduleId].app.isMaintenanceOn = !isMaintenanceOn; + }); if (isMaintenanceOn) { toast.success('Application is on maintenance.'); } else { @@ -67,9 +126,9 @@ export const createAppSlice = (set, get) => ({ } }); }, - globalSettingsChanged: async (options) => { - const componentNameIdMapping = get().modules.canvas.componentNameIdMapping; - const queryNameIdMapping = get().modules.canvas.queryNameIdMapping; + globalSettingsChanged: async (options, moduleId = 'canvas') => { + const componentNameIdMapping = get().modules[moduleId].componentNameIdMapping; + const queryNameIdMapping = get().modules[moduleId].queryNameIdMapping; for (const [key, value] of Object.entries(options)) { if (value?.[1]?.a == undefined) { options[key] = value; @@ -80,10 +139,10 @@ export const createAppSlice = (set, get) => ({ } // Replace entity references with ids if present const newOptions = replaceEntityReferencesWithIds(options, componentNameIdMapping, queryNameIdMapping); - const { app, currentVersionId, currentPageId } = get(); + const { appStore, currentVersionId, currentPageId } = get(); try { const res = await appVersionService.autoSaveApp( - app.appId, + appStore.modules[moduleId].app.appId, currentVersionId, { globalSettings: newOptions }, 'global_settings', @@ -96,7 +155,7 @@ export const createAppSlice = (set, get) => ({ console.error('Error updating page:', error); } }, - switchPage: (pageId, handle, queryParams = [], isBackOrForward = false) => { + switchPage: (pageId, handle, queryParams = [], moduleId = 'canvas', isBackOrForward = false) => { get().debugger.resetUnreadErrorCount(); // reset stores if (get().pageSwitchInProgress) { @@ -114,24 +173,24 @@ export const createAppSlice = (set, get) => ({ setResolvedGlobals, setResolvedPageConstants, setPageSwitchInProgress, - currentMode, license, modules: { canvas: { pages }, }, + getCurrentMode, } = get(); - const isPreview = currentMode !== 'edit'; + const isPreview = getCurrentMode(moduleId) !== 'edit'; //!TODO clear all queued tasks cleanUpStore(true); - setCurrentPageId(pageId, 'canvas'); - setComponentNameIdMapping('canvas'); - setQueryMapping('canvas'); + setCurrentPageId(pageId, moduleId); + setComponentNameIdMapping(moduleId); + setQueryMapping(moduleId); const isLicenseValid = !_.get(license, 'featureAccess.licenseStatus.isExpired', true) && _.get(license, 'featureAccess.licenseStatus.isLicenseValid', false); - const appId = get().app.appId; + const appId = get().appStore.modules[moduleId].app.appId; const filteredQueryParams = queryParams.filter(([key, value]) => { if (!value) return false; if (key === 'env' && isLicenseValid) return false; @@ -139,7 +198,7 @@ export const createAppSlice = (set, get) => ({ }); const queryParamsString = filteredQueryParams.map(([key, value]) => `${key}=${value}`).join('&'); - const slug = get().app.slug; + const slug = get().appStore.modules[moduleId].app.slug; if (!isBackOrForward) { navigate( @@ -155,11 +214,14 @@ export const createAppSlice = (set, get) => ({ } const newPage = pages.find((p) => p.id === pageId); - setResolvedPageConstants({ - id: newPage?.id, - handle: newPage?.handle, - name: newPage?.name, - }); + setResolvedPageConstants( + { + id: newPage?.id, + handle: newPage?.handle, + name: newPage?.name, + }, + moduleId + ); setResolvedGlobals('urlparams', JSON.parse(JSON.stringify(queryString.parse(queryParamsString)))); initDependencyGraph('canvas'); setPageSwitchInProgress(true); @@ -167,8 +229,9 @@ export const createAppSlice = (set, get) => ({ setPageSwitchInProgress: (isInProgress) => set(() => ({ pageSwitchInProgress: isInProgress }), false, 'setPageSwitchInProgress'), - cleanUpStore: (isPageSwitch = false) => { - get().resetUndoRedoStack(); + cleanUpStore: (isPageSwitch = false, moduleId) => { + const { resetUndoRedoStack, initModules } = get(); + resetUndoRedoStack(); set((state) => { state.modules.canvas.componentNameIdMapping = {}; state.selectedComponents = []; @@ -184,26 +247,33 @@ export const createAppSlice = (set, get) => ({ state.resolvedStore.modules.canvas.customResolvables = {}; state.resolvedStore.modules.canvas.exposedValues.components = {}; state.resolvedStore.modules.canvas.exposedValues.page.variables = {}; + // initModules(moduleId); }); }, - setSlug: (slug) => { + setSlug: (slug, moduleId = 'canvas') => { set( (state) => { - state.app.slug = slug; + state.appStore.modules[moduleId].app.slug = slug; }, false, 'setSlug' ); }, - setIsPublic: (isPublic) => { + setIsPublic: (isPublic, moduleId = 'canvas') => { set( (state) => { - state.app.isPublic = isPublic; + state.appStore.modules[moduleId].app.isPublic = isPublic; }, false, 'setIsPublic' ); }, + getAppId: (moduleId = 'canvas') => { + return get().appStore.modules[moduleId].app.appId; + }, + getHomePageId: (moduleId = 'canvas') => { + return get().appStore.modules[moduleId].app.homePageId; + }, updateIsTJDarkMode: (newMode) => set({ isTJDarkMode: newMode }, false, 'updateIsTJDarkMode'), }); diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index b500a4d912..7e74a6a006 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -6,6 +6,7 @@ import { checkSubstringRegex, hasArrayNotation, parsePropertyPath, + resolveCode, } from '@/AppBuilder/_stores/utils'; import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; @@ -29,19 +30,19 @@ import { findHighestLevelofSelection } from '@/AppBuilder/AppCanvas/Grid/gridUti const initialState = { modules: { canvas: { + currentPageId: null, + currentPageIndex: 0, pages: [], componentNameIdMapping: {}, queryNameIdMapping: {}, queryIdNameMapping: {}, + currentPageHandle: null, }, }, - currentPageId: null, - currentPageIndex: 0, containerChildrenMapping: { canvas: [], }, selectedComponents: [], - currentPageHandle: null, showWidgetDeleteConfirmation: false, focusedParentId: null, modalsOpenOnCanvas: [], @@ -50,6 +51,17 @@ const initialState = { export const createComponentsSlice = (set, get) => ({ ...initialState, + initializeComponentsSlice: (moduleId) => { + set( + (state) => { + state.modules[moduleId] = { ...initialState.modules.canvas }; + state.containerChildrenMapping[moduleId] = []; + }, + false, + 'initializeComponentsSlice' + ); + }, + setPages: (pages = [], moduleId = 'canvas') => { set( (state) => { @@ -70,16 +82,16 @@ export const createComponentsSlice = (set, get) => ({ ); }, - setCurrentPageId: (id, moduleId) => + setCurrentPageId: (id, moduleId = 'canvas') => set( (state) => { - const currentPageIndex = state.modules.canvas.pages.findIndex((page) => page.id === id); + const currentPageIndex = state.modules[moduleId].pages.findIndex((page) => page.id === id); const currentPageComponents = state.modules[moduleId].pages[currentPageIndex]?.components || {}; - state.currentPageIndex = currentPageIndex; - state.currentPageId = id; - state.containerChildrenMapping = { canvas: [] }; + state.modules[moduleId].currentPageIndex = currentPageIndex; + state.modules[moduleId].currentPageId = id; + state.containerChildrenMapping[moduleId] = []; Object.entries(currentPageComponents).forEach(([componentId, component]) => { - const parentId = component.component.parent || 'canvas'; + const parentId = component.component.parent || moduleId; if (!state.containerChildrenMapping[parentId]) { state.containerChildrenMapping[parentId] = []; } @@ -89,10 +101,10 @@ export const createComponentsSlice = (set, get) => ({ false, 'setCurrentPageId' ), - setCurrentPageHandle: (handle) => { + setCurrentPageHandle: (handle, moduleId = 'canvas') => { set( (state) => { - state.currentPageHandle = handle; + state.modules[moduleId].currentPageHandle = handle; }, false, 'setCurrentPageHandle' @@ -149,7 +161,7 @@ export const createComponentsSlice = (set, get) => ({ }, setComponentNameIdMapping: (moduleId = 'canvas') => { - const components = get().getCurrentPageComponents(); + const components = get().getCurrentPageComponents(moduleId); set( (state) => { Object.entries(components).forEach(([componentId, component]) => { @@ -162,12 +174,13 @@ export const createComponentsSlice = (set, get) => ({ }, setComponentName: (componentId, newName, moduleId = 'canvas') => { - const { renameComponentNameIdMapping, saveComponentChanges } = get(); + const { renameComponentNameIdMapping, saveComponentChanges, getCurrentPageIndex } = get(); + const currentPageIndex = getCurrentPageIndex(moduleId); let oldName = ''; set( (state) => { - oldName = state.modules[moduleId].pages[state.currentPageIndex].components[componentId].component.name; - state.modules[moduleId].pages[state.currentPageIndex].components[componentId].component.name = newName; + oldName = state.modules[moduleId].pages[currentPageIndex].components[componentId].component.name; + state.modules[moduleId].pages[currentPageIndex].components[componentId].component.name = newName; }, false, 'setComponentName' @@ -177,7 +190,7 @@ export const createComponentsSlice = (set, get) => ({ [componentId]: { component: { name: newName } }, }; - saveComponentChanges(diff, 'components', 'update'); + saveComponentChanges(diff, 'components', 'update', moduleId); renameComponentNameIdMapping(oldName, newName, moduleId); }, @@ -227,7 +240,15 @@ export const createComponentsSlice = (set, get) => ({ get().checkAndSetTrueBuildSuggestionsFlag(); }, - generateDependencyGraphForRefs: (allRefs, key, paramType, property, unResolvedValue, isUpdate = false) => { + generateDependencyGraphForRefs: ( + allRefs, + key, + paramType, + property, + unResolvedValue, + isUpdate = false, + moduleId = 'canvas' + ) => { const { addDependency, updateDependency } = get(); if (allRefs.length !== 0) { allRefs.forEach(({ entityType, entityNameOrId, entityKey }, index) => { @@ -236,9 +257,9 @@ export const createComponentsSlice = (set, get) => ({ : `${entityType}.${entityKey}`; const propertyPath = paramType === undefined ? `others.${key}` : `components.${key}.${paramType}.${property}`; if (isUpdate && index === 0) { - updateDependency(propertyValue, propertyPath, unResolvedValue); + updateDependency(propertyValue, propertyPath, unResolvedValue, moduleId); } else { - addDependency(propertyValue, propertyPath, unResolvedValue); + addDependency(propertyValue, propertyPath, unResolvedValue, moduleId); } }); } @@ -330,7 +351,7 @@ export const createComponentsSlice = (set, get) => ({ const length = Object.keys(customResolvables).length; if (length === 0) { const resolvedValue = shouldResolve - ? resolveDynamicValues(value, getAllExposedValues(), customResolvables, false, []) + ? resolveDynamicValues(value, getAllExposedValues(moduleId), customResolvables, false, []) : value; if (!componentResolvedValues[componentId] || Object.keys(componentResolvedValues[componentId]).length === 0) { componentResolvedValues[componentId] = index === null ? deepClone(DEFAULT_COMPONENT_STRUCTURE) : []; @@ -372,7 +393,7 @@ export const createComponentsSlice = (set, get) => ({ // Loop all the index and set the resolved value for (let i = 0; i < length; i++) { const resolvedValue = shouldResolve - ? resolveDynamicValues(value, getAllExposedValues(), customResolvables[i], false, []) + ? resolveDynamicValues(value, getAllExposedValues(moduleId), customResolvables[i], false, []) : value; if (!componentResolvedValues[componentId] || Object.keys(componentResolvedValues[componentId]).length === 0) { componentResolvedValues[componentId] = []; @@ -425,14 +446,14 @@ export const createComponentsSlice = (set, get) => ({ const length = Object.keys(customResolvables).length; if (length === 0) { const resolvedValue = shouldResolve - ? resolveDynamicValues(unResolvedValue, getAllExposedValues(), customResolvables, false, []) + ? resolveDynamicValues(unResolvedValue, getAllExposedValues(moduleId), customResolvables, false, []) : value; setResolvedComponentByProperty(componentId, paramType, property, resolvedValue, index, moduleId); } else { // Loop all the index and set the resolved value for (let i = 0; i < length; i++) { const resolvedValue = shouldResolve - ? resolveDynamicValues(unResolvedValue, getAllExposedValues(), customResolvables[i], false, []) + ? resolveDynamicValues(unResolvedValue, getAllExposedValues(moduleId), customResolvables[i], false, []) : value; setResolvedComponentByProperty(componentId, paramType, property, resolvedValue, i, moduleId); } @@ -638,7 +659,15 @@ export const createComponentsSlice = (set, get) => ({ ); lodashSet(updatedPropertyValue, [index, ...keys], updatedValue); if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, propertyWithArrayValue, unResolvedValue); + generateDependencyGraphForRefs( + allRefs, + componentId, + paramType, + propertyWithArrayValue, + unResolvedValue, + false, + moduleId + ); } }); } else { @@ -654,9 +683,16 @@ export const createComponentsSlice = (set, get) => ({ moduleId ); updatedPropertyValue[index] = updatedValue; - console.log('updatedPropertyValue', updatedPropertyValue); if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, propertyWithArrayValue, unResolvedValue); + generateDependencyGraphForRefs( + allRefs, + componentId, + paramType, + propertyWithArrayValue, + unResolvedValue, + false, + moduleId + ); } } }); @@ -673,7 +709,7 @@ export const createComponentsSlice = (set, get) => ({ moduleId ); if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue); + generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue, false, moduleId); } return { allRefs, unResolvedValue, updatedValue }; } @@ -711,7 +747,7 @@ export const createComponentsSlice = (set, get) => ({ addToDependencyGraph: (moduleId = 'canvas', componentId, component) => { const { updateDependencyGraphAndResolvedValues, getResolvedComponent } = get(); //TODO: Replace with object of component types - let resolvedComponentValues = { [componentId]: deepClone(getResolvedComponent(componentId) ?? {}) }; + let resolvedComponentValues = { [componentId]: deepClone(getResolvedComponent(componentId, null, moduleId) ?? {}) }; const componentType = componentTypes.find((comp) => component.component === comp.component); ['properties', 'general', 'generalStyles', 'others', 'styles', 'validation'].forEach((key) => { updateDependencyGraphAndResolvedValues( @@ -728,7 +764,7 @@ export const createComponentsSlice = (set, get) => ({ initDependencyGraph: (moduleId) => { const { getCurrentPageComponents, addToDependencyGraph, setResolvedComponents, resolveOthers } = get(); - const components = getCurrentPageComponents(); + const components = getCurrentPageComponents(moduleId); //TODO: Replace with object of component types let resolvedComponentValues = {}; @@ -764,9 +800,9 @@ export const createComponentsSlice = (set, get) => ({ get().modules[moduleId].componentNameIdMapping, get().modules[moduleId].queryNameIdMapping ); - const resolvedValue = resolveDynamicValues(valueWithBrackets, getAllExposedValues(), {}, false, []); + const resolvedValue = resolveDynamicValues(valueWithBrackets, getAllExposedValues(moduleId), {}, false, []); resolvedValues[key] = resolvedValue; - generateDependencyGraphForRefs(allRefs, key, undefined, undefined, valueWithBrackets, isUpdate); + generateDependencyGraphForRefs(allRefs, key, undefined, undefined, valueWithBrackets, isUpdate, moduleId); } else { resolvedValues[key] = item; } @@ -797,7 +833,9 @@ export const createComponentsSlice = (set, get) => ({ canAddToParent, getComponentNameFromId, deleteComponentNameIdMapping, + getCurrentPageId, } = get(); + const currentPageId = getCurrentPageId(moduleId); // This is made into a promise to wait for the saveComponentChanges to complete so that the caller can await it return new Promise((resolve) => { if ( @@ -811,7 +849,7 @@ export const createComponentsSlice = (set, get) => ({ } const newComponents = componentDefinitions.reduce((acc, componentDefinition) => { const currentComponents = { - ...getCurrentPageComponents(), + ...getCurrentPageComponents(moduleId), ...Object.fromEntries(acc.map((component) => [component.id, component])), }; const componentName = @@ -865,7 +903,7 @@ export const createComponentsSlice = (set, get) => ({ if (!state.containerChildrenMapping[parentId].includes(newComponent.id)) { state.containerChildrenMapping[parentId].push(newComponent.id); } - const page = state.modules[moduleId].pages.find((page) => page.id === state.currentPageId); + const page = state.modules[moduleId].pages.find((page) => page.id === currentPageId); page.components[newComponent.id] = newComponent; }, skipUndoRedo), false, @@ -876,7 +914,7 @@ export const createComponentsSlice = (set, get) => ({ get().setSelectedComponents(selectedComponents.map((component) => component.id)); if (saveAfterAction) { - saveComponentChanges(diff, 'components', 'create') + saveComponentChanges(diff, 'components', 'create', moduleId) .then(() => { resolve(); // Resolve the promise after all operations are complete }) @@ -901,7 +939,11 @@ export const createComponentsSlice = (set, get) => ({ selectedComponents, deleteComponentNameIdMapping, removeNode, + getCurrentPageId, + checkIfComponentIsModule, + clearModuleFromStore, } = get(); + const currentPageId = getCurrentPageId(moduleId); const appEvents = get().eventsSlice.getModuleEvents(moduleId); const componentNames = []; const _selectedComponents = selected?.length ? selected : selectedComponents; @@ -910,7 +952,7 @@ export const createComponentsSlice = (set, get) => ({ withUndoRedo((state) => { const toDeleteComponents = []; const toDeleteEvents = []; - const allComponents = getCurrentPageComponents(); + const allComponents = getCurrentPageComponents(moduleId); const findAllChildComponents = (componentId) => { if (!toDeleteComponents.includes(componentId)) { @@ -931,7 +973,7 @@ export const createComponentsSlice = (set, get) => ({ findAllChildComponents(componentId); }); - const page = state.modules?.canvas?.pages.find((page) => page.id === state.currentPageId); + const page = state.modules?.[moduleId]?.pages.find((page) => page.id === currentPageId); const resolvedComponents = state.resolvedStore.modules?.[moduleId]?.components; const componentsExposedValues = state.resolvedStore.modules?.[moduleId]?.exposedValues.components; @@ -943,12 +985,18 @@ export const createComponentsSlice = (set, get) => ({ ); }); + if (checkIfComponentIsModule(id, moduleId)) { + clearModuleFromStore(id); + } + // Remove the container itself if it's a container if (state.containerChildrenMapping[id]) { delete state.containerChildrenMapping[id]; } if (state.containerChildrenMapping?.canvas?.includes(id)) { - state.containerChildrenMapping.canvas = state.containerChildrenMapping.canvas.filter((wid) => wid !== id); + state.containerChildrenMapping[moduleId].canvas = state.containerChildrenMapping[moduleId].filter( + (wid) => wid !== id + ); } componentNames.push(page.components[id]?.component?.name); const eventsToRemove = appEvents.filter((event) => event.sourceId === id).map((event) => event.id); @@ -957,7 +1005,7 @@ export const createComponentsSlice = (set, get) => ({ delete resolvedComponents[id]; // Remove the component from the resolved store delete componentsExposedValues[id]; // Remove the component from the exposed values state.selectedComponents = []; // Empty the selected components - removeNode(`components.${id}`); + removeNode(`components.${id}`, moduleId); state.showWidgetDeleteConfirmation = false; // Set it to false always }); @@ -965,7 +1013,7 @@ export const createComponentsSlice = (set, get) => ({ state.eventsSlice.module[moduleId].events = filteredEvents; if (saveAfterAction) { - saveComponentChanges(toDeleteComponents, 'components', 'delete') + saveComponentChanges(toDeleteComponents, 'components', 'delete', moduleId) .then(() => { get().multiplayer.broadcastUpdates({ selectedComponents: _selectedComponents }, 'components', 'delete'); // Show delete toast message @@ -991,7 +1039,7 @@ export const createComponentsSlice = (set, get) => ({ 'deleteComponents' ); componentNames.forEach((componentName) => { - deleteComponentNameIdMapping(componentName); + deleteComponentNameIdMapping(componentName, moduleId); }); }, @@ -1040,12 +1088,14 @@ export const createComponentsSlice = (set, get) => ({ getComponentDefinition, currentLayout, checkValueAndResolve, + getCurrentPageIndex, } = get(); + const currentPageIndex = getCurrentPageIndex(moduleId); let hasParentChanged = false; let oldParentId; set( withUndoRedo((state) => { - const page = state.modules[moduleId].pages[state.currentPageIndex]; + const page = state.modules[moduleId].pages[currentPageIndex]; if (page) { // ============ Component layout update logic ============ Object.entries(componentLayouts).forEach(([componentId, layout]) => { @@ -1069,8 +1119,8 @@ export const createComponentsSlice = (set, get) => ({ state.containerChildrenMapping[oldParentId] = state.containerChildrenMapping[oldParentId].filter( (id) => id !== componentId ); - } else if (state.containerChildrenMapping.canvas.includes(componentId)) { - state.containerChildrenMapping.canvas = state.containerChildrenMapping.canvas.filter( + } else if (state.containerChildrenMapping[moduleId].includes(componentId)) { + state.containerChildrenMapping[moduleId] = state.containerChildrenMapping[moduleId].filter( (id) => id !== componentId ); } @@ -1082,7 +1132,7 @@ export const createComponentsSlice = (set, get) => ({ } state.containerChildrenMapping[newParentId].push(componentId); } else { - state.containerChildrenMapping.canvas.push(componentId); + state.containerChildrenMapping[moduleId].push(componentId); } } // ============ Parent update logic ends ============ @@ -1149,7 +1199,7 @@ export const createComponentsSlice = (set, get) => ({ }, {}); if (saveAfterAction) { - saveComponentChanges(diff, 'components/layout', 'update'); + saveComponentChanges(diff, 'components/layout', 'update', moduleId); get().multiplayer.broadcastUpdates(diff, 'components/layout', 'update'); } }, @@ -1165,7 +1215,7 @@ export const createComponentsSlice = (set, get) => ({ { skipUndoRedo = false, saveAfterAction = true } = {} ) => { const { - currentPageIndex, + getCurrentPageIndex, saveComponentChanges, withUndoRedo, updateResolvedValues, @@ -1176,12 +1226,14 @@ export const createComponentsSlice = (set, get) => ({ checkValueAndResolve, getResolvedComponent, setResolvedComponent, + getCurrentMode, } = get(); + const currentPageIndex = getCurrentPageIndex(moduleId); const { component } = getComponentDefinition(componentId, moduleId); const oldValue = component.definition[paramType][property]; if (Array.isArray(oldValue?.value)) { - const resolvedComponent = { [componentId]: deepClone(getResolvedComponent(componentId) ?? {}) }; + const resolvedComponent = { [componentId]: deepClone(getResolvedComponent(componentId, null, moduleId) ?? {}) }; resolvedComponent[componentId][paramType][property] = []; const { updatedValue } = checkValueAndResolve( @@ -1221,8 +1273,8 @@ export const createComponentsSlice = (set, get) => ({ }; if (saveAfterAction) { - const currentMode = get().currentMode; - if (currentMode !== 'view') saveComponentChanges(diff, 'components', 'update'); + const currentMode = getCurrentMode(moduleId); + if (currentMode !== 'view') saveComponentChanges(diff, 'components', 'update', moduleId); get().multiplayer.broadcastUpdates({ componentId, property, value, paramType, attr }, 'components', 'update'); } @@ -1273,18 +1325,18 @@ export const createComponentsSlice = (set, get) => ({ }; if (saveAfterAction) { - const currentMode = get().currentMode; - if (currentMode !== 'view') saveComponentChanges(diff, 'components', 'update'); + const currentMode = getCurrentMode(moduleId); + if (currentMode !== 'view') saveComponentChanges(diff, 'components', 'update', moduleId); get().multiplayer.broadcastUpdates({ componentId, property, value, paramType, attr }, 'components', 'update'); } if (attr !== 'value' || skipResolve) return; if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue, true); + generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue, true, moduleId); } else { const propertyPath = `components.${componentId}.${paramType}.${property}`; - removeDependency(propertyPath, true); + removeDependency(propertyPath, true, moduleId); } }, @@ -1317,8 +1369,8 @@ export const createComponentsSlice = (set, get) => ({ state.containerChildrenMapping[oldParentId] = state.containerChildrenMapping[oldParentId].filter( (id) => id !== componentId ); - } else if (state.containerChildrenMapping.canvas.includes(componentId)) { - state.containerChildrenMapping.canvas = state.containerChildrenMapping.canvas.filter( + } else if (state.containerChildrenMapping[moduleId].includes(componentId)) { + state.containerChildrenMapping[moduleId] = state.containerChildrenMapping[moduleId].filter( (id) => id !== componentId ); } @@ -1330,7 +1382,7 @@ export const createComponentsSlice = (set, get) => ({ } state.containerChildrenMapping[newParentId].push(componentId); } else { - state.containerChildrenMapping.canvas.push(componentId); + state.containerChildrenMapping[moduleId].push(componentId); } }, skipUndoRedo), false, @@ -1381,7 +1433,7 @@ export const createComponentsSlice = (set, get) => ({ }; if (saveAfterAction) { - saveComponentChanges(diff, 'components', 'update'); + saveComponentChanges(diff, 'components', 'update', moduleId); get().multiplayer.broadcastUpdates({ componentId, newParentId }, 'components', 'parent'); } }, @@ -1411,21 +1463,21 @@ export const createComponentsSlice = (set, get) => ({ setFocusedParentId: (parentId) => { set((state) => { state.focusedParentId = parentId; - }); + }), + false, + { type: 'setFocusedParentId', payload: { parentId } }; }, - saveComponentChanges: (diff, type, operation) => { + saveComponentChanges: (diff, type, operation, moduleId = 'canvas') => { set( (state) => { - state.app.isSaving = true; + state.appStore.modules[moduleId].app.isSaving = true; }, false, 'setAppSavingChanges' ); - const { - app: { appId }, - currentVersionId, - currentPageId, - } = get(); + const { getAppId, currentVersionId, getCurrentPageId } = get(); + const appId = getAppId(moduleId); + const currentPageId = getCurrentPageId(moduleId); return new Promise((resolve) => { appVersionService @@ -1449,7 +1501,7 @@ export const createComponentsSlice = (set, get) => ({ .finally(() => { set( (state) => { - state.app.isSaving = false; + state.appStore.modules[moduleId].app.isSaving = false; }, false, 'setAppSavingChanges' @@ -1475,7 +1527,8 @@ export const createComponentsSlice = (set, get) => ({ }, turnOffAutoComputeLayout: async (moduleId = 'canvas') => { - const { app, currentPageId, currentVersionId } = get(); + const { app, getCurrentPageId, currentVersionId } = get(); + const currentPageId = getCurrentPageId(moduleId); set( (state) => { state.modules[moduleId].pages[state.currentPageIndex].autoComputeLayout = false; @@ -1492,38 +1545,44 @@ export const createComponentsSlice = (set, get) => ({ }); }, - getCurrentPageId: () => get().currentPageId, + getCurrentPageId: (moduleId = 'canvas') => get().modules[moduleId].currentPageId, + getCurrentPageIndex: (moduleId = 'canvas') => get().modules[moduleId].currentPageIndex, - getComponentsFromAllPages: () => { + getComponentsFromAllPages: (moduleId = 'canvas') => { const { modules } = get(); return Object.fromEntries( - modules.canvas.pages.flatMap((page) => + modules[moduleId].pages.flatMap((page) => Object.entries(page.components).map(([id, { component }]) => [id, component.name]) ) ); }, - getCurrentPageComponents: () => { - const { modules, currentPageId } = get(); - const currentPageIndex = modules.canvas.pages.findIndex((page) => page.id === currentPageId); - return modules.canvas.pages[currentPageIndex]?.components || []; + getCurrentPageComponents: (moduleId = 'canvas') => { + const { modules, getCurrentPageId } = get(); + const currentPageId = getCurrentPageId(moduleId); + const currentPageIndex = modules[moduleId].pages.findIndex((page) => page.id === currentPageId); + return modules[moduleId].pages[currentPageIndex]?.components || []; }, - getCurrentPageComponentIds: () => { - const { pages, currentPageId, modules } = get(); - const currentPageIndex = modules.canvas.pages.findIndex((page) => page.id === currentPageId); + getCurrentPageComponentIds: (moduleId = 'canvas') => { + const { pages, getCurrentPageId, modules } = get(); + const currentPageId = getCurrentPageId(moduleId); + const currentPageIndex = modules[moduleId].pages.findIndex((page) => page.id === currentPageId); return Object.keys(pages[currentPageIndex]?.components || {}); }, getCurrentPage: (moduleId = 'canvas') => { - const { modules, currentPageId } = get(); + const { modules, getCurrentPageId } = get(); + const currentPageId = getCurrentPageId(moduleId); const currentPage = modules[moduleId].pages.find((page) => page.id === currentPageId); return currentPage; }, // Get the component definition from the component id getComponentDefinition: (componentId, moduleId = 'canvas') => { - const currentPage = get().modules[moduleId].pages.find((page) => page.id === get().currentPageId); + const currentPage = get().modules[moduleId].pages.find((page) => page.id === get().getCurrentPageId(moduleId)); + // if (componentId === 'd78554b8-2af0-4add-9d7d-0032bb4c90ce') + // console.trace('here--- getComponentDefinition--- ', componentId, moduleId, currentPage?.components[componentId]); return currentPage?.components[componentId]; }, @@ -1533,24 +1592,26 @@ export const createComponentsSlice = (set, get) => ({ }, // Get the component name from the component id getComponentNameFromId: (componentId, moduleId = 'canvas') => { - const { modules, currentPageIndex } = get(); + const { modules, getCurrentPageIndex } = get(); + const currentPageIndex = getCurrentPageIndex(moduleId); return modules[moduleId].pages[currentPageIndex]?.components[componentId]?.component.name; }, getComponentTypeFromId: (componentId, moduleId = 'canvas') => { - const { modules, currentPageIndex } = get(); + const { modules, getCurrentPageIndex } = get(); + const currentPageIndex = getCurrentPageIndex(moduleId); return modules[moduleId].pages[currentPageIndex]?.components[componentId]?.component.component; }, getComponentNameIdMapping: (moduleId = 'canvas') => { const { modules } = get(); return modules[moduleId].componentNameIdMapping; }, - getComponentIdNameMapping: () => { + getComponentIdNameMapping: (moduleId = 'canvas') => { const { getComponentNameIdMapping } = get(); - return Object.fromEntries(Object.entries(getComponentNameIdMapping()).map(([name, id]) => [id, name])); + return Object.fromEntries(Object.entries(getComponentNameIdMapping(moduleId)).map(([name, id]) => [id, name])); }, - getSelectedComponentsDefinition: () => { + getSelectedComponentsDefinition: (moduleId = 'canvas') => { const { selectedComponents, getCurrentPageComponents } = get(); - const allComponents = getCurrentPageComponents(); + const allComponents = getCurrentPageComponents(moduleId); const _selected = []; for (let componentId of selectedComponents) { const component = { @@ -1574,13 +1635,17 @@ export const createComponentsSlice = (set, get) => ({ const { modules } = get(); return modules[moduleId].queryIdNameMapping; }, + getQueryIdFromName: (queryName, moduleId = 'canvas') => { + const { modules } = get(); + return modules[moduleId].queryNameIdMapping[queryName]; + }, getContainerChildrenMapping: (id) => { const { containerChildrenMapping } = get(); return containerChildrenMapping[id] || []; }, getChildComponents: (parentId, moduleId = 'canvas') => { const { getCurrentPageComponents } = get(); - const allComponents = getCurrentPageComponents(); + const allComponents = getCurrentPageComponents(moduleId); const childComponents = Object.entries(allComponents) .filter(([_, component]) => component.component.parent === parentId) .reduce((acc, [id, component]) => { @@ -1609,8 +1674,8 @@ export const createComponentsSlice = (set, get) => ({ } else { const [entityType, entityId, type, ...keys] = dependency.split('.'); const key = keys.join('.'); - const unResolvedValue = getNodeData(dependency); - const resolvedValue = resolveDynamicValues(unResolvedValue, getAllExposedValues(), {}, false, []); + const unResolvedValue = getNodeData(dependency, moduleId); + const resolvedValue = resolveDynamicValues(unResolvedValue, getAllExposedValues(moduleId), {}, false, []); if (type === undefined) { set( @@ -1624,7 +1689,7 @@ export const createComponentsSlice = (set, get) => ({ } else { const shouldValidate = entityType === 'components' && entityId; const validatedValue = shouldValidate - ? get().debugger.validateProperty(entityId, type, key, resolvedValue) + ? get().debugger.validateProperty(entityId, type, key, resolvedValue, moduleId) : resolvedValue; // logic to handle the key like options[0].visible. It will resolve the visible directly and update the resolved store @@ -1639,7 +1704,7 @@ export const createComponentsSlice = (set, get) => ({ lodashSet( state.resolvedStore.modules[moduleId][entityType][entityId], ['properties', 'shouldRender'], - (getResolvedComponent(entityId)?.['properties']?.['shouldRender'] ?? 0) + 1 + (getResolvedComponent(entityId, null, moduleId)?.['properties']?.['shouldRender'] ?? 0) + 1 ); }, false, @@ -1688,24 +1753,24 @@ export const createComponentsSlice = (set, get) => ({ } }, - getParentIdFromDependency: (dependency) => { + getParentIdFromDependency: (dependency, moduleId = 'canvas') => { const { getComponentDefinition } = get(); const componentId = dependency.split('.')[1]; - const component = getComponentDefinition(componentId); + const component = getComponentDefinition(componentId, moduleId); return component?.component?.parent; }, updateChildComponentResolvedValues: (dependency, path, length, moduleId = 'canvas') => { const { getCustomResolvables, getNodeData, getAllExposedValues, getParentIdFromDependency } = get(); const [entityType, entityId, type, key] = dependency.split('.'); - const parentId = getParentIdFromDependency(dependency); - const unResolvedValue = getNodeData(dependency); + const parentId = getParentIdFromDependency(dependency, moduleId); + const unResolvedValue = getNodeData(dependency, moduleId); // Loop through the customResolvables and update the resolved value for (let i = 0; i < length; i++) { const resolvedValue = resolveDynamicValues( unResolvedValue, - getAllExposedValues(), + getAllExposedValues(moduleId), getCustomResolvables(parentId, i, moduleId), // passing the parent ID and index to get the custom resolvables of the child false, [] @@ -1713,7 +1778,7 @@ export const createComponentsSlice = (set, get) => ({ // If the index is not in the resolved store then add it with first index data const shouldValidate = entityType === 'components' && entityId; const validatedValue = shouldValidate - ? get().debugger.validateProperty(entityId, type, key, resolvedValue) + ? get().debugger.validateProperty(entityId, type, key, resolvedValue, moduleId) : resolvedValue; set( @@ -1736,7 +1801,8 @@ export const createComponentsSlice = (set, get) => ({ getParentComponentType: (parentId, moduleId) => { if (!parentId) return null; - const { modules, currentPageIndex } = get(); + const { modules, getCurrentPageIndex } = get(); + const currentPageIndex = getCurrentPageIndex(moduleId); // Remove the tab id or any other details from the parent id (ie, -modal, -calendar, -0 from parentId) const parentUUID = parentId.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1] || parentId; const component = modules[moduleId].pages[currentPageIndex].components[parentUUID]; @@ -1833,8 +1899,8 @@ export const createComponentsSlice = (set, get) => ({ return match; // Return the original match if no mapping is found }); }, - calculateMoveableBoxHeightWithId: (componentId, currentLayout, stylesDefinition) => { - const componentDefinition = get().getComponentDefinition(componentId); + calculateMoveableBoxHeightWithId: (componentId, currentLayout, stylesDefinition, moduleId = 'canvas') => { + const componentDefinition = get().getComponentDefinition(componentId, moduleId); const layoutData = componentDefinition?.layouts?.[currentLayout]; const componentType = componentDefinition?.component?.component; const label = componentDefinition?.component?.definition?.properties?.label; @@ -1862,8 +1928,8 @@ export const createComponentsSlice = (set, get) => ({ } const { alignment = { value: null }, width = { value: null }, auto = { value: null } } = stylesDefinition ?? {}; const resolvedLabel = label?.value?.length ?? 0; - const resolvedWidth = resolveDynamicValues(width?.value + '', getAllExposedValues()) ?? 0; - const resolvedAuto = resolveDynamicValues(auto?.value + '', getAllExposedValues()) ?? false; + const resolvedWidth = resolveDynamicValues(width?.value + '', getAllExposedValues(moduleId)) ?? 0; + const resolvedAuto = resolveDynamicValues(auto?.value + '', getAllExposedValues(moduleId)) ?? false; const resolvedAlignment = alignment.value === 'top' || alignment.value === 'side' @@ -1896,6 +1962,8 @@ export const createComponentsSlice = (set, get) => ({ state.modalsOpenOnCanvas = newModalOpenOnCanvas; }); }, + checkIfComponentIsModule: (componentId, moduleId = 'canvas') => + get().getComponentDefinition(componentId, moduleId)?.component?.component === 'ModuleViewer', updateContainerAutoHeight: (componentId) => { if ( !componentId || diff --git a/frontend/src/AppBuilder/_stores/slices/createSelectors.js b/frontend/src/AppBuilder/_stores/slices/createSelectors.js deleted file mode 100644 index 50fd299f4e..0000000000 --- a/frontend/src/AppBuilder/_stores/slices/createSelectors.js +++ /dev/null @@ -1,13 +0,0 @@ -// import { useStore } from 'zustand'; - -// const createSelectors = (_store) => { -// const store = _store; -// store.use = {}; -// for (const k of Object.keys(store.getState())) { -// store.use[k] = () => useStore(_store, (s) => s[k]); -// } - -// return store; -// }; - -// export { createSelectors }; diff --git a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js index 79acc2c461..99d1ee660b 100644 --- a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js +++ b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js @@ -22,10 +22,20 @@ const initialState = { }; export const createDataQuerySlice = (set, get) => ({ + initializeDataQuerySlice: (moduleId = 'canvas') => { + set( + (state) => { + state.dataQuery.queries.modules[moduleId] = []; + }, + false, + 'initializeDataQuerySlice' + ); + }, dataQuery: { ...initialState, - checkExistingQueryName: (newName) => get().dataQuery.queries.modules.canvas.some((query) => query.name === newName), - getCurrentModuleQueries: (moduleId) => get().dataQuery.queries.modules[moduleId], + checkExistingQueryName: (newName, moduleId = 'canvas') => + get().dataQuery.queries.modules[moduleId].some((query) => query.name === newName), + getCurrentModuleQueries: (moduleId = 'canvas') => get().dataQuery.queries.modules[moduleId], setQueries: (queries, moduleId = 'canvas') => { set( (state) => { @@ -49,7 +59,7 @@ export const createDataQuerySlice = (set, get) => ({ }, createDataQuery: (selectedDataSource, shouldRunQuery, customOptions = {}, moduleId = 'canvas') => { const appVersionId = get().currentVersionId; - const appId = get().app.appId; + const appId = get().appStore.modules[moduleId].app.appId; const { options: defaultOptions, name } = getDefaultOptions(selectedDataSource); const options = { ...defaultOptions, ...customOptions }; const kind = selectedDataSource.kind; @@ -101,7 +111,7 @@ export const createDataQuerySlice = (set, get) => ({ return query; }); }); - setSelectedQuery(data.id, data); + setSelectedQuery(data.id, moduleId); if (shouldRunQuery) setQueryToBeRun(data); /** Checks if there is an API call cached. If yes execute it */ @@ -121,12 +131,16 @@ export const createDataQuerySlice = (set, get) => ({ get().addNewQueryMapping(data.id, data.name, moduleId); //! we need default value in store so that query can be resolved if referenced from other entity - get().setResolvedQuery(data.id, { - isLoading: false, - data: [], - rawData: [], - id: data.id, - }); + get().setResolvedQuery( + data.id, + { + isLoading: false, + data: [], + rawData: [], + id: data.id, + }, + moduleId + ); }) .catch((error) => { set((state) => { @@ -220,8 +234,8 @@ export const createDataQuerySlice = (set, get) => ({ }) .finally(() => setIsAppSaving(false)); - get().removeNode(`queries.${queryId}`); - get().updateDependencyValues(`queries.${queryId}`); + get().removeNode(`queries.${queryId}`, moduleId); + get().updateDependencyValues(`queries.${queryId}`, moduleId); }, duplicateQuery: (id, appId, moduleId = 'canvas') => { set((state) => { @@ -266,16 +280,20 @@ export const createDataQuerySlice = (set, get) => ({ ...state.dataQuery.queries.modules[moduleId], ]; }); - setSelectedQuery(data.id, { ...data, data_source_id: queryToClone.data_source_id }); + setSelectedQuery(data.id, moduleId); get().addNewQueryMapping(data.id, data.name, moduleId); //! we need default value in store so that query can be resolved if referenced from other entity - get().setResolvedQuery(data.id, { - isLoading: false, - data: [], - rawData: [], - id: data.id, - }); + get().setResolvedQuery( + data.id, + { + isLoading: false, + data: [], + rawData: [], + id: data.id, + }, + moduleId + ); const events = getEventsByComponentsId(queryToClone.id); @@ -416,12 +434,23 @@ export const createDataQuerySlice = (set, get) => ({ }); }); }, 500), - runOnLoadQueries: async () => { - const queries = get().dataQuery.queries.modules.canvas; + runOnLoadQueries: async (moduleId = 'canvas') => { + const queries = get().dataQuery.queries.modules[moduleId]; try { for (const query of queries) { if ((query.options.runOnPageLoad || query.options.run_on_page_load) && isQueryRunnable(query)) { - await get().queryPanel.runQuery(query.id, query.name, undefined, undefined, {}, false, true, 'canvas'); + await get().queryPanel.runQuery( + query.id, + query.name, + undefined, + undefined, + {}, + undefined, + undefined, + false, + true, + moduleId + ); } } return Promise.resolve(); diff --git a/frontend/src/AppBuilder/_stores/slices/debuggerSlice.js b/frontend/src/AppBuilder/_stores/slices/debuggerSlice.js index f4bab3d4ee..912ee596ae 100644 --- a/frontend/src/AppBuilder/_stores/slices/debuggerSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/debuggerSlice.js @@ -32,7 +32,7 @@ export const createDebuggerSlice = (set, get) => ({ log: (log) => { set( (state) => { - log.page = get().currentPageId; + log.page = get().getCurrentPageId('canvas'); state.debugger.logs.unshift(log); if (log.logLevel === 'error') state.debugger.unreadErrorCount++; }, @@ -44,7 +44,7 @@ export const createDebuggerSlice = (set, get) => ({ logMultiple: (logs) => { set( (state) => { - state.debugger.logs.push(...logs.map((log) => ({ ...log, page: get().currentPageId }))); + state.debugger.logs.push(...logs.map((log) => ({ ...log, page: get().getCurrentPageId('canvas') }))); state.debugger.unreadErrorCount += logs.length; }, false, @@ -84,20 +84,20 @@ export const createDebuggerSlice = (set, get) => ({ return transformedStyles; }, - validateComponents: (components) => { + validateComponents: (components, moduleId = 'canvas') => { const validateComponent = get().debugger.validateComponent; const entries = Object.entries(components).map(([id, component]) => { // If component is an array, validate each component in the array and return the array if (Array.isArray(component)) { - return [id, component.map((c) => validateComponent(id, c))]; + return [id, component.map((c) => validateComponent(id, c, moduleId))]; } - return [id, validateComponent(id, component)]; + return [id, validateComponent(id, component, moduleId)]; }); return Object.fromEntries(entries); }, - validateComponent: (id, component) => { - const componentDefinition = get().getComponentDefinition(id); + validateComponent: (id, component, moduleId = 'canvas') => { + const componentDefinition = get().getComponentDefinition(id, moduleId); const componentName = componentDefinition.component.name; const componentType = componentDefinition.component.component; const componentMeta = componentTypeDefinitionMap[componentType]; @@ -135,7 +135,7 @@ export const createDebuggerSlice = (set, get) => ({ }; const logs = allErrors.map((error) => ({ - page: get().currentPageId, + page: get().getCurrentPageId('canvas'), type: 'component', kind: 'component', key: `${componentName} - ${error.property}`, @@ -158,10 +158,10 @@ export const createDebuggerSlice = (set, get) => ({ return newComponent; }, - validateProperty: (componentId, type, property, value) => { + validateProperty: (componentId, type, property, value, moduleId = 'canvas') => { const log = get().debugger.log; - const componentDefinition = get().getComponentDefinition(componentId); + const componentDefinition = get().getComponentDefinition(componentId, moduleId); const componentName = componentDefinition.component.name; const componentType = componentDefinition.component.component; const componentMeta = componentTypeDefinitionMap[componentType]; @@ -179,7 +179,7 @@ export const createDebuggerSlice = (set, get) => ({ if (valid === false) { log({ - page: get().currentPageId, + page: get().getCurrentPageId('canvas'), type: 'component', kind: 'component', key: `${componentName} - ${componentMeta[type][property]?.displayName}`, diff --git a/frontend/src/AppBuilder/_stores/slices/dependencySlice.js b/frontend/src/AppBuilder/_stores/slices/dependencySlice.js index 8bea44819d..66fc0d1b16 100644 --- a/frontend/src/AppBuilder/_stores/slices/dependencySlice.js +++ b/frontend/src/AppBuilder/_stores/slices/dependencySlice.js @@ -12,48 +12,61 @@ const initialState = { export const createDependencySlice = (set, get) => ({ ...initialState, + initializeDependencySlice: (moduleId) => { + set( + (state) => { + state.dependencyGraph.modules[moduleId] = { + graph: new DependencyGraph(), + }; + }, + false, + 'initializeDependencySlice' + ); + }, - addDependency: (fromPath, toPath, nodeData) => { - if (!get().checkIfDependencyExists(fromPath, toPath)) { + addDependency: (fromPath, toPath, nodeData, moduleId = 'canvas') => { + if (!get().checkIfDependencyExists(fromPath, toPath, moduleId)) { set((state) => { - state.dependencyGraph.modules.canvas.graph.addDependency(fromPath, toPath, nodeData); + state.dependencyGraph.modules[moduleId].graph.addDependency(fromPath, toPath, nodeData); return { ...state }; }); } }, - updateDependency: (newFromPath, toPath, nodeData) => + updateDependency: (newFromPath, toPath, nodeData, moduleId = 'canvas') => set((state) => { - state.dependencyGraph.modules.canvas.graph.updateDependency(newFromPath, toPath, nodeData); + state.dependencyGraph.modules[moduleId].graph.updateDependency(newFromPath, toPath, nodeData); return { ...state }; }), - removeDependency: (toPath, clearToPath = false) => + removeDependency: (toPath, clearToPath = false, moduleId = 'canvas') => set((state) => { - state.dependencyGraph.modules.canvas.graph.removeDependency(toPath, clearToPath); + state.dependencyGraph.modules[moduleId].graph.removeDependency(toPath, clearToPath); return { ...state }; }), - removeNode: (path) => + removeNode: (path, moduleId = 'canvas') => set((state) => { - state.dependencyGraph.modules.canvas.graph.removeNode(path); + state.dependencyGraph.modules[moduleId].graph.removeNode(path); return { ...state }; }), - getNodeData: (path) => get().dependencyGraph.modules.canvas.graph.getNodeData(path), + getNodeData: (path, moduleId = 'canvas') => get().dependencyGraph.modules[moduleId].graph.getNodeData(path), - getDependencies: (path) => get().dependencyGraph.modules.canvas.graph.getDependencies(path), + getDependencies: (path, moduleId = 'canvas') => get().dependencyGraph.modules[moduleId].graph.getDependencies(path), - getDirectDependencies: (path) => get().dependencyGraph.modules.canvas.graph.getDirectDependencies(path), + getDirectDependencies: (path, moduleId = 'canvas') => + get().dependencyGraph.modules[moduleId].graph.getDirectDependencies(path), - getDependents: (path) => get().dependencyGraph.modules.canvas.graph.getDependents(path), + getDependents: (path, moduleId = 'canvas') => get().dependencyGraph.modules[moduleId].graph.getDependents(path), - getDirectDependents: (path) => get().dependencyGraph.modules.canvas.graph.getDirectDependents(path), + getDirectDependents: (path, moduleId = 'canvas') => + get().dependencyGraph.modules[moduleId].graph.getDirectDependents(path), - getOverallOrder: () => get().dependencyGraph.modules.canvas.graph.getOverallOrder(), + getOverallOrder: (moduleId = 'canvas') => get().dependencyGraph.modules[moduleId].graph.getOverallOrder(), - checkIfDependencyExists: (fromPath, toPath) => { - const dependencies = get().getDependencies(fromPath); + checkIfDependencyExists: (fromPath, toPath, moduleId = 'canvas') => { + const dependencies = get().getDependencies(fromPath, moduleId); return dependencies.includes(toPath); }, }); diff --git a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js index 1edd3994c5..1077a22608 100644 --- a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js @@ -243,7 +243,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({ const versionIsAvailableInEnvironment = environment?.priority <= get().currentAppVersionEnvironment?.priority; if (!versionIsAvailableInEnvironment) { - const appId = useStore.getState().app.appId; + const { appId } = useStore.getState().appStore.modules.canvas.app; const response = await appEnvironmentService.postEnvironmentChangedAction({ appId, editorEnvironmentId: environmentId, @@ -285,7 +285,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({ promoteAppVersionAction: async (versionId, onSuccess, onFailure) => { try { - const appId = useStore.getState().app.appId; // Correct way to access appId + const { appId } = useStore.getState().appStore.modules.canvas.app; const response = await appVersionService.promoteEnvironment(appId, versionId, get().selectedEnvironment.id); set((state) => ({ diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index f93f64b1c5..5991f093f8 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -6,7 +6,6 @@ import { deepClone } from '@/_helpers/utilities/utils.helpers'; import { dfs } from '@/_stores/handleReferenceTransactions'; import { isQueryRunnable, isValidUUID, serializeNestedObjectToQueryParams } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; -import { handleLowPriorityWork } from '@/AppBuilder/_helpers/editorHelpers'; import _ from 'lodash'; import { logoutAction } from '@/AppBuilder/_utils/auth'; import { copyToClipboard } from '@/_helpers/appUtils'; @@ -59,8 +58,8 @@ export const useEventActions = (moduleId = 'canvas') => { ); const memoizedUpdateEventsField = useCallback( - (field, value) => updateEventsField(field, value, moduleId), - [updateEventsField, moduleId] + (field, value, moduleId) => updateEventsField(field, value, moduleId), + [updateEventsField] ); return { @@ -72,6 +71,17 @@ export const useEventActions = (moduleId = 'canvas') => { }; export const createEventsSlice = (set, get) => ({ + initializeEventsSlice: (moduleId) => { + set( + (state) => { + state.eventsSlice.module[moduleId] = { + ...initialState.module.canvas, + }; + }, + false, + 'initializeEventsSlice' + ); + }, eventsSlice: { ...initialState, setEvents: (events, moduleId = 'canvas') => { @@ -98,25 +108,17 @@ export const createEventsSlice = (set, get) => ({ ); }, fireEvent: (eventName, id, moduleId, customResolvables, options) => { - const { eventsSlice } = get(); - const { - handleEvent, - isEditorLoading, - module: { - [moduleId]: { events }, - }, - } = eventsSlice; + const { eventsSlice, getCurrentMode, getEditorLoading } = get(); + const { handleEvent } = eventsSlice; + const events = get().eventsSlice.module[moduleId].events; const componentEvents = events.filter((event) => event.sourceId === id); - const mode = get().currentMode; - if (isEditorLoading) return; - // if (mode === 'edit' && eventName === 'onClick') { - // onComponentClick(id, component); - // } + const mode = getCurrentMode(moduleId); + if (getEditorLoading(moduleId)) return; handleEvent( eventName, componentEvents, { ...options, customVariables: { ...customResolvables } }, - 'canvas', + moduleId, mode ); }, @@ -129,7 +131,7 @@ export const createEventsSlice = (set, get) => ({ }, } = eventsSlice; const componentEvents = events.filter((event) => event.sourceId === id); - executeActionsForEventId('onClick', componentEvents, mode); + executeActionsForEventId('onClick', componentEvents, mode, moduleId); }, addEvent: (event, moduleId = 'canvas') => set((state) => { @@ -164,46 +166,46 @@ export const createEventsSlice = (set, get) => ({ createAppVersionEventHandlers: async (event, moduleId) => { // get().actions.setIsSaving(true); // set({ eventsCreatedLoader: true }); - get().eventsSlice.updateEventsField('eventsCreatedLoader', true); - const appId = get().app.appId; + get().eventsSlice.updateEventsField('eventsCreatedLoader', true, moduleId); + const appId = get().appStore.modules[moduleId].app.appId; const versionId = get().currentVersionId; appVersionService .createAppVersionEventHandler(appId, versionId, event) .then((response) => { - get().eventsSlice.updateEventsField('eventsCreatedLoader', false); - get().eventsSlice.addEvent(response); + get().eventsSlice.updateEventsField('eventsCreatedLoader', false, moduleId); + get().eventsSlice.addEvent(response, moduleId); }) .catch((err) => { - get().eventsSlice.updateEventsField('eventsCreatedLoader', false); + get().eventsSlice.updateEventsField('eventsCreatedLoader', false, moduleId); toast.error(err?.error || 'An error occurred while creating the event handler'); }); }, deleteAppVersionEventHandler: async (eventId, index, moduleId = 'canvas') => { - const appId = get().app.appId; + const appId = get().appStore.modules[moduleId].app.appId; const versionId = get().currentVersionId; - get().eventsSlice.updateEventsField('eventToDeleteLoaderIndex', index); + get().eventsSlice.updateEventsField('eventToDeleteLoaderIndex', index, moduleId); const response = await appVersionService.deleteAppVersionEventHandler(appId, versionId, eventId); - get().eventsSlice.updateEventsField('eventToDeleteLoaderIndex', null); + get().eventsSlice.updateEventsField('eventToDeleteLoaderIndex', null, moduleId); if (response?.affected === 1) { - get().eventsSlice.removeEvent(eventId); + get().eventsSlice.removeEvent(eventId, moduleId); } }, updateAppVersionEventHandlers: async (events, updateType = 'update', param, moduleId = 'canvas') => { if (param === 'actionId') { - get().eventsSlice.updateEventsField('actionsUpdatedLoader', true); + get().eventsSlice.updateEventsField('actionsUpdatedLoader', true, moduleId); } if (param === 'eventId') { - get().eventsSlice.updateEventsField('eventsUpdatedLoader', true); + get().eventsSlice.updateEventsField('eventsUpdatedLoader', true, moduleId); } const componentNameIdMapping = get().modules['canvas'].componentNameIdMapping; const queryNameIdMapping = get().modules['canvas'].queryNameIdMapping; //! Revisit this - const appId = get().app.appId; + const appId = get().appStore.modules[moduleId].app.appId; const versionId = get().currentVersionId; const newEvents = replaceEntityReferencesWithIds(events, componentNameIdMapping, queryNameIdMapping); const response = await appVersionService.saveAppVersionEventHandlers(appId, versionId, newEvents, updateType); - get().eventsSlice.updateEventsField('actionsUpdatedLoader', false); - get().eventsSlice.updateEventsField('eventsUpdatedLoader', false); + get().eventsSlice.updateEventsField('actionsUpdatedLoader', false, moduleId); + get().eventsSlice.updateEventsField('eventsUpdatedLoader', false, moduleId); set((state) => { const eventsInState = state.eventsSlice.getModuleEvents('canvas'); const newEvents = eventsInState.map((event) => { @@ -266,19 +268,19 @@ export const createEventsSlice = (set, get) => ({ return foundEvent && foundEvent.name === eventName; }); try { - return get().eventsSlice.onEvent(eventName, filteredEvents, options, mode); + return get().eventsSlice.onEvent(eventName, filteredEvents, options, mode, moduleId); } catch (error) { console.error(error); } }, - onEvent: async (eventName, events, options = {}, mode = 'edit') => { + onEvent: async (eventName, events, options = {}, mode = 'edit', moduleId = 'canvas') => { const executeActionsForEventId = get().eventsSlice.executeActionsForEventId; const customVariables = options?.customVariables ?? {}; const { setExposedValue } = get(); if (eventName === 'onPageLoad') { // for onPageLoad events, we need to execute the actions after the page is loaded - executeActionsForEventId('onPageLoad', events, mode, customVariables); + executeActionsForEventId('onPageLoad', events, mode, customVariables, moduleId); } if (eventName === 'onTrigger') { const { queryPanel, dataQuery } = get(); @@ -287,7 +289,7 @@ export const createEventsSlice = (set, get) => ({ const { queryName, parameters } = options; const queryId = queries.filter((query) => query.name === queryName && isQueryRunnable(query))?.[0]?.id; if (!queryId) return; - runQuery(queryId, queryName, true, mode, parameters); + runQuery(queryId, queryName, true, mode, parameters, undefined, undefined, false, false, moduleId); } if (eventName === 'onTableActionButtonClicked') { const { action, tableActionEvents } = options; @@ -296,7 +298,7 @@ export const createEventsSlice = (set, get) => ({ if (action && executeableActions) { for (const event of executeableActions) { if (event?.event?.actionId) { - await get().eventsSlice.executeAction(event.event, mode, customVariables); + await get().eventsSlice.executeAction(event.event, mode, customVariables, moduleId); } } } else { @@ -310,7 +312,7 @@ export const createEventsSlice = (set, get) => ({ if (column && tableColumnEvents) { for (const event of tableColumnEvents) { if (event?.event?.actionId) { - await get().eventsSlice.executeAction(event.event, mode, customVariables); + await get().eventsSlice.executeAction(event.event, mode, customVariables, moduleId); } } } else { @@ -321,13 +323,13 @@ export const createEventsSlice = (set, get) => ({ if (eventName === 'onCalendarEventSelect') { const { id, calendarEvent } = options; setExposedValue(id, 'selectedEvent', calendarEvent); - executeActionsForEventId('onCalendarEventSelect', events, mode, customVariables); + executeActionsForEventId('onCalendarEventSelect', events, mode, customVariables, moduleId); } if (eventName === 'onCalendarSlotSelect') { const { id, selectedSlots } = options; setExposedValue(id, 'selectedSlots', selectedSlots); - executeActionsForEventId('onCalendarSlotSelect', events, mode, customVariables); + executeActionsForEventId('onCalendarSlotSelect', events, mode, customVariables, moduleId); } if ( @@ -385,31 +387,31 @@ export const createEventsSlice = (set, get) => ({ 'onTableDataDownload', ].includes(eventName) ) { - executeActionsForEventId(eventName, events, mode, customVariables); + executeActionsForEventId(eventName, events, mode, customVariables, moduleId); } if (eventName === 'onBulkUpdate') { - await executeActionsForEventId(eventName, events, mode, customVariables); + await executeActionsForEventId(eventName, events, mode, customVariables, moduleId); } if (['onDataQuerySuccess', 'onDataQueryFailure'].includes(eventName)) { if (!events || !Array.isArray(events) || events.length === 0) return; - await executeActionsForEventId(eventName, events, mode, customVariables); + await executeActionsForEventId(eventName, events, mode, customVariables, moduleId); } }, - executeActionsForEventId: async (eventId, events = [], mode, customVariables) => { + executeActionsForEventId: async (eventId, events = [], mode, customVariables, moduleId = 'canvas') => { if (!events || !Array.isArray(events) || events.length === 0) return; const filteredEvents = events ?.filter((event) => event?.event.eventId === eventId) ?.sort((a, b) => a.index - b.index); for (const event of filteredEvents) { - await get().eventsSlice.executeAction(event, mode, customVariables); + await get().eventsSlice.executeAction(event, mode, customVariables, moduleId); } }, logError(errorType, errorKind, error, eventObj = '', options = {}, logLevel = 'error') { const { event = eventObj } = eventObj; const pages = get().modules.canvas.pages; - const currentPageId = get().currentPageId; + const currentPageId = get().getCurrentPageId('canvas'); const currentPage = pages.find((page) => page.id === currentPageId); const componentIdMapping = get().modules['canvas'].componentNameIdMapping; const componentName = Object.keys(componentIdMapping).find( @@ -479,12 +481,12 @@ export const createEventsSlice = (set, get) => ({ timestamp: moment().toISOString(), }); }, - executeAction: debounce(async (eventObj, mode, customVariables = {}) => { + executeAction: debounce(async (eventObj, mode, customVariables = {}, moduleId = 'canvas') => { const { event = eventObj } = eventObj; const { getExposedValueOfComponent, getResolvedValue } = get(); if (event?.runOnlyIf) { - const shouldRun = getResolvedValue(event.runOnlyIf, customVariables); + const shouldRun = getResolvedValue(event.runOnlyIf, customVariables, moduleId); if (!shouldRun) { return false; } @@ -494,7 +496,8 @@ export const createEventsSlice = (set, get) => ({ //! TODO run only if conditions switch (event.actionId) { case 'show-alert': { - let message = getResolvedValue(event.message, customVariables); + let message = getResolvedValue(event.message, customVariables, moduleId); + if (typeof message === 'object') message = JSON.stringify(message); switch (event.alertType) { @@ -552,16 +555,32 @@ export const createEventsSlice = (set, get) => ({ if (!queryId && !queryName) { throw new Error('No query selected'); } + // Check and replace the module input dummy queries with the linked query id + /* Logic starts here */ + const moduleInputDummyQueries = get()?.getModuleInputDummyQueries?.() || {}; + let updatedQueryId = queryId, + updatedQueryName = queryName, + updatedModuleId = moduleId; + if (moduleInputDummyQueries[queryId]) { + updatedQueryId = + get().resolvedStore.modules[moduleId].exposedValues.input[moduleInputDummyQueries[queryId]]?.id; + updatedModuleId = 'canvas'; // Updating the moduleId to canvas as the query is a module input query which will be present on canvas + } + /* Logic ends here */ + + if (!updatedQueryId) { + throw new Error('No query selected'); + } const resolvedParams = {}; if (params) { Object.keys(params).map( - (param) => (resolvedParams[param] = getResolvedValue(params[param], undefined)) + (param) => (resolvedParams[param] = getResolvedValue(params[param], undefined, moduleId)) ); } // !Todo tackle confirm query part once done return get().queryPanel.runQuery( - queryId, - queryName, + updatedQueryId, + updatedQueryName, undefined, undefined, resolvedParams, @@ -569,7 +588,7 @@ export const createEventsSlice = (set, get) => ({ eventId, false, false, - 'canvas' + updatedModuleId ); } catch (error) { get().eventsSlice.logError('run_query', 'run-query', error, eventObj, { @@ -583,7 +602,7 @@ export const createEventsSlice = (set, get) => ({ } case 'open-webpage': { //! if resolvecode default value should be the value itself not empty string ... Ask KAVIN - const resolvedValue = getResolvedValue(event.url, customVariables); + const resolvedValue = getResolvedValue(event.url, customVariables, moduleId); // const url = resolveReferences(event.url, undefined, customVariables); window.open(resolvedValue, event?.windowTarget === 'newTab' ? '_blank' : '_self'); return Promise.resolve(); @@ -593,7 +612,7 @@ export const createEventsSlice = (set, get) => ({ if (!event.slug) { throw new Error('No application slug provided'); } - const resolvedValue = getResolvedValue(event.slug, customVariables); + const resolvedValue = getResolvedValue(event.slug, customVariables, moduleId); const slug = resolvedValue; const queryParams = event.queryParams?.reduce( (result, queryParam) => ({ @@ -631,23 +650,23 @@ export const createEventsSlice = (set, get) => ({ case 'close-modal': return get().eventsSlice.showModal(event.modal, false, eventObj); case 'copy-to-clipboard': { - const contentToCopy = getResolvedValue(event.contentToCopy, customVariables); + const contentToCopy = getResolvedValue(event.contentToCopy, customVariables, moduleId); copyToClipboard(contentToCopy); return Promise.resolve(); } case 'set-localstorage-value': { - const key = getResolvedValue(event.key, customVariables); - const value = getResolvedValue(event.value, customVariables); + const key = getResolvedValue(event.key, customVariables, moduleId); + const value = getResolvedValue(event.value, customVariables, moduleId); localStorage.setItem(key, value); return Promise.resolve(); } case 'generate-file': { // const fileType = event.fileType; - const data = getResolvedValue(event.data, customVariables) || []; - const fileName = getResolvedValue(event.fileName, customVariables) || 'data.txt'; - const fileType = getResolvedValue(event.fileType, customVariables) || 'csv'; + const data = getResolvedValue(event.data, customVariables, moduleId) || []; + const fileName = getResolvedValue(event.fileName, customVariables, moduleId) || 'data.txt'; + const fileType = getResolvedValue(event.fileType, customVariables, moduleId) || 'csv'; const fileData = { csv: generateCSV, plaintext: (plaintext) => plaintext, @@ -658,15 +677,22 @@ export const createEventsSlice = (set, get) => ({ } case 'set-table-page': { - get().eventsSlice.setTablePageIndex(event.table, getResolvedValue(event.pageIndex), eventObj); + get().eventsSlice.setTablePageIndex( + event.table, + getResolvedValue(event.pageIndex, undefined, moduleId), + eventObj + ); break; } case 'set-custom-variable': { const { setVariable } = get(); - const key = getResolvedValue(event.key, customVariables); - const value = getResolvedValue(event.value, customVariables); - setVariable(key, value); + const key = getResolvedValue(event.key, customVariables, moduleId); + const value = getResolvedValue(event.value, customVariables, moduleId); + + console.log('here--- set-custom-variable', key, value, moduleId); + + setVariable(key, value, moduleId); return Promise.resolve(); // customAppVariables[key] = value; // const resp = useCurrentStateStore.getState().actions.setCurrentState({ @@ -687,20 +713,20 @@ export const createEventsSlice = (set, get) => ({ case 'get-custom-variable': { const { getVariable } = get(); - const key = getResolvedValue(event.key, customVariables); - return getVariable(key); + const key = getResolvedValue(event.key, customVariables, moduleId); + return getVariable(key, moduleId); } case 'unset-all-custom-variables': { const { unsetAllVariables } = get(); - unsetAllVariables(); + unsetAllVariables(moduleId); return Promise.resolve(); } case 'unset-custom-variable': { const { unsetVariable } = get(); - const key = getResolvedValue(event.key, customVariables); - unsetVariable(key); + const key = getResolvedValue(event.key, customVariables, moduleId); + unsetVariable(key, moduleId); return Promise.resolve(); // const customAppVariables = { ...getCurrentState().variables }; // delete customAppVariables[key]; @@ -717,9 +743,9 @@ export const createEventsSlice = (set, get) => ({ case 'set-page-variable': { const { setPageVariable } = get(); - const key = getResolvedValue(event.key, customVariables); - const value = getResolvedValue(event.value, customVariables); - setPageVariable(key, value); + const key = getResolvedValue(event.key, customVariables, moduleId); + const value = getResolvedValue(event.value, customVariables, moduleId); + setPageVariable(key, value, moduleId); return Promise.resolve(); // const customPageVariables = { // ...getCurrentState().page.variables, @@ -749,20 +775,20 @@ export const createEventsSlice = (set, get) => ({ case 'get-page-variable': { const { getPageVariable } = get(); - const key = getResolvedValue(event.key, customVariables); - return getPageVariable(key); + const key = getResolvedValue(event.key, customVariables, moduleId); + return getPageVariable(key, moduleId); } case 'unset-all-page-variables': { const { unsetAllPageVariables } = get(); - unsetAllPageVariables(); + unsetAllPageVariables(moduleId); return Promise.resolve(); } case 'unset-page-variable': { const { unsetPageVariable } = get(); - const key = getResolvedValue(event.key, customVariables); - unsetPageVariable(key); + const key = getResolvedValue(event.key, customVariables, moduleId); + unsetPageVariable(key, moduleId); return Promise.resolve(); // useStore.getState().unsetPageVariable(key); @@ -829,17 +855,12 @@ export const createEventsSlice = (set, get) => ({ // })); // console.log('actionArguments', event.componentSpecificActionParams); const actionArguments = event.componentSpecificActionParams.map((param) => { - const value = getResolvedValue(param.value, customVariables); + const value = getResolvedValue(param.value, customVariables, moduleId); return { ...param, value: value, - // value: resolveCode(re.valueWithBrackets, getAllExposedValues()), }; }); - // const actionArguments = _.map(event.componentSpecificActionParams, (param) => ({ - // ...param, - // value: resolveReferences(param.value, getAllExposedValues(), customVariables), - // })); const actionPromise = action && action(...actionArguments.map((argument) => argument.value)); return actionPromise ?? Promise.resolve(); @@ -858,7 +879,7 @@ export const createEventsSlice = (set, get) => ({ throw new Error('No page ID provided'); } const { switchPage } = get(); - const page = get().modules.canvas.pages.find((page) => page.id === event.pageId); + const page = get().modules[moduleId].pages.find((page) => page.id === event.pageId); const queryParams = event.queryParams || []; if (page.restricted && mode !== 'edit') { toast.error('Access to this page is restricted. Contact admin to know more.'); @@ -866,8 +887,8 @@ export const createEventsSlice = (set, get) => ({ const resolvedQueryParams = []; queryParams.forEach((param) => { resolvedQueryParams.push([ - getResolvedValue(param[0], customVariables), - getResolvedValue(param[1], customVariables), + getResolvedValue(param[0], customVariables, moduleId), + getResolvedValue(param[1], customVariables, moduleId), ]); }); const currentUrlParams = new URLSearchParams(window.location.search); @@ -880,7 +901,7 @@ export const createEventsSlice = (set, get) => ({ } } }); - switchPage(page.id, page.handle, resolvedQueryParams); + switchPage(page.id, page.handle, resolvedQueryParams, moduleId); } else { toast.error('Page is disabled'); //!TODO push to debugger @@ -904,14 +925,14 @@ export const createEventsSlice = (set, get) => ({ } }), - generateAppActions: (queryId, mode, isPreview = false) => { + generateAppActions: (queryId, mode, isPreview = false, moduleId = 'canvas') => { const { getCurrentPageComponents, dataQuery, eventsSlice, queryPanel, modules } = get(); const { previewQuery } = queryPanel; const { executeAction } = eventsSlice; - const currentComponents = Object.entries(getCurrentPageComponents()); + const currentComponents = Object.entries(getCurrentPageComponents(moduleId)); - const runQuery = (queryName = '', parameters) => { - const query = dataQuery.queries.modules['canvas'].find((query) => { + const runQuery = (queryName = '', parameters, moduleId = 'canvas') => { + const query = dataQuery.queries.modules[moduleId].find((query) => { const isFound = query.name === queryName; if (isPreview) { return isFound; @@ -944,7 +965,7 @@ export const createEventsSlice = (set, get) => ({ parameters: processedParams, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const setVariable = (key = '', value = '') => { @@ -954,7 +975,7 @@ export const createEventsSlice = (set, get) => ({ key, value, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); } }; @@ -964,7 +985,7 @@ export const createEventsSlice = (set, get) => ({ actionId: 'get-custom-variable', key, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); } }; @@ -981,7 +1002,7 @@ export const createEventsSlice = (set, get) => ({ actionId: 'unset-custom-variable', key, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); } }; @@ -991,14 +1012,14 @@ export const createEventsSlice = (set, get) => ({ alertType, message, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const logout = () => { const event = { actionId: 'logout', }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const showModal = (modalName = '') => { @@ -1013,7 +1034,7 @@ export const createEventsSlice = (set, get) => ({ actionId: 'show-modal', modal, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const closeModal = (modalName = '') => { @@ -1028,7 +1049,7 @@ export const createEventsSlice = (set, get) => ({ actionId: 'close-modal', modal, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const setLocalStorage = (key = '', value = '') => { @@ -1037,7 +1058,7 @@ export const createEventsSlice = (set, get) => ({ key, value, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const copyToClipboard = (contentToCopy = '') => { @@ -1045,7 +1066,7 @@ export const createEventsSlice = (set, get) => ({ actionId: 'copy-to-clipboard', contentToCopy, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const goToApp = (slug = '', queryParams = []) => { @@ -1054,7 +1075,7 @@ export const createEventsSlice = (set, get) => ({ slug, queryParams, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const generateFile = (fileName, fileType, data) => { @@ -1068,7 +1089,7 @@ export const createEventsSlice = (set, get) => ({ data, fileType, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const setPageVariable = (key = '', value = '') => { @@ -1077,7 +1098,7 @@ export const createEventsSlice = (set, get) => ({ key, value, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const getPageVariable = (key = '') => { @@ -1085,7 +1106,7 @@ export const createEventsSlice = (set, get) => ({ actionId: 'get-page-variable', key, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const unsetAllPageVariables = () => { @@ -1100,10 +1121,10 @@ export const createEventsSlice = (set, get) => ({ actionId: 'unset-page-variable', key, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; - const switchPage = (pageHandle, queryParams = []) => { + const switchPage = (pageHandle, queryParams = [], moduleId = 'canvas') => { if (isPreview) { mode != 'view' && toast('Page will not be switched for query preview', { @@ -1111,7 +1132,7 @@ export const createEventsSlice = (set, get) => ({ }); return Promise.resolve(); } - const pages = modules.canvas.pages; + const pages = modules[moduleId].pages; const transformedPageHandle = pageHandle?.toLowerCase(); const pageId = pages.find((page) => page.handle === transformedPageHandle)?.id; @@ -1128,7 +1149,7 @@ export const createEventsSlice = (set, get) => ({ pageId, queryParams, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const logInfo = (log, isFromTransformation) => { @@ -1144,7 +1165,7 @@ export const createEventsSlice = (set, get) => ({ eventType: 'customLog', query, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const logError = (log, isFromTransformation = false) => { @@ -1160,7 +1181,7 @@ export const createEventsSlice = (set, get) => ({ eventType: 'customLog', query, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; const log = (log, isFromTransformation = false) => { @@ -1176,7 +1197,7 @@ export const createEventsSlice = (set, get) => ({ eventType: 'customLog', query, }; - return executeAction(event, mode, {}); + return executeAction(event, mode, {}, moduleId); }; return { diff --git a/frontend/src/AppBuilder/_stores/slices/inspectorSlice.js b/frontend/src/AppBuilder/_stores/slices/inspectorSlice.js new file mode 100644 index 0000000000..c20768a257 --- /dev/null +++ b/frontend/src/AppBuilder/_stores/slices/inspectorSlice.js @@ -0,0 +1,139 @@ +const initialState = { + selectedNodes: new Set(), + searchedNodes: new Set(), + inspectorSearchValue: '', + inspectorSearchResults: new Set(), + selectedNodePath: null, +}; + +export const createInspectorSlice = (set, get) => ({ + ...initialState, + getSelectedNodes: () => { + const selectedNodes = get().selectedNodes; + return Array.from(selectedNodes); + }, + setSelectedNodes: (node) => { + const selectedNodes = get().selectedNodes; + const newSelectedNodes = new Set(selectedNodes); + if (newSelectedNodes.has(node)) { + newSelectedNodes.delete(node); + } else { + newSelectedNodes.add(node); + } + set({ selectedNodes: newSelectedNodes }); + }, + getInspectorSearchResults: () => { + const inspectorSearchResults = get().inspectorSearchResults; + return Array.from(inspectorSearchResults); + }, + setInspectorSearchValue: (value) => { + set({ inspectorSearchValue: value }); + }, + setInspectorSearchResults: (results) => { + set({ inspectorSearchResults: results }); + }, + setSelectedNodePath: (path) => { + set({ selectedNodePath: path }); + }, + getAllComponentChildrenById: (id) => { + const { getComponentDefinition, getResolvedComponent } = get(); + const component = getComponentDefinition(id); + const componentType = component?.component?.component; + switch (componentType) { + case 'Container': + case 'Form': + case 'ModalV2': + return [ + ...get().getContainerChildrenMapping(id), + ...get().getContainerChildrenMapping(`${id}-header`), + ...get().getContainerChildrenMapping(`${id}-footer`), + ]; + case 'Tabs': { + const tabs = getResolvedComponent(id)?.properties?.tabs; + const children = Array.isArray(tabs) ? tabs : []; + const res = children + ?.map((tab) => { + const tabId = `${id}-${tab.id}`; + return get().getContainerChildrenMapping(tabId); + }) + .reduce((acc, curr) => { + return [...acc, ...curr]; + }, []); + return res; + } + default: + return get().getContainerChildrenMapping(id); + } + }, + + formatInspectorComponentData: ( + componentIdNameMapping, + exposedComponentsVariables, + searchablePaths = new Set(), + moduleId = 'canvas' + ) => { + const { getComponentDefinition, getAllComponentChildrenById } = get(); + const data = Object.entries(componentIdNameMapping) + .filter(([key]) => { + const component = getComponentDefinition(key, moduleId); + return !component?.component?.parent; + }) + .map(([key, name]) => { + const component = getComponentDefinition(key, moduleId); + let parentComponentType = null; + if (component?.component?.parent) { + const parentComponent = getComponentDefinition(component.component.parent, moduleId); + parentComponentType = parentComponent?.component?.component; + } + return { + key, + name: name || key, + parentType: parentComponentType, + }; + }) + .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); + + const reduceData = (obj, path = 'components', level = 1) => { + let data = obj; + if (!obj || typeof obj !== 'object') return []; + + return data + .filter((item) => item.name) + .reduce((acc, { key, name, parentType }) => { + const currentPath = `components.${name}`; + const actualPath = `${path}.${name}`; + searchablePaths.add(actualPath); + const children = getAllComponentChildrenById(key).map((childKey) => { + const childComponent = getComponentDefinition(childKey); + let parentComponentType = null; + if (childComponent?.component?.parent) { + const parentComponent = getComponentDefinition(childComponent.component.parent); + parentComponentType = parentComponent?.component?.component; + } + return { + key: childKey, + name: childComponent?.component?.name, + parentType: parentComponentType, + }; + }); + + return [ + ...acc, + { + id: actualPath, + name, + children: reduceData(children, actualPath, level + 1), + metadata: { + type: 'components', + path: currentPath, + parentType: parentType, + actualPath, + }, + }, + ]; + }, []); + }; + + return reduceData(data); + }, +}); diff --git a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js index 367ca4cf0c..8a3989a05b 100644 --- a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js @@ -30,16 +30,23 @@ export const createLeftSideBarSlice = (set, get) => ({ ), setPathToBeInspected: (pathToBeInspected) => set(() => ({ pathToBeInspected }), false, 'setPathToBeInspected'), setComponentToInspect: (componentToInspect) => { - const { setPathToBeInspected, setSelectedSidebarItem, toggleLeftSidebar, selectedSidebarItem } = get(); - setPathToBeInspected(['components', componentToInspect]); + const { + setPathToBeInspected, + setSelectedSidebarItem, + toggleLeftSidebar, + selectedSidebarItem, + setSelectedNodePath, + } = get(); + // setPathToBeInspected(['components', componentToInspect]); + setSelectedNodePath(`components.${componentToInspect}`); if (selectedSidebarItem !== 'inspect') { setSelectedSidebarItem('inspect'); toggleLeftSidebar(true); } }, - getComponentIdToAutoScroll: (componentId) => { + getComponentIdToAutoScroll: (componentId, moduleId = 'canvas') => { const { getCurrentPageComponents, getAllExposedValues, modalsOpenOnCanvas } = get(); - const currentPageComponents = getCurrentPageComponents(); + const currentPageComponents = getCurrentPageComponents(moduleId); let targetComponentId = componentId; let current = componentId; @@ -66,7 +73,7 @@ export const createLeftSideBarSlice = (set, get) => ({ const tabId = parentId.replace(regForTabs, ''); // Extract tab id from parent id - const { currentTab } = getAllExposedValues().components?.[tabId] || {}; + const { currentTab } = getAllExposedValues(moduleId).components?.[tabId] || {}; const activeTabIndex = Number(currentTab); nextPossibleCandidate = tabId; diff --git a/frontend/src/AppBuilder/_stores/slices/loaderSlice.js b/frontend/src/AppBuilder/_stores/slices/loaderSlice.js index 6b1da506e1..6a0ef71271 100644 --- a/frontend/src/AppBuilder/_stores/slices/loaderSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/loaderSlice.js @@ -1,10 +1,43 @@ const initialState = { - isEditorLoading: true, - isCanvasLoading: false, + loaderStore: { + modules: { + canvas: { + isEditorLoading: true, + }, + }, + }, }; -export const createLoaderSlice = (set) => ({ +export const createLoaderSlice = (set, get) => ({ ...initialState, - setEditorLoading: (status) => set(() => ({ isEditorLoading: status }), false, 'setEditorLoading'), - setCanvasLoading: (status) => set(() => ({ isCanvasLoading: status }), false, 'setCanvasLoading'), + initializeLoaderSlice: (moduleId) => { + set( + (state) => { + state.loaderStore.modules[moduleId] = { + ...initialState.loaderStore.modules.canvas, + }; + }, + false, + 'initializeLoaderSlice' + ); + }, + setEditorLoading: (status, moduleId = 'canvas') => + set( + (state) => { + state.loaderStore.modules[moduleId].isEditorLoading = status; + }, + false, + 'setEditorLoading' + ), + setIsLoaderLoading: (status, moduleId = 'canvas') => + set( + (state) => { + state.loaderStore.modules[moduleId] = { + isLoaderLoading: status, + }; + }, + false, + 'setIsLoaderLoading' + ), + getEditorLoading: (moduleId) => get().loaderStore.modules[moduleId].isEditorLoading, }); diff --git a/frontend/src/AppBuilder/_stores/slices/modeSlice.js b/frontend/src/AppBuilder/_stores/slices/modeSlice.js index a24d5c93d6..ce2672a91a 100644 --- a/frontend/src/AppBuilder/_stores/slices/modeSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/modeSlice.js @@ -1,8 +1,33 @@ const initialState = { - currentMode: 'view', + modeStore: { + modules: { + canvas: { + currentMode: 'view', + }, + }, + }, }; -export const createModeSlice = (set) => ({ +export const createModeSlice = (set, get) => ({ ...initialState, - setCurrentMode: (currentMode) => set(() => ({ currentMode }), false, 'setCurrentMode'), + initializeModeSlice: (moduleId) => { + set( + (state) => { + state.modeStore.modules[moduleId] = { + ...initialState.modeStore.modules.canvas, + }; + }, + false, + 'initializeModeSlice' + ); + }, + setCurrentMode: (currentMode, moduleId = 'canvas') => + set( + (state) => { + state.modeStore.modules[moduleId].currentMode = currentMode; + }, + false, + 'setCurrentMode' + ), + getCurrentMode: (moduleId) => get().modeStore.modules[moduleId].currentMode, }); diff --git a/frontend/src/AppBuilder/_stores/slices/moduleSlice.js b/frontend/src/AppBuilder/_stores/slices/moduleSlice.js new file mode 100644 index 0000000000..5c97f74cf1 --- /dev/null +++ b/frontend/src/AppBuilder/_stores/slices/moduleSlice.js @@ -0,0 +1,5 @@ +import { getEditionSpecificSlice } from '../../../modules/common/helpers/getEditionSpecificSlice'; + +const createModuleSlice = getEditionSpecificSlice('createModuleSlice'); + +export { createModuleSlice }; diff --git a/frontend/src/AppBuilder/_stores/slices/multiplayerSlice.js b/frontend/src/AppBuilder/_stores/slices/multiplayerSlice.js index f025563f66..8043a0bd18 100644 --- a/frontend/src/AppBuilder/_stores/slices/multiplayerSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/multiplayerSlice.js @@ -21,14 +21,14 @@ export const createMultiplayerSlice = (set, get) => ({ diff, type, operation, - pageId: get().currentPageId, + pageId: get().getCurrentPageId('canvas'), versionId: get().selectedVersion?.id, }); } }, processUpdate: ({ diff, type, operation, pageId, versionId }) => { - const currentPageId = get().currentPageId; + const currentPageId = get().getCurrentPageId('canvas'); const currentVersionId = get().selectedVersion?.id; if (currentPageId === pageId && currentVersionId === versionId) diff --git a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js index 45a5b86428..5ece338ed8 100644 --- a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js @@ -55,7 +55,8 @@ const createPageUpdateCommand = } }); - const { app, currentVersionId } = get(); + const { appStore, currentVersionId } = get(); + const app = appStore.modules.canvas.app; const diff = _.zipObject(updatePaths, values); if (enableSave) savePageChanges(app.appId, currentVersionId, pageId, diff); }; @@ -199,10 +200,8 @@ export const createPageMenuSlice = (set, get) => { updatePageWithPermissions: (pageId, value) => updatePageWithPermissions(pageId, [value])(set, get), // unsure about this one clonePage: async (pageId) => { - const { - app: { appId }, - currentVersionId, - } = get(); + const { getAppId, currentVersionId } = get(); + const appId = getAppId('canvas'); const pages = get().modules.canvas.pages; const data = await appVersionService.clonePage(appId, currentVersionId, pageId); const newPages = data?.pages; @@ -220,19 +219,21 @@ export const createPageMenuSlice = (set, get) => { } }, deletePage: async (pageId) => { - const { app, currentVersionId } = get(); + const { getAppId, getHomePageId, currentVersionId } = get(); + const appId = getAppId('canvas'); + const homePageId = getHomePageId('canvas'); const diff = { pageId: pageId, }; const pages = get().modules.canvas.pages; - const currentPageId = get().currentPageId; + const currentPageId = get().getCurrentPageId('canvas'); const switchPage = get().switchPage; if (pages.length === 1) { toast.error('You cannot delete the only page in your app.'); return; } if (currentPageId === pageId) { - const homePage = pages.find((p) => p.id === app.homePageId); + const homePage = pages.find((p) => p.id === homePageId); switchPage(homePage.id, homePage.handle); } set((state) => { @@ -241,7 +242,7 @@ export const createPageMenuSlice = (set, get) => { state.showEditingPopover = false; state.editingPage = null; }); - await savePageChanges(app.appId, currentVersionId, pageId, diff, 'delete'); + await savePageChanges(appId, currentVersionId, pageId, diff, 'delete'); toast.success('Page deleted successfully'); }, /* @@ -250,11 +251,11 @@ export const createPageMenuSlice = (set, get) => { * If home page is in the group, the group cannot be deleted * If current page is in the group, the page will be switched to home page */ - deletePageGroup: async (pageGroupId, deleteAssociatedPages = false) => { - const { app, currentVersionId } = get(); + deletePageGroup: async (pageGroupId, deleteAssociatedPages = false, moduleId = 'canvas') => { + const { getAppId, getHomePageId, currentVersionId } = get(); + const appId = getAppId(moduleId); + const homePageId = getHomePageId(moduleId); const pages = get().modules.canvas.pages; - - const homePageId = get().app.homePageId; const diff = { pageId: pageGroupId, deleteAssociatedPages, @@ -267,7 +268,7 @@ export const createPageMenuSlice = (set, get) => { if (pages[i].id === homePageId && pages[i].pageGroupId === pageGroupId) { isHomePageInGroup = true; } - if (pages[i].id === get().currentPageId && pages[i].pageGroupId === pageGroupId) { + if (pages[i].id === get().getCurrentPageId('canvas') && pages[i].pageGroupId === pageGroupId) { isCurrentPageInGroup = true; } } @@ -284,10 +285,10 @@ export const createPageMenuSlice = (set, get) => { }); // switch page to home page if current page is in the group if (isCurrentPageInGroup) { - const homePage = pages.find((p) => p.id === app.homePageId); + const homePage = pages.find((p) => p.id === homePageId); get().switchPage(homePage.id, homePage.handle); } - await savePageChanges(app.appId, currentVersionId, pageGroupId, diff, 'delete'); + await savePageChanges(appId, currentVersionId, pageGroupId, diff, 'delete'); } else { set((state) => { const pages = get().modules.canvas.pages; @@ -306,25 +307,26 @@ export const createPageMenuSlice = (set, get) => { state.modules.canvas.pages = newPages; state.showDeleteConfirmationModal = false; }); - await savePageChanges(app.appId, currentVersionId, pageGroupId, diff, 'delete'); + await savePageChanges(appId, currentVersionId, pageGroupId, diff, 'delete'); } }, - markAsHomePage: async (pageId) => { - const { app, currentVersionId, editingPage } = get(); + markAsHomePage: async (pageId, moduleId = 'canvas') => { + const { getAppId, currentVersionId, editingPage } = get(); + const appId = getAppId(moduleId); const diff = { homePageId: pageId, }; set((state) => { - state.app.homePageId = pageId; + state.appStore.modules[moduleId].app.homePageId = pageId; state.showEditingPopover = false; state.editingPage = null; }); - await savePageChanges(app.appId, currentVersionId, editingPage.id, diff, 'update', null); + await savePageChanges(appId, currentVersionId, editingPage.id, diff, 'update', null); }, reorderPages: async (reorderdPages) => { const diff = {}; - const currentPageId = get().currentPageId; + const currentPageId = get().getCurrentPageId('canvas'); // update index of everything to avoid inconsistencies reorderdPages.forEach((page, index) => { diff[page.id] = { @@ -336,8 +338,9 @@ export const createPageMenuSlice = (set, get) => { set((state) => { state.modules.canvas.pages = reorderdPages; }); - const { app, currentVersionId } = get(); - await savePageChanges(app.appId, currentVersionId, currentPageId, diff, 'update', 'pages/reorder'); + const { getAppId, currentVersionId } = get(); + const appId = getAppId('canvas'); + await savePageChanges(appId, currentVersionId, currentPageId, diff, 'update', 'pages/reorder'); }, addNewPage: async (name, handle, isPageGroup = false) => { @@ -379,8 +382,9 @@ export const createPageMenuSlice = (set, get) => { set((state) => { state.modules.canvas.pages.push(pageObject); }); - const { app, currentVersionId } = get(); - await savePageChanges(app.appId, currentVersionId, '', pageObject, 'create', 'pages'); + const { getAppId, currentVersionId } = get(); + const appId = getAppId('canvas'); + await savePageChanges(appId, currentVersionId, '', pageObject, 'create', 'pages'); if (!isPageGroup) get().switchPage(newPageId, newHandle); }, @@ -408,10 +412,11 @@ export const createPageMenuSlice = (set, get) => { newOptions[key] = hexCode; } } - const { app, currentVersionId, currentPageId } = get(); + const { getAppId, currentVersionId, currentPageId } = get(); + const appId = getAppId('canvas'); try { const res = await appVersionService.autoSaveApp( - app.appId, + appId, currentVersionId, { pageSettings: { [type]: newOptions } }, 'page_settings', diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index ddb67a5445..7344f54aec 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -66,13 +66,13 @@ export const createQueryPanelSlice = (set, get) => ({ 'setQueryPanelHeight' ); }, // updateQueryPanelHeight - setSelectedQuery: (queryId) => { + setSelectedQuery: (queryId, moduleId = 'canvas') => { set((state) => { if (queryId === null) { state.queryPanel.selectedQuery = null; return; } - const query = get().dataQuery.queries.modules.canvas.find((query) => query.id === queryId); + const query = get().dataQuery.queries.modules[moduleId].find((query) => query.id === queryId); state.queryPanel.selectedQuery = query; return; }); @@ -162,7 +162,7 @@ export const createQueryPanelSlice = (set, get) => ({ 'setLoadingDataQueries' ), - onQueryConfirmOrCancel: (queryConfirmationData, isConfirm = false, mode = 'edit') => { + onQueryConfirmOrCancel: (queryConfirmationData, isConfirm = false, mode = 'edit', moduleId = 'canvas') => { const { queryPanel, dataQuery, setResolvedQuery } = get(); const { runQuery } = queryPanel; const { queryConfirmationList } = dataQuery; @@ -185,13 +185,21 @@ export const createQueryPanelSlice = (set, get) => ({ true, mode, queryConfirmationData.parameters, - queryConfirmationData.shouldSetPreviewData + undefined, + undefined, + queryConfirmationData.shouldSetPreviewData, + false, + moduleId ); !isConfirm && - setResolvedQuery(queryConfirmationData.queryId, { - isLoading: false, - }); + setResolvedQuery( + queryConfirmationData.queryId, + { + isLoading: false, + }, + moduleId + ); }, runQuery: ( @@ -211,7 +219,7 @@ export const createQueryPanelSlice = (set, get) => ({ dataQuery: dataQuerySlice, queryPanel, setResolvedQuery, - app, + appStore, selectedEnvironment, isPublicAccess, currentVersionId, @@ -265,14 +273,18 @@ export const createQueryPanelSlice = (set, get) => ({ let dataQuery = {}; //for viewer we will only get the environment id from the url - const { currentAppEnvironmentId, environmentId } = app; + const { currentAppEnvironmentId, environmentId } = appStore.modules[moduleId].app; if (shouldSetPreviewData) { setPreviewPanelExpanded(true); setPreviewLoading(true); - setResolvedQuery(queryId, { - isLoading: true, - }); + setResolvedQuery( + queryId, + { + isLoading: true, + }, + moduleId + ); queryPreviewData && setPreviewData(''); } @@ -293,10 +305,11 @@ export const createQueryPanelSlice = (set, get) => ({ } // const queryState = { ...getCurrentState(), parameters }; - const queryState = { ...get().getAllExposedValues('canvas'), parameters }; + const queryState = { ...get().getAllExposedValues(moduleId), parameters }; + const options = getQueryVariables(dataQuery.options, queryState, { - components: get().getComponentNameIdMapping(), - queries: get().getQueryNameIdMapping(), + components: get().getComponentNameIdMapping(moduleId), + queries: get().getQueryNameIdMapping(moduleId), }); if (dataQuery.options?.requestConfirmation) { const queryConfirmation = { @@ -326,18 +339,22 @@ export const createQueryPanelSlice = (set, get) => ({ queryPreviewData && setPreviewData(''); } - setResolvedQuery(queryId, { - isLoading: true, - data: [], - rawData: [], - id: queryId, - }); + setResolvedQuery( + queryId, + { + isLoading: true, + data: [], + rawData: [], + id: queryId, + }, + moduleId + ); let queryExecutionPromise = null; if (query.kind === 'runjs') { - queryExecutionPromise = executeMultilineJS(query.options.code, query?.id, false, mode, parameters); + queryExecutionPromise = executeMultilineJS(query.options.code, query?.id, false, mode, parameters, moduleId); } else if (query.kind === 'runpy') { - queryExecutionPromise = executeRunPycode(query.options.code, query, false, mode, queryState); + queryExecutionPromise = executeRunPycode(query.options.code, query, false, mode, queryState, moduleId); } else if (query.kind === 'workflows') { queryExecutionPromise = executeWorkflow( moduleId, @@ -347,11 +364,16 @@ export const createQueryPanelSlice = (set, get) => ({ (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id //TODO: currentAppEnvironmentId may no longer required. Need to check ); } else { + let versionId = currentVersionId; + // IMPORTANT: This logic needs to be changed when we implement the module versioning + if (moduleId !== 'canvas') { + versionId = get().resolvedStore.modules.canvas.components[moduleId].properties.moduleVersionId; + } queryExecutionPromise = dataqueryService.run( queryId, options, query?.options, - currentVersionId, + versionId, !isPublicAccess ? (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id : undefined //TODO: currentAppEnvironmentId may no longer required. Need to check ); } @@ -421,17 +443,21 @@ export const createQueryPanelSlice = (set, get) => ({ isQuerySuccessLog: false, }); - setResolvedQuery(queryId, { - isLoading: false, - ...(query.kind === 'restapi' - ? { - metadata: data.metadata, - request: data.data.requestObject, - response: data.data.responseObject, - responseHeaders: data.data.responseHeaders, - } - : {}), - }); + setResolvedQuery( + queryId, + { + isLoading: false, + ...(query.kind === 'restapi' + ? { + metadata: data.metadata, + request: data.data.requestObject, + response: data.data.responseObject, + responseHeaders: data.data.responseHeaders, + } + : {}), + }, + moduleId + ); resolve(data); onEvent('onDataQueryFailure', queryEvents); @@ -445,12 +471,17 @@ export const createQueryPanelSlice = (set, get) => ({ query.options.transformation, query.options.transformationLanguage, query, - 'edit' + 'edit', + moduleId ); - if (finalData?.status === 'failed') { - setResolvedQuery(queryId, { - isLoading: false, - }); + if (finalData.status === 'failed') { + setResolvedQuery( + queryId, + { + isLoading: false, + }, + moduleId + ); resolve(finalData); onEvent('onDataQueryFailure', queryEvents); @@ -482,14 +513,18 @@ export const createQueryPanelSlice = (set, get) => ({ errorTarget: 'Queries', }); - setResolvedQuery(queryId, { - isLoading: false, - data: finalData, - rawData, - metadata: data?.metadata, - request: data?.metadata?.request, - response: data?.metadata?.response, - }); + setResolvedQuery( + queryId, + { + isLoading: false, + data: finalData, + rawData, + metadata: data?.metadata, + request: data?.metadata?.request, + response: data?.metadata?.response, + }, + moduleId + ); resolve({ status: 'ok', data: finalData }); onEvent('onDataQuerySuccess', queryEvents, mode); @@ -557,7 +592,7 @@ export const createQueryPanelSlice = (set, get) => ({ } // const queryState = { ...getCurrentState(), parameters }; - const queryState = { ...get().getAllExposedValues(), parameters }; + const queryState = { ...get().getAllExposedValues(moduleId), parameters }; const options = getQueryVariables(query.options, queryState, { components: get().getComponentNameIdMapping(), queries: get().getQueryNameIdMapping(), @@ -649,7 +684,8 @@ export const createQueryPanelSlice = (set, get) => ({ query.options.transformation, query.options.transformationLanguage, query, - 'edit' + 'edit', + moduleId ); if (finalData?.status === 'failed') { onEvent('onDataQueryFailure', queryEvents); @@ -709,32 +745,32 @@ export const createQueryPanelSlice = (set, get) => ({ let result = {}; try { - const resolvedState = get().getResolvedState(); + const resolvedState = get().getResolvedState(moduleId); const queriesInCurentState = deepClone(resolvedState.queries); const appStateVars = deepClone(resolvedState.variables) ?? {}; if (!isEmpty(query)) { - const actions = generateAppActions(query.id, mode, isPreview); + const actions = generateAppActions(query.id, mode, isPreview, moduleId); for (const key of Object.keys(queriesInCurentState)) { queriesInCurentState[key] = { ...queriesInCurentState[key], run: () => { const query = dataQuery.queries.modules?.[moduleId].find((q) => q.name === key); - return actions.runQuery(query.name); + return actions.runQuery(query.name, undefined, moduleId); }, getData: () => { - const resolvedState = get().getResolvedState(); + const resolvedState = get().getResolvedState(moduleId); return resolvedState.queries[key].data; }, getRawData: () => { - const resolvedState = get().getResolvedState(); + const resolvedState = get().getResolvedState(moduleId); return resolvedState.queries[key].rawData; }, getloadingState: () => { - const resolvedState = get().getResolvedState(); + const resolvedState = get().getResolvedState(moduleId); return resolvedState.queries[key].isLoading; }, }; @@ -776,14 +812,21 @@ export const createQueryPanelSlice = (set, get) => ({ return pyodide.isPyProxy(result) ? convertMapSet(result.toJs()) : result; }, - runTransformation: async (rawData, transformation, transformationLanguage = 'javascript', query, mode = 'edit') => { + runTransformation: async ( + rawData, + transformation, + transformationLanguage = 'javascript', + query, + mode = 'edit', + moduleId = 'canvas' + ) => { const data = rawData; const { queryPanel: { runPythonTransformation, createProxy }, getResolvedState, } = get(); let result = {}; - const currentState = getResolvedState(); + const currentState = getResolvedState(moduleId); if (transformationLanguage === 'python') { result = await runPythonTransformation(currentState, data, transformation, query, mode); @@ -903,12 +946,10 @@ export const createQueryPanelSlice = (set, get) => ({ // queries: updatedQueries, // }); }, - executeWorkflow: async (moduleId, workflowId, _blocking = false, params = {}, appEnvId) => { - const { - app: { appId }, - getAllExposedValues, - } = get(); - const currentState = getAllExposedValues(); + executeWorkflow: async (moduleId = 'canvas', workflowId, _blocking = false, params = {}, appEnvId) => { + const { getAppId, getAllExposedValues } = get(); + const appId = getAppId('canvas'); + const currentState = getAllExposedValues(moduleId); const resolvedParams = get().resolveReferences(moduleId, params, currentState, {}, {}); try { @@ -947,7 +988,7 @@ export const createQueryPanelSlice = (set, get) => ({ return isValidCode; } - const currentState = getAllExposedValues(); + // const currentState = getAllExposedValues(); let result = {}, error = null; @@ -957,7 +998,7 @@ export const createQueryPanelSlice = (set, get) => ({ parameters = {}; } - const actions = generateAppActions(queryId, mode, isPreview); + const actions = generateAppActions(queryId, mode, isPreview, moduleId); const queryDetails = dataQuery.queries.modules?.[moduleId].find((q) => q.id === queryId); @@ -980,7 +1021,7 @@ export const createQueryPanelSlice = (set, get) => ({ //this will handle the preview case where you cannot find the queryDetails in state. formattedParams = { ...parameters }; } - const resolvedState = get().getResolvedState(); + const resolvedState = get().getResolvedState(moduleId); const queriesInResolvedState = deepClone(resolvedState.queries); for (const key of Object.keys(resolvedState.queries)) { queriesInResolvedState[key] = { @@ -992,21 +1033,21 @@ export const createQueryPanelSlice = (set, get) => ({ const processedParams = {}; const query = dataQuery.queries.modules?.[moduleId].find((q) => q.name === key); query.options.parameters?.forEach((arg) => (processedParams[arg.name] = params[arg.name])); - return actions.runQuery(query.name, processedParams); + return actions.runQuery(query.name, processedParams, moduleId); }, getData: () => { - const resolvedState = get().getResolvedState(); + const resolvedState = get().getResolvedState(moduleId); return resolvedState.queries[key].data; }, getRawData: () => { - const resolvedState = get().getResolvedState(); + const resolvedState = get().getResolvedState(moduleId); return resolvedState.queries[key].rawData; }, getloadingState: () => { - const resolvedState = get().getResolvedState(); + const resolvedState = get().getResolvedState(moduleId); return resolvedState.queries[key].isLoading; }, }; diff --git a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js index a253346eec..3b6877b726 100644 --- a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js @@ -3,15 +3,6 @@ import { resolveDynamicValues } from '../utils'; import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast'; import { componentTypeDefinitionMap } from '@/AppBuilder/WidgetManager'; import _ from 'lodash'; -import { - reservedKeyword, - resolveString, - removeNestedDoubleCurlyBraces, - getDynamicVariables, - resolveCode, -} from '@/_helpers/utils'; - -import { validateMultilineCode } from '@/_helpers/utility'; const initialState = { resolvedStore: { @@ -25,7 +16,7 @@ const initialState = { secrets: {}, customResolvables: {}, exposedValues: { - queries: {}, + queries: {} /* IMPORTANT: Query is subscribed by the moduleContainer component */, components: {}, variables: {}, constants: {}, @@ -50,6 +41,17 @@ export const DEFAULT_COMPONENT_STRUCTURE = { export const createResolvedSlice = (set, get) => ({ ...initialState, + initializeResolvedSlice: (moduleId) => { + set( + (state) => { + state.resolvedStore.modules[moduleId] = { + ...initialState.resolvedStore.modules.canvas, + }; + }, + false, + 'initializeResolvedSlice' + ); + }, setResolvedGlobals: (objKey, values, moduleId = 'canvas') => { set( (state) => { @@ -71,7 +73,7 @@ export const createResolvedSlice = (set, get) => ({ 'setResolvedGlobals' ); Object.entries(values).forEach(() => { - get().updateDependencyValues(`globals.${objKey}`); + get().updateDependencyValues(`globals.${objKey}`, moduleId); }); }, setResolvedConstants: (constants = {}, moduleId = 'canvas') => { @@ -85,7 +87,7 @@ export const createResolvedSlice = (set, get) => ({ 'setResolvedConstants' ); Object.entries(constants).forEach(([key, value]) => { - get().updateDependencyValues(`constants.${key}`); + get().updateDependencyValues(`constants.${key}`, moduleId); }); }, @@ -108,7 +110,7 @@ export const createResolvedSlice = (set, get) => ({ 'setResolvedPageConstants' ); Object.entries(constants).forEach(([key, value]) => { - get().updateDependencyValues(`page.${key}`); + get().updateDependencyValues(`page.${key}`, moduleId); }); }, @@ -121,7 +123,7 @@ export const createResolvedSlice = (set, get) => ({ false, 'setVariables' ); - get().updateDependencyValues(`variables.${key}`); + get().updateDependencyValues(`variables.${key}`, moduleId); get().checkAndSetTrueBuildSuggestionsFlag(); }, @@ -137,8 +139,8 @@ export const createResolvedSlice = (set, get) => ({ false, 'unsetVariable' ); - get().removeNode(`variables.${key}`); - get().updateDependencyValues(`variables.${key}`); + get().removeNode(`variables.${key}`, moduleId); + get().updateDependencyValues(`variables.${key}`, moduleId); }, unsetAllVariables: (moduleId = 'canvas') => { @@ -165,7 +167,7 @@ export const createResolvedSlice = (set, get) => ({ false, 'setPageVariable' ); - get().updateDependencyValues(`page.variables.${key}`); + get().updateDependencyValues(`page.variables.${key}`, moduleId); get().checkAndSetTrueBuildSuggestionsFlag(); }, @@ -180,8 +182,8 @@ export const createResolvedSlice = (set, get) => ({ false, 'unsetPageVariable' ); - get().removeNode(`page.variables.${key}`); - get().updateDependencyValues(`page.variables.${key}`); + get().removeNode(`page.variables.${key}`, moduleId); + get().updateDependencyValues(`page.variables.${key}`, moduleId); }, unsetAllPageVariables: (moduleId = 'canvas') => { @@ -213,13 +215,13 @@ export const createResolvedSlice = (set, get) => ({ Object.entries(details).forEach(([key, value]) => { if (['isLoading', 'data', 'rawData', 'request', 'response', 'responseHeaders', 'metadata'].includes(key)) { - if (typeof value !== 'function') get().updateDependencyValues(`queries.${queryId}.${key}`); + if (typeof value !== 'function') get().updateDependencyValues(`queries.${queryId}.${key}`, moduleId); } }); // Flag to update the codehinter suggestions get().checkAndSetTrueBuildSuggestionsFlag(); }, - initialiseResolvedQuery(querIds, moduleId = 'canvas') { + initialiseResolvedQuery: (querIds, moduleId = 'canvas') => { const defaultObject = {}; querIds.forEach((queryId) => { defaultObject[queryId] = { @@ -248,7 +250,7 @@ export const createResolvedSlice = (set, get) => ({ setResolvedComponents: (components, moduleId = 'canvas') => { const validateComponents = get().debugger.validateComponents; - const validatedComponents = validateComponents(components); + const validatedComponents = validateComponents(components, moduleId); set( (state) => { @@ -277,7 +279,7 @@ export const createResolvedSlice = (set, get) => ({ } */ setResolvedComponentByProperty: (componentId, type, property, value, index = null, moduleId = 'canvas') => { - value = get().debugger.validateProperty(componentId, type, property, value); + value = get().debugger.validateProperty(componentId, type, property, value, moduleId); set( (state) => { @@ -338,7 +340,7 @@ export const createResolvedSlice = (set, get) => ({ payload: { componentId, property, value, moduleId }, } ); - get().updateDependencyValues(`components.${componentId}.${property}`); + get().updateDependencyValues(`components.${componentId}.${property}`, moduleId); }, setExposedValues: (id, type, values, moduleId = 'canvas') => { @@ -359,7 +361,7 @@ export const createResolvedSlice = (set, get) => ({ } ); Object.entries(values).forEach(([key, value]) => { - if (typeof value !== 'function') get().updateDependencyValues(`components.${id}.${key}`); + if (typeof value !== 'function') get().updateDependencyValues(`components.${id}.${key}`, moduleId); }); }, @@ -368,7 +370,7 @@ export const createResolvedSlice = (set, get) => ({ if (val && Object.keys(val).length > 0) return; const component = componentTypeDefinitionMap[componentType]; if (!component) return; - const parentComponentType = get().getComponentDefinition(parentId)?.component?.component; + const parentComponentType = get().getComponentDefinition(parentId, moduleId)?.component?.component; if (['Form', 'Listview'].includes(parentComponentType)) return; const exposedVariables = component.exposedVariables || {}; get().setExposedValues(id, 'components', exposedVariables, moduleId); @@ -424,7 +426,7 @@ export const createResolvedSlice = (set, get) => ({ }, getExposedValueOfComponent: (componentId, moduleId = 'canvas') => { try { - const components = get().getCurrentPageComponents(); + const components = get().getCurrentPageComponents(moduleId); const { component: { parent: parentId, name: componentName }, } = components[componentId]; @@ -501,6 +503,9 @@ export const createResolvedSlice = (set, get) => ({ state.resolvedStore.modules[moduleId].exposedValues.components = {}; state.resolvedStore.modules[moduleId].exposedValues.variables = {}; state.resolvedStore.modules[moduleId].exposedValues.globals = {}; + if (state.resolvedStore.modules[moduleId].exposedValues.input) { + state.resolvedStore.modules[moduleId].exposedValues.input = {}; + } if (state.resolvedStore.modules[moduleId].exposedValues.page?.variables) { state.resolvedStore.modules[moduleId].exposedValues.page.variables = {}; } @@ -530,7 +535,7 @@ export const createResolvedSlice = (set, get) => ({ }, // this function simply replaces the id with name for queries and components inside resolvedStore - getResolvedState: (key, moduleId = 'canvas') => { + getResolvedState: (moduleId = 'canvas', key) => { const state = { components: {}, queries: {}, @@ -574,7 +579,7 @@ export const createResolvedSlice = (set, get) => ({ const objectType = typeof object; let error; - const state = _state ?? get().getAllExposedValues(); + const state = _state ?? get().getAllExposedValues(moduleId); if (_state?.parameters) { state.parameters = { ..._state.parameters }; @@ -613,4 +618,41 @@ export const createResolvedSlice = (set, get) => ({ } } }, + + setModuleInputs: (key, value, moduleId = 'canvas') => { + set( + (state) => { + if (!state.resolvedStore.modules[moduleId].exposedValues.input) { + state.resolvedStore.modules[moduleId].exposedValues.input = {}; + } + state.resolvedStore.modules[moduleId].exposedValues.input[key] = value; + }, + false, + 'setModuleInputs' + ); + get().updateDependencyValues(`input.${key}`, moduleId); + }, + setModuleOutputs: (key, value, moduleId = 'canvas') => { + set( + (state) => { + if (!state.resolvedStore.modules[moduleId].exposedValues.output) { + state.resolvedStore.modules[moduleId].exposedValues.output = {}; + } + state.resolvedStore.modules[moduleId].exposedValues.output[key] = value; + }, + false, + 'setModuleOutputs' + ); + get().updateDependencyValues(`output.${key}`, moduleId); + }, + clearModuleInputs: (moduleId = 'canvas') => { + set((state) => { + state.resolvedStore.modules[moduleId].exposedValues.input = {}; + }); + }, + clearModuleOutputs: (moduleId = 'canvas') => { + set((state) => { + state.resolvedStore.modules[moduleId].exposedValues.output = {}; + }); + }, }); diff --git a/frontend/src/AppBuilder/_stores/store.js b/frontend/src/AppBuilder/_stores/store.js index 4d1392fc7c..f7b83e9704 100644 --- a/frontend/src/AppBuilder/_stores/store.js +++ b/frontend/src/AppBuilder/_stores/store.js @@ -28,6 +28,8 @@ import { createDebuggerSlice } from './slices/debuggerSlice'; import { createGitSyncSlice } from './slices/gitSyncSlice'; import { createAiSlice } from './slices/aiSlice'; import { createWhiteLabellingSlice } from './slices/whiteLabellingSlice'; +import { createInspectorSlice } from './slices/inspectorSlice'; +import { createModuleSlice } from './slices/moduleSlice'; export default create( zustandDevTools( @@ -60,6 +62,8 @@ export default create( ...createGitSyncSlice(...state), ...createAiSlice(...state), ...createWhiteLabellingSlice(...state), + ...createInspectorSlice(...state), + ...createModuleSlice(...state), })), { name: 'App Builder Store', anonymousActionType: 'unknown' } ) diff --git a/frontend/src/AppBuilder/_stores/utils.js b/frontend/src/AppBuilder/_stores/utils.js index 012d59aaf5..f333654a16 100644 --- a/frontend/src/AppBuilder/_stores/utils.js +++ b/frontend/src/AppBuilder/_stores/utils.js @@ -6,7 +6,7 @@ import { deepClone } from '@/_helpers/utilities/utils.helpers'; import { dfs } from '@/_stores/handleReferenceTransactions'; import { extractAndReplaceReferencesFromString as extractAndReplaceReferencesFromStringAst } from '@/AppBuilder/_stores/ast'; -import _ from 'lodash'; +var _ = require('lodash'); const resetters = []; @@ -148,6 +148,7 @@ export const resolveCode = ( 'queries', 'globals', 'page', + 'input', 'client', 'server', 'constants', @@ -165,6 +166,7 @@ export const resolveCode = ( isJsCode ? state?.queries : undefined, isJsCode ? state?.globals : undefined, isJsCode ? state?.page : undefined, + isJsCode ? state?.input : undefined, isJsCode ? undefined : state?.client, isJsCode ? undefined : state?.server, state?.constants, // Passing constants as an argument allows the evaluated code to access and utilize the constants value correctly. diff --git a/frontend/src/AppLoader/AppLoader.jsx b/frontend/src/AppLoader/AppLoader.jsx index dba9883d14..e276360843 100644 --- a/frontend/src/AppLoader/AppLoader.jsx +++ b/frontend/src/AppLoader/AppLoader.jsx @@ -12,8 +12,14 @@ const AppLoader = (props) => { resetAllStores(); }, []); - if (appType === 'front-end') return ; - else if (appType === 'workflow') return ; + switch (appType) { + case 'front-end': + return ; + case 'workflow': + return ; + case 'module': + return ; + } }; export default withTranslation()(AppLoader); diff --git a/frontend/src/Editor/ControlledComponentToRender.jsx b/frontend/src/Editor/ControlledComponentToRender.jsx index 54a451188b..120b47f864 100644 --- a/frontend/src/Editor/ControlledComponentToRender.jsx +++ b/frontend/src/Editor/ControlledComponentToRender.jsx @@ -1,5 +1,5 @@ import React, { useState, useCallback } from 'react'; -import { getComponentToRender } from '@/_helpers/editorHelpers'; +// import { getComponentToRender } from '@/_helpers/editorHelpers'; import _ from 'lodash'; import { getComponentsToRenders, flushComponentsToRender } from '@/_stores/editorStore'; @@ -58,7 +58,7 @@ const ComponentWrapper = React.memo(({ componentName, ...props }) => { setKey(Math.random()); }, []); - const ComponentToRender = getComponentToRender(componentName); + const ComponentToRender = <>; // getComponentToRender(componentName); if (ComponentToRender === null) return; if (componentName === 'Form') { diff --git a/frontend/src/Editor/Viewer/PreviewSettings.jsx b/frontend/src/Editor/Viewer/PreviewSettings.jsx index 3103be3e16..a4b20b5e61 100644 --- a/frontend/src/Editor/Viewer/PreviewSettings.jsx +++ b/frontend/src/Editor/Viewer/PreviewSettings.jsx @@ -15,6 +15,7 @@ import { useEditorStore } from '@/_stores/editorStore'; import Cross from '@/_ui/Icon/solidIcons/Cross'; import { checkIfLicenseNotValid } from '@/_helpers/appUtils'; import EnvironmentManager from '@/Editor/Header/EnvironmentManager'; +import { useAppType } from '@/AppBuilder/_contexts/ModuleContext'; const PreviewSettings = ({ isMobileLayout, @@ -23,6 +24,7 @@ const PreviewSettings = ({ showHeader, darkMode, }) => { + const { appType } = useAppType(); const { featureAccess, currentAppEnvironment, setCurrentAppEnvironmentId } = useEditorStore( (state) => ({ featureAccess: state?.featureAccess, @@ -84,9 +86,9 @@ const PreviewSettings = ({
Preview settings - {editingVersion && _renderAppVersionsManager()} + {editingVersion && appType !== 'module' && _renderAppVersionsManager()}
- {editingVersion && _renderEnvironmentManager()} + {editingVersion && appType !== 'module' && _renderEnvironmentManager()} diff --git a/frontend/src/HomePage/AppMenu.jsx b/frontend/src/HomePage/AppMenu.jsx index 493f5d66b4..05281de413 100644 --- a/frontend/src/HomePage/AppMenu.jsx +++ b/frontend/src/HomePage/AppMenu.jsx @@ -19,6 +19,7 @@ export const AppMenu = function AppMenu({ appCreationMode, }) { const { t } = useTranslation(); + const isModuleApp = appType === 'module'; const Field = ({ text, onClick, customClass }) => { const closeMenu = () => { document.body.click(); @@ -81,7 +82,7 @@ export const AppMenu = function AppMenu({ )} )} - {canUpdateApp && canCreateApp && appType !== 'workflow' && ( + {canUpdateApp && canCreateApp && appType !== 'workflow' && !isModuleApp && ( <> { if (_.isEmpty(currentFolder)) { - updateSidebarNAV(`All ${appType === 'workflow' ? 'workflows' : 'apps'}`); + updateSidebarNAV(`All ${appType === 'workflow' ? 'workflows' : appType === 'module' ? 'modules' : 'apps'}`); setActiveFolder({}); } else { updateSidebarNAV(currentFolder.name); @@ -104,7 +104,9 @@ export const Folders = function Folders({ setActiveFolder(folder); } folderChanged(folder); - updateSidebarNAV(folder?.name ?? 'All apps'); + updateSidebarNAV( + folder?.name ?? `All ${appType === 'front-end' ? 'apps' : appType === 'module' ? 'modules' : 'workflows'}` + ); //update the url query parameter with folder name updateFolderQuery(folder?.name); } @@ -112,7 +114,12 @@ export const Folders = function Folders({ function updateFolderQuery(name) { const search = `${name ? `?folder=${name}` : ''}`; navigate( - { pathname: `/${getWorkspaceId()}${appType === 'workflow' ? '/workflows' : ''}`, search }, + { + pathname: `/${getWorkspaceId()}${ + appType === 'workflow' ? '/workflows' : appType === 'module' ? '/modules' : '' + }`, + search, + }, { replace: true } ); } @@ -286,10 +293,12 @@ export const Folders = function Folders({ onClick={() => handleFolderChange({})} data-cy="all-applications-link" > - {t( - `${appType === 'workflow' ? 'workflowsDashboard' : 'homePage'}.foldersSection.allApplications`, - 'All apps' - )} + {appType === 'module' + ? 'All modules' + : t( + `${appType === 'workflow' ? 'workflowsDashboard' : 'homePage'}.foldersSection.allApplications`, + 'All apps' + )}
)} diff --git a/frontend/src/HomePage/Footer.jsx b/frontend/src/HomePage/Footer.jsx index d5520aea77..a95f450e1b 100644 --- a/frontend/src/HomePage/Footer.jsx +++ b/frontend/src/HomePage/Footer.jsx @@ -2,7 +2,7 @@ import React, { useState, useMemo } from 'react'; import Pagination from '@/_ui/Pagination'; import Skeleton from 'react-loading-skeleton'; -const Footer = ({ darkMode, count, pageChanged, dataLoading, itemsPerPage = 9 }) => { +const Footer = ({ darkMode, count, pageChanged, dataLoading, itemsPerPage = 9, appType }) => { const [pageCount, setPageCount] = useState(1); const totalPages = useMemo(() => { return Math.floor((count - 1) / itemsPerPage) + 1; @@ -60,7 +60,7 @@ const Footer = ({ darkMode, count, pageChanged, dataLoading, itemsPerPage = 9 }) ) : ( - {pageRange} of {count} apps + {pageRange} of {count} {appType === 'module' ? 'modules' : 'apps'} )}
diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 0e04b4b2a3..dba8b21962 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -45,8 +45,10 @@ import { OrganizationList, UserGroupMigrationBanner, ConsultationBanner, + AppTypeTab, } from '@/modules/dashboard/components'; import CreateAppWithPrompt from '@/modules/AiBuilder/components/CreateAppWithPrompt'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; const { iconList, defaultIcon } = configs; @@ -100,7 +102,6 @@ class HomePageComponent extends React.Component { importingGitAppOperations: {}, featuresLoaded: false, showCreateAppModal: false, - showCreateModuleModal: false, showCreateAppFromTemplateModal: false, showImportAppModal: false, showCloneAppModal: false, @@ -116,6 +117,9 @@ class HomePageComponent extends React.Component { shouldAutoImportPlugin: false, dependentPlugins: [], dependentPluginsDetail: {}, + showMissingGroupsModal: false, + missingGroups: [], + missingGroupsExpanded: false, }; } @@ -233,14 +237,23 @@ class HomePageComponent extends React.Component { this.fetchFolders(); }; - createApp = async (appName, type) => { + getAppType = () => { + return this.props.appType === 'module' ? 'Module' : this.props.appType === 'workflow' ? 'Workflow' : 'App'; + }; + + createApp = async (appName) => { let _self = this; _self.setState({ creatingApp: true }); + try { - const data = await appsService.createApp({ icon: sample(iconList), name: appName, type: this.props.appType }); + const data = await appsService.createApp({ + icon: sample(iconList), + name: appName, + type: this.props.appType, + }); const workspaceId = getWorkspaceId(); _self.props.navigate(`/${workspaceId}/apps/${data.id}`, { state: { commitEnabled: this.state.commitEnabled } }); - toast.success(`${this.props.appType === 'workflow' ? 'Workflow' : 'App'} created successfully!`); + toast.success(`${this.getAppType()} created successfully!`); _self.setState({ creatingApp: false }); return true; } catch (errorResponse) { @@ -259,7 +272,7 @@ class HomePageComponent extends React.Component { try { await appsService.saveApp(appId, { name: newAppName }); await this.fetchApps(this.state.currentPage, this.state.currentFolder.id); - toast.success(`${this.props.appType === 'workflow' ? 'Workflow' : 'App'} name has been updated!`); + toast.success(`${this.getAppType()} name has been updated!`); _self.setState({ renamingApp: false }); return true; } catch (errorResponse) { @@ -356,17 +369,24 @@ class HomePageComponent extends React.Component { } }; - importFile = async (importJSON, appName) => { + importFile = async (importJSON, appName, skipPagePermissionsGroupCheck = false) => { this.setState({ isImportingApp: true }); // For backward compatibility with legacy app import const organization_id = this.state.currentUser?.organization_id; const isLegacyImport = isEmpty(importJSON.tooljet_version); if (isLegacyImport) { - importJSON = { app: [{ definition: importJSON, appName: appName }], tooljet_version: importJSON.tooljetVersion }; + importJSON = { + app: [{ definition: importJSON, appName: appName }], + tooljet_version: importJSON.tooljetVersion, + }; } else { importJSON.app[0].appName = appName; } - const requestBody = { organization_id, ...importJSON }; + const requestBody = { + organization_id, + ...importJSON, + skip_page_permissions_group_check: skipPagePermissionsGroupCheck, + }; let installedPluginsInfo = []; try { if (this.state.dependentPlugins.length) { @@ -388,6 +408,10 @@ class HomePageComponent extends React.Component { this.props.navigate(`/${getWorkspaceId()}/database`); } } catch (error) { + if (error?.error?.type === 'permission-check') { + this.setState({ showMissingGroupsModal: true, missingGroups: error?.error?.data }); + return; + } if (installedPluginsInfo.length) { const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id); await pluginsService.uninstallPlugins(pluginsId); @@ -501,7 +525,7 @@ class HomePageComponent extends React.Component { .deleteApp(this.state.appToBeDeleted.id) // eslint-disable-next-line no-unused-vars .then((data) => { - toast.success(`${this.props.appType === 'workflow' ? 'Workflow' : 'App'} deleted successfully.`); + toast.success(`${this.getAppType()} deleted successfully.`); this.fetchApps( this.state.currentPage ? this.state.apps?.length === 1 @@ -793,11 +817,11 @@ class HomePageComponent extends React.Component { }; openCreateAppModal = () => { - this.setState({ showCreateAppModal: true, showCreateModuleModal: true }); + this.setState({ showCreateAppModal: true }); }; closeCreateAppModal = () => { - this.setState({ showCreateAppModal: false, showCreateModuleModal: false }); + this.setState({ showCreateAppModal: false }); }; openImportAppModal = async () => { @@ -876,7 +900,6 @@ class HomePageComponent extends React.Component { importingGitAppOperations, featuresLoaded, showCreateAppModal, - showCreateModuleModal, showImportAppModal, fileContent, fileName, @@ -888,15 +911,22 @@ class HomePageComponent extends React.Component { showGroupMigrationBanner, dependentPlugins, dependentPluginsDetail, + showMissingGroupsModal, + missingGroups, + missingGroupsExpanded, } = this.state; + + const invalidLicense = featureAccess?.licenseStatus?.isExpired || !featureAccess?.licenseStatus?.isLicenseValid; + // const invalidLicense = false; + const modalConfigs = { create: { modalType: 'create', closeModal: this.closeCreateAppModal, - processApp: (name) => this.createApp(name, showCreateAppModal ? 'front-end' : 'module'), + processApp: (name) => this.createApp(name), show: this.openCreateAppModal, - title: this.props.appType === 'workflow' ? 'Create workflow' : 'Create app', - actionButton: this.props.appType === 'workflow' ? '+ Create workflow' : '+ Create app', + title: `Create ${this.getAppType().toLocaleLowerCase()}`, + actionButton: `+ Create ${this.getAppType().toLocaleLowerCase()}`, actionLoadingButton: 'Creating', appType: this.props.appType, }, @@ -939,13 +969,18 @@ class HomePageComponent extends React.Component { }; const isAdmin = authenticationService?.currentSessionValue?.admin; const isBuilder = authenticationService?.currentSessionValue?.is_builder; + + //import app missing groups modal config + const threshold = 3; + const isLong = missingGroups.length > threshold; + const displayedGroups = missingGroupsExpanded ? missingGroups : missingGroups.slice(0, threshold); + return (
+ this.importFile(fileContent, fileName, true)} + show={showMissingGroupsModal} + isLoading={importingApp} + handleClose={() => this.setState({ showMissingGroupsModal: false })} + confirmBtnProps={{ + title: 'Import', + tooltipMessage: '', + }} + className="missing-groups-modal" + darkMode={this.props.darkMode} + > +
+
+ +
+
Warning: Missing user groups for permissions
+

+ Permissions for the following user group(s) won’t be applied since they do not exist in this + workspace. +

+
+
+ +
+
+
+ User groups +
+
+ {displayedGroups.map((group, idx) => ( + + {group} + {idx < displayedGroups.length - 1 ? ', ' : ''} + + ))} + {!missingGroupsExpanded && isLong && '...'} +
+
+ + {isLong && ( + + )} +
+ +

+ Restricted pages, queries, or components will become accessible to all users or to existing groups with + permissions. To avoid this, create the missing groups before importing, or reconfigure permissions after + import. +

+ +
+ this.setState({ showMissingGroupsModal: false, isImportingApp: false })} + > + Cancel import + + this.importFile(fileContent, fileName, true)} + className="primary-action" + > + Import with limited permissions + +
+
+
{showRenameAppModal && ( this.setState({ showRenameAppModal: true })} @@ -960,8 +1082,8 @@ class HomePageComponent extends React.Component { processApp={this.renameApp} selectedAppId={appOperations.selectedApp.id} selectedAppName={appOperations.selectedApp.name} - title={`Rename ${this.props.appType === 'workflow' ? 'workflow' : 'app'}`} - actionButton={`Rename ${this.props.appType === 'workflow' ? 'workflow' : 'app'}`} + title={`Rename ${this.getAppType().toLocaleLowerCase()}`} + actionButton={`Rename ${this.getAppType().toLocaleLowerCase()}`} actionLoadingButton={'Renaming'} appType={this.props.appType} /> @@ -1190,6 +1312,7 @@ class HomePageComponent extends React.Component { )}
+ {this.canCreateApp() && (
= 100} disabled={ - this.props.appType === 'front-end' - ? appsLimit?.percentage >= 100 + this.props.appType === 'front-end' || this.props.appType === 'module' + ? appsLimit?.percentage >= 100 || (this.props.appType === 'module' && invalidLicense) : workflowInstanceLevelLimit.percentage >= 100 || workflowWorkspaceLevelLimit.percentage >= 100 } className={`create-new-app-button col-11 ${creatingApp ? 'btn-loading' : ''}`} - onClick={() => this.setState({ showCreateAppModal: true })} + onClick={() => + this.setState({ + showCreateAppModal: true, + }) + } data-cy="create-new-app-button" > {isImportingApp && ( )} - {this.props.t( - `${ - this.props.appType === 'workflow' ? 'workflowsDashboard' : 'homePage' - }.header.createNewApplication`, - 'Create new app' - )} + {this.props.appType === 'module' + ? 'Create new module' + : this.props.t( + `${ + this.props.appType === 'workflow' ? 'workflowsDashboard' : 'homePage' + }.header.createNewApplication`, + 'Create new app' + )} - {this.props.appType !== 'workflow' && ( + {this.props.appType !== 'workflow' && this.props.appType !== 'module' && ( = 100} + disabled={ + appsLimit?.percentage >= 100 || (this.props.appType === 'module' && invalidLicense) + } split className="d-inline" data-cy="import-dropdown-menu" @@ -1315,7 +1446,7 @@ class HomePageComponent extends React.Component { !appSearchKey && )} - {this.props.appType !== 'workflow' && this.canCreateApp() && ( + {this.props.appType !== 'workflow' && this.props.appType !== 'module' && this.canCreateApp() && ( )} @@ -1327,6 +1458,7 @@ class HomePageComponent extends React.Component { onSearchSubmit={this.onSearchSubmit} darkMode={this.props.darkMode} appType={this.props.appType} + disabled={this.props.appType === 'module' && invalidLicense} />
@@ -1354,31 +1486,44 @@ class HomePageComponent extends React.Component {
)} - {!isLoading && featuresLoaded && meta?.total_count === 0 && !currentFolder.id && !appSearchKey && ( - = workflowInstanceLevelLimit.total || - 100 > workflowInstanceLevelLimit.percentage >= 90 || - workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 - ? workflowInstanceLevelLimit - : workflowWorkspaceLevelLimit - } - /> - )} + {!isLoading && + featuresLoaded && + meta?.total_count === 0 && + !currentFolder.id && + !appSearchKey && + (['front-end', 'workflow'].includes(this.props.appType) ? ( + = workflowInstanceLevelLimit.total || + 100 > workflowInstanceLevelLimit.percentage >= 90 || + workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 + ? workflowInstanceLevelLimit + : workflowWorkspaceLevelLimit + } + /> + ) : ( +

+ You have not created any modules.  + + Create a module  + + to start using it within your apps. +

+ ))} {!isLoading && apps?.length === 0 && appSearchKey && (
@@ -1402,7 +1547,7 @@ class HomePageComponent extends React.Component { appActionModal={this.appActionModal} removeAppFromFolder={this.removeAppFromFolder} appType={this.props.appType} - basicPlan={featureAccess?.licenseStatus?.isExpired || !featureAccess?.licenseStatus?.isLicenseValid} + basicPlan={invalidLicense} appSearchKey={this.state.appSearchKey} /> )} @@ -1416,6 +1561,7 @@ class HomePageComponent extends React.Component { pageChanged={this.pageChanged} darkMode={this.props.darkMode} dataLoading={isLoading} + appType={this.props.appType} /> )} {/* need to review the mobile view */} diff --git a/frontend/src/_components/AppModal.jsx b/frontend/src/_components/AppModal.jsx index 54c623dbed..72fb7cdcb5 100644 --- a/frontend/src/_components/AppModal.jsx +++ b/frontend/src/_components/AppModal.jsx @@ -10,6 +10,7 @@ import { PluginsListForAppModal } from './PluginsListForAppModal'; const APP_TYPE = { WORKFLOW: 'workflow', APP: 'app', + MODULE: 'module', }; export function AppModal({ @@ -52,6 +53,8 @@ export function AppModal({ const [isNameChanged, setIsNameChanged] = useState(false); const inputRef = useRef(null); + const appTypeName = APP_TYPE.WORKFLOW == appType ? 'Workflow' : APP_TYPE.MODULE == appType ? 'Module' : 'App'; + useEffect(() => { setIsNameChanged(newAppName?.trim() !== selectedAppName); }, [newAppName, selectedAppName]); @@ -85,7 +88,7 @@ export function AppModal({ success = await processApp(trimmedAppName); } if (success === false) { - setErrorText(`${appType == APP_TYPE.WORKFLOW ? 'Workflow' : 'App'} name already exists`); + setErrorText(`${appTypeName} name already exists`); setInfoText(''); } else { setErrorText(''); @@ -127,8 +130,6 @@ export function AppModal({ (actionButton === 'Rename app' && (!isNameChanged || newAppName.trim().length === 0 || newAppName.length > 50)) || // For rename case (actionButton !== 'Rename app' && (newAppName.length > 50 || newAppName.trim().length === 0)); - const appTypeName = APP_TYPE.WORKFLOW == appType ? 'Workflow' : 'App'; - return ( )} - {orgGit?.is_enabled && appType != APP_TYPE.WORKFLOW && ( + {orgGit?.is_enabled && appType != APP_TYPE.WORKFLOW && appType != APP_TYPE.MODULE && (
maxLetters + ? `${children.substring(0, maxLetters)}...` + : children; + return ( module.PDF); -} +// export const AllComponents = { +// Button, +// Image, +// Text, +// TextInput, +// NumberInput, +// Table, +// TextArea, +// Container, +// Tabs, +// RichTextEditor, +// DropDown, +// DropdownV2, +// Checkbox, +// Datepicker, +// DaterangePicker, +// Multiselect, +// MultiselectV2, +// Modal, +// Chart, +// Map: MapComponent, +// QrScanner, +// ToggleSwitch, +// RadioButton, +// StarRating, +// Divider, +// FilePicker, +// PasswordInput, +// Calendar, +// IFrame, +// CodeEditor, +// Listview, +// Timer, +// Statistics, +// Pagination, +// Tags, +// Spinner, +// CircularProgressBar, +// RangeSlider, +// Timeline, +// SvgImage, +// Html, +// ButtonGroup, +// CustomComponent, +// VerticalDivider, +// ColorPicker, +// KanbanBoard, +// Kanban, +// Steps, +// TreeSelect, +// Link, +// Icon, +// Form, +// BoundedBox, +// ToggleSwitchV2, +// }; +// if (isPDFSupported()) { +// AllComponents.PDF = await import('@/Editor/Components/PDF').then((module) => module.PDF); +// } -export const getComponentToRender = (componentName) => { - const shouldHideWidget = componentName === 'PDF' && !isPDFSupported(); - if (shouldHideWidget) return null; - return AllComponents[componentName]; -}; +// export const getComponentToRender = (componentName) => { +// const shouldHideWidget = componentName === 'PDF' && !isPDFSupported(); +// if (shouldHideWidget) return null; +// return AllComponents[componentName]; +// }; export function isOnlyLayoutUpdate(diffState) { const componentDiff = Object.keys(diffState).filter((key) => diffState[key]?.layouts && !diffState[key]?.component); diff --git a/frontend/src/_helpers/routes.js b/frontend/src/_helpers/routes.js index 0f552cb955..495ca04e18 100644 --- a/frontend/src/_helpers/routes.js +++ b/frontend/src/_helpers/routes.js @@ -21,6 +21,7 @@ export const getPrivateRoute = (page, params = {}) => { workflows: '/workflows', workspace_constants: '/workspace-constants', profile_settings: '/profile-settings', + modules: '/modules', }; let url = routes[page]; diff --git a/frontend/src/_helpers/white-label/whiteLabelling.js b/frontend/src/_helpers/white-label/whiteLabelling.js index cfbbcbfc0c..c32573a96f 100644 --- a/frontend/src/_helpers/white-label/whiteLabelling.js +++ b/frontend/src/_helpers/white-label/whiteLabelling.js @@ -54,7 +54,8 @@ export async function setFaviconAndTitle(location) { 'data-sources': 'Data sources', 'audit-logs': 'Audit logs', 'account-settings': 'Profile settings', - settings: 'Profile settings', + settings: 'Settings', + 'profile-settings': 'Profile settings', login: '', signUp: '', error: '', @@ -65,9 +66,12 @@ export async function setFaviconAndTitle(location) { 'reset-password': '', 'workspace-constants': 'Workspace constants', setup: '', + '/': 'Dashboard', }; - const pageTitleKey = Object.keys(pageTitles).find((path) => location?.pathname.includes(path)); + const pageTitleKey = Object.keys(pageTitles) + .sort((a, b) => b.length - a.length) // Sort by length descending + .find((path) => location?.pathname.includes(path)); const pageTitle = pageTitles[pageTitleKey] || ''; document.title = pageTitle ? `${decodeEntities(pageTitle)} | ${whiteLabelText}` : `${decodeEntities(whiteLabelText)}`; @@ -77,6 +81,9 @@ export async function fetchAndSetWindowTitle(pageDetails) { const whiteLabelText = retrieveWhiteLabelText(); let pageTitleKey = pageDetails?.page || ''; let pageTitle = ''; + let mode = pageDetails?.mode || ''; + let isPreview = !pageDetails?.isReleased || false; + const license = pageDetails?.licenseStatus; switch (pageTitleKey) { case pageTitles.VIEWER: { const titlePrefix = pageDetails?.preview ? 'Preview - ' : ''; @@ -85,7 +92,11 @@ export async function fetchAndSetWindowTitle(pageDetails) { } case pageTitles.EDITOR: case pageTitles.WORKFLOW_EDITOR: { - pageTitle = pageDetails?.appName || 'My App'; + if (mode == 'edit') { + pageTitle = `${pageDetails?.appName}`; + } else { + pageTitle = `Preview - ${pageDetails?.appName}` || 'My App'; + } break; } default: { @@ -93,6 +104,10 @@ export async function fetchAndSetWindowTitle(pageDetails) { break; } } + if (!isPreview && mode === 'view') { + document.title = `${pageDetails?.appName} ${license ? '' : '| ToolJet'}`; + return; + } document.title = !(pageDetails?.preview === false) ? `${pageTitle} | ${whiteLabelText}` : `${pageTitle}`; } diff --git a/frontend/src/_services/apps.service.js b/frontend/src/_services/apps.service.js index 82cf4312e9..c29dea481a 100644 --- a/frontend/src/_services/apps.service.js +++ b/frontend/src/_services/apps.service.js @@ -67,18 +67,30 @@ function getAll(page, folder, searchKey, type = 'front-end') { } function createApp(body = {}) { - if (body.type === 'workflow') { - return createWorkflow(body); + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + switch (body.type) { + case 'workflow': + return createWorkflow(requestOptions); + case 'module': + return createModule(requestOptions); + default: + return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse); } - const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) }; - return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse); } -function createWorkflow(body = {}) { - const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) }; +function createWorkflow(requestOptions) { return fetch(`${config.apiUrl}/workflows`, requestOptions).then(handleResponse); } +function createModule(requestOptions) { + return fetch(`${config.apiUrl}/modules`, requestOptions).then(handleResponse); +} + function cloneApp(id, name) { const requestOptions = { method: 'POST', diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss index 074338602e..b823e7623c 100644 --- a/frontend/src/_styles/components.scss +++ b/frontend/src/_styles/components.scss @@ -95,10 +95,7 @@ $btn-dark-color: #FFFFFF; } .leftsidebar-panel-header { - background-color: var(--slate3); - padding: 12px 16px; - min-height: 52px; - border-bottom: 1px solid var(--slate5); + padding: 12px 16px 0px 16px; .panel-header-container { @@ -125,6 +122,8 @@ $btn-dark-color: #FFFFFF; .page-selector-panel-body { padding: 4px; border-right: 1px solid #DFE3E6; + padding-left:16px; + padding-right:16px; &.dark-theme { border-right: 1px solid var(--slate7); @@ -242,10 +241,16 @@ $btn-dark-color: #FFFFFF; display: flex; align-items: baseline; gap: 5px; + cursor: pointer !important; + pointer-events: unset !important; &.disabled { opacity: 1 !important; } + + svg { + margin-left: 5px; + } } } diff --git a/frontend/src/_styles/left-sidebar.scss b/frontend/src/_styles/left-sidebar.scss index e2fe92105a..6a0311b0c4 100644 --- a/frontend/src/_styles/left-sidebar.scss +++ b/frontend/src/_styles/left-sidebar.scss @@ -182,9 +182,17 @@ } } +.page { + .leftsidebar-panel-header { + margin-bottom: 8px; + } + +} + .debugger { .leftsidebar-panel-header { border-bottom: none; + margin-bottom: 8px; } .text-slate-12 { @@ -207,7 +215,7 @@ } .nav-item .nav-link { - background-color: var(--slate3) !important; + background-color: var(--base) !important; color: var(--slate11) !important; } @@ -859,4 +867,20 @@ height: calc(100% - 48px); min-height: 300px; position: relative; -} \ No newline at end of file +} + +.left-sidebar-scrollbar { + &::-webkit-scrollbar { + width: 6px; + margin-right: 3px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--interactive-default) !important; + border-radius: 3px; + } +} diff --git a/frontend/src/_styles/modules.scss b/frontend/src/_styles/modules.scss new file mode 100644 index 0000000000..dd9891df6d --- /dev/null +++ b/frontend/src/_styles/modules.scss @@ -0,0 +1,76 @@ +.apps-modules-tabs { + .nav-link { + background-color: var(--page-default); + } + + .nav-link.active { + border-bottom-width: medium !important; + } + + li.nav-item { + flex: 1; + + button { + width: 100%; + } + } + +} + +.apps-modules-navigation { + margin-bottom: 10px; + + .tab-content { + display: none; + } +} + +.apps-modules-tabs.dark-mode { + .nav-link { + background-color: inherit; + } + + border-bottom-color: #2B394A; +} + +#homePage-tab-front-end, +#homePage-tab-module { + border-radius: 0; +} + +.show-module-border { + outline: dotted 2px #CCD1D5 !important; +} + +.module-container-canvas { + >div:first-child { + height: 100%; + overflow: hidden auto; + + /* Hide scrollbar by default */ + &::-webkit-scrollbar { + width: 6px; + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: transparent; + } + + /* Show scrollbar only on hover */ + &:hover { + &::-webkit-scrollbar-thumb { + background-color: #6a727c4d; + border-radius: 3px; + } + } + + /* Firefox scrollbar support */ + scrollbar-width: thin; + scrollbar-color: transparent transparent; + + &:hover { + scrollbar-color: #6a727c4d transparent; + } + } +} \ No newline at end of file diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 136bc37f11..c97d63bdf3 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -19,6 +19,7 @@ @import 'tailwindcss/utilities'; @import "./componentdesign.scss"; @import './pages-sidebar.scss'; +@import './modules.scss'; /* ibm-plex-sans-100 - latin */ @font-face { @@ -806,6 +807,21 @@ button { } } + .viewer .main { + height: auto !important; + + .canvas-container { + top: 0; + right: 0; + scrollbar-width: thin; + scrollbar-color: #6a727c4d transparent; + + &::-webkit-scrollbar-thumb { + background-color: #6a727c4d !important; + } + } + } + @media screen and (max-height: 450px) { .sidebar { padding-top: 15px; @@ -1552,6 +1568,13 @@ button { border-top: 1px solid var(--slate5) !important; } + &.module-editor-inspector { + .tab-content { + border-top: none !important; + } + } + + /* Hide scrollbar for Chrome, Safari and Opera */ /* Hide scrollbar for Chrome, Safari and Opera */ .tab-content::-webkit-scrollbar { display: none; @@ -2707,6 +2730,14 @@ hr { max-height: 10px; z-index: 100; min-width: 108px; + + &.module-container { + .handle-content { + cursor: move; + color: #fff; + background: #c6cad0 !important; + } + } } @@ -4119,6 +4150,7 @@ input[type="text"] { .rbc-event-label { display: none; } + background-color: var(--primary-brand) !important; border: transparent } @@ -7574,6 +7606,13 @@ tbody { .apploader { height: 100vh; + &.module-mode { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + .app-container { height: 100%; display: flex; @@ -7935,6 +7974,11 @@ tbody { display: grid; grid-template-rows: auto 1fr auto; + // Added to work with AppTypeTab component + &:has(:nth-child(4):last-child) { + grid-template-rows: auto auto 1fr auto; + } + @media only screen and (max-width: 767px) { display: none; } @@ -9137,7 +9181,8 @@ tbody { } .global-settings-app-wrapper { - max-width: 190px; + max-width: 350px; + margin-right: 10px; } .version-manager-container { @@ -10894,7 +10939,7 @@ tbody { background: var(--indigo3); border-radius: 6px; padding: 5px 10px; - + p { font-weight: 500 !important; line-height: 18px !important; @@ -12304,11 +12349,11 @@ tbody { } .design-component-inputs textarea { - + &.valid-textarea { border: 1.5px solid #519b62 !important; } - + &.invalid-textarea { border: 1.5px solid #e26367 !important; } @@ -15750,7 +15795,7 @@ tbody { /* Set the desired width */ } -textarea.tj-text-input-widget{ +textarea.tj-text-input-widget { resize: none !important; overflow-y: auto !important; } @@ -18659,8 +18704,8 @@ section.ai-message-prompt-input-wrapper { } .codebuilder-color-swatches-options { - width:100%; - height:30px; + width: 100%; + height: 30px; padding: 6px 8px; border-radius: 6px; @@ -18693,8 +18738,8 @@ section.ai-message-prompt-input-wrapper { .theme-create-btn { width: 100%; margin-bottom: 8px; - height:32px; - color:#000; + height: 32px; + color: #000; border: 1px solid var(--Border-brand-weak, #97AEFC); } @@ -18717,14 +18762,14 @@ section.ai-message-prompt-input-wrapper { border-color: var(--primary-brand); } -.multiselct-widget-option{ +.multiselct-widget-option { input:checked { background-color: var(--primary-brand); } } .multiselect-box { - .options{ + .options { input:checked { background-color: var(--primary-brand); } @@ -18733,6 +18778,7 @@ section.ai-message-prompt-input-wrapper { .timer-btn { background-color: var(--primary-brand); + &:hover { background-color: var(--primary-brand); } @@ -18771,6 +18817,7 @@ section.ai-message-prompt-input-wrapper { color: #ffffff; border: 1px solid transparent; } + .canvas-styles-header { background-color: #212325; color: #ffffff; @@ -18816,8 +18863,9 @@ section.ai-message-prompt-input-wrapper { } #inspector-tabpane-properties .accordion-header { - height:32px; + height: 32px; } + .cm-tooltip { z-index: 9999 !important; } @@ -18887,4 +18935,70 @@ section.ai-message-prompt-input-wrapper { .cm-editor { max-height: 100px !important; } +} + +.missing-groups-modal { + .modal-body { + padding: 16px; + + .header { + padding-top: 12px; + font-weight: 500; + font-size: 14px; + } + + .sub-header { + margin-bottom: 0px; + font-size: 12px; + } + + .groups-list { + padding-top: 16px; + padding-bottom: 16px; + + .container { + padding: 12px; + } + } + + .info { + margin-bottom: 0px; + font-size: 12px; + padding-bottom: 24px; + } + + .action-btns { + justify-content: space-between; + } + + .primary-action, + .secondary-action { + padding: 8px !important; + font-size: 12px; + } + + .toggle-button { + display: inline-flex; + align-items: center; + font-size: 14px; + color: var(--icon-brand); + background: none; + border: none; + cursor: pointer; + padding: 0; + font-family: inherit; + } + + .toggle-button:hover { + text-decoration: underline; + } + + .toggle-button .chevron { + transition: transform 0.2s ease; + } + + .toggle-button.expanded .chevron { + transform: rotate(180deg); + } + } } \ No newline at end of file diff --git a/frontend/src/_ui/Icon/solidIcons/Corners.jsx b/frontend/src/_ui/Icon/solidIcons/Corners.jsx new file mode 100644 index 0000000000..72ca376981 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/Corners.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const Corners = ({ style, fill = '#C1C8CD', width = '12', height = '13', className = '', viewBox = '0 0 12 13' }) => ( + + + + + +); + +export default Corners; diff --git a/frontend/src/_ui/Icon/solidIcons/EmptyStateModules.jsx b/frontend/src/_ui/Icon/solidIcons/EmptyStateModules.jsx new file mode 100644 index 0000000000..963c4b7bef --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/EmptyStateModules.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const EmptyStateModules = ({ fill = '#C1C8CD', width = '24', className = '', viewBox = '0 0 24 24' }) => ( + + + + + +); + +export default EmptyStateModules; diff --git a/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx b/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx new file mode 100644 index 0000000000..eed8dd5e8c --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const EnterpriseCrown = ({ fill = '#FCA23F', width = '12', className = '', viewBox = '0 0 16 16' }) => ( + + + +); + +export default EnterpriseCrown; diff --git a/frontend/src/_ui/Icon/solidIcons/FileCode.jsx b/frontend/src/_ui/Icon/solidIcons/FileCode.jsx new file mode 100644 index 0000000000..ed1f3268c9 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/FileCode.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const FileCode = ({ style, fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 12 12' }) => ( + + + + + +); + +export default FileCode; diff --git a/frontend/src/_ui/Icon/solidIcons/RemoveFolder.jsx b/frontend/src/_ui/Icon/solidIcons/RemoveFolder.jsx new file mode 100644 index 0000000000..dd97454175 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/RemoveFolder.jsx @@ -0,0 +1,28 @@ +import React from 'react'; + +const RemoveFolder = ({ width = '14', fill = '#6A727C', className = '', viewBox = '0 0 14 14' }) => ( + + + + + + + + + + +); + +export default RemoveFolder; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index 2a7b801570..0e7192edaf 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -231,10 +231,15 @@ import CalendarSmall from './CalendarSmall.jsx'; import UserGroupsGrey from './UserGroupsGrey.jsx'; import AppLimitSvg from './AppLimitSvg.jsx'; import NewTabSmall from './NewTabSmall.jsx'; +import EmptyStateModules from './EmptyStateModules.jsx'; import Code from './Code.jsx'; import WorkflowV3 from './WorkflowV3.jsx'; import WorkspaceV3 from './WorkspaceV3.jsx'; +import EnterpriseCrown from './EnterrpiseCrown.jsx'; +import FileCode from './FileCode.jsx'; +import Corners from './Corners.jsx'; import Moon from './Moon.jsx'; +import RemoveFolder from './RemoveFolder.jsx'; const Icon = (props) => { switch (props.name) { @@ -356,6 +361,8 @@ const Icon = (props) => { return ; case 'enterprisev3': return ; + case 'enterprisecrown': + return ; case 'lockGradient': return ; case 'datasourceGradient': @@ -370,6 +377,8 @@ const Icon = (props) => { return ; case 'expand': return ; + case 'file-code': + return ; case 'file01': return ; case 'filedownload': @@ -496,6 +505,8 @@ const Icon = (props) => { return ; case 'remove01': return ; + case 'removefolder': + return ; case 'removerectangle': return ; case 'rightarrrow': @@ -532,6 +543,8 @@ const Icon = (props) => { return ; case 'comments': return ; + case 'corners': + return ; case 'share': return ; case 'shield': @@ -706,6 +719,8 @@ const Icon = (props) => { return ; case 'ai-crown': return ; + case 'empty-state-modules': + return ; case 'play01': return ; case 'moon': diff --git a/frontend/src/_ui/Modal/index.jsx b/frontend/src/_ui/Modal/index.jsx index b9d48f0d88..6ed7fa04db 100644 --- a/frontend/src/_ui/Modal/index.jsx +++ b/frontend/src/_ui/Modal/index.jsx @@ -18,6 +18,8 @@ export default function ModalBase({ className = '', size = 'sm', headerAction, + showHeader = true, + showFooter = true, }) { return ( - - - {title} - -
- {headerAction && headerAction()} - -
-
+ {showHeader && ( + + + {title} + +
+ {headerAction && headerAction()} + +
+
+ )} {children ? ( children @@ -45,28 +49,30 @@ export default function ModalBase({
)} - - - Cancel - - -
- - {confirmBtnProps?.title || 'Continue'} - -
-
-
+ {showFooter && ( + + + Cancel + + +
+ + {confirmBtnProps?.title || 'Continue'} + +
+
+
+ )} ); } diff --git a/frontend/src/components/ui/Input/CommonInput/TextInput.jsx b/frontend/src/components/ui/Input/CommonInput/TextInput.jsx index 978e69798f..27903c783f 100644 --- a/frontend/src/components/ui/Input/CommonInput/TextInput.jsx +++ b/frontend/src/components/ui/Input/CommonInput/TextInput.jsx @@ -47,6 +47,7 @@ const TextInput = ({ diff --git a/frontend/src/components/ui/Input/Index.jsx b/frontend/src/components/ui/Input/Index.jsx index 139019a198..dbb6806d6b 100644 --- a/frontend/src/components/ui/Input/Index.jsx +++ b/frontend/src/components/ui/Input/Index.jsx @@ -13,6 +13,7 @@ InputComponent.propTypes = { type: PropTypes.oneOf(['text', 'number', 'editable title', 'password', 'email']), value: PropTypes.string, onChange: PropTypes.func, + onClear: PropTypes.func, placeholder: PropTypes.string, name: PropTypes.string, id: PropTypes.string, @@ -32,6 +33,7 @@ InputComponent.propTypes = { InputComponent.defaultProps = { type: 'text', onChange: (e, validateObj) => {}, + onClear: () => {}, placeholder: '', name: '', id: '', diff --git a/frontend/src/modules/Modules/components/ModuleContainer/ModuleContainer.jsx b/frontend/src/modules/Modules/components/ModuleContainer/ModuleContainer.jsx new file mode 100644 index 0000000000..8c18f534f6 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleContainer/ModuleContainer.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const ModuleContainer = () => { + return <>; +}; + +export default withEditionSpecificComponent(ModuleContainer, 'Modules'); diff --git a/frontend/src/modules/Modules/components/ModuleContainer/index.js b/frontend/src/modules/Modules/components/ModuleContainer/index.js new file mode 100644 index 0000000000..f5495b7be9 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleContainer/index.js @@ -0,0 +1 @@ +export { default } from './ModuleContainer'; diff --git a/frontend/src/modules/Modules/components/ModuleContainerBlank/ModuleContainerBlank.jsx b/frontend/src/modules/Modules/components/ModuleContainerBlank/ModuleContainerBlank.jsx new file mode 100644 index 0000000000..2ddb28f0d2 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleContainerBlank/ModuleContainerBlank.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const ModuleContainerBlank = () => { + return <>; +}; + +export default withEditionSpecificComponent(ModuleContainerBlank, 'Modules'); diff --git a/frontend/src/modules/Modules/components/ModuleContainerBlank/index.js b/frontend/src/modules/Modules/components/ModuleContainerBlank/index.js new file mode 100644 index 0000000000..b8954c974c --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleContainerBlank/index.js @@ -0,0 +1 @@ +export { default } from './ModuleContainerBlank'; diff --git a/frontend/src/modules/Modules/components/ModuleContainerInspector/ModuleContainerInspector.jsx b/frontend/src/modules/Modules/components/ModuleContainerInspector/ModuleContainerInspector.jsx new file mode 100644 index 0000000000..8782ae9ac7 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleContainerInspector/ModuleContainerInspector.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const ModuleContainerInspector = () => { + return <>; +}; + +export default withEditionSpecificComponent(ModuleContainerInspector, 'Modules'); diff --git a/frontend/src/modules/Modules/components/ModuleContainerInspector/index.js b/frontend/src/modules/Modules/components/ModuleContainerInspector/index.js new file mode 100644 index 0000000000..6417a2160e --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleContainerInspector/index.js @@ -0,0 +1 @@ +export { default } from './ModuleContainerInspector'; diff --git a/frontend/src/modules/Modules/components/ModuleEditorBanner/ModuleEditorBanner.jsx b/frontend/src/modules/Modules/components/ModuleEditorBanner/ModuleEditorBanner.jsx new file mode 100644 index 0000000000..3bf0d5b4e8 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleEditorBanner/ModuleEditorBanner.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const ModuleEditorBanner = () => { + return <>; +}; + +export default withEditionSpecificComponent(ModuleEditorBanner, 'Modules'); diff --git a/frontend/src/modules/Modules/components/ModuleEditorBanner/index.js b/frontend/src/modules/Modules/components/ModuleEditorBanner/index.js new file mode 100644 index 0000000000..c157c1e0b9 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleEditorBanner/index.js @@ -0,0 +1 @@ +export { default } from './ModuleEditorBanner'; diff --git a/frontend/src/modules/Modules/components/ModuleManager/ModuleManager.jsx b/frontend/src/modules/Modules/components/ModuleManager/ModuleManager.jsx new file mode 100644 index 0000000000..ee4d9a6a47 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleManager/ModuleManager.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const ModuleManager = () => { + return <>; +}; + +export default withEditionSpecificComponent(ModuleManager, 'Modules'); diff --git a/frontend/src/modules/Modules/components/ModuleManager/index.js b/frontend/src/modules/Modules/components/ModuleManager/index.js new file mode 100644 index 0000000000..5a89ccc37e --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleManager/index.js @@ -0,0 +1 @@ +export { default } from './ModuleManager'; diff --git a/frontend/src/modules/Modules/components/ModuleViewer/ModuleViewer.jsx b/frontend/src/modules/Modules/components/ModuleViewer/ModuleViewer.jsx new file mode 100644 index 0000000000..2297735672 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleViewer/ModuleViewer.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const ModuleViewer = () => { + return <>; +}; + +export default withEditionSpecificComponent(ModuleViewer, 'Modules'); diff --git a/frontend/src/modules/Modules/components/ModuleViewer/index.js b/frontend/src/modules/Modules/components/ModuleViewer/index.js new file mode 100644 index 0000000000..df8725725a --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleViewer/index.js @@ -0,0 +1 @@ +export { default } from './ModuleViewer'; diff --git a/frontend/src/modules/Modules/components/ModuleViewerInspector/ModuleViewerInspector.jsx b/frontend/src/modules/Modules/components/ModuleViewerInspector/ModuleViewerInspector.jsx new file mode 100644 index 0000000000..b5043e170a --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleViewerInspector/ModuleViewerInspector.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const ModuleViewerInspector = () => { + return <>; +}; + +export default withEditionSpecificComponent(ModuleViewerInspector, 'Modules'); diff --git a/frontend/src/modules/Modules/components/ModuleViewerInspector/index.js b/frontend/src/modules/Modules/components/ModuleViewerInspector/index.js new file mode 100644 index 0000000000..c95c5a7a46 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleViewerInspector/index.js @@ -0,0 +1 @@ +export { default } from './ModuleViewerInspector'; diff --git a/frontend/src/modules/Modules/components/ModuleWidgetBox/ModuleWidgetBox.jsx b/frontend/src/modules/Modules/components/ModuleWidgetBox/ModuleWidgetBox.jsx new file mode 100644 index 0000000000..7a43aa67e3 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleWidgetBox/ModuleWidgetBox.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const ModuleWidgetBox = () => { + return <>; +}; + +export default withEditionSpecificComponent(ModuleWidgetBox, 'Modules'); diff --git a/frontend/src/modules/Modules/components/ModuleWidgetBox/index.js b/frontend/src/modules/Modules/components/ModuleWidgetBox/index.js new file mode 100644 index 0000000000..1a903dadf7 --- /dev/null +++ b/frontend/src/modules/Modules/components/ModuleWidgetBox/index.js @@ -0,0 +1 @@ +export { default } from './ModuleWidgetBox'; diff --git a/frontend/src/modules/Modules/components/index.js b/frontend/src/modules/Modules/components/index.js new file mode 100644 index 0000000000..0cd5f2301e --- /dev/null +++ b/frontend/src/modules/Modules/components/index.js @@ -0,0 +1,19 @@ +import ModuleContainer from './ModuleContainer'; +import ModuleViewer from './ModuleViewer'; +import ModuleContainerInspector from './ModuleContainerInspector'; +import ModuleViewerInspector from './ModuleViewerInspector'; +import ModuleWidgetBox from './ModuleWidgetBox'; +import ModuleManager from './ModuleManager'; +import ModuleEditorBanner from './ModuleEditorBanner'; +import ModuleContainerBlank from './ModuleContainerBlank'; + +export { + ModuleContainer, + ModuleViewer, + ModuleContainerInspector, + ModuleViewerInspector, + ModuleWidgetBox, + ModuleManager, + ModuleEditorBanner, + ModuleContainerBlank, +}; diff --git a/frontend/src/modules/Modules/index.js b/frontend/src/modules/Modules/index.js new file mode 100644 index 0000000000..04dab890ed --- /dev/null +++ b/frontend/src/modules/Modules/index.js @@ -0,0 +1,2 @@ +const Modules = (props) => []; +export default Modules; diff --git a/frontend/src/modules/common/components/BaseAppActionModal/BaseAppActionModal.jsx b/frontend/src/modules/common/components/BaseAppActionModal/BaseAppActionModal.jsx index 76b647f545..3d21041205 100644 --- a/frontend/src/modules/common/components/BaseAppActionModal/BaseAppActionModal.jsx +++ b/frontend/src/modules/common/components/BaseAppActionModal/BaseAppActionModal.jsx @@ -4,7 +4,7 @@ import { AppModal } from '@/_components'; const BaseAppActionModal = ({ configs, modalStates, ...props }) => { const getActiveConfig = () => { switch (true) { - case modalStates.showCreateAppModal || modalStates.showCreateModuleModal: + case modalStates.showCreateAppModal: return configs.create; case modalStates.showCloneAppModal: return configs.clone; diff --git a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx index a0e14f7bb2..f30c40b82f 100644 --- a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx +++ b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx @@ -4,12 +4,14 @@ import { shallow } from 'zustand/shallow'; import { ToolTip } from '@/_components/ToolTip'; import { PromoteConfirmationModal } from './components'; import useStore from '@/AppBuilder/_stores/store'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const PromoteVersionButton = () => { + const { moduleId } = useModuleContext(); const [promoteModalData, setPromoteModalData] = useState(null); const { isSaving, editingVersion, appVersionEnvironment, environments, selectedEnvironment, currentEnvIndex } = useStore( (state) => ({ - isSaving: state.app.isSaving, + isSaving: state.appStore.modules[moduleId].app.isSaving, editingVersion: state.currentVersionId, selectedEnvironment: state.selectedEnvironment, environments: state.environments, diff --git a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx index cae938d130..cbc1284c06 100644 --- a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx +++ b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx @@ -9,8 +9,10 @@ import ArrowRightIcon from '@assets/images/icons/arrow-right.svg'; import '@/_styles/versions.scss'; import { shallow } from 'zustand/shallow'; import useStore from '@/AppBuilder/_stores/store'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const PromoteConfirmationModal = React.memo(({ data, onClose }) => { + const { moduleId } = useModuleContext(); const [promotingEnvironment, setPromotingEnvironment] = useState(false); const darkMode = localStorage.getItem('darkMode') === 'true' || false; const currentVersionId = useStore((state) => state.currentVersionId); @@ -22,7 +24,7 @@ const PromoteConfirmationModal = React.memo(({ data, onClose }) => { (state) => ({ promoteAppVersionAction: state.promoteAppVersionAction, selectedVersion: state.selectedVersion, - creationMode: state.app.creationMode, + creationMode: state.appStore.modules[moduleId].app.creationMode, }), shallow ); diff --git a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/ReleaseVersionButton/ReleaseVersionButton.jsx b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/ReleaseVersionButton/ReleaseVersionButton.jsx index 494eaa52e9..48b77cc238 100644 --- a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/ReleaseVersionButton/ReleaseVersionButton.jsx +++ b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/ReleaseVersionButton/ReleaseVersionButton.jsx @@ -8,8 +8,10 @@ import { shallow } from 'zustand/shallow'; import '@/_styles/versions.scss'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import useStore from '@/AppBuilder/_stores/store'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const ReleaseVersionButton = function DeployVersionButton() { + const { moduleId } = useModuleContext(); const [isReleasing, setIsReleasing] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const { isVersionReleased, editingVersion, updateReleasedVersionId, appId, versionToBeReleased, name } = useStore( @@ -19,7 +21,7 @@ const ReleaseVersionButton = function DeployVersionButton() { editingVersion: state.editingVersion, isEditorFreezed: state.isEditorFreezed, updateReleasedVersionId: state.updateReleasedVersionId, - appId: state.app.appId, + appId: state.appStore.modules[moduleId].app.appId, versionToBeReleased: state.currentVersionId, // selectedVersionId: state.selectedVersion.id, }), diff --git a/frontend/src/modules/dashboard/components/AppTypeTab/AppTypeTab.jsx b/frontend/src/modules/dashboard/components/AppTypeTab/AppTypeTab.jsx new file mode 100644 index 0000000000..293d325c59 --- /dev/null +++ b/frontend/src/modules/dashboard/components/AppTypeTab/AppTypeTab.jsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const AppTypeTab = () => { + return <>; +}; +export default withEditionSpecificComponent(AppTypeTab, 'Dashboard'); diff --git a/frontend/src/modules/dashboard/components/AppTypeTab/index.js b/frontend/src/modules/dashboard/components/AppTypeTab/index.js new file mode 100644 index 0000000000..ae18e0c4c3 --- /dev/null +++ b/frontend/src/modules/dashboard/components/AppTypeTab/index.js @@ -0,0 +1 @@ +export { default } from './AppTypeTab'; diff --git a/frontend/src/modules/dashboard/components/index.js b/frontend/src/modules/dashboard/components/index.js index ab540b5feb..d4ffff00c4 100644 --- a/frontend/src/modules/dashboard/components/index.js +++ b/frontend/src/modules/dashboard/components/index.js @@ -6,6 +6,7 @@ import SettingsMenu from './SettingsMenu'; import WorkspaceActions from './WorkspaceActions'; import ConsultationBanner from './ConsultationBanner'; import UserGroupMigrationBanner from './UserGroupMigrationBanner'; +import AppTypeTab from './AppTypeTab'; export { ImportAppMenu, @@ -16,4 +17,5 @@ export { WorkspaceActions, ConsultationBanner, UserGroupMigrationBanner, + AppTypeTab, }; diff --git a/frontend/src/modules/index.js b/frontend/src/modules/index.js index 69786dee07..f6feaf45c0 100644 --- a/frontend/src/modules/index.js +++ b/frontend/src/modules/index.js @@ -13,6 +13,7 @@ import Settings from './Settings'; import Workflows from './workflows'; import WorkspaceSettings from './WorkspaceSettings'; import RenderWorkflow from './RenderWorkflow'; +import Modules from './Modules'; export { onboarding, @@ -27,4 +28,5 @@ export { getAuditLogsRoutes, RenderWorkflow, AiBuilder, + Modules, }; diff --git a/server/ee b/server/ee index f70ac83c38..b58301bdd8 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit f70ac83c38e0a8b44aeb2a0fb2059690eb5e2f46 +Subproject commit b58301bdd81d035f491debec0a55c7fc342c4f12 diff --git a/server/migrations/1744610362161-CreatePagePermissions.ts b/server/migrations/1744610362161-CreatePagePermissions.ts index ca4afbac66..ebf622da8b 100644 --- a/server/migrations/1744610362161-CreatePagePermissions.ts +++ b/server/migrations/1744610362161-CreatePagePermissions.ts @@ -1,13 +1,7 @@ import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; -import { TOOLJET_EDITIONS } from '@modules/app/constants'; -import { getTooljetEdition } from '@helpers/utils.helper'; export class CreatePagePermissions1744610362161 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - if (getTooljetEdition() === TOOLJET_EDITIONS.CE) { - return; - } - await queryRunner.createTable( new Table({ name: 'page_permissions', diff --git a/server/migrations/1744611380594-CreatePageUsers.ts b/server/migrations/1744611380594-CreatePageUsers.ts index 5fe4d126c7..f1c6c89beb 100644 --- a/server/migrations/1744611380594-CreatePageUsers.ts +++ b/server/migrations/1744611380594-CreatePageUsers.ts @@ -1,13 +1,7 @@ import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; -import { TOOLJET_EDITIONS } from '@modules/app/constants'; -import { getTooljetEdition } from '@helpers/utils.helper'; export class CreatePageUsers1744611380594 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - if (getTooljetEdition() === TOOLJET_EDITIONS.CE) { - return; - } - await queryRunner.createTable( new Table({ name: 'page_users', diff --git a/server/src/dto/import-resources.dto.ts b/server/src/dto/import-resources.dto.ts index f00992213e..89b3ee182c 100644 --- a/server/src/dto/import-resources.dto.ts +++ b/server/src/dto/import-resources.dto.ts @@ -1,4 +1,4 @@ -import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested } from 'class-validator'; +import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested, IsBoolean } from 'class-validator'; import { Transform, Type } from 'class-transformer'; import { ValidateTooljetDatabaseSchema } from './validators/tooljet-database.validator'; import { TjdbSchemaToLatestVersion } from './transformers/resource-transformer'; @@ -28,6 +28,10 @@ export class ImportResourcesDto { // and instantiated data @ValidateTooljetDatabaseSchema({ each: true }) tooljet_database: ImportTooljetDatabaseDto[]; + + @IsOptional() + @IsBoolean() + skip_page_permissions_group_check?: boolean; } export class ImportAppDto { diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts index 92868d7510..693f4f930c 100644 --- a/server/src/entities/group_permissions.entity.ts +++ b/server/src/entities/group_permissions.entity.ts @@ -3,7 +3,6 @@ import { Column, CreateDateColumn, Entity, - Index, JoinColumn, ManyToOne, OneToMany, @@ -21,7 +20,6 @@ export class GroupPermissions extends BaseEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Index() @Column({ name: 'organization_id', nullable: false }) organizationId: string; diff --git a/server/src/entities/group_users.entity.ts b/server/src/entities/group_users.entity.ts index 29771a5557..03ac55386b 100644 --- a/server/src/entities/group_users.entity.ts +++ b/server/src/entities/group_users.entity.ts @@ -3,7 +3,6 @@ import { Column, CreateDateColumn, Entity, - Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, @@ -17,11 +16,9 @@ export class GroupUsers extends BaseEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Index() @Column({ name: 'user_id', nullable: false }) userId: string; - @Index() @Column({ name: 'group_id', nullable: false }) groupId: string; diff --git a/server/src/entities/page_users.entity.ts b/server/src/entities/page_users.entity.ts index ca3ef77c65..960be5b32f 100644 --- a/server/src/entities/page_users.entity.ts +++ b/server/src/entities/page_users.entity.ts @@ -1,4 +1,4 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, Index } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm'; import { User } from './user.entity'; import { PagePermission } from './page_permissions.entity'; import { GroupPermissions } from './group_permissions.entity'; @@ -8,15 +8,12 @@ export class PageUser { @PrimaryGeneratedColumn('uuid') id: string; - @Index() @Column({ name: 'page_permissions_id', type: 'uuid' }) pagePermissionsId: string; - @Index() @Column({ name: 'user_id', type: 'uuid', nullable: true }) userId: string | null; - @Index() @Column({ name: 'permission_groups_id', type: 'uuid', nullable: true }) permissionGroupsId: string | null; diff --git a/server/src/helpers/error_type.constant.ts b/server/src/helpers/error_type.constant.ts index 4b968f002e..cb483e8896 100644 --- a/server/src/helpers/error_type.constant.ts +++ b/server/src/helpers/error_type.constant.ts @@ -1,5 +1,7 @@ export const APP_ERROR_TYPE = { IMPORT_EXPORT_SERVICE: { UNSUPPORTED_VERSION_ERROR: 'Apps built on later versions of ToolJet cannot be imported', + PAGE_PERMISSION_GROUP_ERROR: 'Following groups are missing from the workspace', + PERMISSION_CHECK: 'permission-check', }, }; diff --git a/server/src/modules/app/constants/modules.ts b/server/src/modules/app/constants/modules.ts index ab87064a84..1522332a09 100644 --- a/server/src/modules/app/constants/modules.ts +++ b/server/src/modules/app/constants/modules.ts @@ -39,4 +39,5 @@ export enum MODULES { APP_PERMISSIONS = 'AppPermissions', AUDIT_LOGS = 'auditLogs', EXTERNAL_APIS = 'externalApis', + MODULES = 'Modules', } diff --git a/server/src/modules/app/module.ts b/server/src/modules/app/module.ts index 435122b0cb..7f95f46b46 100644 --- a/server/src/modules/app/module.ts +++ b/server/src/modules/app/module.ts @@ -39,6 +39,7 @@ import { TemplatesModule } from '@modules/templates/module'; import { ImportExportResourcesModule } from '@modules/import-export-resources/module'; import { TooljetDbModule } from '@modules/tooljet-db/module'; import { WorkflowsModule } from '@modules/workflows/module'; +import { ModulesModule } from '@modules/modules/module'; import { AiModule } from '@modules/ai/module'; import { CustomStylesModule } from '@modules/custom-styles/module'; import { AppPermissionsModule } from '@modules/app-permissions/module'; @@ -95,6 +96,7 @@ export class AppModule implements OnModuleInit { await TemplatesModule.register(configs), await TooljetDbModule.register(configs), await WorkflowsModule.register(configs), + await ModulesModule.register(configs), await AiModule.register(configs), await CustomStylesModule.register(configs), await AppPermissionsModule.register(configs), diff --git a/server/src/modules/apps/constants/index.ts b/server/src/modules/apps/constants/index.ts index 234950ca4d..81b7b8cae4 100644 --- a/server/src/modules/apps/constants/index.ts +++ b/server/src/modules/apps/constants/index.ts @@ -15,6 +15,7 @@ export enum FEATURE_KEY { export enum APP_TYPES { FRONT_END = 'front-end', WORKFLOW = 'workflow', + MODULE = 'module', } export enum LayoutDimensionUnits { diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts index 6565c17ed1..2a54c29326 100644 --- a/server/src/modules/apps/module.ts +++ b/server/src/modules/apps/module.ts @@ -39,15 +39,7 @@ export class AppsModule { return { module: AppsModule, imports: [ - TypeOrmModule.forFeature([ - App, - Page, - EventHandler, - Organization, - Component, - VersionRepository, - RolesRepository, - ]), + TypeOrmModule.forFeature([App, Page, EventHandler, Organization, Component, VersionRepository]), await FolderAppsModule.register(configs), await ThemesModule.register(configs), await FoldersModule.register(configs), diff --git a/server/src/modules/apps/service.ts b/server/src/modules/apps/service.ts index 27fd03c034..7d3b865b60 100644 --- a/server/src/modules/apps/service.ts +++ b/server/src/modules/apps/service.ts @@ -31,6 +31,7 @@ import { FoldersUtilService } from '@modules/folders/util.service'; import { FolderAppsUtilService } from '@modules/folder-apps/util.service'; import { PageService } from './services/page.service'; import { EventsService } from './services/event.service'; +import { ComponentsService } from './services/component.service'; import { LICENSE_FIELD } from '@modules/licensing/constants'; import { AppEnvironment } from '@entities/app_environments.entity'; import { OrganizationThemesUtilService } from '@modules/organization-themes/util.service'; @@ -38,6 +39,7 @@ import { IAppsService } from './interfaces/IService'; import { AiUtilService } from '@modules/ai/util.service'; import { RequestContext } from '@modules/request-context/service'; import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants'; +import { MODULES } from '@modules/app/constants/modules'; @Injectable() export class AppsService implements IAppsService { @@ -52,8 +54,9 @@ export class AppsService implements IAppsService { protected readonly pageService: PageService, protected readonly eventService: EventsService, protected readonly organizationThemeUtilService: OrganizationThemesUtilService, - protected readonly aiUtilService: AiUtilService - ) { } + protected readonly aiUtilService: AiUtilService, + protected readonly componentsService: ComponentsService + ) {} async create(user: User, appCreateDto: AppCreateDto) { const { name, icon, type } = appCreateDto; return await dbTransactionWrap(async (manager: EntityManager) => { @@ -98,8 +101,8 @@ export class AppsService implements IAppsService { const version = versionId ? await this.versionRepository.findById(versionId, app.id) : versionName - ? await this.versionRepository.findByName(versionName, app.id) - : // Handle version retrieval based on env + ? await this.versionRepository.findByName(versionName, app.id) + : // Handle version retrieval based on env await this.versionRepository.findLatestVersionForEnvironment( app.id, envId, @@ -200,6 +203,13 @@ export class AppsService implements IAppsService { apps = await this.appsUtilService.all(user, parseInt(page || '1'), searchKey, type); } + if (type === 'module') { + for (const app of apps) { + const appVersionId = app?.appVersions[0]?.id; + app.moduleContainer = await this.pageService.findModuleContainer(appVersionId); + } + } + const totalCount = await this.appsUtilService.count(user, searchKey, type); const totalPageCount = folderId ? totalFolderCount : totalCount; @@ -296,42 +306,53 @@ export class AppsService implements IAppsService { } async getBySlug(app: App, user: User): Promise { - const versionToLoad = app.currentVersionId - ? await this.versionRepository.findVersion(app.currentVersionId) - : await this.versionRepository.findVersion(app.editingVersion?.id); + const prepareResponse = async (app) => { + const versionToLoad = app.currentVersionId + ? await this.versionRepository.findVersion(app.currentVersionId) + : await this.versionRepository.findVersion(app.editingVersion?.id); - const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(versionToLoad.id) : []; - const eventsForVersion = app.editingVersion ? await this.eventService.findEventsForVersion(versionToLoad.id) : []; - const appTheme = await this.organizationThemeUtilService.getTheme( - app.organizationId, - versionToLoad?.globalSettings?.theme?.id - ); + const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(versionToLoad.id) : []; + const eventsForVersion = app.editingVersion ? await this.eventService.findEventsForVersion(versionToLoad.id) : []; + const appTheme = await this.organizationThemeUtilService.getTheme( + app.organizationId, + versionToLoad?.globalSettings?.theme?.id + ); - if (app?.isPublic && user) { - RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, { - userId: user.id, - organizationId: user.organizationId, - resourceId: app.id, - resourceName: app.name, - }); - } + if (app?.isPublic && user) { + RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, { + userId: user.id, + organizationId: user.organizationId, + resourceId: app.id, + resourceName: app.name, + resourceType: MODULES.APP, + }); + } - // serialize - return { - current_version_id: app['currentVersionId'], - data_queries: versionToLoad?.dataQueries, - definition: versionToLoad?.definition, - is_public: app.isPublic, - is_maintenance_on: app.isMaintenanceOn, - name: app.name, - slug: app.slug, - events: eventsForVersion, - pages: this.appsUtilService.mergeDefaultComponentData(pagesForVersion), - homePageId: versionToLoad.homePageId, - globalSettings: { ...versionToLoad.globalSettings, theme: appTheme }, - showViewerNavigation: versionToLoad.showViewerNavigation, - pageSettings: versionToLoad?.pageSettings, + // serialize + return { + current_version_id: app['currentVersionId'], + data_queries: versionToLoad?.dataQueries, + definition: versionToLoad?.definition, + is_public: app.isPublic, + is_maintenance_on: app.isMaintenanceOn, + name: app.name, + slug: app.slug, + events: eventsForVersion, + pages: this.appsUtilService.mergeDefaultComponentData(pagesForVersion), + homePageId: versionToLoad.homePageId, + globalSettings: { ...versionToLoad.globalSettings, theme: appTheme }, + showViewerNavigation: versionToLoad.showViewerNavigation, + pageSettings: versionToLoad?.pageSettings, + }; }; + + const response = await prepareResponse(app); + + const modules = await this.appsUtilService.fetchModules(app, false, undefined); + + response['modules'] = await Promise.all(modules.map((module) => prepareResponse(module))); + + return response; } async release(app: App, user: User, versionReleaseDto: VersionReleaseDto) { diff --git a/server/src/modules/apps/services/app-import-export.service.ts b/server/src/modules/apps/services/app-import-export.service.ts index 16fa8289f6..5d6da2ea7d 100644 --- a/server/src/modules/apps/services/app-import-export.service.ts +++ b/server/src/modules/apps/services/app-import-export.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { isEmpty, set } from 'lodash'; import { App } from 'src/entities/app.entity'; import { AppEnvironment } from 'src/entities/app_environments.entity'; @@ -33,6 +33,11 @@ import { DataSourcesUtilService } from '@modules/data-sources/util.service'; import { DataSourcesRepository } from '@modules/data-sources/repository'; import { AppEnvironmentUtilService } from '@modules/app-environments/util.service'; import { ComponentsService } from './component.service'; +import { GroupPermissions } from '@entities/group_permissions.entity'; +import { APP_ERROR_TYPE } from '@helpers/error_type.constant'; +import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants'; +import { PagePermission } from '@entities/page_permissions.entity'; +import { PageUser } from '@entities/page_users.entity'; import { UsersUtilService } from '@modules/users/util.service'; interface AppResourceMappings { defaultDataSourceIdMapping: Record; @@ -186,13 +191,31 @@ export class AppImportExportService { } const pages = await manager - .createQueryBuilder(Page, 'pages') - .where('pages.appVersionId IN(:...versionId)', { + .createQueryBuilder(Page, 'page') + .leftJoinAndSelect('page.permissions', 'permission') + .leftJoinAndSelect('permission.users', 'pageUser') + .leftJoinAndSelect('pageUser.permissionGroup', 'permissionGroup') + .where('page.appVersionId IN(:...versionId)', { versionId: appVersions.map((v) => v.id), }) - .orderBy('pages.created_at', 'ASC') + .orderBy('page.created_at', 'ASC') .getMany(); + const pagesWithPermissionGroups = pages.map((page) => { + const groupPermission = page.permissions.find((perm) => perm.type === 'GROUP'); + + return { + ...page, + permissions: groupPermission + ? { + permissionGroup: groupPermission.users + .map((user) => user.permissionGroup?.name) + .filter((name): name is string => Boolean(name)), + } + : undefined, + }; + }); + const components = pages.length > 0 ? await manager @@ -214,7 +237,7 @@ export class AppImportExportService { .getMany(); appToExport['components'] = components; - appToExport['pages'] = pages; + appToExport['pages'] = pagesWithPermissionGroups; appToExport['events'] = events; appToExport['dataQueries'] = dataQueries; appToExport['dataSources'] = dataSources; @@ -812,6 +835,10 @@ export class AppImportExportService { }); } + if (page.permissions) { + pageCreated.permissions = page.permissions; + } + appResourceMappings.pagesMapping[page.id] = pageCreated.id; isHomePage = importingAppVersion.homePageId === page.id; @@ -820,6 +847,9 @@ export class AppImportExportService { updateHomepageId = pageCreated.id; } + //create page permissions of page if flag enabled in dto + await this.createPagePermissionsForGroups(pageCreated, user.organizationId, manager); + const pageComponents = importingComponents.filter((component) => component.pageId === page.id); const newComponentIdsMap = {}; @@ -936,6 +966,7 @@ export class AppImportExportService { }); } } + // relink page groups const updateArr = []; for (const { pageId, groupId } of pageGroupIdArr) { @@ -1327,6 +1358,76 @@ export class AppImportExportService { return pageSettings; } + async checkIfGroupPermissionsExist(pages, organizationId) { + const allGroupNames = new Set(); + + for (const page of pages) { + const groupNames = page.permissions?.permissionGroup || []; + for (const name of groupNames) { + allGroupNames.add(name); + } + } + + if (!allGroupNames.size) return; + + return await dbTransactionWrap(async (manager: EntityManager) => { + const existingGroups = await manager + .createQueryBuilder(GroupPermissions, 'gp') + .where('gp.name IN (:...names)', { names: Array.from(allGroupNames) }) + .andWhere('gp.organizationId = :organizationId', { organizationId }) + .select(['gp.name']) + .getMany(); + + const existingGroupNames = new Set(existingGroups.map((g) => g.name)); + + const missingGroups = Array.from(allGroupNames).filter((name) => !existingGroupNames.has(name)); + + if (missingGroups.length > 0) { + throw new HttpException( + { + message: { type: APP_ERROR_TYPE.IMPORT_EXPORT_SERVICE.PERMISSION_CHECK, data: missingGroups }, + }, + HttpStatus.BAD_REQUEST + ); + } + }); + } + + async createPagePermissionsForGroups(page, organizationId: string, manager: EntityManager) { + const groupNames = page.permissions?.permissionGroup || []; + if (!groupNames.length) return; + + const existingGroups = await manager + .createQueryBuilder(GroupPermissions, 'gp') + .where('gp.name IN (:...names)', { names: groupNames }) + .andWhere('gp.organizationId = :organizationId', { organizationId }) + .getMany(); + + const groupMap = new Map(existingGroups.map((g) => [g.name, g])); + + // Filter to only existing group names + const validGroupNames = groupNames.filter((name) => groupMap.has(name)); + + // If no valid group names exist, do not create permissions + if (!validGroupNames.length) return; + + const permission = manager.create(PagePermission, { + pageId: page.id, + type: PAGE_PERMISSION_TYPE.GROUP, + }); + + const savedPermission = await manager.save(permission); + + const pageUsers = validGroupNames.map((name) => + manager.create(PageUser, { + pagePermissionsId: savedPermission.id, + permissionGroupsId: groupMap.get(name).id, + }) + ); + + await manager.save(pageUsers); + } + async createAppVersionsForImportedApp( manager: EntityManager, user: User, diff --git a/server/src/modules/apps/services/component.service.ts b/server/src/modules/apps/services/component.service.ts index fcc01e52f0..c6ef31f5b8 100644 --- a/server/src/modules/apps/services/component.service.ts +++ b/server/src/modules/apps/services/component.service.ts @@ -12,7 +12,7 @@ const _ = require('lodash'); @Injectable() export class ComponentsService implements IComponentsService { - constructor(protected eventHandlerService: EventsService) {} + constructor(protected eventHandlerService: EventsService) { } findOne(id: string): Promise { return dbTransactionWrap((manager: EntityManager) => { @@ -97,6 +97,7 @@ export class ComponentsService implements IComponentsService { } else if ( (componentData.type === 'DropdownV2' || componentData.type === 'MultiselectV2' || + componentData.type === 'ModuleContainer' || componentData.type === 'Steps') && _.isArray(objValue) ) { diff --git a/server/src/modules/apps/services/page.service.ts b/server/src/modules/apps/services/page.service.ts index fa0b2864e0..02bea48dab 100644 --- a/server/src/modules/apps/services/page.service.ts +++ b/server/src/modules/apps/services/page.service.ts @@ -304,4 +304,8 @@ export class PageService implements IPageService { return await this.pageHelperService.rearrangePagesOrderPostDeletion(pageExists, manager); }, appVersionId); } + + async findModuleContainer(appVersionId: string): Promise { + return this.pageHelperService.findModuleContainer(appVersionId); + } } diff --git a/server/src/modules/apps/services/page.util.service.ts b/server/src/modules/apps/services/page.util.service.ts index cb10863972..aface2cc16 100644 --- a/server/src/modules/apps/services/page.util.service.ts +++ b/server/src/modules/apps/services/page.util.service.ts @@ -79,4 +79,8 @@ export class PageHelperService implements IPageHelperService { page.index = dto.index; return page; } + + public async findModuleContainer(appVersionId: string): Promise { + return null; + } } diff --git a/server/src/modules/apps/services/widget-config/index.js b/server/src/modules/apps/services/widget-config/index.js index fdb4cf26df..c1ac0d205b 100644 --- a/server/src/modules/apps/services/widget-config/index.js +++ b/server/src/modules/apps/services/widget-config/index.js @@ -58,6 +58,8 @@ import { kanbanBoardConfig } from './kanbanBoard'; import { datetimePickerV2Config } from './datetimepickerV2'; import { datePickerV2Config } from './datepickerV2'; import { timePickerConfig } from './timepicker'; +import { moduleContainerConfig } from './moduleContainer'; +import { moduleViewerConfig } from './moduleViewer'; import { emailinputConfig } from './emailinput'; import { phoneinputConfig } from './phoneinput'; import {currencyinputConfig} from './currencyinput'; @@ -126,6 +128,8 @@ const widgets = { linkConfig, iconConfig, boundedBoxConfig, + moduleContainerConfig, + moduleViewerConfig }; const universalProps = { diff --git a/server/src/modules/apps/services/widget-config/moduleContainer.js b/server/src/modules/apps/services/widget-config/moduleContainer.js new file mode 100644 index 0000000000..af0f77c823 --- /dev/null +++ b/server/src/modules/apps/services/widget-config/moduleContainer.js @@ -0,0 +1,36 @@ +export const moduleContainerConfig = { + name: 'ModuleContainer', + displayName: 'Module Container', + description: 'Module Container', + component: 'ModuleContainer', + defaultSize: { + width: 10, + height: 400, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: { + inputItems: { type: 'array', displayName: 'Input' }, + outputItems: { type: 'array', displayName: 'Output' }, + }, + events: {}, + styles: {}, + exposedVariables: {}, + actions: [], + definition: { + others: { + showOnDesktop: { value: '{{true}}' }, + showOnMobile: { value: '{{false}}' }, + }, + properties: { + inputItems: { value: [] }, + outputItems: { value: [] }, + }, + events: [], + styles: { + backgroundColor: { value: '#fff' }, + }, + }, +}; diff --git a/server/src/modules/apps/services/widget-config/moduleViewer.js b/server/src/modules/apps/services/widget-config/moduleViewer.js new file mode 100644 index 0000000000..b0f5342787 --- /dev/null +++ b/server/src/modules/apps/services/widget-config/moduleViewer.js @@ -0,0 +1,31 @@ +export const moduleViewerConfig = { + name: 'ModuleViewer', + displayName: 'Module', + description: 'Module', + component: 'ModuleViewer', + defaultSize: { + width: 10, + height: 400, + }, + others: { + showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' }, + showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, + }, + properties: {}, + events: {}, + styles: {}, + exposedVariables: {}, + actions: [], + definition: { + others: { + showOnDesktop: { value: '{{true}}' }, + showOnMobile: { value: '{{false}}' }, + }, + properties: {}, + events: [], + styles: { + backgroundColor: { value: '#fff' }, + }, + }, + }; + \ No newline at end of file diff --git a/server/src/modules/apps/util.service.ts b/server/src/modules/apps/util.service.ts index 3db7df4a99..ff8aaeda48 100644 --- a/server/src/modules/apps/util.service.ts +++ b/server/src/modules/apps/util.service.ts @@ -37,6 +37,8 @@ import { DataSourcesRepository } from '@modules/data-sources/repository'; import { IAppsUtilService } from './interfaces/IUtilService'; import { DataSourcesUtilService } from '@modules/data-sources/util.service'; import { AppVersionUpdateDto } from '@dto/app-version-update.dto'; +import { Component } from 'src/entities/component.entity'; +import { Layout } from 'src/entities/layout.entity'; import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto'; import { DataQuery } from '@entities/data_query.entity'; import { DataSource } from '@entities/data_source.entity'; @@ -52,7 +54,7 @@ export class AppsUtilService implements IAppsUtilService { protected readonly abilityService: AbilityService, protected readonly dataSourceRepository: DataSourcesRepository, protected readonly dataSourceUtilService: DataSourcesUtilService - ) {} + ) { } async create(name: string, user: User, type: string, manager: EntityManager): Promise { return await dbTransactionWrap(async (manager: EntityManager) => { const app = await catchDbException(() => { @@ -92,8 +94,52 @@ export class AppsUtilService implements IAppsUtilService { }) ); + if (type === 'module') { + const moduleContainer = await manager.save( + manager.create(Component, { + name: 'ModuleContainer', + type: 'ModuleContainer', + pageId: defaultHomePage.id, + properties: { + inputItems: { value: [] }, + outputItems: { value: [] }, + visibility: { value: '{{true}}' }, + }, + styles: { + backgroundColor: { value: '#fff' }, + }, + displayPreferences: { + showOnDesktop: { value: '{{true}}' }, + showOnMobile: { value: '{{true}}' }, + }, + }) + ); + + await manager.save( + manager.create(Layout, { + component: moduleContainer, + type: 'desktop', + top: 50, + left: 6, + height: 400, + width: 38, + }) + ); + + await manager.save( + manager.create(Layout, { + component: moduleContainer, + type: 'mobile', + top: 50, + left: 6, + height: 400, + width: 38, + }) + ); + } + // Set default values for app version - appVersion.showViewerNavigation = true; + appVersion.showViewerNavigation = type === 'module' ? false : true; appVersion.homePageId = defaultHomePage.id; appVersion.globalSettings = { hideHeader: false, @@ -179,8 +225,8 @@ export class AppsUtilService implements IAppsUtilService { const processEnvironmentName = environmentName ? environmentName : !isMultiEnvironmentEnabled - ? 'development' - : null; + ? 'development' + : null; const environment: AppEnvironment = environmentId ? await this.appEnvironmentUtilService.get(organizationId, environmentId) @@ -396,20 +442,24 @@ export class AppsUtilService implements IAppsUtilService { const viewableApps = userAppPermissions.hideAll ? [null, ...userAppPermissions.editableAppsId] : [ - null, - ...Array.from( - new Set([ - ...userAppPermissions.editableAppsId, - ...userAppPermissions.viewableAppsId.filter((id) => !userAppPermissions.hiddenAppsId.includes(id)), - ]) - ), - ]; + null, + ...Array.from( + new Set([ + ...userAppPermissions.editableAppsId, + ...userAppPermissions.viewableAppsId.filter((id) => !userAppPermissions.hiddenAppsId.includes(id)), + ]) + ), + ]; const viewableAppsQb = manager .createQueryBuilder(AppBase, 'viewable_apps') .innerJoin('viewable_apps.user', 'user') .addSelect(['user.firstName', 'user.lastName']) .where('viewable_apps.organizationId = :organizationId', { organizationId: user.organizationId }); + if (type === 'module') { + viewableAppsQb.leftJoinAndSelect('viewable_apps.appVersions', 'versions'); + } + if (type) viewableAppsQb.andWhere('viewable_apps.type = :type', { type: type }); if (searchKey) { @@ -526,6 +576,43 @@ export class AppsUtilService implements IAppsUtilService { return components; } + async fetchModules(app: App, allVersions: boolean = false, versionId: string): Promise { + const versionToLoad = versionId + ? await this.versionRepository.findVersion(versionId) + : app.currentVersionId + ? await this.versionRepository.findVersion(app.currentVersionId) + : await this.versionRepository.findVersion(app.editingVersion?.id); + + const modules = await dbTransactionWrap(async (manager) => { + const moduleComponents = await manager + .createQueryBuilder(Component, 'component') + .leftJoinAndSelect(Page, 'page', 'page.id = component.page_id') + .leftJoinAndSelect(AppVersion, 'app_version', 'app_version.id = page.app_version_id') + .leftJoinAndSelect(App, 'app', 'app.id = app_version.app_id') + .andWhere( + `component.type = :module ${allVersions ? '' : 'AND app_version.id = :appVersionId'} AND app.id = :appId`, + { + module: 'ModuleViewer', + appVersionId: versionToLoad.id, + appId: app.id, + } + ) + .getMany(); + + const moduleAppIds = moduleComponents.map((moduleComponent) => moduleComponent.properties.moduleAppId.value); + + const modules = + moduleAppIds.length > 0 + ? await manager + .createQueryBuilder(App, 'app') + .where('app.id IN (:...moduleAppIds)', { moduleAppIds }) + .distinct(true) + .getMany() + : []; + return modules; + }); + return modules; + } async findAllOrganizationApps(organizationId: string): Promise { return await this.appRepository.findAllOrganizationApps(organizationId); } diff --git a/server/src/modules/import-export-resources/service.ts b/server/src/modules/import-export-resources/service.ts index e8292c8fd7..e47205258e 100644 --- a/server/src/modules/import-export-resources/service.ts +++ b/server/src/modules/import-export-resources/service.ts @@ -69,6 +69,18 @@ export class ImportExportResourcesService { let tableNameMapping = {}; const imports = { app: [], tooljet_database: [], tableNameMapping: {} }; const importingVersion = importResourcesDto.tooljet_version; + const skipPagePermissionsGroupCheck = importResourcesDto.skip_page_permissions_group_check; + + if (!isEmpty(importResourcesDto.app) && !skipPagePermissionsGroupCheck) { + for (const appImportDto of importResourcesDto.app) { + let appParams = appImportDto.definition; + if (appParams?.appV2) { + appParams = { ...appParams.appV2 }; + const pages = appParams?.pages; + pages?.length && (await this.appImportExportService.checkIfGroupPermissionsExist(pages, user.organizationId)); + } + } + } if (!isEmpty(importResourcesDto.tooljet_database)) { const res = await this.tooljetDbImportExportService.bulkImport(importResourcesDto, importingVersion, cloning); diff --git a/server/src/modules/modules/IModulesController.ts b/server/src/modules/modules/IModulesController.ts new file mode 100644 index 0000000000..cd84b9c534 --- /dev/null +++ b/server/src/modules/modules/IModulesController.ts @@ -0,0 +1,6 @@ +import { User } from '@entities/user.entity'; +import { AppCreateDto } from '@modules/apps/dto'; + +export interface IModulesController { + create(user: User, appCreateDto: AppCreateDto): Promise; +} diff --git a/server/src/modules/modules/constants/index.ts b/server/src/modules/modules/constants/index.ts new file mode 100644 index 0000000000..4adfa4a519 --- /dev/null +++ b/server/src/modules/modules/constants/index.ts @@ -0,0 +1,4 @@ +export enum FEATURE_KEY { + CREATE_MODULE = 'create_module', + GET_MODULES = 'get_modules', +} diff --git a/server/src/modules/modules/module.ts b/server/src/modules/modules/module.ts new file mode 100644 index 0000000000..a7af218b81 --- /dev/null +++ b/server/src/modules/modules/module.ts @@ -0,0 +1,54 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { getImportPath } from '@modules/app/constants'; +import { ThemesModule } from '@modules/organization-themes/module'; +import { FoldersModule } from '@modules/folders/module'; +import { FolderAppsModule } from '@modules/folder-apps/module'; +import { OrganizationsModule } from '@modules/organizations/module'; +import { AppEnvironmentsModule } from '@modules/app-environments/module'; +import { OrganizationRepository } from '@modules/organizations/repository'; +import { DataSourcesRepository } from '@modules/data-sources/repository'; +import { VersionRepository } from '@modules/versions/repository'; +import { DataSourcesModule } from '@modules/data-sources/module'; +import { AiModule } from '@modules/ai/module'; +import { AppsRepository } from '@modules/apps/repository'; +import { AppPermissionsModule } from '@modules/app-permissions/module'; +@Module({}) +export class ModulesModule { + static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { + const importPath = await getImportPath(configs.IS_GET_CONTEXT); + const { ModulesController } = await import(`${importPath}/modules/modules.controller`); + const { AppsService } = await import(`${importPath}/apps/service`); + const { AppsUtilService } = await import(`${importPath}/apps/util.service`); + const { PageService } = await import(`${importPath}/apps/services/page.service`); + const { EventsService } = await import(`${importPath}/apps/services/event.service`); + const { ComponentsService } = await import(`${importPath}/apps/services/component.service`); + const { PageHelperService } = await import(`${importPath}/apps/services/page.util.service`); + + return { + module: ModulesModule, + imports: [ + await FolderAppsModule.register(configs), + await ThemesModule.register(configs), + await FoldersModule.register(configs), + await OrganizationsModule.register(configs), + await AppEnvironmentsModule.register(configs), + await DataSourcesModule.register(configs), + await AiModule.register(configs), + await AppPermissionsModule.register(configs), + ], + controllers: [ModulesController], + providers: [ + AppsService, + VersionRepository, + AppsRepository, + PageService, + EventsService, + AppsUtilService, + ComponentsService, + PageHelperService, + OrganizationRepository, + DataSourcesRepository, + ], + }; + } +} diff --git a/server/src/modules/modules/modules.controller.ts b/server/src/modules/modules/modules.controller.ts new file mode 100644 index 0000000000..705d81dece --- /dev/null +++ b/server/src/modules/modules/modules.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Post, UseGuards, Body } from '@nestjs/common'; +import { User } from '@modules/app/decorators/user.decorator'; +import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard'; +import { AppCreateDto } from '@modules/apps/dto'; +import { IModulesController } from '@modules/modules/IModulesController'; +import { InitModule } from '@modules/app/decorators/init-module'; +import { MODULES } from '@modules/app/constants/modules'; +import { InitFeature } from '@modules/app/decorators/init-feature.decorator'; +import { FEATURE_KEY } from '@modules/modules/constants'; + +@InitModule(MODULES.MODULES) +@Controller('modules') +export class ModulesController implements IModulesController { + @InitFeature(FEATURE_KEY.CREATE_MODULE) + @UseGuards(JwtAuthGuard) + @Post() + async create(@User() user, @Body() appCreateDto: AppCreateDto): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/server/src/modules/versions/module.ts b/server/src/modules/versions/module.ts index 261401a18d..7fac84f033 100644 --- a/server/src/modules/versions/module.ts +++ b/server/src/modules/versions/module.ts @@ -9,6 +9,7 @@ import { DataSourcesModule } from '@modules/data-sources/module'; import { AppsRepository } from '@modules/apps/repository'; import { FeatureAbilityFactory } from './ability'; import { getImportPath } from '@modules/app/constants'; +import { AppPermissionsModule } from '@modules/app-permissions/module'; export class VersionModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { @@ -33,6 +34,7 @@ export class VersionModule { await DataSourcesModule.register(configs), await AppEnvironmentsModule.register(configs), await ThemesModule.register(configs), + await AppPermissionsModule.register(configs), ], controllers: [ComponentsController, EventsController, PagesController, VersionController, VersionControllerV2], providers: [ diff --git a/server/src/modules/versions/repository.ts b/server/src/modules/versions/repository.ts index d67b56def1..110f41f2df 100644 --- a/server/src/modules/versions/repository.ts +++ b/server/src/modules/versions/repository.ts @@ -164,4 +164,20 @@ export class VersionRepository extends Repository { return appVersion.app; }, manager || this.manager); } + + async findVersionsFromApp(app: App, manager?: EntityManager): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const appVersions = await manager.find(AppVersion, { + where: { appId: app.id }, + relations: [ + 'app', + 'dataQueries', + 'dataQueries.dataSource', + 'dataQueries.plugins', + 'dataQueries.plugins.manifestFile', + ], + }); + return appVersions; + }, manager || this.manager); + } } diff --git a/server/src/modules/versions/service.ts b/server/src/modules/versions/service.ts index 5ab17efd5c..84620d446c 100644 --- a/server/src/modules/versions/service.ts +++ b/server/src/modules/versions/service.ts @@ -108,62 +108,78 @@ export class VersionService implements IVersionService { } async getVersion(app: App, user: User): Promise { - const versionId = app.appVersions[0].id; - const appVersion = await this.versionRepository.findVersion(versionId); - - const pagesForVersion = await this.pageService.findPagesForVersion(versionId); - const eventsForVersion = await this.eventsService.findEventsForVersion(versionId); - - const appCurrentEditingVersion = JSON.parse(JSON.stringify(appVersion)); - - if ( - appCurrentEditingVersion && - !(await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT)) - ) { - const developmentEnv = await this.appEnvironmentUtilService.getByPriority(user.organizationId); - appCurrentEditingVersion['currentEnvironmentId'] = developmentEnv.id; - } - - let shouldFreezeEditor = false; - if (appCurrentEditingVersion) { - const hasMultiEnvLicense = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT); - if (hasMultiEnvLicense) { - const currentEnvironment = await this.appEnvironmentUtilService.get( - user.organizationId, - appCurrentEditingVersion['currentEnvironmentId'] - ); - shouldFreezeEditor = currentEnvironment.priority > 1; + const prepareResponse = async (app: App, versionId: string) => { + let appVersion, + updatedVersionId = versionId; + if (updatedVersionId) { + appVersion = await this.versionRepository.findVersion(updatedVersionId); } else { + appVersion = await this.versionRepository.findVersionsFromApp(app); + appVersion = appVersion[0]; + updatedVersionId = appVersion.id; + } + + const pagesForVersion = await this.pageService.findPagesForVersion(updatedVersionId); + const eventsForVersion = await this.eventsService.findEventsForVersion(updatedVersionId); + + const appCurrentEditingVersion = JSON.parse(JSON.stringify(appVersion)); + + if ( + appCurrentEditingVersion && + !(await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT)) + ) { const developmentEnv = await this.appEnvironmentUtilService.getByPriority(user.organizationId); appCurrentEditingVersion['currentEnvironmentId'] = developmentEnv.id; } - } - delete appCurrentEditingVersion['app']; + let shouldFreezeEditor = false; + if (appCurrentEditingVersion) { + const hasMultiEnvLicense = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT); + if (hasMultiEnvLicense) { + const currentEnvironment = await this.appEnvironmentUtilService.get( + user.organizationId, + appCurrentEditingVersion['currentEnvironmentId'] + ); + shouldFreezeEditor = currentEnvironment.priority > 1; + } else { + const developmentEnv = await this.appEnvironmentUtilService.getByPriority(user.organizationId); + appCurrentEditingVersion['currentEnvironmentId'] = developmentEnv.id; + } + } - const appData = { - ...app, + delete appCurrentEditingVersion['app']; + + const appData = { + ...app, + }; + + delete appData['editingVersion']; + + const editingVersion = camelizeKeys(appCurrentEditingVersion); + + // Inject app theme + const appTheme = await this.organizationThemesUtilService.getTheme( + user.organizationId, + editingVersion?.globalSettings?.theme?.id + ); + + editingVersion['globalSettings']['theme'] = appTheme; + + return { + ...appData, + editing_version: editingVersion, + pages: this.appUtilService.mergeDefaultComponentData(pagesForVersion), + events: eventsForVersion, + should_freeze_editor: app.creationMode === 'GIT' || shouldFreezeEditor, + }; }; - delete appData['editingVersion']; + const response = await prepareResponse(app, app.appVersions?.[0]?.id); + const modules = await this.appUtilService.fetchModules(app, false, undefined); - const editingVersion = camelizeKeys(appCurrentEditingVersion); + response['modules'] = await Promise.all(modules.map((module) => prepareResponse(module, undefined))); - // Inject app theme - const appTheme = await this.organizationThemesUtilService.getTheme( - user.organizationId, - editingVersion?.globalSettings?.theme?.id - ); - - editingVersion['globalSettings']['theme'] = appTheme; - - return { - ...appData, - editing_version: editingVersion, - pages: this.appUtilService.mergeDefaultComponentData(pagesForVersion), - events: eventsForVersion, - should_freeze_editor: app.creationMode === 'GIT' || shouldFreezeEditor, - }; + return response; } async update(app: App, user: User, appVersionUpdateDto: AppVersionUpdateDto) { diff --git a/server/src/modules/workflows/module.ts b/server/src/modules/workflows/module.ts index 77dfbd0af3..389a754701 100644 --- a/server/src/modules/workflows/module.ts +++ b/server/src/modules/workflows/module.ts @@ -71,7 +71,6 @@ export class WorkflowsModule { WorkflowExecutionNode, WorkflowExecutionNode, WorkflowExecutionEdge, - RolesRepository, ]), ThrottlerModule.forRootAsync({ imports: [ConfigModule],