diff --git a/.github/workflows/render-preview-deploy.yml b/.github/workflows/render-preview-deploy.yml index f10dff0651..9abca231dd 100644 --- a/.github/workflows/render-preview-deploy.yml +++ b/.github/workflows/render-preview-deploy.yml @@ -59,7 +59,7 @@ jobs: }, { "key": "TOOLJET_DB", - "value": "${{ env.PR_NUMBER }}" + "value": "${{ env.PR_NUMBER }}-tjdb" }, { "key": "TOOLJET_DB_HOST", @@ -68,7 +68,7 @@ jobs: { "key": "TOOLJET_DB_USER", "value": "${{ secrets.RENDER_PG_USER }}" - }, + }, { "key": "TOOLJET_DB_PASS", "value": "${{ secrets.RENDER_PG_PASS }}" @@ -77,17 +77,13 @@ jobs: "key": "TOOLJET_DB_PORT", "value": "5432" }, - { - "key": "TOOLJET_DB_STATEMENT_TIMEOUT", - "value": "60000" - }, { "key": "PGRST_DB_PRE_CONFIG", "value": "postgrest.pre_config" }, { "key": "PGRST_DB_URI", - "value": "postgres://${{ secrets.RENDER_PG_USER }}:${{ secrets.RENDER_PG_PASS }}@${{ secrets.RENDER_PG_HOST }}/${{ env.PR_NUMBER }}" + "value": "postgres://${{ secrets.RENDER_PG_USER }}:${{ secrets.RENDER_PG_PASS }}@${{ secrets.RENDER_PG_HOST }}/${{ env.PR_NUMBER }}-tjdb" }, { "key": "PGRST_HOST", @@ -258,12 +254,13 @@ jobs: - name: Wait after installing PostgreSQL run: sleep 25 - - name: Drop PostgreSQL PR database + - name: Drop PostgreSQL PR databases env: PGHOST: ${{ secrets.RENDER_DS_PG_HOST }} PGPORT: 5432 PGUSER: ${{ secrets.RENDER_DS_PG_USER }} PGDATABASE: ${{ env.PR_NUMBER }} + PGTJBDATABASE: ${{ env.PR_NUMBER }}-tjdb run: | if PGPASSWORD=${{ secrets.RENDER_DS_PG_PASS }} psql -h $PGHOST -p $PGPORT -U $PGUSER -lqt | cut -d \| -f 1 | grep -qw $PGDATABASE; then echo "Database $PGDATABASE exists, deleting..." @@ -272,6 +269,13 @@ jobs: echo "Database $PGDATABASE does not exist." fi + if PGPASSWORD=${{ secrets.RENDER_DS_PG_PASS }} psql -h $PGHOST -p $PGPORT -U $PGUSER -lqt | cut -d \| -f 1 | grep -qw $PGTJBDATABASE; then + echo "Database $PGTJBDATABASE exists, deleting..." + PGPASSWORD=${{ secrets.RENDER_DS_PG_PASS }} psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -c "drop database \"$PGTJBDATABASE\" ;" + else + echo "Database $PGTJBDATABASE does not exist." + fi + suspend-review-app: if: ${{ github.event.action == 'labeled' && github.event.label.name == 'suspend-review-app' }} runs-on: ubuntu-latest @@ -342,3 +346,275 @@ jobs: } catch (e) { console.log(e) } + + recreate-review-app: + if: ${{ github.event.action == 'labeled' && github.event.label.name == 'recreate-review-app' }} + runs-on: ubuntu-latest + + steps: + - name: Delete service + run: | + export SERVICE_ID=$(curl --request GET \ + --url 'https://api.render.com/v1/services?name=ToolJet%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \ + --header 'accept: application/json' \ + --header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \ + jq -r '.[0].service.id') + + curl --request DELETE \ + --url https://api.render.com/v1/services/$SERVICE_ID \ + --header 'accept: application/json' \ + --header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' + + - uses: actions/github-script@v6 + with: + script: | + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'destroy-review-app' + }) + } catch (e) { + console.log(e) + } + + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'suspend-review-app' + }) + } catch (e) { + console.log(e) + } + + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'active-review-app' + }) + } catch (e) { + console.log(e) + } + + - name: Install PostgreSQL client + run: | + sudo apt update + sudo apt install postgresql-client -y + + - name: Wait after installing PostgreSQL + run: sleep 25 + + - name: Drop PostgreSQL PR databases + env: + PGHOST: ${{ secrets.RENDER_DS_PG_HOST }} + PGPORT: 5432 + PGUSER: ${{ secrets.RENDER_DS_PG_USER }} + PGDATABASE: ${{ env.PR_NUMBER }} + PGTJBDATABASE: ${{ env.PR_NUMBER }}-tjdb + run: | + if PGPASSWORD=${{ secrets.RENDER_DS_PG_PASS }} psql -h $PGHOST -p $PGPORT -U $PGUSER -lqt | cut -d \| -f 1 | grep -qw $PGDATABASE; then + echo "Database $PGDATABASE exists, deleting..." + PGPASSWORD=${{ secrets.RENDER_DS_PG_PASS }} psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -c "drop database \"$PGDATABASE\" ;" + else + echo "Database $PGDATABASE does not exist." + fi + + if PGPASSWORD=${{ secrets.RENDER_DS_PG_PASS }} psql -h $PGHOST -p $PGPORT -U $PGUSER -lqt | cut -d \| -f 1 | grep -qw $PGTJBDATABASE; then + echo "Database $PGTJBDATABASE exists, deleting..." + PGPASSWORD=${{ secrets.RENDER_DS_PG_PASS }} psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -c "drop database \"$PGTJBDATABASE\" ;" + else + echo "Database $PGTJBDATABASE does not exist." + fi + + - name: Create deployment + id: create-deployment + run: | + export RESPONSE=$(curl --request POST \ + --url https://api.render.com/v1/services \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header 'Authorization: Bearer ${{ secrets.RENDER_API_KEY }}' \ + --data ' + { + "autoDeploy": "yes", + "branch": "${{ env.BRANCH_NAME }}", + "name": "ToolJet PR #${{ env.PR_NUMBER }}", + "notifyOnFail": "default", + "ownerId": "tea-caeo4bj19n072h3dddc0", + "repo": "https://${{ secrets.RENDER_GITHUB_PAT }}@github.com/ToolJet/tj-ee", + "slug": "tooljet-pr-${{ env.PR_NUMBER }}", + "suspended": "not_suspended", + "suspenders": [], + "type": "web_service", + "envVars": [ + { + "key": "PG_HOST", + "value": "${{ secrets.RENDER_PG_HOST }}" + }, + { + "key": "PG_PORT", + "value": "5432" + }, + { + "key": "PG_USER", + "value": "${{ secrets.RENDER_PG_USER }}" + }, + { + "key": "PG_PASS", + "value": "${{ secrets.RENDER_PG_PASS }}" + }, + { + "key": "PG_DB", + "value": "${{ env.PR_NUMBER }}" + }, + { + "key": "TOOLJET_DB", + "value": "${{ env.PR_NUMBER }}-tjdb" + }, + { + "key": "TOOLJET_DB_HOST", + "value": "${{ secrets.RENDER_PG_HOST }}" + }, + { + "key": "TOOLJET_DB_USER", + "value": "${{ secrets.RENDER_PG_USER }}" + }, + { + "key": "TOOLJET_DB_PASS", + "value": "${{ secrets.RENDER_PG_PASS }}" + }, + { + "key": "TOOLJET_DB_PORT", + "value": "5432" + }, + { + "key": "PGRST_DB_PRE_CONFIG", + "value": "postgrest.pre_config" + }, + { + "key": "PGRST_DB_URI", + "value": "postgres://${{ secrets.RENDER_PG_USER }}:${{ secrets.RENDER_PG_PASS }}@${{ secrets.RENDER_PG_HOST }}/${{ env.PR_NUMBER }}-tjdb" + }, + { + "key": "PGRST_HOST", + "value": "127.0.0.1:3000" + }, + { + "key": "PGRST_JWT_SECRET", + "value": "r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" + }, + { + "key": "PGRST_LOG_LEVEL", + "value": "info" + }, + { + "key": "PORT", + "value": "80" + }, + { + "key": "TOOLJET_HOST", + "value": "https://tooljet-pr-${{ env.PR_NUMBER }}.onrender.com" + }, + { + "key": "DISABLE_TOOLJET_TELEMETRY", + "value": "true" + }, + { + "key": "SMTP_ADDRESS", + "value": "smtp.mailtrap.io" + }, + { + "key": "SMTP_DOMAIN", + "value": "smtp.mailtrap.io" + }, + { + "key": "SMTP_PORT", + "value": "2525" + }, + { + "key": "SMTP_USERNAME", + "value": "${{ secrets.RENDER_SMTP_USERNAME }}" + }, + { + "key": "SMTP_PASSWORD", + "value": "${{ secrets.RENDER_SMTP_PASSWORD }}" + }, + { + "key": "REDIS_HOST", + "value": "${{ secrets.RENDER_REDIS_HOST }}" + }, + { + "key": "REDIS_PORT", + "value": "${{ secrets.RENDER_REDIS_PORT }}" + }, + { + "key": "LICENSE_KEY", + "value": "${{ secrets.RENDER_LICENSE_KEY }}" + }, + { + "key": "TOOLJET_MARKETPLACE_URL", + "value": "${{ secrets.MARKETPLACE_BUCKET }}" + } + ], + "serviceDetails": { + "disk": null, + "env": "docker", + "envSpecificDetails": { + "dockerCommand": "", + "dockerContext": "./", + "dockerfilePath": "./docker/preview.Dockerfile" + }, + "healthCheckPath": "/api/health", + "numInstances": 1, + "openPorts": [{ + "port": 80, + "protocol": "TCP" + }], + "plan": "starter", + "pullRequestPreviewsEnabled": "no", + "region": "oregon", + "url": "https://tooljet-pr-${{ env.PR_NUMBER }}.onrender.com" + } + }') + + echo "response: $RESPONSE" + export SERVICE_ID=$(echo $RESPONSE | jq -r '.service.id') + echo "SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV + + - name: Comment deployment URL + uses: actions/github-script@v5 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Deployment: https://tooljet-pr-${{ env.PR_NUMBER }}.onrender.com \n Dashboard: https://dashboard.render.com/web/${{ env.SERVICE_ID }}' + }) + + - uses: actions/github-script@v6 + with: + script: | + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'create-review-app' + }) + } catch (e) { + console.log(e) + } + + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['active-review-app'] + }) diff --git a/.github/workflows/review-path-deploy.yml b/.github/workflows/review-path-deploy.yml index f45d90aa47..8d3bed14e3 100644 --- a/.github/workflows/review-path-deploy.yml +++ b/.github/workflows/review-path-deploy.yml @@ -59,7 +59,7 @@ jobs: }, { "key": "TOOLJET_DB", - "value": "${{ env.PR_NUMBER }}" + "value": "${{ env.PR_NUMBER }}-tjdb" }, { "key": "TOOLJET_DB_HOST", @@ -68,7 +68,7 @@ jobs: { "key": "TOOLJET_DB_USER", "value": "${{ secrets.RENDER_PG_USER }}" - }, + }, { "key": "TOOLJET_DB_PASS", "value": "${{ secrets.RENDER_PG_PASS }}" @@ -87,7 +87,7 @@ jobs: }, { "key": "PGRST_DB_URI", - "value": "postgres://${{ secrets.RENDER_PG_USER }}:${{ secrets.RENDER_PG_PASS }}@${{ secrets.RENDER_PG_HOST }}/${{ env.PR_NUMBER }}" + "value": "postgres://${{ secrets.RENDER_PG_USER }}:${{ secrets.RENDER_PG_PASS }}@${{ secrets.RENDER_PG_HOST }}/${{ env.PR_NUMBER }}-tjdb" }, { "key": "PGRST_HOST", diff --git a/.github/workflows/tooljet-release-docker-image-build.yml b/.github/workflows/tooljet-release-docker-image-build.yml index 39bbf3c9a9..131d33c619 100644 --- a/.github/workflows/tooljet-release-docker-image-build.yml +++ b/.github/workflows/tooljet-release-docker-image-build.yml @@ -21,19 +21,19 @@ jobs: steps: - name: Checkout code to main - if: contains(github.event.release.tag_name, '-ce-beta') + if: "!contains(github.event.release.tag_name, 'ce-lts')" uses: actions/checkout@v2 with: ref: refs/heads/main - name: Checkout code to LTS-2.50 - if: contains(github.event.release.tag_name, '2.50') + if: "contains(github.event.release.tag_name, '2.50')" uses: actions/checkout@v2 with: ref: refs/heads/lts-2.50 - name: Checkout code to LTS-3.0 - if: contains(github.event.release.tag_name, '3.0') + if: "contains(github.event.release.tag_name, '-ce-lts')" uses: actions/checkout@v2 with: ref: refs/heads/lts-3.0 @@ -60,20 +60,20 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push Docker image for beta tag - if: "contains(github.event.release.tag_name, '-ce-beta')" + if: "!contains(github.event.release.tag_name, '-ce-lts')" uses: docker/build-push-action@v4 with: context: . file: docker/production.Dockerfile push: true - tags: tooljet/tooljet-ce:${{ github.event.release.tag_name }},tooljet/tooljet-ce:ce-beta-latest + tags: tooljet/tooljet-ce:${{ github.event.release.tag_name }},tooljet/tooljet-ce:ce-latest platforms: linux/amd64 env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push Docker image for LTS 2.50 tag - if: contains(github.event.release.tag_name, '-ce-lts') + if: "contains(github.event.release.tag_name, '2.50')" uses: docker/build-push-action@v4 with: context: . @@ -86,7 +86,7 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push Docker image for LTS 3.0 tag - if: contains(github.event.release.tag_name, '3.0') + if: "contains(github.event.release.tag_name, '-ce-lts')" uses: docker/build-push-action@v4 with: context: . diff --git a/.version b/.version index fe96cad5cf..2ae831adb3 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.0.0-ce +3.0.4-ce-lts diff --git a/frontend/.version b/frontend/.version index fe96cad5cf..2ae831adb3 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -3.0.0-ce +3.0.4-ce-lts diff --git a/frontend/assets/images/icons/info-icon.svg b/frontend/assets/images/icons/info-icon.svg new file mode 100644 index 0000000000..66528cbddb --- /dev/null +++ b/frontend/assets/images/icons/info-icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx index 63470acd85..50b1d3b296 100644 --- a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx +++ b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx @@ -2,12 +2,11 @@ import React, { useState, useEffect, useRef } from 'react'; import { Container } from './Container'; import Grid from './Grid'; import { EditorSelecto } from './Selecto'; -import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext'; import { HotkeyProvider } from './HotkeyProvider'; import './appCanvas.scss'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; -import { getCanvasWidth } from './appCanvasUtils'; +import { getCanvasWidth, computeViewerBackgroundColor } from './appCanvasUtils'; import { NO_OF_GRIDS } from './appCanvasConstants'; import cx from 'classnames'; import FreezeVersionInfo from '@/AppBuilder/Header/FreezeVersionInfo'; @@ -38,8 +37,9 @@ export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => { const setIsComponentLayoutReady = useStore((state) => state.setIsComponentLayoutReady, shallow); const canvasMaxWidth = useAppCanvasMaxWidth({ mode: currentMode }); const editorMarginLeft = useSidebarMargin(canvasContainerRef); - const pageSwitchInProgress = useStore((state) => state.pageSwitchInProgress); - const setPageSwitchInProgress = useStore((state) => state.setPageSwitchInProgress); + const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow); + const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow); + useEffect(() => { // Need to remove this if we shift setExposedVariable Logic outside of components // Currently present to run onLoadQueries after the component is mounted @@ -60,21 +60,27 @@ export const AppCanvas = ({ moduleId, appId, isViewerSidebarPinned }) => { return (
+ {creationMode === 'GIT' && } + {creationMode !== 'GIT' && }
{ }} className={`app-${appId}`} > - - {creationMode === 'GIT' && } - {creationMode !== 'GIT' && } + {currentMode === 'edit' && ( + + )} {environmentLoadingState !== 'loading' && ( diff --git a/frontend/src/AppBuilder/AppCanvas/AutoComputeMobileLayoutAlert.jsx b/frontend/src/AppBuilder/AppCanvas/AutoComputeMobileLayoutAlert.jsx index a36209e525..1124c326ad 100644 --- a/frontend/src/AppBuilder/AppCanvas/AutoComputeMobileLayoutAlert.jsx +++ b/frontend/src/AppBuilder/AppCanvas/AutoComputeMobileLayoutAlert.jsx @@ -77,6 +77,7 @@ export default function AutoComputeMobileLayoutAlert({ currentLayout, darkMode } padding: 'var(--7, 16px)', background: 'var(--base)', margin: '10px', + zIndex: '1', }} className="d-flex flex-row" > diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index 8b6e27f4e6..d2f6625488 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -5,7 +5,7 @@ import WidgetWrapper from './WidgetWrapper'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { useDrop } from 'react-dnd'; -import { addChildrenWidgetsToParent, addNewWidgetToTheEditor } from './appCanvasUtils'; +import { addChildrenWidgetsToParent, addNewWidgetToTheEditor, computeViewerBackgroundColor } from './appCanvasUtils'; import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants'; import { useGridStore } from '@/_stores/gridStore'; import NoComponentCanvasContainer from './NoComponentCanvasContainer'; @@ -40,7 +40,6 @@ export const Container = React.memo( const components = useStore((state) => state.getContainerChildrenMapping(id), shallow); const componentType = useStore((state) => state.getComponentTypeFromId(id), shallow); const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow); - const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow); const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow); const canvasBgColor = useStore( (state) => (id === 'canvas' ? state.getCanvasBackgroundColor('canvas', darkMode) : ''), @@ -124,7 +123,12 @@ export const Container = React.memo( height: id === 'canvas' ? `${canvasHeight}` : '100%', // backgroundSize: '25.3953px 10px', backgroundSize: `${gridWidth}px 10px`, - backgroundColor: id === 'canvas' ? canvasBgColor : '#f0f0f0', + backgroundColor: + currentMode === 'view' + ? computeViewerBackgroundColor(darkMode, canvasBgColor) + : id === 'canvas' + ? canvasBgColor + : '#f0f0f0', width: getCanvasWidth(), maxWidth: (() => { // For Main Canvas diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index 985b62eb89..a8a10caf55 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -44,7 +44,8 @@ export default function Grid({ gridWidth, currentLayout }) { const isGroupHandleHoverd = useIsGroupHandleHoverd(); const openModalWidgetId = useOpenModalWidgetId(); const moveableRef = useRef(null); - const [triggerCanvasUpdater, setTriggerCanvasUpdater] = useState(false); + const triggerCanvasUpdater = useStore((state) => state.triggerCanvasUpdater, shallow); + const toggleCanvasUpdater = useStore((state) => state.toggleCanvasUpdater, shallow); const groupResizeDataRef = useRef([]); const isDraggingRef = useRef(false); const canvasWidth = NO_OF_GRIDS * gridWidth; @@ -347,7 +348,7 @@ export default function Grid({ gridWidth, currentLayout }) { return layouts; }, {}); setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: true }); - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }, // eslint-disable-next-line react-hooks/exhaustive-deps [boxList, currentLayout, gridWidth] @@ -488,7 +489,7 @@ export default function Grid({ gridWidth, currentLayout }) { console.error('ResizeEnd error ->', error); } useGridStore.getState().actions.setDragTarget(); - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }} onResizeStart={(e) => { if (!isComponentVisible(e.target.id)) { @@ -575,7 +576,7 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('Error resizing group', error); } - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }} checkInput onDragStart={(e) => { @@ -595,7 +596,10 @@ export default function Grid({ gridWidth, currentLayout }) { isDragOnTableORCalendar = tableElem.contains(e.inputEvent.target); } if (box?.component?.component === 'Calendar') { - const calenderElem = e.target.querySelector('.rbc-month-view'); + const calenderElem = + e.target.querySelector('.rbc-month-view') || + e.target.querySelector('.rbc-time-view') || + e.target.querySelector('.rbc-day-view'); isDragOnTableORCalendar = calenderElem.contains(e.inputEvent.target); } @@ -722,7 +726,7 @@ export default function Grid({ gridWidth, currentLayout }) { element.classList.add('hide-grid'); }); document.getElementById('real-canvas')?.classList.remove('show-grid'); - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }} onDrag={(e) => { // Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again @@ -857,7 +861,7 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('Error dragging group', error); } - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }} // throttleDrag={1} // edgeDraggable={false} diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index 0969a99358..de89652d18 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -80,7 +80,7 @@ const RenderWidget = ({ ...{ validationObject: unResolvedValidation }, customResolveObjects: customResolvables, }), - [validateWidget, customResolvables, unResolvedValidation] + [validateWidget, customResolvables, unResolvedValidation, resolvedValidation] ); const resetComponent = useCallback(() => { diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js index fd706e7cb1..6171473174 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js @@ -350,7 +350,14 @@ export function pasteComponents(parentId, copiedComponentObj) { const currentPageId = useStore.getState().getCurrentPageId(); const { isCut = false, newComponents: pastedComponents = [], pageId, isCloning = false } = copiedComponentObj; // Prevent pasting if the parent subcontainer was deleted during a cut operation - if (parentId && !Object.keys(components).find((key) => parentId === key)) { + if ( + parentId && + !Object.keys(components).find( + (key) => + parentId === key || + (components?.[key]?.component.component === 'Tabs' && parentId?.split('-')?.slice(0, -1)?.join('-') === key) + ) + ) { return; } if (parentId) { @@ -444,10 +451,9 @@ export const getCanvasWidth = (currentLayout) => { } }; -export const computeCanvasBackgroundColor = (isAppDarkMode, canvasBgColor) => { - const canvasBackgroundColor = canvasBgColor ? canvasBgColor : '#edeff5'; - if (['#2f3c4c', '#edeff5'].includes(canvasBackgroundColor)) { - return isAppDarkMode ? '#2f3c4c' : '#edeff5'; +export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => { + if (['#2f3c4c', '#F2F2F5', '#edeff5'].includes(canvasBgColor)) { + return isAppDarkMode ? '#2f3c4c' : '#F2F2F5'; } - return canvasBackgroundColor; + return canvasBgColor; }; diff --git a/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js b/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js index e12e14c8a9..969e60f398 100644 --- a/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js +++ b/frontend/src/AppBuilder/AppCanvas/useSidebarMargin.js @@ -6,11 +6,13 @@ import { LEFT_SIDEBAR_WIDTH } from './appCanvasConstants'; const useSidebarMargin = (canvasContainerRef) => { const [editorMarginLeft, setEditorMarginLeft] = useState(0); - const selectedSidebarItem = useStore((state) => state.selectedSidebarItem, shallow); + const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow); + const mode = useStore((state) => state.currentMode, shallow); useEffect(() => { - setEditorMarginLeft(selectedSidebarItem ? LEFT_SIDEBAR_WIDTH : 0); - }, [selectedSidebarItem]); + if (mode !== 'view') setEditorMarginLeft(isSidebarOpen ? LEFT_SIDEBAR_WIDTH : 0); + else setEditorMarginLeft(0); + }, [isSidebarOpen, mode]); useEffect(() => { if (!isEmpty(canvasContainerRef?.current)) { diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx index 09b7d84a43..76dcae3c7a 100644 --- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx @@ -30,6 +30,12 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r const [currentValue, setCurrentValue] = useState(''); const [errorStateActive, setErrorStateActive] = useState(false); const [cursorInsidePreview, setCursorInsidePreview] = useState(false); + const componentDefinition = useStore((state) => state.getComponentDefinition(componentId), shallow); + const parentId = componentDefinition?.component?.parent; + const customResolvables = useStore((state) => state.resolvedStore.modules.canvas?.customResolvables, shallow); + + const customVariables = customResolvables?.[parentId]?.[0] || {}; + const isPreviewFocused = useRef(false); const wrapperRef = useRef(null); @@ -52,8 +58,10 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r // ? resolveReferences(newInitialValue, validation, customVariables) // : [true, null]; - // Need to add customVariables while resolving the value like above - const [valid, _error] = !isEmpty(validation) ? resolveReferences(newInitialValue, validation) : [true, null]; + //!TODO use the updated new resolver + const [valid, _error] = !isEmpty(validation) + ? resolveReferences(newInitialValue, validation, customVariables) + : [true, null]; if (!valid) { setErrorStateActive(true); @@ -90,6 +98,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r style={{ width: '100%', height: restProps?.lang === 'jsx' && '320px' }} >
diff --git a/frontend/src/AppBuilder/Header/AppVersionsManager.jsx b/frontend/src/AppBuilder/Header/AppVersionsManager.jsx index 52752794d5..c4dba6d3da 100644 --- a/frontend/src/AppBuilder/Header/AppVersionsManager.jsx +++ b/frontend/src/AppBuilder/Header/AppVersionsManager.jsx @@ -125,10 +125,7 @@ export const AppVersionsManager = function ({ darkMode }) { deleteVersionAction( appId, versionId, - (newVersionDef) => { - if (newVersionDef) { - setCurrentVersionId(newVersionDef.id); - } + () => { toast.dismiss(deleteingToastId); toast.success(`Version - ${decodeEntities(versionName)} Deleted`); resetDeleteModal(); diff --git a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx index cb3fc653c2..e7284767d1 100644 --- a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx +++ b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx @@ -201,8 +201,9 @@ export const CreateVersion = ({ showCreateAppVersion, setShowCreateAppVersion }) width: '100%', }} > + {/* EE - change to development */}
- The new version will be created in development environment + The new version will be created in production environment
diff --git a/frontend/src/AppBuilder/Header/EditorHeader.jsx b/frontend/src/AppBuilder/Header/EditorHeader.jsx index d5ac4b767d..d346ff0826 100644 --- a/frontend/src/AppBuilder/Header/EditorHeader.jsx +++ b/frontend/src/AppBuilder/Header/EditorHeader.jsx @@ -18,13 +18,6 @@ export const EditorHeader = ({ darkMode }) => { isSaving: state.app.isSaving, saveError: state.app.saveError, isVersionReleased: state.isVersionReleased, - user: state.user, - app: state.app, - appId: state.app.appId, - editingVersion: state.editingVersion, - updateReleasedVersionId: state.updateReleasedVersionId, - updateEditingVersion: state.updateEditingVersion, - featureAccess: state.featureAccess, }), shallow ); diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx index 8c10f89a23..44f34d8446 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx @@ -62,12 +62,13 @@ export const LeftSidebar = ({ darkMode = false, switchDarkMode }) => { }; useEffect(() => { - setPopoverContentHeight(((window.innerHeight - queryPanelHeight - 45) / window.innerHeight) * 100); - // eslint-disable-next-line react-hooks/exhaustive-deps + setPopoverContentHeight( + ((window.innerHeight - (queryPanelHeight == 0 ? 40 : queryPanelHeight) - 45) / window.innerHeight) * 100 + ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [queryPanelHeight]); const renderPopoverContent = () => { - if (selectedSidebarItem === null) return null; + if (selectedSidebarItem === null || !isSidebarOpen) return null; switch (selectedSidebarItem) { case 'page': return ( diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx index 6b816e0ecf..6bc5bbd974 100644 --- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebarInspector/LeftSidebarInspector.jsx @@ -85,7 +85,7 @@ const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => { return jsontreeData; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables]); + }, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]); return (
{ @@ -57,7 +56,7 @@ export const PageHandlerMenu = ({ darkMode }) => { return ( { e.preventDefault(); e.stopPropagation(); - closeMenu(); callback(id); + closeMenu(); }; return ( diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx index 36e022e4dd..528a2262dd 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx @@ -47,8 +47,9 @@ export const PageMenuItem = withRouter( const isEditingPage = editingPage?.id === page?.id; const icon = () => { + const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon; if (!isDisabled && !isHidden) { - return ; + return ; } if (isDisabled || (isDisabled && isHidden)) { return ( @@ -136,8 +137,21 @@ export const PageMenuItem = withRouter( setCurrentPageHandle(page.handle); }, [currentPageId, page.id, page.handle, switchPage, setCurrentPageHandle]); + const handlePageMenuSettings = useCallback( + (event) => { + event.stopPropagation(); + openPageEditPopover(page, popoverRef); + }, + [popoverRef.current, page] + ); + return ( -
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > {editingPageName && editingPage?.id === page?.id ? ( <>
{icon()} -
- {page.name} -
+ + {page.name} + { - event.stopPropagation(); - openPageEditPopover(page, popoverRef); - }} + onClick={handlePageMenuSettings} > @@ -200,15 +211,6 @@ export const PageMenuItem = withRouter(
)} -
); }) diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx index c4c820ff69..c891ba4c95 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx @@ -19,6 +19,7 @@ import { DATA_SOURCE_TYPE } from '@/_helpers/constants'; import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers'; import useStore from '@/AppBuilder/_stores/store'; import { EventManager } from '@/AppBuilder/RightSideBar/Inspector/EventManager'; +import NotificationBanner from '@/_components/NotificationBanner'; export const QueryManagerBody = ({ darkMode, options, setOptions, activeTab }) => { const { t } = useTranslation(); @@ -30,6 +31,7 @@ export const QueryManagerBody = ({ darkMode, options, setOptions, activeTab }) = const selectedDataSource = useStore((state) => state.queryPanel.selectedDataSource); const changeDataQuery = useStore((state) => state.dataQuery.changeDataQuery); const updateDataQuery = useStore((state) => state.dataQuery.updateDataQuery); + const [showLocalDataSourceDeprecationBanner, setshowLocalDataSourceDeprecationBanner] = useState(false); const [dataSourceMeta, setDataSourceMeta] = useState(null); /* - Added the below line to cause re-rendering when the query is switched @@ -280,7 +282,7 @@ export const QueryManagerBody = ({ darkMode, options, setOptions, activeTab }) = return ( <>
- {selectedQuery && ( + {selectedQuery && !showLocalDataSourceDeprecationBanner && ( ); }; + useEffect(() => { + const staticDataSources = ['runjs', 'runpy', 'tooljetdb']; + // added specific check for rest api - as it is a part of both : default and global data sources + const showDeprecationBanner = + selectedDataSource == null && + selectedQuery && + !staticDataSources.includes(selectedDataSource?.kind) && + (selectedDataSource?.kind !== 'restapi' || selectedDataSource?.type !== 'default'); + if (showDeprecationBanner) { + setshowLocalDataSourceDeprecationBanner(true); + } else { + setshowLocalDataSourceDeprecationBanner(false); + } + }, [selectedDataSource, selectedQuery]); + + // if (selectedQueryId !== selectedQuery?.id) return; const hasPermissions = selectedDataSource?.scope === 'global' && selectedDataSource?.type !== DATA_SOURCE_TYPE.SAMPLE ? canUpdateDataSource(selectedQuery?.data_source_id) || canReadDataSource(selectedQuery?.data_source_id) || canDeleteDataSource() : true; - return (
{selectedDataSource === null || !selectedQuery ? ( - renderDataSourcesList() + showLocalDataSourceDeprecationBanner ? ( + <> + + {renderChangeDataSource()} + + ) : ( + renderDataSourcesList() + ) ) : ( <> {selectedQuery?.data_source_id && activeTab === 1 && renderChangeDataSource()} diff --git a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx index 6469601b81..9ac052ae51 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx @@ -21,20 +21,22 @@ export const QueryDataPane = ({ darkMode }) => { const { t } = useTranslation(); const loadingDataQueries = useStore((state) => state.queryPanel.loadingDataQueries); + const setQueryPanelSearchTerm = useStore((state) => state.queryPanel.setQueryPanelSearchTerm); + const storedSearchTerm = useStore((state) => state.queryPanel.queryPanelSearchTem); const dataQueries = useStore((state) => state.dataQuery.queries.modules.canvas); const dataSources = useStore((state) => state.dataSources); const [filteredQueries, setFilteredQueries] = useState(dataQueries); - const [showSearchBox, setShowSearchBox] = useState(false); + const [showSearchBox, setShowSearchBox] = useState(!!storedSearchTerm); const searchBoxRef = useRef(null); const [dataSourcesForFilters, setDataSourcesForFilters] = useState([]); - const [searchTermForFilters, setSearchTermForFilters] = useState(); - + const [searchTermForFilters, setSearchTermForFilters] = useState(storedSearchTerm ?? ''); function isDataSourceLocal(dataQuery) { return dataSources.some((dataSource) => dataSource.id === dataQuery.data_source_id); } useEffect(() => { + setQueryPanelSearchTerm(searchTermForFilters); // Create a copy of the dataQueries array to perform filtering without modifying the original data. let filteredDataQueries = [...dataQueries]; @@ -84,7 +86,7 @@ export const QueryDataPane = ({ darkMode }) => { }; useEffect(() => { - showSearchBox && searchBoxRef.current.focus(); + showSearchBox && !storedSearchTerm && searchBoxRef.current.focus(); }, [showSearchBox]); return ( @@ -135,6 +137,7 @@ export const QueryDataPane = ({ darkMode }) => { placeholder={t('globals.search', 'Search')} customClass="query-manager-search-box-wrapper flex-grow-1" showClearButton + clearTextOnBlur={false} /> { const { showDarkModeToggle, isReleasedVersionId } = useStore( (state) => ({ - isReleasedVersionId: state?.releasedVersionId == state.selectedVersion?.id || state.isVersionReleased, + isReleasedVersionId: state?.releasedVersionId == state.currentVersionId || state.isVersionReleased, showDarkModeToggle: state.globalSettings.appMode === 'auto' || !state.globalSettings.appMode, }), shallow diff --git a/frontend/src/AppBuilder/Viewer/MobileHeader.jsx b/frontend/src/AppBuilder/Viewer/MobileHeader.jsx index 623e755d2c..61284bb821 100644 --- a/frontend/src/AppBuilder/Viewer/MobileHeader.jsx +++ b/frontend/src/AppBuilder/Viewer/MobileHeader.jsx @@ -22,7 +22,12 @@ const MobileHeader = ({ setAppDefinitionFromVersion, showViewerNavigation, }) => { - const isVersionReleased = useStore((state) => state.isVersionReleased); + const { isReleasedVersionId } = useStore( + (state) => ({ + isReleasedVersionId: state?.releasedVersionId == state.currentVersionId || state.isVersionReleased, + }), + shallow + ); const editingVersion = useStore((state) => state.editingVersion); const showDarkModeToggle = useStore((state) => state.globalSettings.appMode === 'auto'); @@ -33,7 +38,7 @@ const MobileHeader = ({ const _renderAppNameAndLogo = () => (

); - const _renderPreviewSettings = () => ( - - ); + const _renderPreviewSettings = () => + !isReleasedVersionId && ( + + ); const _renderDarkModeBtn = (args) => { if (!showDarkModeToggle) return null; @@ -88,11 +94,11 @@ const MobileHeader = ({ ); }; - if (!showHeader) { + if (!showHeader && isReleasedVersionId) { return <>{showViewerNavigation ? _renderMobileNavigationMenu() : _renderDarkModeBtn()}; } - if (!showHeader && !isVersionReleased) { + if (!showHeader && !isReleasedVersionId) { return ( <>
{_renderAppNameAndLogo()} {_renderMobileNavigationMenu()}

- {!isVersionReleased && !isEmpty(editingVersion) && _renderPreviewSettings()} + {!isReleasedVersionId && !isEmpty(editingVersion) && _renderPreviewSettings()} {!showViewerNavigation && _renderDarkModeBtn({ styles: { top: '2px' } })} ); diff --git a/frontend/src/AppBuilder/Viewer/Viewer.jsx b/frontend/src/AppBuilder/Viewer/Viewer.jsx index 18d01867ef..a0135a8ea0 100644 --- a/frontend/src/AppBuilder/Viewer/Viewer.jsx +++ b/frontend/src/AppBuilder/Viewer/Viewer.jsx @@ -13,8 +13,6 @@ import DesktopHeader from './DesktopHeader'; import MobileHeader from './MobileHeader'; import ViewerSidebarNavigation from './ViewerSidebarNavigation'; import { shallow } from 'zustand/shallow'; -import { computeCanvasBackgroundColor } from '@/AppBuilder/AppCanvas/appCanvasUtils'; -import { resolveReferences } from '@/_helpers/utils'; import Popups from '../Popups'; import TooljetBanner from './TooljetBanner'; import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext'; @@ -41,6 +39,7 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod homePageId, isMaintenanceOn, setIsViewer, + toggleCurrentLayout, } = useStore( (state) => ({ isEditorLoading: state.isEditorLoading, @@ -60,14 +59,15 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod updateCanvasHeight: state.updateCanvasBottomHeight, isMaintenanceOn: state.app.isMaintenanceOn, setIsViewer: state.setIsViewer, + toggleCurrentLayout: state.toggleCurrentLayout, }), shallow ); - const getCurrentPageComponents = useStore((state) => state.getCurrentPageComponents, shallow); - const currentPageComponents = useMemo(() => getCurrentPageComponents(), [getCurrentPageComponents]); - const changeDarkMode = useStore((state) => state.changeDarkMode); + const getCurrentPageComponents = useStore((state) => state.getCurrentPageComponents(), shallow); + const currentPageComponents = useMemo(() => getCurrentPageComponents, [getCurrentPageComponents]); const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow); const canvasBgColor = useStore((state) => state.getCanvasBackgroundColor('canvas', darkMode), shallow); + const deviceWindowWidth = window.screen.width - 5; const computeCanvasMaxWidth = useCallback(() => { if (globalSettings?.maxCanvasWidth) { @@ -92,7 +92,6 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod const isLoading = false; const isMobilePreviewMode = selectedVersion?.id && currentLayout === 'mobile'; const isAppLoaded = !!editingVersion; - const deviceWindowWidth = window.screen.width - 5; const isMobileDevice = deviceWindowWidth < 600; const switchPage = useStore((state) => state.switchPage); @@ -106,6 +105,8 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod switchDarkMode(newMode); }; useEffect(() => { + const isMobileDevice = deviceWindowWidth < 600; + toggleCurrentLayout(isMobileDevice ? 'mobile' : 'desktop'); setIsViewer(true); return () => { setIsViewer(false); @@ -170,7 +171,7 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod }} >
- {!isPagesSidebarHidden && ( + {currentLayout !== 'mobile' && !isPagesSidebarHidden && ( {currentLayout === 'mobile' && isMobilePreviewMode && ( diff --git a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx index fe1bbe4c31..923e30a8ce 100644 --- a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx +++ b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx @@ -7,6 +7,7 @@ import FolderList from '@/_ui/FolderList/FolderList'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import useStore from '@/AppBuilder/_stores/store'; import { APP_HEADER_HEIGHT } from '../AppCanvas/appCanvasConstants'; +import OverflowTooltip from '@/_components/OverflowTooltip'; export const ViewerSidebarNavigation = ({ isMobileDevice, @@ -21,6 +22,7 @@ export const ViewerSidebarNavigation = ({ 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); if (isMobileDevice) { return null; @@ -127,8 +129,10 @@ export const ViewerSidebarNavigation = ({ >
{pages.map((page) => { + const isHomePage = page.id === homePageId; + const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon; // eslint-disable-next-line import/namespace - const IconElement = Icons?.[page.icon] ?? Icons?.['IconHome2']; + const IconElement = Icons?.[iconName] ?? Icons?.['IconFileDescription']; return page.hidden || page.disabled ? null : ( {!labelStyle?.label?.hidden && ( - - {_.truncate(page?.name, { length: 18 })} + + + {page.name} + )} diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js index 65b7e77807..c0fa889dd5 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js @@ -146,8 +146,8 @@ export const buttonGroupConfig = { visibility: { value: '{{true}}' }, borderRadius: { value: '{{4}}' }, disabledState: { value: '{{false}}' }, - selectedTextColor: { value: '' }, - selectedBackgroundColor: { value: '' }, + selectedTextColor: { value: '#FFFFFF' }, + selectedBackgroundColor: { value: '#4368E3' }, }, }, }; diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/codeEditor.js b/frontend/src/AppBuilder/WidgetManager/widgets/codeEditor.js index d7ff03d9a8..eb76891af0 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/codeEditor.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/codeEditor.js @@ -67,6 +67,13 @@ export const codeEditorConfig = { exposedVariables: { value: '', }, + actions: [ + { + handle: 'setValue', + displayName: 'Set value', + params: [{ handle: 'setValue', defaultValue: '' }], + }, + ], definition: { others: { showOnDesktop: { value: '{{true}}' }, diff --git a/frontend/src/AppBuilder/Widgets/Calendar/Calendar.jsx b/frontend/src/AppBuilder/Widgets/Calendar/Calendar.jsx index 080a51c47c..b7957d9ba0 100644 --- a/frontend/src/AppBuilder/Widgets/Calendar/Calendar.jsx +++ b/frontend/src/AppBuilder/Widgets/Calendar/Calendar.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Calendar as ReactCalendar, momentLocalizer } from 'react-big-calendar'; import moment from 'moment'; import 'react-big-calendar/lib/css/react-big-calendar.css'; @@ -54,7 +54,7 @@ export const Calendar = function ({ const [currentDate, setCurrentDate] = useState(defaultDate); const [eventPopoverOptions, setEventPopoverOptions] = useState({ show: false }); - const [defaultView, setDefaultValue] = useState(allowedCalendarViews[0]); + const isInitialRender = useRef(true); const eventPropGetter = (event) => { const backgroundColor = event.color; @@ -100,10 +100,10 @@ export const Calendar = function ({ const view = allowedCalendarViews.includes(properties.defaultView) ? properties.defaultView : allowedCalendarViews[0]; - if (currentView !== view) { - setDefaultValue(view); + if (currentView !== view || isInitialRender.current) { setExposedVariable('currentView', view); setCurrentView(view); + isInitialRender.current = false; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [properties.defaultView]); @@ -145,10 +145,9 @@ export const Calendar = function ({ endAccessor="end" style={style} views={allowedCalendarViews} - defaultView={defaultView} - view={defaultView} + defaultView={properties.defaultView || allowedCalendarViews[0]} + view={currentView} onView={(view) => { - setDefaultValue(view); setExposedVariable('currentView', view); setCurrentView(view); fireEvent('onCalendarViewChange'); diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 3eedc24f51..eeec8760fc 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -19,7 +19,6 @@ export const Form = function Form(props) { component, width, height, - removeComponent, styles, setExposedVariable, setExposedVariables, @@ -28,11 +27,6 @@ export const Form = function Form(props) { properties, resetComponent = () => {}, dataCy, - paramUpdated, - currentLayout, - mode, - getContainerProps, - containerProps, } = props; const childComponents = useStore((state) => state.getChildComponents(id), shallow); const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles; @@ -307,12 +301,16 @@ export const Form = function Form(props) { } key={index} > - +
+ +
{/* { if (uiComponentsDraft?.length > 0 && uiComponentsDraft[index * 2 + 1]) { @@ -148,7 +149,7 @@ export function generateUIComponents(JSONSchema, advanced, componentName = '') { uiComponentsDraft[index * 2 + 1]['definition']['properties']['display_values'] = value?.displayValues; if (value?.label) uiComponentsDraft[index * 2 + 1]['definition']['properties']['label'] = value?.label; if (value?.value) uiComponentsDraft[index * 2 + 1]['definition']['properties']['value'] = value?.value; - if (value?.values) uiComponentsDraft[index * 2 + 1]['definition']['properties']['value'] = value?.values; + if (value?.values) uiComponentsDraft[index * 2 + 1]['definition']['properties']['values'] = value?.values; if (value?.loading) uiComponentsDraft[index * 2 + 1]['definition']['properties']['loadingState'] = value?.loading; break; @@ -362,7 +363,7 @@ export function generateUIComponents(JSONSchema, advanced, componentName = '') { uiComponentsDraft[index * 2 + 1]['definition']['properties']['display_values'] = value?.displayValues; if (value?.label) uiComponentsDraft[index * 2 + 1]['definition']['properties']['label'] = value?.label; if (value?.value) uiComponentsDraft[index * 2 + 1]['definition']['properties']['value'] = value?.value; - if (value?.values) uiComponentsDraft[index * 2 + 1]['definition']['properties']['value'] = value?.values; + if (value?.values) uiComponentsDraft[index * 2 + 1]['definition']['properties']['values'] = value?.values; if (value?.showAllOption) uiComponentsDraft[index * 2 + 1]['definition']['properties']['showAllOption'] = value?.showAllOption; break; diff --git a/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx b/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx index 5fe4eb09ad..b5bfa9e4c3 100644 --- a/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx @@ -3,7 +3,7 @@ import { getComponentToRender } from '@/AppBuilder/_helpers/editorHelpers'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; -const RenderSchema = ({ component, id, onOptionChange, onOptionsChange }) => { +const RenderSchema = ({ component, parent, id, onOptionChange, onOptionsChange, darkMode }) => { const ComponentToRender = useMemo(() => getComponentToRender(component?.component), [component?.component]); const validateWidget = useStore((state) => state.validateWidget, shallow); @@ -21,13 +21,21 @@ const RenderSchema = ({ component, id, onOptionChange, onOptionsChange }) => { [id, onOptionsChange] ); - const validate = (value) => { - return validateWidget({ - ...{ widgetValue: value }, - ...{ validationObject: component.definition.validation }, - }); - }; + const validate = useCallback( + (value) => { + return validateWidget({ + ...{ widgetValue: value }, + ...{ validationObject: component.definition.validation }, + }); + }, + [component.definition.validation] + ); + const fireEvent = useCallback(() => { + return Promise.resolve(); + }, []); + + const formId = `${parent}-${id}`; return ( { setExposedVariable={setExposedVariable} setExposedVariables={setExposedVariables} validate={validate} - fireEvent={() => {}} + darkMode={darkMode} + fireEvent={fireEvent} + formId={formId} + id={id} /> ); }; diff --git a/frontend/src/AppBuilder/Widgets/Modal.jsx b/frontend/src/AppBuilder/Widgets/Modal.jsx index 7632770d91..a88aff630f 100644 --- a/frontend/src/AppBuilder/Widgets/Modal.jsx +++ b/frontend/src/AppBuilder/Widgets/Modal.jsx @@ -72,34 +72,22 @@ export const Modal = function Modal({ setShowModal(true); }, close: async function () { - setShowModal(false); setExposedVariable('show', false); + setShowModal(false); }, }; setExposedVariables(exposedVariables); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - if (isInitialRender.current) { - isInitialRender.current = false; - return; - } - - fireEvent(!showModal ? 'onClose' : 'onOpen'); - const inputRef = document?.getElementsByClassName('tj-text-input-widget')?.[0]; - inputRef?.blur(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [showModal]); - function hideModal() { - setShowModal(false); setExposedVariable('show', false); + setShowModal(false); } function openModal() { - setShowModal(true); setExposedVariable('show', true); + setShowModal(true); } useEffect(() => { @@ -149,6 +137,19 @@ export const Modal = function Modal({ }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [showModal, modalHeight]); + + useEffect(() => { + if (isInitialRender.current) { + isInitialRender.current = false; + return; + } + + fireEvent(!showModal ? 'onClose' : 'onOpen'); + const inputRef = document?.getElementsByClassName('tj-text-input-widget')?.[0]; + inputRef?.blur(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showModal]); + const backwardCompatibilityCheck = height == '34' || modalHeight != undefined ? true : false; const customStyles = { diff --git a/frontend/src/AppBuilder/Widgets/Table/Components/TableRow.jsx b/frontend/src/AppBuilder/Widgets/Table/Components/TableRow.jsx index 8db79b1b5d..221e6b4594 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Components/TableRow.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Components/TableRow.jsx @@ -85,16 +85,20 @@ export const TableRow = React.memo( : '' }`} {...rowProps} - onClickCapture={async () => { + onClickCapture={() => { // toggleRowSelected will triggered useRededcuer function in useTable and in result will get the selectedFlatRows consisting row which are selected + const selectedRow = row.original; + const selectedRowId = row.id; + setExposedVariables({ selectedRow, selectedRowId }); + fireEvent('onRowClicked'); + }} + onClick={async () => { if (allowSelection) { await toggleRowSelected(row.id); } const selectedRow = row.original; const selectedRowId = row.id; - setExposedVariables({ selectedRow, selectedRowId }); mergeToTableDetails({ selectedRow, selectedRowId }); - fireEvent('onRowClicked'); }} onMouseOver={() => { if (hoverAdded) { diff --git a/frontend/src/AppBuilder/Widgets/Table/Pagination.jsx b/frontend/src/AppBuilder/Widgets/Table/Pagination.jsx index 3333e540cd..e53096dff6 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Pagination.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Pagination.jsx @@ -67,7 +67,7 @@ export const Pagination = function Pagination({ {!serverSide && tableWidth > 460 && ( 460 && ( fireEvent('onCellValueChanged'), 0); + return; } const copyOfTableDetails = useRef(tableDetails); @@ -565,12 +567,12 @@ export const Table = React.memo( useEffect(() => { if ( - tableData.length != 0 && + properties.data.length != 0 && properties.autogenerateColumns && (useDynamicColumn || mode === 'edit' || mode === 'view') ) { const generatedColumnFromData = autogenerateColumns( - tableData, + properties.data, properties.columns, properties?.columnDeletionHistory ?? [], useDynamicColumn, @@ -579,9 +581,25 @@ export const Table = React.memo( properties.autogenerateColumns ?? false, id ); - useDynamicColumn && setGeneratedColumn(generatedColumnFromData); + if (useDynamicColumn) { + const dynamicColumnHasId = dynamicColumn && dynamicColumn.every((column) => 'id' in column); + if (!dynamicColumnHasId) { + // if dynamic columns do not have an id then we need to manually compare the generated columns with the columns in the state because the id that we generate for columns without id is a uuid and it will be different every time + const generatedColumnsWithoutIds = generatedColumnFromData.map(({ id, ...rest }) => ({ + ...rest, + })); + const columnsFromStateWithoutIds = generatedColumn.map(({ id, ...rest }) => ({ + ...rest, + })); + !isEqual(generatedColumnsWithoutIds, columnsFromStateWithoutIds) && + setGeneratedColumn(generatedColumnFromData); + return; + } + setGeneratedColumn(generatedColumnFromData); + } } - }, [tableData, JSON.stringify(dynamicColumn)]); + // }, [tableData, JSON.stringify(dynamicColumn)]); + }, [JSON.stringify(properties.data), JSON.stringify(dynamicColumn)]); const computedStyles = { // width: `${width}px`, @@ -865,7 +883,6 @@ export const Table = React.memo( currentData: data, selectedRow: [], selectedRowId: null, - pageIndex: pageIndex + 1, }); if (tableDetails.selectedRowId || !isEmpty(tableDetails.selectedRowDetails)) { toggleAllRowsSelected(false); diff --git a/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js b/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js index 235205dbb3..98faa12da0 100644 --- a/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js +++ b/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js @@ -107,7 +107,14 @@ export default function autogenerateColumns( finalKeys.includes(column?.key || column?.name) ); - setTimeout(() => setProperty(id, 'columns', finalColumns, 'properties'), 10); + setTimeout( + () => + setProperty(id, 'columns', finalColumns, 'properties', 'value', false, 'canvas', { + skipUndoRedo: true, + saveAfterAction: true, + }), + 10 + ); } const dataTypeToColumnTypeMapping = { diff --git a/frontend/src/AppBuilder/Widgets/Tabs.jsx b/frontend/src/AppBuilder/Widgets/Tabs.jsx index 5a78a07809..3a93fa698b 100644 --- a/frontend/src/AppBuilder/Widgets/Tabs.jsx +++ b/frontend/src/AppBuilder/Widgets/Tabs.jsx @@ -69,14 +69,14 @@ export const Tabs = function Tabs({ }, [parsedDefaultTab]); useEffect(() => { - const currentTabData = parsedTabs.filter((tab) => tab.id === currentTab); + const currentTabData = parsedTabs.filter((tab) => tab.id == currentTab); setBgColor(currentTabData[0]?.backgroundColor ? currentTabData[0]?.backgroundColor : darkMode ? '#324156' : '#fff'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentTab, darkMode]); function computeTabDisplay(componentId, id) { let tabVisibility = 'none'; - if (id !== currentTab) { + if (id != currentTab) { return tabVisibility; } @@ -87,14 +87,13 @@ export const Tabs = function Tabs({ } } - return id === currentTab ? 'block' : 'none'; + return id == currentTab ? 'block' : 'none'; } useEffect(() => { const exposedVariables = { setTab: async function (id) { - id = typeof id === 'number' ? String(id) : id; - if (id && currentTab !== id) { + if (currentTab != id) { setCurrentTab(id); setExposedVariable('currentTab', id); fireEvent('onTabSwitch'); @@ -134,7 +133,7 @@ export const Tabs = function Tabs({ function shouldRenderTabContent(tab) { if (parsedRenderOnlyActiveTab) { - return tab.id === currentTab; + return tab.id == currentTab; } return true; // Render by default if no specific conditions are met } @@ -162,7 +161,7 @@ export const Tabs = function Tabs({ className="nav-item" style={{ opacity: tab?.disabled && '0.5', width: tabWidth == 'split' && equalSplitWidth + '%' }} onClick={() => { - if (currentTab === tab.id) return; + if (currentTab == tab.id) return; !tab?.disabled && setCurrentTab(tab.id); !tab?.disabled && setExposedVariable('currentTab', tab.id); @@ -193,7 +192,7 @@ export const Tabs = function Tabs({
{ - if (currentTab === tab.id) { + if (currentTab == tab.id) { parentRef.current = newCurrent; } }} @@ -202,7 +201,7 @@ export const Tabs = function Tabs({ > {shouldRenderTabContent(tab) && renderTabContent(id, tab)} - {/* {tab.id === currentTab && } */} + {/* {tab.id == currentTab && } */}
))}
diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index 21c95a0144..e758835a91 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -16,7 +16,7 @@ import { usePrevious } from '@dnd-kit/utilities'; import { deepCamelCase } from '@/_helpers/appUtils'; import { useEventActions } from '../_stores/slices/eventsSlice'; import useRouter from '@/_hooks/use-router'; -import { navigate } from '../_utils/misc'; +import { extractEnvironmentConstantsFromConstantsList, navigate } from '../_utils/misc'; import { getWorkspaceId } from '@/_helpers/utils'; import { shallow } from 'zustand/shallow'; import { fetchAndSetWindowTitle, pageTitles, defaultWhiteLabellingSettings } from '@white-label/whiteLabelling'; @@ -93,7 +93,9 @@ const useAppData = (appId, moduleId, mode = 'edit', { environmentId, versionId } if (pageSwitchInProgress) { const currentPageEvents = events.filter((event) => event.target === 'page' && event.sourceId === currentPageId); setPageSwitchInProgress(false); - handleEvent('onPageLoad', currentPageEvents, {}); + setTimeout(() => { + handleEvent('onPageLoad', currentPageEvents, {}); + }, 0); } }, [pageSwitchInProgress, currentPageId]); @@ -171,10 +173,25 @@ const useAppData = (appId, moduleId, mode = 'edit', { environmentId, versionId } }; } - const constantsResp = - isPublicAccess && appData.is_public - ? await orgEnvironmentConstantService.getConstantsFromPublicApp(slug) - : await orgEnvironmentConstantService.getConstantsFromApp(); + let constantsResp; + if (mode === 'edit') { + let defaultEnvId = null; + if (editorEnvironment?.id == null) { + const envs = await appEnvironmentService.getAllEnvironments(appId); + const defaultEnv = envs.environments.find((env) => env?.is_default === true); + defaultEnvId = defaultEnv ? defaultEnv.id : null; + } + constantsResp = await orgEnvironmentConstantService.getConstantsFromEnvironment( + editorEnvironment?.id || defaultEnvId + ); + } else { + constantsResp = + isPublicAccess && appData.is_public + ? await orgEnvironmentConstantService.getConstantsFromPublicApp(slug) + : await orgEnvironmentConstantService.getConstantsFromApp(slug); + } + + constantsResp.constants = extractEnvironmentConstantsFromConstantsList(constantsResp?.constants, 'production'); const pages = appData.pages.map((page) => { return page; @@ -189,7 +206,12 @@ const useAppData = (appId, moduleId, mode = 'edit', { environmentId, versionId } appId: appData.id, slug: appData.slug, currentAppEnvironmentId: editorEnvironmentId, - isMaintenanceOn: result.is_maintenance_on, + 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, @@ -201,8 +223,7 @@ const useAppData = (appId, moduleId, mode = 'edit', { environmentId, versionId } ); setPages(pages, moduleId); - setPageSettings(deepCamelCase(appData?.editing_version?.page_settings)); - + setPageSettings(deepCamelCase(appData?.editing_version?.page_settings ?? appData?.page_settings)); // set starting page as homepage initially let startingPage = appData.pages.find((page) => page.id === homePageId); @@ -347,7 +368,12 @@ const useAppData = (appId, moduleId, mode = 'edit', { environmentId, versionId } appId: appData.id, slug: appData.slug, creationMode: appData.creationMode, - isMaintenanceOn: appData.is_maintenance_on, + isMaintenanceOn: + 'is_maintenance_on' in appData + ? appData.is_maintenance_on + : 'isMaintenanceOn' in appData + ? appData.isMaintenanceOn + : false, organizationId: appData.organizationId || appData.organization_id, homePageId: appData.editing_version.homePageId, isPublic: appData.isPublic, diff --git a/frontend/src/AppBuilder/_stores/ast.js b/frontend/src/AppBuilder/_stores/ast.js index 8da30d7845..106298588f 100644 --- a/frontend/src/AppBuilder/_stores/ast.js +++ b/frontend/src/AppBuilder/_stores/ast.js @@ -1,9 +1,38 @@ const acorn = require('acorn'); const walk = require('acorn-walk'); +function findExpression(input) { + const matches = []; + let startIdx = -1; + let braceCount = 0; + + for (let i = 0; i < input.length; i++) { + if (input[i] === '{' && input[i + 1] === '{' && braceCount === 0) { + startIdx = i; + braceCount = 2; + i++; // Skip the second '{' + } else if (input[i] === '{' && braceCount > 0) { + braceCount++; + } else if (input[i] === '}' && braceCount > 0) { + braceCount--; + if (braceCount === 0 && startIdx !== -1) { + matches.push({ + fullMatch: input.slice(startIdx, i + 1), + expression: input.slice(startIdx + 2, i - 1).trim(), + index: startIdx, + }); + startIdx = -1; + } + } + } + + return matches; +} + export function extractAndReplaceReferencesFromString(input, componentIdNameMapping = {}, queryIdNameMapping = {}) { // Quick check for relevant keywords - const regexForQuickCheck = /\b(components|queries|globals|variables|page|parameters|secrets)(?:\[\S*|\.\S*|\?\.\S*)/; + const regexForQuickCheck = + /\b(components|queries|globals|variables|page|parameters|secrets|constants)(?:\[\S*|\.\S*|\?\.\S*)/; if (!regexForQuickCheck.test(input)) { return { allRefs: [], @@ -12,7 +41,7 @@ export function extractAndReplaceReferencesFromString(input, componentIdNameMapp }; } - const relevantKeywords = /\b(components|queries|globals|variables|page|parameters|secrets)\b/; + const relevantKeywords = /\b(components|queries|globals|variables|page|parameters|secrets|constants)\b/; const expressionRegex = /{{(.*?)}}/gs; const results = []; let lastIndex = 0; @@ -24,12 +53,94 @@ export function extractAndReplaceReferencesFromString(input, componentIdNameMapp /\b(components|queries)(\??\.|\??\.?\[['"]?)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(['"]?\])?/g; let match; + if (input.startsWith('{{{') && input.endsWith('}}}')) { + const inputContent = input.slice(3, -3); + input = `{{({${inputContent}})}}`; + const matches = findExpression(input); + for (const match of matches) { + const { fullMatch, expression, index } = match; + + // Check if the expression contains relevant keywords + if (!relevantKeywords.test(expression)) { + replacedString += input.slice(lastIndex, index); + bracketNotationString += input.slice(lastIndex, index); + replacedString += fullMatch; + bracketNotationString += fullMatch; + lastIndex = index + fullMatch.length; + continue; + } + + try { + const { processedExpression, uuidMappings } = preprocessExpression( + expression, + uuidRegex, + componentIdNameMapping, + queryIdNameMapping + ); + const parsedResult = parseExpression( + processedExpression, + componentIdNameMapping, + queryIdNameMapping, + uuidMappings + ); + + replacedString += input.slice(lastIndex, index); + bracketNotationString += input.slice(lastIndex, index); + + const replacedExpression = replaceIdsInExpression( + processedExpression, + componentIdNameMapping, + queryIdNameMapping, + false, + uuidMappings + ); + const bracketNotationExpression = replaceIdsInExpression( + processedExpression, + componentIdNameMapping, + queryIdNameMapping, + true, + uuidMappings + ); + + replacedString += `{{${replacedExpression}}}`; + bracketNotationString += `{{${bracketNotationExpression}}}`; + + results.push({ + allRefs: parsedResult.references, + valueWithId: `{{${replacedExpression}}}`, + valueWithBrackets: `{{${bracketNotationExpression}}}`, + }); + } catch (error) { + replacedString += fullMatch; + bracketNotationString += fullMatch; + results.push({ + allRefs: [], + valueWithId: fullMatch, + valueWithBrackets: fullMatch, + }); + } + + lastIndex = index + fullMatch.length; + } + + replacedString += input.slice(lastIndex); + bracketNotationString += input.slice(lastIndex); + // remove the parentheses that were added + + return { + valueWithId: `{{${replacedString.slice(3, -3)}}}`, + valueWithBrackets: `{{${bracketNotationString.slice(3, -3)}}}`, + allRefs: results.flatMap((r) => r.allRefs), + }; + } while ((match = expressionRegex.exec(input)) !== null) { const fullMatch = match[0]; const expression = match[1].trim(); // Check if the expression contains relevant keywords if (!relevantKeywords.test(expression)) { + replacedString += input.slice(lastIndex, match.index); + bracketNotationString += input.slice(lastIndex, match.index); replacedString += fullMatch; bracketNotationString += fullMatch; lastIndex = match.index + fullMatch.length; diff --git a/frontend/src/AppBuilder/_stores/slices/appSlice.js b/frontend/src/AppBuilder/_stores/slices/appSlice.js index a850b96926..c9b21052e6 100644 --- a/frontend/src/AppBuilder/_stores/slices/appSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/appSlice.js @@ -6,6 +6,7 @@ import DependencyGraph from './DependencyClass'; import { getWorkspaceId } from '@/_helpers/utils'; import { navigate } from '@/AppBuilder/_utils/misc'; import queryString from 'query-string'; +import { replaceEntityReferencesWithIds } from '../utils'; const initialState = { app: {}, @@ -63,15 +64,19 @@ export const createAppSlice = (set, get) => ({ } }); }, - globalSettingsChanged: async (newOptions) => { - for (const [key, value] of Object.entries(newOptions)) { + globalSettingsChanged: async (options) => { + const componentNameIdMapping = get().modules.canvas.componentNameIdMapping; + const queryNameIdMapping = get().modules.canvas.queryNameIdMapping; + for (const [key, value] of Object.entries(options)) { if (value?.[1]?.a == undefined) { - newOptions[key] = value; + options[key] = value; } else { const hexCode = `${value?.[0]}${decimalToHex(value?.[1]?.a)}`; - newOptions[key] = hexCode; + options[key] = hexCode; } } + // Replace entity references with ids if present + const newOptions = replaceEntityReferencesWithIds(options, componentNameIdMapping, queryNameIdMapping); const { app, currentVersionId, currentPageId } = get(); try { const res = await appVersionService.autoSaveApp( diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index d5b0afa9d4..7fb9650075 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -4,12 +4,12 @@ import { resolveDynamicValues, // extractAndReplaceReferencesFromString, checkSubstringRegex, + hasArrayNotation, + parsePropertyPath, } from '@/AppBuilder/_stores/utils'; import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast'; -import { componentTypeDefinitionMap } from '@/AppBuilder/WidgetManager'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; -import { v4 as uuidv4 } from 'uuid'; -import _, { cloneDeep, merge } from 'lodash'; +import { cloneDeep, merge, set as lodashSet } from 'lodash'; import { computeComponentName, getAllChildComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils'; import { pageConfig } from '@/AppBuilder/RightSideBar/PageSettingsTab/pageConfig'; import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants'; @@ -243,7 +243,8 @@ export const createComponentsSlice = (set, get) => ({ property, value, component, - componentResolvedValues = {}, // If componentResolvedValues is null, then it is from a setComponentProperty call + componentResolvedValues = {}, + updatePassedValue = true, moduleId ) => { const { @@ -281,7 +282,7 @@ export const createComponentsSlice = (set, get) => ({ allRefs.push(customResolvablePath); } } - if (componentResolvedValues !== null) + if (updatePassedValue) setAllValueToComponent( componentDetails, valueWithBrackets, @@ -294,7 +295,7 @@ export const createComponentsSlice = (set, get) => ({ return { updatedValue: valueWithId, allRefs, unResolvedValue: valueWithBrackets, componentResolvedValues }; } else { - if (componentResolvedValues !== null) + if (updatePassedValue) setAllValueToComponent( componentDetails, value, @@ -317,7 +318,7 @@ export const createComponentsSlice = (set, get) => ({ componentResolvedValues = {}, moduleId ) => { - const { getAllExposedValues } = get(); + const { getAllExposedValues, getComponentTypeFromId } = get(); const { componentId, paramType, property } = componentDetails; const length = Object.keys(customResolvables).length; if (length === 0) { @@ -334,12 +335,31 @@ export const createComponentsSlice = (set, get) => ({ if (!componentResolvedValues[componentId][index][paramType]) { componentResolvedValues[componentId][index][paramType] = {}; } - componentResolvedValues[componentId][index][paramType][property] = resolvedValue; + if (hasArrayNotation(property)) { + const keys = parsePropertyPath(property); + lodashSet( + componentResolvedValues, + [componentId, index, paramType, ...keys], + getComponentTypeFromId(componentId) === 'Table' ? value : resolvedValue + ); + } else { + componentResolvedValues[componentId][index][paramType][property] = resolvedValue; + } } else { if (!componentResolvedValues[componentId][paramType]) { componentResolvedValues[componentId][paramType] = {}; } - componentResolvedValues[componentId][paramType][property] = resolvedValue; + + if (hasArrayNotation(property)) { + const keys = parsePropertyPath(property); + lodashSet( + componentResolvedValues, + [componentId, paramType, ...keys], + getComponentTypeFromId(componentId) === 'Table' ? value : resolvedValue + ); + } else { + componentResolvedValues[componentId][paramType][property] = resolvedValue; + } } } else { // Loop all the index and set the resolved value @@ -564,6 +584,83 @@ export const createComponentsSlice = (set, get) => ({ }; }, + // This function checks whether the property value is an array or not and then resolves the value accordingly + // Cases like Table column, Dropdown options, etc. + checkValueAndResolve: ( + componentId, + paramType, + property, + value, + component, + resolvedComponentValues, + updatePassedValue = true, + moduleId + ) => { + const { updateResolvedValues, generateDependencyGraphForRefs } = get(); + const updatedPropertyValue = cloneDeep(value); + if (Array.isArray(value)) { + value.forEach((val, index) => { + //This code assumes that the array always consists of objects the else condition is to handle the case when the value is an array of strings/numbers + if (val && typeof val === 'object') { + Object.entries(val).forEach(([key, keyValue]) => { + const propertyWithArrayValue = `${property}[${index}].${key}`; + const keys = [key]; + if (keyValue?.value) { + keys.push('value'); + } + const { allRefs, unResolvedValue, updatedValue } = updateResolvedValues( + componentId, + paramType, + propertyWithArrayValue, + keyValue?.value ?? keyValue, + component, + resolvedComponentValues, + updatePassedValue, + moduleId + ); + lodashSet(updatedPropertyValue, [index, ...keys], updatedValue); + if (allRefs.length) { + generateDependencyGraphForRefs(allRefs, componentId, paramType, propertyWithArrayValue, unResolvedValue); + } + }); + } else { + const propertyWithArrayValue = `${property}[${index}]`; + const { allRefs, unResolvedValue, updatedValue } = updateResolvedValues( + componentId, + paramType, + propertyWithArrayValue, + val, + component, + resolvedComponentValues, + updatePassedValue, + moduleId + ); + updatedPropertyValue[index] = updatedValue; + console.log('updatedPropertyValue', updatedPropertyValue); + if (allRefs.length) { + generateDependencyGraphForRefs(allRefs, componentId, paramType, propertyWithArrayValue, unResolvedValue); + } + } + }); + return { updatedValue: updatedPropertyValue }; + } else { + const { allRefs, unResolvedValue, updatedValue } = updateResolvedValues( + componentId, + paramType, + property, + value, + component, + resolvedComponentValues, + updatePassedValue, + moduleId + ); + if (allRefs.length) { + generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue); + } + return { allRefs, unResolvedValue, updatedValue }; + } + }, + updateDependencyGraphAndResolvedValues: ( moduleId, componentId, @@ -572,22 +669,20 @@ export const createComponentsSlice = (set, get) => ({ resolvedComponentValues = {}, paramType ) => { - const { updateResolvedValues, generateDependencyGraphForRefs, setAllValueToComponent } = get(); + const { checkValueAndResolve, setAllValueToComponent } = get(); if (component.definition[paramType] === undefined) return; Object.entries(component.definition[paramType]).forEach(([property, value]) => { if (!value?.skipResolve) { - const { allRefs, unResolvedValue } = updateResolvedValues( + checkValueAndResolve( componentId, paramType, property, value?.value, component, resolvedComponentValues, + true, moduleId ); - if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue); - } } else { const componentDetails = { componentId, paramType, property }; setAllValueToComponent(componentDetails, value?.value, false, null, {}, resolvedComponentValues, moduleId); @@ -925,11 +1020,10 @@ export const createComponentsSlice = (set, get) => ({ saveComponentChanges, withUndoRedo, getComponentTypeFromId, - updateResolvedValues, - generateDependencyGraphForRefs, setResolvedComponent, getComponentDefinition, currentLayout, + checkValueAndResolve, } = get(); let hasParentChanged = false; let oldParentId; @@ -1003,19 +1097,16 @@ export const createComponentsSlice = (set, get) => ({ objectsToUpdate.forEach((paramType) => { if (component.definition[paramType]) { Object.entries(component.definition[paramType]).forEach(([property, value]) => { - const { allRefs, unResolvedValue } = updateResolvedValues( + checkValueAndResolve( componentId, paramType, property, value.value, component, resolvedComponentValues, + true, moduleId ); - - if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue, true); - } }); } }); @@ -1066,22 +1157,75 @@ export const createComponentsSlice = (set, get) => ({ removeDependency, getComponentDefinition, setValueToComponent, + checkValueAndResolve, + getResolvedComponent, + setResolvedComponent, } = get(); const { component } = getComponentDefinition(componentId, moduleId); + const oldValue = component.definition[paramType][property]; + + if (Array.isArray(oldValue?.value)) { + const resolvedComponent = { [componentId]: deepClone(getResolvedComponent(componentId) ?? {}) }; + resolvedComponent[componentId][paramType][property] = []; + + const { updatedValue } = checkValueAndResolve( + componentId, + paramType, + property, + value, + component, + resolvedComponent, + true, + moduleId + ); + setResolvedComponent(componentId, resolvedComponent[componentId], moduleId); + + // If the value is not changed, return + if (oldValue?.[attr] === updatedValue || oldValue === updatedValue) return; + + set( + withUndoRedo((state) => { + const pageComponent = state.modules[moduleId].pages[currentPageIndex].components[componentId].component; + lodashSet(pageComponent, ['definition', paramType, property, attr], updatedValue); + }, skipUndoRedo), + false, + 'setComponentProperty' + ); + + const oldComponent = get().modules[moduleId].pages[currentPageIndex].components[componentId].component; + const { events, exposedVariables, ...filteredDefinition } = oldComponent.definition || {}; + + const diff = { + [componentId]: { + component: { + ...oldComponent, + definition: filteredDefinition, + }, + }, + }; + + if (saveAfterAction) { + const currentMode = get().currentMode; + if (currentMode !== 'view') saveComponentChanges(diff, 'components', 'update'); + + get().multiplayer.broadcastUpdates({ componentId, property, value, paramType, attr }, 'components', 'update'); + } + return; + } + // Update the value and get new dependencies const { updatedValue, allRefs, unResolvedValue } = attr === 'value' && !skipResolve - ? updateResolvedValues(componentId, paramType, property, value, component, null, moduleId) + ? updateResolvedValues(componentId, paramType, property, value, component, null, false, moduleId) : { updatedValue: value, allRefs: [], unResolvedValue: value }; // If the value is not changed, return - const oldValue = component.definition[paramType][property]; if (oldValue?.[attr] === updatedValue || oldValue === updatedValue) return; set( withUndoRedo((state) => { const pageComponent = state.modules[moduleId].pages[currentPageIndex].components[componentId].component; - _.set(pageComponent, ['definition', paramType, property, attr], updatedValue); + lodashSet(pageComponent, ['definition', paramType, property, attr], updatedValue); }, skipUndoRedo), false, 'setComponentProperty' @@ -1100,8 +1244,16 @@ export const createComponentsSlice = (set, get) => ({ ); } + const oldComponent = get().modules[moduleId].pages[currentPageIndex].components[componentId].component; + const { events, exposedVariables, ...filteredDefinition } = oldComponent.definition || {}; + const diff = { - [componentId]: { component: get().modules[moduleId].pages[currentPageIndex].components[componentId].component }, + [componentId]: { + component: { + ...oldComponent, + definition: filteredDefinition, + }, + }, }; if (saveAfterAction) { @@ -1130,9 +1282,8 @@ export const createComponentsSlice = (set, get) => ({ const { currentPageIndex, saveComponentChanges, - updateResolvedValues, + checkValueAndResolve, getComponentDefinition, - generateDependencyGraphForRefs, getComponentTypeFromId, setResolvedComponent, withUndoRedo, @@ -1189,19 +1340,16 @@ export const createComponentsSlice = (set, get) => ({ objectsToUpdate.forEach((paramType) => { if (component.definition[paramType]) { Object.entries(component.definition[paramType]).forEach(([property, value]) => { - const { allRefs, unResolvedValue } = updateResolvedValues( + checkValueAndResolve( componentId, paramType, property, value.value, component, resolvedComponentValues, + true, moduleId ); - - if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue, true); - } }); } }); @@ -1427,6 +1575,8 @@ export const createComponentsSlice = (set, get) => ({ getNodeData, getEntityResolvedValueLength, updateChildComponentResolvedValues, + getComponentTypeFromId, + getResolvedComponent, } = get(); const dependecies = getDependencies(path, moduleId); if (dependecies?.length) { @@ -1436,7 +1586,8 @@ export const createComponentsSlice = (set, get) => ({ if (itemsLength) { updateChildComponentResolvedValues(dependency, path, itemsLength, moduleId); } else { - const [entityType, entityId, type, key] = dependency.split('.'); + const [entityType, entityId, type, ...keys] = dependency.split('.'); + const key = keys.join('.'); const unResolvedValue = getNodeData(dependency); const resolvedValue = resolveDynamicValues(unResolvedValue, getAllExposedValues(), {}, false, []); @@ -1455,13 +1606,46 @@ export const createComponentsSlice = (set, get) => ({ ? get().debugger.validateProperty(entityId, type, key, resolvedValue) : resolvedValue; - set( - (state) => { - state.resolvedStore.modules[moduleId][entityType][entityId][type][key] = validatedValue; - }, - false, - 'updateDependencyValues' - ); + // logic to handle the key like options[0].visible. It will resolve the visible directly and update the resolved store + if (hasArrayNotation(key)) { + const keys = parsePropertyPath(key); + // Triggering a re-render of the table component if any of the dependent component is updated + // This is done to calculate the callValues in the table component + // Need to find a better way to handle this + if (getComponentTypeFromId(entityId, moduleId) === 'Table') { + set( + (state) => { + lodashSet( + state.resolvedStore.modules[moduleId][entityType][entityId], + ['properties', 'shouldRender'], + (getResolvedComponent(entityId)?.['properties']?.['shouldRender'] ?? 0) + 1 + ); + }, + false, + 'updateDependencyValues' + ); + } else { + set( + (state) => { + lodashSet( + state.resolvedStore.modules[moduleId][entityType][entityId], + [type, ...keys], + getComponentTypeFromId(entityId, moduleId) === 'Table' ? unResolvedValue + ' ' : validatedValue + ); + }, + false, + 'updateDependencyValues' + ); + } + } else { + set( + (state) => { + state.resolvedStore.modules[moduleId][entityType][entityId][type][key] = validatedValue; + }, + false, + 'updateDependencyValues' + ); + } } } }); @@ -1588,7 +1772,7 @@ export const createComponentsSlice = (set, get) => ({ }; const regex = - /(components|queries)(\??\.|\??\.?\[['"]?)([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(['"]?\])?(\??\.?)([^\s|},[\])?]+)?/g; + /(components|queries)(\??\.|\??\.?\[['"]?)([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(['"]?\])?(\??\.|\[['"]?)([^\s:?[\]'"+\-&|]+)/g; return input.replace(regex, (match, category, prefix, id, suffix, optionalChaining, property) => { if (mappings[category] && mappings[category][id]) { diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index 02ee8fc29c..6817e6aa39 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -211,7 +211,7 @@ export const createEventsSlice = (set, get) => ({ state.eventsSlice.module[moduleId].events = newEvents; }); }, - setTablePageIndex: (tableId, index) => { + setTablePageIndex: (tableId, index = 1) => { const { getExposedValueOfComponent } = get(); if (_.isEmpty(tableId)) { console.log('No table is associated with this event.'); diff --git a/frontend/src/AppBuilder/_stores/slices/gridSlice.js b/frontend/src/AppBuilder/_stores/slices/gridSlice.js index 3a6e3146d5..69e5560497 100644 --- a/frontend/src/AppBuilder/_stores/slices/gridSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/gridSlice.js @@ -1,7 +1,9 @@ import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants'; +import { debounce } from 'lodash'; const initialState = { hoveredComponentForGrid: '', + triggerCanvasUpdater: false, }; export const createGridSlice = (set, get) => ({ @@ -9,8 +11,13 @@ export const createGridSlice = (set, get) => ({ setHoveredComponentForGrid: (id) => set(() => ({ hoveredComponentForGrid: id }), false, { type: 'setHoveredComponentForGrid', id }), getHoveredComponentForGrid: () => get().hoveredComponentForGrid, + toggleCanvasUpdater: () => + set((state) => ({ triggerCanvasUpdater: !state.triggerCanvasUpdater }), false, { type: 'toggleCanvasUpdater' }), + debouncedToggleCanvasUpdater: debounce(() => { + get().toggleCanvasUpdater(); + }, 200), moveComponentPosition: (direction) => { - const { setComponentLayout, currentLayout, getSelectedComponentsDefinition } = get(); + const { setComponentLayout, currentLayout, getSelectedComponentsDefinition, debouncedToggleCanvasUpdater } = get(); let layouts = {}; const selectedComponents = getSelectedComponentsDefinition(); selectedComponents.forEach((selectedComponent) => { @@ -53,5 +60,6 @@ export const createGridSlice = (set, get) => ({ }; }); setComponentLayout(layouts); + debouncedToggleCanvasUpdater(); }, }); diff --git a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js index 1670efd995..ed6703146f 100644 --- a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js @@ -1,21 +1,23 @@ -const selectedItem = localStorage.getItem('selectedSidebarItem'); -const isLeftSideBarPinned = !!selectedItem; -const selectedSidebarItem = selectedItem; +const validSidebarItems = ['page', 'inspect', 'debugger', 'settings']; +const storedIsSidebarPinned = localStorage.getItem('isLeftSideBarPinned') === 'true' ? true : false; +const storedSelectedSidebarItem = !storedIsSidebarPinned + ? null + : localStorage.getItem('selectedSidebarItem') && + validSidebarItems.includes(localStorage.getItem('selectedSidebarItem')) + ? localStorage.getItem('selectedSidebarItem') + : 'page'; const initialState = { - isSidebarOpen: isLeftSideBarPinned || !!selectedSidebarItem, - isLeftSideBarPinned, - selectedSidebarItem, + isLeftSideBarPinned: storedIsSidebarPinned, + selectedSidebarItem: storedIsSidebarPinned ? storedSelectedSidebarItem : null, + isSidebarOpen: storedIsSidebarPinned, }; -export const createLeftSideBarSlice = (set, get) => ({ +export const createLeftSideBarSlice = (set) => ({ ...initialState, setIsLeftSideBarPinned: (status) => { - status - ? localStorage.setItem('selectedSidebarItem', get().selectedSidebarItem) - : localStorage.removeItem('selectedSidebarItem'); - - set(() => ({ isLeftSideBarPinned: status })); + localStorage.setItem('isLeftSideBarPinned', status === true ? 'true' : 'false'); + set(() => ({ isLeftSideBarPinned: status }), false, 'setIsLeftSideBarPinned'); }, setSelectedSidebarItem: (selectedSidebarItem) => set(() => ({ selectedSidebarItem }), false, 'setSelectedSidebarItem'), diff --git a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js index 89b93741b7..4176985aa6 100644 --- a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js @@ -111,8 +111,8 @@ export const createPageMenuSlice = (set, get) => { set((state) => { state.editingPage = page; if (ref) { - state.showEditingPopover = true; state.popoverTargetId = ref?.current?.id; + state.showEditingPopover = true; } }), diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index 27a59740db..c989df9051 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -26,11 +26,20 @@ const initialState = { previewPanelExpanded: false, loadingDataQueries: false, isPreviewQueryLoading: false, + queryPanelSearchTem: '', }; export const createQueryPanelSlice = (set, get) => ({ queryPanel: { ...initialState, + setQueryPanelSearchTerm: (searchTerm) => + set( + (state) => { + state.queryPanel.queryPanelSearchTem = searchTerm; + }, + false, + 'setQueryPanelSearchTerm' + ), setIsDraggingQueryPane: (isDraggingQueryPane) => set( (state) => { @@ -432,7 +441,9 @@ export const createQueryPanelSlice = (set, get) => ({ isLoading: false, data: finalData, rawData, - metadata: data.metadata, + metadata: data?.metadata, + request: data?.metadata?.request, + response: data?.metadata?.response, }); resolve({ status: 'ok', data: finalData }); diff --git a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js index c83f8408ce..dc6eb9cecc 100644 --- a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js @@ -183,7 +183,7 @@ export const createResolvedSlice = (set, get) => ({ ); Object.entries(details).forEach(([key, value]) => { - if (['isLoading', 'data', 'rawData'].includes(key)) { + if (['isLoading', 'data', 'rawData', 'request', 'response', 'responseHeaders', 'metadata'].includes(key)) { if (typeof value !== 'function') get().updateDependencyValues(`queries.${queryId}.${key}`); } }); @@ -394,7 +394,22 @@ export const createResolvedSlice = (set, get) => ({ return get().resolvedStore?.modules?.[moduleId]?.components?.[componentId]; }, getExposedValueOfComponent: (componentId, moduleId = 'canvas') => { - return get().resolvedStore.modules[moduleId].exposedValues.components[componentId] || {}; + try { + const components = get().getCurrentPageComponents(); + const { + component: { parent: parentId, name: componentName }, + } = components[componentId]; + if (parentId) { + // if parent is form get exposed values from children + const { component: parentComopnent } = components?.[parentId] || {}; + if (parentComopnent?.component === 'Form') { + return get().resolvedStore.modules[moduleId].exposedValues.components[parentId].children[componentName] || {}; + } + } + return get().resolvedStore.modules[moduleId].exposedValues.components[componentId] || {}; + } catch (error) { + return {}; + } }, getAllExposedValues: (moduleId = 'canvas') => { return get().resolvedStore.modules[moduleId].exposedValues; diff --git a/frontend/src/AppBuilder/_stores/slices/undoRedoSlice.js b/frontend/src/AppBuilder/_stores/slices/undoRedoSlice.js index ec8e9bbf58..901baed1f3 100644 --- a/frontend/src/AppBuilder/_stores/slices/undoRedoSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/undoRedoSlice.js @@ -106,6 +106,7 @@ export const createUndoRedoSlice = (set, get) => { componenetPropertiesToUpdate.paramType, componenetPropertiesToUpdate.attr, undefined, + undefined, { skipUndoRedo: true } ); } diff --git a/frontend/src/AppBuilder/_stores/utils.js b/frontend/src/AppBuilder/_stores/utils.js index 20570b9b36..cc598af3a9 100644 --- a/frontend/src/AppBuilder/_stores/utils.js +++ b/frontend/src/AppBuilder/_stores/utils.js @@ -672,3 +672,34 @@ export function convertAllKeysToSnakeCase(o) { // return { suggestionList, hintsMap, resolvedRefs }; // } + +export const hasArrayNotation = (property) => { + // Regular expression to match array notation pattern + const arrayPattern = /\[\d+\]/; + return arrayPattern.test(property); +}; + +export const parsePropertyPath = (property) => { + // Split the property path into segments + const segments = property.split('.'); + const result = []; + + for (const segment of segments) { + // Check if segment contains array notation + if (hasArrayNotation(segment)) { + // Extract the property name and array index + const [name, ...rest] = segment.split('['); + if (name) result.push(name); + + // Extract and clean up array indices + for (const item of rest) { + const index = parseInt(item.replace(']', '')); + result.push(index); + } + } else { + result.push(segment); + } + } + + return result; +}; diff --git a/frontend/src/AppBuilder/_utils/misc.js b/frontend/src/AppBuilder/_utils/misc.js index 820e4c5d83..14929007f8 100644 --- a/frontend/src/AppBuilder/_utils/misc.js +++ b/frontend/src/AppBuilder/_utils/misc.js @@ -24,6 +24,26 @@ export async function copyToClipboard(text) { } } +export const extractEnvironmentConstantsFromConstantsList = (constantsList = [], environmentName = 'development') => { + try { + return constantsList.map((constant) => { + if (constant.values && Array.isArray(constant.values)) { + const { value } = constant.values.find((value) => value.environmentName === environmentName); + return { + id: constant.id, + name: constant.name, + value, + type: constant.type, + }; + } else { + return constant; + } + }); + } catch (error) { + return []; + } +}; + export function setTablePageIndex(tableId, index) { if (_.isEmpty(tableId)) { console.log('No table is associated with this event.'); diff --git a/frontend/src/Editor/Components/ButtonGroup.jsx b/frontend/src/Editor/Components/ButtonGroup.jsx index 9711820bc7..7364348271 100644 --- a/frontend/src/Editor/Components/ButtonGroup.jsx +++ b/frontend/src/Editor/Components/ButtonGroup.jsx @@ -34,6 +34,12 @@ export const ButtonGroup = function Button({ display: visibility ? '' : 'none', }; + const disabledStyles = { + opacity: 0.5, + pointerEvents: 'none', + cursor: 'not-allowed', + }; + const [defaultActive, setDefaultActive] = useState(defaultSelected); const [data, setData] = useState(values); @@ -62,7 +68,7 @@ export const ButtonGroup = function Button({ const buttonClick = (index) => { if (defaultActive?.includes(values[index]) && multiSelection) { - const copyDefaultActive = defaultActive; + const copyDefaultActive = [...defaultActive]; copyDefaultActive?.splice(copyDefaultActive?.indexOf(values[index]), 1); setDefaultActive(copyDefaultActive); setExposedVariable('selected', copyDefaultActive.join(',')); @@ -100,6 +106,7 @@ export const ButtonGroup = function Button({ color: defaultActive?.includes(values[index]) ? selectedTextColor : textColor, transition: 'all .1s ease', boxShadow, + ...(disabledState && disabledStyles), }} key={index} disabled={disabledState} diff --git a/frontend/src/Editor/Components/Checkbox.jsx b/frontend/src/Editor/Components/Checkbox.jsx index 5b0021114c..f64986c6a4 100644 --- a/frontend/src/Editor/Components/Checkbox.jsx +++ b/frontend/src/Editor/Components/Checkbox.jsx @@ -20,7 +20,6 @@ export const Checkbox = ({ const isMandatory = validation?.mandatory ?? false; const [defaultValue, setDefaultValue] = useState(defaultValueFromProperties); const [checked, setChecked] = useState(defaultValueFromProperties); - const [value, setValue] = React.useState(defaultValueFromProperties); const [userInteracted, setUserInteracted] = useState(false); const { label } = properties; @@ -33,14 +32,12 @@ export const Checkbox = ({ const [loading, setLoading] = useState(properties?.loadingState); const [disable, setDisable] = useState(disabledState || loadingState); const [visibility, setVisibility] = useState(properties.visibility); - const { isValid, validationError } = validate(checked); + const [validationStatus, setValidationStatus] = useState(validate(checked)); + const { isValid, validationError } = validationStatus; const toggleValue = (e) => { const isChecked = e.target.checked; - setChecked(isChecked); - setValue(isChecked); - - setExposedVariable('value', isChecked); + setInputValue(isChecked); if (isChecked) { fireEvent('onCheck'); } else { @@ -52,9 +49,8 @@ export const Checkbox = ({ useEffect(() => { if (isInitialRender.current) return; setDefaultValue(defaultValueFromProperties); - setChecked(defaultValueFromProperties); - setValue(defaultValueFromProperties); - setExposedVariable('value', defaultValueFromProperties); + setInputValue(defaultValueFromProperties); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValueFromProperties]); @@ -104,20 +100,29 @@ export const Checkbox = ({ useEffect(() => { if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); + const validationStatus = validate(checked); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); + }, [validate]); + + useEffect(() => { + if (isInitialRender.current) return; + setExposedVariable('toggle', async function () { + setInputValue(!checked); + fireEvent('onChange'); + setUserInteracted(true); + }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); + }, [checked]); useEffect(() => { const setCheckedAndNotify = async (status) => { - await setExposedVariable('value', status); + setInputValue(status); if (status) { fireEvent('onCheck'); } else { fireEvent('onUnCheck'); } - setChecked(status); - setValue(status); }; const exposedVariables = { @@ -137,14 +142,9 @@ export const Checkbox = ({ setExposedVariable('isDisabled', disable); }, toggle: () => { - setExposedVariable('toggle', async function () { - setExposedVariable('value', !checked); - fireEvent('onChange'); - setChecked(!checked); - setValue(!checked); - setUserInteracted(true); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps + setInputValue(!checked); + fireEvent('onChange'); + setUserInteracted(true); }, label: label, isMandatory: isMandatory, @@ -154,10 +154,6 @@ export const Checkbox = ({ isValid: isValid, }; - setDefaultValue(defaultValueFromProperties); - setChecked(defaultValueFromProperties); - setValue(defaultValueFromProperties); - setExposedVariables(exposedVariables); isInitialRender.current = false; @@ -167,9 +163,7 @@ export const Checkbox = ({ const handleToggleChange = () => { const newCheckedState = !checked; - setChecked(newCheckedState); - setValue(newCheckedState); - setExposedVariable('value', newCheckedState); + setInputValue(newCheckedState); fireEvent('onChange'); if (newCheckedState) { fireEvent('onCheck'); @@ -179,6 +173,14 @@ export const Checkbox = ({ setUserInteracted(true); }; + const setInputValue = (value) => { + setChecked(value); + setExposedVariable('value', value); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); + }; + const renderCheckBox = () => ( <>
)}
- {validationError && visibility && !checked && userInteracted && ( + {!isValid && visibility && userInteracted && (
{ + const _setValue = (value) => { + if (typeof value === 'string') { + codeChanged(value); + } + }; + setExposedVariable('setValue', _setValue); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return (
{ setShowValidationError(true); - setDate(date); - const dateString = computeDateString(date); - setExposedVariable('value', dateString); + setInputValue(date); fireEvent('onSelect'); }; @@ -51,11 +52,9 @@ export const Datepicker = function Datepicker({ if (isInitialRender.current) return; const dateMomentInstance = defaultValue && moment(defaultValue, selectedDateFormat); if (dateMomentInstance && dateMomentInstance.isValid()) { - setDate(dateMomentInstance.toDate()); - setExposedVariable('value', defaultValue); + setInputValue(dateMomentInstance.toDate()); } else { - setDate(null); - setExposedVariable('value', undefined); + setInputValue(null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValue]); @@ -73,32 +72,28 @@ export const Datepicker = function Datepicker({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [disabledDates, format]); - const validationData = validate(computeDateString(date)); - const { isValid, validationError } = validationData; + useEffect(() => { + if (isInitialRender.current) return; + const validationStatus = validate(computeDateString(date)); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); + }, [validate]); useEffect(() => { - isInitialRender.current = false; - setExposedVariable('isValid', isValid); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); - - useEffect(() => { - const exposedVariables = { - isValid, - }; const dateMomentInstance = defaultValue && moment(defaultValue, selectedDateFormat); - if (dateMomentInstance && dateMomentInstance.isValid()) { - setDate(dateMomentInstance.toDate()); - exposedVariables.value = defaultValue; - } else { - setDate(null); - exposedVariables.value = undefined; - } - setExposedVariables(exposedVariables); + setInputValue(dateMomentInstance && dateMomentInstance.isValid() ? dateMomentInstance.toDate() : null); isInitialRender.current = false; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const setInputValue = (value) => { + setDate(value); + setExposedVariable('value', value ? computeDateString(value) : undefined); + const validationStatus = validate(computeDateString(value)); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); + }; + return (
(advanced ? findDefaultItem(schema) : value)); const [showValidationError, setShowValidationError] = useState(false); - const validationData = validate(value); - const { isValid, validationError } = validationData; + const [validationStatus, setValidationStatus] = useState(validate(value)); + const { isValid, validationError } = validationStatus; function findDefaultItem(schema) { const foundItem = schema?.find((item) => item?.default === true); return !hasVisibleFalse(foundItem?.value) ? foundItem?.value : undefined; @@ -59,15 +59,11 @@ export const DropDown = function DropDown({ } const setExposedItem = (value, index, onSelectFired = false) => { - setCurrentValue(value); + const selectedOptionLabel = index === undefined ? undefined : display_values?.[index]; + setInputValue(value, selectedOptionLabel); if (onSelectFired) { fireEvent('onSelect'); } - const exposedVariables = { - value, - selectedOptionLabel: index === undefined ? undefined : display_values?.[index], - }; - setExposedVariables(exposedVariables); }; function selectOption(value) { @@ -80,17 +76,6 @@ export const DropDown = function DropDown({ setExposedItem(undefined, undefined, true); } } - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); - - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('value', currentValue); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentValue]); useEffect(() => { if (isInitialRender.current) return; @@ -105,6 +90,13 @@ export const DropDown = function DropDown({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [label]); + useEffect(() => { + if (isInitialRender.current) return; + const validationStatus = validate(currentValue); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); + }, [validate]); + useEffect(() => { if (isInitialRender.current) return; if (advanced) { @@ -113,7 +105,7 @@ export const DropDown = function DropDown({ schema?.filter((item) => item?.visible)?.map((item) => item.label) ); if (hasVisibleFalse(currentValue)) { - setCurrentValue(findDefaultItem(schema)); + setInputValue(findDefaultItem(schema)); } } else setExposedVariable('optionLabels', display_values); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -137,14 +129,21 @@ export const DropDown = function DropDown({ }; setExposedVariables(exposedVariables); - if (hasVisibleFalse(currentValue)) { - setCurrentValue(findDefaultItem(schema)); - } isInitialRender.current = false; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + const exposedVariables = { + selectOption: async function (value) { + selectOption(value); + }, + }; + + setExposedVariables(exposedVariables); + }, [JSON.stringify(properties.values)]); + useEffect(() => { let newValue = undefined; let index = null; @@ -184,6 +183,14 @@ export const DropDown = function DropDown({ } }; + const setInputValue = (value, label) => { + setCurrentValue(value); + setExposedVariables({ value, selectedOptionLabel: label }); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); + }; + const customStyles = { control: (provided, state) => ({ ...provided, @@ -276,10 +283,8 @@ export const DropDown = function DropDown({ onChange={(selectedOption, actionProps) => { setShowValidationError(true); if (actionProps.action === 'select-option') { - setCurrentValue(selectedOption.value); - setExposedVariable('value', selectedOption.value); + setInputValue(selectedOption.value, selectedOption.label); fireEvent('onSelect'); - setExposedVariable('selectedOptionLabel', selectedOption.label); } }} options={selectOptions} diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index a4b318eadf..a597b5bf57 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -90,17 +90,19 @@ export const DropdownV2 = ({ } = styles; const isInitialRender = useRef(true); const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value)); - const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); const isMandatory = validation?.mandatory ?? false; const options = properties?.options; - const validationData = validate(currentValue); - const { isValid, validationError } = validationData; + const [validationStatus, setValidationStatus] = useState(validate(currentValue)); + const { isValid, validationError } = validationStatus; const ref = React.useRef(null); + const dropdownRef = React.useRef(null); const [visibility, setVisibility] = useState(properties.visibility); const [isDropdownLoading, setIsDropdownLoading] = useState(dropdownLoadingState); const [isDropdownDisabled, setIsDropdownDisabled] = useState(disabledState); - const [isFocused, setIsFocused] = useState(false); const [searchInputValue, setSearchInputValue] = useState(''); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [userInteracted, setUserInteracted] = useState(false); + const _height = padding === 'default' ? `${height}px` : `${height + 4}px`; const labelRef = useRef(); function findDefaultItem(schema) { @@ -115,12 +117,12 @@ export const DropdownV2 = ({ let _options = advanced ? schema : options; if (Array.isArray(_options)) { let _selectOptions = _options - .filter((data) => getResolvedValue(advanced ? data?.visible : data?.visible?.value) ?? true) + .filter((data) => data?.visible ?? true) .map((data) => ({ ...data, - label: String(getResolvedValue(data?.label)), - value: getResolvedValue(data?.value), - isDisabled: getResolvedValue(advanced ? data?.disable : data?.disable?.value) ?? false, + label: data?.label, + value: data?.value, + isDisabled: data?.disable ?? false, })); return _selectOptions; @@ -132,7 +134,7 @@ export const DropdownV2 = ({ function selectOption(value) { const val = selectOptions.filter((option) => !option.isDisabled)?.find((option) => option.value === value); if (val) { - setCurrentValue(value); + setInputValue(value); fireEvent('onSelect'); } } @@ -157,24 +159,48 @@ export const DropdownV2 = ({ const handleOutsideClick = (e) => { let menu = ref.current.querySelector('.select__menu'); if (!ref.current.contains(e.target) || !menu || !menu.contains(e.target)) { - setIsFocused(false); setSearchInputValue(''); } + if (dropdownRef.current && !dropdownRef.current?.contains(e.target) && !menu && !menu?.contains(e.target)) { + if (isDropdownOpen) { + fireEvent('onBlur'); + } + setIsDropdownOpen(false); + } + }; + + const setInputValue = (value) => { + setCurrentValue(value); + const _selectedOption = selectOptions.find((option) => option.value === value); + setExposedVariables({ + value, + selectedOption: pick(_selectedOption, ['label', 'value']), + }); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); }; useEffect(() => { if (advanced) { - setCurrentValue(findDefaultItem(schema)); - } else setCurrentValue(value); + setInputValue(findDefaultItem(schema)); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [advanced, value, JSON.stringify(schema)]); + }, [advanced, JSON.stringify(schema)]); + + useEffect(() => { + if (!advanced) { + setInputValue(value); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [advanced, value]); useEffect(() => { document.addEventListener('mousedown', handleOutsideClick); return () => { document.removeEventListener('mousedown', handleOutsideClick); }; - }, []); + }, [isDropdownOpen]); useEffect(() => { if (visibility !== properties.visibility) setVisibility(properties.visibility); @@ -186,18 +212,6 @@ export const DropdownV2 = ({ // Exposed variables - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('value', currentValue); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentValue]); - - useEffect(() => { - const _selectedOption = selectOptions.find((option) => option.value === currentValue); - setExposedVariable('selectedOption', pick(_selectedOption, ['label', 'value'])); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentValue, JSON.stringify(selectOptions)]); - useEffect(() => { if (isInitialRender.current) return; const _options = selectOptions?.map(({ label, value }) => ({ label, value })); @@ -221,11 +235,6 @@ export const DropdownV2 = ({ setExposedVariable('searchText', searchInputValue); }, [searchInputValue]); - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); - }, [isValid]); - useEffect(() => { if (isInitialRender.current) return; setExposedVariable('isVisible', properties.visibility); @@ -246,11 +255,18 @@ export const DropdownV2 = ({ setExposedVariable('isMandatory', isMandatory); }, [isMandatory]); + useEffect(() => { + if (isInitialRender.current) return; + const validationStatus = validate(currentValue); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); + }, [validate]); + useEffect(() => { const _options = selectOptions?.map(({ label, value }) => ({ label, value })); const exposedVariables = { clear: async function () { - setCurrentValue(null); + setInputValue(null); }, setVisibility: async function (value) { setVisibility(value); @@ -303,6 +319,7 @@ export const DropdownV2 = ({ accentColor, isLoading: isDropdownLoading, isDisabled: isDropdownDisabled, + userInteracted, }), backgroundColor: getInputBackgroundColor({ fieldBackgroundColor, @@ -406,6 +423,7 @@ export const DropdownV2 = ({ return ( <>
-
+
{ + if (!isDropdownDisabled) { + fireEvent('onFocus'); + setIsDropdownOpen((prev) => !prev); + } + }} + ref={ref} + >