mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
Merge pull request #9860 from ToolJet/chore/v2.45.0-conflicts
Merge main back to develop (v2.45.0)
This commit is contained in:
commit
879201fb1f
312 changed files with 25714 additions and 25973 deletions
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
2.44.0
|
||||
2.45.0
|
||||
|
|
|
|||
|
|
@ -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"]',
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.44.0
|
||||
2.45.0
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
3
frontend/assets/images/icons/tj-info-error.svg
Normal file
3
frontend/assets/images/icons/tj-info-error.svg
Normal 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
21773
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
34
frontend/src/Editor/AutoLayoutAlert.jsx
Normal file
34
frontend/src/Editor/AutoLayoutAlert.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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%',
|
||||
|
|
|
|||
182
frontend/src/Editor/BoxUI.jsx
Normal file
182
frontend/src/Editor/BoxUI.jsx
Normal 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;
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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' }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
141
frontend/src/Editor/CodeEditor/CodeHinter.jsx
Normal file
141
frontend/src/Editor/CodeEditor/CodeHinter.jsx
Normal 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;
|
||||
43
frontend/src/Editor/CodeEditor/DynamicFxTypeRenderer.jsx
Normal file
43
frontend/src/Editor/CodeEditor/DynamicFxTypeRenderer.jsx
Normal 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} />;
|
||||
};
|
||||
273
frontend/src/Editor/CodeEditor/MultiLineCodeEditor.jsx
Normal file
273
frontend/src/Editor/CodeEditor/MultiLineCodeEditor.jsx
Normal 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;
|
||||
390
frontend/src/Editor/CodeEditor/PreviewBox.jsx
Normal file
390
frontend/src/Editor/CodeEditor/PreviewBox.jsx
Normal 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;
|
||||
404
frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx
Normal file
404
frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx
Normal 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;
|
||||
211
frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js
Normal file
211
frontend/src/Editor/CodeEditor/autocompleteExtensionConfig.js
Normal 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;
|
||||
}
|
||||
11
frontend/src/Editor/CodeEditor/autocompleteUtils.js
Normal file
11
frontend/src/Editor/CodeEditor/autocompleteUtils.js
Normal 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] : '';
|
||||
}
|
||||
3
frontend/src/Editor/CodeEditor/index.js
Normal file
3
frontend/src/Editor/CodeEditor/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import CodeHinter from './CodeHinter';
|
||||
|
||||
export default CodeHinter;
|
||||
534
frontend/src/Editor/CodeEditor/styles.scss
Normal file
534
frontend/src/Editor/CodeEditor/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
409
frontend/src/Editor/CodeEditor/utils.js
Normal file
409
frontend/src/Editor/CodeEditor/utils.js
Normal 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);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -104,7 +104,8 @@ export const RenderHighlight = ({
|
|||
}}
|
||||
useCustomStyles={true}
|
||||
value={annotation.data.text}
|
||||
useMenuPortal={false}
|
||||
// useMenuPortal={false}
|
||||
useMenuPortal
|
||||
styles={selectElementStyles(darkMode, '100%')}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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'}`}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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()}</>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
59
frontend/src/Editor/ControlledComponentToRender.jsx
Normal file
59
frontend/src/Editor/ControlledComponentToRender.jsx
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
179
frontend/src/Editor/DragContainer.css
Normal file
179
frontend/src/Editor/DragContainer.css
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
/* */
|
||||
844
frontend/src/Editor/DragContainer.jsx
Normal file
844
frontend/src/Editor/DragContainer.jsx
Normal 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 };
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
27
frontend/src/Editor/GhostWidget.jsx
Normal file
27
frontend/src/Editor/GhostWidget.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue