Merge pull request #9860 from ToolJet/chore/v2.45.0-conflicts

Merge main back to develop (v2.45.0)
This commit is contained in:
Kavin Venkatachalam 2024-05-24 17:12:27 +05:30 committed by GitHub
commit 879201fb1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
312 changed files with 25714 additions and 25973 deletions

View file

@ -1 +1 @@
2.44.0
2.45.0

View file

@ -4,6 +4,7 @@ export const postgreSqlSelector = {
addDatasourceLink: "[data-cy='add-datasource-link']",
allDatasourceLabelAndCount: '[data-cy="datasource-list-header"]',
commonlyUsedLabelAndCount: '[data-cy="commonlyused-datasource-button"]',
databaseLabelAndCount: '[data-cy="databases-datasource-button"]',
apiLabelAndCount: '[data-cy="apis-datasource-button"]',
cloudStorageLabelAndCount: '[data-cy="cloudstorage-datasource-button"]',

View file

@ -5,12 +5,13 @@ export const postgreSqlText = {
allDataSources: () => {
return Cypress.env("marketplace_action")
? "All data sources (44)"
: "All data sources (42)";
: "All data sources (41)";
},
commonlyUsed: "Commonly used (5)",
allDatabase: () => {
return Cypress.env("marketplace_action")
? "Databases (20)"
: "Databases (18)";
: "Databases (17)";
},
allApis: "APIs (20)",
allCloudStorage: "Cloud Storages (4)",

View file

@ -35,6 +35,10 @@ describe("Data source Azure Blob Storage", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -33,6 +33,10 @@ describe("Data source BigQuery", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -34,6 +34,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -34,6 +34,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -35,6 +35,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -33,6 +33,10 @@ describe("Data source DynamoDB", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -29,6 +29,10 @@ describe("Data source Elasticsearch", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -30,6 +30,10 @@ describe("Data source Firestore", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -37,6 +37,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -33,6 +33,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -40,6 +40,10 @@ describe("Data source MongoDB", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -39,6 +39,10 @@ describe("Data sources MySql", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -27,6 +27,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -35,6 +35,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -32,6 +32,10 @@ describe("Data source Redis", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -32,6 +32,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -33,6 +33,10 @@ describe("Data sources AWS S3", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -30,6 +30,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -26,6 +26,10 @@ describe("Data source SMTP", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -33,6 +33,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -34,6 +34,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -34,6 +34,10 @@ describe("Data sources", () => {
"have.text",
postgreSqlText.allDataSources()
);
cy.get(postgreSqlSelector.commonlyUsedLabelAndCount).should(
"have.text",
postgreSqlText.commonlyUsed
);
cy.get(postgreSqlSelector.databaseLabelAndCount).should(
"have.text",
postgreSqlText.allDatabase()

View file

@ -143,4 +143,4 @@ Under the <b>General</b> accordion, you can set the value in the string format.
| Visibility | This is to control the visibility of the component. If `{{false}}`/disabled the component will not visible after the app is deployed. By default, it's enabled (set to `{{true}}`). |
| Accent color | You can change the accent color of the column title by entering the Hex color code or choosing a color of your choice from the color picker. |
</div>
</div>

View file

@ -1 +1 @@
2.44.0
2.45.0

View file

@ -1,45 +1,4 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 488.3 488.3" style="enable-background:new 0 0 488.3 488.3;" xml:space="preserve">
<g>
<g>
<path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124
C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124
c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/>
<path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227
c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6
V38.6C439.65,17.3,422.35,0,401.05,0z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6429 6.5H8V6C8 3.79086 9.79086 2 12 2H18C20.2091 2 22 3.79086 22 6V12C22 14.2091 20.2091 16 18 16H17.5V11.3571C17.5 8.67462 15.3254 6.5 12.6429 6.5Z" fill="#6A727C"/>
<path d="M12 22H6C3.79086 22 2 20.2091 2 18V12C2 9.79086 3.79086 8 6 8H12C14.2091 8 16 9.79086 16 12V18C16 20.2091 14.2091 22 12 22Z" fill="#6A727C"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 438 B

View file

@ -1,3 +1,3 @@
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.54419 0H2.54419C1.43962 0 0.544189 0.895431 0.544189 2V8C0.544189 9.10457 1.43962 10 2.54419 10H8.54419C9.64876 10 10.5442 9.10457 10.5442 8V2C10.5442 0.895431 9.64876 0 8.54419 0ZM5.66919 3C5.66919 3.20711 5.83708 3.375 6.04419 3.375H6.63886L3.91919 6.09467V5.5C3.91919 5.29289 3.7513 5.125 3.54419 5.125C3.33708 5.125 3.16919 5.29289 3.16919 5.5V7C3.16919 7.20711 3.33708 7.375 3.54419 7.375H5.04419C5.2513 7.375 5.41919 7.20711 5.41919 7C5.41919 6.79289 5.2513 6.625 5.04419 6.625H4.44952L7.16919 3.90533V4.5C7.16919 4.70711 7.33708 4.875 7.54419 4.875C7.7513 4.875 7.91919 4.70711 7.91919 4.5V3C7.91919 2.79289 7.7513 2.625 7.54419 2.625H6.04419C5.83708 2.625 5.66919 2.79289 5.66919 3Z" fill="#11181C"/>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0H2C0.895431 0 0 0.895431 0 2V8C0 9.10457 0.895431 10 2 10H8C9.10457 10 10 9.10457 10 8V2C10 0.895431 9.10457 0 8 0ZM5.125 3C5.125 3.20711 5.29289 3.375 5.5 3.375H6.09467L3.375 6.09467V5.5C3.375 5.29289 3.20711 5.125 3 5.125C2.79289 5.125 2.625 5.29289 2.625 5.5V7C2.625 7.20711 2.79289 7.375 3 7.375H4.5C4.70711 7.375 4.875 7.20711 4.875 7C4.875 6.79289 4.70711 6.625 4.5 6.625H3.90533L6.625 3.90533V4.5C6.625 4.70711 6.79289 4.875 7 4.875C7.20711 4.875 7.375 4.70711 7.375 4.5V3C7.375 2.79289 7.20711 2.625 7 2.625H5.5C5.29289 2.625 5.125 2.79289 5.125 3Z" fill="#889099"/>
</svg>

Before

Width:  |  Height:  |  Size: 863 B

After

Width:  |  Height:  |  Size: 730 B

View file

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.99984 11.8333C9.2215 11.8333 11.8332 9.22163 11.8332 5.99996C11.8332 2.7783 9.2215 0.166626 5.99984 0.166626C2.77818 0.166626 0.166504 2.7783 0.166504 5.99996C0.166504 9.22163 2.77818 11.8333 5.99984 11.8333ZM4.74984 7.97913C4.46219 7.97913 4.229 8.21231 4.229 8.49996C4.229 8.78763 4.46219 9.02079 4.74984 9.02079H7.24984C7.53749 9.02079 7.77067 8.78763 7.77067 8.49996C7.77067 8.21231 7.53749 7.97913 7.24984 7.97913H6.52067V5.58329C6.52067 5.29564 6.28749 5.06246 5.99984 5.06246H5.1665C4.87885 5.06246 4.64567 5.29564 4.64567 5.58329C4.64567 5.87094 4.87885 6.10413 5.1665 6.10413H5.479V7.97913H4.74984ZM6.83317 3.49996C6.83317 3.96019 6.46007 4.33329 5.99984 4.33329C5.5396 4.33329 5.1665 3.96019 5.1665 3.49996C5.1665 3.03973 5.5396 2.66663 5.99984 2.66663C6.46007 2.66663 6.83317 3.03973 6.83317 3.49996Z" fill="#D72D39"/>
</svg>

After

Width:  |  Height:  |  Size: 985 B

21773
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.12.0",
"@codemirror/commands": "^6.3.3",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-python": "^6.1.3",
"@codemirror/lang-sass": "^6.0.2",
"@codemirror/lang-sql": "^6.5.5",
"@codemirror/language": "^6.10.0",
"@codemirror/view": "^6.24.0",
"@dnd-kit/core": "^6.0.7",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
@ -17,9 +25,14 @@
"@sentry/tracing": "^7.100.1",
"@sentry/webpack-plugin": "^2.14.0",
"@tabler/icons-react": "^2.4.0",
"@textea/json-viewer": "^3.3.2",
"@tooljet/plugins": "../plugins",
"@uiw/react-codemirror": "^3.0.6",
"@uiw/codemirror-theme-github": "^4.21.21",
"@uiw/codemirror-theme-okaidia": "^4.21.21",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/react-codemirror": "^4.21.21",
"@y-presence/react": "^2.0.1",
"acorn": "^8.11.3",
"array-move": "^4.0.0",
"axios": "^1.3.3",
"bootstrap": "^5.2.3",
@ -82,6 +95,7 @@
"react-loading-skeleton": "^3.1.1",
"react-markdown": "^9.0.0",
"react-mentions": "^4.4.7",
"react-moveable": "^0.54.1",
"react-multi-select-component": "^4.3.4",
"react-pdf": "^6.2.2",
"react-phone-input-2": "^2.15.1",

View file

@ -1,41 +1,11 @@
import React, { useEffect } from 'react';
import React from 'react';
import { withTranslation } from 'react-i18next';
import { Editor } from '../Editor/Editor';
import { RealtimeEditor } from '@/Editor/RealtimeEditor';
import config from 'config';
import { appService } from '@/_services';
import { useAppDataActions } from '@/_stores/appDataStore';
const AppLoaderComponent = React.memo((props) => {
const [shouldLoadApp, setShouldLoadApp] = React.useState(false);
const { updateState } = useAppDataActions();
useEffect(() => {
props?.id && props?.slug && loadAppDetails(props?.id);
return () => {
setShouldLoadApp(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadAppDetails = (appId) => {
appService.fetchApp(appId, 'edit').then((data) => {
setShouldLoadApp(true);
updateState({
app: data,
appId: data.id,
});
});
};
if (!shouldLoadApp) return <></>;
return config.ENABLE_MULTIPLAYER_EDITING ? (
<RealtimeEditor {...props} shouldLoadApp={shouldLoadApp} />
) : (
<Editor {...props} />
);
return config.ENABLE_MULTIPLAYER_EDITING ? <RealtimeEditor {...props} /> : <Editor {...props} />;
});
export const AppLoader = withTranslation()(AppLoaderComponent);

View file

@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import cx from 'classnames';
import { appVersionService } from '@/_services';
import { CustomSelect } from './CustomSelect';
import { toast } from 'react-hot-toast';
import { shallow } from 'zustand/shallow';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useEditorStore } from '@/_stores/editorStore';
import { useEnvironmentsAndVersionsStore } from '@/_stores/environmentsAndVersionsStore';
import { useAppDataStore } from '@/_stores/appDataStore';
import { decodeEntities } from '@/_helpers/utils';
const appVersionLoadingStatus = Object.freeze({
@ -17,7 +18,6 @@ const appVersionLoadingStatus = Object.freeze({
export const AppVersionsManager = function ({
appId,
setAppDefinitionFromVersion,
onVersionDelete,
isEditable = true,
isViewer,
darkMode,
@ -28,12 +28,11 @@ export const AppVersionsManager = function ({
versionName: '',
showModal: false,
});
const [forceMenuOpen, setForceMenuOpen] = useState(false);
const { releasedVersionId, editingVersion, appVersions, setAppVersions } = useAppVersionStore(
const { releasedVersionId, editingVersion } = useAppVersionStore(
(state) => ({
editingVersion: state.editingVersion,
appVersions: state.appVersions,
setAppVersions: state.actions?.setAppVersions,
releasedVersionId: state.releasedVersionId,
}),
shallow
@ -45,26 +44,63 @@ export const AppVersionsManager = function ({
shallow
);
const {
initializedEnvironmentDropdown,
versionsPromotedToEnvironment,
lazyLoadAppVersions,
appVersionsLazyLoaded,
setEnvironmentAndVersionsInitStatus,
changeEditorVersionAction,
selectedVersion,
deleteVersionAction,
} = useEnvironmentsAndVersionsStore(
(state) => ({
appVersionsLazyLoaded: state.appVersionsLazyLoaded,
initializedEnvironmentDropdown: state.initializedEnvironmentDropdown,
versionsPromotedToEnvironment: state.versionsPromotedToEnvironment,
selectedVersion: state.selectedVersion,
lazyLoadAppVersions: state.actions.lazyLoadAppVersions,
setEnvironmentAndVersionsInitStatus: state.actions.setEnvironmentAndVersionsInitStatus,
deleteVersionAction: state.actions.deleteVersionAction,
changeEditorVersionAction: state.actions.changeEditorVersionAction,
}),
shallow
);
useEffect(() => {
if (appVersions && appVersions.length > 0) {
setEnvironmentAndVersionsInitStatus(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (initializedEnvironmentDropdown) {
setGetAppVersionStatus(appVersionLoadingStatus.loaded);
}
return () => {
setGetAppVersionStatus(appVersionLoadingStatus.loading);
};
}, [appVersions]);
}, [initializedEnvironmentDropdown]);
const selectVersion = (id) => {
appVersionService
.getAppVersionData(appId, id)
.then((data) => {
const isCurrentVersionReleased = data.currentVersionId ? true : false;
setAppDefinitionFromVersion(data, isCurrentVersionReleased);
})
.catch((error) => {
toast.error(error);
const currentVersionId = useAppDataStore.getState().currentVersionId;
const isSameVersionSelected = currentVersionId === id;
if (isSameVersionSelected) {
return toast('You are already editing this version', {
icon: '⚠️',
});
}
changeEditorVersionAction(
appId,
id,
(newDeff) => {
setAppDefinitionFromVersion(newDeff);
},
(error) => {
toast.error(error);
}
);
return;
};
const resetDeleteModal = () => {
@ -77,29 +113,29 @@ export const AppVersionsManager = function ({
const deleteAppVersion = (versionId, versionName) => {
const deleteingToastId = toast.loading('Deleting version...');
appVersionService
.del(appId, versionId)
.then(() => {
deleteVersionAction(
appId,
versionId,
(newVersionDef) => {
if (newVersionDef) {
/* User deleted new version */
setAppDefinitionFromVersion(newVersionDef);
}
toast.dismiss(deleteingToastId);
toast.success(`Version - ${decodeEntities(versionName)} Deleted`);
resetDeleteModal();
setGetAppVersionStatus(appVersionLoadingStatus.loading);
})
.catch((error) => {
setGetAppVersionStatus(appVersionLoadingStatus.loaded);
},
(error) => {
toast.dismiss(deleteingToastId);
toast.error(error?.error ?? 'Oops, something went wrong');
setGetAppVersionStatus(appVersionLoadingStatus.error);
resetDeleteModal();
})
.finally(() => {
appVersionService.getAll(appId, true).then((data) => {
setAppVersions(data.versions);
onVersionDelete();
});
});
}
);
};
const options = appVersions.map((appVersion) => ({
const options = versionsPromotedToEnvironment.map((appVersion) => ({
value: appVersion.id,
isReleasedVersion: appVersion.id === releasedVersionId,
appVersionName: appVersion.name,
@ -139,10 +175,17 @@ export const AppVersionsManager = function ({
),
}));
const onMenuOpen = async () => {
if (!appVersionsLazyLoaded) {
setGetAppVersionStatus(appVersionLoadingStatus.loading);
await lazyLoadAppVersions(appId);
setGetAppVersionStatus(appVersionLoadingStatus.loaded);
}
setForceMenuOpen(!forceMenuOpen);
};
const customSelectProps = {
appId,
appVersions,
setAppVersions,
setAppDefinitionFromVersion,
editingVersion,
setDeleteVersion,
@ -151,10 +194,28 @@ export const AppVersionsManager = function ({
resetDeleteModal,
};
/* Force close is not working with usual blur function of react-select */
const clickedOutsideRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (clickedOutsideRef.current && !clickedOutsideRef.current.contains(event.target)) {
if (!forceMenuOpen) {
setForceMenuOpen(false);
}
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [clickedOutsideRef]);
return (
<div
className="d-flex align-items-center p-0"
style={{ margin: isViewer && currentLayout === 'mobile' ? '0px' : '0 24px' }}
ref={clickedOutsideRef}
>
<div
className={cx('d-flex version-manager-container p-0', {
@ -170,10 +231,13 @@ export const AppVersionsManager = function ({
<CustomSelect
isLoading={appVersionStatus === 'loading'}
options={options}
value={editingVersion?.id}
value={selectedVersion?.id}
onChange={(id) => selectVersion(id)}
{...customSelectProps}
isEditable={isEditable}
onMenuOpen={onMenuOpen}
onMenuClose={() => setForceMenuOpen(false)}
menuIsOpen={forceMenuOpen}
darkMode={darkMode}
/>
</div>

View file

@ -6,17 +6,25 @@ import { useTranslation } from 'react-i18next';
import Select from '@/_ui/Select';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { useEnvironmentsAndVersionsStore } from '@/_stores/environmentsAndVersionsStore';
export const CreateVersion = ({
appId,
appVersions,
setAppVersions,
setAppDefinitionFromVersion,
showCreateAppVersion,
setShowCreateAppVersion,
}) => {
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
const [versionName, setVersionName] = useState('');
const { versionsPromotedToEnvironment: appVersions, createNewVersionAction } = useEnvironmentsAndVersionsStore(
(state) => ({
appVersionsLazyLoaded: state.appVersionsLazyLoaded,
versionsPromotedToEnvironment: state.versionsPromotedToEnvironment,
lazyLoadAppVersions: state.actions.lazyLoadAppVersions,
createNewVersionAction: state.actions.createNewVersionAction,
}),
shallow
);
const { t } = useTranslation();
const { editingVersion } = useAppVersionStore(
@ -46,30 +54,29 @@ export const CreateVersion = ({
setIsCreatingVersion(true);
appVersionService
.create(appId, versionName, selectedVersion.id)
.then((data) => {
createNewVersionAction(
appId,
versionName,
selectedVersion.id,
(newVersion) => {
toast.success('Version Created');
appVersionService.getAll(appId).then((data) => {
setVersionName('');
setIsCreatingVersion(false);
setAppVersions(data.versions);
setShowCreateAppVersion(false);
});
setVersionName('');
setIsCreatingVersion(false);
setShowCreateAppVersion(false);
appVersionService
.getAppVersionData(appId, data.id)
.getAppVersionData(appId, newVersion.id)
.then((data) => {
setAppDefinitionFromVersion(data);
})
.catch((error) => {
toast.error(error);
});
})
.catch((error) => {
},
(error) => {
toast.error(error?.error);
setIsCreatingVersion(false);
});
}
);
};
return (

View file

@ -1,19 +1,19 @@
import React, { useState } from 'react';
import { appVersionService } from '@/_services';
import AlertDialog from '@/_ui/AlertDialog';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { shallow } from 'zustand/shallow';
import { useEnvironmentsAndVersionsStore } from '@/_stores/environmentsAndVersionsStore';
export const EditVersion = ({
appId,
value: editingVersionId,
setAppVersions,
setShowEditAppVersion,
showEditAppVersion,
appVersions,
}) => {
export const EditVersion = ({ appId, setShowEditAppVersion, showEditAppVersion }) => {
const [isEditingVersion, setIsEditingVersion] = useState(false);
const editingVersion = appVersions?.find((version) => version.id === editingVersionId);
const { updateVersionNameAction, selectedVersion: editingVersion } = useEnvironmentsAndVersionsStore(
(state) => ({
updateVersionNameAction: state.actions.updateVersionNameAction,
selectedVersion: state.selectedVersion,
}),
shallow
);
const [versionName, setVersionName] = useState(editingVersion?.name || '');
const { t } = useTranslation();
@ -28,21 +28,20 @@ export const EditVersion = ({
}
setIsEditingVersion(true);
appVersionService
.save(appId, editingVersionId, { name: versionName })
.then(() => {
updateVersionNameAction(
appId,
editingVersion?.id,
versionName,
() => {
toast.success('Version name updated');
appVersionService.getAll(appId).then((data) => {
const versions = data.versions;
setAppVersions(versions);
});
setIsEditingVersion(false);
setShowEditAppVersion(false);
})
.catch((error) => {
},
(error) => {
setIsEditingVersion(false);
toast.error(error?.error);
});
}
);
};
return (

View file

@ -0,0 +1,34 @@
import React from 'react';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
export default function AutoLayoutAlert({ show, onClick }) {
if (!show) {
return '';
}
return (
<div
style={{
position: 'absolute',
top: '0',
right: '0',
width: '300px',
padding: 'var(--7, 16px)',
background: 'var(--base)',
margin: '10px',
}}
className="d-flex flex-row"
>
<div className="pe-2">
<SolidIcon name="warning" fill="#E54D2E" />
</div>
<div style={{ fontSize: '12px', fontStyle: 'normal', fontWeight: '400', lineHeight: '20px' }}>
You have to disable auto alignment to manually adjust mobile components. Once disabled, the mobile layout will
not automatically align with desktop changes
<ButtonSolid size="sm" variant="tertiary" onClick={onClick} className="mt-2">
Disable auto alignment
</ButtonSolid>
</div>
</div>
);
}

View file

@ -1,381 +1,35 @@
import React, { useEffect, useState, useMemo, useContext, memo } from 'react';
import { Button } from './Components/Button';
import { Image } from './Components/Image';
import { Text } from './Components/Text';
import { Table } from './Components/Table/Table';
import { TextInput } from './Components/TextInput';
import { NumberInput } from './Components/NumberInput';
import { TextArea } from './Components/TextArea';
import { Container } from './Components/Container';
import { Tabs } from './Components/Tabs';
import { RichTextEditor } from './Components/RichTextEditor';
import { DropDown } from './Components/DropDown';
import { Checkbox } from './Components/Checkbox';
import { Datepicker } from './Components/Datepicker';
import { DaterangePicker } from './Components/DaterangePicker';
import { Multiselect } from './Components/Multiselect';
import { Modal } from './Components/Modal';
import { Chart } from './Components/Chart';
import { Map } from './Components/Map/Map';
import { QrScanner } from './Components/QrScanner/QrScanner';
import { ToggleSwitch } from './Components/Toggle';
import { RadioButton } from './Components/RadioButton';
import { StarRating } from './Components/StarRating';
import { Divider } from './Components/Divider';
import { FilePicker } from './Components/FilePicker';
import { PasswordInput } from './Components/PasswordInput';
import { Calendar } from './Components/Calendar';
import { Listview } from './Components/Listview';
import { IFrame } from './Components/IFrame';
import { CodeEditor } from './Components/CodeEditor';
import { Timer } from './Components/Timer';
import { Statistics } from './Components/Statistics';
import { Pagination } from './Components/Pagination';
import { Tags } from './Components/Tags';
import { Spinner } from './Components/Spinner';
import { CircularProgressBar } from './Components/CirularProgressbar';
import { renderTooltip, getComponentName } from '@/_helpers/appUtils';
import { RangeSlider } from './Components/RangeSlider';
import { Timeline } from './Components/Timeline';
import { SvgImage } from './Components/SvgImage';
import { Html } from './Components/Html';
import { ButtonGroup } from './Components/ButtonGroup';
import { CustomComponent } from './Components/CustomComponent/CustomComponent';
import { VerticalDivider } from './Components/verticalDivider';
import { ColorPicker } from './Components/ColorPicker';
import { KanbanBoard } from './Components/KanbanBoard/KanbanBoard';
import { Kanban } from './Components/Kanban/Kanban';
import { Steps } from './Components/Steps';
import { TreeSelect } from './Components/TreeSelect';
import { Icon } from './Components/Icon';
import { Link } from './Components/Link';
import { Form } from './Components/Form/Form';
import { BoundedBox } from './Components/BoundedBox/BoundedBox';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import '@/_styles/custom.scss';
import { validateProperties } from './component-properties-validation';
import { validateWidget } from '@/_helpers/utils';
import { componentTypes } from './WidgetManager/components';
import {
resolveProperties,
resolveStyles,
resolveGeneralProperties,
resolveGeneralStyles,
} from './component-properties-resolution';
import React from 'react';
import HydrateWithResolveReferences from './Middlewares/HydrateWithResolveReferences';
import BoxUI from './BoxUI';
import _ from 'lodash';
import { EditorContext } from '@/Editor/Context/EditorContextWrapper';
import { useTranslation } from 'react-i18next';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppInfo } from '@/_stores/appDataStore';
import { isPDFSupported } from '@/_stores/utils';
import { useEditorStore } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
export const AllComponents = {
Button,
Image,
Text,
TextInput,
NumberInput,
Table,
TextArea,
Container,
Tabs,
RichTextEditor,
DropDown,
Checkbox,
Datepicker,
DaterangePicker,
Multiselect,
Modal,
Chart,
Map,
QrScanner,
ToggleSwitch,
RadioButton,
StarRating,
Divider,
FilePicker,
PasswordInput,
Calendar,
IFrame,
CodeEditor,
Listview,
Timer,
Statistics,
Pagination,
Tags,
Spinner,
CircularProgressBar,
RangeSlider,
Timeline,
SvgImage,
Html,
ButtonGroup,
CustomComponent,
VerticalDivider,
ColorPicker,
KanbanBoard,
Kanban,
Steps,
TreeSelect,
Link,
Icon,
Form,
BoundedBox,
};
/**
* Conditionally importing PDF component since importing it breaks app in older versions of browsers.
* refer: https://github.com/wojtekmaj/react-pdf?tab=readme-ov-file#compatibility
**/
if (isPDFSupported()) {
AllComponents.PDF = await import('./Components/PDF').then((module) => module.PDF);
function deepEqualityCheckusingLoDash(obj1, obj2) {
return _.isEqual(obj1, obj2);
}
export const Box = memo(
({
id,
width,
height,
yellow,
preview,
component,
inCanvas,
onComponentClick,
onEvent,
onComponentOptionChanged,
onComponentOptionsChanged,
paramUpdated,
changeCanDrag,
containerProps,
removeComponent,
canvasWidth,
mode,
customResolvables,
parentId,
sideBarDebugger,
readOnly,
childComponents,
isResizing,
adjustHeightBasedOnAlignment,
currentLayout,
darkMode,
}) => {
const { t } = useTranslation();
const backgroundColor = yellow ? 'yellow' : '';
const currentState = useCurrentState();
const { events } = useAppInfo();
const shouldAddBoxShadowAndVisibility = ['TextInput', 'PasswordInput', 'NumberInput', 'Text'];
export const shouldUpdate = (prevProps, nextProps) => {
return (
deepEqualityCheckusingLoDash(prevProps?.id, nextProps?.id) &&
deepEqualityCheckusingLoDash(prevProps?.component?.definition, nextProps?.component?.definition) &&
prevProps?.width === nextProps?.width &&
prevProps?.height === nextProps?.height
);
};
const componentMeta = useMemo(() => {
return componentTypes.find((comp) => component.component === comp.component);
}, [component]);
export const Box = (props) => {
const { id, component, mode, customResolvables } = props;
const ComponentToRender = AllComponents[component.component];
const [renderCount, setRenderCount] = useState(0);
const [renderStartTime, setRenderStartTime] = useState(new Date());
const [resetComponent, setResetStatus] = useState(false);
/**
* !This component does not consume the value returned from the below hook.
* Only purpose of the hook is to force one rerender the component
* */
useEditorStore((state) => state.componentsNeedsUpdateOnNextRender.find((compId) => compId === id), shallow);
const resolvedProperties = resolveProperties(component, currentState, null, customResolvables);
const [validatedProperties, propertyErrors] =
mode === 'edit' && component.validate
? validateProperties(resolvedProperties, componentMeta.properties)
: [resolvedProperties, []];
if (shouldAddBoxShadowAndVisibility.includes(component.component)) {
validatedProperties.visibility = validatedProperties.visibility !== false ? true : false;
}
const resolvedStyles = resolveStyles(component, currentState, null, customResolvables);
const [validatedStyles, styleErrors] =
mode === 'edit' && component.validate
? validateProperties(resolvedStyles, componentMeta.styles)
: [resolvedStyles, []];
if (!shouldAddBoxShadowAndVisibility.includes(component.component)) {
validatedStyles.visibility = validatedStyles.visibility !== false ? true : false;
}
const resolvedGeneralProperties = resolveGeneralProperties(component, currentState, null, customResolvables);
const [validatedGeneralProperties, generalPropertiesErrors] =
mode === 'edit' && component.validate
? validateProperties(resolvedGeneralProperties, componentMeta.general)
: [resolvedGeneralProperties, []];
const resolvedGeneralStyles = resolveGeneralStyles(component, currentState, null, customResolvables);
const [validatedGeneralStyles, generalStylesErrors] =
mode === 'edit' && component.validate
? validateProperties(resolvedGeneralStyles, componentMeta.generalStyles)
: [resolvedGeneralStyles, []];
const { variablesExposedForPreview, exposeToCodeHinter } = useContext(EditorContext) || {};
let styles = {
height: '100%',
};
if (inCanvas) {
styles = {
...styles,
};
}
useEffect(() => {
if (!component?.parent) {
onComponentOptionChanged && onComponentOptionChanged(component, 'id', id);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); /*computeComponentState was not getting the id on initial render therefore exposed variables were not set.
computeComponentState was being executed before addNewWidgetToTheEditor was completed.*/
useEffect(() => {
const currentPage = currentState?.page;
const componentName = getComponentName(currentState, id);
const errorLog = Object.fromEntries(
[...propertyErrors, ...styleErrors, ...generalPropertiesErrors, ...generalStylesErrors].map((error) => [
`${componentName} - ${error.property}`,
{
page: currentPage,
type: 'component',
kind: 'component',
strace: 'page_level',
data: { message: `${error.message}`, status: true },
resolvedProperties: resolvedProperties,
effectiveProperties: validatedProperties,
},
])
);
sideBarDebugger?.error(errorLog);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ propertyErrors, styleErrors, generalPropertiesErrors })]);
useEffect(() => {
setRenderCount(renderCount + 1);
if (renderCount > 10) {
setRenderCount(0);
const currentTime = new Date();
const timeDifference = Math.abs(currentTime - renderStartTime);
if (timeDifference < 1000) {
throw Error;
}
setRenderStartTime(currentTime);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ resolvedProperties, resolvedStyles })]);
useEffect(() => {
if (customResolvables && !readOnly && mode === 'edit') {
const newCustomResolvable = {};
newCustomResolvable[id] = { ...customResolvables };
exposeToCodeHinter((prevState) => ({ ...prevState, ...newCustomResolvable }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(customResolvables), readOnly]);
useEffect(() => {
if (resetComponent) setResetStatus(false);
}, [resetComponent]);
let exposedVariables = currentState?.components[component.name] ?? {};
const fireEvent = (eventName, options) => {
if (mode === 'edit' && eventName === 'onClick') {
onComponentClick(id, component);
}
const componentEvents = events.filter((event) => event.sourceId === id);
onEvent(eventName, componentEvents, { ...options, customVariables: { ...customResolvables } });
};
const validate = (value) =>
validateWidget({
...{ widgetValue: value },
...{ validationObject: component.definition.validation, currentState },
customResolveObjects: customResolvables,
});
const shouldHideWidget = component.component === 'PDF' && !isPDFSupported();
return (
<OverlayTrigger
placement={inCanvas ? 'auto' : 'top'}
delay={{ show: 500, hide: 0 }}
trigger={
inCanvas && shouldAddBoxShadowAndVisibility.includes(component.component)
? !validatedProperties.tooltip?.toString().trim()
? null
: ['hover', 'focus']
: !validatedGeneralProperties.tooltip?.toString().trim()
? null
: ['hover', 'focus']
}
overlay={(props) =>
renderTooltip({
props,
text: inCanvas
? `${
shouldAddBoxShadowAndVisibility.includes(component.component)
? validatedProperties.tooltip
: validatedGeneralProperties.tooltip
}`
: `${t(`widget.${component.name}.description`, component.description)}`,
})
}
>
<div
style={{
...styles,
backgroundColor,
padding: validatedStyles?.padding == 'none' ? '0px' : '2px', //chart and image has a padding property other than container padding
}}
role={preview ? 'BoxPreview' : 'Box'}
>
{!resetComponent && !shouldHideWidget ? (
<ComponentToRender
onComponentClick={onComponentClick}
onComponentOptionChanged={onComponentOptionChanged}
currentState={currentState}
onEvent={onEvent}
id={id}
paramUpdated={paramUpdated}
width={width}
changeCanDrag={changeCanDrag}
onComponentOptionsChanged={onComponentOptionsChanged}
height={height}
component={component}
containerProps={containerProps}
darkMode={darkMode}
removeComponent={removeComponent}
canvasWidth={canvasWidth}
properties={validatedProperties}
exposedVariables={exposedVariables}
styles={{
...validatedStyles,
...(!shouldAddBoxShadowAndVisibility.includes(component.component)
? { boxShadow: validatedGeneralStyles?.boxShadow }
: {}),
}}
setExposedVariable={(variable, value) => onComponentOptionChanged(component, variable, value, id)}
setExposedVariables={(variableSet) =>
onComponentOptionsChanged(component, Object.entries(variableSet), id)
}
fireEvent={fireEvent}
validate={validate}
parentId={parentId}
customResolvables={customResolvables}
variablesExposedForPreview={variablesExposedForPreview}
exposeToCodeHinter={exposeToCodeHinter}
setProperty={(property, value) => {
paramUpdated(id, property, { value });
}}
mode={mode}
resetComponent={() => setResetStatus(true)}
childComponents={childComponents}
dataCy={`draggable-widget-${String(component.name).toLowerCase()}`}
isResizing={isResizing}
adjustHeightBasedOnAlignment={adjustHeightBasedOnAlignment}
currentLayout={currentLayout}
></ComponentToRender>
) : (
<></>
)}
</div>
</OverlayTrigger>
);
}
);
return (
<HydrateWithResolveReferences id={id} mode={mode} component={component} customResolvables={customResolvables}>
<BoxUI {...props} />
</HydrateWithResolveReferences>
);
};

View file

@ -33,7 +33,7 @@ export const BoxDragPreview = memo(function BoxDragPreview({ item, canvasWidth }
>
<div
style={{
background: '#438fd7',
background: '#D9E2FC',
opacity: '0.7',
height: '100%',
width: '100%',

View file

@ -0,0 +1,182 @@
import React, { useContext, useEffect } from 'react';
import ControlledComponentToRender from './ControlledComponentToRender';
import { renderTooltip, onComponentOptionChanged, onComponentOptionsChanged } from '@/_helpers/appUtils';
import { useTranslation } from 'react-i18next';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import '@/_styles/custom.scss';
import { EditorContext } from './Context/EditorContextWrapper';
import { validateWidget } from '@/_helpers/utils';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppDataStore } from '@/_stores/appDataStore';
import _ from 'lodash';
const shouldAddBoxShadowAndVisibility = ['TextInput', 'PasswordInput', 'NumberInput', 'Text'];
const BoxUI = (props) => {
const { t } = useTranslation();
const {
inCanvas,
component,
properties,
styles,
generalProperties,
generalStyles,
mode,
onComponentClick,
onEvent,
id,
getContainerProps,
paramUpdated,
width,
height,
changeCanDrag,
removeComponent,
canvasWidth,
parentId,
customResolvables,
currentLayout,
readOnly,
currentPageId,
onOptionChanged,
onOptionsChanged,
isFromSubContainer,
childComponents,
darkMode,
} = props;
const { variablesExposedForPreview, exposeToCodeHinter } = useContext(EditorContext) || {};
const currentState = useCurrentState();
const validate = (value) =>
validateWidget({
...{ widgetValue: value },
...{ validationObject: component.definition.validation, currentState },
customResolveObjects: customResolvables,
});
useEffect(() => {
if (customResolvables && !readOnly && mode === 'edit') {
const newCustomResolvable = {};
newCustomResolvable[id] = { ...customResolvables };
exposeToCodeHinter((prevState) => ({ ...prevState, ...newCustomResolvable }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(customResolvables), readOnly]);
let exposedVariables = !_.isEmpty(currentState?.components) ? currentState?.components[component.name] ?? {} : {};
const fireEvent = (eventName, options) => {
if (mode === 'edit' && eventName === 'onClick') {
onComponentClick(id, component);
}
const componentEvents = useAppDataStore.getState().events.filter((event) => event.sourceId === id);
onEvent(eventName, componentEvents, { ...options, customVariables: { ...customResolvables } });
};
let _styles = {
height: '100%',
};
useEffect(() => {
if (!component?.parent) {
onComponentOptionChanged(component, 'id', id);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<OverlayTrigger
placement={inCanvas ? 'auto' : 'top'}
delay={{ show: 500, hide: 0 }}
trigger={
inCanvas && shouldAddBoxShadowAndVisibility.includes(component.component)
? !properties.tooltip?.toString().trim()
? null
: ['hover', 'focus']
: !generalProperties.tooltip?.toString().trim()
? null
: ['hover', 'focus']
}
overlay={(props) =>
renderTooltip({
props,
text: inCanvas
? `${
shouldAddBoxShadowAndVisibility.includes(component.component)
? properties.tooltip
: generalProperties.tooltip
}`
: `${t(`widget.${component.name}.description`, component.description)}`,
})
}
>
<div
style={{
..._styles,
padding: styles?.padding == 'none' ? '0px' : '2px', //chart and image has a padding property other than container padding
}}
role={'Box'}
>
<ControlledComponentToRender
componentName={component.component}
onComponentClick={onComponentClick}
onEvent={onEvent}
id={id}
paramUpdated={paramUpdated}
width={width}
changeCanDrag={changeCanDrag}
onComponentOptionChanged={isFromSubContainer ? onOptionChanged : onComponentOptionChanged}
onComponentOptionsChanged={isFromSubContainer ? onOptionsChanged : onComponentOptionsChanged}
setExposedVariable={(variable, value) =>
isFromSubContainer
? onOptionChanged(component, variable, value, id)
: onComponentOptionChanged(component, variable, value, id)
}
setExposedVariables={(variableSet) => {
if (isFromSubContainer) {
onOptionsChanged(component, Object.entries(variableSet), id);
} else {
onComponentOptionsChanged(component, Object.entries(variableSet), id);
}
}}
height={height}
component={component}
containerProps={getContainerProps(id)}
darkMode={darkMode}
removeComponent={removeComponent}
canvasWidth={canvasWidth}
properties={properties}
exposedVariables={exposedVariables}
styles={{
...styles,
...(!shouldAddBoxShadowAndVisibility.includes(component.component)
? { boxShadow: generalStyles?.boxShadow }
: {}),
}}
fireEvent={fireEvent}
validate={validate}
parentId={parentId}
customResolvables={customResolvables}
variablesExposedForPreview={variablesExposedForPreview}
exposeToCodeHinter={exposeToCodeHinter}
setProperty={(property, value) => {
paramUpdated(id, property, { value });
}}
mode={mode}
// resetComponent={() => setResetStatus(true)}
dataCy={`draggable-widget-${String(component.name).toLowerCase()}`}
currentLayout={currentLayout}
currentState={currentState}
currentPageId={currentPageId}
getContainerProps={component.component === 'Form' ? getContainerProps : null}
childComponents={childComponents}
/>
</div>
</OverlayTrigger>
);
};
export default BoxUI;

View file

@ -1,6 +1,5 @@
import React, { useState } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import 'codemirror/theme/duotone-light.css';
import { componentTypes } from '../WidgetManager/components';
import { DataSourceTypes } from '../DataSourceManager/SourceComponents';
import { debounce } from 'lodash';
@ -96,7 +95,6 @@ export function CodeBuilder({ initialValue, onChange, components }) {
return { item: item };
});
} else {
console.log(currentWord);
filteredVariables = fuse.search(currentWord);
}
return filteredVariables.map((variable) => renderVariable(type, key, variable.item.name));

View file

@ -1,663 +0,0 @@
import React, { useEffect, useState, useRef, useContext } from 'react';
import { useSpring, config, animated } from 'react-spring';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Tooltip from 'react-bootstrap/Tooltip';
import CodeMirror from '@uiw/react-codemirror';
import 'codemirror/mode/handlebars/handlebars';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/sql/sql';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/search/match-highlighter';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/theme/base16-light.css';
import 'codemirror/theme/duotone-light.css';
import 'codemirror/theme/monokai.css';
import { onBeforeChange, handleChange } from './utils';
import { resolveReferences, hasCircularDependency, handleCircularStructureToJSON } from '@/_helpers/utils';
import useHeight from '@/_hooks/use-height-transition';
import usePortal from '@/_hooks/use-portal';
import { Color } from './Elements/Color';
import { Json } from './Elements/Json';
import { Select } from './Elements/Select';
import { Toggle } from './Elements/Toggle';
import { AlignButtons } from './Elements/AlignButtons';
import { TypeMapping } from './TypeMapping';
import { Number } from './Elements/Number';
import { BoxShadow } from './Elements/BoxShadow';
import FxButton from './Elements/FxButton';
import { ToolTip } from '../Inspector/Elements/Components/ToolTip';
import { toast } from 'react-hot-toast';
import { EditorContext } from '@/Editor/Context/EditorContextWrapper';
import { camelCase } from 'lodash';
import { useTranslation } from 'react-i18next';
import cx from 'classnames';
import { Alert } from '@/_ui/Alert/Alert';
import { useCurrentState } from '@/_stores/currentStateStore';
import ClientServerSwitch from './Elements/ClientServerSwitch';
import { CodeHinterContext } from './CodeHinterContext';
import Switch from './Elements/Switch';
import Checkbox from './Elements/Checkbox';
import Slider from './Elements/Slider';
import { Input } from './Elements/Input';
import { Icon } from './Elements/Icon';
import { Visibility } from './Elements/Visibility';
import { NumberInput } from './Elements/NumberInput';
import TableRowHeightInput from './Elements/TableRowHeightInput';
import { validateProperty } from '../component-properties-validation';
const HIDDEN_CODE_HINTER_LABELS = ['Table data', 'Column data', 'Text Format', 'TextComponentTextInput'];
const AllElements = {
Color,
Json,
Toggle,
Select,
AlignButtons,
Number,
BoxShadow,
ClientServerSwitch,
Slider,
Switch,
Input,
Checkbox,
Icon,
Visibility,
NumberInput,
TableRowHeightInput,
};
export function CodeHinter({
initialValue,
onChange,
onVisibilityChange,
mode,
theme,
lineNumbers,
placeholder,
ignoreBraces,
enablePreview,
height,
minHeight,
lineWrapping,
componentName = null,
usePortalEditor = true,
className,
width = '',
paramName,
paramLabel,
type,
fieldMeta,
onFxPress,
fxActive,
component,
popOverCallback,
cyLabel = '',
callgpt = () => null,
isCopilotEnabled = false,
currentState: _currentState,
isIcon = false,
inspectorTab,
staticText,
}) {
const context = useContext(CodeHinterContext);
const darkMode = localStorage.getItem('darkMode') === 'true';
const options = {
lineNumbers: lineNumbers ?? false,
lineWrapping: lineWrapping ?? true,
singleLine: true,
mode: mode || 'handlebars',
tabSize: 2,
theme: theme ? theme : darkMode ? 'monokai' : 'default',
readOnly: false,
highlightSelectionMatches: true,
placeholder,
};
const currentState = useCurrentState();
const definedConstants = currentState?.constants;
const [realState, setRealState] = useState({ ...currentState, ..._currentState, ...context });
const [currentValue, setCurrentValue] = useState('');
const [prevCurrentValue, setPrevCurrentValue] = useState(null);
const [resolvedValue, setResolvedValue] = useState(null);
const [resolvingError, setResolvingError] = useState(null);
const [isFocused, setFocused] = useState(false);
const [heightRef, currentHeight] = useHeight();
const isPreviewFocused = useRef(false);
const [isPropertyHovered, setPropertyHovered] = useState(false);
const wrapperRef = useRef(null);
// Todo: Remove this when workspace variables are deprecated
const isWorkspaceVariable =
typeof currentValue === 'string' && (currentValue.includes('%%client') || currentValue.includes('%%server'));
const constantRegex = /{{constants\.([a-zA-Z0-9_]+)}}/g;
const slideInStyles = useSpring({
config: { ...config.stiff },
from: { opacity: 0, height: 0 },
to: {
opacity: isFocused ? 1 : 0,
height: isFocused ? currentHeight + (isWorkspaceVariable ? 30 : 0) : 0,
},
});
const { t } = useTranslation();
const { variablesExposedForPreview } = useContext(EditorContext);
const prevCountRef = useRef(false);
function getPropertyDefinition(paramName, component) {
if (component?.properties?.hasOwnProperty(`${paramName}`)) {
return component.properties?.[paramName];
} else if (component?.styles?.hasOwnProperty(`${paramName}`)) {
return component?.styles?.[paramName];
} else if (component?.general?.hasOwnProperty(`${paramName}`)) {
return component?.general?.[paramName];
} else if (component?.generalStyles?.hasOwnProperty(`${paramName}`)) {
return component?.generalStyles?.[paramName];
} else {
return {};
}
}
const checkTypeErrorInRunTime = (preview) => {
const propertyDefinition = getPropertyDefinition(paramName, component?.component);
const resolvedProperty = Object.keys(component?.component?.definition || {}).reduce((accumulator, currentKey) => {
if (
component?.component?.definition?.[currentKey]?.hasOwnProperty(paramName) ||
(paramName === 'tooltip' &&
currentKey === 'general' &&
!component?.component?.definition?.[currentKey]?.hasOwnProperty(paramName))
//added second condition because initilly general is empty object and hence it was not going inside if statement and thus codehinter was always receiving undefined for initial render and thus showing error message in the preview
) {
accumulator[`${paramName}`] = resolveReferences(preview, currentState);
}
return accumulator;
}, {});
const [_valid, errorMessages] = validateProperty(resolvedProperty, propertyDefinition, paramName);
return [_valid, errorMessages];
};
const getPreviewAndErrorFromValue = (value) => {
const customResolvables = getCustomResolvables();
const invalidConstants = verifyConstant(value);
if (invalidConstants?.length) {
return [value, `undefined constants: ${invalidConstants}`];
}
const [preview, error] = resolveReferences(value, realState, null, customResolvables, true, true);
return [preview, error];
};
const verifyConstant = (value) => {
if (typeof value !== 'string') {
return [];
}
const matches = value.match(constantRegex);
if (!matches) {
return [];
}
const resolvedMatches = matches.map((match) => {
const cleanedMatch = match.replace(/{{constants\./, '').replace(/}}/, '');
return Object.keys(definedConstants).includes(cleanedMatch) ? null : cleanedMatch;
});
const invalidConstants = resolvedMatches?.filter((item) => item != null);
if (invalidConstants?.length) {
return invalidConstants;
}
};
useEffect(() => {
setCurrentValue(initialValue);
const [preview, error] = getPreviewAndErrorFromValue(initialValue);
const [_valid] = checkTypeErrorInRunTime(preview);
if (!_valid || error) setResolvingError(true);
return () => {
setPrevCurrentValue(null);
setResolvedValue(null);
setResolvingError(null);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const newState = { ...currentState, ..._currentState, ...context };
setRealState(newState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify([currentState.components, _currentState, context])]);
useEffect(() => {
const handleClickOutside = (event) => {
if (isOpen) {
return;
}
if (wrapperRef.current && isFocused && !wrapperRef.current.contains(event.target) && prevCountRef.current) {
isPreviewFocused.current = false;
setFocused(false);
prevCountRef.current = false;
} else if (isFocused) {
prevCountRef.current = true;
} else if (!isFocused && prevCountRef.current) prevCountRef.current = false;
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [wrapperRef, isFocused, isPreviewFocused, currentValue, prevCountRef, isOpen]);
const updatePreview = () => {
let globalPreviewCopy = null;
let globalErrorCopy = null;
const [preview, error] = getPreviewAndErrorFromValue(currentValue);
setPrevCurrentValue(currentValue);
const [_valid, errorMessages] = checkTypeErrorInRunTime(preview);
setPrevCurrentValue(currentValue);
if (error || !_valid || typeof preview === 'function') {
globalPreviewCopy = null;
globalErrorCopy = error || errorMessages?.[errorMessages?.length - 1];
setResolvingError(error || errorMessages?.[errorMessages?.length - 1]);
setResolvedValue(null);
} else {
globalPreviewCopy = preview;
globalErrorCopy = null;
setResolvingError(null);
setResolvedValue(preview);
}
return [globalPreviewCopy, globalErrorCopy];
};
useEffect(() => {
let [globalPreviewCopy, globalErrorCopy] = enablePreview ? updatePreview() : [null, null];
return () => {
if (enablePreview) {
setPrevCurrentValue(null);
setResolvedValue(globalPreviewCopy);
setResolvingError(globalErrorCopy);
}
};
}, [JSON.stringify({ currentValue, realState, isFocused, context })]);
// eslint-disable-next-line react-hooks/exhaustive-deps
// }, [JSON.stringify({ currentValue, realState, isFocused, context })]);
function valueChanged(editor, onChange, ignoreBraces) {
if (editor.getValue()?.trim() !== currentValue) {
handleChange(editor, onChange, ignoreBraces, realState, componentName, getCustomResolvables());
setCurrentValue(editor.getValue()?.trim());
}
}
const getPreviewContent = (content, type) => {
try {
switch (type) {
case 'object':
return JSON.stringify(content);
case 'boolean':
return content.toString();
default:
return content;
}
} catch (e) {
return undefined;
}
};
const focusPreview = () => (isPreviewFocused.current = true);
const unFocusPreview = () => (isPreviewFocused.current = false);
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text);
toast.success('Copied to clipboard');
};
const getCustomResolvables = () => {
if (variablesExposedForPreview.hasOwnProperty(component?.id)) {
if (component?.component?.component === 'Table' && fieldMeta?.name) {
return {
...variablesExposedForPreview[component?.id],
cellValue: variablesExposedForPreview[component?.id]?.rowData?.[fieldMeta?.name],
rowData: { ...variablesExposedForPreview[component?.id]?.rowData },
};
}
return variablesExposedForPreview[component.id];
}
return {};
};
const getPreview = () => {
if (!enablePreview) return;
const themeCls = darkMode ? 'bg-dark py-1' : 'bg-light py-1';
const preview = resolvedValue;
const error = resolvingError;
if (resolvingError !== null && resolvedValue === null && error) {
const err = String(error);
const errorMessage = err.includes('.run()')
? `${err} in ${componentName ? componentName.split('::')[0] + "'s" : 'fx'} field`
: err;
return (
<animated.div className={isOpen ? themeCls : null} style={{ ...slideInStyles, overflow: 'hidden' }}>
<div ref={heightRef} className="dynamic-variable-preview bg-red-lt px-1 py-1">
<div>
<div className="heading my-1">
<span>Error</span>
</div>
{errorMessage}
</div>
</div>
</animated.div>
);
}
let previewType = typeof preview;
let previewContent = preview;
if (hasCircularDependency(preview)) {
previewContent = JSON.stringify(preview, handleCircularStructureToJSON());
previewType = typeof previewContent;
}
const content = getPreviewContent(previewContent, previewType);
return (
<animated.div
className={isOpen ? themeCls : null}
style={{ ...slideInStyles, overflow: 'hidden' }}
onMouseEnter={() => focusPreview()}
onMouseLeave={() => unFocusPreview()}
>
<div ref={heightRef} className="dynamic-variable-preview bg-green-lt px-1 py-1">
<div>
<div className="d-flex my-1">
<div className="flex-grow-1" style={{ fontWeight: 700, textTransform: 'capitalize' }}>
{previewType}
</div>
{isFocused && (
<div className="preview-icons position-relative">
<CodeHinter.PopupIcon callback={() => copyToClipboard(content)} icon="copy" tip="Copy to clipboard" />
</div>
)}
</div>
{content}
</div>
</div>
{/* Todo: Remove this when workspace variables are deprecated */}
{enablePreview && isWorkspaceVariable && (
<CodeHinter.DepericatedAlertForWorkspaceVariable text={'Deprecating soon'} />
)}
</animated.div>
);
};
enablePreview = enablePreview ?? true;
const [isOpen, setIsOpen] = React.useState(false);
const handleToggle = () => {
const changeOpen = (newOpen) => {
setIsOpen(newOpen);
if (typeof popOverCallback === 'function') popOverCallback(newOpen);
};
if (!isOpen) {
changeOpen(true);
}
return new Promise((resolve) => {
const element = document.getElementsByClassName('portal-container');
if (element) {
const checkPortalExits = element[0]?.classList.contains(componentName);
if (checkPortalExits === false) {
const parent = element[0].parentNode;
parent.removeChild(element[0]);
}
changeOpen(false);
resolve();
}
}).then(() => {
changeOpen(true);
forceUpdate();
});
};
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
const defaultClassName =
className === 'query-hinter' || className === 'custom-component' || undefined ? '' : 'code-hinter';
const ElementToRender = AllElements[TypeMapping[type]];
const [forceCodeBox, setForceCodeBox] = useState(fxActive);
const codeShow = (type ?? 'code') === 'code' || forceCodeBox;
cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : cyLabel;
const fxBtn = () => (
<div className="col-auto pt-0 fx-common">
{![
'Type',
'selectRowOnCellEdit',
'Select row on cell edit',
' ',
'Padding',
'Width',
'Make all columns editable',
].includes(paramLabel) && ( //add some key if these extends
<FxButton
active={codeShow}
onPress={() => {
if (codeShow) {
setForceCodeBox(false);
onFxPress(false);
} else {
setForceCodeBox(true);
onFxPress(true);
}
}}
dataCy={cyLabel}
/>
)}
</div>
);
const _renderFxBtn = () => {
if (inspectorTab === 'styles') {
return isPropertyHovered || codeShow ? fxBtn() : null;
} else {
return fxBtn();
}
};
const onFocusHandler = () => {
setFocused(true);
updatePreview();
};
return (
<div
ref={wrapperRef}
className={cx({ 'codeShow-active': codeShow, 'd-flex': paramLabel == 'Tooltip' })}
onMouseEnter={() => setPropertyHovered(true)}
onMouseLeave={() => setPropertyHovered(false)}
>
<div
className={cx('d-flex justify-content-between', { 'w-full': fieldMeta?.fullWidth })}
style={{
marginRight: paramLabel == 'Tooltip' && '40px',
alignItems: paramLabel == 'Tooltip' ? 'flex-start' : 'center',
}}
>
{paramLabel && !HIDDEN_CODE_HINTER_LABELS.includes(paramLabel) && (
<div className={`field ${options.className}`} data-cy={`${cyLabel}-widget-parameter-label`}>
<ToolTip
label={t(`widget.commonProperties.${camelCase(paramLabel)}`, paramLabel)}
meta={fieldMeta}
labelClass={`tj-text-xsm color-slate12 ${codeShow ? 'label-hinter-margin' : 'mb-0'} ${
darkMode && 'color-whitish-darkmode'
}`}
/>
</div>
)}
<div className={cx(`${(type ?? 'code') === 'code' ? 'd-none' : ''}`, { 'w-full': fieldMeta?.fullWidth })}>
<div
style={{ width: width, marginBottom: codeShow ? '0.5rem' : '0px' }}
className={cx('d-flex align-items-center', { 'w-full': fieldMeta?.fullWidth })}
>
{!fieldMeta?.isFxNotRequired && _renderFxBtn()}
{!codeShow && (
<ElementToRender
value={resolveReferences(initialValue, realState)}
onChange={(value) => {
if (value !== currentValue) {
onChange(value);
setCurrentValue(value);
}
}}
onVisibilityChange={(value) => {
if (value !== currentValue) {
onVisibilityChange(value);
setCurrentValue(value);
}
}}
paramName={paramName}
paramLabel={paramLabel}
forceCodeBox={() => {
setForceCodeBox(true);
onFxPress(true);
}}
meta={fieldMeta}
cyLabel={cyLabel}
isIcon={isIcon}
staticText={staticText}
component={component}
/>
)}
</div>
</div>
</div>
<div
className={`row${height === '150px' || height === '300px' ? ' tablr-gutter-x-0' : ''} custom-row`}
style={{ width: paramLabel == 'Tooltip' ? '100%' : width, display: codeShow ? 'flex' : 'none' }}
>
<div className={`col code-hinter-col`}>
<div className="d-flex">
<div className="code-hinter-wrapper position-relative" style={{ width: '100%' }}>
<div
className={`${defaultClassName} ${className || 'codehinter-default-input'} ${
paramName && resolvingError && 'border-danger'
}`}
key={componentName}
style={{
height: height || 'auto',
minHeight,
maxHeight: '320px',
overflow: 'auto',
fontSize: ' .875rem',
maxWidth: paramLabel == 'Tooltip' && '190px',
}}
data-cy={`${cyLabel}-input-field`}
>
{usePortalEditor && (
<CodeHinter.PopupIcon
callback={handleToggle}
icon="portal-open"
tip="Pop out code editor into a new window"
transformation={componentName === 'transformation'}
/>
)}
<CodeHinter.Portal
isCopilotEnabled={isCopilotEnabled}
isOpen={isOpen}
callback={setIsOpen}
componentName={componentName}
key={componentName}
customComponent={getPreview}
forceUpdate={forceUpdate}
optionalProps={{ styles: { height: 300 }, cls: className }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal' }}
dragResizePortal={true}
callgpt={callgpt}
>
<CodeMirror
value={typeof initialValue === 'string' ? initialValue : ''}
realState={realState}
scrollbarStyle={null}
height={'100%'}
onFocus={onFocusHandler}
onBlur={(editor, e) => {
e?.stopPropagation();
const value = editor?.getValue()?.trimEnd();
onChange(value);
if (!isPreviewFocused?.current) {
setFocused(false);
}
}}
onChange={(editor) => valueChanged(editor, onChange, ignoreBraces)}
onBeforeChange={(editor, change) => onBeforeChange(editor, change, ignoreBraces)}
options={options}
viewportMargin={Infinity}
/>
</CodeHinter.Portal>
</div>
{enablePreview && !isOpen && getPreview()}
</div>
</div>
</div>
</div>
</div>
);
}
const PopupIcon = ({ callback, icon, tip, transformation = false }) => {
const size = transformation ? 20 : 12;
return (
<div className="d-flex justify-content-end w-100 position-absolute" style={{ top: 0 }}>
<OverlayTrigger
trigger={['hover', 'focus']}
placement="top"
delay={{ show: 800, hide: 100 }}
overlay={<Tooltip id="button-tooltip">{tip}</Tooltip>}
>
<img
className="svg-icon m-2 popup-btn"
src={`assets/images/icons/${icon}.svg`}
width={size}
height={size}
onClick={(e) => {
e.stopPropagation();
callback();
}}
/>
</OverlayTrigger>
</div>
);
};
const Portal = ({ children, ...restProps }) => {
const renderPortal = usePortal({ children, ...restProps });
return <React.Fragment>{renderPortal}</React.Fragment>;
};
const DepericatedAlertForWorkspaceVariable = ({ text }) => {
return (
<Alert
svg="tj-info-warning"
cls="codehinter workspace-variables-alert-banner p-1 mb-0"
data-cy={``}
imgHeight={18}
imgWidth={18}
>
<div className="d-flex align-items-center">
<div class="">{text}</div>
</div>
</Alert>
);
};
CodeHinter.PopupIcon = PopupIcon;
CodeHinter.Portal = Portal;
CodeHinter.DepericatedAlertForWorkspaceVariable = DepericatedAlertForWorkspaceVariable;

View file

@ -142,7 +142,7 @@ export const BoxShadow = ({ value, onChange, cyLabel }) => {
</Popover>
);
};
const _value = `#${value.split('#')[1]}`;
const _value = `#${(value || '').split('#')[1]}`;
const outerStyles = {
width: '142px',
height: '32px',

View file

@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
function Checkbox({ value, onChange }) {
const [isChecked, setIsChecked] = useState(value); // Initial state of the checkbox
useEffect(() => {
setIsChecked(value);
}, [value]);
@ -15,7 +14,7 @@ function Checkbox({ value, onChange }) {
checked={isChecked}
onChange={() => {
setIsChecked(!isChecked); // Toggle the checkbox state
onChange(!isChecked);
onChange(`{{${!isChecked}}}`);
}}
value={isChecked}
style={{ height: '16px', width: '16px' }}

View file

@ -7,7 +7,7 @@ import * as Icons from '@tabler/icons-react';
import { VirtuosoGrid } from 'react-virtuoso';
import { Visibility } from './Visibility';
export const Icon = ({ value, onChange, onVisibilityChange, component }) => {
export const Icon = ({ value, onChange, onVisibilityChange, styleDefinition, component }) => {
const [searchText, setSearchText] = useState('');
const [showPopOver, setPopOverVisibility] = useState(false);
const iconList = useRef(Object.keys(Icons));
@ -81,7 +81,7 @@ export const Icon = ({ value, onChange, onVisibilityChange, component }) => {
return (
<>
<div className="color-picker-input icon-style-container">
<div className="color-picker-input icon-style-container" style={{ position: 'relative' }}>
<div className="p-0">
<div className="field">
<OverlayTrigger
@ -116,6 +116,7 @@ export const Icon = ({ value, onChange, onVisibilityChange, component }) => {
onChange={onChange}
onVisibilityChange={onVisibilityChange}
component={component}
styleDefinition={styleDefinition}
/>
</div>
</OverlayTrigger>

View file

@ -1,6 +1,6 @@
import React from 'react';
export const Input = ({ value, onChange, cyLabel, staticText }) => {
export const Input = ({ value, onChange, cyLabel, meta }) => {
return (
<div className="form-text">
<input
@ -16,7 +16,7 @@ export const Input = ({ value, onChange, cyLabel, staticText }) => {
}}
/>
<label for="labelId" className="static-value tj-text-xsm">
{staticText?.length > 0 ? staticText : staticText?.length == 0 ? '' : 'px'}
{meta.staticText?.length > 0 ? meta.staticText : meta.staticText?.length == 0 ? '' : 'px'}
</label>
</div>
);

View file

@ -1,6 +1,6 @@
import React from 'react';
import CodeMirror from '@uiw/react-codemirror';
import 'codemirror/theme/duotone-light.css';
// import 'codemirror/theme/duotone-light.css';
export const Json = ({ value, onChange }) => {
const jsonValue = value

View file

@ -1,6 +1,6 @@
import React from 'react';
export const NumberInput = ({ value, onChange, cyLabel, staticText }) => {
export const NumberInput = ({ value, onChange, cyLabel, meta }) => {
return (
<div className="form-text tj-number-input-element">
<input
@ -17,7 +17,7 @@ export const NumberInput = ({ value, onChange, cyLabel, staticText }) => {
autoComplete="off"
/>
<label for="labelId" className="static-value tj-text-xsm">
{staticText?.length > 0 ? staticText : staticText?.length == 0 ? '' : 'px'}
{meta.staticText?.length > 0 ? meta.staticText : meta.staticText?.length == 0 ? '' : 'px'}
</label>
</div>
);

View file

@ -5,13 +5,14 @@ import * as Slider from '@radix-ui/react-slider';
import './Slider.scss';
import { debounce } from 'lodash';
function Slider1({ value, onChange, component }) {
function Slider1({ value, onChange, component, styleDefinition }) {
const [sliderValue, setSliderValue] = useState(value ? value : 33); // Initial value of the slider
const isDisabled =
styleDefinition?.auto?.value === '{{false}}' ? false : styleDefinition?.auto?.value === '{{true}}' ? true : false;
useEffect(() => {
setSliderValue(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [component.id]);
}, [component?.id]);
const debouncedOnChange = debounce((value) => {
onChange(value);
@ -32,7 +33,7 @@ function Slider1({ value, onChange, component }) {
return (
<div className="d-flex flex-column " style={{ width: '142px', marginBottom: '16px', position: 'relative' }}>
<CustomInput
disabled={component.component.definition.styles.auto.value}
disabled={isDisabled}
value={sliderValue}
staticText="% of the field"
onInputChange={onInputChange}
@ -48,9 +49,9 @@ function Slider1({ value, onChange, component }) {
value={[sliderValue]}
onValueChange={handleSliderChange}
onValueCommit={(value) => {
onChange(value);
onChange(`{{${value}}}`);
}}
disabled={component.component.definition.styles.auto.value}
disabled={isDisabled}
>
<Slider.Track className="SliderTrack">
<Slider.Range className="SliderRange" />

View file

@ -3,7 +3,7 @@ import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
import React from 'react';
import cx from 'classnames';
const Switch = ({ value, onChange, cyLabel, meta, paramName, isIcon }) => {
const Switch = ({ value, onChange, meta }) => {
const options = meta?.options;
const defaultValue = value;
return (
@ -13,10 +13,10 @@ const Switch = ({ value, onChange, cyLabel, meta, paramName, isIcon }) => {
<ToggleGroupItem
key={option.value}
value={option.value}
isIcon={isIcon}
isIcon={meta.isIcon}
style={{ width: meta?.fullWidth ? '100%' : '67px' }}
>
{isIcon ? option?.iconName ?? '' : option?.displayName}
{meta.isIcon ? option?.iconName ?? '' : option?.displayName}
</ToggleGroupItem>
))}
</ToggleGroup>

View file

@ -1,22 +1,20 @@
import React from 'react';
import SolidIcon from '@/_ui/Icon/SolidIcons';
export const Visibility = ({ value, onVisibilityChange, component }) => {
export const Visibility = ({ onVisibilityChange, styleDefinition }) => {
const iconVisibility = styleDefinition?.iconVisibility?.value || false;
return (
<div
data-cy={`icon-visibility-button`}
className="cursor-pointer visibility-eye"
style={{ top: component.component.definition.styles.iconVisibility?.value && '42%' }}
style={{ top: iconVisibility && '42%' }}
onClick={(e) => {
e.stopPropagation();
onVisibilityChange(!component.component.definition.styles?.iconVisibility?.value);
onVisibilityChange(!iconVisibility);
}}
>
<SolidIcon
name={component.component.definition.styles?.iconVisibility?.value ? 'eye1' : 'eyedisable'}
width="20"
fill={'var(--slate8)'}
/>
<SolidIcon name={iconVisibility ? 'eye1' : 'eyedisable'} width="20" fill={'var(--slate8)'} />
</div>
);
};

View file

@ -65,7 +65,7 @@ function getResult(suggestionList, query) {
return suggestions;
}
export function getSuggestionKeys(refState, refSource) {
export function getSuggestionKeys(refState) {
const state = _.cloneDeep(refState);
const queries = state['queries'];
const actions = [
@ -135,11 +135,15 @@ export function getSuggestionKeys(refState, refSource) {
return suggestionList.push(key);
});
if (['Runjs', 'Runpy'].includes(refSource)) {
actions.forEach((action) => {
suggestionList.push(`actions.${action}()`);
});
}
// if (['Runjs', 'Runpy'].includes(refSource)) {
// actions.forEach((action) => {
// suggestionList.push(`actions.${action}()`);
// });
// }
actions.forEach((action) => {
suggestionList.push(`actions.${action}()`);
});
return suggestionList;
}

View file

@ -0,0 +1,141 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useResolveStore } from '@/_stores/resolverStore';
import { shallow } from 'zustand/shallow';
import './styles.scss';
import SingleLineCodeEditor from './SingleLineCodeEditor';
import MultiLineCodeEditor from './MultiLineCodeEditor';
import usePortal from '@/_hooks/use-portal';
import Tooltip from 'react-bootstrap/Tooltip';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import { isNumber } from 'lodash';
import { Alert } from '@/_ui/Alert/Alert';
const CODE_EDITOR_TYPE = {
fxEditor: SingleLineCodeEditor.EditorBridge,
basic: SingleLineCodeEditor,
multiline: MultiLineCodeEditor,
extendedSingleLine: SingleLineCodeEditor,
};
const CodeHinter = ({ type = 'basic', initialValue, componentName, ...restProps }) => {
const { suggestions } = useResolveStore(
(state) => ({
suggestions: state.suggestions,
}),
shallow
);
const darkMode = localStorage.getItem('darkMode') === 'true';
const [isOpen, setIsOpen] = React.useState(false);
const handleTogglePopupExapand = () => {
const changeOpen = (newOpen) => {
setIsOpen(newOpen);
if (typeof restProps?.popOverCallback === 'function') restProps?.popOverCallback(newOpen);
};
if (!isOpen) {
changeOpen(true);
}
return new Promise((resolve) => {
const element = document.getElementsByClassName('portal-container');
if (element) {
const checkPortalExits = element[0]?.classList.contains(componentName);
if (checkPortalExits === false) {
const parent = element[0].parentNode;
parent.removeChild(element[0]);
}
changeOpen(false);
resolve();
}
}).then(() => {
changeOpen(true);
forceUpdate();
});
};
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
const RenderCodeEditor = CODE_EDITOR_TYPE[type];
return (
<RenderCodeEditor
type={type}
initialValue={initialValue}
suggestions={suggestions}
darkMode={darkMode}
portalProps={{
isOpen,
setIsOpen,
handleTogglePopupExapand,
forceUpdate,
}}
componentName={componentName}
{...restProps}
/>
);
};
const Portal = ({ children, ...restProps }) => {
const renderPortal = usePortal({ children, ...restProps });
return <React.Fragment>{renderPortal}</React.Fragment>;
};
const PopupIcon = ({ callback, icon, tip, position, isMultiEditor = false }) => {
const size = 16;
const topRef = isNumber(position?.height) ? Math.floor(position?.height) - 30 : 32;
let top = isMultiEditor ? 270 : topRef > 32 ? topRef : 0;
return (
<div className="d-flex justify-content-end w-100 position-absolute codehinter-popup-icon" style={{ top: top }}>
<OverlayTrigger
trigger={['hover', 'focus']}
placement="top"
delay={{ show: 800, hide: 100 }}
overlay={<Tooltip id="button-tooltip">{tip}</Tooltip>}
>
<img
style={{ zIndex: 10000 }}
className="svg-icon m-2 popup-btn"
src={`assets/images/icons/${icon}.svg`}
width={size}
height={size}
onClick={(e) => {
e.stopPropagation();
callback();
}}
/>
</OverlayTrigger>
</div>
);
};
const DepericatedAlertForWorkspaceVariable = ({ text }) => {
return (
<Alert
svg="tj-info-warning"
cls="codehinter workspace-variables-alert-banner p-1 mb-0 mt-2"
data-cy={``}
imgHeight={18}
imgWidth={18}
>
<div className="d-flex align-items-center">
<div class="">{text}</div>
</div>
</Alert>
);
};
CodeHinter.Portal = Portal;
CodeHinter.PopupIcon = PopupIcon;
CodeHinter.DepericatedAlert = DepericatedAlertForWorkspaceVariable;
CodeHinter.propTypes = {
type: PropTypes.string.isRequired,
};
export default CodeHinter;

View file

@ -0,0 +1,43 @@
import React from 'react';
import { FxParamTypeMapping } from './utils';
import { Color } from '../CodeBuilder/Elements/Color';
import { Json } from '../CodeBuilder/Elements/Json';
import { Select } from '../CodeBuilder/Elements/Select';
import { Toggle } from '../CodeBuilder/Elements/Toggle';
import { AlignButtons } from '../CodeBuilder/Elements/AlignButtons';
import { Number } from '../CodeBuilder/Elements/Number';
import { BoxShadow } from '../CodeBuilder/Elements/BoxShadow';
import ClientServerSwitch from '../CodeBuilder/Elements/ClientServerSwitch';
import Switch from '../CodeBuilder/Elements/Switch';
import Checkbox from '../CodeBuilder/Elements/Checkbox';
import Slider from '../CodeBuilder/Elements/Slider';
import { Input } from '../CodeBuilder/Elements/Input';
import { Icon } from '../CodeBuilder/Elements/Icon';
import { Visibility } from '../CodeBuilder/Elements/Visibility';
import { NumberInput } from '../CodeBuilder/Elements/NumberInput';
const AllElements = {
Color,
Json,
Toggle,
Select,
AlignButtons,
Number,
BoxShadow,
ClientServerSwitch,
Switch,
Checkbox,
Slider,
Input,
Icon,
Visibility,
NumberInput,
};
export const DynamicFxTypeRenderer = ({ paramType, ...restProps }) => {
const componentType = FxParamTypeMapping[paramType];
const DynamicComponent = AllElements[componentType];
return <DynamicComponent {...restProps} />;
};

View file

@ -0,0 +1,273 @@
/* eslint-disable import/no-unresolved */
import React, { useContext, useEffect } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { defaultKeymap } from '@codemirror/commands';
import { keymap } from '@codemirror/view';
import { completionKeymap } from '@codemirror/autocomplete';
import { python } from '@codemirror/lang-python';
import { sql } from '@codemirror/lang-sql';
import { sass, sassCompletionSource } from '@codemirror/lang-sass';
import { okaidia } from '@uiw/codemirror-theme-okaidia';
import { githubLight } from '@uiw/codemirror-theme-github';
import { findNearestSubstring, generateHints } from './autocompleteExtensionConfig';
import ErrorBoundary from '../ErrorBoundary';
import CodeHinter from './CodeHinter';
import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext';
import { createReferencesLookup } from '@/_stores/utils';
import { PreviewBox } from './PreviewBox';
const langSupport = Object.freeze({
javascript: javascript(),
python: python(),
sql: sql(),
jsx: javascript({ jsx: true }),
css: sass(),
});
const MultiLineCodeEditor = (props) => {
const {
darkMode,
height,
initialValue,
lang,
className,
onChange,
componentName,
lineNumbers,
placeholder,
hideSuggestion,
suggestions: hints,
portalProps,
showPreview,
paramLabel = '',
} = props;
const [currentValue, setCurrentValue] = React.useState(() => initialValue);
const context = useContext(CodeHinterContext);
const { suggestionList } = createReferencesLookup(context, true);
const diffOfCurrentValue = React.useRef(null);
const handleChange = React.useCallback((val) => {
setCurrentValue(val);
const diff = val.length - currentValue.length;
if (diff > 0) {
diffOfCurrentValue.current = val.slice(-diff);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleOnBlur = () => {
setTimeout(() => {
onChange(currentValue);
}, 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
};
useEffect(() => {
setCurrentValue(initialValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lang]);
const heightInPx = typeof height === 'string' && height?.includes('px') ? height : `${height}px`;
const theme = darkMode ? okaidia : githubLight;
const langExtention = langSupport[lang] ?? null;
const setupConfig = {
lineNumbers: lineNumbers ?? true,
syntaxHighlighting: true,
bracketMatching: true,
foldGutter: true,
highlightActiveLine: false,
autocompletion: hideSuggestion ?? true,
highlightActiveLineGutter: false,
completionKeymap: true,
searchKeymap: false,
};
function autoCompleteExtensionConfig(context) {
const currentCursor = context.pos;
const currentString = context.state.doc.text;
const inputStr = currentString.join(' ');
const currentCurosorPos = currentCursor;
const nearestSubstring = findNearestSubstring(inputStr, currentCurosorPos).replace(/{{|}}/g, '');
let JSLangHints = [];
if (lang === 'javascript') {
JSLangHints = Object.keys(hints['jsHints'])
.map((key) => {
return hints['jsHints'][key]['methods'].map((hint) => ({
hint: hint,
type: 'js_method',
}));
})
.flat();
JSLangHints = JSLangHints.filter((cm) => {
let lastWordAfterDot = nearestSubstring.split('.');
lastWordAfterDot = lastWordAfterDot[lastWordAfterDot.length - 1];
if (cm.hint.includes(lastWordAfterDot)) return true;
});
}
const appHints = hints['appHints'];
let autoSuggestionList = appHints.filter((suggestion) => {
return suggestion.hint.includes(nearestSubstring);
});
const suggestions = generateHints(
[...JSLangHints, ...autoSuggestionList, ...suggestionList],
null,
nearestSubstring
).map((hint) => {
if (hint.label.startsWith('client') || hint.label.startsWith('server')) return;
delete hint['apply'];
hint.apply = (view, completion, from, to) => {
/**
* This function applies an auto-completion logic to a text editing view based on user interaction.
* It uses a pre-defined completion object and modifies the document's content accordingly.
*
* Parameters:
* - view: The editor view where the changes will be applied.
* - completion: An object containing details about the completion to be applied. Includes properties like 'label' (the text to insert) and 'type' (e.g., 'js_methods').
* - from: The initial position (index) in the document where the completion starts.
* - to: The position (index) in the document where the completion ends.
*
* Logic:
* - The function calculates the start index for the change by subtracting the length of the word to be replaced (finalQuery) from the 'from' index.
* - It configures the completion details such as where to insert the text and the exact text to insert.
* - If the completion type is 'js_methods', it adjusts the insertion point to the 'to' index and sets the cursor position after the inserted text.
* - Finally, it dispatches these configurations to the editor view to apply the changes.
*
* The dispatch configuration (dispacthConfig) includes changes and, optionally, the cursor selection position if the type is 'js_methods'.
*/
const wordToReplace = nearestSubstring;
const fromIndex = from - wordToReplace.length;
const pickedCompletionConfig = {
from: fromIndex === 1 ? 0 : fromIndex,
to: to,
insert: completion.label,
};
const dispacthConfig = {
changes: pickedCompletionConfig,
};
if (completion.type === 'js_methods') {
pickedCompletionConfig.from = to;
dispacthConfig.selection = {
anchor: pickedCompletionConfig.to + completion.label.length - 1,
};
}
view.dispatch(dispacthConfig);
};
return hint;
});
return {
from: context.pos,
options: [...suggestions],
};
}
const customKeyMaps = [...defaultKeymap, ...completionKeymap];
// eslint-disable-next-line react-hooks/exhaustive-deps
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [hints]);
const { handleTogglePopupExapand, isOpen, setIsOpen, forceUpdate } = portalProps;
let cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : props.cyLabel;
return (
<div className="code-hinter-wrapper position-relative" style={{ width: '100%' }}>
<div className={`${className} ${darkMode && 'cm-codehinter-dark-themed'}`}>
<CodeHinter.PopupIcon
callback={handleTogglePopupExapand}
icon="portal-open"
tip="Pop out code editor into a new window"
isMultiEditor={true}
/>
<CodeHinter.Portal
isCopilotEnabled={false}
isOpen={isOpen}
callback={setIsOpen}
componentName={componentName}
key={componentName}
forceUpdate={forceUpdate}
optionalProps={{ styles: { height: 300 }, cls: '' }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal' }}
dragResizePortal={true}
callgpt={null}
>
<ErrorBoundary>
<div className="codehinter-container w-100 " data-cy={`${cyLabel}-input-field`} style={{ height: '100%' }}>
<CodeMirror
value={currentValue}
placeholder={placeholder}
height={'100%'}
minHeight={heightInPx}
maxHeight={heightInPx}
width="100%"
theme={theme}
extensions={[
langExtention,
javascriptLanguage.data.of({
autocomplete: overRideFunction,
}),
python().language.data.of({
autocomplete: overRideFunction,
}),
sql().language.data.of({
autocomplete: overRideFunction,
}),
sass().language.data.of({
autocomplete: sassCompletionSource,
}),
keymap.of([...customKeyMaps]),
]}
onChange={handleChange}
onBlur={handleOnBlur}
basicSetup={setupConfig}
style={{
overflowY: 'auto',
}}
className={`codehinter-multi-line-input`}
indentWithTab={true}
/>
</div>
{showPreview && (
<div className="multiline-previewbox-wrapper">
<PreviewBox
currentValue={currentValue}
validationSchema={null}
setErrorStateActive={() => null}
componentId={null}
setErrorMessage={() => null}
/>
</div>
)}
</ErrorBoundary>
</CodeHinter.Portal>
</div>
</div>
);
};
export default MultiLineCodeEditor;

View file

@ -0,0 +1,390 @@
import React, { useEffect, useState } from 'react';
import { computeCoercion, getCurrentNodeType, resolveReferences } from './utils';
import CodeHinter from '.';
import { copyToClipboard } from '@/_helpers/appUtils';
import { Alert } from '@/_ui/Alert/Alert';
import _, { isEmpty } from 'lodash';
import { handleCircularStructureToJSON, hasCircularDependency } from '@/_helpers/utils';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import Card from 'react-bootstrap/Card';
// eslint-disable-next-line import/no-unresolved
import { JsonViewer } from '@textea/json-viewer';
export const PreviewBox = ({
currentValue,
validationSchema,
setErrorStateActive,
setErrorMessage,
customVariables,
}) => {
const [resolvedValue, setResolvedValue] = useState('');
const [error, setError] = useState(null);
const [coersionData, setCoersionData] = useState(null);
const getPreviewContent = (content, type) => {
if (content === undefined || content === null) return currentValue;
try {
switch (type) {
case 'Object':
case 'Array':
return JSON.stringify(content);
case 'Boolean':
return content.toString();
default:
return content;
}
} catch (e) {
return undefined;
}
};
let previewType = getCurrentNodeType(resolvedValue);
let previewContent = resolvedValue;
if (hasCircularDependency(resolvedValue)) {
previewContent = JSON.stringify(resolvedValue, handleCircularStructureToJSON());
previewType = typeof previewContent;
}
const ifCoersionErrorHasCircularDependency = (value) => {
if (hasCircularDependency(value)) {
return JSON.stringify(value, handleCircularStructureToJSON());
}
return value;
};
const content = getPreviewContent(previewContent, previewType);
useEffect(() => {
if (error) {
setErrorStateActive(true);
setErrorMessage(error.message);
} else {
setErrorStateActive(false);
setErrorMessage(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
useEffect(() => {
const [valid, _error, newValue, resolvedValue] = resolveReferences(currentValue, validationSchema, customVariables);
if (!validationSchema || isEmpty(validationSchema)) {
return setResolvedValue(newValue);
}
if (valid) {
const [coercionPreview, typeAfterCoercion, typeBeforeCoercion] = computeCoercion(resolvedValue, newValue);
setResolvedValue(resolvedValue);
setCoersionData({
coercionPreview,
typeAfterCoercion,
typeBeforeCoercion,
});
setError(null);
} else if (!valid && !newValue && !resolvedValue) {
const err = !error ? `Invalid value for ${validationSchema?.schema?.type}` : `${_error}`;
setError({ message: err, value: resolvedValue, type: 'Invalid' });
} else {
const jsErrorType = _error?.includes('ReferenceError')
? 'ReferenceError'
: _error?.includes('TypeError')
? 'TypeError'
: _error?.includes('SyntaxError')
? 'SyntaxError'
: 'Invalid';
const errValue = ifCoersionErrorHasCircularDependency(resolvedValue);
setError({
message: _error,
value: jsErrorType === 'Invalid' ? JSON.stringify(errValue) : resolvedValue,
type: jsErrorType,
});
setCoersionData(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentValue]);
return (
<>
<PreviewBox.RenderResolvedValue
error={error}
currentValue={currentValue}
previewType={previewType}
resolvedValue={content}
coersionData={coersionData}
withValidation={!isEmpty(validationSchema)}
/>
<CodeHinter.PopupIcon
callback={() => copyToClipboard(error ? error?.value : content)}
icon={'copy'}
tip={'Copy to clipboard'}
/>
</>
);
};
const RenderResolvedValue = ({ error, previewType, resolvedValue, coersionData, withValidation }) => {
const computeCoersionPreview = (resolvedValue, coersionData) => {
if (coersionData?.typeBeforeCoercion === coersionData?.typeAfterCoercion) return resolvedValue;
if (coersionData?.typeBeforeCoercion === 'array') {
return '[...]' + coersionData?.coercionPreview;
}
if (coersionData?.typeBeforeCoercion === 'object') {
return '{...}' + coersionData?.coercionPreview;
}
return resolvedValue + coersionData?.coercionPreview;
};
const previewValueType =
withValidation || (coersionData && coersionData?.typeBeforeCoercion)
? `${coersionData?.typeBeforeCoercion} ${
coersionData?.coercionPreview ? `${coersionData?.typeAfterCoercion}` : ''
}`
: previewType;
const previewContent = !withValidation ? resolvedValue : computeCoersionPreview(resolvedValue, coersionData);
const cls = error ? 'codehinter-error-banner' : 'codehinter-success-banner';
return (
<div className={`d-flex flex-column align-content-between flex-wrap`}>
<div className="p-2">
<span className={`badge text-capitalize font-500 ${cls}`}> {error ? error.type : previewValueType}</span>
</div>
<PreviewBox.CodeBlock code={error ? error.value : previewContent} />
</div>
);
};
const PreviewContainer = ({
children,
isFocused,
enablePreview,
setCursorInsidePreview,
isPortalOpen,
...restProps
}) => {
const { validationSchema, isWorkspaceVariable, errorStateActive, previewPlacement } = restProps;
const [errorMessage, setErrorMessage] = useState('');
const typeofError = getCurrentNodeType(errorMessage);
const errorMsg = typeofError === 'Array' ? errorMessage[0] : errorMessage;
const darkMode = localStorage.getItem('darkMode') === 'true';
const popover = (
<Popover
bsPrefix="codehinter-preview-popover"
id="popover-basic"
className={`${darkMode && 'dark-theme'}`}
style={{
width: '250px',
maxWidth: '350px',
marginRight: 2,
zIndex: 1400,
}}
onMouseEnter={() => setCursorInsidePreview(true)}
onMouseLeave={() => setCursorInsidePreview(false)}
>
<Popover.Body
style={{
border: !isEmpty(validationSchema) && '1px solid var(--slate6)',
padding: isEmpty(validationSchema) && '0px',
boxShadow: ' 0px 4px 8px 0px #3032331A, 0px 0px 1px 0px #3032330D',
}}
>
<div>
{errorStateActive && (
<div className="mb-2">
<Alert
svg="tj-info-error"
cls={`codehinter preview-alert-banner p-2 mb-0 mt-2 bg-red-lt`}
iconCls="align-items-start"
data-cy={``}
imgHeight={18}
imgWidth={18}
>
<div className="d-flex align-items-center">
<div className="">{errorMsg !== 'null' ? errorMsg : 'Invalid'}</div>
</div>
</Alert>
</div>
)}
{!isEmpty(validationSchema) && (
<>
<div className="mb-1">
<span
style={{
fontSize: '11px',
fontWeight: '500',
lineHeight: '16px',
letterSpacing: '0em',
color: '#6A727C',
}}
>
Expected
</span>
</div>
<Card className={darkMode && 'bg-slate2'}>
<Card.Body
className="p-1"
style={{
minHeight: '60px',
maxHeight: '100px',
}}
>
<div className="d-flex flex-column align-content-between flex-wrap p-0">
<div className="p-2">
<span
className={`badge bg-light-gray font-500 mute-text text-capitalize`}
style={{ fontSize: '12px', background: 'var(--interactive-default)' }}
>
{validationSchema?.schema?.type}
</span>
</div>
<PreviewBox.CodeBlock code={validationSchema?.defaultValue} isExpectValue={true} />
</div>
</Card.Body>
</Card>
</>
)}
</div>
<div className={`${!isEmpty(validationSchema) && 'mt-2'}`}>
{!isEmpty(validationSchema) && (
<div className={`mb-1`}>
<span
style={{
fontSize: '11px',
fontWeight: '500',
lineHeight: '16px',
letterSpacing: '0em',
color: '#6A727C',
}}
>
Current
</span>
</div>
)}
<Card
className={darkMode && 'bg-slate2'}
style={{
borderColor: errorStateActive ? 'var(--tomato8)' : 'var(--slate6)',
}}
>
<Card.Body
className="p-1 code-hinter-preview-card-body"
style={{
minHeight: '60px',
maxHeight: '240px',
overflowY: 'auto',
}}
>
<PreviewBox isFocused={isFocused} setErrorMessage={setErrorMessage} {...restProps} />
</Card.Body>
</Card>
</div>
{isWorkspaceVariable && <CodeHinter.DepericatedAlert text={'Deprecating soon'} />}
</Popover.Body>
</Popover>
);
return (
<OverlayTrigger
trigger="click"
show={enablePreview && isFocused && !isPortalOpen}
placement={previewPlacement}
overlay={popover}
>
{children}
</OverlayTrigger>
);
};
const PreviewCodeBlock = ({ code, isExpectValue = false }) => {
let preview = code && code.trim ? code?.trim() : `${code}`;
const shouldTrim = preview.length > 35;
let showJSONTree = false;
if (isExpectValue && shouldTrim) {
preview = preview.substring(0, 35) + '...' + preview.substring(preview.length - 2, preview.length);
}
let prettyPrintedJson = preview;
try {
prettyPrintedJson = JSON.parse(preview);
const typeOfValue = typeof prettyPrintedJson;
if (typeOfValue === 'object' || typeOfValue === 'array') {
showJSONTree = true;
} else {
prettyPrintedJson = preview;
showJSONTree = false;
}
} catch (e) {
prettyPrintedJson = preview;
showJSONTree = false;
}
if (showJSONTree) {
const darkMode = localStorage.getItem('darkMode') === 'true';
return (
<div className="preview-json">
<JsonViewer
value={prettyPrintedJson}
displayDataTypes={false}
displaySize={false}
displayObjectSize={false}
enableClipboard={false}
rootName={false}
theme={darkMode ? 'dark' : 'light'}
groupArraysAfterLength={500}
/>
</div>
);
}
return (
<div className="p-2 pt-0">
<pre
className="text-secondary"
style={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
overflowWrap: 'break-word',
display: 'block',
background: 'transparent',
border: 'none',
lineHeight: '1.5',
maxHeight: 'none',
overflow: 'auto',
width: '100%',
fontSize: '12px',
overflowY: 'auto',
padding: '0',
margin: '0',
}}
>
{prettyPrintedJson?.startsWith('{{') && prettyPrintedJson?.endsWith('{{')
? prettyPrintedJson?.replace(/{{/g, '').replace(/}}/g, '')
: prettyPrintedJson}
</pre>
</div>
);
};
PreviewBox.RenderResolvedValue = RenderResolvedValue;
PreviewBox.Container = PreviewContainer;
PreviewBox.CodeBlock = PreviewCodeBlock;

View file

@ -0,0 +1,404 @@
/* eslint-disable import/no-unresolved */
import React, { useContext, useEffect, useRef, useState } from 'react';
import { PreviewBox } from './PreviewBox';
import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip';
import { useTranslation } from 'react-i18next';
import { camelCase, isEmpty } from 'lodash';
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
import { defaultKeymap } from '@codemirror/commands';
import { keymap } from '@codemirror/view';
import FxButton from '../CodeBuilder/Elements/FxButton';
import cx from 'classnames';
import { DynamicFxTypeRenderer } from './DynamicFxTypeRenderer';
import { resolveReferences } from './utils';
import { okaidia } from '@uiw/codemirror-theme-okaidia';
import { githubLight } from '@uiw/codemirror-theme-github';
import { getAutocompletion } from './autocompleteExtensionConfig';
import ErrorBoundary from '../ErrorBoundary';
import CodeHinter from './CodeHinter';
import { EditorContext } from '../Context/EditorContextWrapper';
const SingleLineCodeEditor = ({ suggestions, componentName, fieldMeta = {}, componentId, ...restProps }) => {
const { initialValue, onChange, enablePreview = true, portalProps } = restProps;
const { validation = {} } = fieldMeta;
const [isFocused, setIsFocused] = useState(false);
const [currentValue, setCurrentValue] = useState('');
const [errorStateActive, setErrorStateActive] = useState(false);
const [cursorInsidePreview, setCursorInsidePreview] = useState(false);
const isPreviewFocused = useRef(false);
const wrapperRef = useRef(null);
//! Re render the component when the componentName changes as the initialValue is not updated
const { variablesExposedForPreview } = useContext(EditorContext);
const customVariables = variablesExposedForPreview?.[componentId] ?? {};
useEffect(() => {
if (typeof initialValue !== 'string') return;
const [valid, _error] = !isEmpty(validation)
? resolveReferences(initialValue, validation, customVariables)
: [true, null];
if (!valid) {
setErrorStateActive(true);
}
setCurrentValue(initialValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [componentName, initialValue]);
useEffect(() => {
const handleClickOutside = (event) => {
if (cursorInsidePreview || portalProps?.isOpen || event.target.closest('.cm-tooltip-autocomplete')) {
return;
}
if (wrapperRef.current && isFocused && !wrapperRef.current.contains(event.target)) {
isPreviewFocused.current = false;
setIsFocused(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wrapperRef, isFocused, isPreviewFocused, currentValue, portalProps?.isOpen, cursorInsidePreview]);
const isWorkspaceVariable =
typeof currentValue === 'string' && (currentValue.includes('%%client') || currentValue.includes('%%server'));
return (
<div
ref={wrapperRef}
className="code-hinter-wrapper position-relative"
style={{ width: '100%', height: restProps?.lang === 'jsx' && '320px' }}
>
<PreviewBox.Container
enablePreview={enablePreview}
currentValue={currentValue}
isFocused={isFocused}
setCursorInsidePreview={setCursorInsidePreview}
componentName={componentName}
validationSchema={validation}
setErrorStateActive={setErrorStateActive}
ignoreValidation={restProps?.ignoreValidation || isEmpty(validation)}
componentId={restProps?.componentId ?? null}
isWorkspaceVariable={isWorkspaceVariable}
errorStateActive={errorStateActive}
previewPlacement={restProps?.cyLabel === 'canvas-bg-colour' ? 'top' : 'left-start'}
isPortalOpen={restProps?.portalProps?.isOpen}
customVariables={customVariables}
>
<div className="code-editor-basic-wrapper d-flex">
<div className="codehinter-container w-100">
<SingleLineCodeEditor.Editor
currentValue={currentValue}
setCurrentValue={setCurrentValue}
hints={suggestions}
isFocused={isFocused}
setFocus={setIsFocused}
validationType={validation?.schema?.type}
onBlurUpdate={onChange}
error={errorStateActive}
cyLabel={restProps.cyLabel}
portalProps={portalProps}
componentName={componentName}
{...restProps}
/>
</div>
</div>
</PreviewBox.Container>
</div>
);
};
const EditorInput = ({
currentValue,
setCurrentValue,
hints,
setFocus,
validationType,
onBlurUpdate,
placeholder = '',
error,
cyLabel = '',
componentName,
usePortalEditor = true,
renderPreview,
portalProps,
lang,
isFocused,
componentId,
type,
delayOnChange = true, // Added this prop to immediately update the onBlurUpdate callback
paramLabel = '',
}) => {
function autoCompleteExtensionConfig(context) {
let word = context.matchBefore(/\w*/);
const totalReferences = (context.state.doc.toString().match(/{{/g) || []).length;
let queryInput = context.state.doc.toString();
const originalQueryInput = queryInput;
if (totalReferences > 0) {
const currentCursor = context.state.selection.main.head;
const currentCursorPos = context.pos;
let currentWord = queryInput.substring(currentCursor, currentCursorPos);
if (currentWord?.length === 0) {
const lastBracesFromPos = queryInput.lastIndexOf('{{', currentCursorPos);
currentWord = queryInput.substring(lastBracesFromPos, currentCursorPos);
//remove curly braces from the current word as will append it later
currentWord = currentWord.replace(/{{|}}/g, '');
}
if (currentWord.includes(' ')) {
currentWord = currentWord.split(' ').pop();
}
// remove \n from the current word if it is present
currentWord = currentWord.replace(/\n/g, '');
queryInput = '{{' + currentWord + '}}';
}
let completions = getAutocompletion(queryInput, validationType, hints, totalReferences, originalQueryInput);
return {
from: word.from,
options: completions,
validFor: /^\{\{.*\}\}$/,
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [hints]);
const autoCompleteConfig = autocompletion({
override: [overRideFunction],
compareCompletions: (a, b) => {
return a.section.rank - b.section.rank && a.label.localeCompare(b.label);
},
aboveCursor: false,
defaultKeymap: true,
positionInfo: () => {
return {
class: 'cm-completionInfo-top cm-custom-completion-info',
};
},
});
const customKeyMaps = [...defaultKeymap, ...completionKeymap];
const handleOnChange = React.useCallback((val) => {
setCurrentValue(val);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleOnBlur = () => {
if (!delayOnChange) {
setFirstTimeFocus(false);
return onBlurUpdate(currentValue);
}
setTimeout(() => {
setFirstTimeFocus(false);
onBlurUpdate(currentValue);
}, 0);
};
const darkMode = localStorage.getItem('darkMode') === 'true';
const theme = darkMode ? okaidia : githubLight;
const { handleTogglePopupExapand, isOpen, setIsOpen, forceUpdate } = portalProps;
const [firstTimeFocus, setFirstTimeFocus] = useState(false);
const customClassNames = cx('codehinter-input', {
'border-danger': error,
focused: isFocused,
'focus-box-shadow-active': firstTimeFocus,
'widget-code-editor': componentId,
});
const currentEditorHeightRef = useRef(null);
const handleFocus = () => {
setFirstTimeFocus(true);
setTimeout(() => {
setFocus(true);
}, 50);
};
const showLineNumbers = lang == 'jsx' || type === 'extendedSingleLine' || false;
cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : cyLabel;
return (
<div
ref={currentEditorHeightRef}
className={`cm-codehinter ${darkMode && 'cm-codehinter-dark-themed'}`}
data-cy={`${cyLabel}-input-field`}
>
{usePortalEditor && (
<CodeHinter.PopupIcon
callback={handleTogglePopupExapand}
icon="portal-open"
tip="Pop out code editor into a new window"
position={currentEditorHeightRef?.current?.getBoundingClientRect()}
/>
)}
<CodeHinter.Portal
isCopilotEnabled={false}
isOpen={isOpen}
callback={setIsOpen}
componentName={componentName}
key={componentName}
customComponent={renderPreview}
forceUpdate={forceUpdate}
optionalProps={{ styles: { height: 300 }, cls: '' }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal' }}
dragResizePortal={true}
callgpt={null}
>
<ErrorBoundary>
<CodeMirror
value={currentValue}
placeholder={placeholder}
height={showLineNumbers ? '400px' : '100%'}
width="100%"
extensions={[javascript({ jsx: lang === 'jsx' }), autoCompleteConfig, keymap.of([...customKeyMaps])]}
onChange={(val) => {
setFirstTimeFocus(false);
handleOnChange(val);
}}
basicSetup={{
lineNumbers: showLineNumbers,
syntaxHighlighting: true,
bracketMatching: true,
foldGutter: false,
highlightActiveLine: false,
autocompletion: true,
completionKeymap: true,
searchKeymap: false,
}}
onMouseDown={() => handleFocus()}
onBlur={() => handleOnBlur()}
className={customClassNames}
theme={theme}
indentWithTab={true}
/>
</ErrorBoundary>
</CodeHinter.Portal>
</div>
);
};
const DynamicEditorBridge = (props) => {
const {
initialValue,
type,
fxActive,
paramType = 'code',
paramLabel,
paramName,
fieldMeta,
darkMode,
className,
onFxPress,
cyLabel = '',
onChange,
styleDefinition,
onVisibilityChange,
isEventManagerParam = false,
} = props;
const [forceCodeBox, setForceCodeBox] = React.useState(fxActive);
const codeShow = paramType === 'code' || forceCodeBox;
const HIDDEN_CODE_HINTER_LABELS = ['Table data', 'Column data', 'Text Format'];
const { isFxNotRequired } = fieldMeta;
const { t } = useTranslation();
const [_, error, value] = type === 'fxEditor' ? resolveReferences(initialValue) : [];
const fxClass = isEventManagerParam ? 'justify-content-start' : 'justify-content-end';
return (
<div className={cx({ 'codeShow-active': codeShow }, 'wrapper-div-code-editor')}>
<div className={cx('d-flex align-items-center justify-content-between')}>
{paramLabel !== ' ' && !HIDDEN_CODE_HINTER_LABELS.includes(paramLabel) && (
<div className={`field ${className}`} data-cy={`${cyLabel}-widget-parameter-label`}>
<ToolTip
label={t(`widget.commonProperties.${camelCase(paramLabel)}`, paramLabel)}
meta={fieldMeta}
labelClass={`tj-text-xsm color-slate12 ${codeShow ? 'mb-2' : 'mb-0'} ${
darkMode && 'color-whitish-darkmode'
}`}
/>
</div>
)}
<div className={`${(paramType ?? 'code') === 'code' ? 'd-none' : ''} flex-grow-1`}>
<div style={{ marginBottom: codeShow ? '0.5rem' : '0px' }} className={`d-flex align-items-center ${fxClass}`}>
{paramLabel !== 'Type' && isFxNotRequired === undefined && (
<div
className={`col-auto pt-0 fx-common fx-button-container ${
(isEventManagerParam || codeShow) && 'show-fx-button-container'
}`}
>
<FxButton
active={codeShow}
onPress={() => {
if (codeShow) {
setForceCodeBox(false);
onFxPress(false);
} else {
setForceCodeBox(true);
onFxPress(true);
}
}}
dataCy={cyLabel}
/>
</div>
)}
{!codeShow && (
<DynamicFxTypeRenderer
value={!error ? value : ''}
onChange={onChange}
paramName={paramName}
paramLabel={paramLabel}
paramType={paramType}
forceCodeBox={() => {
setForceCodeBox(true);
onFxPress(true);
}}
meta={fieldMeta}
cyLabel={cyLabel}
styleDefinition={styleDefinition}
onVisibilityChange={onVisibilityChange}
/>
)}
</div>
</div>
</div>
{codeShow && (
<div className={`row custom-row`} style={{ display: codeShow ? 'flex' : 'none' }}>
<div className={`col code-hinter-col`}>
<div className="d-flex">
<SingleLineCodeEditor initialValue {...props} />
</div>
</div>
</div>
)}
</div>
);
};
SingleLineCodeEditor.Editor = EditorInput;
SingleLineCodeEditor.EditorBridge = DynamicEditorBridge;
export default SingleLineCodeEditor;

View file

@ -0,0 +1,211 @@
import { getLastDepth, getLastSubstring } from './autocompleteUtils';
export const getAutocompletion = (input, fieldType, hints, totalReferences = 1, originalQueryInput = null) => {
if (!input.startsWith('{{') || !input.endsWith('}}')) return [];
const actualInput = input.replace(/{{|}}/g, '');
let JSLangHints = [];
if (fieldType) {
JSLangHints = hints['jsHints'][fieldType]['methods'].map((hint) => ({
hint: hint,
type: 'js_method',
}));
} else {
JSLangHints = Object.keys(hints['jsHints'])
.map((key) => {
return hints['jsHints'][key]['methods'].map((hint) => ({
hint: hint,
type: 'js_method',
}));
})
.flat();
}
const deprecatedWorkspaceVarsHints = ['client', 'server'];
const appHints = hints['appHints'].filter((cm) => {
const { hint } = cm;
if (hint.includes('actions') || hint.endsWith('run()')) {
return false;
}
if (deprecatedWorkspaceVarsHints.includes(hint)) {
return false;
}
const lastChar = hint[cm.length - 1];
if (lastChar === ')') {
return false;
}
return true;
});
const appHintsFilteredByDepth = filterHintsByDepth(actualInput, appHints);
const autoSuggestionList = appHintsFilteredByDepth.filter((suggestion) => {
if (actualInput.length === 0) return true;
return suggestion.hint.includes(actualInput);
});
const jsHints = JSLangHints.filter((cm) => {
const lastCharsAfterDot = actualInput.split('.').pop();
if (cm.hint.includes(lastCharsAfterDot)) return true;
if (autoSuggestionList.length === 0 && !cm.hint.includes(actualInput)) return true;
});
const searchInput = input.replace(/{{|}}/g, '');
const suggestions = generateHints(
[...jsHints, ...autoSuggestionList],
totalReferences,
originalQueryInput,
searchInput
);
return orderSuggestions(suggestions, fieldType);
};
function orderSuggestions(suggestions, validationType) {
if (!validationType) return suggestions;
const matchingSuggestions = suggestions.filter((s) => s.detail === validationType);
const otherSuggestions = suggestions.filter((s) => s.detail !== validationType);
return [...matchingSuggestions, ...otherSuggestions];
}
export const generateHints = (hints, totalReferences = 1, input, searchText) => {
if (!hints) return [];
const suggestions = hints.map(({ hint, type }) => {
let displayedHint = type === 'js_method' || (type === 'Function' && !hint.endsWith('.run()')) ? `${hint}()` : hint;
const currentWord = input.split('{{').pop().split('}}')[0];
const hasDepth = currentWord.includes('.');
const lastDepth = getLastSubstring(currentWord);
const displayLabel = getLastDepth(displayedHint);
return {
displayLabel: lastDepth === '' ? displayedHint : displayLabel,
label: displayedHint,
info: displayedHint,
type: type === 'js_method' ? 'js_methods' : type?.toLowerCase(),
section:
type === 'js_method'
? { name: 'JS methods', rank: 2 }
: { name: !hasDepth ? 'Suggestions' : lastDepth, rank: 1 },
detail: type === 'js_method' ? 'method' : type?.toLowerCase() || '',
apply: (view, completion, from, to) => {
const doc = view.state.doc;
const { from: _, to: end } = doc.lineAt(from);
const actualStartIndex = input.lastIndexOf('{{');
const pickedFrom =
actualStartIndex === 0 && end - to > 2 ? from - currentWord.length : actualStartIndex + (end - to);
const pickedCompletionConfig = {
from: pickedFrom,
to: to,
insert: completion.label,
};
let anchorSelection = pickedCompletionConfig.insert.length + 2;
if (completion.type === 'js_methods') {
pickedCompletionConfig.from = from;
}
const multiReferenceInSingleIndentifier = totalReferences == 1 && searchText !== currentWord;
if (multiReferenceInSingleIndentifier) {
const splitAtSearchString = doc.toString().split(searchText)[0];
const newFrom = splitAtSearchString.length;
pickedCompletionConfig.from = newFrom;
} else if (totalReferences > 1 && completion.type !== 'js_methods') {
const splitIndex = from;
const substring = doc.toString().substring(0, splitIndex).split('{{').pop();
pickedCompletionConfig.from = from - substring.length;
}
const dispatchConfig = {
changes: pickedCompletionConfig,
};
const actualInput = doc.toString().replace(/{{|}}/g, '');
if (actualInput.length === 0) {
dispatchConfig.selection = {
anchor: anchorSelection,
};
}
view.dispatch(dispatchConfig);
},
};
});
return suggestions;
};
function filterHintsByDepth(input, hints) {
if (input === '') return hints;
const inputDepth = input.includes('.') ? input.split('.').length : 0;
const filteredHints = hints.filter((cm) => {
const hintParts = cm.hint.split('.');
let shouldInclude =
(cm.hint.startsWith(input) && hintParts.length === inputDepth + 1) ||
(cm.hint.startsWith(input) && hintParts.length === inputDepth);
const shouldFuzzyMatch = !shouldInclude ? hintParts.length > inputDepth : false;
if (shouldFuzzyMatch) {
// fuzzy match
let matchedDepth = -1;
for (let i = 0; i < hintParts.length; i++) {
if (hintParts[i].includes(input)) {
matchedDepth = i;
break;
}
}
if (matchedDepth !== -1) {
shouldInclude = hintParts.length === matchedDepth + 1;
}
} else if (input.endsWith('.')) {
shouldInclude = cm.hint.startsWith(input) && hintParts.length === inputDepth;
}
return shouldInclude;
});
return filteredHints;
}
export function findNearestSubstring(inputStr, currentCurosorPos) {
let end = currentCurosorPos - 1; // Adjust for zero-based indexing
let substring = '';
const inputSubstring = inputStr.substring(0, end + 1);
console.log(`Initial cursor position: ${currentCurosorPos}`);
console.log(`Character at cursor: '${inputStr[end]}'`);
console.log(`Input substring: '${inputSubstring}'`);
// Iterate backwards from the character before the cursor
for (let i = end; i >= 0; i--) {
if (inputStr[i] === ' ') {
break; // Stop if a space is found
}
substring = inputStr[i] + substring;
}
return substring;
}

View file

@ -0,0 +1,11 @@
export function getLastSubstring(inputString) {
if (!inputString.includes('.')) return '';
let parts = inputString.trim().split('.').filter(Boolean);
return parts.length > 0 ? parts[parts.length - 1] : '';
}
export function getLastDepth(inputString) {
let parts = inputString.split('.').filter(Boolean);
return parts.length > 0 ? parts[parts.length - 1] : '';
}

View file

@ -0,0 +1,3 @@
import CodeHinter from './CodeHinter';
export default CodeHinter;

View file

@ -0,0 +1,534 @@
@import "../../_styles/colors.scss";
.codehinter-input-wrapper {
display: flex;
padding: 6px 0px;
align-items: flex-start;
gap: 8px;
align-self: stretch;
.codehinter-container {
height: inherit !important;
.codehinter-vertical-line {
position: relative;
width: 0;
border-left: 1px solid var(--slate5);
content: '';
margin-right: 1rem;
}
}
.list-group-item {
border-radius: 0 !important;
}
}
.cm-widgetBuffer {
display: none !important;
}
.inspector {
.cm-base-autocomplete {
left: auto !important;
}
}
.cm-base-hint-info {
color: var(--text-default, #1B1F24) !important;
background-color: var(--surfaces-surface-02);
border: 1px solid var(--borders-disabled-on-white, #E4E7EB) !important;
border-radius: 0px 0px 6px 6px !important;
box-shadow: 0px 4px 8px 0px rgba(48, 49, 51, 0.10), 0px 0px 1px 0px rgba(48, 49, 51, 0.05);
font-weight: 400;
}
.cm-base-autocomplete {
// height: 300px !important;
color: var(--text-default, #1B1F24);
background: var(--slate1) !important;
border: 1px solid var(--borders-disabled-on-white, #E4E7EB) !important;
border-radius: 6px !important;
box-shadow: 0px 4px 8px 0px rgba(48, 49, 51, 0.10), 0px 0px 1px 0px rgba(48, 49, 51, 0.05);
z-index: 99999 !important;
// overflow-y: auto !important;
// overflow-x: hidden !important;
width: 270px !important;
ul {
width: 270px !important;
max-width: 100% !important;
max-height: 300px !important;
completion-section {
color: var(--text-placeholder, #1B1F24) !important;
border-bottom: none !important;
background-color: var(--surfaces-surface-02) !important;
font-size: 13px !important;
line-height: 20px !important;
font-weight: 500 !important;
padding: 8px !important;
}
li {
max-width: 100% !important;
width: 100% !important;
display: flex !important;
align-items: start !important;
justify-content: space-between !important;
padding: 0.35rem !important;
font-size: 11px !important;
font-style: normal !important;
font-weight: 400 !important;
line-height: 16px !important;
color: var(--text-default, #1B1F24) !important;
.cm-completionIcon-js_methods::after {
content: '';
background-image: url("data:image/svg+xml,%3Csvg enable-background='new 0 0 1073.9 1074' viewBox='0 0 1073.9 1074' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m1005.4 0h-936.8c-37.9 0-68.6 30.7-68.6 68.6v936.8c0 37.8 30.7 68.6 68.6 68.6h936.8c37.8 0 68.6-30.7 68.6-68.6v-936.9c-.1-37.8-30.8-68.5-68.6-68.5zm-517.8 605.3c0 29.8-2.8 55.3-8.5 76.3s-14.3 37.8-25.7 51c-11.5 13-25.8 22.6-42.9 28.6-17.1 6.1-37 9.1-59.7 9.1-20 0-38-3.2-54.2-9.4-31.7-11.9-56.5-37.4-67.4-69.4-6.4-17.9-9.5-36.7-9.2-55.7v-13.4h64.3c0 59.9 22.2 89.9 66.6 89.9 11.4 0 21.4-1.3 30.4-4 8.9-2.8 16.4-7.8 22.6-15.4 6.2-7.5 10.8-17.9 14-31.4s4.8-31 4.8-52.7v-335.2h65.1zm359.1 97.3c-6.8 13.7-16.4 25.8-28.2 35.5-13.2 10.6-28.4 18.7-44.6 23.7-17.5 5.6-37.1 8.4-58.8 8.4-96.1 0-146.8-37.9-152-113.9h63.2c.3 42.9 29.7 64.4 88.1 64.4 13 0 24.6-1.4 34.5-4.3 10-2.8 18.3-6.8 24.9-11.8 12.9-9.3 20.5-24.2 20.4-40.1.3-7-1.1-13.9-3.9-20.3-2.6-5.2-7.7-9.7-15.2-13.7-10-4.9-20.4-8.8-31.2-11.6-17.3-5-34.7-9.8-52.2-14.3-20.9-5.2-39-10.4-54.1-15.6-13.4-4.2-26-10.5-37.4-18.8-9.5-7.1-17-16.5-21.7-27.3-4.7-10.9-7-24.6-7-41.1 0-14.5 3.1-28 9.2-40.5 6.2-12.4 14.9-23.2 26.5-32.4 11.4-9.1 25.3-16.2 41.5-21.4 17.6-5.4 36-8 54.4-7.8 92.9 0 140.2 33 141.6 99.1h-62.2c-2.2-33-27.1-49.6-74.7-49.6-10.6 0-20.3 1.1-29.4 3.2s-16.8 5.1-23.5 9.1c-6.7 3.9-11.8 8.8-15.6 14.6-3.7 6-5.7 13-5.5 20.1 0 6.9.9 12.6 2.6 17 1.7 4.5 5.8 8.6 12.4 12.4 6.5 3.8 16.2 7.6 29 11.3s30.3 8.3 52.5 13.7c21.4 5.2 40.1 10.5 55.9 16.1 15.7 5.6 28.8 12.3 39.4 20.2 10.5 7.8 18.2 17.5 23.3 28.8 5 11.3 7.6 25.5 7.6 42.5.2 16.1-3.2 30.8-9.8 44.4z' fill='%23030104'/%3E%3C/svg%3E");
background-size: cover;
display: flex;
width: 16px;
height: 16px;
border: none;
}
.cm-completionIcon-object::after {
content: '';
background-image: url('data:image/svg+xml,<svg viewBox="-4.08 -4.08 32.16 32.16" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"><rect x="-4.08" y="-4.08" width="32.16" height="32.16" rx="16.08" fill="%23849DFF" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M9.5 5H9C7.89543 5 7 5.89543 7 7V9C7 10 6.4 12 4 12C5 12 7 12.6 7 15V17.0002C7 18.1048 7.89543 19 9 19H9.5M14.5 5H15C16.1046 5 17 5.89543 17 7V9C17 10 17.6 12 20 12C19 12 17 12.6 17 15V17.0002C17 18.1048 16.1046 19 15 19H14.5" stroke="%23000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path></g></svg>');
background-size: cover;
display: flex;
width: 16px;
height: 16px;
border: none;
border-radius: 50%;
background-color: red;
}
.cm-completionIcon-array::after {
content: '';
background-image: url('data:image/svg+xml,<svg fill="%23000000" viewBox="-25.6 -25.6 307.20 307.20" id="Flat" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"><rect x="-25.6" y="-25.6" width="307.20" height="307.20" rx="153.6" fill="%23C1C8CD" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M48,48V208H80a8,8,0,0,1,0,16H40a8.00039,8.00039,0,0,1-8-8V40a8.00039,8.00039,0,0,1,8-8H80a8,8,0,0,1,0,16ZM216,32H176a8,8,0,0,0,0,16h32V208H176a8,8,0,0,0,0,16h40a8.00039,8.00039,0,0,0,8-8V40A8.00039,8.00039,0,0,0,216,32Z"></path></g></svg>');
background-size: cover;
display: flex;
width: 16px;
height: 16px;
border: none;
border-radius: 50%;
background-color: red;
}
.cm-completionIcon-string::after {
content: 'str';
display: flex;
width: 16px;
height: 16px;
border: none;
border-radius: 50%;
background-color: greenyellow;
font-size: 6px;
align-items: center;
justify-content: center;
}
}
li[aria-selected="true"] {
background-color: var(--interactive-hover) !important;
color: var(--text-default, #1B1F24) !important;
}
// li > :first-child,
li> :nth-child(2) {
display: flex !important;
align-self: flex-start !important;
width: 100% !important;
text-wrap: nowrap !important;
// if the text is too long, it will be cut off
overflow: hidden !important;
}
li> :last-child {
align-self: flex-end !important;
}
.cm-completionIcon {
width: 16px !important;
height: 16px !important;
}
}
.cm-custom-completion-info {
@extend .cm-base-hint-info;
position: relative !important;
width: 100% !important;
background-color: var(--surfaces-surface-03) !important;
}
}
.query-manager-sort-filter-popup {
.cm-base-autocomplete {
position: fixed !important;
top: 130px !important;
}
}
.canvas-codehinter-container {
.cm-base-autocomplete {
position: fixed !important;
top: 500px !important;
left: 38px !important;
}
}
.widget-code-editor {
height: 100%;
.cm-content {
max-width: 100% !important;
white-space: pre-wrap;
word-wrap: break-word;
}
}
.ͼ1 .cm-placeholder {
vertical-align: middle;
}
.code-hinter-wrapper {
.cm-editor {
min-height: 32px;
max-height: 220px;
}
}
.codehinter-input {
font-family: IBM Plex Sans;
font-size: 12px !important;
display: block;
// width: 100%;
font-weight: 400;
color: var(--slate9);
background-clip: padding-box;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
cursor: text;
justify-content: center;
.cm-tooltip-autocomplete {
@extend .cm-base-autocomplete;
}
.cm-editor {
min-height: 32px;
justify-content: center !important;
}
}
.codehinter-input.focused {
.cm-editor {
outline: none;
border: 2px solid #4368E3 !important;
justify-content: center !important;
}
}
.modal-body {
.codehinter-multi-line-input {
.cm-editor {
height: 100%;
}
}
}
.codehinter-multi-line-input {
height: 100%;
.cm-editor {
min-height: 300px;
height: 300px;
max-height: fit-content !important;
.cm-gutters {
width: 42px;
background-color: var(--interactive-default) !important;
color: var(--text-disabled) !important;
border-right: 1px solid var(--borders-disabled-on-white) !important;
}
}
.cm-tooltip-autocomplete {
@extend .cm-base-autocomplete;
top: content-box !important;
.cm-completionInfo {
@extend .cm-base-hint-info;
}
}
}
.suggest-list-item {
color: var(--text-default, #1B1F24);
font-family: IBM Plex Sans;
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px;
/* 145.455% */
&:hover,
&:active {
border-radius: 0 !important;
}
}
.curly-braces {
color: #1E823B;
}
.styled-par {
color: #1E823B;
}
.cm-codehinter-dark-themed {
.cm-tooltip-autocomplete {
border-color: var(--slate5) !important;
}
.cm-tooltip-autocomplete>ul>completion-section {
background: var(--slate5);
color: #c9cbcf !important;
}
.cm-tooltip-autocomplete>ul>li {
background: var(--surfaces-surface-02);
color: #c9cbcf !important;
}
}
.cm-scroller {
overflow-y: auto !important;
overflow-x: hidden !important;
overscroll-behavior: contain;
}
.cm-focused {
outline: none !important;
}
.cm-editor {
border: 1px solid var(--slate7);
border-radius: 4px;
transition: box-shadow 0.15s ease-in-out;
}
.fields-container {
.cm-editor {
border-radius: 0 !important;
box-shadow: none !important;
}
}
.border-danger {
.cm-editor {
border: 1px solid red !important;
}
}
.runjs-editor .cm-editor {
border: none !important;
}
.preview-alert-banner {
height: fit-content;
max-height: 300px;
border: none !important;
}
.code-hinter-preview-card-body::-webkit-scrollbar {
width: 4px !important;
}
.code-hinter-preview-card-body::-webkit-scrollbar-track {
margin: 5px !important;
}
.code-hinter-preview-card-body>.codehinter-popup-icon {
left: 2px !important;
}
.editor-container {
.codehinter-input .cm-editor {
max-height: auto !important;
justify-content: center;
}
}
.codehinter-error-banner {
color: #D72D39 !important;
background-color: #FCEEEF !important;
font-size: 12px !important;
}
.codehinter-success-banner {
color: #1E823B !important;
background-color: #E8F3EB !important;
font-size: 12px !important;
}
.cm-gutterElement {
color: var(--text-disabled);
}
.rest-api-tabpanes-body {
.cm-gutters {
width: 42px !important;
background-color: var(--interactive-default) !important;
color: var(--text-disabled) !important;
border-right: 1px solid var(--borders-disabled-on-white) !important;
}
}
.cm-content {
padding-right: 20px !important;
}
.modal-body {
.codehinter-input .cm-editor {
border: 1px solid var(--borders-disabled-on-white, #E4E7EB) !important;
}
.codehinter-input .cm-editor {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
}
.codehinter-input {
height: 100%;
border: none !important;
}
}
.cm-gutters {
width: 42px !important;
background-color: var(--interactive-default) !important;
color: var(--text-disabled) !important;
border-right: 1px solid var(--borders-disabled-on-white) !important;
}
.query-hinter {
.cm-editor {
border-bottom-right-radius: 4px !important;
border-bottom-left-radius: 4px !important;
border-color: var(--borders-disabled-on-white);
border-top-right-radius: 0px !important;
border-top-left-radius: 0px !important;
justify-content: center;
}
}
.transformation-container {
box-shadow: 0px 4px 8px 0px #3032331A;
}
.codehinder-popup-badge {
background-color: var(--surfaces-surface-02);
color: var(--text-default);
padding: 6px 8px 6px 8px;
gap: 10px;
border-radius: 6px 0px 0px 0px;
}
.resize-modal-portal .resize-modal .modal-content .modal-body .editor-container {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
.resize-modal-portal .resize-modal .resize-handle {
border-bottom: none !important;
}
.cm-content {
word-break: break-all !important;
max-width: 100% !important;
white-space: pre-wrap !important;
word-wrap: break-all !important;
}
.cm-content {
max-width: 98%;
flex-shrink: 1 !important;
}
.cm-line {
cursor: text;
}
.runjs-editor {
.cm-line {
padding: 0 6px 0 6px !important;
}
}
.cm-selectionLayer {
width: 100%;
}
.code-editor-widget {
.codehinter-multi-line-input {
height: 100%;
.cm-editor {
height: 100%;
}
}
}
.table-column-popover {
.cm-tooltip-autocomplete {
left: auto !important;
max-width: 100% !important;
ul {
min-width: auto !important;
}
}
.column-popover-card-ui {
.cm-tooltip-autocomplete {
max-width: 100% !important;
}
}
}

View file

@ -0,0 +1,409 @@
import { useResolveStore } from '@/_stores/resolverStore';
import moment from 'moment';
import _, { isEmpty } from 'lodash';
import { useCurrentStateStore } from '@/_stores/currentStateStore';
import { any } from 'superstruct';
import { generateSchemaFromValidationDefinition, validate } from '../component-properties-validation';
import { hasCircularDependency, resolveReferences as olderResolverMethod } from '@/_helpers/utils';
const acorn = require('acorn');
const acorn_code = `
const array = [1, 2, 3];
const string = "hello";
const object = {};
const boolean = true;
const number = 1;
`;
const ast = acorn.parse(acorn_code, { ecmaVersion: 2020 });
export const getCurrentNodeType = (node) => Object.prototype.toString.call(node).slice(8, -1);
function traverseAST(node, callback) {
callback(node);
for (let key in node) {
if (node[key] && typeof node[key] === 'object') {
traverseAST(node[key], callback);
}
}
}
function getMethods(type) {
const arrayMethods = Object.getOwnPropertyNames(Array.prototype).filter(
(p) => typeof Array.prototype[p] === 'function'
);
const stringMethods = Object.getOwnPropertyNames(String.prototype).filter(
(p) => typeof String.prototype[p] === 'function'
);
const objectMethods = Object.getOwnPropertyNames(Object.prototype).filter(
(p) => typeof Object.prototype[p] === 'function'
);
const booleanMethods = Object.getOwnPropertyNames(Boolean.prototype).filter(
(p) => typeof Boolean.prototype[p] === 'function'
);
const numberMethods = Object.getOwnPropertyNames(Number.prototype).filter(
(p) => typeof Number.prototype[p] === 'function'
);
switch (type) {
case 'Array':
return arrayMethods;
case 'String':
return stringMethods;
case 'Object':
return objectMethods;
case 'Boolean':
return booleanMethods;
case 'Number':
return numberMethods;
default:
return [];
}
}
function inferType(node) {
if (node.type === 'ArrayExpression') {
return 'Array';
} else if (node.type === 'Literal') {
if (typeof node.value === 'string') {
return 'String';
} else if (typeof node.value === 'number') {
return 'Number';
} else if (typeof node.value === 'boolean') {
return 'Boolean';
}
} else if (node.type === 'ObjectExpression') {
return 'Object';
}
return null;
}
export const createJavaScriptSuggestions = () => {
const allMethods = {};
traverseAST(ast, (node) => {
if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier') {
const type = inferType(node.init);
if (type) {
allMethods[node.id.name] = {
type: type,
methods: getMethods(type),
};
}
}
});
return allMethods;
};
const resolveWorkspaceVariables = (query) => {
let resolvedStr = query;
let error = null;
let valid = false;
// Resolve %%object%%
const serverRegex = /(%%.+?%%)/g;
const serverMatch = resolvedStr.match(serverRegex)?.[0];
if (serverMatch) {
const code = serverMatch.replace(/%%/g, '');
if (code.includes('server.')) {
resolvedStr = resolvedStr.replace(serverMatch, 'HiddenEnvironmentVariable');
error = 'Server variables cannot be resolved in the client.';
} else {
const [resolvedCode, err] = resolveCode(code);
if (!resolvedCode) {
error = err ? err : `Cannot resolve ${query}`;
} else {
resolvedStr = resolvedStr.replace(serverMatch, resolvedCode);
valid = true;
}
}
}
return [valid, error, resolvedStr];
};
function resolveCode(code, customObjects = {}, withError = false, reservedKeyword, isJsCode) {
let result = '';
let error;
// dont resolve if code starts with "queries." and ends with "run()"
if (code.startsWith('queries.') && code.endsWith('run()')) {
error = `Cannot resolve function call ${code}`;
} else {
try {
const state = useCurrentStateStore.getState();
const evalFunction = Function(
[
'variables',
'components',
'queries',
'globals',
'page',
'client',
'server',
'constants',
'moment',
'_',
...Object.keys(customObjects),
reservedKeyword,
],
`return ${code}`
);
result = evalFunction(
isJsCode ? state?.variables : undefined,
isJsCode ? state?.components : undefined,
isJsCode ? state?.queries : undefined,
isJsCode ? state?.globals : undefined,
isJsCode ? state?.page : undefined,
isJsCode ? undefined : state?.client,
isJsCode ? undefined : state?.server,
state?.constants, // Passing constants as an argument allows the evaluated code to access and utilize the constants value correctly.
moment,
_,
...Object.values(customObjects),
null
);
} catch (err) {
error = err.toString();
}
}
if (withError) return [result, error];
return result;
}
function getDynamicVariables(text) {
/* eslint-disable no-useless-escape */
const matchedParams = text.match(/\{\{(.*?)\}\}/g) || text.match(/\%\%(.*?)\%\%/g);
return matchedParams;
}
const resolveMultiDynamicReferences = (code, lookupTable, queryHasJSCode) => {
let resolvedValue = code;
const isComponentValue = code.includes('components.') || false;
const allDynamicVariables = getDynamicVariables(code) || [];
let isJSCodeResolver = queryHasJSCode && (allDynamicVariables.length === 1 || allDynamicVariables.length === 0);
if (!isJSCodeResolver) {
allDynamicVariables.forEach((variable) => {
const variableToResolve = variable.replace(/{{|}}/g, '').trim();
const { toResolveReference } = inferJSExpAndReferences(variableToResolve, lookupTable.hints);
if (!isComponentValue && toResolveReference && lookupTable.hints.has(toResolveReference)) {
const idToLookUp = lookupTable.hints.get(variableToResolve);
const res = lookupTable.resolvedRefs.get(idToLookUp);
resolvedValue = resolvedValue.replace(variable, res);
} else {
const [resolvedCode] = resolveCode(variableToResolve, {}, true, [], true);
resolvedValue = resolvedValue.replace(variable, resolvedCode);
}
});
} else {
const variableToResolve = code.replace(/{{|}}/g, '').trim();
const [resolvedCode] = resolveCode(variableToResolve, {}, true, [], true);
resolvedValue = typeof resolvedCode === 'string' ? resolvedValue.replace(code, resolvedCode) : resolvedCode;
}
return resolvedValue;
};
const queryHasStringOtherThanVariable = (query) => {
const startsWithDoubleCurly = query.startsWith('{{');
const endsWithDoubleCurly = query.endsWith('}}');
if (startsWithDoubleCurly && endsWithDoubleCurly) {
// Extract the content within the curly braces
const content = query.slice(2, -2).trim();
// Check if there is a space within the content
return content.includes(' ');
}
return false;
};
export const resolveReferences = (query, validationSchema, customResolvers = {}) => {
if (query !== '' && (!query || typeof query !== 'string')) return [false, null, null];
let resolvedValue = query;
let error = null;
//Todo : remove resolveWorkspaceVariables when workspace variables are removed
if (query?.startsWith('%%') && query?.endsWith('%%')) {
return resolveWorkspaceVariables(query);
}
if ((!validationSchema || isEmpty(validationSchema)) && (!query?.includes('{{') || !query?.includes('}}'))) {
return [true, error, resolvedValue];
}
if (validationSchema && !query?.includes('{{') && !query?.includes('}}')) {
const [valid, errors, newValue] = validateComponentProperty(query, validationSchema);
return [valid, errors, newValue, resolvedValue];
}
const queryHasJSCode = queryHasStringOtherThanVariable(query);
let useJSResolvers = queryHasJSCode || getDynamicVariables(query)?.length > 1;
if (!queryHasJSCode && getDynamicVariables(query)?.length === 1 && !query.startsWith('{{') && query.includes('{{')) {
useJSResolvers = true;
}
const customWidgetResolvers = ['listItem'];
const isCustomResolvers = customWidgetResolvers.some((resolver) => query.includes(resolver));
const { lookupTable } = useResolveStore.getState();
if (useJSResolvers) {
resolvedValue = resolveMultiDynamicReferences(query, lookupTable, queryHasJSCode);
} else if (isCustomResolvers && !_.isEmpty(customResolvers)) {
const currentState = useCurrentStateStore.getState();
const resolvedCode = olderResolverMethod(query, currentState, '', customResolvers);
resolvedValue = resolvedCode;
} else {
let value = query?.replace(/{{|}}/g, '').trim();
if (value.startsWith('#') || value.includes('table-')) {
value = JSON.stringify(value);
}
const { toResolveReference, jsExpression, jsExpMatch } =
lookupTable.hints || lookupTable.hints.has
? inferJSExpAndReferences(value, lookupTable.hints)
: { toResolveReference: null, jsExpression: null, jsExpMatch: null };
if (!jsExpMatch && toResolveReference && lookupTable.hints.has(toResolveReference)) {
const idToLookUp = lookupTable.hints.get(toResolveReference);
resolvedValue = lookupTable.resolvedRefs.get(idToLookUp);
if (jsExpression) {
let jscode = value.replace(toResolveReference, resolvedValue);
jscode = value.replace(toResolveReference, `'${resolvedValue}'`);
resolvedValue = resolveCode(jscode, customResolvers);
}
} else {
const [resolvedCode, errorRef] = resolveCode(value, customResolvers, true, [], true);
resolvedValue = resolvedCode;
error = errorRef || null;
}
}
if (!validationSchema || isEmpty(validationSchema)) {
return [true, error, resolvedValue, resolvedValue];
}
if (error) {
return [false, error, query, query];
}
if (hasCircularDependency(resolvedValue)) {
return [false, `${resolvedValue} has circular dependency, unable to resolve`, query, query];
}
if (validationSchema) {
const [valid, errors, newValue] = validateComponentProperty(resolvedValue, validationSchema);
return [valid, errors, newValue, resolvedValue];
}
};
export const paramValidation = (expectedType, value) => {
const type = getCurrentNodeType(value)?.toLowerCase();
return type === expectedType;
};
const inferJSExpAndReferences = (code, hintsMap) => {
if (!code) return { toResolveReference: null, jsExpression: null };
//check starts with JS expression like JSON.parse or JSON.stringify !
const jsExpRegex = /(JSON\..+?\(.+?\))/g;
const jsExpMatch = code.match(jsExpRegex)?.[0];
if (jsExpMatch) {
return { toResolveReference: null, jsExpression: null, jsExpMatch };
}
// Split the code into segments using '.' as a delimiter
const segments = code.split('.');
let referenceChain = '';
let jsExpression = '';
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const potentialReference = referenceChain ? referenceChain + '.' + segment : segment;
// Check if the potential reference exists in hintsMap
if (hintsMap.has && hintsMap.has(potentialReference)) {
// If it does, update the referenceChain
referenceChain = potentialReference;
} else {
// If it doesn't, treat the rest as a JS expression
jsExpression = segments.slice(i).join('.');
break;
}
}
return {
toResolveReference: referenceChain || null,
jsExpression: jsExpression || null,
};
};
export const FxParamTypeMapping = Object.freeze({
text: 'Text',
string: 'Text',
color: 'Color',
json: 'Json',
code: 'Code',
toggle: 'Toggle',
select: 'Select',
alignButtons: 'AlignButtons',
number: 'Number',
boxShadow: 'BoxShadow',
clientServerSwitch: 'ClientServerSwitch',
switch: 'Switch',
checkbox: 'Checkbox',
slider: 'Slider',
input: 'Input',
icon: 'Icon',
visibility: 'Visibility',
numberInput: 'NumberInput',
});
export function computeCoercion(oldValue, newValue) {
const oldValueType = Array.isArray(oldValue) ? 'array' : typeof oldValue;
const newValueType = Array.isArray(newValue) ? 'array' : typeof newValue;
if (oldValueType === newValueType) {
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
return [`${JSON.stringify(newValue)}`, newValueType, oldValueType];
}
} else {
return [`${JSON.stringify(newValue)}`, newValueType, oldValueType];
}
return ['', newValueType, oldValueType];
}
export const validateComponentProperty = (resolvedValue, validation) => {
const validationDefinition = validation?.schema;
const defaultValue = validation?.defaultValue;
const schema = _.isUndefined(validationDefinition)
? any()
: generateSchemaFromValidationDefinition(validationDefinition);
return validate(resolvedValue, schema, defaultValue, true);
};

View file

@ -40,7 +40,7 @@ function CommentFooter({
setOpen(false);
};
useHotkeys('meta+enter, control+enter', () => handleClick());
useHotkeys('meta+enter, control+enter', () => handleClick(), { scopes: 'editor' });
const darkMode = localStorage.getItem('darkMode') === 'true';
return (
<>

View file

@ -2,7 +2,7 @@ import React from 'react';
import cx from 'classnames';
import { useDrag } from 'react-dnd';
import { ItemTypes } from '@/Editor/ItemTypes';
import { ItemTypes } from '@/Editor/editorConstants';
import CommentHeader from '@/Editor/Comment/CommentHeader';
import CommentBody from '@/Editor/Comment/CommentBody';
import CommentFooter from '@/Editor/Comment/CommentFooter';

View file

@ -34,7 +34,6 @@ const CommentNotifications = ({ socket, pageId }) => {
async function fetchData(selectedKey) {
if (appId) {
console.log('inside-CommentNotifications', appId);
const isResolved = selectedKey === 'resolved';
setLoading(true);
const { data } = await commentsService.getNotifications(appId, isResolved, appVersionsId, pageId);

View file

@ -53,7 +53,8 @@ export const RenderEditor = ({
}}
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
useCustomStyles={true}
useMenuPortal={false}
// useMenuPortal={false}
useMenuPortal
styles={selectElementStyles(darkMode, '100%')}
/>
</div>

View file

@ -104,7 +104,8 @@ export const RenderHighlight = ({
}}
useCustomStyles={true}
value={annotation.data.text}
useMenuPortal={false}
// useMenuPortal={false}
useMenuPortal
styles={selectElementStyles(darkMode, '100%')}
/>
</div>

View file

@ -6,14 +6,16 @@ export const Button = function Button(props) {
const { height, properties, styles, fireEvent, id, dataCy, setExposedVariable, setExposedVariables } = props;
const { backgroundColor, textColor, borderRadius, loaderColor, disabledState, borderColor, boxShadow } = styles;
const [label, setLabel] = useState(properties.text);
const [label, setLabel] = useState(typeof properties.text === 'string' ? properties.text : '');
const [disable, setDisable] = useState(disabledState);
const [visibility, setVisibility] = useState(styles.visibility);
const [loading, setLoading] = useState(properties.loadingState);
useEffect(() => {
setLabel(properties.text);
setExposedVariable('buttonText', properties.text);
if (typeof properties.text === 'string') {
setLabel(properties.text);
setExposedVariable('buttonText', properties.text);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.text]);

View file

@ -24,7 +24,12 @@ export const CalendarEventPopover = function ({
const calendarElement = document.getElementById(calendarWidgetId);
const handleClickOutside = (event) => {
if (parentRef.current && !parentRef.current.contains(event.target) && !event.target.closest('.editor-sidebar')) {
if (
parentRef.current &&
!parentRef.current.contains(event.target) &&
!event.target.closest('.editor-sidebar') &&
!isMoveableControlClicked(event)
) {
popoverClosed();
}
};
@ -112,3 +117,14 @@ export const CalendarEventPopover = function ({
</div>
);
};
function isMoveableControlClicked(event) {
// Get the element that was clicked on
const clickedElement = event.target;
// Check if the clicked element or any of its parents have the class 'moveable-control-box'
return (
clickedElement.classList.contains('moveable-control-box') ||
clickedElement.closest('.moveable-control-box') !== null
);
}

View file

@ -1,42 +1,53 @@
/* eslint-disable import/no-unresolved */
import React from 'react';
import CodeMirror from '@uiw/react-codemirror';
import 'codemirror/addon/comment/comment';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/search/match-highlighter';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/theme/base16-light.css';
import 'codemirror/theme/duotone-light.css';
import 'codemirror/theme/monokai.css';
import { onBeforeChange, handleChange } from '../CodeBuilder/utils';
import { okaidia } from '@uiw/codemirror-theme-okaidia';
import { githubLight } from '@uiw/codemirror-theme-github';
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { sql } from '@codemirror/lang-sql';
import { sass } from '@codemirror/lang-sass';
import { debounce } from 'lodash';
const langSupport = Object.freeze({
javascript: javascript(),
python: python(),
sql: sql(),
jsx: javascript({ jsx: true }),
css: sass(),
});
export const CodeEditor = ({ height, darkMode, properties, styles, exposedVariables, setExposedVariable, dataCy }) => {
const { enableLineNumber, mode, placeholder } = properties;
const { visibility, disabledState } = styles;
function codeChanged(code) {
const codeChanged = debounce((code) => {
setExposedVariable('value', code);
}
}, 500);
const editorStyles = {
height: height,
display: !visibility ? 'none' : 'block',
};
const options = {
lineNumbers: enableLineNumber,
lineWrapping: true,
singleLine: true,
mode: mode,
tabSize: 2,
theme: darkMode ? 'monokai' : 'duotone-light',
readOnly: false,
highlightSelectionMatches: true,
placeholder,
const setupConfig = {
lineNumbers: enableLineNumber ?? true,
syntaxHighlighting: true,
bracketMatching: true,
foldGutter: true,
highlightActiveLine: false,
autocompletion: true,
highlightActiveLineGutter: false,
completionKeymap: true,
searchKeymap: false,
};
function valueChanged(editor, onChange, ignoreBraces = false) {
handleChange(editor, onChange, [], ignoreBraces);
}
const theme = darkMode ? okaidia : githubLight;
const langExtention = langSupport[mode?.toLowerCase()] ?? null;
const editorHeight = React.useMemo(() => {
return height || 'auto';
}, [height]);
return (
<div data-disabled={disabledState} style={editorStyles} data-cy={dataCy}>
@ -45,7 +56,7 @@ export const CodeEditor = ({ height, darkMode, properties, styles, exposedVariab
style={{
height: height || 'auto',
minHeight: height - 1,
maxHeight: '320px',
// maxHeight: '320px',
overflow: 'auto',
borderRadius: `${styles.borderRadius}px`,
boxShadow: styles.boxShadow,
@ -53,15 +64,20 @@ export const CodeEditor = ({ height, darkMode, properties, styles, exposedVariab
>
<CodeMirror
value={exposedVariables.value}
scrollbarStyle={null}
height={height - 1}
onBlur={(editor) => {
const value = editor.getValue();
codeChanged(value);
placeholder={placeholder}
height={'100%'}
minHeight={editorHeight}
maxHeight="100%"
width="100%"
theme={theme}
extensions={[langExtention]}
onChange={codeChanged}
basicSetup={setupConfig}
style={{
overflowY: 'auto',
}}
onChange={(editor) => valueChanged(editor, codeChanged)}
onBeforeChange={(editor, change) => onBeforeChange(editor, change)}
options={options}
className={`codehinter-multi-line-input`}
indentWithTab={true}
/>
</div>
</div>

View file

@ -3,11 +3,26 @@ import { isEqual } from 'lodash';
import iframeContent from './iframe.html';
import { useDataQueries } from '@/_stores/dataQueriesStore';
import { useGridStore } from '@/_stores/gridStore';
import { isQueryRunnable } from '@/_helpers/utils';
import { shallow } from 'zustand/shallow';
export const CustomComponent = (props) => {
const dataQueries = useDataQueries();
const { height, properties, styles, id, setExposedVariable, exposedVariables, fireEvent, dataCy, component } = props;
const dataQueries = useDataQueries();
const showPlaceholder = useGridStore((state) => {
const { resizingComponentId, draggingComponentId } = state;
if (
(resizingComponentId === null && draggingComponentId === id) ||
(draggingComponentId === null && resizingComponentId === id) ||
id === 'resizingComponentId'
) {
return true;
}
return false;
}, shallow);
const { visibility, boxShadow } = styles;
const { code, data } = properties;
const [customProps, setCustomProps] = useState(data);
@ -106,12 +121,14 @@ export const CustomComponent = (props) => {
return (
<div className="card" style={{ display: visibility ? '' : 'none', height, boxShadow }} data-cy={dataCy}>
<iframe
srcDoc={iframeContent}
style={{ width: '100%', height: '100%', border: 'none' }}
ref={iFrameRef}
data-id={id}
></iframe>
{showPlaceholder ? null : (
<iframe
srcDoc={iframeContent}
style={{ width: '100%', height: '100%', border: 'none' }}
ref={iFrameRef}
data-id={id}
></iframe>
)}
</div>
);
};

View file

@ -90,6 +90,7 @@ export const Datepicker = function Datepicker({
}}
>
<DatePickerComponent
// portalId="real-canvas"
className={`input-field form-control ${
!isValid && showValidationError ? 'is-invalid' : ''
} validation-without-icon px-2 ${darkMode ? 'bg-dark color-white' : 'bg-light'}`}

View file

@ -60,7 +60,10 @@ export const DropDown = function DropDown({
const setExposedItem = (value, index, onSelectFired = false) => {
setCurrentValue(value);
onSelectFired ? setExposedVariable('value', value).then(fireEvent('onSelect')) : setExposedVariable('value', value);
if (onSelectFired) {
setExposedVariable('value', value);
fireEvent('onSelect');
} else setExposedVariable('value', value);
setExposedVariable('selectedOptionLabel', index === undefined ? undefined : display_values?.[index]);
};

View file

@ -4,7 +4,7 @@ import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { toast } from 'react-hot-toast';
// eslint-disable-next-line import/no-unresolved
import * as XLSX from 'xlsx/xlsx.mjs';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppInfo } from '@/_stores/appDataStore';
export const FilePicker = ({
@ -19,7 +19,6 @@ export const FilePicker = ({
setExposedVariable,
dataCy,
}) => {
const currentState = useCurrentState();
//* properties definitions
const instructionText =
component.definition.properties.instructionText?.value ?? 'Drag and drop files here or click to select files';
@ -30,31 +29,25 @@ export const FilePicker = ({
const fileType = component.definition.properties.fileType?.value ?? 'image/*';
const maxSize = component.definition.properties.maxSize?.value ?? 1048576;
const minSize = component.definition.properties.minSize?.value ?? 0;
const parseContent = resolveWidgetFieldValue(
component.definition.properties.parseContent?.value ?? false,
currentState
);
const parseContent = resolveWidgetFieldValue(component.definition.properties.parseContent?.value);
const fileTypeFromExtension = component.definition.properties.parseFileType?.value ?? 'auto-detect';
const parsedEnableDropzone =
typeof enableDropzone !== 'boolean' ? resolveWidgetFieldValue(enableDropzone, currentState) : true;
const parsedEnablePicker =
typeof enablePicker !== 'boolean' ? resolveWidgetFieldValue(enablePicker, currentState) : true;
const parsedEnableDropzone = typeof enableDropzone !== 'boolean' ? resolveWidgetFieldValue(enableDropzone) : true;
const parsedEnablePicker = typeof enablePicker !== 'boolean' ? resolveWidgetFieldValue(enablePicker) : true;
const parsedMaxFileCount =
typeof maxFileCount !== 'number' ? resolveWidgetFieldValue(maxFileCount, currentState) : maxFileCount;
const parsedMaxFileCount = typeof maxFileCount !== 'number' ? resolveWidgetFieldValue(maxFileCount) : maxFileCount;
const parsedEnableMultiple =
typeof enableMultiple !== 'boolean' ? resolveWidgetFieldValue(enableMultiple, currentState) : enableMultiple;
const parsedFileType = resolveWidgetFieldValue(fileType, currentState);
const parsedMinSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(minSize, currentState) : minSize;
const parsedMaxSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(maxSize, currentState) : maxSize;
typeof enableMultiple !== 'boolean' ? resolveWidgetFieldValue(enableMultiple) : enableMultiple;
const parsedFileType = resolveWidgetFieldValue(fileType);
const parsedMinSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(minSize) : minSize;
const parsedMaxSize = typeof fileType !== 'number' ? resolveWidgetFieldValue(maxSize) : maxSize;
//* styles definitions
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState) : disabledState;
const parsedWidgetVisibility =
typeof widgetVisibility !== 'boolean' ? resolveWidgetFieldValue(widgetVisibility, currentState) : widgetVisibility;
typeof widgetVisibility !== 'boolean' ? resolveWidgetFieldValue(widgetVisibility) : widgetVisibility;
const { events: allAppEvents } = useAppInfo();

View file

@ -7,7 +7,12 @@ import _, { omit } from 'lodash';
import { Box } from '@/Editor/Box';
import { generateUIComponents } from './FormUtils';
import { useMounted } from '@/_hooks/use-mount';
import { removeFunctionObjects } from '@/_helpers/appUtils';
import {
onComponentClick,
onComponentOptionChanged,
onComponentOptionsChanged,
removeFunctionObjects,
} from '@/_helpers/appUtils';
import { useAppInfo } from '@/_stores/appDataStore';
export const Form = function Form(props) {
const {
@ -15,7 +20,6 @@ export const Form = function Form(props) {
component,
width,
height,
containerProps,
removeComponent,
styles,
setExposedVariable,
@ -25,11 +29,14 @@ export const Form = function Form(props) {
fireEvent,
properties,
resetComponent,
childComponents,
onEvent,
dataCy,
paramUpdated,
adjustHeightBasedOnAlignment,
currentLayout,
mode,
getContainerProps,
containerProps,
childComponents,
} = props;
const { events: allAppEvents } = useAppInfo();
@ -72,8 +79,9 @@ export const Form = function Form(props) {
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isValid]);
}, []);
const extractData = (data) => {
const result = {};
@ -119,7 +127,7 @@ export const Form = function Form(props) {
let formattedChildData = {};
let childValidation = true;
if (childComponents === null) {
if (!childComponents) {
const exposedVariables = {
data: formattedChildData,
isValid: childValidation,
@ -134,7 +142,7 @@ export const Form = function Form(props) {
formattedChildData = extractData(childrenData);
childValidation = checkJsonChildrenValidtion();
} else {
Object.keys(childComponents).forEach((childId) => {
Object.keys(childComponents ?? {}).forEach((childId) => {
if (childrenData[childId]?.name) {
formattedChildData[childrenData[childId].name] = { ...omit(childrenData[childId], 'name'), id: childId };
childValidation = childValidation && (childrenData[childId]?.isValid ?? true);
@ -176,7 +184,7 @@ export const Form = function Form(props) {
document.addEventListener('submitForm', handleFormSubmission);
return () => document.removeEventListener('submitForm', handleFormSubmission);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [buttonToSubmit, isValid, advanced, JSON.stringify(uiComponents)]);
}, [buttonToSubmit, isValid, advanced, JSON.stringify(uiComponents), formEvents]);
const handleSubmit = (event) => {
event.preventDefault();
@ -204,7 +212,7 @@ export const Form = function Form(props) {
return Promise.resolve();
}
onOptionChange({ component, optionName, value, componentId });
return containerProps.onComponentOptionChanged(component, optionName, value);
return onComponentOptionChanged(component, optionName, value);
}
const onOptionChange = ({ component, optionName, value, componentId }) => {
@ -227,7 +235,7 @@ export const Form = function Form(props) {
style={computedStyles}
onSubmit={handleSubmit}
onClick={(e) => {
if (e.target.className === 'real-canvas') containerProps.onComponentClick(id, component);
if (e.target.className === 'real-canvas') onComponentClick(id, component);
}} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked
>
{loadingState ? (
@ -244,7 +252,6 @@ export const Form = function Form(props) {
parentComponent={component}
containerCanvasWidth={width}
parent={id}
{...containerProps}
parentRef={parentRef}
removeComponent={removeComponent}
onOptionChange={function ({ component, optionName, value, componentId }) {
@ -252,12 +259,15 @@ export const Form = function Form(props) {
onOptionChange({ component, optionName, value, componentId });
}
}}
currentPageId={props.currentPageId}
{...props}
{...containerProps}
/>
<SubCustomDragLayer
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
currentLayout={containerProps.currentLayout}
currentLayout={currentLayout}
/>
</>
)}
@ -276,26 +286,26 @@ export const Form = function Form(props) {
key={index}
>
<Box
{...props}
component={item}
id={index}
width={width}
mode={containerProps.mode}
height={item.defaultSize.height}
mode={mode}
inCanvas={true}
paramUpdated={paramUpdated}
onEvent={onEvent}
onComponentOptionChanged={onComponentOptionChangedForSubcontainer}
onComponentOptionsChanged={containerProps.onComponentOptionsChanged}
onComponentClick={containerProps.onComponentClick}
currentState={currentState}
containerProps={containerProps}
onComponentClick={onComponentClick}
darkMode={darkMode}
removeComponent={removeComponent}
// canvasWidth={width}
// readOnly={readOnly}
// customResolvables={customResolvables}
parentId={id}
allComponents={containerProps.allComponents}
sideBarDebugger={containerProps.sideBarDebugger}
childComponents={childComponents}
adjustHeightBasedOnAlignment={adjustHeightBasedOnAlignment}
height={item.defaultSize.height}
getContainerProps={getContainerProps}
onOptionChanged={onComponentOptionChangedForSubcontainer}
onOptionsChanged={onComponentOptionsChanged}
isFromSubContainer={true}
/>
</div>
);

View file

@ -3,7 +3,7 @@ import React, { useRef } from 'react';
import { KanbanBoard } from './KanbanBoard';
export const Kanban = (props) => {
const { height, width, properties, styles, id } = props;
const { height, width, properties, styles, id, mode } = props;
const { showDeleteButton } = properties;
const { visibility, disabledState, boxShadow } = styles;
@ -23,7 +23,7 @@ export const Kanban = (props) => {
ref={parentRef}
data-disabled={disabledState}
>
<KanbanBoard handle kanbanProps={props} parentRef={parentRef} widgetHeight={widgetHeight} />
<KanbanBoard handle kanbanProps={props} parentRef={parentRef} widgetHeight={widgetHeight} id={id} mode={mode} />
</div>
);
};

View file

@ -23,6 +23,7 @@ import { toast } from 'react-hot-toast';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import cx from 'classnames';
import { useGridStore } from '@/_stores/gridStore';
const dropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
@ -36,7 +37,7 @@ const dropAnimation = {
const TRASH_ID = 'void';
export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, mode, id }) {
const { properties, fireEvent, setExposedVariable, setExposedVariables, styles } = kanbanProps;
const { columnData, cardData, cardWidth, cardHeight, showDeleteButton, enableAddCard } = properties;
const { accentColor } = styles;
@ -55,6 +56,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
const cardMovementRef = useRef(null);
const shouldUpdateData = useRef(false);
const droppableItemsColumnId = useRef(0);
const controlBoxRef = useRef(null);
const colAccentColor = {
color: '#fff',
@ -67,6 +69,25 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(columnData)]);
useEffect(() => {
if (!showModal && mode === 'edit') {
controlBoxRef.current?.classList?.remove('modal-moveable');
controlBoxRef.current = null;
if (useGridStore.getState().openModalWidgetId === id) {
useGridStore.getState().actions.setOpenModalWidgetId(null);
}
}
if (showModal) {
useGridStore.getState().actions.setOpenModalWidgetId(id);
/**** Start - Logic to reduce the zIndex of modal control box ****/
controlBoxRef.current = document.querySelector(`.selected-component.sc-${id}`)?.parentElement;
if (mode === 'edit' && controlBoxRef.current) {
controlBoxRef.current.classList.add('modal-moveable');
}
/**** End - Logic to reduce the zIndex of modal control box ****/
}
}, [showModal]);
useEffect(() => {
setItems(() => getCardData(cardData, { ...columnDataAsObj }));
shouldUpdateData.current = true;

View file

@ -1,8 +1,8 @@
import React, { useRef, useState, useEffect } from 'react';
import { SubContainer } from '../SubContainer';
import _ from 'lodash';
import { Pagination } from '@/_components/Pagination';
import { removeFunctionObjects } from '@/_helpers/appUtils';
import _ from 'lodash';
export const Listview = function Listview({
id,
@ -14,10 +14,10 @@ export const Listview = function Listview({
properties,
styles,
fireEvent,
setExposedVariable,
setExposedVariables,
darkMode,
dataCy,
childComponents,
}) {
const fallbackProperties = { height: 100, showBorder: false, data: [] };
const fallbackStyles = { visibility: true, disabledState: false };
@ -80,7 +80,6 @@ export const Listview = function Listview({
useEffect(() => {
const childrenDataClone = _.cloneDeep(childrenData);
const exposedVariables = {
data: removeFunctionObjects(childrenDataClone),
children: childrenData,
@ -94,7 +93,35 @@ export const Listview = function Listview({
setExposedVariables(exposedVariables);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [childrenData]);
}, [childrenData, childComponents]);
function filterComponents() {
if (!childrenData || childrenData.length === 0) {
return [];
}
const componentNamesSet = new Set(
Object.values(childComponents ?? {}).map((component) => component.component.name)
);
const filteredData = _.cloneDeep(childrenData);
if (filteredData?.[0]) {
Object.keys(filteredData?.[0]).forEach((item) => {
if (!componentNamesSet?.has(item)) {
for (const key in filteredData) {
delete filteredData[key][item];
}
}
});
}
return filteredData;
}
useEffect(() => {
const data = filterComponents(childComponents, childrenData);
if (!_.isEqual(data, childrenData)) setChildrenData(data);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [childComponents, childrenData]);
const [currentPage, setCurrentPage] = useState(1);
const pageChanged = (page) => {

View file

@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useCallback, useEffect } from 'react';
import { GoogleMap, LoadScript, Marker, Autocomplete, Polygon } from '@react-google-maps/api';
import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { darkModeStyles } from './styles';
import { useTranslation } from 'react-i18next';
@ -12,7 +12,6 @@ export const Map = function Map({
component,
darkMode,
onComponentClick,
currentState,
onComponentOptionChanged,
onComponentOptionsChanged,
styles,
@ -27,27 +26,27 @@ export const Map = function Map({
const { t } = useTranslation();
const addNewMarkersProp = component.definition.properties.addNewMarkers;
const canAddNewMarkers = addNewMarkersProp ? resolveReferences(addNewMarkersProp.value, currentState) : false;
const canAddNewMarkers = addNewMarkersProp ? resolveWidgetFieldValue(addNewMarkersProp.value) : false;
const canSearchProp = component.definition.properties.canSearch;
const canSearch = canSearchProp ? resolveReferences(canSearchProp.value, currentState) : false;
const canSearch = canSearchProp ? resolveWidgetFieldValue(canSearchProp.value) : false;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState) : disabledState;
let parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
parsedWidgetVisibility = resolveWidgetFieldValue(parsedWidgetVisibility);
} catch (err) {
console.log(err);
}
const [gmap, setGmap] = useState(null);
const [autoComplete, setAutoComplete] = useState(null);
const [mapCenter, setMapCenter] = useState(resolveReferences(center, currentState));
const [mapCenter, setMapCenter] = useState(() => resolveWidgetFieldValue(center));
const [markers, setMarkers] = useState(defaultMarkers);
const containerStyle = {
@ -97,7 +96,7 @@ export const Map = function Map({
}
useEffect(() => {
const resolvedCenter = resolveReferences(center, currentState);
const resolvedCenter = resolveWidgetFieldValue(center);
setMapCenter(resolvedCenter);
onComponentOptionsChanged(component, [['center', addMapUrlToJson(resolvedCenter)]]);
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -126,7 +125,7 @@ export const Map = function Map({
useEffect(() => {
setExposedVariable('setLocation', async function (lat, lng) {
if (lat && lng) setMapCenter(resolveReferences({ lat, lng }, currentState));
if (lat && lng) setMapCenter(resolveWidgetFieldValue({ lat, lng }));
});
}, [setMapCenter]);

View file

@ -3,6 +3,7 @@ import { default as BootstrapModal } from 'react-bootstrap/Modal';
import { SubCustomDragLayer } from '../SubCustomDragLayer';
import { SubContainer } from '../SubContainer';
import { ConfigHandle } from '../ConfigHandle';
import { useGridStore } from '@/_stores/gridStore';
var tinycolor = require('tinycolor2');
export const Modal = function Modal({
@ -18,6 +19,7 @@ export const Modal = function Modal({
fireEvent,
dataCy,
height,
mode,
}) {
const [showModal, setShowModal] = useState(false);
@ -42,11 +44,28 @@ export const Modal = function Modal({
boxShadow,
} = styles;
const parentRef = useRef(null);
const controlBoxRef = useRef(null);
const isInitialRender = useRef(true);
const title = properties.title ?? '';
const size = properties.size ?? 'lg';
/**** Start - Logic to reset the zIndex of modal control box ****/
useEffect(() => {
if (!showModal && mode === 'edit') {
controlBoxRef.current?.classList?.remove('modal-moveable');
controlBoxRef.current = null;
}
if (showModal) {
useGridStore.getState().actions.setOpenModalWidgetId(id);
} else {
if (useGridStore.getState().openModalWidgetId === id) {
useGridStore.getState().actions.setOpenModalWidgetId(null);
}
}
}, [showModal]);
/**** End - Logic to reset the zIndex of modal control box ****/
useEffect(() => {
const exposedVariables = {
open: async function () {
@ -194,6 +213,13 @@ export const Modal = function Modal({
className="jet-button btn btn-primary p-1 overflow-hidden"
style={customStyles.buttonStyles}
onClick={(event) => {
/**** Start - Logic to reduce the zIndex of modal control box ****/
controlBoxRef.current = document.querySelector(`.selected-component.sc-${id}`)?.parentElement;
if (mode === 'edit' && controlBoxRef.current) {
controlBoxRef.current.classList.add('modal-moveable');
}
/**** End - Logic to reduce the zIndex of modal control box ****/
event.stopPropagation();
setShowModal(true);
setExposedVariable('show', true);

View file

@ -3,8 +3,8 @@ import './numberinput.scss';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import * as Icons from '@tabler/icons-react';
import Loader from '@/ToolJetUI/Loader/Loader';
import { resolveReferences } from '@/_helpers/utils';
import { useCurrentState } from '@/_stores/currentStateStore';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
const tinycolor = require('tinycolor2');
import Label from '@/_ui/Label';
@ -19,8 +19,6 @@ export const NumberInput = function NumberInput({
darkMode,
dataCy,
isResizing,
adjustHeightBasedOnAlignment,
currentLayout,
}) {
const { loadingState, disabledState, label, placeholder } = properties;
const {
@ -40,9 +38,9 @@ export const NumberInput = function NumberInput({
} = styles;
const textColor = darkMode && ['#232e3c', '#000000ff'].includes(styles.textColor) ? '#CFD3D8' : styles.textColor;
const isMandatory = resolveReferences(component?.definition?.validation?.mandatory?.value, currentState) ?? false;
const minValue = resolveReferences(component?.definition?.validation?.minValue?.value, currentState) ?? null;
const maxValue = resolveReferences(component?.definition?.validation?.maxValue?.value, currentState) ?? null;
const isMandatory = resolveWidgetFieldValue(component?.definition?.validation?.mandatory?.value) ?? false;
const minValue = resolveWidgetFieldValue(component?.definition?.validation?.minValue?.value) ?? null;
const maxValue = resolveWidgetFieldValue(component?.definition?.validation?.maxValue?.value) ?? null;
const [visibility, setVisibility] = useState(properties.visibility);
const [loading, setLoading] = useState(loadingState);
@ -52,7 +50,7 @@ export const NumberInput = function NumberInput({
const [isFocused, setIsFocused] = useState(false);
const inputRef = useRef(null);
const currentState = useCurrentState();
const [disable, setDisable] = useState(disabledState || loadingState);
const labelRef = useRef();
const _width = (width / 100) * 70; // Max width which label can go is 70% for better UX calculate width based on this value
@ -62,13 +60,6 @@ export const NumberInput = function NumberInput({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [label]);
useEffect(() => {
if (alignment == 'top' && ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)))
adjustHeightBasedOnAlignment(true);
else adjustHeightBasedOnAlignment(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [alignment, label?.length, currentLayout, width, auto]);
useEffect(() => {
setValue(Number(parseFloat(value).toFixed(properties.decimalPlaces)));
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -207,10 +198,12 @@ export const NumberInput = function NumberInput({
setValue(Number(parseFloat(e.target.value)));
if (e.target.value == '') {
setValue(null);
setExposedVariable('value', null).then(fireEvent('onChange'));
setExposedVariable('value', null);
fireEvent('onChange');
}
if (!isNaN(Number(parseFloat(e.target.value)))) {
setExposedVariable('value', Number(parseFloat(e.target.value))).then(fireEvent('onChange'));
setExposedVariable('value', Number(parseFloat(e.target.value)));
fireEvent('onChange');
}
};
useEffect(() => {
@ -232,7 +225,8 @@ export const NumberInput = function NumberInput({
const newValue = (value || 0) + 1;
setValue(newValue);
if (!isNaN(newValue)) {
setExposedVariable('value', newValue).then(fireEvent('onChange'));
setExposedVariable('value', newValue);
fireEvent('onChange');
}
};
const handleDecrement = (e) => {
@ -240,7 +234,8 @@ export const NumberInput = function NumberInput({
const newValue = (value || 0) - 1;
setValue(newValue);
if (!isNaN(newValue)) {
setExposedVariable('value', newValue).then(fireEvent('onChange'));
setExposedVariable('value', newValue);
fireEvent('onChange');
}
};
useEffect(() => {
@ -254,13 +249,15 @@ export const NumberInput = function NumberInput({
if (text) {
const newValue = Number(parseFloat(text));
setValue(newValue);
setExposedVariable('value', text).then(fireEvent('onChange'));
setExposedVariable('value', text);
fireEvent('onChange');
}
});
setExposedVariable('clear', async function () {
setValue('');
setExposedVariable('value', '').then(fireEvent('onChange'));
setExposedVariable('value', '');
fireEvent('onChange');
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View file

@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { resolveReferences } from '@/_helpers/utils';
import { useCurrentState } from '@/_stores/currentStateStore';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import * as Icons from '@tabler/icons-react';
import Loader from '@/ToolJetUI/Loader/Loader';
import SolidIcon from '@/_ui/Icon/SolidIcons';
@ -17,8 +17,6 @@ export const PasswordInput = function PasswordInput({
darkMode,
dataCy,
isResizing,
adjustHeightBasedOnAlignment,
currentLayout,
}) {
const textInputRef = useRef();
const labelRef = useRef();
@ -46,8 +44,8 @@ export const PasswordInput = function PasswordInput({
const [visibility, setVisibility] = useState(properties.visibility);
const { isValid, validationError } = validate(passwordValue);
const [showValidationError, setShowValidationError] = useState(false);
const currentState = useCurrentState();
const isMandatory = resolveReferences(component?.definition?.validation?.mandatory?.value, currentState);
const isMandatory = resolveWidgetFieldValue(component?.definition?.validation?.mandatory?.value);
const [labelWidth, setLabelWidth] = useState(0);
const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side';
const [iconVisibility, setIconVisibility] = useState(false);
@ -170,11 +168,13 @@ export const PasswordInput = function PasswordInput({
useEffect(() => {
setExposedVariable('setText', async function (text) {
setPasswordValue(text);
setExposedVariable('value', text).then(fireEvent('onChange'));
setExposedVariable('value', text);
fireEvent('onChange');
});
setExposedVariable('clear', async function () {
setPasswordValue('');
setExposedVariable('value', '').then(fireEvent('onChange'));
setExposedVariable('value', '');
fireEvent('onChange');
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setPasswordValue]);
@ -184,13 +184,6 @@ export const PasswordInput = function PasswordInput({
const IconElement = Icons[iconName] == undefined ? Icons['IconHome2'] : Icons[iconName];
// eslint-disable-next-line import/namespace
useEffect(() => {
if (alignment == 'top' && ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)))
adjustHeightBasedOnAlignment(true);
else adjustHeightBasedOnAlignment(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [alignment, label?.length, currentLayout, width, auto]);
useEffect(() => {
setExposedVariable('isMandatory', isMandatory);
// eslint-disable-next-line react-hooks/exhaustive-deps

View file

@ -9,7 +9,7 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
const DISABLED_DATE_FORMAT = 'MM/DD/YYYY';
const TjDatepicker = forwardRef(({ value, onClick, styles, dateInputRef, readOnly }, ref) => {
const TjDatepicker = forwardRef(({ value, onClick, styles, dateInputRef, readOnly }) => {
return (
<div className="table-column-datepicker-input-container">
<input

View file

@ -14,7 +14,12 @@ import {
useColumnOrder,
} from 'react-table';
import cx from 'classnames';
import { resolveReferences, validateWidget, determineJustifyContentValue } from '@/_helpers/utils';
import {
resolveReferences,
validateWidget,
determineJustifyContentValue,
resolveWidgetFieldValue,
} from '@/_helpers/utils';
import { useExportData } from 'react-table-plugins';
import Papa from 'papaparse';
import { Pagination } from './Pagination';
@ -404,11 +409,11 @@ export function Table({
let tableData = [],
dynamicColumn = [];
const useDynamicColumn = resolveReferences(component.definition.properties?.useDynamicColumn?.value, currentState);
const useDynamicColumn = resolveWidgetFieldValue(component.definition.properties?.useDynamicColumn?.value);
if (currentState) {
tableData = resolveReferences(component.definition.properties.data.value, currentState, []);
tableData = resolveWidgetFieldValue(component.definition.properties.data.value);
dynamicColumn = useDynamicColumn
? resolveReferences(component.definition.properties?.columnData?.value, currentState, []) ?? []
? resolveWidgetFieldValue(component.definition.properties?.columnData?.value) ?? []
: [];
if (!Array.isArray(tableData)) {
tableData = [];
@ -649,7 +654,7 @@ export function Table({
columns,
data,
defaultColumn,
initialState: { pageIndex: 0, pageSize: -1 },
initialState: { pageIndex: 0, pageSize: 1 },
pageCount: -1,
manualPagination: false,
getExportFileBlob,

File diff suppressed because one or more lines are too long

View file

@ -597,16 +597,13 @@ export default function generateColumnsData({
readOnly={!isEditable}
activeColor={column.activeColor}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original).then(
() => {
fireEvent('OnTableToggleCellChanged', {
column: column,
rowId: cell.row.id,
row: cell.row.original,
tableColumnEvents,
});
}
);
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
fireEvent('OnTableToggleCellChanged', {
column: column,
rowId: cell.row.id,
row: cell.row.original,
tableColumnEvents,
});
}}
/>
</div>

View file

@ -7,7 +7,7 @@ export default function loadPropertiesAndStyles(properties, styles, darkMode, co
const enableNextButton = properties.enableNextButton ?? true;
const enablePrevButton = properties.enablePrevButton ?? true;
const totalRecords = properties.totalRecords ?? '';
const totalRecords = properties.totalRecords ?? 10;
const enabledSort = properties?.enabledSort ?? true;
const hideColumnSelectorButton = properties?.hideColumnSelectorButton ?? false;

View file

@ -1,7 +1,8 @@
import React, { useRef, useState, useEffect } from 'react';
import { SubCustomDragLayer } from '../SubCustomDragLayer';
import { SubContainer } from '../SubContainer';
import { resolveReferences, resolveWidgetFieldValue, isExpectedDataType } from '@/_helpers/utils';
import { resolveWidgetFieldValue, isExpectedDataType } from '@/_helpers/utils';
import { handleLowPriorityWork } from '@/_helpers/editorHelpers';
export const Tabs = function Tabs({
id,
@ -9,7 +10,6 @@ export const Tabs = function Tabs({
width,
height,
containerProps,
currentState,
removeComponent,
setExposedVariable,
setExposedVariables,
@ -24,13 +24,13 @@ export const Tabs = function Tabs({
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const defaultTab = component.definition.properties.defaultTab.value;
// config for tabs. Includes title
const tabs = isExpectedDataType(resolveReferences(component.definition.properties.tabs.value, currentState), 'array');
const tabs = isExpectedDataType(resolveWidgetFieldValue(component.definition.properties?.tabs?.value), 'array');
let parsedTabs = tabs;
parsedTabs = resolveWidgetFieldValue(parsedTabs, currentState);
parsedTabs = resolveWidgetFieldValue(parsedTabs);
const hideTabs = component.definition.properties?.hideTabs?.value ?? false;
// renderOnlyActiveTab - TRUE (renders only the content of the active tab)
// renderOnlyActiveTab - FALSE (renders all the content irrespective of the active tab to persist value from other tabs)
//* renderOnlyActiveTab - TRUE (renders only the content of the active tab)
//* renderOnlyActiveTab - FALSE (renders all the content irrespective of the active tab to persist value from other tabs)
const renderOnlyActiveTab = component.definition.properties?.renderOnlyActiveTab?.value ?? false;
// set index as id if id is not provided
@ -39,25 +39,23 @@ export const Tabs = function Tabs({
// Highlight color - for active tab text and border
const highlightColor = component.definition.styles?.highlightColor?.value ?? '#f44336';
let parsedHighlightColor = highlightColor;
parsedHighlightColor = resolveWidgetFieldValue(highlightColor, currentState);
parsedHighlightColor = resolveWidgetFieldValue(highlightColor);
// Default tab
let parsedDefaultTab = defaultTab;
parsedDefaultTab = resolveWidgetFieldValue(parsedDefaultTab, currentState, 1);
parsedDefaultTab = resolveWidgetFieldValue(parsedDefaultTab, 1);
const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState) : disabledState;
const parsedHideTabs = typeof hideTabs !== 'boolean' ? resolveWidgetFieldValue(hideTabs, currentState) : hideTabs;
const parsedHideTabs = typeof hideTabs !== 'boolean' ? resolveWidgetFieldValue(hideTabs) : hideTabs;
const parsedRenderOnlyActiveTab =
typeof renderOnlyActiveTab !== 'boolean'
? resolveWidgetFieldValue(renderOnlyActiveTab, currentState)
: renderOnlyActiveTab;
typeof renderOnlyActiveTab !== 'boolean' ? resolveWidgetFieldValue(renderOnlyActiveTab) : renderOnlyActiveTab;
let parsedWidgetVisibility = widgetVisibility;
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
parsedWidgetVisibility = resolveWidgetFieldValue(parsedWidgetVisibility);
} catch (err) {
console.log(err);
}
@ -66,6 +64,8 @@ export const Tabs = function Tabs({
const [currentTab, setCurrentTab] = useState(parsedDefaultTab);
const [bgColor, setBgColor] = useState('#fff');
const [tabSwitchingOnProgress, setTabSwitchingOnProgress] = useState(false);
useEffect(() => {
setCurrentTab(parsedDefaultTab);
}, [parsedDefaultTab]);
@ -74,7 +74,7 @@ export const Tabs = function Tabs({
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
}, [currentState, currentTab]);
}, [currentTab, darkMode]);
function computeTabVisibility(componentId, id) {
let tabVisibility = 'hidden';
@ -126,10 +126,18 @@ export const Tabs = function Tabs({
removeComponent={removeComponent}
containerCanvasWidth={width - 4}
parentComponent={component}
readOnly={tab.id !== currentTab}
/>
</div>
);
function shouldRenderTabContent(tab) {
if (tabSwitchingOnProgress || parsedRenderOnlyActiveTab) {
return tab.id === currentTab;
}
return true; // Render by default if no specific conditions are met
}
return (
<div
data-disabled={parsedDisabledState}
@ -152,9 +160,15 @@ export const Tabs = function Tabs({
className="nav-item"
style={{ opacity: tab?.disabled && '0.5', width: tabWidth == 'split' && '33.3%' }}
onClick={() => {
setTabSwitchingOnProgress(true);
!tab?.disabled && setCurrentTab(tab.id);
!tab?.disabled && setExposedVariable('currentTab', tab.id);
fireEvent('onTabSwitch');
handleLowPriorityWork(() => {
fireEvent('onTabSwitch');
setTabSwitchingOnProgress(false);
});
}}
key={tab.id}
>
@ -187,7 +201,8 @@ export const Tabs = function Tabs({
id={`${id}-${tab.id}`}
key={tab.id}
>
{parsedRenderOnlyActiveTab ? tab.id === currentTab && renderTabContent(id, tab) : renderTabContent(id, tab)}
{shouldRenderTabContent(tab) && renderTabContent(id, tab)}
{tab.id === currentTab && (
<SubCustomDragLayer
parent={`${id}-${tab.id}`}

View file

@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { resolveReferences } from '@/_helpers/utils';
import { useCurrentState } from '@/_stores/currentStateStore';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import * as Icons from '@tabler/icons-react';
import Loader from '@/ToolJetUI/Loader/Loader';
const tinycolor = require('tinycolor2');
@ -18,8 +18,6 @@ export const TextInput = function TextInput({
darkMode,
dataCy,
isResizing,
adjustHeightBasedOnAlignment,
currentLayout,
}) {
const textInputRef = useRef();
const labelRef = useRef();
@ -47,8 +45,8 @@ export const TextInput = function TextInput({
const [visibility, setVisibility] = useState(properties.visibility);
const { isValid, validationError } = validate(value);
const [showValidationError, setShowValidationError] = useState(false);
const currentState = useCurrentState();
const isMandatory = resolveReferences(component?.definition?.validation?.mandatory?.value, currentState);
const isMandatory = resolveWidgetFieldValue(component?.definition?.validation?.mandatory?.value);
const [labelWidth, setLabelWidth] = useState(0);
const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side';
const [loading, setLoading] = useState(loadingState);
@ -182,11 +180,13 @@ export const TextInput = function TextInput({
const exposedVariables = {
setText: async function (text) {
setValue(text);
setExposedVariable('value', text).then(fireEvent('onChange'));
setExposedVariable('value', text);
fireEvent('onChange');
},
clear: async function () {
setValue('');
setExposedVariable('value', '').then(fireEvent('onChange'));
setExposedVariable('value', '');
fireEvent('onChange');
},
};
setExposedVariables(exposedVariables);
@ -197,15 +197,6 @@ export const TextInput = function TextInput({
const IconElement = Icons[iconName] == undefined ? Icons['IconHome2'] : Icons[iconName];
// eslint-disable-next-line import/namespace
useEffect(() => {
if (alignment == 'top' && ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)))
adjustHeightBasedOnAlignment(true);
else {
adjustHeightBasedOnAlignment(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [alignment, label?.length, currentLayout, width, auto]);
useEffect(() => {
setExposedVariable('isMandatory', isMandatory);
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -250,7 +241,7 @@ export const TextInput = function TextInput({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disable]);
const renderInput = () => (
return (
<>
<div
data-cy={`label-${String(component.name).toLowerCase()} `}
@ -260,8 +251,8 @@ export const TextInput = function TextInput({
? 'flex-column'
: 'align-items-center '
} ${direction === 'right' && defaultAlignment === 'side' ? 'flex-row-reverse' : ''}
${direction === 'right' && defaultAlignment === 'top' ? 'text-right' : ''}
${visibility || 'invisible'}`}
${direction === 'right' && defaultAlignment === 'top' ? 'text-right' : ''}
${visibility || 'invisible'}`}
style={{
position: 'relative',
whiteSpace: 'nowrap',
@ -366,6 +357,4 @@ export const TextInput = function TextInput({
)}
</>
);
return <>{renderInput()}</>;
};

View file

@ -1,3 +1,4 @@
import { useEditorStore } from '@/_stores/editorStore';
import React from 'react';
export const ConfigHandle = function ConfigHandle({
@ -13,13 +14,18 @@ export const ConfigHandle = function ConfigHandle({
customClassName = '',
configWidgetHandlerForModalComponent = false,
isVersionReleased,
showHandle,
}) {
const shouldShowHandle = useEditorStore((state) => state.hoveredComponent === id) || showHandle;
return (
<div
className={`config-handle ${customClassName}`}
ref={dragRef}
style={{
top: position === 'top' ? '-22px' : widgetTop + widgetHeight - 10,
top: position === 'top' ? '-20px' : widgetTop + widgetHeight - (widgetTop < 10 ? 15 : 10),
visibility: shouldShowHandle && !isMultipleComponentsSelected ? 'visible' : 'hidden',
left: '-1px',
}}
>
<span
@ -36,6 +42,7 @@ export const ConfigHandle = function ConfigHandle({
}}
role="button"
data-cy={`${component.name.toLowerCase()}-config-handle`}
className="text-truncate"
>
<img
style={{ cursor: 'pointer', marginRight: '5px', verticalAlign: 'middle' }}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
import React, { useState, useCallback } from 'react';
import { getComponentToRender } from '@/_helpers/editorHelpers';
import _ from 'lodash';
import { getComponentsToRenders } from '@/_stores/editorStore';
function deepEqualityCheckusingLoDash(obj1, obj2) {
return _.isEqual(obj1, obj2);
}
export const shouldUpdate = (prevProps, nextProps) => {
const listToRender = getComponentsToRenders();
let needToRender = false;
const componentId = prevProps?.id === nextProps?.id ? prevProps?.id : null;
if (componentId) {
const componentToRender = listToRender.find((item) => item === componentId);
const parentReRendered = listToRender.find((item) => item === prevProps?.parentId);
if (componentToRender || parentReRendered) {
needToRender = true;
}
}
// Added to render the defaukt child components
if (prevProps?.childComponents === null && nextProps?.childComponents) return false;
return (
deepEqualityCheckusingLoDash(prevProps?.id, nextProps?.id) &&
deepEqualityCheckusingLoDash(prevProps?.component?.definition, nextProps?.component?.definition) &&
prevProps?.width === nextProps?.width &&
prevProps?.height === nextProps?.height &&
prevProps?.darkMode === nextProps?.darkMode &&
prevProps?.childComponents === nextProps?.childComponents &&
!needToRender
);
};
const ComponentWrapper = React.memo(({ componentName, ...props }) => {
const [key, setKey] = useState(Math.random());
const resetComponent = useCallback(() => {
setKey(Math.random());
}, []);
const ComponentToRender = getComponentToRender(componentName);
if (ComponentToRender === null) return;
if (componentName === 'Form') {
return <ComponentToRender key={key} resetComponent={resetComponent} {...props} />;
}
return <ComponentToRender {...props} />;
}, shouldUpdate);
export default ComponentWrapper;

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { useDragLayer } from 'react-dnd';
import { ItemTypes } from './ItemTypes';
import { ItemTypes } from './editorConstants';
import { BoxDragPreview } from './BoxDragPreview';
import { snapToGrid } from '@/_helpers/appUtils';
import { useEditorStore } from '@/_stores/editorStore';
@ -97,7 +97,7 @@ export const CustomDragLayer = ({ canvasWidth, onDragging }) => {
}
return (
<div style={layerStyles}>
<div style={{ ...layerStyles, ...(isDragging ? { zIndex: 1061 } : {}) }}>
<div
style={getItemStyles(
delta,

View file

@ -0,0 +1,179 @@
.target, .nested-target {
position: absolute;
/* width: 100px;
height: 100px; */
/* top: 150px;
left: 100px; */
/* line-height: 100px; */
/* text-align: center; */
/* background: #ee8; */
/* color: #333; */
/* font-weight: bold; */
box-sizing: border-box;
/* transition: transform 0.1s; */
/* z-index: 3001; */
}
.target.hovered{
z-index: 2;
}
.moveable-control-box>.moveable-control-box:not(.moveable-control-box-d-block, .moveable-dragging, .selected-component){
visibility: hidden !important;
}
.moveable-control-box>.moveable-control-box:hover, .selected-component{
visibility: visible !important;
}
.moveable-control-box>.moveable-control-box:hover, .moveable-control-box>.moveable-dragging{
visibility: visible !important;
}
.moveable-control-box.modal-moveable{
z-index: 3001 !important;
}
.moveable-e.moveable-control{
/* height: 24px !important;
top: -5px !important; */
border-radius: 2px !important;
border: 1px solid #3E63DD !important;
background: #fff !important;
width: 6px !important;
left: 4px !important;
}
.moveable-w.moveable-control{
/* height: 24px !important;
top: -5px !important; */
border-radius: 2px !important;
border: 1px solid #3E63DD !important;
background: #fff !important;
width: 6px !important;
left: 4px !important;
}
.moveable-n.moveable-control{
/* height: 24px !important; */
top: 4px !important;
border-radius: 2px !important;
border: 1px solid #3E63DD !important;
background: #fff !important;
height: 6px !important;
/* left: 3px !important; */
}
.moveable-s.moveable-control{
/* height: 24px !important; */
top: 4px !important;
border-radius: 2px !important;
border: 1px solid #3E63DD !important;
background: #fff !important;
height: 6px !important;
/* left: 3px !important; */
}
.grid-guide-lines {
background: #8DA4EF !important;
}
/* Hides all the control lines*/
/* .moveable-line {
color: transparent !important;
--moveable-color: transparent !important;
}
.moveable-control {
visibility: hidden;
}
.target {
outline: 1px solid #4af;
} */
.active-target, .resizing-target {
outline: 1px solid #4af;
/* z-index: 1000000 !important; */
}
.main-editor-canvas .hovered-target {
outline: 1px solid #4af;
z-index: 4 !important;
}
.moveable-control-box:not([data-able-groupable]) .moveable-control-box:not(:hover) {
opacity: 0;
}
.dragged-movable-control-box, [data-hovered-control="true"] {
opacity: 1 !important;
}
.moveable-line.moveable-e,
.moveable-line.moveable-w {
border: 5px solid #fff0;
}
.moveable-line.moveable-n {
border-bottom: 5px solid #fff0;
}
.moveable-line.moveable-s {
border-bottom: 5px solid #fff0;
}
.moveable-control[data-rotation="0"], .moveable-control[data-rotation="90"],
.moveable-around-control[data-rotation="0"], .moveable-around-control[data-rotation="90"] {
opacity: 0;
width: 0px !important;
height: 0px !important;
}
.resizing-target * {
opacity: 0;
}
.moveable-control {
width: 8px !important;
height: 8px !important;
border: 1px solid var(--moveable-color) !important;
background: #fff !important;
margin-top: -4px !important;
margin-left: -4px !important;
}
.moveable-around-control{
height: 10px !important;
width: 10px !important;
}
.moveable-around-control[data-direction*="nw"] {
left: -11px;
top: -11px;
}
.moveable-around-control[data-direction*="ne"] {
top: -11px;
}
.moveable-around-control[data-direction*="ne"] {
top: -11px;
}
.moveable-around-control[data-direction*="sw"] {
left: -11px;
top: -1px;
}
.moveable-draggable-dragging {
opacity: 1 !important;
}
[data-off-screen="true"] {
display: none;
}
/* */

View file

@ -0,0 +1,844 @@
// import '@/Editor/wdyr';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import Moveable from 'react-moveable';
import { useEditorStore } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
import './DragContainer.css';
import _, { isEmpty } from 'lodash';
import { flushSync } from 'react-dom';
import { restrictedWidgetsObj } from './WidgetManager/restrictedWidgetsConfig';
import { useGridStore, useIsGroupHandleHoverd, useOpenModalWidgetId } from '@/_stores/gridStore';
import toast from 'react-hot-toast';
import { individualGroupableProps } from './gridUtils';
const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, bottom: 0, position: 'css' };
const RESIZABLE_CONFIG = {
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
};
export default function DragContainer({
widgets,
mode,
onResizeStop,
onDrag,
gridWidth,
selectedComponents = [],
currentLayout,
draggedSubContainer,
}) {
const lastDraggedEventsRef = useRef(null);
const boxes = Object.keys(widgets).map((key) => ({ ...widgets[key], id: key }));
const isGroupHandleHoverd = useIsGroupHandleHoverd();
const openModalWidgetId = useOpenModalWidgetId();
const configHandleForMultiple = (id) => {
return (
<div
className={'multiple-components-config-handle'}
onMouseUpCapture={() => {
if (lastDraggedEventsRef.current) {
const preant = boxes.find((box) => box.id == lastDraggedEventsRef.current.events[0].target.id)?.component
?.parent;
// Adding the new updates to the macro task queue to unblock UI
onDrag(
lastDraggedEventsRef.current.events.map((ev) => ({
id: ev.target.id,
x: ev.translate[0],
y: ev.translate[1],
parent: preant,
}))
);
}
if (useGridStore.getState().isGroupHandleHoverd) {
useGridStore.getState().actions.setIsGroupHandleHoverd(false);
}
const parentElm = lastDraggedEventsRef?.current?.events?.[0]?.target?.closest('.real-canvas');
if (parentElm && parentElm?.classList?.contains('show-grid')) {
parentElm?.classList?.remove('show-grid');
}
}}
onMouseDownCapture={() => {
lastDraggedEventsRef.current = null;
if (!useGridStore.getState().isGroupHandleHoverd) {
useGridStore.getState().actions.setIsGroupHandleHoverd(true);
}
}}
>
<span className="badge handle-content" id={id} style={{ background: '#4d72fa' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<img
style={{ cursor: 'pointer', marginRight: '5px', verticalAlign: 'middle' }}
src="assets/images/icons/settings.svg"
width="12"
height="12"
draggable="false"
/>
<span>components</span>
</div>
</span>
</div>
);
};
const DimensionViewable = {
name: 'dimensionViewable',
props: [],
events: [],
render() {
return configHandleForMultiple('multiple-components-config-handle');
},
};
const MouseCustomAble = {
name: 'mouseTest',
props: {},
events: {},
mouseEnter(e) {
const controlBoxes = document.getElementsByClassName('moveable-control-box');
for (const element of controlBoxes) {
element.classList.remove('moveable-control-box-d-block');
}
e.props.target.classList.add('hovered');
e.controlBox.classList.add('moveable-control-box-d-block');
},
mouseLeave(e) {
e.props.target.classList.remove('hovered');
e.controlBox.classList.remove('moveable-control-box-d-block');
},
};
const moveableRef = useRef();
const draggedOverElemRef = useRef(null);
const childMoveableRefs = useRef({});
const groupResizeDataRef = useRef([]);
const isDraggingRef = useRef(false);
const boxList = boxes
.filter((box) =>
['{{true}}', true].includes(
box?.component?.definition?.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value
)
)
.map((box) => ({
id: box.id,
height: box?.layouts?.[currentLayout]?.height,
left: box?.layouts?.[currentLayout]?.left,
top: box?.layouts?.[currentLayout]?.top,
width: box?.layouts?.[currentLayout]?.width,
parent: box?.component?.parent,
}));
const [list, setList] = useState(boxList);
const hoveredComponent = useEditorStore((state) => state?.hoveredComponent, shallow);
useEffect(() => {
if (!moveableRef.current) {
return;
}
moveableRef.current.updateRect();
moveableRef.current.updateTarget();
moveableRef.current.updateSelectors();
for (let refObj of Object.values(childMoveableRefs.current)) {
if (refObj) {
refObj.updateRect();
refObj.updateTarget();
refObj.updateSelectors();
}
}
setTimeout(reloadGrid, 100);
try {
const boxes = document.querySelectorAll('.jet-container');
var timer;
boxes.forEach((box) => {
box.addEventListener('scroll', function handleClick() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
reloadGrid();
}, 250); //Threshold is 100ms
});
});
} catch (error) {
console.error('Error---->', error);
}
}, [hoveredComponent, reloadGrid]);
useEffect(() => {
setList(boxList);
setTimeout(reloadGrid, 100);
}, [currentLayout]);
useEffect(() => {
const controlBoxes = document.querySelectorAll('.moveable-control-box[target-id]');
controlBoxes.forEach((box) => {
box.style.display = '';
});
if (openModalWidgetId) {
const children = findChildrenAndGrandchildren(openModalWidgetId, boxes);
const controlBoxes = document.querySelectorAll('.moveable-control-box[target-id]');
controlBoxes.forEach((box) => {
const id = box.getAttribute('target-id');
if (!children.includes(id)) {
box.style.display = 'none';
}
});
}
}, [openModalWidgetId, selectedComponents]);
const reloadGrid = useCallback(async () => {
if (moveableRef.current) {
moveableRef.current.updateRect();
moveableRef.current.updateTarget();
moveableRef.current.updateSelectors();
}
Array.isArray(moveableRef.current?.moveable?.moveables) &&
moveableRef.current?.moveable?.moveables.forEach((moveable) => {
const {
props: { target },
controlBox,
} = moveable;
controlBox.setAttribute('target-id', target.id);
});
const selectedComponentsId = new Set(
selectedComponents.map((component) => {
return component.id;
})
);
// Get all elements with the old class name
var elements = document.getElementsByClassName('selected-component');
// Iterate through the elements and replace the old class with the new one
for (var i = 0; i < elements.length; i++) {
elements[i].className = 'moveable-control-box modal-moveable rCS1w3zcxh';
}
const controlBoxes = moveableRef?.current?.moveable?.getMoveables();
if (controlBoxes) {
for (const element of controlBoxes) {
if (selectedComponentsId.has(element?.props?.target?.id)) {
element?.controlBox?.classList.add('selected-component', `sc-${element?.props?.target?.id}`);
}
}
}
}, [selectedComponents]);
useEffect(() => {
setList(boxList);
}, [JSON.stringify(boxes)]);
const groupedTargets = [
...findHighestLevelofSelection(selectedComponents).map((component) => '.ele-' + component.id),
];
useEffect(() => {
reloadGrid();
}, [selectedComponents, openModalWidgetId, widgets]);
const updateNewPosition = (events, parent = null) => {
const posWithParent = {
events,
parent,
};
lastDraggedEventsRef.current = posWithParent;
};
return mode === 'edit' ? (
<>
<Moveable
dragTargetSelf={true}
dragTarget={isGroupHandleHoverd ? document.getElementById('multiple-components-config-handle') : undefined}
ref={moveableRef}
ables={[MouseCustomAble, DimensionViewable]}
props={{
mouseTest: groupedTargets.length < 2,
dimensionViewable: groupedTargets.length > 1,
}}
flushSync={flushSync}
target={groupedTargets?.length > 1 ? groupedTargets : '.target'}
origin={false}
individualGroupable={groupedTargets.length <= 1}
draggable={true}
resizable={RESIZABLE_CONFIG}
keepRatio={false}
// key={list.length}
individualGroupableProps={individualGroupableProps}
onResize={(e) => {
const currentLayout = list.find(({ id }) => id === e.target.id);
const currentWidget = boxes.find(({ id }) => id === e.target.id);
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
useGridStore.getState().actions.setDragTarget(currentWidget.component?.parent);
const currentWidth = currentLayout.width * _gridWidth;
const diffWidth = e.width - currentWidth;
const diffHeight = e.height - currentLayout.height;
const isLeftChanged = e.direction[0] === -1;
const isTopChanged = e.direction[1] === -1;
let transformX = currentLayout.left * _gridWidth;
let transformY = currentLayout.top;
if (isLeftChanged) {
transformX = currentLayout.left * _gridWidth - diffWidth;
}
if (isTopChanged) {
transformY = currentLayout.top - diffHeight;
}
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
const containerWidth = elemContainer.clientWidth;
const maxY = containerHeight - e.target.clientHeight;
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${e.width}px`;
}
if (!maxHeightHit || e.height < e.target.clientHeight) {
e.target.style.height = `${e.height}px`;
}
e.target.style.transform = `translate(${transformX}px, ${transformY}px)`;
}}
onResizeEnd={(e) => {
try {
useGridStore.getState().actions.setResizingComponentId(null);
// setIsResizing(false);
const currentWidget = boxes.find(({ id }) => {
return id === e.target.id;
});
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid');
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
let width = Math.round(e.lastEvent.width / _gridWidth) * _gridWidth;
const height = Math.round(e.lastEvent.height / 10) * 10;
const currentLayout = list.find(({ id }) => id === e.target.id);
const currentWidth = currentLayout.width * _gridWidth;
const diffWidth = e.lastEvent.width - currentWidth;
const diffHeight = e.lastEvent.height - currentLayout.height;
const isLeftChanged = e.lastEvent.direction[0] === -1;
const isTopChanged = e.lastEvent.direction[1] === -1;
let transformX = currentLayout.left * _gridWidth;
let transformY = currentLayout.top;
if (isLeftChanged) {
transformX = currentLayout.left * _gridWidth - diffWidth;
}
if (isTopChanged) {
transformY = currentLayout.top - diffHeight;
}
width = adjustWidth(width, transformX, _gridWidth);
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
const containerWidth = elemContainer.clientWidth;
const maxY = containerHeight - e.target.clientHeight;
const maxLeft = containerWidth - e.target.clientWidth;
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
const roundedTransformY = Math.round(transformY / 10) * 10;
transformY = transformY % 10 === 5 ? roundedTransformY - 10 : roundedTransformY;
e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${
Math.round(transformY / 10) * 10
}px)`;
if (!maxWidthHit || e.width < e.target.clientWidth) {
e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`;
}
if (!maxHeightHit || e.height < e.target.clientHeight) {
e.target.style.height = `${Math.round(e.lastEvent.height / 10) * 10}px`;
}
const resizeData = {
id: e.target.id,
height: height,
width: width,
x: transformX,
y: transformY,
};
if (currentWidget.component?.parent) {
resizeData.gw = _gridWidth;
}
// Adding the new updates to the macro task queue to unblock UI
// setTimeout(() => {
// });
onResizeStop([resizeData]);
} catch (error) {
console.error('ResizeEnd error ->', error);
}
useGridStore.getState().actions.setDragTarget();
}}
onResizeStart={(e) => {
performance.mark('onResizeStart');
useGridStore.getState().actions.setResizingComponentId(e.target.id);
e.setMin([gridWidth, 10]);
}}
onResizeGroupStart={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
parentElm.classList.add('show-grid');
}}
onResizeGroup={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
const parentWidth = parentElm?.clientWidth;
const parentHeight = parentElm?.clientHeight;
const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight);
events.forEach((ev) => {
ev.target.style.width = `${ev.width}px`;
ev.target.style.height = `${ev.height}px`;
ev.target.style.transform = ev.drag.transform;
});
if (!(posLeft < 0 || posTop < 0 || posRight < 0 || posBottom < 0)) {
groupResizeDataRef.current = events;
}
}}
onResizeGroupEnd={(e) => {
try {
const { events } = e;
const newBoxs = [];
const parentElm = events[0].target.closest('.real-canvas');
parentElm.classList.remove('show-grid');
// TODO: Logic needs to be relooked post go live P2
groupResizeDataRef.current.forEach((ev) => {
const currentWidget = boxes.find(({ id }) => {
return id === ev.target.id;
});
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
let width = Math.round(ev.width / _gridWidth) * _gridWidth;
width = width < _gridWidth ? _gridWidth : width;
let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth;
let posY = Math.round(ev.drag.translate[1] / 10) * 10;
let height = Math.round(ev.height / 10) * 10;
height = height < 10 ? 10 : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
newBoxs.push({
id: ev.target.id,
height: height,
width: width,
x: posX,
y: posY,
gw: _gridWidth,
});
});
if (groupResizeDataRef.current.length) {
// Adding the new updates to the macro task queue to unblock UI
// setTimeout(() => {
// });
onResizeStop(newBoxs);
} else {
events.forEach((ev) => {
const currentWidget = boxes.find(({ id }) => {
return id === ev.target.id;
});
let _gridWidth =
useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
let width = currentWidget?.layouts[currentLayout].width * _gridWidth;
let posX = currentWidget?.layouts[currentLayout].left * _gridWidth;
let posY = currentWidget?.layouts[currentLayout].top;
let height = currentWidget?.layouts[currentLayout].height;
height = height < 10 ? 10 : height;
ev.target.style.width = `${width}px`;
ev.target.style.height = `${height}px`;
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
});
}
groupResizeDataRef.current = [];
reloadGrid();
} catch (error) {
console.error('Error resizing group', error);
}
}}
checkInput
onDragStart={(e) => {
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
const box = boxes.find((box) => box.id === e.target.id);
let isDragOnTable = false;
/* Checking if the dragged elemenent is a table. If its a table drag is disabled since it will affect column resizing and reordering */
if (box?.component?.component === 'Table') {
const tableElem = e.target.querySelector('.jet-data-table');
isDragOnTable = tableElem.contains(e.inputEvent.target);
}
if (
['RangeSlider', 'Container', 'BoundedBox', 'Kanban'].includes(box?.component?.component) ||
isDragOnTable
) {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
const isHandle = targetElems.find((ele) => ele.classList.contains('handle-content'));
if (!isHandle) {
return false;
}
}
if (hoveredComponent !== e.target.id) {
return false;
}
}}
onDragEnd={(e) => {
try {
if (isDraggingRef.current) {
useGridStore.getState().actions.setDraggingComponentId(null);
isDraggingRef.current = false;
}
if (draggedSubContainer) {
return;
}
let draggedOverElemId = widgets[e.target.id]?.component?.parent;
let draggedOverElemIdType;
const parentComponent = widgets[widgets[e.target.id]?.component?.parent];
let draggedOverElem;
if (document.elementFromPoint(e.clientX, e.clientY) && parentComponent?.component?.component !== 'Modal') {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
draggedOverElem = targetElems.find((ele) => {
const isOwnChild = e.target.contains(ele); // if the hovered element is a child of actual draged element its not considered
if (isOwnChild) return false;
let isDroppable = ele.id !== e.target.id && ele.classList.contains('drag-container-parent');
if (isDroppable) {
// debugger;
let widgetId = ele?.getAttribute('component-id') || ele.id;
let widgetType = boxes.find(({ id }) => id === widgetId)?.component?.component;
if (!widgetType) {
widgetId = widgetId.split('-').slice(0, -1).join('-');
widgetType = boxes.find(({ id }) => id === widgetId)?.component?.component;
}
if (
!['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes(
widgetType
)
) {
isDroppable = false;
}
}
return isDroppable;
});
draggedOverElemId = draggedOverElem?.getAttribute('component-id') || draggedOverElem?.id;
draggedOverElemIdType = draggedOverElem?.getAttribute('data-parent-type');
}
const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth;
const currentParentId = boxes.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent;
let left = e.lastEvent.translate[0];
let top = e.lastEvent.translate[1];
if (['Listview', 'Kanban'].includes(widgets[draggedOverElemId]?.component?.component)) {
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
const maxY = containerHeight - e.target.clientHeight;
top = top > maxY ? maxY : top;
}
const currentWidget = boxes.find(({ id }) => id === e.target.id)?.component?.component;
const parentWidget = draggedOverElemIdType === 'Kanban' ? 'Kanban_card' : draggedOverElemIdType;
const restrictedWidgets = restrictedWidgetsObj?.[parentWidget] || [];
const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
if (draggedOverElemId !== currentParentId) {
// debugger;
if (isParentChangeAllowed) {
const draggedOverWidget = widgets[draggedOverElemId];
let { left: _left, top: _top } = getMouseDistanceFromParentDiv(
e,
draggedOverWidget?.component?.component === 'Kanban' ? draggedOverElem : draggedOverElemId,
widgets[draggedOverElemId]?.component?.component
);
left = _left;
top = _top;
} else {
const currBox = list.find((l) => l.id === e.target.id);
left = currBox.left * gridWidth;
top = currBox.top;
toast.error(`${currentWidget} is not compatible as a child component of ${parentWidget}`);
e.target.style.transform = `translate(${left}px, ${top}px)`;
}
}
e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${
Math.round(top / 10) * 10
}px)`;
if (draggedOverElemId === currentParentId || isParentChangeAllowed) {
// Adding the new updates to the macro task queue to unblock UI
// setTimeout(() =>
// );
onDrag([
{
id: e.target.id,
x: left,
y: Math.round(top / 10) * 10,
parent: isParentChangeAllowed ? draggedOverElemId : undefined,
},
]);
}
const box = boxes.find((box) => box.id === e.target.id);
setTimeout(() => useEditorStore.getState().actions.setSelectedComponents([{ ...box }]));
} catch (error) {
console.log('draggedOverElemId->error', error);
}
var canvasElms = document.getElementsByClassName('sub-canvas');
var elementsArray = Array.from(canvasElms);
elementsArray.forEach(function (element) {
element.classList.remove('show-grid');
element.classList.add('hide-grid');
});
}}
onDrag={(e) => {
if (!isDraggingRef.current) {
useGridStore.getState().actions.setDraggingComponentId(e.target.id);
isDraggingRef.current = true;
}
if (draggedSubContainer) {
return;
}
if (!draggedSubContainer) {
const parentComponent = widgets[widgets[e.target.id]?.component?.parent];
let top = e.translate[1];
let left = e.translate[0];
if (parentComponent?.component?.component === 'Modal') {
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
const containerWidth = elemContainer.clientWidth;
const maxY = containerHeight - e.target.clientHeight;
const maxLeft = containerWidth - e.target.clientWidth;
top = top < 0 ? 0 : top > maxY ? maxY : top;
left = left < 0 ? 0 : left > maxLeft ? maxLeft : left;
}
e.target.style.transform = `translate(${left}px, ${top}px)`;
e.target.setAttribute(
'widget-pos2',
`translate: ${e.translate[0]} | Round: ${
Math.round(e.translate[0] / gridWidth) * gridWidth
} | ${gridWidth}`
);
}
if (document.elementFromPoint(e.clientX, e.clientY)) {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
const draggedOverElements = targetElems.filter(
(ele) =>
ele.id !== e.target.id && (ele.classList.contains('target') || ele.classList.contains('real-canvas'))
);
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
var canvasElms = document.getElementsByClassName('sub-canvas');
var elementsArray = Array.from(canvasElms);
elementsArray.forEach(function (element) {
element.classList.remove('show-grid');
element.classList.add('hide-grid');
});
const parentWidgetId = draggedOverContainer.getAttribute('data-parent') || draggedOverElem?.id;
document.getElementById('canvas-' + parentWidgetId)?.classList.add('show-grid');
useGridStore.getState().actions.setDragTarget(parentWidgetId);
if (
draggedOverElemRef.current?.id !== draggedOverContainer?.id &&
!draggedOverContainer.classList.contains('hide-grid')
) {
draggedOverContainer.classList.add('show-grid');
draggedOverElemRef.current && draggedOverElemRef.current.classList.remove('show-grid');
draggedOverElemRef.current = draggedOverContainer;
}
}
const offset = getOffset(e.target, document.querySelector('#real-canvas'));
if (document.getElementById('moveable-drag-ghost')) {
document.getElementById('moveable-drag-ghost').style.transform = `translate(${offset.x}px, ${offset.y}px)`;
document.getElementById('moveable-drag-ghost').style.width = `${e.target.clientWidth}px`;
document.getElementById('moveable-drag-ghost').style.height = `${e.target.clientHeight}px`;
}
}}
onDragGroup={(ev) => {
const { events } = ev;
const parentElm = events[0]?.target?.closest('.real-canvas');
if (parentElm && !parentElm.classList.contains('show-grid')) {
parentElm?.classList?.add('show-grid');
}
events.forEach((ev) => {
let posX = ev.translate[0];
let posY = ev.translate[1];
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
});
updateNewPosition(events);
}}
onDragGroupStart={({ events }) => {
const parentElm = events[0]?.target?.closest('.real-canvas');
parentElm?.classList?.add('show-grid');
}}
onDragGroupEnd={(e) => {
try {
const { events } = e;
const parentId = widgets[events[0]?.target?.id]?.component?.parent;
const parentElm = events[0].target.closest('.real-canvas');
parentElm.classList.remove('show-grid');
const parentWidth = parentElm?.clientWidth;
const parentHeight = parentElm?.clientHeight;
const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight);
const _gridWidth = useGridStore.getState().subContainerWidths[parentId] || gridWidth;
onDrag(
events.map((ev) => {
let posX = ev.lastEvent.translate[0];
let posY = ev.lastEvent.translate[1];
if (posLeft < 0) {
posX = ev.lastEvent.translate[0] - posLeft;
}
if (posTop < 0) {
posY = ev.lastEvent.translate[1] - posTop;
}
if (posRight < 0) {
posX = ev.lastEvent.translate[0] + posRight;
}
if (posBottom < 0) {
posY = ev.lastEvent.translate[1] + posBottom;
}
ev.target.style.transform = `translate(${Math.round(posX / _gridWidth) * _gridWidth}px, ${
Math.round(posY / 10) * 10
}px)`;
return {
id: ev.target.id,
x: posX,
y: posY,
parent: parentId,
};
})
);
} catch (error) {
console.error('Error dragging group', error);
}
}}
//snap settgins
snappable={true}
snapThreshold={10}
isDisplaySnapDigit={false}
bounds={CANVAS_BOUNDS}
displayAroundControls={true}
controlPadding={20}
/>
</>
) : (
''
);
}
function getMouseDistanceFromParentDiv(event, id, parentWidgetType) {
let parentDiv = id
? typeof id === 'string'
? document.getElementById(id)
: id
: document.getElementsByClassName('real-canvas')[0];
if (parentWidgetType === 'Container') {
parentDiv = document.getElementById('canvas-' + id);
}
// Get the bounding rectangle of the parent div.
const parentDivRect = parentDiv.getBoundingClientRect();
const targetDivRect = event.target.getBoundingClientRect();
const mouseX = targetDivRect.left - parentDivRect.left;
const mouseY = targetDivRect.top - parentDivRect.top;
// Calculate the distance from the mouse pointer to the top and left edges of the parent div.
const top = mouseY;
const left = mouseX;
return { top, left };
}
export function findHighestLevelofSelection(selectedComponents) {
let result = [...selectedComponents];
if (selectedComponents.some((widget) => !widget?.component?.parent)) {
result = selectedComponents.filter((widget) => !widget?.component?.parent);
} else {
result = selectedComponents.filter(
(widget) => widget?.component?.parent === selectedComponents[0]?.component?.parent
);
}
return result;
}
function findChildrenAndGrandchildren(parentId, widgets) {
if (isEmpty(widgets)) {
return [];
}
const type = widgets.find(({ id }) => id === parentId)?.component?.component;
let pid = parentId;
if (type === 'Kanban') {
pid = pid + '-modal';
}
const children = widgets.filter((widget) => widget?.component?.parent === pid);
let result = [];
for (const child of children) {
result.push(child.id);
result = result.concat(...findChildrenAndGrandchildren(child.id));
}
return result;
}
function adjustWidth(width, posX, gridWidth) {
posX = Math.round(posX / gridWidth);
width = Math.round(width / gridWidth);
if (posX + width > 43) {
width = 43 - posX;
}
return width * gridWidth;
}
function getPositionForGroupDrag(events, parentWidth, parentHeight) {
return events.reduce((positions, ev) => {
const eventObj = ev.lastEvent ? ev.lastEvent : ev;
const { width, height } = eventObj;
const {
translate: [elemPosX, elemPosY],
} = eventObj.drag ? eventObj.drag : eventObj;
return {
...positions,
posRight: Math.min(
positions.posRight ?? Infinity, // Handle potential initial undefined value
parentWidth - (width + elemPosX)
),
posBottom: Math.min(positions.posBottom ?? Infinity, parentHeight - (height + elemPosY)),
posLeft: Math.min(positions.posLeft ?? Infinity, elemPosX),
posTop: Math.min(positions.posTop ?? Infinity, elemPosY),
};
}, {});
}
function getOffset(childElement, grandparentElement) {
if (!childElement || !grandparentElement) return null;
// Get bounding rectangles for both elements
const childRect = childElement.getBoundingClientRect();
const grandparentRect = grandparentElement.getBoundingClientRect();
// Calculate offset by subtracting grandparent's position from child's position
const offsetX = childRect.left - grandparentRect.left;
const offsetY = childRect.top - grandparentRect.top;
return { x: offsetX, y: offsetY };
}

View file

@ -2,26 +2,18 @@
import React, { useCallback, useEffect, useState } from 'react';
import cx from 'classnames';
import { useDrag } from 'react-dnd';
import { ItemTypes } from './ItemTypes';
import { ItemTypes } from './editorConstants';
import { getEmptyImage } from 'react-dnd-html5-backend';
import { Box } from './Box';
import { ConfigHandle } from './ConfigHandle';
import { Rnd } from 'react-rnd';
import { resolveWidgetFieldValue, resolveReferences } from '@/_helpers/utils';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import ErrorBoundary from './ErrorBoundary';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useEditorStore } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
import { useNoOfGrid, useGridStore } from '@/_stores/gridStore';
import WidgetBox from './WidgetBox';
import * as Sentry from '@sentry/react';
const NO_OF_GRIDS = 43;
const resizerClasses = {
topRight: 'top-right',
bottomRight: 'bottom-right',
bottomLeft: 'bottom-left',
topLeft: 'top-left',
};
import { findHighestLevelofSelection } from './DragContainer';
function computeWidth(currentLayoutOptions) {
return `${currentLayoutOptions?.width}%`;
@ -37,90 +29,56 @@ function getStyles(isDragging, isSelectedComponent) {
};
}
export const DraggableBox = React.memo(
const DraggableBox = React.memo(
({
id,
className,
mode,
title,
parent,
allComponents,
component,
index,
inCanvas,
onEvent,
onComponentClick,
onComponentOptionChanged,
onComponentOptionsChanged,
onResizeStop,
onDragStop,
paramUpdated,
resizingStatusChanged,
zoomLevel,
containerProps,
setSelectedComponent,
removeComponent,
layouts,
draggingStatusChanged,
darkMode,
canvasWidth,
readOnly,
customResolvables,
parentId,
sideBarDebugger,
getContainerProps,
currentPageId,
onComponentOptionChanged = null,
onComponentOptionsChanged = null,
isFromSubContainer = false,
childComponents = null,
}) => {
const [isResizing, setResizing] = useState(false);
const [isDragging2, setDragging] = useState(false);
const isResizing = useGridStore((state) => state.resizingComponentId === id);
const [canDrag, setCanDrag] = useState(true);
const noOfGrid = useNoOfGrid();
const {
currentLayout,
setHoveredComponent,
mouseOver,
selectionInProgress,
isSelectedComponent,
isMultipleComponentsSelected,
autoComputeLayout,
} = useEditorStore(
(state) => ({
currentLayout: state?.currentLayout,
setHoveredComponent: state?.actions?.setHoveredComponent,
mouseOver: state?.hoveredComponent === id,
selectionInProgress: state?.selectionInProgress,
isSelectedComponent:
mode === 'edit' ? state?.selectedComponents?.some((component) => component?.id === id) : false,
isMultipleComponentsSelected: state?.selectedComponents?.length > 1 ? true : false,
isMultipleComponentsSelected: findHighestLevelofSelection(state?.selectedComponents)?.length > 1 ? true : false,
autoComputeLayout: state?.appDefinition?.pages?.[state?.currentPageId]?.autoComputeLayout,
}),
shallow
);
const currentState = useCurrentState();
const [boxHeight, setboxHeight] = useState(layoutData?.height); // height for layouting with top and side values
const resizerStyles = {
topRight: {
width: '8px',
height: '8px',
right: '-4px',
top: '-4px',
},
bottomRight: {
width: '8px',
height: '8px',
right: '-4px',
bottom: '-4px',
},
bottomLeft: {
width: '8px',
height: '8px',
left: '-4px',
bottom: '-4px',
},
topLeft: {
width: '8px',
height: '8px',
left: '-4px',
top: '-4px',
},
};
const [{ isDragging }, drag, preview] = useDrag(
() => ({
@ -134,40 +92,19 @@ export const DraggableBox = React.memo(
layouts,
canvasWidth,
currentLayout,
autoComputeLayout,
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[id, title, component, index, currentLayout, zoomLevel, parent, layouts, canvasWidth]
[id, title, component, index, currentLayout, zoomLevel, parent, layouts, canvasWidth, autoComputeLayout]
);
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true });
}, [isDragging]);
useEffect(() => {
if (resizingStatusChanged) {
resizingStatusChanged(isResizing);
}
}, [isResizing]);
useEffect(() => {
if (draggingStatusChanged) {
draggingStatusChanged(isDragging2);
}
if (isDragging2 && !isSelectedComponent) {
setSelectedComponent(id, component);
}
}, [isDragging2]);
const style = {
display: 'inline-block',
alignItems: 'center',
justifyContent: 'center',
};
let _refProps = {};
if (mode === 'edit' && canDrag) {
@ -186,57 +123,31 @@ export const DraggableBox = React.memo(
const defaultData = {
top: 100,
left: 0,
width: 445,
width: 43,
height: 500,
};
const layoutData = inCanvas ? layouts[currentLayout] || defaultData : defaultData;
const gridWidth = canvasWidth / NO_OF_GRIDS;
const width = (canvasWidth * layoutData.width) / NO_OF_GRIDS;
const layoutData = inCanvas ? layouts[currentLayout] || layouts['desktop'] : defaultData;
const width = (canvasWidth * layoutData.width) / noOfGrid;
const configWidgetHandlerForModalComponent =
!isSelectedComponent &&
component.component === 'Modal' &&
resolveWidgetFieldValue(component.definition.properties.useDefaultButton, currentState)?.value === false;
resolveWidgetFieldValue(component.definition.properties.useDefaultButton?.value) === false;
const onComponentHover = (id) => {
if (selectionInProgress) return;
setHoveredComponent(id);
};
const onComponentHover = useCallback(
(id) => {
if (selectionInProgress) return;
setHoveredComponent(id);
},
[id]
);
const { label = { value: null } } = component?.definition?.properties ?? {};
useEffect(() => {
if (
component.component == 'TextInput' ||
component.component == 'PasswordInput' ||
component.component == 'NumberInput'
) {
const { alignment = { value: null } } = component?.definition?.styles ?? {};
let newHeight = layoutData?.height;
if (alignment?.value && resolveReferences(alignment?.value, currentState, null, customResolvables) === 'top') {
const { width = { value: null } } = component?.definition?.styles ?? {};
const { auto = { value: null } } = component?.definition?.styles ?? {};
const resolvedWidth = resolveReferences(width?.value, currentState, null, customResolvables);
const resolvedAuto = resolveReferences(auto?.value, currentState, null, customResolvables);
if (
(label?.value?.length > 0 && resolvedWidth > 0) ||
(resolvedAuto && resolvedWidth == 0 && label?.value && label?.value?.length != 0)
) {
newHeight = layoutData?.height + 20;
}
}
setboxHeight(newHeight);
}
}, [layoutData?.height, label?.value?.length, currentLayout]);
const adjustHeightBasedOnAlignment = (increase) => {
if (increase) return setboxHeight(layoutData?.height + 20);
else return setboxHeight(layoutData?.height);
};
return (
<div
className={
inCanvas
? ''
? 'widget-in-canvas'
: cx('text-center align-items-center clearfix draggable-box-wrapper', {
'': component.component !== 'KanbanBoard',
'd-none': component.component === 'KanbanBoard',
@ -246,124 +157,80 @@ export const DraggableBox = React.memo(
>
{inCanvas ? (
<div
className={cx(`draggable-box widget-${id}`, {
className={cx(`draggable-box w-100 widget-${id}`, {
[className]: !!className,
'draggable-box-in-editor': mode === 'edit',
})}
onMouseEnter={(e) => {
if (e.currentTarget.className.includes(`widget-${id}`)) {
onComponentHover?.(id);
if (useGridStore.getState().draggingComponentId) return;
const closestDraggableBox = e.target.closest('.draggable-box');
if (closestDraggableBox) {
const classNames = closestDraggableBox.className.split(' ');
let compId = null;
classNames.forEach((className) => {
if (className.startsWith('widget-')) {
compId = className.replace('widget-', '');
}
});
onComponentHover?.(compId);
e.stopPropagation();
}
}}
onMouseLeave={() => {
if (useGridStore.getState().draggingComponentId) return;
setHoveredComponent('');
}}
style={getStyles(isDragging, isSelectedComponent)}
>
<Rnd
maxWidth={canvasWidth}
style={{ ...style }}
resizeGrid={[gridWidth, 10]}
dragGrid={[gridWidth, 10]}
size={{
width: width,
height: boxHeight,
}}
position={{
x: layoutData ? (layoutData.left * canvasWidth) / 100 : 0,
y: layoutData ? layoutData.top : 0,
}}
defaultSize={{}}
className={`resizer ${
mouseOver || isResizing || isDragging2 || isSelectedComponent ? 'resizer-active' : ''
} `}
onResize={() => setResizing(true)}
onDrag={(e) => {
e.preventDefault();
e.stopImmediatePropagation();
if (!isDragging2) {
setDragging(true);
}
}}
resizeHandleClasses={isSelectedComponent || mouseOver ? resizerClasses : {}}
resizeHandleStyles={resizerStyles}
enableResizing={{
top: mode == 'edit' && !readOnly,
right: mode == 'edit' && !readOnly && true,
bottom: mode == 'edit' && !readOnly,
left: mode == 'edit' && !readOnly && true,
topRight: mode == 'edit' && !readOnly,
bottomRight: mode == 'edit' && !readOnly,
bottomLeft: mode == 'edit' && !readOnly,
topLeft: mode == 'edit' && !readOnly,
}}
disableDragging={mode !== 'edit' || readOnly}
onDragStop={(e, direction) => {
setDragging(false);
onDragStop(e, id, direction, currentLayout, layoutData);
}}
cancel={`div.table-responsive.jet-data-table, div.calendar-widget, div.text-input, .textarea, .map-widget, .range-slider, .kanban-container, div.real-canvas, .overlay-cell-table`}
onResizeStop={(e, direction, ref, d, position) => {
setResizing(false);
onResizeStop(id, e, direction, ref, d, position);
}}
bounds={parent !== undefined ? `#canvas-${parent}` : '.real-canvas'}
widgetId={id}
>
<div ref={preview} role="DraggableBox" style={isResizing ? { opacity: 0.5 } : { opacity: 1 }}>
{mode === 'edit' &&
!readOnly &&
(configWidgetHandlerForModalComponent || mouseOver || isSelectedComponent) &&
!isResizing && (
<ConfigHandle
id={id}
removeComponent={removeComponent}
component={component}
position={layoutData.top < 15 ? 'bottom' : 'top'}
widgetTop={layoutData.top}
widgetHeight={layoutData.height}
isMultipleComponentsSelected={isMultipleComponentsSelected}
configWidgetHandlerForModalComponent={configWidgetHandlerForModalComponent}
/>
)}
{/* Adding a sentry's error boundary to differentiate between our generic error boundary and one from editor's component */}
<Sentry.ErrorBoundary
fallback={<h2>Something went wrong.</h2>}
beforeCapture={(scope) => {
scope.setTag('errorType', 'component');
}}
>
<Box
component={component}
id={id}
width={width}
height={layoutData.height - 4}
mode={mode}
changeCanDrag={changeCanDrag}
inCanvas={inCanvas}
paramUpdated={paramUpdated}
onEvent={onEvent}
onComponentOptionChanged={onComponentOptionChanged}
onComponentOptionsChanged={onComponentOptionsChanged}
onComponentClick={onComponentClick}
containerProps={containerProps}
darkMode={darkMode}
removeComponent={removeComponent}
canvasWidth={canvasWidth}
readOnly={readOnly}
customResolvables={customResolvables}
parentId={parentId}
allComponents={allComponents}
sideBarDebugger={sideBarDebugger}
childComponents={childComponents}
isResizing={isResizing}
adjustHeightBasedOnAlignment={adjustHeightBasedOnAlignment}
currentLayout={currentLayout}
/>
</Sentry.ErrorBoundary>
</div>
</Rnd>
<div ref={preview} role="DraggableBox" style={isResizing ? { opacity: 0.5 } : { opacity: 1 }}>
{mode === 'edit' && !readOnly && (
<ConfigHandle
id={id}
removeComponent={removeComponent}
component={component}
position={layoutData.top < 15 ? 'bottom' : 'top'}
widgetTop={layoutData.top}
widgetHeight={layoutData.height}
isMultipleComponentsSelected={isMultipleComponentsSelected}
configWidgetHandlerForModalComponent={configWidgetHandlerForModalComponent}
isSelectedComponent={isSelectedComponent}
showHandle={configWidgetHandlerForModalComponent || isSelectedComponent}
/>
)}
<Sentry.ErrorBoundary
fallback={<h2>Something went wrong.</h2>}
beforeCapture={(scope) => {
scope.setTag('errorType', 'component');
}}
>
<Box
component={component}
id={id}
width={width}
height={layoutData.height - 4}
mode={mode}
changeCanDrag={changeCanDrag}
inCanvas={inCanvas}
paramUpdated={paramUpdated}
onEvent={onEvent}
onComponentClick={onComponentClick}
darkMode={darkMode}
removeComponent={removeComponent}
canvasWidth={canvasWidth}
readOnly={readOnly}
customResolvables={customResolvables}
parentId={parentId}
getContainerProps={getContainerProps}
currentPageId={currentPageId}
onOptionChanged={onComponentOptionChanged}
onOptionsChanged={onComponentOptionsChanged}
isFromSubContainer={isFromSubContainer}
childComponents={childComponents}
/>
</Sentry.ErrorBoundary>
</div>
</div>
) : (
<div ref={drag} role="DraggableBox" className="draggable-box" style={{ height: '100%' }}>
@ -376,3 +243,5 @@ export const DraggableBox = React.memo(
);
}
);
export { DraggableBox };

File diff suppressed because it is too large Load diff

View file

@ -1,33 +1,28 @@
import React, { useCallback, memo, useContext } from 'react';
import React, { useCallback, memo } from 'react';
import Selecto from 'react-selecto';
import { useEditorStore, EMPTY_ARRAY } from '@/_stores/editorStore';
import { useEditorStore } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
import { setMultipleComponentsSelected } from '@/_helpers/appUtils';
const EditorSelecto = ({
selectionRef,
canvasContainerRef,
currentPageId,
setSelectedComponent,
appDefinition,
selectionDragRef,
}) => {
const { setSelectionInProgress, setSelectedComponents, scrollOptions } = useEditorStore(
const EditorSelecto = ({ selectionRef, canvasContainerRef, setSelectedComponent, selectionDragRef }) => {
const { setSelectionInProgress, currentPageId, appDefinition } = useEditorStore(
(state) => ({
setSelectionInProgress: state?.actions?.setSelectionInProgress,
setSelectedComponents: state?.actions?.setSelectedComponents,
scrollOptions: state.scrollOptions,
currentPageId: state?.currentPageId,
appDefinition: state?.appDefinition,
}),
shallow
);
const onAreaSelectionStart = useCallback(
(e) => {
const isMultiSelect = e.inputEvent.shiftKey || useEditorStore.getState().selectedComponents.length > 0;
setSelectionInProgress(true);
setSelectedComponents([...(isMultiSelect ? useEditorStore.getState().selectedComponents : EMPTY_ARRAY)]);
},
[setSelectionInProgress, setSelectedComponents]
);
const scrollOptions = {
container: canvasContainerRef.current,
throttleTime: 30,
threshold: 0,
};
const onAreaSelectionStart = useCallback(() => {
setSelectionInProgress(true);
}, [setSelectionInProgress]);
const onAreaSelection = useCallback((e) => {
e.added.forEach((el) => {
@ -38,17 +33,31 @@ const EditorSelecto = ({
el.classList.remove('resizer-select');
});
}
e.removed.forEach((el) => {
el.classList.remove('resizer-select');
});
}, []);
const onAreaSelectionEnd = useCallback(
(e) => {
setSelectionInProgress(false);
const selectedItems = [];
e.selected.forEach((el, index) => {
const id = el.getAttribute('widgetid');
const component = appDefinition.pages[currentPageId].components[id].component;
const isMultiSelect = e.inputEvent.shiftKey || (!e.isClick && index != 0);
setSelectedComponent(id, component, isMultiSelect);
if (e.selected.length > 0 && !e.isClick) {
selectedItems.push({
id,
component,
});
} else {
setSelectedComponent(id, component, isMultiSelect);
}
});
if (selectedItems.length > 0) {
setMultipleComponentsSelected(selectedItems);
}
},
[appDefinition, currentPageId, setSelectedComponent, setSelectionInProgress]
);
@ -80,24 +89,26 @@ const EditorSelecto = ({
};
return (
<Selecto
dragContainer={'.canvas-container'}
selectableTargets={['.react-draggable']}
hitRate={0}
selectByClick={true}
toggleContinueSelect={['shift']}
ref={selectionRef}
scrollOptions={scrollOptions}
onSelectStart={onAreaSelectionStart}
onSelectEnd={onAreaSelectionEnd}
onSelect={onAreaSelection}
onDragStart={onAreaSelectionDragStart}
onDrag={onAreaSelectionDrag}
onDragEnd={onAreaSelectionDragEnd}
onScroll={(e) => {
canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10);
}}
/>
<>
<Selecto
dragContainer={'.canvas-container'}
selectableTargets={['.moveable-box']}
hitRate={0}
selectByClick={true}
toggleContinueSelect={['shift']}
ref={selectionRef}
scrollOptions={scrollOptions}
onSelectStart={onAreaSelectionStart}
onSelectEnd={onAreaSelectionEnd}
onSelect={onAreaSelection}
onDragStart={onAreaSelectionDragStart}
onDrag={onAreaSelectionDrag}
onDragEnd={onAreaSelectionDragEnd}
onScroll={(e) => {
canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10);
}}
/>
</>
);
};

View file

@ -0,0 +1,27 @@
import React from 'react';
import { isEmpty } from 'lodash';
export default function GhostWidget({ layouts, currentLayout, canvasWidth, gridWidth }) {
let layoutStyle = {};
if (!isEmpty(layouts?.[currentLayout] || layouts?.['desktop'])) {
const layoutData = layouts?.[currentLayout] || layouts?.['desktop'];
let width = (canvasWidth * layoutData.width) / 43;
layoutStyle = {
width: width + 'px',
height: layoutData.height + 'px',
transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`,
};
}
return (
<div
className="resize-ghost-widget"
style={{
zIndex: 4,
position: 'absolute',
background: '#D9E2FC',
opacity: '0.7',
...layoutStyle,
}}
></div>
);
}

View file

@ -0,0 +1,35 @@
import { useEnvironmentsAndVersionsStore } from '@/_stores/environmentsAndVersionsStore';
import React, { useEffect } from 'react';
import { shallow } from 'zustand/shallow';
import { useAppVersionStore } from '@/_stores/appVersionStore';
const EnvironmentManager = () => {
const { editingVersionId } = useAppVersionStore(
(state) => ({
editingVersionId: state?.editingVersion?.id,
}),
shallow
);
const { init, setEnvironmentDropdownStatus, initializedEnvironmentDropdown } = useEnvironmentsAndVersionsStore(
(state) => ({
initializedEnvironmentDropdown: state.initializedEnvironmentDropdown,
init: state.actions.init,
setEnvironmentDropdownStatus: state.actions.setEnvironmentDropdownStatus,
}),
shallow
);
useEffect(() => {
!initializedEnvironmentDropdown && initComponent();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initComponent = async () => {
await init(editingVersionId);
setEnvironmentDropdownStatus(true);
};
return <></>;
};
export default EnvironmentManager;

Some files were not shown because too many files have changed in this diff Show more