mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
Merge branch 'lts-3.0' into main-merge-lts-3.0
This commit is contained in:
commit
8e3986e0a1
43 changed files with 1159 additions and 336 deletions
292
.github/workflows/render-preview-deploy.yml
vendored
292
.github/workflows/render-preview-deploy.yml
vendored
|
|
@ -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']
|
||||
})
|
||||
|
|
|
|||
6
.github/workflows/review-path-deploy.yml
vendored
6
.github/workflows/review-path-deploy.yml
vendored
|
|
@ -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",
|
||||
|
|
|
|||
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
3.0.1-ce-lts
|
||||
3.0.4-ce-lts
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.0.1-ce-lts
|
||||
3.0.4-ce-lts
|
||||
|
|
|
|||
5
frontend/assets/images/icons/info-icon.svg
Normal file
5
frontend/assets/images/icons/info-icon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="18" viewBox="0 0 17 18" fill="none">
|
||||
<circle opacity="0.4" cx="8.5118" cy="9.25781" r="8.33333" fill="#BF4F03"/>
|
||||
<path d="M9.34513 5.09115C9.34513 5.55138 8.97204 5.92448 8.5118 5.92448C8.05156 5.92448 7.67847 5.55138 7.67847 5.09115C7.67847 4.63091 8.05156 4.25781 8.5118 4.25781C8.97204 4.25781 9.34513 4.63091 9.34513 5.09115Z" fill="#BF4F03"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.05347 7.59115C7.05347 7.24597 7.33329 6.96615 7.67847 6.96615H8.5118C8.85698 6.96615 9.1368 7.24597 9.1368 7.59115V13.4245C9.1368 13.7697 8.85698 14.0495 8.5118 14.0495C8.16662 14.0495 7.8868 13.7697 7.8868 13.4245V8.21615H7.67847C7.33329 8.21615 7.05347 7.93633 7.05347 7.59115Z" fill="#BF4F03"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 769 B |
|
|
@ -596,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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -201,8 +201,9 @@ export const CreateVersion = ({ showCreateAppVersion, setShowCreateAppVersion })
|
|||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{/* EE - change to development */}
|
||||
<div className="" data-cy="workspace-constant-helper-text">
|
||||
The new version will be created in development environment
|
||||
The new version will be created in production environment
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<div className="" ref={paramListContainerRef}>
|
||||
{selectedQuery && (
|
||||
{selectedQuery && !showLocalDataSourceDeprecationBanner && (
|
||||
<ParameterList
|
||||
parameters={options.parameters}
|
||||
handleAddParameter={handleAddParameter}
|
||||
|
|
@ -310,6 +312,21 @@ export const QueryManagerBody = ({ darkMode, options, setOptions, activeTab }) =
|
|||
</>
|
||||
);
|
||||
};
|
||||
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 =
|
||||
|
|
@ -318,7 +335,6 @@ export const QueryManagerBody = ({ darkMode, options, setOptions, activeTab }) =
|
|||
canReadDataSource(selectedQuery?.data_source_id) ||
|
||||
canDeleteDataSource()
|
||||
: true;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`query-details ${selectedDataSource?.kind === 'tooljetdb' ? 'tooljetdb-query-details' : ''} ${
|
||||
|
|
@ -331,7 +347,14 @@ export const QueryManagerBody = ({ darkMode, options, setOptions, activeTab }) =
|
|||
}} // 40px for preview header height
|
||||
>
|
||||
{selectedDataSource === null || !selectedQuery ? (
|
||||
renderDataSourcesList()
|
||||
showLocalDataSourceDeprecationBanner ? (
|
||||
<>
|
||||
<NotificationBanner enhanceDisabledVisibility={!hasPermissions || isFreezed} darkMode={darkMode} />
|
||||
{renderChangeDataSource()}
|
||||
</>
|
||||
) : (
|
||||
renderDataSourcesList()
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{selectedQuery?.data_source_id && activeTab === 1 && renderChangeDataSource()}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,13 @@ export const codeEditorConfig = {
|
|||
exposedVariables: {
|
||||
value: '',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
handle: 'setValue',
|
||||
displayName: 'Set value',
|
||||
params: [{ handle: 'setValue', defaultValue: '' }],
|
||||
},
|
||||
],
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ const RenderSchema = ({ component, parent, id, onOptionChange, onOptionsChange,
|
|||
darkMode={darkMode}
|
||||
fireEvent={fireEvent}
|
||||
formId={formId}
|
||||
id={id}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -237,7 +237,8 @@ export const Table = React.memo(
|
|||
const changesToBeSavedAndExposed = { dataUpdates: newDataUpdates, changeSet: newChangeset };
|
||||
mergeToTableDetails(changesToBeSavedAndExposed);
|
||||
setExposedVariables({ ...changesToBeSavedAndExposed, updatedData: clonedTableData });
|
||||
fireEvent('onCellValueChanged');
|
||||
// Need to add a timeout here as changes are happening in the next render
|
||||
setTimeout(() => fireEvent('onCellValueChanged'), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ export function extractAndReplaceReferencesFromString(input, componentIdNameMapp
|
|||
|
||||
let match;
|
||||
if (input.startsWith('{{{') && input.endsWith('}}}')) {
|
||||
input = input.replace(/\{\{(.*)\}\}/, '{{($1)}}');
|
||||
const inputContent = input.slice(3, -3);
|
||||
input = `{{({${inputContent}})}}`;
|
||||
const matches = findExpression(input);
|
||||
for (const match of matches) {
|
||||
const { fullMatch, expression, index } = match;
|
||||
|
|
|
|||
|
|
@ -1192,8 +1192,16 @@ export const createComponentsSlice = (set, get) => ({
|
|||
'setComponentProperty'
|
||||
);
|
||||
|
||||
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) {
|
||||
|
|
@ -1236,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) {
|
||||
|
|
|
|||
|
|
@ -441,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 });
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable import/no-unresolved */
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { okaidia } from '@uiw/codemirror-theme-okaidia';
|
||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
|
|
@ -51,6 +51,16 @@ export const CodeEditor = ({ id, height, darkMode, properties, styles, setExpose
|
|||
return height || 'auto';
|
||||
}, [height]);
|
||||
|
||||
useEffect(() => {
|
||||
const _setValue = (value) => {
|
||||
if (typeof value === 'string') {
|
||||
codeChanged(value);
|
||||
}
|
||||
};
|
||||
setExposedVariable('setValue', _setValue);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-disabled={disabledState} style={editorStyles} data-cy={dataCy}>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
border: 1px solid #e9ece;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
width: 920px;
|
||||
// width: 920px; //Add it for EE
|
||||
height: 620px;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ const ConstantTable = ({
|
|||
<tbody>
|
||||
{constants.map((constant) => (
|
||||
<tr key={constant.id}>
|
||||
<td className="p-3">
|
||||
<td className="p-3-constants">
|
||||
<span
|
||||
data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-workspace-constant-name`}
|
||||
data-tooltip-id="tooltip-for-org-constant-cell"
|
||||
|
|
@ -101,7 +101,7 @@ const ConstantTable = ({
|
|||
: constant.name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-muted p-3" style={{ width: '350px' }}>
|
||||
<td className="text-muted p-3-constants" style={{ width: '350px' }}>
|
||||
<a
|
||||
className="text-reset user-email"
|
||||
data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-workspace-constant-value`}
|
||||
|
|
@ -111,7 +111,7 @@ const ConstantTable = ({
|
|||
</td>
|
||||
|
||||
{canUpdateDeleteConstant && (
|
||||
<td className="p-3">
|
||||
<td className="p-3-constants">
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between', gap: 5 }}
|
||||
data-cy={`${constant.name.toLowerCase().replace(/\s+/g, '-')}-workspace-constant-update`}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { BreadCrumbContext } from '@/App';
|
|||
import './ConstantFormStyle.scss';
|
||||
import { Constants, redirectToWorkspace } from '@/_helpers/utils';
|
||||
import { SearchBox } from '@/_components/SearchBox';
|
||||
import { OrganizationList } from '@/_components/OrganizationManager/List';
|
||||
const MODES = Object.freeze({
|
||||
CREATE: 'create',
|
||||
EDIT: 'edit',
|
||||
|
|
@ -429,156 +430,166 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
<div className="align-items-center d-flex justify-content-between" style={{ marginBottom: '10px' }}>
|
||||
<div className="tj-text-sm font-weight-500" data-cy="env-name">
|
||||
{capitalize(activeTabEnvironment?.name)} ({globalCount + secretCount})
|
||||
</div>
|
||||
<div className="workspace-setting-buttons-wrap">
|
||||
{canCreateVariable() && (
|
||||
<ButtonSolid
|
||||
data-cy="add-new-constant-button"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setMode(() => MODES.CREATE);
|
||||
setIsManageVarDrawerOpen(() => true);
|
||||
}}
|
||||
className="add-new-constant-button"
|
||||
customStyles={{ minWidth: '200px', height: '32px' }}
|
||||
disabled={isManageVarDrawerOpen}
|
||||
>
|
||||
+ Create new constant
|
||||
</ButtonSolid>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="constant-page-wrapper">
|
||||
<div className="container-xl">
|
||||
<div>
|
||||
<div className="workspace-constant-header">
|
||||
<div className="tabs-and-search">
|
||||
<div className="tabs">
|
||||
<button
|
||||
className={`tab ${activeTab === Constants.Global ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(Constants.Global)}
|
||||
>
|
||||
Global constants
|
||||
<span className={`tab-count ${activeTab === Constants.Global ? 'active' : ''}`}>
|
||||
({globalCount})
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${activeTab === Constants.Secret ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(Constants.Secret)}
|
||||
>
|
||||
Secrets
|
||||
<span className={`tab-count ${activeTab === Constants.Secret ? 'active' : ''}`}>
|
||||
({secretCount})
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="search-bar">
|
||||
<SearchBox
|
||||
width={250}
|
||||
callBack={handleSearchChange}
|
||||
customClass="tj-common-search-input-group"
|
||||
autoFocus={true}
|
||||
placeholder={activeTab === Constants.Global ? 'Search global constants' : 'Search secrets'}
|
||||
onClearCallback={handleSearchClear}
|
||||
/>
|
||||
</div>
|
||||
<div className="row gx-0">
|
||||
<div className="organization-page-sidebar col">
|
||||
<div className="workspace-nav-list-wrap">
|
||||
<ManageOrgConstantsComponent.EnvironmentsTabs
|
||||
allEnvironments={environments}
|
||||
currentEnvironment={activeTabEnvironment}
|
||||
setActiveTabEnvironment={setActiveTabEnvironment}
|
||||
isLoading={isLoading}
|
||||
allConstants={constants}
|
||||
/>
|
||||
</div>
|
||||
<OrganizationList />
|
||||
</div>
|
||||
|
||||
<div className="page-wrapper mt-4" style={{ marginLeft: '50px' }}>
|
||||
<div className="container-xl" style={{ width: '880px' }}>
|
||||
<div className="align-items-center d-flex justify-content-between">
|
||||
<div className="tj-text-sm font-weight-500" data-cy="env-name">
|
||||
{capitalize(activeTabEnvironment?.name)} ({globalCount + secretCount})
|
||||
</div>
|
||||
<div className="workspace-setting-buttons-wrap">
|
||||
{canCreateVariable() && (
|
||||
<ButtonSolid
|
||||
data-cy="add-new-constant-button"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setMode(() => MODES.CREATE);
|
||||
setIsManageVarDrawerOpen(() => true);
|
||||
}}
|
||||
className="add-new-constant-button"
|
||||
customStyles={{ minWidth: '200px', height: '32px' }}
|
||||
disabled={isManageVarDrawerOpen}
|
||||
>
|
||||
+ Create new constant
|
||||
</ButtonSolid>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-variable-container-wrap mt-2">
|
||||
<div className="container-xl" style={{ width: '880px', padding: '0px' }}>
|
||||
<div className="workspace-constant-table-card">
|
||||
<div className="mt-3">
|
||||
<Alert svg="tj-info">
|
||||
<div
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="text-muted" data-cy="workspace-constant-helper-text">
|
||||
{activeTab === Constants.Global ? (
|
||||
<>
|
||||
To resolve a global workspace constant use{' '}
|
||||
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{constants.access_token}}'}</strong>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
To resolve a secret workspace constant use{' '}
|
||||
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{secrets.access_token}}'}</strong>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
// Todo: Update link to documentation: workspace constants
|
||||
onClick={() =>
|
||||
window.open(
|
||||
'https://docs.tooljet.com/docs/org-management/workspaces/workspace_constants/',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
darkMode={darkMode}
|
||||
size="sm"
|
||||
styles={{
|
||||
width: '100%',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Button.Content title={'Read documentation'} iconSrc="assets/images/icons/student.svg" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="workspace-variable-container-wrap constants-list mt-2">
|
||||
<div className="container-xl constant-page-wrapper">
|
||||
<div className="workspace-constant-header">
|
||||
<div className="tabs-and-search">
|
||||
<div className="tabs">
|
||||
<button
|
||||
className={`tab ${activeTab === Constants.Global ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(Constants.Global)}
|
||||
>
|
||||
Global constants
|
||||
<span className={`tab-count ${activeTab === Constants.Global ? 'active' : ''}`}>
|
||||
({globalCount})
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${activeTab === Constants.Secret ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(Constants.Secret)}
|
||||
>
|
||||
Secrets
|
||||
<span className={`tab-count ${activeTab === Constants.Secret ? 'active' : ''}`}>
|
||||
({secretCount})
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
<div className="manage-sso-container h-100">
|
||||
<div className="d-flex manage-constant-wrapper-card">
|
||||
<ManageOrgConstantsComponent.EnvironmentsTabs
|
||||
allEnvironments={environments}
|
||||
currentEnvironment={activeTabEnvironment}
|
||||
setActiveTabEnvironment={setActiveTabEnvironment}
|
||||
isLoading={isLoading}
|
||||
allConstants={constants}
|
||||
/>
|
||||
{(activeTab === Constants.Global && globalCount > 0) ||
|
||||
(activeTab === Constants.Secret && secretCount > 0) ? (
|
||||
<div className="w-100">
|
||||
<ConstantTable
|
||||
constants={currentTableData}
|
||||
onEditBtnClicked={onEditBtnClicked}
|
||||
onDeleteBtnClicked={onDeleteBtnClicked}
|
||||
isLoading={isLoading}
|
||||
canUpdateDeleteConstant={canUpdateVariable() || canDeleteVariable()}
|
||||
/>
|
||||
<ManageOrgConstantsComponent.Footer
|
||||
darkMode={darkMode}
|
||||
totalPage={totalPages}
|
||||
pageCount={currentPage}
|
||||
dataLoading={false}
|
||||
gotoNextPage={goToNextPage}
|
||||
gotoPreviousPage={goToPreviousPage}
|
||||
showPagination={constants.length > 0}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
canCreateVariable={canCreateVariable()}
|
||||
setIsManageVarDrawerOpen={setIsManageVarDrawerOpen}
|
||||
isLoading={isLoading}
|
||||
searchTerm={searchTerm}
|
||||
|
||||
<div className="search-bar">
|
||||
<SearchBox
|
||||
width={250}
|
||||
callBack={handleSearchChange}
|
||||
customClass="tj-common-search-input-group"
|
||||
autoFocus={true}
|
||||
placeholder={activeTab === Constants.Global ? 'Search global constants' : 'Search secrets'}
|
||||
onClearCallback={handleSearchClear}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-constant-table-card">
|
||||
<div className="mt-3">
|
||||
<Alert svg="tj-info">
|
||||
<div
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="text-muted" data-cy="workspace-constant-helper-text">
|
||||
{activeTab === Constants.Global ? (
|
||||
<>
|
||||
To resolve a global workspace constant use{' '}
|
||||
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>
|
||||
{'{{constants.access_token}}'}
|
||||
</strong>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
To resolve a secret workspace constant use{' '}
|
||||
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{secrets.access_token}}'}</strong>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open(
|
||||
'https://docs.tooljet.com/docs/org-management/workspaces/workspace_constants/',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
darkMode={darkMode}
|
||||
size="sm"
|
||||
styles={{
|
||||
width: '100%',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Button.Content title={'Read documentation'} iconSrc="assets/images/icons/student.svg" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<div className="manage-sso-container h-100">
|
||||
<div className="d-flex manage-constant-wrapper-card">
|
||||
{(activeTab === Constants.Global && globalCount > 0) ||
|
||||
(activeTab === Constants.Secret && secretCount > 0) ? (
|
||||
<div className="w-100">
|
||||
<ConstantTable
|
||||
constants={currentTableData}
|
||||
onEditBtnClicked={onEditBtnClicked}
|
||||
onDeleteBtnClicked={onDeleteBtnClicked}
|
||||
isLoading={isLoading}
|
||||
canUpdateDeleteConstant={canUpdateVariable() || canDeleteVariable()}
|
||||
/>
|
||||
<ManageOrgConstantsComponent.Footer
|
||||
darkMode={darkMode}
|
||||
totalPage={totalPages}
|
||||
pageCount={currentPage}
|
||||
dataLoading={false}
|
||||
gotoNextPage={goToNextPage}
|
||||
gotoPreviousPage={goToPreviousPage}
|
||||
showPagination={constants.length > 0}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
canCreateVariable={canCreateVariable()}
|
||||
setIsManageVarDrawerOpen={setIsManageVarDrawerOpen}
|
||||
isLoading={isLoading}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { Alert } from '@/_ui/Alert/Alert';
|
||||
import './resources/styles.scss';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
docsLink: ' https://docs.tooljet.com/docs/data-sources/local-data-sources-migration',
|
||||
};
|
||||
|
||||
const DEFAULT_MESSAGES = {
|
||||
prefix: 'This query is connected to a local data source which has been',
|
||||
highlightedText: 'discontinued',
|
||||
middle: 'Please create a global data source connection to reconnect your query.',
|
||||
suffix: 'to know more.',
|
||||
linkText: 'Read documentation',
|
||||
};
|
||||
|
||||
const NotificationBanner = ({
|
||||
docsLink,
|
||||
customMessage,
|
||||
darkMode = false,
|
||||
highlightedText = DEFAULT_MESSAGES.highlightedText,
|
||||
highlightedClassName = 'highlighted-text',
|
||||
enhanceDisabledVisibility = false,
|
||||
}) => {
|
||||
const currentDocsLink = docsLink || DEFAULT_CONFIG.docsLink;
|
||||
|
||||
const bannerMessage = customMessage || (
|
||||
<>
|
||||
{DEFAULT_MESSAGES.prefix} <span className={highlightedClassName}>{highlightedText}</span>.{' '}
|
||||
{DEFAULT_MESSAGES.middle}{' '}
|
||||
<a href={currentDocsLink} className="documentation-link" target="_blank" rel="noopener noreferrer">
|
||||
{DEFAULT_MESSAGES.linkText}
|
||||
</a>{' '}
|
||||
{DEFAULT_MESSAGES.suffix}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="notification-banner-component">
|
||||
<Alert svg="info-icon" cls="notification-banner" useDarkMode={darkMode}>
|
||||
<div className={`notification-content ${enhanceDisabledVisibility ? 'disabled' : ''}`}>{bannerMessage}</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationBanner;
|
||||
|
||||
// To Do later: Expand this component properly to make it generic notification component
|
||||
1
frontend/src/_components/NotificationBanner/index.js
Normal file
1
frontend/src/_components/NotificationBanner/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './NotificationBanner';
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
.notification-banner {
|
||||
display: flex;
|
||||
padding: var(--3, 6px) var(--6, 12px);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 32px;
|
||||
flex: 1 0 0;
|
||||
border-radius: var(--3, 6px);
|
||||
background: var(--background-warning-weak, #FAEFE7);
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
opacity: 1;
|
||||
|
||||
img {
|
||||
margin-top: -5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
.notification-banner {
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
&>div {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
color: var(--text-default, #1B1F24);
|
||||
font-family: "IBM Plex Sans";
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
&.disabled {
|
||||
font-weight: 700 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.highlighted-text {
|
||||
font-weight: 600 !important;
|
||||
font-style: bold;
|
||||
}
|
||||
|
||||
.documentation-link {
|
||||
font-weight: 400 !important;
|
||||
color: var(--primary, #3E63DD) !important;
|
||||
text-decoration-line: underline !important;
|
||||
}
|
||||
|
|
@ -148,9 +148,9 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
setSlug({ value: defaultValue, error: '' });
|
||||
|
||||
const checkWorkspaceUniqueness = async () => {
|
||||
sluginput.current.value = defaultValue;
|
||||
try {
|
||||
await organizationService.checkWorkspaceUniqueness(null, defaultValue);
|
||||
sluginput.current.value = defaultValue;
|
||||
} catch (errResponse) {
|
||||
let error = {
|
||||
status: false,
|
||||
|
|
|
|||
|
|
@ -7864,7 +7864,7 @@ tbody {
|
|||
|
||||
.marketplace-page-sidebar {
|
||||
height: calc(100vh - 64px);
|
||||
max-width: 288px;
|
||||
max-width: 272px;
|
||||
background-color: var(--page-default);
|
||||
border-right: 1px solid var(--slate5) !important;
|
||||
display: grid !important;
|
||||
|
|
@ -12830,6 +12830,11 @@ color: var(--text-default);
|
|||
padding: 16px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
.p-3-constants{
|
||||
padding: 1rem !important;
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
|
|
@ -15569,9 +15574,10 @@ color: var(--text-default);
|
|||
background-color: var(--page-default);
|
||||
height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 1.5rem;
|
||||
//Uncomment for EE
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.blank-page-wrapper {
|
||||
|
|
|
|||
|
|
@ -38,13 +38,15 @@ const SignupPage = ({ configs, organizationId }) => {
|
|||
});
|
||||
}, []);
|
||||
|
||||
const handleSignup = (formData, onSuccess = () => {}, onFaluire = () => {}) => {
|
||||
const handleSignup = (formData, onSuccess = () => {}, onFailure = () => {}) => {
|
||||
const { email, name, password } = formData;
|
||||
|
||||
if (organizationToken) {
|
||||
authenticationService
|
||||
.activateAccountWithToken(email, password, organizationToken)
|
||||
.then((response) => onInvitedUserSignUpSuccess(response, navigate))
|
||||
.then((response) => {
|
||||
onInvitedUserSignUpSuccess(response, navigate);
|
||||
onSuccess();
|
||||
})
|
||||
.catch((errorObj) => {
|
||||
let errorMessage;
|
||||
const isThereAnyErrorsArray = errorObj?.error?.length && typeof errorObj?.error?.[0] === 'string';
|
||||
|
|
@ -54,6 +56,7 @@ const SignupPage = ({ configs, organizationId }) => {
|
|||
errorMessage = errorObj?.error?.error;
|
||||
}
|
||||
errorMessage && toast.error(errorMessage);
|
||||
onFailure();
|
||||
});
|
||||
} else {
|
||||
authenticationService
|
||||
|
|
@ -69,7 +72,7 @@ const SignupPage = ({ configs, organizationId }) => {
|
|||
toast.error(e?.error || 'Something went wrong!', {
|
||||
position: 'top-center',
|
||||
});
|
||||
onFaluire();
|
||||
onFailure();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -118,9 +118,11 @@ const SignupForm = ({
|
|||
onSubmit(
|
||||
formData,
|
||||
() => {
|
||||
// Success callback
|
||||
setIsLoading(false);
|
||||
},
|
||||
() => {
|
||||
// Error callback
|
||||
setIsLoading(false);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ import {
|
|||
import { SourceOptions, QueryOptions } from './types';
|
||||
import { isEmpty } from '@tooljet-plugins/common';
|
||||
|
||||
const STATEMENT_TIMEOUT = 10000;
|
||||
|
||||
export default class MssqlQueryService implements QueryService {
|
||||
private static _instance: MssqlQueryService;
|
||||
private STATEMENT_TIMEOUT;
|
||||
|
||||
constructor() {
|
||||
this.STATEMENT_TIMEOUT =
|
||||
process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT && !isNaN(Number(process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT))
|
||||
? Number(process.env.PLUGINS_SQL_DB_STATEMENT_TIMEOUT)
|
||||
: 120000;
|
||||
|
||||
if (MssqlQueryService._instance) {
|
||||
return MssqlQueryService._instance;
|
||||
}
|
||||
|
|
@ -84,13 +88,13 @@ export default class MssqlQueryService implements QueryService {
|
|||
private async executeQuery(knexInstance: Knex, query: string, sanitizedQueryParams: Record<string, any> = {}) {
|
||||
if (isEmpty(query)) throw new Error('Query is empty');
|
||||
|
||||
const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(STATEMENT_TIMEOUT);
|
||||
const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(this.STATEMENT_TIMEOUT);
|
||||
return result;
|
||||
}
|
||||
|
||||
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
|
||||
const knexInstance = await this.getConnection(sourceOptions, {}, false);
|
||||
await knexInstance.raw('select @@version;').timeout(STATEMENT_TIMEOUT);
|
||||
await knexInstance.raw('select @@version;').timeout(this.STATEMENT_TIMEOUT);
|
||||
knexInstance.destroy();
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ import {
|
|||
import { SourceOptions, QueryOptions } from './types';
|
||||
import { isEmpty } from '@tooljet-plugins/common';
|
||||
|
||||
const STATEMENT_TIMEOUT = 10000;
|
||||
|
||||
export default class MysqlQueryService implements QueryService {
|
||||
private static _instance: MysqlQueryService;
|
||||
private STATEMENT_TIMEOUT;
|
||||
|
||||
constructor() {
|
||||
this.STATEMENT_TIMEOUT =
|
||||
process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT && !isNaN(Number(process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT))
|
||||
? Number(process.env.PLUGINS_SQL_DB_STATEMENT_TIMEOUT)
|
||||
: 120000;
|
||||
|
||||
if (MysqlQueryService._instance) {
|
||||
return MysqlQueryService._instance;
|
||||
}
|
||||
|
|
@ -51,7 +55,7 @@ export default class MysqlQueryService implements QueryService {
|
|||
|
||||
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
|
||||
const knexInstance = await this.getConnection(sourceOptions, {}, false);
|
||||
await knexInstance.raw('select @@version;').timeout(STATEMENT_TIMEOUT);
|
||||
await knexInstance.raw('select @@version;').timeout(this.STATEMENT_TIMEOUT);
|
||||
knexInstance.destroy();
|
||||
return { status: 'ok' };
|
||||
}
|
||||
|
|
@ -77,7 +81,7 @@ export default class MysqlQueryService implements QueryService {
|
|||
private async executeQuery(knexInstance: Knex, query: string, sanitizedQueryParams: Record<string, any> = {}) {
|
||||
if (isEmpty(query)) throw new Error('Query is empty');
|
||||
|
||||
const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(STATEMENT_TIMEOUT);
|
||||
const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(this.STATEMENT_TIMEOUT);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ import { SourceOptions, QueryOptions } from './types';
|
|||
import knex, { Knex } from 'knex';
|
||||
import { isEmpty } from '@tooljet-plugins/common';
|
||||
|
||||
const STATEMENT_TIMEOUT = 10000;
|
||||
|
||||
export default class PostgresqlQueryService implements QueryService {
|
||||
private static _instance: PostgresqlQueryService;
|
||||
private STATEMENT_TIMEOUT;
|
||||
|
||||
constructor() {
|
||||
this.STATEMENT_TIMEOUT =
|
||||
process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT && !isNaN(Number(process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT))
|
||||
? Number(process.env.PLUGINS_SQL_DB_STATEMENT_TIMEOUT)
|
||||
: 120000;
|
||||
|
||||
if (PostgresqlQueryService._instance) {
|
||||
return PostgresqlQueryService._instance;
|
||||
}
|
||||
|
|
@ -51,7 +55,7 @@ export default class PostgresqlQueryService implements QueryService {
|
|||
|
||||
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
|
||||
const knexInstance = await this.getConnection(sourceOptions, {}, false);
|
||||
await knexInstance.raw('SELECT version();').timeout(STATEMENT_TIMEOUT);
|
||||
await knexInstance.raw('SELECT version();').timeout(this.STATEMENT_TIMEOUT);
|
||||
return { status: 'ok' };
|
||||
}
|
||||
|
||||
|
|
@ -75,9 +79,7 @@ export default class PostgresqlQueryService implements QueryService {
|
|||
|
||||
private async executeQuery(knexInstance: Knex, query: string, sanitizedQueryParams: Record<string, any> = {}) {
|
||||
if (isEmpty(query)) throw new Error('Query is empty');
|
||||
|
||||
const { rows } = await knexInstance.raw(query, sanitizedQueryParams).timeout(STATEMENT_TIMEOUT);
|
||||
|
||||
const { rows } = await knexInstance.raw(query, sanitizedQueryParams);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +102,7 @@ export default class PostgresqlQueryService implements QueryService {
|
|||
password: sourceOptions.password,
|
||||
port: sourceOptions.port,
|
||||
ssl: this.getSslConfig(sourceOptions),
|
||||
statement_timeout: this.STATEMENT_TIMEOUT,
|
||||
};
|
||||
} else if (sourceOptions.connection_type === 'string' && sourceOptions.connection_string) {
|
||||
connectionConfig = {
|
||||
|
|
@ -111,7 +114,7 @@ export default class PostgresqlQueryService implements QueryService {
|
|||
client: 'pg',
|
||||
connection: connectionConfig,
|
||||
pool: { min: 0, max: 10, acquireTimeoutMillis: 10000 },
|
||||
acquireConnectionTimeout: 10000,
|
||||
acquireConnectionTimeout: 60000,
|
||||
...this.connectionOptions(sourceOptions),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.0.1-ce-lts
|
||||
3.0.4-ce-lts
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@
|
|||
"@sentry/tracing": "6.17.6",
|
||||
"@tooljet/plugins": "../plugins",
|
||||
"@types/express-serve-static-core": "^4.19.5",
|
||||
"acorn": "^8.13.0",
|
||||
"acorn-walk": "^8.3.4",
|
||||
"bcrypt": "^5.0.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ export class AppController {
|
|||
return await this.authService.validateInvitedUserSession(user, invitedUser, tokens);
|
||||
}
|
||||
|
||||
@UseGuards(SignupDisableGuard)
|
||||
@UseGuards(FirstUserSignupDisableGuard)
|
||||
@Post('activate-account-with-token')
|
||||
async activateAccountWithToken(
|
||||
|
|
@ -164,7 +163,6 @@ export class AppController {
|
|||
return await this.authService.acceptOrganizationInvite(response, user, acceptInviteDto);
|
||||
}
|
||||
|
||||
@UseGuards(SignupDisableGuard)
|
||||
@UseGuards(FirstUserSignupDisableGuard)
|
||||
@Post('signup')
|
||||
async signup(@Body() appSignUpDto: AppSignupDto, @Res({ passthrough: true }) response: Response) {
|
||||
|
|
|
|||
|
|
@ -1,39 +1,40 @@
|
|||
import * as acorn from 'acorn';
|
||||
import * as walk from '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 updateEntityReferences(node, resourceMapping: Record<string, string> = {}) {
|
||||
if (typeof node === 'object') {
|
||||
for (const key in node) {
|
||||
let value = node[key];
|
||||
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
|
||||
const referenceExists = value;
|
||||
|
||||
if (referenceExists) {
|
||||
const matches = value.match(/{{(.*?)}}/g);
|
||||
// gett all references {{entityName}}
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
// remove curly braces and extract the entity "component.entityName.something"
|
||||
const ref = match.slice(2, -2).trim();
|
||||
const entityName = ref.split('.')[1];
|
||||
|
||||
if (resourceMapping[entityName]) {
|
||||
const newValue = value.replace(entityName, resourceMapping[entityName]);
|
||||
|
||||
node[key] = newValue;
|
||||
value = newValue;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// kept this logic for fallback, although it should not be needed
|
||||
const ref = value.replace('{{', '').replace('}}', '');
|
||||
|
||||
const entityName = ref.split('.')[1];
|
||||
|
||||
if (resourceMapping[entityName]) {
|
||||
const newValue = value.replace(entityName, resourceMapping[entityName]);
|
||||
|
||||
node[key] = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
node[key] = extractAndReplaceReferencesFromString(value, resourceMapping, resourceMapping)?.valueWithId;
|
||||
} else if (typeof value === 'object') {
|
||||
value = updateEntityReferences(value, resourceMapping);
|
||||
}
|
||||
|
|
@ -43,53 +44,373 @@ export function updateEntityReferences(node, resourceMapping: Record<string, str
|
|||
return node;
|
||||
}
|
||||
|
||||
function containsBracketNotation(queryString) {
|
||||
const bracketNotationRegex = /\[\s*['"][^'"]+['"]\s*\]/;
|
||||
return bracketNotationRegex.test(queryString);
|
||||
}
|
||||
|
||||
export function findAllEntityReferences(node, allRefs): [] {
|
||||
if (typeof node === 'object') {
|
||||
for (const key in node) {
|
||||
const value = node[key];
|
||||
|
||||
if (typeof value === 'string' && containsBracketNotation(value)) {
|
||||
//skip if the value is a bracket notation
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
|
||||
const referenceExists = value;
|
||||
|
||||
if (referenceExists) {
|
||||
const matches = value.match(/{{(.*?)}}/g);
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
const ref = match.slice(2, -2).trim(); // Remove {{ and }}
|
||||
const entityName = ref.split('.')[1];
|
||||
if (entityName && !allRefs.includes(entityName)) {
|
||||
allRefs.push(entityName);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// kept this logic for fallback, although it should not be needed
|
||||
const ref = value.replace('{{', '').replace('}}', '');
|
||||
|
||||
const entityName = ref.split('.')[1];
|
||||
|
||||
allRefs.push(entityName);
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
findAllEntityReferences(value, allRefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
return allRefs;
|
||||
}
|
||||
|
||||
export function isValidUUID(uuid) {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(uuid);
|
||||
}
|
||||
|
||||
export function extractAndReplaceReferencesFromString(input, componentIdNameMapping = {}, queryIdNameMapping = {}) {
|
||||
// Quick check for relevant keywords
|
||||
const regexForQuickCheck =
|
||||
/\b(components|queries|globals|variables|page|parameters|secrets|constants)(?:\[\S*|\.\S*|\?\.\S*)/;
|
||||
if (!regexForQuickCheck.test(input)) {
|
||||
return {
|
||||
allRefs: [],
|
||||
valueWithId: input,
|
||||
valueWithBrackets: input,
|
||||
};
|
||||
}
|
||||
|
||||
const relevantKeywords = /\b(components|queries|globals|variables|page|parameters|secrets|constants)\b/;
|
||||
const expressionRegex = /{{(.*?)}}/gs;
|
||||
const results = [];
|
||||
let lastIndex = 0;
|
||||
let replacedString = '';
|
||||
let bracketNotationString = '';
|
||||
|
||||
// Precompile the UUID regex
|
||||
const uuidRegex =
|
||||
/\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;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const { processedExpression, uuidMappings } = preprocessExpression(
|
||||
expression,
|
||||
uuidRegex,
|
||||
componentIdNameMapping,
|
||||
queryIdNameMapping
|
||||
);
|
||||
const parsedResult = parseExpression(
|
||||
processedExpression,
|
||||
componentIdNameMapping,
|
||||
queryIdNameMapping,
|
||||
uuidMappings
|
||||
);
|
||||
|
||||
replacedString += input.slice(lastIndex, match.index);
|
||||
bracketNotationString += input.slice(lastIndex, match.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 = match.index + fullMatch.length;
|
||||
}
|
||||
|
||||
replacedString += input.slice(lastIndex);
|
||||
bracketNotationString += input.slice(lastIndex);
|
||||
|
||||
return {
|
||||
allRefs: results.flatMap((r) => r.allRefs),
|
||||
valueWithId: replacedString,
|
||||
valueWithBrackets: bracketNotationString,
|
||||
};
|
||||
}
|
||||
|
||||
function preprocessExpression(expression, uuidRegex, componentIdNameMapping, queryIdNameMapping) {
|
||||
const uuidMappings = {};
|
||||
let placeholderCounter = 0;
|
||||
|
||||
const processedExpression = expression.replace(uuidRegex, (match, p1, p2, p3, p4) => {
|
||||
const placeholder = `__UUID_PLACEHOLDER_${placeholderCounter}__`;
|
||||
uuidMappings[placeholder] = (p1 === 'components' ? componentIdNameMapping[p3] : queryIdNameMapping[p3]) || p3;
|
||||
placeholderCounter++;
|
||||
return `${p1}${p2}${placeholder}${p4 || ''}`;
|
||||
});
|
||||
|
||||
return { processedExpression, uuidMappings };
|
||||
}
|
||||
|
||||
function replaceIdsInExpression(
|
||||
expression,
|
||||
componentIdNameMapping,
|
||||
queryIdNameMapping,
|
||||
useBracketNotation,
|
||||
uuidMappings
|
||||
) {
|
||||
try {
|
||||
const ast = acorn.parse(expression, { ecmaVersion: 2020 });
|
||||
const replacements = [];
|
||||
|
||||
walk.simple(ast, {
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
node.object.type === 'Identifier' &&
|
||||
(node.object.name === 'components' || node.object.name === 'queries')
|
||||
) {
|
||||
const isComponent = node.object.name === 'components';
|
||||
const mapping = isComponent ? componentIdNameMapping : queryIdNameMapping;
|
||||
|
||||
if (node.property.type === 'Identifier') {
|
||||
const name = node.property.name;
|
||||
const nameWithOptionalCheck = node.optional
|
||||
? useBracketNotation
|
||||
? `${node.object.name}?.`
|
||||
: `${node.object.name}?`
|
||||
: `${node.object.name}`;
|
||||
if (mapping[name] || name.startsWith('__UUID_PLACEHOLDER_')) {
|
||||
const start = node.start;
|
||||
const end = node.end;
|
||||
let replacement;
|
||||
if (name.startsWith('__UUID_PLACEHOLDER_')) {
|
||||
const actualName = uuidMappings[name];
|
||||
replacement = useBracketNotation
|
||||
? `${nameWithOptionalCheck}["${actualName}"]`
|
||||
: `${nameWithOptionalCheck}.${actualName}`;
|
||||
} else {
|
||||
replacement = useBracketNotation
|
||||
? `${nameWithOptionalCheck}["${mapping[name]}"]`
|
||||
: `${nameWithOptionalCheck}.${mapping[name]}`;
|
||||
}
|
||||
replacements.push({ start, end, replacement });
|
||||
}
|
||||
} else if (node.property.type === 'Literal') {
|
||||
const name = node.property.value as string;
|
||||
const nameWithOptionalCheck = node.optional ? `${node.object.name}?.` : `${node.object.name}`;
|
||||
if (mapping[name] || name.startsWith('__UUID_PLACEHOLDER_')) {
|
||||
const start = node.start;
|
||||
const end = node.end;
|
||||
let replacement;
|
||||
if (name.startsWith('__UUID_PLACEHOLDER_')) {
|
||||
const actualName = uuidMappings[name];
|
||||
replacement = `${nameWithOptionalCheck}["${actualName}"]`;
|
||||
} else {
|
||||
replacement = `${nameWithOptionalCheck}["${mapping[name]}"]`;
|
||||
}
|
||||
replacements.push({ start, end, replacement });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (replacements.length === 0) return expression;
|
||||
|
||||
replacements.sort((a, b) => b.start - a.start);
|
||||
|
||||
let result = expression;
|
||||
for (const { start, end, replacement } of replacements) {
|
||||
result = result.slice(0, start) + replacement + result.slice(end);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
|
||||
function parseExpression(expression, componentIdNameMapping, queryIdNameMapping, uuidMappings) {
|
||||
try {
|
||||
const ast = acorn.parse(expression, { ecmaVersion: 2020 });
|
||||
const references = [];
|
||||
const validRootObjects = {
|
||||
components: true,
|
||||
queries: true,
|
||||
variables: true,
|
||||
globals: true,
|
||||
page: true,
|
||||
};
|
||||
walk.simple(ast, {
|
||||
MemberExpression: handleMemberExpression,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function handleMemberExpression(node) {
|
||||
const reference = extractPath(node);
|
||||
if (reference) references.push(reference);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function extractPath(node) {
|
||||
const path = [];
|
||||
let current = node;
|
||||
let rootObject = '';
|
||||
|
||||
while (current) {
|
||||
if (current.type === 'Identifier') {
|
||||
path.unshift(current.name);
|
||||
if (validRootObjects[current.name]) {
|
||||
rootObject = current.name;
|
||||
break;
|
||||
}
|
||||
} else if (current.type === 'MemberExpression' || current.type === 'OptionalMemberExpression') {
|
||||
if (current.computed) {
|
||||
if (
|
||||
current.property.type === 'Literal' &&
|
||||
(typeof current.property.value === 'string' || typeof current.property.value === 'number')
|
||||
) {
|
||||
path.unshift(current.property.value.toString());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
path.unshift(current.property.name);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
current = current.object;
|
||||
}
|
||||
|
||||
if (
|
||||
(rootObject && (rootObject === 'queries' || rootObject === 'components') && path.length >= 3) ||
|
||||
((rootObject === 'variables' || rootObject === 'globals') && path.length === 2) ||
|
||||
(rootObject === 'page' && path.length === 3)
|
||||
) {
|
||||
return createReferenceObject(rootObject, path, uuidMappings, componentIdNameMapping, queryIdNameMapping);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return { references };
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { references: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function createReferenceObject(entityType, path, uuidMappings, componentIdNameMapping, queryIdNameMapping) {
|
||||
let entityNameOrId, entityKey;
|
||||
|
||||
if (entityType === 'components' || entityType === 'queries') {
|
||||
entityNameOrId = path[1];
|
||||
entityKey = path[2];
|
||||
|
||||
if (entityNameOrId.startsWith('__UUID_PLACEHOLDER_')) {
|
||||
entityNameOrId = uuidMappings[entityNameOrId];
|
||||
} else {
|
||||
const mapping = entityType === 'components' ? componentIdNameMapping : queryIdNameMapping;
|
||||
entityNameOrId = mapping[entityNameOrId] || entityNameOrId;
|
||||
}
|
||||
} else if (entityType === 'variables' || entityType === 'globals') {
|
||||
entityKey = path[1];
|
||||
} else if (entityType === 'page') {
|
||||
entityNameOrId = path[1];
|
||||
entityKey = path[2];
|
||||
}
|
||||
|
||||
return { entityType, entityNameOrId, entityKey };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -424,3 +424,13 @@ export function mergeDeep(target, source, seen = new WeakMap()) {
|
|||
|
||||
return target;
|
||||
}
|
||||
export const getSubpath = () => {
|
||||
const subpath = process.env.SUB_PATH || '';
|
||||
// Ensure subpath starts and ends with slashes
|
||||
if (subpath) {
|
||||
if (!subpath.startsWith('/') || !subpath.endsWith('/')) {
|
||||
throw new Error('SUB_PATH must start and end with a slash');
|
||||
}
|
||||
}
|
||||
return subpath;
|
||||
};
|
||||
|
|
@ -67,6 +67,13 @@ export const codeEditorConfig = {
|
|||
exposedVariables: {
|
||||
value: '',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
handle: 'setValue',
|
||||
displayName: 'Set value',
|
||||
params: [{ handle: 'setValue', defaultValue: '' }],
|
||||
},
|
||||
],
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { bootstrap as globalAgentBootstrap } from 'global-agent';
|
|||
import { join } from 'path';
|
||||
import * as helmet from 'helmet';
|
||||
import * as express from 'express';
|
||||
import { getSubpath } from '@helpers/utils.helper';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
|
|
@ -100,7 +101,15 @@ function setSecurityHeaders(app, configService) {
|
|||
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Permissions-Policy', 'geolocation=(self), camera=(), microphone=()');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
|
||||
const subpath = getSubpath();
|
||||
const path = req.path.replace(subpath, subpath ? '/' : '');
|
||||
if (path.startsWith('/api/')) {
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
} else {
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, set } from 'lodash';
|
||||
import { App } from 'src/entities/app.entity';
|
||||
import { AppEnvironment } from 'src/entities/app_environments.entity';
|
||||
import { AppVersion } from 'src/entities/app_version.entity';
|
||||
|
|
@ -30,7 +30,7 @@ import { Component } from 'src/entities/component.entity';
|
|||
import { Layout } from 'src/entities/layout.entity';
|
||||
import { EventHandler, Target } from 'src/entities/event_handler.entity';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { findAllEntityReferences, isValidUUID, updateEntityReferences } from 'src/helpers/import_export.helpers';
|
||||
import { updateEntityReferences } from 'src/helpers/import_export.helpers';
|
||||
interface AppResourceMappings {
|
||||
defaultDataSourceIdMapping: Record<string, string>;
|
||||
dataQueryMapping: Record<string, string>;
|
||||
|
|
@ -290,13 +290,7 @@ export class AppImportExportService {
|
|||
.getMany();
|
||||
|
||||
const toUpdateComponents = components.filter((component) => {
|
||||
const entityReferencesInComponentDefinitions = findAllEntityReferences(component, []).filter(
|
||||
(entity) => entity && isValidUUID(entity)
|
||||
);
|
||||
|
||||
if (entityReferencesInComponentDefinitions.length > 0) {
|
||||
return updateEntityReferences(component, mappings);
|
||||
}
|
||||
return updateEntityReferences(component, mappings);
|
||||
});
|
||||
|
||||
if (!isEmpty(toUpdateComponents)) {
|
||||
|
|
@ -312,13 +306,7 @@ export class AppImportExportService {
|
|||
.getMany();
|
||||
|
||||
const toUpdateDataQueries = dataQueries.filter((dataQuery) => {
|
||||
const entityReferencesInQueryOptions = findAllEntityReferences(dataQuery, []).filter(
|
||||
(entity) => entity && isValidUUID(entity)
|
||||
);
|
||||
|
||||
if (entityReferencesInQueryOptions.length > 0) {
|
||||
return updateEntityReferences(dataQuery, mappings);
|
||||
}
|
||||
return updateEntityReferences(dataQuery, mappings);
|
||||
});
|
||||
|
||||
if (!isEmpty(toUpdateDataQueries)) {
|
||||
|
|
@ -517,6 +505,7 @@ export class AppImportExportService {
|
|||
autoComputeLayout: page.autoComputeLayout || false,
|
||||
isPageGroup: page.isPageGroup || false,
|
||||
pageGroupIndex: page.pageGroupIndex || null,
|
||||
icon: page.icon || null,
|
||||
});
|
||||
const pageCreated = await transactionalEntityManager.save(newPage);
|
||||
|
||||
|
|
@ -784,6 +773,7 @@ export class AppImportExportService {
|
|||
disabled: page.disabled || false,
|
||||
hidden: page.hidden || false,
|
||||
autoComputeLayout: page.autoComputeLayout || false,
|
||||
icon: page.icon || null,
|
||||
});
|
||||
|
||||
const pageCreated = await manager.save(newPage);
|
||||
|
|
@ -809,6 +799,10 @@ export class AppImportExportService {
|
|||
const newComponent = new Component();
|
||||
|
||||
let parentId = component.parent ? component.parent : null;
|
||||
if (component?.properties?.buttonToSubmit) {
|
||||
const newButtonToSubmitValue = newComponentIdsMap[component?.properties?.buttonToSubmit?.value];
|
||||
if (newButtonToSubmitValue) set(component, 'properties.buttonToSubmit.value', newButtonToSubmitValue);
|
||||
}
|
||||
|
||||
const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, pageComponents, parentId, true);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ import { Component } from 'src/entities/component.entity';
|
|||
import { EventHandler, Target } from 'src/entities/event_handler.entity';
|
||||
import { VersionReleaseDto } from '@dto/version-release.dto';
|
||||
|
||||
import { findAllEntityReferences, isValidUUID, updateEntityReferences } from 'src/helpers/import_export.helpers';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { updateEntityReferences } from 'src/helpers/import_export.helpers';
|
||||
import { isEmpty, set } from 'lodash';
|
||||
import { AppBase } from 'src/entities/app_base.entity';
|
||||
import { LayoutDimensionUnits } from 'src/helpers/components.helper';
|
||||
import { AbilityService } from './permissions-ability.service';
|
||||
|
|
@ -435,13 +435,7 @@ export class AppsService {
|
|||
.getMany();
|
||||
|
||||
const toUpdateComponents = components.filter((component) => {
|
||||
const entityReferencesInComponentDefinitions = findAllEntityReferences(component, []).filter(
|
||||
(entity) => entity && isValidUUID(entity)
|
||||
);
|
||||
|
||||
if (entityReferencesInComponentDefinitions.length > 0) {
|
||||
return updateEntityReferences(component, mappings);
|
||||
}
|
||||
return updateEntityReferences(component, mappings);
|
||||
});
|
||||
|
||||
if (!isEmpty(toUpdateComponents)) {
|
||||
|
|
@ -457,13 +451,7 @@ export class AppsService {
|
|||
.getMany();
|
||||
|
||||
const toUpdateDataQueries = dataQueries.filter((dataQuery) => {
|
||||
const entityReferencesInQueryOptions = findAllEntityReferences(dataQuery, []).filter(
|
||||
(entity) => entity && isValidUUID(entity)
|
||||
);
|
||||
|
||||
if (entityReferencesInQueryOptions.length > 0) {
|
||||
return updateEntityReferences(dataQuery, mappings);
|
||||
}
|
||||
return updateEntityReferences(dataQuery, mappings);
|
||||
});
|
||||
|
||||
if (!isEmpty(toUpdateDataQueries)) {
|
||||
|
|
@ -660,9 +648,14 @@ export class AppsService {
|
|||
await manager.save(newEvent);
|
||||
});
|
||||
});
|
||||
|
||||
newComponents.forEach((component) => {
|
||||
let parentId = component.parent ? component.parent : null;
|
||||
// re establish mapping relationship
|
||||
if (component?.properties?.buttonToSubmit) {
|
||||
const newButtonToSubmitValue =
|
||||
oldComponentToNewComponentMapping[component?.properties?.buttonToSubmit?.value];
|
||||
if (newButtonToSubmitValue) set(component, 'properties.buttonToSubmit.value', newButtonToSubmitValue);
|
||||
}
|
||||
|
||||
if (!parentId) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ export class AuthService {
|
|||
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
// Check if the configs allows user signups
|
||||
if (this.configService.get<string>('DISABLE_SIGNUPS') === 'true') {
|
||||
if (!organizationId && this.configService.get<string>('DISABLE_SIGNUPS') === 'true') {
|
||||
throw new NotAcceptableException();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export class OrganizationsService {
|
|||
enable_sign_up: this.configService.get<string>('DISABLE_SIGNUPS') !== 'true',
|
||||
enabled: true,
|
||||
},
|
||||
enableSignUp: this.configService.get<string>('SSO_DISABLE_SIGNUPS') !== 'true',
|
||||
enableSignUp: this.configService.get<string>('DISABLE_SIGNUPS') !== 'true',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ import { EventsService } from './events_handler.service';
|
|||
import { Component } from 'src/entities/component.entity';
|
||||
import { Layout } from 'src/entities/layout.entity';
|
||||
import { EventHandler } from 'src/entities/event_handler.entity';
|
||||
import { findAllEntityReferences, isValidUUID, updateEntityReferences } from 'src/helpers/import_export.helpers';
|
||||
import { updateEntityReferences } from 'src/helpers/import_export.helpers';
|
||||
import { isEmpty } from 'class-validator';
|
||||
import { PageHelperService } from '@apps/services/pages/service.helper';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class PageService {
|
||||
|
|
@ -101,19 +102,25 @@ export class PageService {
|
|||
const componentsIdMap = {};
|
||||
|
||||
// Clone components
|
||||
// array to store maapings and update them later with path
|
||||
const mappingsToUpdate = [];
|
||||
const clonedComponents = await Promise.all(
|
||||
pageComponents.map(async (component) => {
|
||||
const clonedComponent = { ...component, id: undefined, pageId: clonePageId };
|
||||
const newComponent = await manager.save(manager.create(Component, clonedComponent));
|
||||
|
||||
componentsIdMap[component.id] = newComponent.id;
|
||||
const componentLayouts = await manager.find(Layout, { where: { componentId: component.id } });
|
||||
if (component?.properties?.buttonToSubmit?.value) {
|
||||
mappingsToUpdate.push({
|
||||
component: newComponent,
|
||||
pathToUpdate: 'properties.buttonToSubmit.value',
|
||||
});
|
||||
}
|
||||
const clonedLayouts = componentLayouts.map((layout) => ({
|
||||
...layout,
|
||||
id: undefined,
|
||||
componentId: newComponent.id,
|
||||
}));
|
||||
|
||||
// Clone component events
|
||||
const clonedComponentEvents = await this.eventHandlerService.findAllEventsWithSourceId(component.id);
|
||||
const clonedEvents = clonedComponentEvents.map((event) => {
|
||||
|
|
@ -150,7 +157,18 @@ export class PageService {
|
|||
return newComponent;
|
||||
})
|
||||
);
|
||||
|
||||
// re estabilish mappings
|
||||
await Promise.all(
|
||||
mappingsToUpdate.map((itemToUpdate) => {
|
||||
const { component, pathToUpdate: path } = itemToUpdate;
|
||||
const oldId = _.get(component, path);
|
||||
const newId = componentsIdMap[oldId];
|
||||
if (newId) {
|
||||
_.set(component, path, newId);
|
||||
}
|
||||
manager.save(component);
|
||||
})
|
||||
);
|
||||
// Clone events
|
||||
await Promise.all(
|
||||
pageEvents.map(async (event) => {
|
||||
|
|
@ -225,13 +243,7 @@ export class PageService {
|
|||
}
|
||||
|
||||
const toUpdateComponents = clonedComponents.filter((component) => {
|
||||
const entityReferencesInComponentDefinitions = findAllEntityReferences(component, []).filter(
|
||||
(entity) => entity && isValidUUID(entity)
|
||||
);
|
||||
|
||||
if (entityReferencesInComponentDefinitions.length > 0) {
|
||||
return updateEntityReferences(component, componentsIdMap);
|
||||
}
|
||||
return updateEntityReferences(component, componentsIdMap);
|
||||
});
|
||||
|
||||
if (!isEmpty(toUpdateComponents)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue