mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 13:37:28 +00:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
a2eebf8595
commit
921714d8a9
1409 changed files with 116633 additions and 48397 deletions
|
|
@ -1,14 +1,19 @@
|
|||
// storybookDecorators.js
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
export function withColorScheme(story, context) {
|
||||
const darkMode = context?.globals?.backgrounds?.value === '#333333'; // Access theme mode from globals
|
||||
const className = darkMode ? 'dark-theme' : '';
|
||||
const darkMode = context?.globals?.backgrounds?.value === "#333333"; // Access theme mode from globals
|
||||
const className = darkMode ? "dark-theme" : "";
|
||||
|
||||
return (
|
||||
<div className={className} style={{ backgroundColor: 'transparent' }}>
|
||||
<div className={className} style={{ backgroundColor: "transparent" }}>
|
||||
{story()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function withRouter(story) {
|
||||
return <MemoryRouter>{story()}</MemoryRouter>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,77 @@
|
|||
import customWebpackConfig from '../webpack.config';
|
||||
import path from 'path';
|
||||
import customWebpackConfig from "../webpack.config";
|
||||
import path from "path";
|
||||
|
||||
const config = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-docs",
|
||||
],
|
||||
|
||||
framework: {
|
||||
name: "@storybook/react-webpack5",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
|
||||
webpackFinal: async (storybookConfig) => {
|
||||
// Filter out the babel-loader rule from custom config to avoid conflicts
|
||||
const customRules = customWebpackConfig.module.rules.filter((rule) => {
|
||||
if (rule.test && rule.test.toString().includes("js|jsx")) {
|
||||
return false; // Skip the babel-loader rule that includes react-refresh
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Add a custom babel-loader rule for JSX files without react-refresh
|
||||
const babelRule = {
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: ["@babel/preset-env", "@babel/preset-react"],
|
||||
plugins: [
|
||||
[
|
||||
"import",
|
||||
{
|
||||
libraryName: "lodash",
|
||||
libraryDirectory: "",
|
||||
camel2DashComponentName: false,
|
||||
},
|
||||
"lodash",
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...storybookConfig,
|
||||
module: { ...storybookConfig.module, rules: [...storybookConfig.module.rules, ...customWebpackConfig.module.rules] },
|
||||
module: {
|
||||
...storybookConfig.module,
|
||||
rules: [...storybookConfig.module.rules, ...customRules, babelRule],
|
||||
},
|
||||
resolve: {
|
||||
...storybookConfig.resolve,
|
||||
alias: {
|
||||
...storybookConfig.resolve.alias,
|
||||
'@': path.resolve(__dirname, '../src/')
|
||||
}
|
||||
}
|
||||
"@": path.resolve(__dirname, "../src/"),
|
||||
"@ee": path.resolve(__dirname, "../ee/"),
|
||||
"@cloud": path.resolve(__dirname, "../cloud/"),
|
||||
"@assets": path.resolve(__dirname, "../assets/"),
|
||||
"@white-label": path.resolve(
|
||||
__dirname,
|
||||
"../src/_helpers/white-label"
|
||||
),
|
||||
},
|
||||
fallback: {
|
||||
...storybookConfig.resolve.fallback,
|
||||
process: require.resolve("process/browser.js"),
|
||||
path: require.resolve("path-browserify"),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/** @type { import('@storybook/react').Preview } */
|
||||
/** @type { import('@storybook/react-webpack5').Preview } */
|
||||
|
||||
import '../src/_styles/theme.scss';
|
||||
import './preview.scss';
|
||||
import { withColorScheme } from './decorators'; // Import the decorator
|
||||
import "../src/_styles/theme.scss";
|
||||
import "./preview.scss";
|
||||
import { withColorScheme, withRouter } from "./decorators"; // Import the decorators
|
||||
|
||||
const preview = {
|
||||
parameters: {
|
||||
|
|
@ -14,7 +14,7 @@ const preview = {
|
|||
},
|
||||
},
|
||||
},
|
||||
decorators: [withColorScheme], // Adding the decorator to the decorators array
|
||||
decorators: [withRouter, withColorScheme], // Adding the decorators to the decorators array
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,6 @@
|
|||
@import '~bootstrap/scss/bootstrap';
|
||||
@import '../src/_styles/componentdesign.scss'
|
||||
@import '../src/_styles/componentdesign.scss';
|
||||
|
||||
body {
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
3.16.1-lts
|
||||
3.20.74-lts
|
||||
|
|
|
|||
9
frontend/assets/images/dynamic-height-info.svg
Normal file
9
frontend/assets/images/dynamic-height-info.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg width="184" height="71" viewBox="0 0 184 71" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="183.712" height="70.5391" fill="url(#pattern0_363_29626)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0_363_29626" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_363_29626" transform="scale(0.0021097 0.00549451)"/>
|
||||
</pattern>
|
||||
<image id="image0_363_29626" width="474" height="182" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAdoAAAC2CAYAAAB+pUw6AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAyXSURBVHgB7d1PbJTnncDx3zMelz9rUpImKSQmMmq2yi3srT3VhDSKtCvBNvcWbs4JIvWyJ2ypqlarSoHT0hPWqoe9NCKHlaI0FHJqbuvcqjSVR0AaNiFZl3UB4/G8+75jmwJlMOPhwZ7h85HCvJ533pnMC/bXz/tvUjxEY2NjO2+26kdTivEi0liKYizgLiliroiYKSfeHY76mUbj940AGFApHoKxsZfGFovm6XJyPKBbRZoerg1NCS4wiIaiR6Njf3+0VbT+s5x8KWA9UuxrRevwyJNPLczPff1RAAyQnkL73NiLx6OIfy0ntwb0Zmu5eeX1Hd98Kv7vz19/GAADYt2hrUayK5GFh6bav1+ObP9sZAsMinXto632yTaL5n8XETsDHr654VT/B/tsgUFQi3VYbC0dF1ky2rlycB1A3+t6RLtyhPFsQGbDqflko9GYC4A+1vWIdjGahwIegWarfiwA+tx6Nh0fDHgEihQ/CIA+13VoUxH7Ah6JNBYAfa7r0DoIikfHJTyB/reuo44BgAcjtACQkdACQEZCCwAZCS0AZCS0AJCR0AJARkILABkJLQBkJLQAkJHQAkBGQgsAGQktAGQktACQkdACQEZCCwAZCS0AZLSO0KZGwKMxFwB9rvvQpqIR8AikiJkA6HNdh7b84fdhwCNQpHg3APpc16GtR/NEwCOwFPUzAdDnug5to9GYK4e15wNySmn6cuP3jQDoc+s66rgcaRxJDlQhk+rf1lIMTQXAABiKdZifuzK348mnFsrJ1wMethT/8nnjk/cCYACku+94deLieKTiYFHUDkUUY+0HpZgpijQzXI4y3ju1u7H62NGxFyeLIo4HPCSpiKlLFz6dDIBNqptOtuetThw6Nrvz6sLQ8VSkY/d7gVQUJ0a2LU2dObG3ven4+bEXj5U/HI8XETsD1qnaXFykmPqs8amD7YBNab2dTKsLzy/Uz5Wj030j22vxo/0j8fJ3t7T/q/zx0s349dn5eP+ja8tPUpZ7ZEtz/+qT7Bp7aWwoliajKH4S0KXy39P5Zrnf38FPwGbVSyfboX3lzQtvV4Xe9a16/OKtp6O6vZfLXzXjp29fad9Wxf7gly+8dfv85eA2D5VPejDK/xmjXO4tNdLyhU8+rE4Xax/JDrCJ9dLJ9PrE7Nhi1GerB/zqZ7s6Lnz7k0z8/IuYv9aqhsP7Pzi153wAwIDqtZO1xRhuH8z02ve2r7lwpXpMNWSulCPWQwEAA6zXTtbLTXj7ym3O8caBHfGg2tuk/6uaSgfLP27tFB4vqz+0Uv0Ozp89tWd/p5k/nLhwuOz/6U7zy98MpsoR9GSn+QcmLkyXj+q4n7gW6chvTo1Od5r/6sTFc+WqGO80fynqe8/fdTTZna9/sYiOUuPsqdG90fm1x8uFz3VefmPX3VpbLzb3ukvT5fJHOs3d6HV3YOLS7OqRi/dS/r2nuI+NXHfl8pPl8h3PPFjre24j112vP682et2t9T23mdfdWt9zG73u7v551Wsna9WO3WryO6PD3T1BW+e/JAAYBL12Mq3+NvzBv49GN15981L7dq3ftgGgn/Xaydrq58t+/MnCAy/86cWb7dtU+BgzAAZdb50sQ1u0P4rs4z88+BO8c26+fVukJLQADLjeOlkrt/u2P4rsnbPz7UOS11I95v3fLZ+Q68LvAAy6XjvZ3r9abn+uLnt3tJsTcUsny/2z7SOOZ2f/d+fwtubh8sl+UBRpn4OkAOhL5abeVItGURTvju5+dnr17l462Q7t+LHZnUML9XOxcmTVa9/fHm+8suPWEVbVdulqyPzOb+eXT8BNMdPc0tx//sTeuQt/+p9DtdrQ6SgKV4ECYIBU+2ZbU1Vwe+nkrSOG209yoz5ZTh5d45VPLm1tTlYLX7r85dvli7ZHtVu+MRxbt34jtm7ZEvWhdX3MLQBsqMVmMxYXm3F1/losLbXa95WhnHx+9zNT6+nkyvJ3Wj4ReWjyzpPIq6ovvZuidmb1BOjPPv/qeBGtyVTu5X1iZHuMbN8WADAo5q9dL4N7PYpWq6rlidFdz7Sv77/ayZTSy6vn2C5rnby9k6s6ngO7et7Qvc6TvfT5F4fLRU9XkX3mqW/GcH3tS1IBQL+pRrhffn21HdtWkf75heeePnP3Y+7Xy8o6t/HW2pe+qkayIgvAoKoa98TI8hbbWq04XR38G13qOrTLo9libKjcD2tzMQCDrmpddRxSFLFzeEvzcHSp69Cmov1BAu3RLAA8DrZuXb52cRGt8ehS16EtN0SPVbfDwzYZA/B42LblG+3blIZeji51v482LR9hZd8sAI+LoVunrXZ/QSYnvAJARkILABkJLQBkJLQAkJHQAkBGQgsAGQktAGQktACQkdACQEZCCwAZCS0AZCS0AJCR0AJARkILABkJLQBkJLQAkJHQAkBGQgsAGQktAGQktACQkdACQEZCCwAZ1aPPfHb5Smxmz+96Onrh/W0s7+/+vL+N5f3d32Z9f0a0AJCR0AJARkILABkJLQBkJLQAkJHQAkBGQgsAGQktAGQktACQkdACQEZCCwAZCS0AZCS0AJCR0AJARkILABkJLQBkJLQAkJHQAkBGQgsAGQktAGQktACQkdACQEZCCwAZCS0AZCS0AJCR0AJARkILABkJLQBkJLQAkJHQAkBGQgsAGQktAGQktACQkdACQEZCCwAZCS0AZCS0AJCR0AJARkILABkJLQBkJLQAkJHQAkBGQgsAGQktAGQktACQkdACQEZCCwAZCS0AZCS0AJCR0AJARkILABkJLQBkJLQAkJHQAkBGQgsAGQktAGQktACQUT36zPO7no5B5v31N++vv3l//S33+/vs8pVYDyNaAMhIaAEgI6EFgIyEFgAyEloAyEhoASAjoQWAjIQWADISWgDISGgBICOhBYCMhBYAMhJaAMhIaAEgI6EFgIyEFgAyEloAyEhoASAjoQWAjIQWADISWgDISGgBIKN1hDY1qj+bS60AgMfBzebSytRyA7vRdWhTUcxUtzcWFgIAHgfNxcX2bVEsfRxd6jq0RS0+rG5v3LgZAPA4uDp/rX2bUjoTXeo6tIvX69ORYm7h5mL85dr1AIBBNn/tRiy1d5cWjdHdz05Hl7oO7d69T861WulINV0VfrHZDAAYRFXjrs7/ZfXLqViHdPcdr05cHC93xB4sinTs1oNSzJRfzwzH0NR7p3Y3qvsu/unKiZSKo7VaiidGtsffbd8WADAoqpFsFdmiVUQRrak9u789Wd3/107WDpWj3LG/LpGmb+/krXtXJw4dm915dWHoeLotsPeSiuLEyLalqTMn9s5d/PzLyfIJjlf3Dw3V2sEdHq7HcL0eANBvqjNqqoN9q+OQql2klaIoTu557tlj6+lk++vqj2rh+YX6uaKIfSPba/Gj/SPx8ne3tP+r/PHSzfj12fl4/6PVncExM7Klub96kkuff3E42rFNYwEAgyJFuau0deSF5759ppdOtkP7ypsX3q4Kvetb9fjFW09HdXsvl79qxk/fvtK+rYr9wS9feGt1XhXclFK1yXnfnUNpAOgXqVHuFp0pIj5cvD4/vXfv8qi0l06m1ydmxxajPls94Fc/29Vx4dufZOLnX5TbrlvVcHj/B6f2nA8AGFC9drK2GMPtfayvfW/7mgtXqsdUQ+ZKWfxDAQADrNdO1soh8r7qizcO7IgHtbpNuhxiHwwAGGC9drJW7ditJr8zOryOJ7AvFoDB1msnH+p5OOPlduyhle3YHZw/e2rP/k4zfzhx4XC5Rft0p/nltu6pcp/wZKf5ByYuTJeP+kmn+bVIR35zanS60/xXJy6eK4f5453mL0V97/m7zo+68/UvFtFRapw9Nbo3Or/2eLnwuc7Lb+y6W2t//OZed2m6XP5Ip7kbve4OTFyavd8vreXfe4r72Mh1Vy4/Wayc4ncva33PbeS66/Xn1Uavu7W+5zbzulvre26j191aP6+6VVv9JIKPP3nwDwn49OLydY5TETMBAAOtt06m8rfhE+X00R//0xPx43984oGe4N/+4+t4/3fVuUL3/60DAPpdr52sldsG2p9E8M7Z+fYhyWupHrO8cDW8HlrXdR8BoF/02snayjb8k/PXW7dOsr3fwtVjVpx8mNuwAWAz6rWT7Z3d48dmdw4t1M/FypFVr31/e7zxyo5bR1hV26U//sNCvPPb+eUTcFPMNLc0959fuY4jAAyyXjqZ7niSG/XJcvLoGq93cmlrc1JkAXicrLeTf3P49vJh20OTKaWXV88dWj7iaundFLUzLrkIwOOs207+P/WAm6XwRtQeAAAAAElFTkSuQmCC"/>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
3
frontend/assets/images/icons/warning-icon.svg
Normal file
3
frontend/assets/images/icons/warning-icon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.47599 3.12444C8.14024 1.95852 9.85976 1.95852 10.524 3.12444L16.2781 13.2243C16.9237 14.3575 16.0834 15.75 14.7541 15.75H3.24591C1.91659 15.75 1.07633 14.3575 1.7219 13.2243L7.47599 3.12444ZM9.74995 12.75C9.74995 13.1642 9.41417 13.5 8.99995 13.5C8.58574 13.5 8.24995 13.1642 8.24995 12.75C8.24995 12.3358 8.58574 12 8.99995 12C9.41417 12 9.74995 12.3358 9.74995 12.75ZM9.56245 6.75C9.56245 6.43934 9.31061 6.1875 8.99995 6.1875C8.68929 6.1875 8.43745 6.43934 8.43745 6.75V10.5C8.43745 10.8107 8.68929 11.0625 8.99995 11.0625C9.31061 11.0625 9.56245 10.8107 9.56245 10.5V6.75Z" fill="#4368E3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 750 B |
20
frontend/assets/images/icons/widgets/audiorecorder.jsx
Normal file
20
frontend/assets/images/icons/widgets/audiorecorder.jsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
const AudioRecorder = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 18 20' }) => (
|
||||
<svg width={width} height={width} viewBox={viewBox} fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M13.2143 4.64289C13.2143 2.07869 11.1356 0 8.57143 0C6.00724 0 3.92856 2.07869 3.92856 4.64289V8.5714C3.92856 11.1356 6.00724 13.2143 8.57143 13.2143C11.1356 13.2143 13.2143 11.1356 13.2143 8.5714V4.64289Z"
|
||||
fill="#CCD1D5"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.14285 8.92861C2.14285 8.33688 1.66316 7.85718 1.07142 7.85718C0.479693 7.85718 0 8.33688 0 8.92861C0 13.2996 3.27184 16.9065 7.50002 17.4337V18.9286C7.50002 19.5203 7.97972 20 8.57145 20C9.16318 20 9.64288 19.5203 9.64288 18.9286V17.4337C13.871 16.9065 17.1429 13.2996 17.1429 8.92861C17.1429 8.33688 16.6632 7.85718 16.0714 7.85718C15.4797 7.85718 15 8.33688 15 8.92861C15 12.479 12.1219 15.3572 8.57145 15.3572C5.02105 15.3572 2.14285 12.479 2.14285 8.92861Z"
|
||||
fill="#4368E3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default AudioRecorder;
|
||||
20
frontend/assets/images/icons/widgets/camera.jsx
Normal file
20
frontend/assets/images/icons/widgets/camera.jsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
const Camera = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 20 18' }) => (
|
||||
<svg width={width} height={width} viewBox={viewBox} fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.78571 0C6.53481 0 6.30231 0.131643 6.17321 0.346786L4.23843 3.57143H2.14286C1.57453 3.57143 1.02949 3.7972 0.627629 4.19906C0.225764 4.60091 0 5.14597 0 5.71429V15C0 15.5683 0.225764 16.1134 0.627629 16.5153C1.02949 16.9171 1.57453 17.1429 2.14286 17.1429H17.8571C18.4254 17.1429 18.9706 16.9171 19.3724 16.5153C19.7743 16.1134 20 15.5683 20 15V5.71429C20 5.14596 19.7743 4.60091 19.3724 4.19906C18.9706 3.7972 18.4254 3.57143 17.8571 3.57143H15.7616L13.8268 0.346786C13.6977 0.131643 13.4652 0 13.2143 0H6.78571Z"
|
||||
fill="#CCD1D5"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10 5.75897C7.83031 5.75897 6.07143 7.51786 6.07143 9.68754C6.07143 11.8572 7.83031 13.616 10 13.616C12.1697 13.616 13.9286 11.8572 13.9286 9.68754C13.9286 7.51786 12.1697 5.75897 10 5.75897Z"
|
||||
fill="#4368E3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Camera;
|
||||
33
frontend/assets/images/icons/widgets/currencyinput.jsx
Normal file
33
frontend/assets/images/icons/widgets/currencyinput.jsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
|
||||
const CurrencyInput = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 49 48' }) => (
|
||||
<svg width={width} height={width} viewBox={viewBox} fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_66_24)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.03153 10.2858C3.19121 10.2858 0.888672 12.1662 0.888672 14.4858V34.0858C0.888672 36.4053 3.19121 38.2858 6.03153 38.2858H43.7458C46.586 38.2858 48.8887 36.4053 48.8887 34.0858V14.4858C48.8887 12.1662 46.586 10.2858 43.7458 10.2858H6.03153Z"
|
||||
fill={fill}
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M17.8826 28.6575C17.8826 27.7817 18.5925 27.0718 19.4683 27.0718H24.3405C25.2163 27.0718 25.9262 27.7817 25.9262 28.6575C25.9262 29.5332 25.2163 30.2432 24.3405 30.2432H19.4683C18.5925 30.2432 17.8826 29.5332 17.8826 28.6575Z"
|
||||
fill="#2859C5"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.315 14.1373C12.3659 14.1373 13.2178 14.9893 13.2178 16.0402V16.9911C14.1945 17.2024 15.0573 17.7204 15.6977 18.436C16.3986 19.2191 16.332 20.422 15.5489 21.1229C14.7658 21.8237 13.5629 21.7571 12.862 20.974C12.7044 20.798 12.4815 20.6917 12.2312 20.6917H10.1037C9.79959 20.6917 9.55306 20.9382 9.55306 21.2423C9.55306 21.501 9.73325 21.7249 9.98602 21.7802L13.2252 22.4888C15.3612 22.956 16.8826 24.8483 16.8826 27.0335C16.8826 29.2632 15.3134 31.1283 13.2178 31.5811V32.5315C13.2178 33.5823 12.3659 34.4343 11.315 34.4343C10.2641 34.4343 9.41215 33.5823 9.41215 32.5315V31.581C8.11337 31.3003 7.01789 30.4772 6.37356 29.3664C5.84627 28.4573 6.15575 27.2929 7.0648 26.7656C7.97388 26.2383 9.13827 26.5478 9.66553 27.4569C9.81476 27.7142 10.0885 27.8803 10.3988 27.8803H11.2926C11.3 27.8802 11.3075 27.8801 11.315 27.8801C11.3225 27.8801 11.33 27.8802 11.3374 27.8803H12.2312C12.6973 27.8803 13.0769 27.5015 13.0769 27.0335C13.0769 26.6354 12.7995 26.2913 12.4119 26.2065L9.17275 25.498C7.17286 25.0605 5.74738 23.2895 5.74738 21.2423C5.74738 19.0717 7.33488 17.2719 9.41215 16.9406V16.0402C9.41215 14.9893 10.2641 14.1373 11.315 14.1373Z"
|
||||
fill="#2859C5"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_66_24">
|
||||
<rect width="48" height="48" fill="white" transform="translate(0.888672)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default CurrencyInput;
|
||||
|
|
@ -1,12 +1,29 @@
|
|||
import React from 'react';
|
||||
|
||||
const Downstatistics = () => (
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.78199 11.3232C9.66186 11.2031 9.59438 11.0401 9.59438 10.8702C9.59438 10.7004 9.66186 10.5374 9.78199 10.4173C9.90212 10.2972 10.0651 10.2297 10.2349 10.2297H14.0079L9.54698 4.77721L6.20452 8.12095C6.13902 8.18638 6.06011 8.23684 5.97322 8.26885C5.88634 8.30085 5.79356 8.31364 5.70126 8.30634C5.60896 8.29903 5.51934 8.2718 5.43857 8.22653C5.35781 8.18125 5.28782 8.119 5.23343 8.04408L0.108928 0.997891C0.0164671 0.860327 -0.0190707 0.692278 0.00978223 0.529059C0.0386352 0.36584 0.129633 0.220159 0.263652 0.12263C0.397671 0.025101 0.564277 -0.0166822 0.728459 0.00606107C0.892641 0.0288043 1.04162 0.114303 1.14408 0.24459L5.82787 6.6848L9.14214 3.37053C9.20542 3.30737 9.28122 3.25815 9.36467 3.22605C9.44812 3.19394 9.53736 3.17966 9.62666 3.18413C9.71596 3.18859 9.80333 3.2117 9.88316 3.25197C9.96299 3.29224 10.0335 3.34877 10.0902 3.41794L14.7189 9.07666V5.74574C14.7189 5.57585 14.7864 5.41292 14.9065 5.29279C15.0266 5.17267 15.1895 5.10518 15.3594 5.10518C15.5293 5.10518 15.6923 5.17267 15.8124 5.29279C15.9325 5.41292 16 5.57585 16 5.74574V10.8702C16 11.0401 15.9325 11.2031 15.8124 11.3232C15.6923 11.4433 15.5293 11.5108 15.3594 11.5108H10.2349C10.0651 11.5108 9.90212 11.4433 9.78199 11.3232Z"
|
||||
fill="#EE2C4D"
|
||||
/>
|
||||
const Downstatistics = ({ width = 20, height = 20 }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 20 20" fill="none">
|
||||
<g clip-path="url(#clip0_135_2145)">
|
||||
<path
|
||||
d="M2.5 5.8335L7.5 10.8335L10.8333 7.50016L17.5 14.1668"
|
||||
stroke="#D72D39"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M17.5 8.3335V14.1668H11.6667"
|
||||
stroke="#D72D39"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_135_2145">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Downstatistics;
|
||||
export default Downstatistics;
|
||||
|
|
@ -61,6 +61,11 @@ import HorizontalDivider from './horizontalDivider.jsx';
|
|||
import PhoneInput from './phoneinput.jsx';
|
||||
import EmailInput from './emailinput.jsx';
|
||||
import Chat from './chat.jsx';
|
||||
import CurrencyInput from './currencyinput.jsx';
|
||||
import PopoverMenu from './popovermenu.jsx';
|
||||
import AudioRecorder from './audiorecorder.jsx';
|
||||
import Camera from './camera.jsx';
|
||||
import TagsInput from './tagsinput.jsx';
|
||||
|
||||
const WidgetIcon = (props) => {
|
||||
// TO_DO -> Use widget type instead of widget name
|
||||
|
|
@ -89,9 +94,8 @@ const WidgetIcon = (props) => {
|
|||
return <Customcomponent {...props} />;
|
||||
case 'datetimepickerlegacy':
|
||||
return <Datepicker {...props} />;
|
||||
case 'datepickerlegacy':
|
||||
case 'datepicker':
|
||||
return <Datepicker {...props} />;
|
||||
case 'datepickerv2':
|
||||
return <DatepickerV2 {...props} />;
|
||||
case 'timepicker':
|
||||
return <TimePicker {...props} />;
|
||||
|
|
@ -157,14 +161,16 @@ const WidgetIcon = (props) => {
|
|||
return <Passwordinput {...props} />;
|
||||
case 'pdf':
|
||||
return <Pdf {...props} />;
|
||||
case 'popovermenu':
|
||||
return <PopoverMenu {...props} />;
|
||||
case 'qrscanner':
|
||||
return <Qrscanner {...props} />;
|
||||
case 'radiobutton':
|
||||
case 'radiobuttonlegacy':
|
||||
case 'radiobuttonv2':
|
||||
return <RadioButton {...props} />;
|
||||
case 'rangesliderlegacy':
|
||||
case 'rangeslider':
|
||||
case 'rangesliderv2':
|
||||
return <Rangeslider {...props} />;
|
||||
case 'rating':
|
||||
return <Rating {...props} />;
|
||||
|
|
@ -186,6 +192,8 @@ const WidgetIcon = (props) => {
|
|||
return <Tabs {...props} />;
|
||||
case 'tags':
|
||||
return <Tags {...props} />;
|
||||
case 'tagsinput':
|
||||
return <TagsInput {...props} />;
|
||||
case 'text':
|
||||
return <Text {...props} />;
|
||||
case 'textarea':
|
||||
|
|
@ -210,6 +218,13 @@ const WidgetIcon = (props) => {
|
|||
return <Verticaldivider {...props} />;
|
||||
case 'chat':
|
||||
return <Chat {...props} />;
|
||||
case 'currencyinput':
|
||||
case 'currencyinputlegacy':
|
||||
return <CurrencyInput {...props} />;
|
||||
case 'audiorecorder':
|
||||
return <AudioRecorder {...props} />;
|
||||
case 'camera':
|
||||
return <Camera {...props} />;
|
||||
default:
|
||||
return <BoundedBox {...props} />;
|
||||
}
|
||||
|
|
|
|||
27
frontend/assets/images/icons/widgets/popovermenu.jsx
Normal file
27
frontend/assets/images/icons/widgets/popovermenu.jsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
|
||||
const PopoverMenu = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 49 48' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.74582 1.71423C4.90549 1.71423 2.60297 4.01676 2.60297 6.85709V41.1428C2.60297 43.983 4.90549 46.2857 7.74582 46.2857H42.0315C44.8718 46.2857 47.1744 43.983 47.1744 41.1428V6.85709C47.1744 4.01676 44.8718 1.71423 42.0315 1.71423H7.74582Z"
|
||||
fill="#CCD1D5"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.1744 15.4285C14.1744 14.245 15.1338 13.2856 16.3172 13.2856H33.4601C34.6436 13.2856 35.6029 14.245 35.6029 15.4285C35.6029 16.612 34.6436 17.5714 33.4601 17.5714H16.3172C15.1338 17.5714 14.1744 16.612 14.1744 15.4285ZM14.1744 23.9999C14.1744 22.8165 15.1338 21.8571 16.3172 21.8571H33.4601C34.6436 21.8571 35.6029 22.8165 35.6029 23.9999C35.6029 25.1834 34.6436 26.1428 33.4601 26.1428H16.3172C15.1338 26.1428 14.1744 25.1834 14.1744 23.9999ZM16.3172 30.4285C15.1338 30.4285 14.1744 31.3879 14.1744 32.5714C14.1744 33.7548 15.1338 34.7142 16.3172 34.7142H33.4601C34.6436 34.7142 35.6029 33.7548 35.6029 32.5714C35.6029 31.3879 34.6436 30.4285 33.4601 30.4285H16.3172Z"
|
||||
fill="#4368E3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default PopoverMenu;
|
||||
27
frontend/assets/images/icons/widgets/tagsinput.jsx
Normal file
27
frontend/assets/images/icons/widgets/tagsinput.jsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
|
||||
const TagsInput = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 48 48' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.98879 7.37061C3.23356 7.37061 1 9.60415 1 12.3594V35.6404C1 38.3956 3.23356 40.6292 5.98879 40.6292H31.1021C32.5271 40.6292 33.8841 40.0199 34.8307 38.9547L46.1599 26.2095C47.28 24.9494 47.28 23.0504 46.1599 21.7903L34.8307 9.04501C33.8841 7.97997 32.5271 7.37061 31.1021 7.37061H5.98879Z"
|
||||
fill={fill}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M35.0901 23.9999C35.0901 22.1631 33.601 20.6741 31.7642 20.6741C29.9274 20.6741 28.4384 22.1631 28.4384 23.9999C28.4384 25.8367 29.9274 27.3258 31.7642 27.3258C33.601 27.3258 35.0901 25.8367 35.0901 23.9999ZM24.281 23.9999C24.281 22.1631 22.792 20.6741 20.9552 20.6741C19.1184 20.6741 17.6293 22.1631 17.6293 23.9999C17.6293 25.8367 19.1184 27.3258 20.9552 27.3258C22.792 27.3258 24.281 25.8367 24.281 23.9999ZM10.1461 20.6741C11.9829 20.6741 13.472 22.1631 13.472 23.9999C13.472 25.8367 11.9829 27.3258 10.1461 27.3258C8.30927 27.3258 6.82025 25.8367 6.82025 23.9999C6.82025 22.1631 8.30927 20.6741 10.1461 20.6741Z"
|
||||
fill="#4368E3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default TagsInput;
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
const Upstatistics = () => (
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
const Upstatistics = ({ width = 20, height = 21 }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 20 21" fill="none">
|
||||
<path
|
||||
d="M9.78199 0.187617C9.66186 0.307745 9.59438 0.470675 9.59438 0.640562C9.59438 0.81045 9.66186 0.97338 9.78199 1.09351C9.90212 1.21364 10.0651 1.28112 10.2349 1.28112H14.0079L9.54698 6.73359L6.20452 3.38986C6.13902 3.32442 6.06011 3.27396 5.97322 3.24195C5.88634 3.20995 5.79356 3.19716 5.70126 3.20447C5.60896 3.21177 5.51934 3.239 5.43857 3.28428C5.35781 3.32955 5.28782 3.3918 5.23343 3.46672L0.108928 10.5129C0.0164671 10.6505 -0.0190707 10.8185 0.00978223 10.9817C0.0386352 11.145 0.129633 11.2906 0.263652 11.3882C0.397671 11.4857 0.564277 11.5275 0.728459 11.5047C0.892641 11.482 1.04162 11.3965 1.14408 11.2662L5.82787 4.826L9.14214 8.14027C9.20542 8.20343 9.28122 8.25265 9.36467 8.28475C9.44812 8.31686 9.53736 8.33114 9.62666 8.32667C9.71596 8.32221 9.80333 8.2991 9.88316 8.25883C9.96299 8.21856 10.0335 8.16203 10.0902 8.09287L14.7189 2.43414V5.76506C14.7189 5.93495 14.7864 6.09788 14.9065 6.21801C15.0266 6.33814 15.1895 6.40562 15.3594 6.40562C15.5293 6.40562 15.6923 6.33814 15.8124 6.21801C15.9325 6.09788 16 5.93495 16 5.76506V0.640562C16 0.470675 15.9325 0.307745 15.8124 0.187617C15.6923 0.0674878 15.5293 0 15.3594 0H10.2349C10.0651 0 9.90212 0.0674878 9.78199 0.187617Z"
|
||||
fill="#36AF8B"
|
||||
d="M18.3333 12.5146C18.3333 12.9748 17.9602 13.3479 17.5 13.3479C17.0398 13.3479 16.6667 12.9748 16.6667 12.5146V8.69295L11.4225 13.9371C11.0971 14.2625 10.5696 14.2625 10.2441 13.9371L7.5 11.193L3.08919 15.6038C2.76376 15.9292 2.23624 15.9292 1.91081 15.6038C1.58537 15.2783 1.58537 14.7508 1.91081 14.4254L6.91081 9.42537L6.97428 9.36841C7.3016 9.10145 7.7841 9.12028 8.08919 9.42537L10.8333 12.1695L15.4883 7.51457H11.6667C11.2064 7.51457 10.8333 7.14147 10.8333 6.68123C10.8333 6.221 11.2064 5.8479 11.6667 5.8479H17.5C17.9602 5.8479 18.3333 6.221 18.3333 6.68123V12.5146Z"
|
||||
fill="#1E823B"
|
||||
/>
|
||||
|
||||
<rect x="27.7549" y="17.1086" width="5.45898" height="13.7825" rx="2.72949" fill="#3E63DD" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Upstatistics;
|
||||
export default Upstatistics;
|
||||
18
frontend/assets/images/logo-dark.svg
Normal file
18
frontend/assets/images/logo-dark.svg
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<svg width="120" height="21" viewBox="0 0 120 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5938_1632)">
|
||||
<path d="M31.8734 10.4669L28.8252 14.8269L28.3754 15.4669L25.7769 19.1819V19.1869L24.8774 20.4669H12.6743L13.5738 19.1819L16.1773 15.4669H22.2739L22.7236 14.8269L25.7769 10.4669L22.7236 6.10692L22.2739 5.46692H17.0718L19.6754 1.75192V1.74692L20.5748 0.466919H24.8774L25.7769 1.74692V1.75192L28.3754 5.46692L28.8252 6.10692L31.8734 10.4669Z" fill="#4368E3"/>
|
||||
<path d="M13.5738 10.4669L10.5255 14.8269L10.0758 15.4669L6.57774 20.4669H0.476196L3.97422 15.4669L7.47224 10.4669L3.97422 5.46692L0.476196 0.466919H6.57774L10.0758 5.46692L10.5255 6.10692L13.5738 10.4669Z" fill="#4368E3"/>
|
||||
<path d="M41.8377 19.2219V4.16692H36.9905V1.71692H49.3835V4.17192H44.5362V19.2219H41.8377Z" fill="#FAFCFF"/>
|
||||
<path d="M55.2301 19.5219C53.9958 19.5219 52.9015 19.2669 51.942 18.7569C50.9826 18.2519 50.228 17.5169 49.6783 16.5669C49.1286 15.6169 48.8538 14.4919 48.8538 13.1919V12.8169C48.8538 11.5169 49.1286 10.3969 49.6783 9.45694C50.233 8.51694 50.9826 7.78194 51.942 7.26694C52.9015 6.75194 53.9958 6.49194 55.2301 6.49194C56.4645 6.49194 57.5638 6.75194 58.5283 7.26694C59.4927 7.78194 60.2523 8.51194 60.807 9.45694C61.3617 10.4019 61.6315 11.5169 61.6315 12.8169V13.1919C61.6315 14.4919 61.3567 15.6169 60.807 16.5669C60.2523 17.5169 59.5027 18.2469 58.5283 18.7569C57.5638 19.2619 56.4645 19.5219 55.2301 19.5219ZM55.2301 17.2169C56.3595 17.2169 57.284 16.8569 57.9936 16.1269C58.7032 15.4019 59.058 14.3969 59.058 13.1169V12.8919C59.058 11.6119 58.7082 10.6069 58.0086 9.88194C57.304 9.15194 56.3845 8.79194 55.2352 8.79194C54.0858 8.79194 53.2063 9.15194 52.4967 9.88194C51.7871 10.6069 51.4323 11.6119 51.4323 12.8919V13.1169C51.4323 14.4019 51.7871 15.4019 52.4967 16.1269C53.2063 16.8519 54.1158 17.2169 55.2352 17.2169H55.2301Z" fill="#FAFCFF"/>
|
||||
<path d="M70.1418 19.5219C68.9075 19.5219 67.8131 19.2669 66.8536 18.7569C65.8942 18.2519 65.1396 17.5169 64.5899 16.5669C64.0402 15.6169 63.7654 14.4919 63.7654 13.1919V12.8169C63.7654 11.5169 64.0402 10.3969 64.5899 9.45694C65.1446 8.51694 65.8942 7.78194 66.8536 7.26694C67.8131 6.75194 68.9075 6.49194 70.1418 6.49194C71.3761 6.49194 72.4754 6.75194 73.4399 7.26694C74.4044 7.78194 75.1639 8.51194 75.7186 9.45694C76.2733 10.4019 76.5431 11.5169 76.5431 12.8169V13.1919C76.5431 14.4919 76.2683 15.6169 75.7186 16.5669C75.1639 17.5169 74.4144 18.2469 73.4399 18.7569C72.4754 19.2619 71.3761 19.5219 70.1418 19.5219ZM70.1418 17.2169C71.2711 17.2169 72.1956 16.8569 72.9052 16.1269C73.6148 15.4019 73.9696 14.3969 73.9696 13.1169V12.8919C73.9696 11.6119 73.6198 10.6069 72.9202 9.88194C72.2156 9.15194 71.2961 8.79194 70.1468 8.79194C68.9974 8.79194 68.1179 9.15194 67.4083 9.88194C66.6987 10.6069 66.3439 11.6119 66.3439 12.8919V13.1169C66.3439 14.4019 66.6987 15.4019 67.4083 16.1269C68.1179 16.8519 69.0274 17.2169 70.1468 17.2169H70.1418Z" fill="#FAFCFF"/>
|
||||
<path d="M78.9418 19.2769V1.72192H81.5203V19.2769H78.9418Z" fill="#FAFCFF"/>
|
||||
<path d="M89.8806 19.5719C88.1316 19.5719 86.7423 19.0869 85.7029 18.1119C84.6685 17.1369 84.1538 15.7569 84.1538 13.9769V12.4719H86.8523V13.9769C86.8523 14.9119 87.1071 15.6569 87.6169 16.2119C88.1216 16.7719 88.8611 17.0469 89.8256 17.0469C90.7901 17.0469 91.4497 16.7719 91.8994 16.2219C92.3492 15.6769 92.574 14.9219 92.574 13.9719V4.16692H88.8761V1.71692H97.2264V4.17192H95.2725V13.9719C95.2725 15.7919 94.8028 17.1719 93.8583 18.1319C92.9188 19.0919 91.5846 19.5669 89.8706 19.5669L89.8806 19.5719Z" fill="#FAFCFF"/>
|
||||
<path d="M104.028 19.5719C102.778 19.5719 101.684 19.3069 100.754 18.7819C99.82 18.2569 99.0904 17.5169 98.5657 16.5569C98.041 15.5969 97.7761 14.4869 97.7761 13.2219V12.9219C97.7761 11.6369 98.036 10.5169 98.5507 9.56187C99.0654 8.60187 99.79 7.86187 100.714 7.33687C101.639 6.81187 102.713 6.54688 103.928 6.54688C105.142 6.54688 106.146 6.81187 107.041 7.33687C107.935 7.86187 108.625 8.59187 109.125 9.52687C109.624 10.4619 109.874 11.5519 109.874 12.8019V13.7769H100.4C100.43 14.8619 100.799 15.7219 101.489 16.3619C102.179 17.0019 103.043 17.3219 104.078 17.3219C105.112 17.3219 105.772 17.1069 106.266 16.6719C106.756 16.2369 107.141 15.7369 107.401 15.1769L109.529 16.2769C109.3 16.7419 108.955 17.2369 108.515 17.7519C108.07 18.2669 107.491 18.7019 106.776 19.0519C106.056 19.4019 105.142 19.5769 104.023 19.5769H104.028V19.5719ZM100.43 11.7969H107.256C107.191 10.8619 106.851 10.1319 106.256 9.60687C105.657 9.08187 104.872 8.81687 103.908 8.81687C102.943 8.81687 102.154 9.08187 101.544 9.60687C100.934 10.1319 100.565 10.8619 100.435 11.7969H100.43Z" fill="#FAFCFF"/>
|
||||
<path d="M115.716 19.2769C114.966 19.2769 114.367 19.0519 113.932 18.6019C113.487 18.1519 113.272 17.5519 113.272 16.8019V8.7219H111.244V6.5469H113.272V4.1719L115.851 2.8269V6.5469H119.529V8.7219H115.851V16.3519C115.851 16.8519 116.081 17.1019 116.556 17.1019H119.529V19.2769H115.721H115.716Z" fill="#FAFCFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5938_1632">
|
||||
<rect width="119.048" height="20" fill="white" transform="translate(0.476196 0.466919)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5 KiB |
10
frontend/assets/images/logo-fallback.svg
Normal file
10
frontend/assets/images/logo-fallback.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4324_3515)">
|
||||
<path d="M15.0001 18.3418V18.3337C15.0001 17.8734 15.3732 17.5003 15.8334 17.5003C16.2937 17.5003 16.6667 17.8734 16.6667 18.3337V18.3418C16.6667 18.802 16.2937 19.1751 15.8334 19.1751C15.3732 19.1751 15.0001 18.802 15.0001 18.3418ZM15.0001 15.8337V13.3337C15.0001 12.8734 15.3732 12.5003 15.8334 12.5003C16.2937 12.5003 16.6667 12.8734 16.6667 13.3337V15.8337C16.6667 16.2939 16.2937 16.667 15.8334 16.667C15.3732 16.667 15.0001 16.2939 15.0001 15.8337ZM16.6667 10.0003V5.00033C16.6667 4.5583 16.491 4.1345 16.1785 3.82194C15.8659 3.50938 15.4421 3.33366 15.0001 3.33366H5.00008C4.55805 3.33366 4.13426 3.50938 3.8217 3.82194C3.50914 4.1345 3.33341 4.5583 3.33341 5.00033V11.3219L6.08895 8.56641C6.60073 8.07396 7.23116 7.77539 7.91675 7.77539C8.60233 7.77539 9.23277 8.07396 9.74455 8.56641L11.6667 10.4886L11.9223 10.2331C12.6692 9.51427 13.6465 9.23834 14.6013 9.60075C15.0315 9.76408 15.2479 10.2447 15.0847 10.675C14.9214 11.1052 14.4407 11.3215 14.0105 11.1584C13.7538 11.0609 13.4392 11.0865 13.0779 11.4342L12.8451 11.667L13.0893 11.9111C13.4147 12.2366 13.4147 12.7641 13.0893 13.0895C12.7638 13.415 12.2363 13.415 11.9109 13.0895L8.58895 9.76758L8.49211 9.68132C8.2699 9.49865 8.07224 9.44206 7.91675 9.44206C7.73903 9.44206 7.50609 9.5159 7.24455 9.76758L3.33341 13.6787V15.0003L3.34155 15.1647C3.37937 15.5464 3.54812 15.9051 3.8217 16.1787C4.13426 16.4913 4.55805 16.667 5.00008 16.667H12.5001C12.9603 16.667 13.3334 17.0401 13.3334 17.5003C13.3334 17.9606 12.9603 18.3337 12.5001 18.3337H5.00008C4.11603 18.3337 3.26843 17.9822 2.64331 17.3571C2.01819 16.732 1.66675 15.8844 1.66675 15.0003V5.00033C1.66675 4.11627 2.01819 3.26868 2.64331 2.64355C3.26843 2.01843 4.11603 1.66699 5.00008 1.66699H15.0001C15.8841 1.66699 16.7317 2.01843 17.3569 2.64355C17.982 3.26868 18.3334 4.11627 18.3334 5.00033V10.0003C18.3334 10.4606 17.9603 10.8337 17.5001 10.8337C17.0398 10.8337 16.6667 10.4606 16.6667 10.0003ZM12.5082 5.83366C12.9685 5.83366 13.3416 6.20676 13.3416 6.66699C13.3416 7.12723 12.9685 7.50033 12.5082 7.50033H12.5001C12.0398 7.50033 11.6667 7.12723 11.6667 6.66699C11.6667 6.20676 12.0398 5.83366 12.5001 5.83366H12.5082Z" fill="#ACB2B9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4324_3515">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
18
frontend/assets/images/logo-light.svg
Normal file
18
frontend/assets/images/logo-light.svg
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<svg width="120" height="21" viewBox="0 0 120 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5892_2467)">
|
||||
<path d="M31.8734 10.4669L28.8252 14.8269L28.3754 15.4669L25.7769 19.1819V19.1869L24.8774 20.4669H12.6743L13.5738 19.1819L16.1773 15.4669H22.2739L22.7236 14.8269L25.7769 10.4669L22.7236 6.10692L22.2739 5.46692H17.0718L19.6754 1.75192V1.74692L20.5748 0.466919H24.8774L25.7769 1.74692V1.75192L28.3754 5.46692L28.8252 6.10692L31.8734 10.4669Z" fill="#4368E3"/>
|
||||
<path d="M13.5738 10.4669L10.5255 14.8269L10.0758 15.4669L6.57774 20.4669H0.476196L3.97422 15.4669L7.47224 10.4669L3.97422 5.46692L0.476196 0.466919H6.57774L10.0758 5.46692L10.5255 6.10692L13.5738 10.4669Z" fill="#4368E3"/>
|
||||
<path d="M41.8378 19.2219V4.16692H36.9905V1.71692H49.3835V4.17192H44.5363V19.2219H41.8378Z" fill="#2D343B"/>
|
||||
<path d="M55.2302 19.5219C53.9959 19.5219 52.9015 19.2669 51.9421 18.7569C50.9826 18.2519 50.228 17.5169 49.6784 16.5669C49.1287 15.6169 48.8538 14.4919 48.8538 13.1919V12.8169C48.8538 11.5169 49.1287 10.3969 49.6784 9.45694C50.233 8.51694 50.9826 7.78194 51.9421 7.26694C52.9015 6.75194 53.9959 6.49194 55.2302 6.49194C56.4645 6.49194 57.5639 6.75194 58.5283 7.26694C59.4928 7.78194 60.2524 8.51194 60.8071 9.45694C61.3617 10.4019 61.6316 11.5169 61.6316 12.8169V13.1919C61.6316 14.4919 61.3567 15.6169 60.8071 16.5669C60.2524 17.5169 59.5028 18.2469 58.5283 18.7569C57.5639 19.2619 56.4645 19.5219 55.2302 19.5219ZM55.2302 17.2169C56.3596 17.2169 57.284 16.8569 57.9936 16.1269C58.7032 15.4019 59.058 14.3969 59.058 13.1169V12.8919C59.058 11.6119 58.7082 10.6069 58.0086 9.88194C57.304 9.15194 56.3846 8.79194 55.2352 8.79194C54.0859 8.79194 53.2064 9.15194 52.4968 9.88194C51.7872 10.6069 51.4324 11.6119 51.4324 12.8919V13.1169C51.4324 14.4019 51.7872 15.4019 52.4968 16.1269C53.2064 16.8519 54.1158 17.2169 55.2352 17.2169H55.2302Z" fill="#2D343B"/>
|
||||
<path d="M70.1418 19.5219C68.9075 19.5219 67.8131 19.2669 66.8536 18.7569C65.8942 18.2519 65.1396 17.5169 64.5899 16.5669C64.0402 15.6169 63.7654 14.4919 63.7654 13.1919V12.8169C63.7654 11.5169 64.0402 10.3969 64.5899 9.45694C65.1446 8.51694 65.8942 7.78194 66.8536 7.26694C67.8131 6.75194 68.9075 6.49194 70.1418 6.49194C71.3761 6.49194 72.4754 6.75194 73.4399 7.26694C74.4044 7.78194 75.1639 8.51194 75.7186 9.45694C76.2733 10.4019 76.5431 11.5169 76.5431 12.8169V13.1919C76.5431 14.4919 76.2683 15.6169 75.7186 16.5669C75.1639 17.5169 74.4144 18.2469 73.4399 18.7569C72.4754 19.2619 71.3761 19.5219 70.1418 19.5219ZM70.1418 17.2169C71.2711 17.2169 72.1956 16.8569 72.9052 16.1269C73.6148 15.4019 73.9696 14.3969 73.9696 13.1169V12.8919C73.9696 11.6119 73.6198 10.6069 72.9202 9.88194C72.2156 9.15194 71.2961 8.79194 70.1468 8.79194C68.9974 8.79194 68.1179 9.15194 67.4083 9.88194C66.6987 10.6069 66.3439 11.6119 66.3439 12.8919V13.1169C66.3439 14.4019 66.6987 15.4019 67.4083 16.1269C68.1179 16.8519 69.0274 17.2169 70.1468 17.2169H70.1418Z" fill="#2D343B"/>
|
||||
<path d="M78.9418 19.2769V1.72192H81.5203V19.2769H78.9418Z" fill="#2D343B"/>
|
||||
<path d="M89.8806 19.5719C88.1316 19.5719 86.7423 19.0869 85.7029 18.1119C84.6685 17.1369 84.1538 15.7569 84.1538 13.9769V12.4719H86.8523V13.9769C86.8523 14.9119 87.1071 15.6569 87.6169 16.2119C88.1216 16.7719 88.8611 17.0469 89.8256 17.0469C90.7901 17.0469 91.4497 16.7719 91.8994 16.2219C92.3492 15.6769 92.574 14.9219 92.574 13.9719V4.16692H88.8761V1.71692H97.2264V4.17192H95.2725V13.9719C95.2725 15.7919 94.8028 17.1719 93.8583 18.1319C92.9188 19.0919 91.5846 19.5669 89.8706 19.5669L89.8806 19.5719Z" fill="#2D343B"/>
|
||||
<path d="M104.028 19.5719C102.778 19.5719 101.684 19.3069 100.754 18.7819C99.82 18.2569 99.0904 17.5169 98.5657 16.5569C98.041 15.5969 97.7761 14.4869 97.7761 13.2219V12.9219C97.7761 11.6369 98.036 10.5169 98.5507 9.56187C99.0654 8.60187 99.79 7.86187 100.714 7.33687C101.639 6.81187 102.713 6.54688 103.928 6.54688C105.142 6.54688 106.146 6.81187 107.041 7.33687C107.935 7.86187 108.625 8.59187 109.125 9.52687C109.624 10.4619 109.874 11.5519 109.874 12.8019V13.7769H100.4C100.43 14.8619 100.799 15.7219 101.489 16.3619C102.179 17.0019 103.043 17.3219 104.078 17.3219C105.112 17.3219 105.772 17.1069 106.266 16.6719C106.756 16.2369 107.141 15.7369 107.401 15.1769L109.529 16.2769C109.3 16.7419 108.955 17.2369 108.515 17.7519C108.07 18.2669 107.491 18.7019 106.776 19.0519C106.056 19.4019 105.142 19.5769 104.023 19.5769H104.028V19.5719ZM100.43 11.7969H107.256C107.191 10.8619 106.851 10.1319 106.256 9.60687C105.657 9.08187 104.872 8.81687 103.908 8.81687C102.943 8.81687 102.154 9.08187 101.544 9.60687C100.934 10.1319 100.565 10.8619 100.435 11.7969H100.43Z" fill="#2D343B"/>
|
||||
<path d="M115.716 19.2769C114.966 19.2769 114.367 19.0519 113.932 18.6019C113.487 18.1519 113.272 17.5519 113.272 16.8019V8.7219H111.244V6.5469H113.272V4.1719L115.851 2.8269V6.5469H119.529V8.7219H115.851V16.3519C115.851 16.8519 116.081 17.1019 116.556 17.1019H119.529V19.2769H115.721H115.716Z" fill="#2D343B"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5892_2467">
|
||||
<rect width="119.048" height="20" fill="white" transform="translate(0.476196 0.466919)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5 KiB |
10
frontend/assets/images/tooljetdb.svg
Normal file
10
frontend/assets/images/tooljetdb.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="37" height="37" viewBox="0 0 37 37" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.4">
|
||||
<path d="M33.7922 27.5C33.7922 30.8137 31.1059 33.5 27.7922 33.5H12.7922V12.5H33.7922V27.5Z" fill="#3E63DD"/>
|
||||
</g>
|
||||
<path d="M12.7922 23H33.7922V29.5C33.7922 31.7091 32.0014 33.5 29.7922 33.5H12.7922V23Z" fill="#3E63DD"/>
|
||||
<path d="M9.79199 3.5H27.792C31.1057 3.5 33.792 6.18629 33.792 9.5V12.5H3.79199V9.5C3.79199 6.18629 6.47828 3.5 9.79199 3.5Z" fill="#3E63DD"/>
|
||||
<g opacity="0.4">
|
||||
<path d="M9.79199 33.5C6.47828 33.5 3.79199 30.8137 3.79199 27.5V12.5H12.792V33.5H9.79199Z" fill="#3E63DD"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 617 B |
|
|
@ -380,8 +380,10 @@
|
|||
"to": "zu"
|
||||
},
|
||||
"noApplicationFound": "Keine Apps gefunden",
|
||||
"noWorkflowFound": "Keine Workflows gefunden",
|
||||
"thisFolderIsEmpty": "Dieser Ordner ist leer",
|
||||
"deleteAppAndData": "Die App und die zugehörigen Daten werden endgültig gelöscht. Möchten Sie fortfahren?",
|
||||
"deleteWorkflowAndData": "Der Workflow {{appName}} und die zugehörigen Daten werden dauerhaft gelöscht. Fortfahren?",
|
||||
"removeAppFromFolder": "Die App wird aus diesem Ordner entfernt. Möchten Sie fortfahren?",
|
||||
"change": "Ändern Sie",
|
||||
"templateCard": {
|
||||
|
|
@ -460,9 +462,12 @@
|
|||
"properties": "Eigenschaften",
|
||||
"events": "Ereignisse",
|
||||
"layout": "Layout",
|
||||
"devices": "Geräte",
|
||||
"styles": "Stile",
|
||||
"general": "Allgemein",
|
||||
"validation": "Validierung",
|
||||
"data": "Daten",
|
||||
"additionalActions": "Zusätzliche Aktionen",
|
||||
"documentation": "{{componentMeta}} Dokumentation",
|
||||
"widgetNameEmptyError": "Widget-Name darf nicht leer sein",
|
||||
"componentNameExistsError": "Komponentenname existiert bereits",
|
||||
|
|
@ -532,6 +537,9 @@
|
|||
"enableTimeSelection": "Zeitauswahl aktivieren?",
|
||||
"enableDateSelection": "Datumsauswahl aktivieren?",
|
||||
"disabledDates": "Deaktivierte Dateien",
|
||||
"minDate": "Mindestdatum",
|
||||
"maxDate": "Maximaldatum",
|
||||
"makeThisFieldMandatory": "Dieses Feld als Pflichtfeld festlegen",
|
||||
"setChecked": "Als geprüft einstellen",
|
||||
"status": "status",
|
||||
"defaultStatus": "Standard-Status",
|
||||
|
|
@ -583,6 +591,9 @@
|
|||
"parseContent": "Inhalt analysieren",
|
||||
"fileType": "Dateityp",
|
||||
"dateFormat": "Datumsformat",
|
||||
"timeFormat": "Zeitformat",
|
||||
"displayIn": "Anzeigen in",
|
||||
"storeIn": "Speichern in",
|
||||
"defaultDate": "Standard-Datum",
|
||||
"events": "Events",
|
||||
"resources": "Ressourcen",
|
||||
|
|
@ -913,5 +924,21 @@
|
|||
"text": "Zurück",
|
||||
"tip": "Zurück zur Startseite"
|
||||
}
|
||||
},
|
||||
"datepicker": {
|
||||
"months": {
|
||||
"january": "Januar",
|
||||
"february": "Februar",
|
||||
"march": "März",
|
||||
"april": "April",
|
||||
"may": "Mai",
|
||||
"june": "Juni",
|
||||
"july": "Juli",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "Oktober",
|
||||
"november": "November",
|
||||
"december": "Dezember"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,14 +138,20 @@
|
|||
"appVersionManager": {
|
||||
"version": "Version",
|
||||
"currentlyReleased": "Currently Released",
|
||||
"createVersion": "Create new version",
|
||||
"createVersion": "Create version",
|
||||
"saveVersion": "Save version",
|
||||
"createDraftVersion": "Create draft version",
|
||||
"versionName": "Version name",
|
||||
"createVersionFrom": "Create version from",
|
||||
"versionDescription": "Version description",
|
||||
"createVersionFrom": "Create from version",
|
||||
"save": "Save",
|
||||
"create": "Create Version",
|
||||
"editVersion": "Edit Version",
|
||||
"editVersion": "Edit version",
|
||||
"deleteVersion": "Do you really want to delete this version ({{version}})?",
|
||||
"enterVersionName": "Enter version name",
|
||||
"enterVersionDescription": "Enter version description",
|
||||
"versionNameHelper": "Version name must be unique and max 25 characters",
|
||||
"versionDescriptionHelper": "Description must be max 500 characters",
|
||||
"versionAlreadyReleased": "You cannot make changes to a version that has already been released. \n Create a new version or switch to a different version if you want to make changes."
|
||||
},
|
||||
"queries": "Queries",
|
||||
|
|
@ -440,10 +446,11 @@
|
|||
"to": "to"
|
||||
},
|
||||
"noApplicationFound": "No Applications found",
|
||||
"noWorkflowFound": "No workflows found",
|
||||
"thisFolderIsEmpty": "This folder is empty",
|
||||
"nonAccessibleFolderApps": "You do not have access to any applications in this folder.",
|
||||
"deleteAppAndData": "The app {{appName}} and the associated data will be permanently deleted, do you want to continue?",
|
||||
"deleteWorkflowAndData": "Are you sure you want to delete the workflow {{appName}}? This action will not only remove it from the system but also from all the apps where it is currently in use. Please confirm to proceed.",
|
||||
"deleteWorkflowAndData": "The workflow {{appName}} and the associated data will be permanently deleted, do you want to continue?",
|
||||
"removeAppFromFolder": "The app will be removed from this folder, do you want to continue?",
|
||||
"change": "Change",
|
||||
"templateCard": {
|
||||
|
|
@ -575,6 +582,8 @@
|
|||
"general": "General",
|
||||
"validation": "Validation",
|
||||
"structure": "Structure",
|
||||
"data": "Data",
|
||||
"additionalActions": "Additional Actions",
|
||||
"documentation": "Read documentation for {{componentMeta}}",
|
||||
"widgetNameEmptyError": "Widget name cannot be empty",
|
||||
"componentNameExistsError": "Component name already exists",
|
||||
|
|
@ -644,6 +653,9 @@
|
|||
"enableTimeSelection": "Enable time selection?",
|
||||
"enableDateSelection": "Enable date selection?",
|
||||
"disabledDates": "Disabled dates",
|
||||
"minDate": "Min Date",
|
||||
"maxDate": "Max Date",
|
||||
"makeThisFieldMandatory": "Make this field mandatory",
|
||||
"setChecked": "Set checked",
|
||||
"status": "status",
|
||||
"defaultStatus": "Default status",
|
||||
|
|
@ -695,6 +707,9 @@
|
|||
"parseContent": "Parse content",
|
||||
"fileType": "File type",
|
||||
"dateFormat": "Date format",
|
||||
"timeFormat": "Time Format",
|
||||
"displayIn": "Display in",
|
||||
"storeIn": "Store in",
|
||||
"defaultDate": "Default date",
|
||||
"events": "Events",
|
||||
"resources": "Resources",
|
||||
|
|
@ -849,11 +864,11 @@
|
|||
"description": "User-controlled on-off switch"
|
||||
},
|
||||
"Textarea": {
|
||||
"displayName": "Textarea",
|
||||
"displayName": "Text Area",
|
||||
"description": "Multi-line text input"
|
||||
},
|
||||
"DateRangePicker": {
|
||||
"displayName": "Range Picker",
|
||||
"displayName": "Date Range Picker",
|
||||
"description": "Choose date ranges"
|
||||
},
|
||||
"Text": {
|
||||
|
|
@ -873,7 +888,7 @@
|
|||
"description": "Single item selector"
|
||||
},
|
||||
"Multiselect": {
|
||||
"displayName": "Multiselect",
|
||||
"displayName": "Multi Select",
|
||||
"description": "Multiple item selector"
|
||||
},
|
||||
"RichTextEditor": {
|
||||
|
|
@ -1033,5 +1048,21 @@
|
|||
"text": "Back",
|
||||
"tip": "Back to Home"
|
||||
}
|
||||
},
|
||||
"datepicker": {
|
||||
"months": {
|
||||
"january": "January",
|
||||
"february": "February",
|
||||
"march": "March",
|
||||
"april": "April",
|
||||
"may": "May",
|
||||
"june": "June",
|
||||
"july": "July",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "October",
|
||||
"november": "November",
|
||||
"december": "December"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -284,9 +284,9 @@
|
|||
"createUpdateDelete": "Crear/Actualizar/Eliminar",
|
||||
"folder": "Carpeta"
|
||||
},
|
||||
"groupOptions":{
|
||||
"deleteGroup":"Borrar Grupo",
|
||||
"duplicateGroup":"Duplicar Grupo"
|
||||
"groupOptions": {
|
||||
"deleteGroup": "Borrar Grupo",
|
||||
"duplicateGroup": "Duplicar Grupo"
|
||||
}
|
||||
},
|
||||
"manageSSO": {
|
||||
|
|
@ -414,8 +414,10 @@
|
|||
"to": "hasta"
|
||||
},
|
||||
"noApplicationFound": "No se encontraron aplicaciones",
|
||||
"noWorkflowFound": "No se encontraron flujos de trabajo",
|
||||
"thisFolderIsEmpty": "Esta carpeta está vacía",
|
||||
"deleteAppAndData": "La aplicación {{appName}} y los datos asociados se eliminarán permanentemente, ¿desea continuar?",
|
||||
"deleteWorkflowAndData": "El flujo de trabajo {{appName}} y sus datos asociados se eliminarán permanentemente. ¿Continuar?",
|
||||
"removeAppFromFolder": "La aplicación se eliminará de esta carpeta, ¿desea continuar?",
|
||||
"change": "Cambiar",
|
||||
"templateCard": {
|
||||
|
|
@ -494,9 +496,12 @@
|
|||
"properties": "Propiedades",
|
||||
"events": "Eventos",
|
||||
"layout": "Diseño",
|
||||
"devices": "Dispositivos",
|
||||
"styles": "Estilos",
|
||||
"general": "General",
|
||||
"validation": "Validación",
|
||||
"data": "Datos",
|
||||
"additionalActions": "Acciones adicionales",
|
||||
"documentation": "{{componentMeta}} documentación",
|
||||
"widgetNameEmptyError": "El nombre del widget no puede estar vacío",
|
||||
"componentNameExistsError": "El nombre del componente ya existe",
|
||||
|
|
@ -566,6 +571,9 @@
|
|||
"enableTimeSelection": "Habilitar selección de tiempo",
|
||||
"enableDateSelection": "Habilitar selección de fecha",
|
||||
"disabledDates": "Fechas deshabilitadas",
|
||||
"minDate": "Fecha mínima",
|
||||
"maxDate": "Fecha máxima",
|
||||
"makeThisFieldMandatory": "Hacer este campo obligatorio",
|
||||
"setChecked": "Establecer marcado",
|
||||
"status": "Estado",
|
||||
"defaultStatus": "Estado predeterminado",
|
||||
|
|
@ -617,6 +625,9 @@
|
|||
"parseContent": "Analizar contenido",
|
||||
"fileType": "Tipo de archivo",
|
||||
"dateFormat": "Formato de fecha",
|
||||
"timeFormat": "Formato de hora",
|
||||
"displayIn": "Mostrar en",
|
||||
"storeIn": "Almacenar en",
|
||||
"defaultDate": "Fecha predeterminada",
|
||||
"events": "Eventos",
|
||||
"resources": "Recursos",
|
||||
|
|
@ -725,9 +736,9 @@
|
|||
"addColumn": "+ Agregar columna",
|
||||
"addNewColumn": "Agregar nueva columna",
|
||||
"noActionMessage": "Esta tabla no tiene botones de acción",
|
||||
"textAlignment":"Alineacion del texto",
|
||||
"deciamalPlaces":"Decimales",
|
||||
"imageFit":"Ajuste de imagen"
|
||||
"textAlignment": "Alineacion del texto",
|
||||
"deciamalPlaces": "Decimales",
|
||||
"imageFit": "Ajuste de imagen"
|
||||
},
|
||||
"Button": {
|
||||
"displayName": "Botón",
|
||||
|
|
@ -953,5 +964,21 @@
|
|||
"text": "Atrás",
|
||||
"tip": "Atrás a la vista anterior (página principal)"
|
||||
}
|
||||
},
|
||||
"datepicker": {
|
||||
"months": {
|
||||
"january": "Enero",
|
||||
"february": "Febrero",
|
||||
"march": "Marzo",
|
||||
"april": "Abril",
|
||||
"may": "Mayo",
|
||||
"june": "Junio",
|
||||
"july": "Julio",
|
||||
"august": "Agosto",
|
||||
"september": "Septiembre",
|
||||
"october": "Octubre",
|
||||
"november": "Noviembre",
|
||||
"december": "Diciembre"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -380,8 +380,10 @@
|
|||
"to": "à"
|
||||
},
|
||||
"noApplicationFound": "Aucune application trouvée",
|
||||
"noWorkflowFound": "Aucun flux de travail trouvé",
|
||||
"thisFolderIsEmpty": "Ce dossier est vide",
|
||||
"deleteAppAndData": "L'application et les données associées seront supprimées en permanence, voulez-vous continuer ?",
|
||||
"deleteWorkflowAndData": "Le workflow {{appName}} et ses données associées seront définitivement supprimés. Continuer ?",
|
||||
"removeAppFromFolder": "L'application sera supprimée de ce dossier, voulez-vous continuer ?",
|
||||
"change": "Changer",
|
||||
"templateCard": {
|
||||
|
|
@ -460,9 +462,12 @@
|
|||
"properties": "Propriétés",
|
||||
"events": "Événements",
|
||||
"layout": "Disposition",
|
||||
"devices": "Appareils",
|
||||
"styles": "modes",
|
||||
"general": "Générale",
|
||||
"validation": "Validation",
|
||||
"data": "Données",
|
||||
"additionalActions": "Actions supplémentaires",
|
||||
"documentation": "{{componentMeta}} documentation",
|
||||
"widgetNameEmptyError": "Le nom du widget ne peut pas être vide",
|
||||
"componentNameExistsError": "Le nom du composant existe déjà",
|
||||
|
|
@ -532,6 +537,9 @@
|
|||
"enableTimeSelection": "Activer la sélection du temps ?",
|
||||
"enableDateSelection": "Activer la sélection de la date ?",
|
||||
"disabledDates": "Dates désactivées",
|
||||
"minDate": "Date minimum",
|
||||
"maxDate": "Date maximum",
|
||||
"makeThisFieldMandatory": "Rendre ce champ obligatoire",
|
||||
"setChecked": "Définir vérifié",
|
||||
"status": "statut",
|
||||
"defaultStatus": "État par défaut",
|
||||
|
|
@ -583,6 +591,9 @@
|
|||
"parseContent": "Analyser le contenu",
|
||||
"fileType": "Type de fichier",
|
||||
"dateFormat": "Format de date",
|
||||
"timeFormat": "Format de l'heure",
|
||||
"displayIn": "Afficher dans",
|
||||
"storeIn": "Stocker dans",
|
||||
"defaultDate": "Date de défaut",
|
||||
"events": "Événements",
|
||||
"resources": "Ressources",
|
||||
|
|
@ -913,5 +924,21 @@
|
|||
"text": "Retour",
|
||||
"tip": "De retour à la maison"
|
||||
}
|
||||
},
|
||||
"datepicker": {
|
||||
"months": {
|
||||
"january": "Janvier",
|
||||
"february": "Février",
|
||||
"march": "Mars",
|
||||
"april": "Avril",
|
||||
"may": "Mai",
|
||||
"june": "Juin",
|
||||
"july": "Juillet",
|
||||
"august": "Août",
|
||||
"september": "Septembre",
|
||||
"october": "Octobre",
|
||||
"november": "Novembre",
|
||||
"december": "Décembre"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,41 +1,41 @@
|
|||
{
|
||||
"globals":{
|
||||
"readDocumentation":"Leggi documentazione",
|
||||
"cancel":"Annulla",
|
||||
"save":"Salva",
|
||||
"back":"Indietro",
|
||||
"edit":"Modifica",
|
||||
"search":"Cerca",
|
||||
"update":"Aggiorna",
|
||||
"delete":"Cancella",
|
||||
"add":"Aggiungi",
|
||||
"view":"Esplora",
|
||||
"create":"Crea",
|
||||
"enabled":"Abilitato",
|
||||
"disabled":"Disabilitato",
|
||||
"yes":"Sì",
|
||||
"submit":"Conferma",
|
||||
"select":"Selezione",
|
||||
"environmentVar":"Variabili globali",
|
||||
"saving":"Salvataggio in corso...",
|
||||
"saveDatasource":"Salva dati",
|
||||
"authorize":"Autoizza",
|
||||
"connect":"Connetti",
|
||||
"reconnect":"Riconnetti",
|
||||
"components":"Componenti",
|
||||
"send":"Invia",
|
||||
"noConnection":"Impossibile connettersi",
|
||||
"connectionVerifeid":"Connessione verificata",
|
||||
"left":"Sinistra",
|
||||
"center":"Centro",
|
||||
"right":"Destra",
|
||||
"justified":"Giustifica",
|
||||
"host":"Host",
|
||||
"operation":"Operazione",
|
||||
"header":"TESTATA",
|
||||
"path":"PERCORSO",
|
||||
"query":"RICHIESTA",
|
||||
"requestBody":"CORPO DELLA RICHIESTA"
|
||||
"globals": {
|
||||
"readDocumentation": "Leggi documentazione",
|
||||
"cancel": "Annulla",
|
||||
"save": "Salva",
|
||||
"back": "Indietro",
|
||||
"edit": "Modifica",
|
||||
"search": "Cerca",
|
||||
"update": "Aggiorna",
|
||||
"delete": "Cancella",
|
||||
"add": "Aggiungi",
|
||||
"view": "Esplora",
|
||||
"create": "Crea",
|
||||
"enabled": "Abilitato",
|
||||
"disabled": "Disabilitato",
|
||||
"yes": "Sì",
|
||||
"submit": "Conferma",
|
||||
"select": "Selezione",
|
||||
"environmentVar": "Variabili globali",
|
||||
"saving": "Salvataggio in corso...",
|
||||
"saveDatasource": "Salva dati",
|
||||
"authorize": "Autoizza",
|
||||
"connect": "Connetti",
|
||||
"reconnect": "Riconnetti",
|
||||
"components": "Componenti",
|
||||
"send": "Invia",
|
||||
"noConnection": "Impossibile connettersi",
|
||||
"connectionVerifeid": "Connessione verificata",
|
||||
"left": "Sinistra",
|
||||
"center": "Centro",
|
||||
"right": "Destra",
|
||||
"justified": "Giustifica",
|
||||
"host": "Host",
|
||||
"operation": "Operazione",
|
||||
"header": "TESTATA",
|
||||
"path": "PERCORSO",
|
||||
"query": "RICHIESTA",
|
||||
"requestBody": "CORPO DELLA RICHIESTA"
|
||||
},
|
||||
"errorBoundary": "Qualcosa è andato storto.",
|
||||
"viewer": "Spiacente! L'app è al momento in fase di manutenzione.",
|
||||
|
|
@ -380,8 +380,10 @@
|
|||
"to": "a"
|
||||
},
|
||||
"noApplicationFound": "Nessuna Applicazione trovata",
|
||||
"noWorkflowFound": "Nessun flusso di lavoro trovato",
|
||||
"thisFolderIsEmpty": "Questa cartella è vuota",
|
||||
"deleteAppAndData": "L'app e i dati ad essa associati saranno eliminati in modo permanente, vuoi continuare?",
|
||||
"deleteWorkflowAndData": "Il flusso di lavoro {{appName}} e i dati associati verranno eliminati definitivamente. Continuare?",
|
||||
"removeAppFromFolder": "L'applicazione verrà rimossa da questa cartella, vuoi continuare?",
|
||||
"change": "Modifica",
|
||||
"templateCard": {
|
||||
|
|
@ -460,9 +462,12 @@
|
|||
"properties": "Proprietà",
|
||||
"events": "Eventi",
|
||||
"layout": "Layout",
|
||||
"devices": "Dispositivi",
|
||||
"styles": "Stili",
|
||||
"general": "Generale",
|
||||
"validation": "Validazione",
|
||||
"data": "Dati",
|
||||
"additionalActions": "Azioni aggiuntive",
|
||||
"documentation": "{{componentMeta}} documentazione",
|
||||
"widgetNameEmptyError": "Il nome del Widget non può essere lasciato vuoto",
|
||||
"componentNameExistsError": "Nome del componente già esistente",
|
||||
|
|
@ -582,6 +587,14 @@
|
|||
"parseContent": "Effettua il parsing del contenuto",
|
||||
"fileType": "Tipo di file",
|
||||
"dateFormat": "Formato data",
|
||||
"timeFormat": "Formato ora",
|
||||
"minTime": "Ora minima",
|
||||
"maxTime": "Ora massima",
|
||||
"displayIn": "Visualizza in",
|
||||
"storeIn": "Memorizza in",
|
||||
"minDate": "Data minima",
|
||||
"maxDate": "Data massima",
|
||||
"makeThisFieldMandatory": "Rendi questo campo obbligatorio",
|
||||
"defaultDate": "Data predefinita",
|
||||
"events": "Eventi",
|
||||
"resources": "Risorse",
|
||||
|
|
@ -912,5 +925,21 @@
|
|||
"text": "Indietro",
|
||||
"tip": "Torna alla Home"
|
||||
}
|
||||
},
|
||||
"datepicker": {
|
||||
"months": {
|
||||
"january": "Gennaio",
|
||||
"february": "Febbraio",
|
||||
"march": "Marzo",
|
||||
"april": "Aprile",
|
||||
"may": "Maggio",
|
||||
"june": "Giugno",
|
||||
"july": "Luglio",
|
||||
"august": "Agosto",
|
||||
"september": "Settembre",
|
||||
"october": "Ottobre",
|
||||
"november": "Novembre",
|
||||
"december": "Dicembre"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -411,8 +411,10 @@
|
|||
"to": "到"
|
||||
},
|
||||
"noApplicationFound": "未找到任何应用",
|
||||
"noWorkflowFound": "未找到工作流程",
|
||||
"thisFolderIsEmpty": "此文件夹为空",
|
||||
"deleteAppAndData": "应用 {{appName}} 和相关数据将被永久删除,你还要继续吗?",
|
||||
"deleteWorkflowAndData": "工作流程 {{appName}} 和相关数据将被永久删除,是否继续",
|
||||
"removeAppFromFolder": "应用将从该文件夹中移除,你还要继续吗?",
|
||||
"change": "更改",
|
||||
"templateCard": {
|
||||
|
|
@ -491,9 +493,12 @@
|
|||
"properties": "属性",
|
||||
"events": "事件",
|
||||
"layout": "布局",
|
||||
"devices": "设备",
|
||||
"styles": "样式",
|
||||
"general": "一般",
|
||||
"validation": "验证",
|
||||
"data": "数据",
|
||||
"additionalActions": "附加操作",
|
||||
"documentation": "读取 {{componentMeta}} 的文档",
|
||||
"widgetNameEmptyError": "部件名称不能为空",
|
||||
"componentNameExistsError": "组件名称已存在",
|
||||
|
|
@ -562,6 +567,9 @@
|
|||
"enableTimeSelection": "启用时间选择?",
|
||||
"enableDateSelection": "启用日期选择?",
|
||||
"disabledDates": "禁用日期",
|
||||
"minDate": "最小日期",
|
||||
"maxDate": "最大日期",
|
||||
"makeThisFieldMandatory": "将此字段设为必填",
|
||||
"setChecked": "设置选中状态",
|
||||
"status": "状态",
|
||||
"defaultStatus": "默认状态",
|
||||
|
|
@ -613,6 +621,9 @@
|
|||
"parseContent": "解析内容",
|
||||
"fileType": "文件类型",
|
||||
"dateFormat": "日期格式",
|
||||
"timeFormat": "时间格式",
|
||||
"displayIn": "显示时区",
|
||||
"storeIn": "存储时区",
|
||||
"defaultDate": "默认日期",
|
||||
"events": "事件",
|
||||
"resources": "资源",
|
||||
|
|
@ -947,5 +958,21 @@
|
|||
"text": "返回",
|
||||
"tip": "返回主页"
|
||||
}
|
||||
},
|
||||
"datepicker": {
|
||||
"months": {
|
||||
"january": "一月",
|
||||
"february": "二月",
|
||||
"march": "三月",
|
||||
"april": "四月",
|
||||
"may": "五月",
|
||||
"june": "六月",
|
||||
"july": "七月",
|
||||
"august": "八月",
|
||||
"september": "九月",
|
||||
"october": "十月",
|
||||
"november": "十一月",
|
||||
"december": "十二月"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/styles/theme.scss",
|
||||
"baseColor": "zinc",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
"prefix": "tw-"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 71c536473b79fc008b7d54b0ba7d4d70b74f8ee9
|
||||
Subproject commit 33df96293f1621058ba6207e8992088f9168f595
|
||||
13933
frontend/package-lock.json
generated
13933
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -22,11 +22,14 @@
|
|||
"@radix-ui/colors": "^0.1.8",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
|
|
@ -49,8 +52,9 @@
|
|||
"axios": "^1.3.3",
|
||||
"bootstrap": "^5.2.3",
|
||||
"buffer": "^6.0.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"classnames": "^2.3.2",
|
||||
"clsx": "^2.1.1",
|
||||
"cron-validator": "^1.3.1",
|
||||
"cronstrue": "^2.51.0",
|
||||
"deep-object-diff": "^1.1.9",
|
||||
|
|
@ -73,13 +77,15 @@
|
|||
"i18next-http-backend": "^2.1.1",
|
||||
"immer": "^9.0.19",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"jspdf-autotable": "^3.5.28",
|
||||
"jspdf": "^3.0.2",
|
||||
"jspdf-autotable": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.525.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.40",
|
||||
"papaparse": "^5.3.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "5.3.93",
|
||||
"plotly.js-dist-min": "^2.29.1",
|
||||
"posthog-js": "^1.255.1",
|
||||
"process": "^0.11.10",
|
||||
|
|
@ -115,10 +121,11 @@
|
|||
"react-lazyload": "^3.2.0",
|
||||
"react-loading-skeleton": "^3.1.1",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-media-recorder": "^1.7.2",
|
||||
"react-mentions": "^4.4.7",
|
||||
"react-moveable": "^0.54.1",
|
||||
"react-moveable": "^0.56.0",
|
||||
"react-multi-select-component": "^4.3.4",
|
||||
"react-pdf": "^6.2.2",
|
||||
"react-pdf": "^10.1.0",
|
||||
"react-phone-input-2": "^2.15.1",
|
||||
"react-phone-number-input": "^3.4.12",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
|
|
@ -136,6 +143,7 @@
|
|||
"react-virtuoso": "^4.1.0",
|
||||
"react-zoom-pan-pinch": "^2.6.1",
|
||||
"reactflow": "^11.7.4",
|
||||
"read-excel-file": "^5.8.8",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rfdc": "^1.3.1",
|
||||
|
|
@ -143,15 +151,18 @@
|
|||
"semver": "^7.3.8",
|
||||
"string-hash": "^1.1.3",
|
||||
"superstruct": "^1.0.3",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"tw-animate-css": "^1.3.7",
|
||||
"url-join": "^5.0.0",
|
||||
"use-react-router-breadcrumbs": "^4.0.1",
|
||||
"util": "^0.12.5",
|
||||
"uuid": "9.0.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"y-websocket": "^1.4.5",
|
||||
"yjs": "^13.5.46",
|
||||
"zipcelx": "^1.6.2",
|
||||
"zustand": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -161,13 +172,10 @@
|
|||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
|
||||
"@storybook/addon-essentials": "^7.2.1",
|
||||
"@storybook/addon-interactions": "^7.2.1",
|
||||
"@storybook/addon-links": "^7.2.1",
|
||||
"@storybook/addon-onboarding": "^1.0.8",
|
||||
"@storybook/blocks": "^7.2.1",
|
||||
"@storybook/react": "^7.2.1",
|
||||
"@storybook/react-webpack5": "^7.2.1",
|
||||
"@storybook/addon-docs": "^9.1.5",
|
||||
"@storybook/addon-links": "^9.1.5",
|
||||
"@storybook/addon-onboarding": "^9.1.5",
|
||||
"@storybook/react-webpack5": "^9.1.5",
|
||||
"@storybook/testing-library": "^0.2.0",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
|
|
@ -180,7 +188,7 @@
|
|||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"esbuild": "^0.17.8",
|
||||
"esbuild": "0.25.9",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-import-resolver-webpack": "^0.13.2",
|
||||
|
|
@ -189,7 +197,7 @@
|
|||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-storybook": "^0.6.13",
|
||||
"eslint-plugin-storybook": "^9.1.5",
|
||||
"html-loader": "^4.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"jest": "^29.4.2",
|
||||
|
|
@ -198,15 +206,16 @@
|
|||
"postcss-loader": "^8.1.0",
|
||||
"prettier": "^2.8.4",
|
||||
"react-refresh": "^0.17.0",
|
||||
"sass": "^1.78.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
"storybook": "^7.2.1",
|
||||
"sass": "^1.93.2",
|
||||
"sass-loader": "^16.0.5",
|
||||
"storybook": "^9.1.5",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "4.11.1"
|
||||
"webpack-dev-server": "^5.2.2"
|
||||
},
|
||||
"overrides": {
|
||||
"react-dates": {
|
||||
|
|
@ -236,11 +245,31 @@
|
|||
"react-image-annotation": {
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom"
|
||||
}
|
||||
},
|
||||
"react-mentions": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
"jspdf": {
|
||||
"@babel/runtime": ">=7.26.10",
|
||||
"dompurify": "^3.2.5"
|
||||
},
|
||||
"refractor": {
|
||||
"prismjs": "1.30.0"
|
||||
},
|
||||
"isomorphic-fetch@2.x": {
|
||||
"node-fetch": "^2.6.13"
|
||||
},
|
||||
"esbuild": "0.25.9",
|
||||
"on-headers": "1.1.0",
|
||||
"tar-fs": "^3.1.0",
|
||||
"brace-expansion": ">=2.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack serve --hot --port 8082 --host 0.0.0.0",
|
||||
"build": "webpack --mode=production && cp -a ./assets/. ./build/assets/",
|
||||
"analyze": "ANALYZE=true webpack --mode=production",
|
||||
"analyze:dev": "ANALYZE=true webpack --mode=development",
|
||||
"analyze:stats": "webpack --mode=production --json > bundle-stats.json && webpack-bundle-analyzer bundle-stats.json",
|
||||
"lint": "eslint . '**/*.{js,jsx}'",
|
||||
"format": "eslint . --fix '**/*.{js,jsx}'",
|
||||
"test": "jest",
|
||||
|
|
|
|||
|
|
@ -43,8 +43,14 @@ import { shallow } from 'zustand/shallow';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { checkIfToolJetCloud } from '@/_helpers/utils';
|
||||
import { BasicPlanMigrationBanner } from '@/HomePage/BasicPlanMigrationBanner/BasicPlanMigrationBanner';
|
||||
import BlankHomePage from '@/HomePage/BlankHomePage.jsx';
|
||||
import EmbedApp from '@/AppBuilder/EmbedApp';
|
||||
import withAdminOrBuilderOnly from '@/GetStarted/withAdminOrBuilderOnly';
|
||||
import posthogHelper from '@/modules/common/helpers/posthogHelper';
|
||||
import hubspotHelper from '@/modules/common/helpers/hubspotHelper';
|
||||
import DesktopOnlyRoute from '@/Routes/DesktopOnlyRoute';
|
||||
|
||||
const GuardedHomePage = withAdminOrBuilderOnly(BlankHomePage);
|
||||
|
||||
const AppWrapper = (props) => {
|
||||
const { isAppDarkMode } = useAppDarkMode();
|
||||
|
|
@ -77,7 +83,7 @@ class AppComponent extends React.Component {
|
|||
currentUser: null,
|
||||
fetchedMetadata: false,
|
||||
darkMode: localStorage.getItem('darkMode') === 'true',
|
||||
showBanner: false,
|
||||
showBanner: false, //Show banner
|
||||
// isEditorOrViewer: '',
|
||||
};
|
||||
}
|
||||
|
|
@ -86,10 +92,8 @@ class AppComponent extends React.Component {
|
|||
};
|
||||
updateMargin() {
|
||||
const isAdmin = authenticationService?.currentSessionValue?.admin;
|
||||
const isBuilder = authenticationService?.currentSessionValue?.is_builder;
|
||||
const setupDate = authenticationService?.currentSessionValue?.consultation_banner_date;
|
||||
const showBannerCondition =
|
||||
(isAdmin || isBuilder) && setupDate && this.isExistingPlanUser(setupDate) && this.state.showBanner;
|
||||
const isBuilder = authenticationService?.currentSessionValue?.role?.name === 'builder';
|
||||
const showBannerCondition = (isAdmin || isBuilder) && this.state.showBanner;
|
||||
const marginValue = showBannerCondition ? '25' : '0';
|
||||
const marginValueLayout = showBannerCondition ? '35' : '0';
|
||||
document.documentElement.style.setProperty('--dynamic-margin', `${marginValue}px`);
|
||||
|
|
@ -117,13 +121,17 @@ class AppComponent extends React.Component {
|
|||
async componentDidMount() {
|
||||
setFaviconAndTitle();
|
||||
authorizeWorkspace();
|
||||
hubspotHelper.loadHubspot();
|
||||
this.fetchMetadata();
|
||||
// check if version is cloud or ee
|
||||
const data = localStorage.getItem('currentVersion');
|
||||
if (data && data.includes('cloud')) {
|
||||
this.setState({
|
||||
showBanner: false, // show banner when required for ee or cloud
|
||||
});
|
||||
}
|
||||
setInterval(this.fetchMetadata, 1000 * 60 * 60 * 1);
|
||||
this.updateMargin(); // Set initial margin
|
||||
const featureAccess = await licenseService.getFeatureAccess();
|
||||
const isBasicPlan = !featureAccess?.licenseStatus?.isLicenseValid || featureAccess?.licenseStatus?.isExpired;
|
||||
this.setState({ showBanner: isBasicPlan });
|
||||
this.updateColorScheme();
|
||||
let counter = 0;
|
||||
let interval;
|
||||
|
||||
|
|
@ -160,15 +168,11 @@ class AppComponent extends React.Component {
|
|||
// Update margin when showBanner changes
|
||||
this.updateMargin();
|
||||
// Update color scheme if darkMode changed
|
||||
if (prevProps.darkMode !== this.props.darkMode) {
|
||||
this.updateColorScheme();
|
||||
}
|
||||
}
|
||||
|
||||
switchDarkMode = (newMode) => {
|
||||
this.props.updateIsTJDarkMode(newMode);
|
||||
localStorage.setItem('darkMode', newMode);
|
||||
this.updateColorScheme(newMode);
|
||||
};
|
||||
|
||||
isEditorOrViewerFromPath = () => {
|
||||
|
|
@ -184,17 +188,6 @@ class AppComponent extends React.Component {
|
|||
closeBasicPlanMigrationBanner = () => {
|
||||
this.setState({ showBanner: false });
|
||||
};
|
||||
isExistingPlanUser = (date) => {
|
||||
return new Date(date) < new Date('2025-04-24'); //show banner if user created before 2 april (24 for testing)
|
||||
};
|
||||
updateColorScheme = (darkModeValue) => {
|
||||
const isDark = darkModeValue !== undefined ? darkModeValue : this.props.darkMode;
|
||||
if (isDark) {
|
||||
document.documentElement.style.setProperty('color-scheme', 'dark');
|
||||
} else {
|
||||
document.documentElement.style.removeProperty('color-scheme');
|
||||
}
|
||||
};
|
||||
render() {
|
||||
const { updateAvailable, isEditorOrViewer, showBanner } = this.state;
|
||||
const { darkMode } = this.props;
|
||||
|
|
@ -224,18 +217,13 @@ class AppComponent extends React.Component {
|
|||
const { updateSidebarNAV } = this;
|
||||
const isApplicationsPath = window.location.pathname.includes('/applications/');
|
||||
const isAdmin = authenticationService?.currentSessionValue?.admin;
|
||||
const isBuilder = authenticationService?.currentSessionValue?.is_builder;
|
||||
const setupDate = authenticationService?.currentSessionValue?.consultation_banner_date;
|
||||
const isBuilder = authenticationService?.currentSessionValue?.role?.name === 'builder';
|
||||
return (
|
||||
<>
|
||||
<div className={!isApplicationsPath && (isAdmin || isBuilder) ? 'banner-layout-wrapper' : ''}>
|
||||
{!isApplicationsPath &&
|
||||
(isAdmin || isBuilder) &&
|
||||
showBanner &&
|
||||
setupDate &&
|
||||
this.isExistingPlanUser(setupDate) && (
|
||||
<BasicPlanMigrationBanner darkMode={darkMode} closeBanner={this.closeBasicPlanMigrationBanner} />
|
||||
)}
|
||||
{!isApplicationsPath && !this.isEditorOrViewerFromPath() && (isAdmin || isBuilder) && showBanner && (
|
||||
<BasicPlanMigrationBanner darkMode={darkMode} closeBanner={this.closeBasicPlanMigrationBanner} />
|
||||
)}
|
||||
<div
|
||||
className={cx('main-wrapper', {
|
||||
'theme-dark dark-theme': !this.isEditorOrViewerFromPath() && darkMode,
|
||||
|
|
@ -269,15 +257,15 @@ class AppComponent extends React.Component {
|
|||
)}
|
||||
<BreadCrumbContext.Provider value={{ sidebarNav, updateSidebarNAV }}>
|
||||
<Routes>
|
||||
{onboarding(this.props)}
|
||||
{auth(this.props)}
|
||||
{onboarding({ ...this.props, darkMode })}
|
||||
{auth({ ...this.props, darkMode })}
|
||||
<Route path="/sso/:origin/:configId" exact element={<Oauth {...this.props} />} />
|
||||
<Route path="/sso/:origin" exact element={<Oauth {...this.props} />} />
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/apps/:slug/:pageHandle?/*"
|
||||
element={
|
||||
<AppsRoute componentType="editor">
|
||||
<AppsRoute componentType="editor" darkMode={darkMode}>
|
||||
<AppLoader switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</AppsRoute>
|
||||
}
|
||||
|
|
@ -286,9 +274,11 @@ class AppComponent extends React.Component {
|
|||
exact
|
||||
path="/:workspaceId/workspace-constants"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<WorkspaceConstants switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
<DesktopOnlyRoute darkMode={darkMode}>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<WorkspaceConstants switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
|
|
@ -313,7 +303,7 @@ class AppComponent extends React.Component {
|
|||
exact
|
||||
path="/oauth2/authorize"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<Authorize switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
|
|
@ -323,51 +313,74 @@ class AppComponent extends React.Component {
|
|||
exact
|
||||
path="/:workspaceId/workflows/*"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Workflows switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
<DesktopOnlyRoute darkMode={darkMode}>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<Workflows switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...mergedProps} />} />
|
||||
<Route
|
||||
path="/:workspaceId/workspace-settings/*"
|
||||
element={
|
||||
<DesktopOnlyRoute darkMode={darkMode}>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<WorkspaceSettings {...mergedProps} />
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/*"
|
||||
element={
|
||||
<InstanceSettings switchDarkMode={this.switchDarkMode} darkMode={darkMode} {...this.props} />
|
||||
<DesktopOnlyRoute darkMode={darkMode}>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<InstanceSettings switchDarkMode={this.switchDarkMode} darkMode={darkMode} {...this.props} />
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/:workspaceId/settings/*"
|
||||
element={
|
||||
<InstanceSettings {...this.props} darkMode={darkMode} switchDarkMode={this.switchDarkMode} />
|
||||
<DesktopOnlyRoute darkMode={darkMode}>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<InstanceSettings {...this.props} darkMode={darkMode} switchDarkMode={this.switchDarkMode} />
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/modules"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<HomePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} appType={'module'} />
|
||||
</PrivateRoute>
|
||||
<DesktopOnlyRoute darkMode={darkMode}>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<HomePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} appType={'module'} />
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{getAuditLogsRoutes(mergedProps)}
|
||||
{getAuditLogsRoutes({ ...mergedProps, darkMode })}
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/profile-settings"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<SettingsPage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
<DesktopOnlyRoute darkMode={darkMode}>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<SettingsPage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
{getDataSourcesRoutes(mergedProps)}
|
||||
{getDataSourcesRoutes({ ...mergedProps, darkMode })}
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:id/versions/:versionId/:pageHandle?"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<Viewer switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
|
|
@ -381,14 +394,31 @@ class AppComponent extends React.Component {
|
|||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/home"
|
||||
element={
|
||||
<DesktopOnlyRoute>
|
||||
<PrivateRoute>
|
||||
<GuardedHomePage
|
||||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
version={this.state.tooljetVersion}
|
||||
/>
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/database"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<TooljetDatabase switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
<DesktopOnlyRoute darkMode={darkMode}>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<TooljetDatabase switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
@ -397,17 +427,25 @@ class AppComponent extends React.Component {
|
|||
exact
|
||||
path="/integrations"
|
||||
element={
|
||||
<AdminRoute {...this.props}>
|
||||
<AdminRoute {...this.props} darkMode={darkMode}>
|
||||
<MarketplacePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</AdminRoute>
|
||||
}
|
||||
>
|
||||
<Route path="installed" element={<InstalledPlugins />} />
|
||||
<Route path="marketplace" element={<MarketplacePlugins />} />/
|
||||
<Route path="marketplace" element={<DesktopOnlyRoute>{<MarketplacePlugins />}</DesktopOnlyRoute>} />
|
||||
</Route>
|
||||
)}
|
||||
|
||||
<Route exact path="/" element={<Navigate to="/:workspaceId" />} />
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
element={
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<Navigate to="/:workspaceId" />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/error/:errorType"
|
||||
|
|
@ -429,7 +467,7 @@ class AppComponent extends React.Component {
|
|||
exact
|
||||
path="/switch-workspace"
|
||||
element={
|
||||
<SwitchWorkspaceRoute>
|
||||
<SwitchWorkspaceRoute darkMode={darkMode}>
|
||||
<SwitchWorkspacePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</SwitchWorkspaceRoute>
|
||||
}
|
||||
|
|
@ -438,7 +476,7 @@ class AppComponent extends React.Component {
|
|||
exact
|
||||
path="/switch-workspace-archived"
|
||||
element={
|
||||
<SwitchWorkspaceRoute>
|
||||
<SwitchWorkspaceRoute darkMode={darkMode}>
|
||||
<SwitchWorkspacePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} archived={true} />
|
||||
</SwitchWorkspaceRoute>
|
||||
}
|
||||
|
|
@ -447,11 +485,26 @@ class AppComponent extends React.Component {
|
|||
exact
|
||||
path="/:workspaceId"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<PrivateRoute darkMode={darkMode}>
|
||||
<HomePage switchDarkMode={this.switchDarkMode} darkMode={darkMode} appType={'front-end'} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/home"
|
||||
element={
|
||||
<DesktopOnlyRoute>
|
||||
<PrivateRoute>
|
||||
<GuardedHomePage
|
||||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
version={this.state.tooljetVersion}
|
||||
/>
|
||||
</PrivateRoute>
|
||||
</DesktopOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/embed-apps/:appId" element={<EmbedApp />} />
|
||||
<Route
|
||||
path="*"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Suspense } from 'react';
|
||||
import React, { Suspense, useEffect } from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import useAppData from '@/AppBuilder/_hooks/useAppData';
|
||||
import { TJLoader } from '@/_ui/TJLoader/TJLoader';
|
||||
|
|
@ -16,6 +16,7 @@ import Popups from './Popups';
|
|||
import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import RightSidebarToggle from '@/AppBuilder/RightSideBar/RightSidebarToggle';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import ArtifactPreview from './ArtifactPreview';
|
||||
|
||||
|
|
@ -30,10 +31,12 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
|
|||
useAppData(appId, moduleId, darkMode);
|
||||
const isEditorLoading = useStore((state) => state.loaderStore.modules[moduleId].isEditorLoading, shallow);
|
||||
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
|
||||
const hasModuleAccess = useStore((state) => state.license.featureAccess?.modulesEnabled);
|
||||
const isModuleEditor = appType === 'module';
|
||||
|
||||
const updateIsTJDarkMode = useStore((state) => state.updateIsTJDarkMode, shallow);
|
||||
const appBuilderMode = useStore((state) => state.appStore.modules[moduleId]?.app?.appBuilderMode ?? 'visual');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isUserInZeroToOneFlow = appBuilderMode === 'ai';
|
||||
|
||||
|
|
@ -42,6 +45,12 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
|
|||
switchDarkMode(newMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasModuleAccess === false && isModuleEditor) {
|
||||
navigate('/error/restricted');
|
||||
}
|
||||
}, [hasModuleAccess, isModuleEditor]);
|
||||
|
||||
//TODO: This can be added to the mode slice and set based on the mode
|
||||
if (isEditorLoading) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -8,13 +8,7 @@ import './appCanvas.scss';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { computeViewerBackgroundColor, getCanvasWidth } from './appCanvasUtils';
|
||||
import {
|
||||
LEFT_SIDEBAR_WIDTH,
|
||||
NO_OF_GRIDS,
|
||||
PAGES_SIDEBAR_WIDTH_COLLAPSED,
|
||||
PAGES_SIDEBAR_WIDTH_EXPANDED,
|
||||
RIGHT_SIDEBAR_WIDTH,
|
||||
} from './appCanvasConstants';
|
||||
import { NO_OF_GRIDS, PAGES_SIDEBAR_WIDTH_COLLAPSED, PAGES_SIDEBAR_WIDTH_EXPANDED } from './appCanvasConstants';
|
||||
import cx from 'classnames';
|
||||
import { computeCanvasContainerHeight } from '../_helpers/editorHelpers';
|
||||
import AutoComputeMobileLayoutAlert from './AutoComputeMobileLayoutAlert';
|
||||
|
|
@ -23,15 +17,18 @@ import useAppCanvasMaxWidth from './useAppCanvasMaxWidth';
|
|||
import { DeleteWidgetConfirmation } from './DeleteWidgetConfirmation';
|
||||
import useSidebarMargin from './useSidebarMargin';
|
||||
import PagesSidebarNavigation from '../RightSideBar/PageSettingsTab/PageMenu/PagesSidebarNavigation';
|
||||
import { DragGhostWidget, ResizeGhostWidget } from './GhostWidgets';
|
||||
import { DragResizeGhostWidget } from './GhostWidgets';
|
||||
import AppCanvasBanner from '../../AppBuilder/Header/AppCanvasBanner';
|
||||
import { debounce } from 'lodash';
|
||||
import useCanvasMinWidth from './useCanvasMinWidth';
|
||||
import useEnableMainCanvasScroll from './useEnableMainCanvasScroll';
|
||||
|
||||
export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
|
||||
const { moduleId, isModuleMode, appType } = useModuleContext();
|
||||
const canvasContainerRef = useRef();
|
||||
const canvasContentRef = useRef(null);
|
||||
const isScrolling = useEnableMainCanvasScroll({ canvasContentRef });
|
||||
const handleCanvasContainerMouseUp = useStore((state) => state.handleCanvasContainerMouseUp, shallow);
|
||||
const resolveReferences = useStore((state) => state.resolveReferences);
|
||||
const canvasHeight = useStore((state) => state.appStore.modules[moduleId].canvasHeight);
|
||||
const environmentLoadingState = useStore(
|
||||
(state) => state.environmentLoadingState || state.loaderStore.modules[moduleId].isEditorLoading,
|
||||
|
|
@ -52,12 +49,12 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
|
|||
const editorMarginLeft = useSidebarMargin(canvasContainerRef);
|
||||
const getPageId = useStore((state) => state.getCurrentPageId, shallow);
|
||||
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
|
||||
const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
|
||||
const currentPageId = useStore((state) => state.modules[moduleId].currentPageId);
|
||||
const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId);
|
||||
|
||||
const [isViewerSidebarPinned, setIsSidebarPinned] = useState(
|
||||
localStorage.getItem('isPagesSidebarPinned') !== 'false'
|
||||
localStorage.getItem('isPagesSidebarPinned') === null
|
||||
? false
|
||||
: localStorage.getItem('isPagesSidebarPinned') !== 'false'
|
||||
);
|
||||
|
||||
const { globalSettings, pageSettings, switchPage } = useStore(
|
||||
|
|
@ -68,28 +65,29 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
|
|||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
const showHeader = !globalSettings?.hideHeader;
|
||||
const { definition: { properties = {} } = {} } = pageSettings ?? {};
|
||||
const { position, disableMenu, showOnDesktop } = properties ?? {};
|
||||
const { position } = properties ?? {};
|
||||
const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility(moduleId), shallow);
|
||||
const minCanvasWidth = useCanvasMinWidth({ currentMode, position, isModuleMode, isViewerSidebarPinned });
|
||||
const [isCurrentVersionLocked, setIsCurrentVersionLocked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Need to remove this if we shift setExposedVariable Logic outside of components
|
||||
// Currently present to run onLoadQueries after the component is mounted
|
||||
setIsComponentLayoutReady(true, moduleId);
|
||||
return () => setIsComponentLayoutReady(false, moduleId);
|
||||
}, []);
|
||||
}, [moduleId, setIsComponentLayoutReady]);
|
||||
|
||||
const handleResizeImmediate = useCallback(() => {
|
||||
const _canvasWidth =
|
||||
moduleId === 'canvas'
|
||||
? document.getElementById('real-canvas')?.getBoundingClientRect()?.width
|
||||
: document.getElementById(moduleId)?.getBoundingClientRect()?.width;
|
||||
if (_canvasWidth !== 0) setCanvasWidth(_canvasWidth);
|
||||
}, [moduleId]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleResizeImmediate() {
|
||||
const _canvasWidth =
|
||||
moduleId === 'canvas'
|
||||
? document.getElementById('real-canvas')?.getBoundingClientRect()?.width
|
||||
: document.getElementById(moduleId)?.getBoundingClientRect()?.width;
|
||||
if (_canvasWidth !== 0) setCanvasWidth(_canvasWidth);
|
||||
}
|
||||
|
||||
const handleResize = debounce(handleResizeImmediate, 300);
|
||||
|
||||
if (moduleId === 'canvas') {
|
||||
|
|
@ -111,7 +109,20 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
|
|||
window.removeEventListener('resize', handleResize);
|
||||
handleResize.cancel();
|
||||
};
|
||||
}, [currentLayout, canvasMaxWidth, isViewerSidebarPinned, moduleId, isRightSidebarOpen]);
|
||||
}, [handleResizeImmediate, currentLayout, canvasMaxWidth, moduleId, isRightSidebarOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (moduleId === 'canvas') {
|
||||
const _canvasWidth =
|
||||
document.querySelector('.canvas-container.page-container')?.getBoundingClientRect()?.width -
|
||||
(isViewerSidebarPinned ? PAGES_SIDEBAR_WIDTH_EXPANDED : PAGES_SIDEBAR_WIDTH_COLLAPSED) -
|
||||
16; // padding of 'div.canvas-container.page-container' container
|
||||
if (_canvasWidth !== 0) setCanvasWidth(_canvasWidth);
|
||||
}
|
||||
|
||||
localStorage.setItem('isPagesSidebarPinned', isViewerSidebarPinned);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isViewerSidebarPinned]);
|
||||
|
||||
const canvasContainerStyles = useMemo(() => {
|
||||
const canvasBgColor =
|
||||
|
|
@ -138,137 +149,116 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
|
|||
justifyContent: 'unset',
|
||||
borderRight: currentMode === 'edit' && isRightSidebarOpen && `300px solid ${canvasBgColor}`,
|
||||
padding: currentMode === 'edit' && '8px',
|
||||
paddingTop: currentMode === 'edit' && (isCurrentVersionLocked ? '38px' : '8px'),
|
||||
paddingBottom: currentMode === 'edit' && '2px',
|
||||
};
|
||||
}, [currentMode, isAppDarkMode, isModuleMode, editorMarginLeft, canvasContainerHeight, isRightSidebarOpen]);
|
||||
|
||||
const toggleSidebarPinned = useCallback(() => {
|
||||
const newValue = !isViewerSidebarPinned;
|
||||
setIsSidebarPinned(newValue);
|
||||
localStorage.setItem('isPagesSidebarPinned', JSON.stringify(newValue));
|
||||
}, [isViewerSidebarPinned]);
|
||||
|
||||
function getMinWidth() {
|
||||
if (isModuleMode) return '100%';
|
||||
|
||||
const isSidebarOpenInEditor = currentMode === 'edit' ? isSidebarOpen : false;
|
||||
|
||||
const shouldAdjust = isSidebarOpen || (isRightSidebarOpen && currentMode === 'edit');
|
||||
|
||||
if (!shouldAdjust) return '';
|
||||
let offset;
|
||||
if (isViewerSidebarPinned && !isPagesSidebarHidden) {
|
||||
if (position === 'side' && isSidebarOpenInEditor && isRightSidebarOpen && !isPagesSidebarHidden) {
|
||||
offset = `${LEFT_SIDEBAR_WIDTH + RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`;
|
||||
} else if (position === 'side' && isSidebarOpenInEditor && !isRightSidebarOpen && !isPagesSidebarHidden) {
|
||||
offset = `${LEFT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`;
|
||||
} else if (position === 'side' && isRightSidebarOpen && !isSidebarOpenInEditor && !isPagesSidebarHidden) {
|
||||
offset = `${RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`;
|
||||
}
|
||||
} else {
|
||||
if (position === 'side' && isSidebarOpenInEditor && isRightSidebarOpen && !isPagesSidebarHidden) {
|
||||
offset = `${LEFT_SIDEBAR_WIDTH + RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`;
|
||||
} else if (position === 'side' && isSidebarOpenInEditor && !isRightSidebarOpen && !isPagesSidebarHidden) {
|
||||
offset = `${LEFT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`;
|
||||
} else if (position === 'side' && isRightSidebarOpen && !isSidebarOpenInEditor && !isPagesSidebarHidden) {
|
||||
offset = `${RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentMode === 'edit') {
|
||||
if ((position === 'top' || isPagesSidebarHidden) && isSidebarOpenInEditor && isRightSidebarOpen) {
|
||||
offset = `${LEFT_SIDEBAR_WIDTH + RIGHT_SIDEBAR_WIDTH}px`;
|
||||
} else if ((position === 'top' || isPagesSidebarHidden) && isSidebarOpenInEditor && !isRightSidebarOpen) {
|
||||
offset = `${LEFT_SIDEBAR_WIDTH}px`;
|
||||
} else if ((position === 'top' || isPagesSidebarHidden) && isRightSidebarOpen && !isSidebarOpenInEditor) {
|
||||
offset = `${RIGHT_SIDEBAR_WIDTH}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return `calc(100% + ${offset})`;
|
||||
}
|
||||
}, [
|
||||
currentMode,
|
||||
isAppDarkMode,
|
||||
isModuleMode,
|
||||
editorMarginLeft,
|
||||
canvasContainerHeight,
|
||||
isRightSidebarOpen,
|
||||
isCurrentVersionLocked,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(`main main-editor-canvas position-relative`, {})}
|
||||
id="main-editor-canvas"
|
||||
onMouseUp={handleCanvasContainerMouseUp}
|
||||
>
|
||||
<AppCanvasBanner appId={appId} />
|
||||
<div id="sidebar-page-navigation" className="areas d-flex flex-rows">
|
||||
<div
|
||||
ref={canvasContainerRef}
|
||||
className={cx(
|
||||
'canvas-container d-flex page-container',
|
||||
{ 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned },
|
||||
{ 'overflow-x-auto': currentMode === 'edit' },
|
||||
{ 'position-top': position === 'top' || isPagesSidebarHidden },
|
||||
{ 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode
|
||||
)}
|
||||
style={canvasContainerStyles}
|
||||
>
|
||||
{appType !== 'module' && (
|
||||
<PagesSidebarNavigation
|
||||
showHeader={showHeader}
|
||||
isMobileDevice={currentLayout === 'mobile'}
|
||||
currentPageId={currentPageId ?? homePageId}
|
||||
switchPage={switchPage}
|
||||
height={currentMode === 'edit' ? canvasContainerHeight : '100%'}
|
||||
switchDarkMode={switchDarkMode}
|
||||
isSidebarPinned={isViewerSidebarPinned}
|
||||
toggleSidebarPinned={toggleSidebarPinned}
|
||||
darkMode={darkMode}
|
||||
canvasMaxWidth={canvasMaxWidth}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<div
|
||||
className={cx(`main main-editor-canvas position-relative`, {})}
|
||||
id="main-editor-canvas"
|
||||
onMouseUp={handleCanvasContainerMouseUp}
|
||||
>
|
||||
<div id="sidebar-page-navigation" className="areas d-flex flex-rows">
|
||||
<div
|
||||
style={{
|
||||
minWidth: getMinWidth(),
|
||||
scrollbarWidth: 'none',
|
||||
overflow: 'auto',
|
||||
width: currentMode === 'view' ? `calc(100% - ${isViewerSidebarPinned ? '0px' : '0px'})` : '100%',
|
||||
...(appType === 'module' && isModuleMode && { height: 'inherit' }),
|
||||
}}
|
||||
className={`app-${appId} _tooljet-page-${getPageId()} canvas-content`}
|
||||
ref={canvasContainerRef}
|
||||
className={cx(
|
||||
'canvas-container d-flex page-container',
|
||||
{ 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned },
|
||||
{ 'overflow-x-auto': currentMode === 'edit' },
|
||||
{ 'position-top': position === 'top' || isPagesSidebarHidden },
|
||||
{ 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode
|
||||
)}
|
||||
style={canvasContainerStyles}
|
||||
>
|
||||
{currentMode === 'edit' && (
|
||||
<AutoComputeMobileLayoutAlert currentLayout={currentLayout} darkMode={isAppDarkMode} />
|
||||
<AppCanvasBanner
|
||||
appId={appId}
|
||||
onVersionLockStatusChange={(isLocked) => {
|
||||
setIsCurrentVersionLocked(isLocked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DeleteWidgetConfirmation darkMode={isAppDarkMode} />
|
||||
<HotkeyProvider mode={currentMode} canvasMaxWidth={canvasMaxWidth} currentLayout={currentLayout}>
|
||||
{environmentLoadingState !== 'loading' && (
|
||||
<div>
|
||||
<Container
|
||||
id={moduleId}
|
||||
gridWidth={gridWidth}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
darkMode={isAppDarkMode}
|
||||
canvasMaxWidth={canvasMaxWidth}
|
||||
isViewerSidebarPinned={isViewerSidebarPinned}
|
||||
pageSidebarStyle={pageSidebarStyle}
|
||||
pagePositionType={position}
|
||||
appType={appType}
|
||||
/>
|
||||
{currentMode === 'edit' && (
|
||||
<>
|
||||
<DragGhostWidget />
|
||||
<ResizeGhostWidget />
|
||||
</>
|
||||
)}
|
||||
<div id="component-portal" />
|
||||
{appType !== 'module' && <div id="component-portal" />}
|
||||
</div>
|
||||
{appType !== 'module' && (
|
||||
<PagesSidebarNavigation
|
||||
showHeader={showHeader}
|
||||
isMobileDevice={currentLayout === 'mobile'}
|
||||
currentPageId={currentPageId ?? homePageId}
|
||||
switchPage={switchPage}
|
||||
height={currentMode === 'edit' ? canvasContainerHeight : '100%'}
|
||||
switchDarkMode={switchDarkMode}
|
||||
isSidebarPinned={isViewerSidebarPinned}
|
||||
setIsSidebarPinned={setIsSidebarPinned}
|
||||
darkMode={darkMode}
|
||||
canvasMaxWidth={canvasMaxWidth}
|
||||
canvasContentRef={canvasContentRef}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={canvasContentRef}
|
||||
style={{
|
||||
minWidth: minCanvasWidth,
|
||||
overflow: currentMode === 'view' ? 'auto' : 'hidden auto',
|
||||
width: currentMode === 'view' ? `calc(100% - ${isViewerSidebarPinned ? '0px' : '0px'})` : '100%',
|
||||
...(appType === 'module' && isModuleMode && { height: 'inherit' }),
|
||||
}}
|
||||
className={cx(`app-${appId} _tooljet-page-${getPageId()} canvas-content`, {
|
||||
'scrollbar-hidden': !isScrolling,
|
||||
})}
|
||||
>
|
||||
{currentMode === 'edit' && (
|
||||
<AutoComputeMobileLayoutAlert currentLayout={currentLayout} darkMode={isAppDarkMode} />
|
||||
)}
|
||||
<DeleteWidgetConfirmation darkMode={isAppDarkMode} />
|
||||
<HotkeyProvider
|
||||
mode={currentMode}
|
||||
canvasMaxWidth={canvasMaxWidth}
|
||||
currentLayout={currentLayout}
|
||||
isModuleMode={isModuleMode}
|
||||
>
|
||||
{environmentLoadingState !== 'loading' && (
|
||||
<div className={cx({ 'h-100': isModuleMode })}>
|
||||
<Container
|
||||
id={moduleId}
|
||||
gridWidth={gridWidth}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
darkMode={isAppDarkMode}
|
||||
canvasMaxWidth={canvasMaxWidth}
|
||||
isViewerSidebarPinned={isViewerSidebarPinned}
|
||||
pageSidebarStyle={pageSidebarStyle}
|
||||
pagePositionType={position}
|
||||
appType={appType}
|
||||
/>
|
||||
{currentMode === 'edit' && (
|
||||
<>
|
||||
<DragResizeGhostWidget />
|
||||
</>
|
||||
)}
|
||||
<div id="component-portal" />
|
||||
{appType !== 'module' && <div id="component-portal" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentMode === 'view' || (currentLayout === 'mobile' && isAutoMobileLayout) ? null : (
|
||||
<Grid currentLayout={currentLayout} gridWidth={gridWidth} />
|
||||
)}
|
||||
</HotkeyProvider>
|
||||
{currentMode === 'view' || (currentLayout === 'mobile' && isAutoMobileLayout) ? null : (
|
||||
<Grid currentLayout={currentLayout} gridWidth={gridWidth} mainCanvasWidth={canvasWidth} />
|
||||
)}
|
||||
</HotkeyProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentMode === 'edit' && <EditorSelecto />}
|
||||
</div>
|
||||
{currentMode === 'edit' && <EditorSelecto />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import './configHandle.scss';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
|
@ -9,12 +9,19 @@ import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
|||
import { DROPPABLE_PARENTS } from '../appCanvasConstants';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants';
|
||||
import MentionComponentInChat from './MentionComponentInChat';
|
||||
import ConfigHandleButton from '../../../_components/ConfigHandleButton';
|
||||
import { SquareDashedMousePointer, PencilRuler, Lock, VectorSquare, EyeClosed, Trash } from 'lucide-react';
|
||||
import Popover from '@/_ui/Popover';
|
||||
import DynamicHeightInfo from '@assets/images/dynamic-height-info.svg';
|
||||
import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx';
|
||||
|
||||
const CONFIG_HANDLE_HEIGHT = 20;
|
||||
const BUFFER_HEIGHT = 1;
|
||||
|
||||
export const ConfigHandle = ({
|
||||
id,
|
||||
readOnly,
|
||||
widgetTop,
|
||||
widgetHeight,
|
||||
setSelectedComponentAsModal = () => null, //! Only Modal widget passes this uses props down. All other widgets use selecto lib
|
||||
|
|
@ -25,21 +32,28 @@ export const ConfigHandle = ({
|
|||
visibility,
|
||||
isModuleContainer,
|
||||
subContainerIndex,
|
||||
isDynamicHeightEnabled,
|
||||
}) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const isLicenseValid = useStore((state) => state.isLicenseValid(), shallow);
|
||||
const isModulesEnabled = useStore((state) => state.license.featureAccess?.modulesEnabled, shallow);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const componentName = useStore((state) => state.getComponentDefinition(id, moduleId)?.component?.name || '', shallow);
|
||||
const isMultipleComponentsSelected = useStore(
|
||||
(state) => (findHighestLevelofSelection(state?.selectedComponents)?.length > 1 ? true : false),
|
||||
shallow
|
||||
);
|
||||
const deleteComponents = useStore((state) => state.deleteComponents, shallow);
|
||||
const getSelectedComponents = useStore((state) => state.getSelectedComponents, shallow);
|
||||
const setWidgetDeleteConfirmation = useStore((state) => state.setWidgetDeleteConfirmation, shallow);
|
||||
const setFocusedParentId = useStore((state) => state.setFocusedParentId, shallow);
|
||||
const currentTab = useStore(
|
||||
(state) => componentType === 'Tabs' && state.getExposedValueOfComponent(id)?.currentTab,
|
||||
shallow
|
||||
);
|
||||
const [hideDynamicHeightInfo, setHideDynamicHeightInfo] = useState(
|
||||
localStorage.getItem('hideDynamicHeightInfo') === 'true'
|
||||
);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const timeoutRef = useRef(null);
|
||||
const position = widgetTop < 15 ? 'bottom' : 'top';
|
||||
|
||||
const setComponentToInspect = useStore((state) => state.setComponentToInspect);
|
||||
|
|
@ -49,10 +63,9 @@ export const ConfigHandle = ({
|
|||
const anyComponentHovered = state.getHoveredComponentForGrid() !== '' || state.hoveredComponentBoundaryId !== '';
|
||||
// If one component is hovered and one is selected, show the handle for the hovered component
|
||||
return (
|
||||
(subContainerIndex === 0 || subContainerIndex === null) &&
|
||||
(isWidgetHovered ||
|
||||
isModuleContainer ||
|
||||
(showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) && !anyComponentHovered))
|
||||
((subContainerIndex === 0 || subContainerIndex === null) && (isModuleContainer || (isModal && isModalOpen))) ||
|
||||
isWidgetHovered ||
|
||||
(showHandle && !isMultipleComponentsSelected && !anyComponentHovered)
|
||||
);
|
||||
}, shallow);
|
||||
|
||||
|
|
@ -67,12 +80,19 @@ export const ConfigHandle = ({
|
|||
|
||||
let height = visibility === false ? 10 : widgetHeight;
|
||||
|
||||
const deleteComponents = () => {
|
||||
const selectedComponents = getSelectedComponents();
|
||||
if (selectedComponents.length > 0) {
|
||||
setWidgetDeleteConfirmation(true);
|
||||
}
|
||||
};
|
||||
|
||||
const getTooltip = () => {
|
||||
const permission = component.permissions?.[0];
|
||||
if (!permission) return null;
|
||||
if (!permission) return 'Access restricted';
|
||||
|
||||
const users = permission.groups || permission.users || [];
|
||||
if (users.length === 0) return null;
|
||||
if (users.length === 0) return 'Access restricted';
|
||||
|
||||
const isSingle = permission.type === 'SINGLE';
|
||||
const isGroup = permission.type === 'GROUP';
|
||||
|
|
@ -89,9 +109,90 @@ export const ConfigHandle = ({
|
|||
: `Access restricted to ${users.length} user groups`;
|
||||
}
|
||||
|
||||
return null;
|
||||
return 'Access restricted';
|
||||
};
|
||||
|
||||
const isHiddenOrModalOpen = visibility === false || (componentType === 'Modal' && isModalOpen);
|
||||
const getConfigHandleButtonStyle = isHiddenOrModalOpen
|
||||
? {
|
||||
background: 'var(--interactive-selected)',
|
||||
color: 'var(--text-default)',
|
||||
padding: '2px 6px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
height: '24px',
|
||||
}
|
||||
: {
|
||||
color: 'var(--text-on-solid)',
|
||||
padding: '2px 6px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
height: '24px',
|
||||
};
|
||||
if (isDynamicHeightEnabled && !isHiddenOrModalOpen) {
|
||||
getConfigHandleButtonStyle.background = '#9747FF';
|
||||
}
|
||||
|
||||
const iconOnlyButtonStyle = {
|
||||
height: '24px',
|
||||
width: '24px',
|
||||
cursor: 'pointer',
|
||||
background: 'var(--background-surface-layer-01)',
|
||||
border: '1px solid var(--border-weak)',
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (hideDynamicHeightInfo) return;
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
setIsPopoverOpen(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (hideDynamicHeightInfo) return;
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setIsPopoverOpen(false);
|
||||
}, 50); // Small delay to allow moving mouse to popover
|
||||
};
|
||||
|
||||
const popoverContent = (
|
||||
<div className="dynamic-height-info-wrapper" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<div className="dynamic-height-info-image">
|
||||
<DynamicHeightInfo />
|
||||
</div>
|
||||
<div className="dynamic-height-info-body">
|
||||
<p className="dynamic-height-info-text-title">Dynamic Height enabled</p>
|
||||
<p className="dynamic-height-info-text-description">
|
||||
Your component expands based on content but won't shrink below the height you set on canvas.
|
||||
</p>
|
||||
</div>
|
||||
<div className="dynamic-height-info-button">
|
||||
<ButtonComponent
|
||||
size="medium"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
localStorage.setItem('hideDynamicHeightInfo', 'true');
|
||||
setIsPopoverOpen(false);
|
||||
setHideDynamicHeightInfo(true);
|
||||
}}
|
||||
>
|
||||
Never show this again
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (readOnly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`config-handle ${customClassName}`}
|
||||
|
|
@ -101,10 +202,13 @@ export const ConfigHandle = ({
|
|||
componentType === 'Modal' && isModalOpen
|
||||
? '0px'
|
||||
: position === 'top'
|
||||
? '-20px'
|
||||
: `${height - (CONFIG_HANDLE_HEIGHT + BUFFER_HEIGHT)}px`,
|
||||
? '-26px'
|
||||
: `${height - (CONFIG_HANDLE_HEIGHT + BUFFER_HEIGHT)}px`,
|
||||
visibility: _showHandle || visibility === false ? 'visible' : 'hidden',
|
||||
left: '-1px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '2px',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -120,108 +224,95 @@ export const ConfigHandle = ({
|
|||
data-tooltip-html="Your plan is expired. <br/> Renew to use the modules."
|
||||
data-tooltip-place="right"
|
||||
>
|
||||
{licenseValid && isRestricted && (
|
||||
<ToolTip message={getTooltip()} show={licenseValid && isRestricted && !draggingComponentId}>
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
visibility === false ? '#c6cad0' : componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
border: position === 'bottom' ? '1px solid white' : 'none',
|
||||
color: visibility === false && 'var(--text-placeholder)',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
className="badge handle-content"
|
||||
<ConfigHandleButton customStyles={getConfigHandleButtonStyle} className="no-hover component-name-btn">
|
||||
{isDynamicHeightEnabled && (
|
||||
<Popover
|
||||
open={isPopoverOpen}
|
||||
side="bottom-start"
|
||||
popoverContent={popoverContent}
|
||||
popoverContentClassName="dynamic-height-info-popover"
|
||||
>
|
||||
<SolidIcon width="12" name="lock" fill="var(--icon-on-solid)" />
|
||||
</span>
|
||||
</ToolTip>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
visibility === false ? '#c6cad0' : componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
border: position === 'bottom' ? '1px solid white' : 'none',
|
||||
color: visibility === false && 'var(--text-placeholder)',
|
||||
}}
|
||||
className="badge handle-content"
|
||||
>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedComponentAsModal(id);
|
||||
}}
|
||||
role="button"
|
||||
data-cy={`${componentName?.toLowerCase()}-config-handle`}
|
||||
className="text-truncate"
|
||||
>
|
||||
{/* Settings Icon */}
|
||||
<span
|
||||
onClick={() => {
|
||||
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
|
||||
setRightSidebarOpen(true);
|
||||
}}
|
||||
style={{ cursor: 'pointer', marginRight: '5px' }}
|
||||
>
|
||||
<SolidIcon
|
||||
name="propertiesstyles"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 16 16"
|
||||
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
|
||||
/>
|
||||
</span>
|
||||
<span>{componentName}</span>
|
||||
{/* Divider */}
|
||||
<hr
|
||||
style={{
|
||||
marginLeft: '10px',
|
||||
height: '12px',
|
||||
width: '2px',
|
||||
backgroundColor: visibility === false ? 'var(--text-placeholder)' : '#fff',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Delete Button */}
|
||||
{!isMultipleComponentsSelected && !shouldFreeze && (
|
||||
<div>
|
||||
<img
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
src="assets/images/icons/inspect.svg"
|
||||
width="12"
|
||||
role="button"
|
||||
height="12"
|
||||
draggable="false"
|
||||
onClick={() => setComponentToInspect(componentName)}
|
||||
data-cy={`${componentName.toLowerCase()}-inspect-button`}
|
||||
className="config-handle-inspect"
|
||||
/>
|
||||
{!isModuleContainer && (
|
||||
<span
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
onClick={() => {
|
||||
deleteComponents([id]);
|
||||
}}
|
||||
data-cy={`${componentName.toLowerCase()}-delete-button`}
|
||||
>
|
||||
<SolidIcon
|
||||
name="trash"
|
||||
width="12"
|
||||
height="12"
|
||||
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ToolTip message="Dynamic height enabled" show={hideDynamicHeightInfo} delay={{ show: 500, hide: 50 }}>
|
||||
<VectorSquare
|
||||
size={14}
|
||||
color={
|
||||
isDynamicHeightEnabled && !isHiddenOrModalOpen ? 'var(--icon-on-solid)' : 'var(--icon-default)'
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</ToolTip>
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
{!visibility && (
|
||||
<div>
|
||||
<EyeClosed
|
||||
size={14}
|
||||
color={isDynamicHeightEnabled && !isHiddenOrModalOpen ? 'var(--icon-on-solid)' : 'var(--icon-default)'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
<span>{componentName}</span>
|
||||
</ConfigHandleButton>
|
||||
|
||||
<ConfigHandleButton
|
||||
customStyles={iconOnlyButtonStyle}
|
||||
onClick={() => setComponentToInspect(componentName)}
|
||||
message="State inspector"
|
||||
show={true}
|
||||
dataCy={`${componentName.toLowerCase()}-inspect-button`}
|
||||
>
|
||||
<SquareDashedMousePointer size={14} color="var(--icon-strong)" />
|
||||
</ConfigHandleButton>
|
||||
|
||||
<ConfigHandleButton
|
||||
customStyles={iconOnlyButtonStyle}
|
||||
onClick={() => {
|
||||
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
|
||||
setRightSidebarOpen(true);
|
||||
}}
|
||||
message="Properties & Styles"
|
||||
show={true}
|
||||
dataCy={`${componentName.toLowerCase()}-properties-styles-button`}
|
||||
>
|
||||
<PencilRuler size={14} color="var(--icon-strong)" />
|
||||
</ConfigHandleButton>
|
||||
|
||||
{licenseValid && isRestricted && (
|
||||
<ConfigHandleButton
|
||||
customStyles={iconOnlyButtonStyle}
|
||||
message={getTooltip()}
|
||||
show={licenseValid && isRestricted && !draggingComponentId}
|
||||
dataCy={`${componentName.toLowerCase()}-permissions-button`}
|
||||
>
|
||||
<Lock size={14} color="var(--icon-strong)" />
|
||||
</ConfigHandleButton>
|
||||
)}
|
||||
{!isMultipleComponentsSelected && !shouldFreeze && <MentionComponentInChat componentName={componentName} />}
|
||||
<ConfigHandleButton
|
||||
customStyles={iconOnlyButtonStyle}
|
||||
onClick={() => {
|
||||
!shouldFreeze && deleteComponents();
|
||||
}}
|
||||
message="Delete component"
|
||||
show={true}
|
||||
dataCy={`${componentName.toLowerCase()}-delete-component-button`}
|
||||
shouldHide={shouldFreeze}
|
||||
>
|
||||
<Trash size={14} color="var(--icon-strong)" />
|
||||
</ConfigHandleButton>
|
||||
{/* Tooltip for invalid license on ModuleViewer */}
|
||||
{!isLicenseValid && componentType === 'ModuleViewer' && (
|
||||
{(componentType === 'ModuleViewer' || componentType === 'ModuleContainer') && !isModulesEnabled && (
|
||||
<Tooltip
|
||||
delay={{ show: 500, hide: 50 }}
|
||||
id={`invalid-license-modules-${componentName?.toLowerCase()}`}
|
||||
className="tooltip"
|
||||
isOpen={_showHandle && componentType === 'ModuleViewer'}
|
||||
isOpen={_showHandle && (componentType === 'ModuleViewer' || componentType === 'ModuleContainer')}
|
||||
style={{ textAlign: 'center' }}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
|
||||
|
||||
function MentionComponentInChat() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default withEditionSpecificComponent(MentionComponentInChat, 'Appbuilder');
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
.config-handle {
|
||||
position: fixed;
|
||||
max-height: 10px;
|
||||
z-index: 100;
|
||||
min-width: 108px;
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
transition: all .15s ease-in-out;
|
||||
|
||||
}
|
||||
|
||||
.config-handle-visible {
|
||||
|
|
@ -15,9 +15,12 @@
|
|||
.multiple-components-config-handle {
|
||||
position: absolute;
|
||||
left: 54px;
|
||||
top: -20px;
|
||||
top: -26px;
|
||||
transform: translate(-50%, 0px);
|
||||
width: 110px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.config-handle,
|
||||
|
|
@ -31,7 +34,22 @@
|
|||
.badge {
|
||||
font-size: 9px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
.delete-part {
|
||||
margin-left: 10px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.delete-part::before {
|
||||
height: 12px;
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
opacity: 0.5;
|
||||
content: "";
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,9 +62,48 @@
|
|||
.config-handle {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
.badge {
|
||||
font-size: 9px;
|
||||
background: #c6cad0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dynamic-height-info-popover {
|
||||
width: 290px !important;
|
||||
// height: 220px;
|
||||
border-radius: 8px !important;
|
||||
padding: 12px !important;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid var(--border-weak, #E4E7EB) !important;
|
||||
color: var(--text-default, #1B1F24) !important;
|
||||
box-shadow: var(--elevation-300-box-shadow) !important;
|
||||
|
||||
|
||||
.dynamic-height-info-body {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.dynamic-height-info-text-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
height: 18px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.dynamic-height-info-text-description {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
height: 36px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.dynamic-height-info-button {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.config-handle-button {
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ import { ModuleContainerBlank } from '@/modules/Modules/components';
|
|||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import useSortedComponents from '../_hooks/useSortedComponents';
|
||||
import { useDropVirtualMoveableGhost } from './Grid/hooks/useDropVirtualMoveableGhost';
|
||||
import { useCanvasDropHandler } from './useCanvasDropHandler';
|
||||
import { findNewParentIdFromMousePosition } from './Grid/gridUtils';
|
||||
|
||||
//TODO: Revisit the logic of height (dropRef)
|
||||
|
|
@ -38,6 +37,7 @@ const Container = React.memo(
|
|||
canvasMaxWidth,
|
||||
componentType,
|
||||
appType,
|
||||
hasNoScroll = false,
|
||||
}) => {
|
||||
const { moduleId, isModuleEditor } = useModuleContext();
|
||||
const realCanvasRef = useRef(null);
|
||||
|
|
@ -50,6 +50,10 @@ const Container = React.memo(
|
|||
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
|
||||
const currentLayout = useStore((state) => state.currentLayout, shallow);
|
||||
const setFocusedParentId = useStore((state) => state.setFocusedParentId, shallow);
|
||||
const isWidgetInSubContainerDragging = useStore(
|
||||
(state) => state.containerChildrenMapping?.[id]?.includes(state?.draggingComponentId),
|
||||
shallow
|
||||
);
|
||||
|
||||
// Initialize ghost moveable hook
|
||||
const { activateMoveableGhost, deactivateMoveableGhost } = useDropVirtualMoveableGhost();
|
||||
|
|
@ -58,7 +62,6 @@ const Container = React.memo(
|
|||
const { isDragging } = useDragLayer((monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}));
|
||||
|
||||
// // // Cleanup ghost when drag ends
|
||||
useEffect(() => {
|
||||
if (!isDragging) {
|
||||
|
|
@ -72,10 +75,6 @@ const Container = React.memo(
|
|||
|
||||
const setCurrentDragCanvasId = useGridStore((state) => state.actions.setCurrentDragCanvasId);
|
||||
|
||||
const { handleDrop } = useCanvasDropHandler({
|
||||
appType,
|
||||
});
|
||||
|
||||
const [{ isOverCurrent }, drop] = useDrop({
|
||||
accept: 'box',
|
||||
hover: (item, monitor) => {
|
||||
|
|
@ -117,11 +116,10 @@ const Container = React.memo(
|
|||
}
|
||||
|
||||
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
|
||||
|
||||
useEffect(() => {
|
||||
useGridStore.getState().actions.setSubContainerWidths(id, gridWidth);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [canvasWidth, listViewMode, columns]);
|
||||
}, [canvasWidth, listViewMode, columns, id]);
|
||||
|
||||
const handleCanvasClick = useCallback(
|
||||
(e) => {
|
||||
|
|
@ -130,8 +128,10 @@ const Container = React.memo(
|
|||
setFocusedParentId(canvasId);
|
||||
if (realCanvas) {
|
||||
const rect = realCanvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const scrollLeft = realCanvas.scrollLeft || 0;
|
||||
const scrollTop = realCanvas.scrollTop || 0;
|
||||
const x = e.clientX - rect.left + scrollLeft;
|
||||
const y = e.clientY - rect.top + scrollTop;
|
||||
setLastCanvasClickPosition({ x, y });
|
||||
}
|
||||
},
|
||||
|
|
@ -190,6 +190,8 @@ const Container = React.memo(
|
|||
})(),
|
||||
transform: 'translateZ(0)', //Very very imp --> Hack to make modal position respect canvas container, else it positions w.r.t window.
|
||||
...styles,
|
||||
// Prevent the scroll when dragging a widget inside the container or moving out of the container
|
||||
overflow: isWidgetInSubContainerDragging ? 'hidden' : undefined,
|
||||
...(id !== 'canvas' && appType !== 'module' && { backgroundColor: 'transparent' }), // Ensure the container's background isn't overridden by the canvas background color.
|
||||
}}
|
||||
className={cx('real-canvas', {
|
||||
|
|
@ -197,6 +199,8 @@ const Container = React.memo(
|
|||
'show-grid': isDragging && (index === 0 || index === null) && currentMode === 'edit' && appType !== 'module',
|
||||
'module-container': appType === 'module',
|
||||
'is-module-editor': isModuleEditor,
|
||||
'has-no-scroll': hasNoScroll,
|
||||
'is-child-being-dragged': !hasNoScroll && isWidgetInSubContainerDragging,
|
||||
})}
|
||||
id={id === 'canvas' ? 'real-canvas' : `canvas-${id}`}
|
||||
data-cy="real-canvas"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import React from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
export const DragGhostWidget = () => {
|
||||
export const DragResizeGhostWidget = () => {
|
||||
const draggingComponentId = useStore((state) => state.draggingComponentId);
|
||||
const isGroupDragging = useStore((state) => state.isGroupDragging);
|
||||
const isGroupResizing = useStore((state) => state.isGroupResizing);
|
||||
const resizingComponentId = useStore((state) => state.resizingComponentId);
|
||||
|
||||
if (!draggingComponentId) return null;
|
||||
if (!draggingComponentId && !resizingComponentId && !isGroupDragging && !isGroupResizing) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="moveable-drag-ghost"
|
||||
id="moveable-ghost-widget"
|
||||
style={{
|
||||
zIndex: 4,
|
||||
position: 'absolute',
|
||||
|
|
@ -21,20 +24,3 @@ export const DragGhostWidget = () => {
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ResizeGhostWidget = () => {
|
||||
const resizingComponentId = useStore((state) => state.resizingComponentId);
|
||||
if (!resizingComponentId) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="resize-ghost-widget"
|
||||
style={{
|
||||
zIndex: 4,
|
||||
position: 'absolute',
|
||||
background: '#D9E2FC',
|
||||
opacity: '0.7',
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,42 @@
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-line[data-line-key="render-line-0"],
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-line[data-line-key="render-line-1"],
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-line[data-line-key="render-line-2"],
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-line[data-line-key="render-line-3"]{
|
||||
background-color: #9747FF !important;
|
||||
}
|
||||
|
||||
/*
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-line[data-line-key="render-line-0"],
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-line[data-line-key="render-line-2"] {
|
||||
background: none !important;
|
||||
border: 1px dashed #4af !important;
|
||||
}
|
||||
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-control.moveable-direction.moveable-e.moveable-resizable[data-direction="e"],
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-control.moveable-direction.moveable-w.moveable-resizable[data-direction="w"] {
|
||||
height:24px !important;
|
||||
width:8px !important;
|
||||
border-radius: 4px !important;
|
||||
border: 1px solid #4af !important;
|
||||
background: #fff !important;
|
||||
opacity: 1 !important;
|
||||
flex-shrink: 1 !important;
|
||||
}
|
||||
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-control.moveable-direction.moveable-e.moveable-resizable[data-direction="e"] {
|
||||
left: 0px !important;
|
||||
top: -10px !important;
|
||||
}
|
||||
|
||||
.moveable-control-box.moveable-dynamic-height .moveable-control.moveable-direction.moveable-w.moveable-resizable[data-direction="w"] {
|
||||
left: 0px !important;
|
||||
top: -10px !important;
|
||||
} */
|
||||
|
||||
|
||||
.moveable-control-box
|
||||
> .moveable-control-box:not(
|
||||
.moveable-control-box-d-block,
|
||||
|
|
@ -212,4 +248,9 @@
|
|||
}
|
||||
.disable-moveable-line .moveable-line.moveable-direction {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.show-ghost-group-dragging-resizing {
|
||||
outline: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,19 +19,26 @@ import {
|
|||
handleActivateTargets,
|
||||
handleDeactivateTargets,
|
||||
handleActivateNonDraggingComponents,
|
||||
computeScrollDelta,
|
||||
computeScrollDeltaOnDrag,
|
||||
getDraggingWidgetWidth,
|
||||
positionGhostElement,
|
||||
positionGroupGhostElement,
|
||||
clearActiveTargetClassNamesAfterSnapping,
|
||||
isDraggingModalToCanvas,
|
||||
updateDashedBordersOnHover,
|
||||
updateDashedBordersOnDragResize,
|
||||
} from './gridUtils';
|
||||
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
|
||||
import { dragContextBuilder, getAdjustedDropPosition, getDroppableSlotIdOnScreen } from './helpers/dragEnd';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import './Grid.css';
|
||||
import { useGroupedTargetsScrollHandler } from './hooks/useGroupedTargetsScrollHandler';
|
||||
import { useCanvasAutoScroll } from './hooks/useCanvasAutoScroll';
|
||||
import { DROPPABLE_PARENTS, NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { useElementGuidelines } from './hooks/useElementGuidelines';
|
||||
import { RIGHT_SIDE_BAR_TAB } from '../../RightSideBar/rightSidebarConstants';
|
||||
import MentionComponentInChat from '../ConfigHandle/MentionComponentInChat';
|
||||
import ConfigHandleButton from '@/_components/ConfigHandleButton';
|
||||
|
||||
const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, position: 'css' };
|
||||
const RESIZABLE_CONFIG = {
|
||||
|
|
@ -39,16 +46,11 @@ const RESIZABLE_CONFIG = {
|
|||
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
|
||||
};
|
||||
|
||||
const HORIZONTAL_CONFIG = {
|
||||
edge: ['e', 'w'],
|
||||
renderDirections: ['w', 'e'],
|
||||
};
|
||||
|
||||
export const GRID_HEIGHT = 10;
|
||||
|
||||
export default function Grid({ gridWidth, currentLayout }) {
|
||||
export default function Grid({ gridWidth, currentLayout, mainCanvasWidth }) {
|
||||
const { moduleId, isModuleEditor } = useModuleContext();
|
||||
const lastDraggedEventsRef = useRef(null);
|
||||
const lastGroupDragEventRef = useRef(null);
|
||||
const updateCanvasBottomHeight = useStore((state) => state.updateCanvasBottomHeight, shallow);
|
||||
const setComponentLayout = useStore((state) => state.setComponentLayout, shallow);
|
||||
const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
|
||||
|
|
@ -57,42 +59,46 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const selectedComponents = useStore((state) => state.selectedComponents, shallow);
|
||||
const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow);
|
||||
const getComponentTypeFromId = useStore((state) => state.getComponentTypeFromId, shallow);
|
||||
const getComponentDefinition = useStore((state) => state.getComponentDefinition, shallow);
|
||||
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
|
||||
const temporaryHeight = useStore((state) => state.temporaryLayouts?.[selectedComponents?.[0]]?.height, shallow);
|
||||
const isGroupHandleHoverd = useIsGroupHandleHoverd();
|
||||
const checkHoveredComponentDynamicHeight = useStore((state) => state.checkHoveredComponentDynamicHeight, shallow);
|
||||
const openModalWidgetId = useOpenModalWidgetId();
|
||||
const moveableRef = useRef(null);
|
||||
const virtualTarget = useGridStore((state) => state.virtualTarget, shallow);
|
||||
|
||||
const { startAutoScroll, stopAutoScroll, updateMousePosition, getScrollDelta } = useCanvasAutoScroll(
|
||||
{},
|
||||
boxList,
|
||||
virtualTarget,
|
||||
moveableRef
|
||||
);
|
||||
const triggerCanvasUpdater = useStore((state) => state.triggerCanvasUpdater, shallow);
|
||||
const toggleCanvasUpdater = useStore((state) => state.toggleCanvasUpdater, shallow);
|
||||
const incrementCanvasUpdater = useStore((state) => state.incrementCanvasUpdater, shallow);
|
||||
const groupResizeDataRef = useRef([]);
|
||||
const isDraggingRef = useRef(false);
|
||||
const canvasWidth = NO_OF_GRIDS * gridWidth;
|
||||
const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow);
|
||||
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
|
||||
const getTemporaryLayouts = useStore((state) => state.getTemporaryLayouts, shallow);
|
||||
const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow);
|
||||
const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
|
||||
// const [dragParentId, setDragParentId] = useState(null);
|
||||
const componentsSnappedTo = useRef(null);
|
||||
const prevDragParentId = useRef(null);
|
||||
const newDragParentId = useRef(null);
|
||||
// const [isGroupDragging, setIsGroupDragging] = useState(false);
|
||||
// const checkIfAnyWidgetVisibilityChanged = useStore((state) => state.checkIfAnyWidgetVisibilityChanged(), shallow);
|
||||
const getExposedValueOfComponent = useStore((state) => state.getExposedValueOfComponent, shallow);
|
||||
const setReorderContainerChildren = useStore((state) => state.setReorderContainerChildren, shallow);
|
||||
const virtualTarget = useGridStore((state) => state.virtualTarget, shallow);
|
||||
const currentDragCanvasId = useGridStore((state) => state.currentDragCanvasId, shallow);
|
||||
const [isVerticalExpansionRestricted, setIsVerticalExpansionRestricted] = useState(false);
|
||||
const checkHoveredComponentDynamicHeight = useStore((state) => state.checkHoveredComponentDynamicHeight, shallow);
|
||||
const pageMenuProperties = useStore((state) => state?.pageSettings?.definition?.properties ?? {});
|
||||
const isPageMenuHidden = useStore((state) => state?.getPagesSidebarVisibility(moduleId), shallow);
|
||||
const groupedTargets = [...findHighestLevelofSelection().map((component) => '.ele-' + component.id)];
|
||||
|
||||
const isGroupResizingRef = useRef(false);
|
||||
const isGroupDraggingRef = useRef(false);
|
||||
const isWidgetResizable = useMemo(() => {
|
||||
if (virtualTarget) {
|
||||
return false;
|
||||
}
|
||||
return isVerticalExpansionRestricted ? HORIZONTAL_CONFIG : RESIZABLE_CONFIG;
|
||||
}, [isVerticalExpansionRestricted, virtualTarget]);
|
||||
return RESIZABLE_CONFIG;
|
||||
}, [virtualTarget]);
|
||||
|
||||
const getMoveableTarget = useCallback(() => {
|
||||
if (virtualTarget) {
|
||||
|
|
@ -118,6 +124,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
Object.keys(currentPageComponents)
|
||||
.map((key) => {
|
||||
const widget = currentPageComponents[key];
|
||||
|
||||
return {
|
||||
id: key,
|
||||
...widget,
|
||||
|
|
@ -147,25 +154,26 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
};
|
||||
|
||||
const noOfBoxs = Object.values(boxList || []).length;
|
||||
|
||||
const { position: menuPosition, hideLogo, hideHeader } = pageMenuProperties;
|
||||
|
||||
useEffect(() => {
|
||||
updateCanvasBottomHeight(boxList, moduleId);
|
||||
noOfBoxs != 0;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [noOfBoxs, triggerCanvasUpdater]);
|
||||
}, [noOfBoxs, triggerCanvasUpdater, menuPosition, hideLogo, hideHeader, isPageMenuHidden]);
|
||||
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
|
||||
const handleResizeStop = useCallback(
|
||||
(boxList) => {
|
||||
const temporaryLayouts = getTemporaryLayouts();
|
||||
// Batch all layout updates into a single object
|
||||
const batchedLayouts = {};
|
||||
|
||||
boxList.forEach(({ id, height, width, x, y, gw }) => {
|
||||
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
|
||||
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
|
||||
|
||||
// Consider temporary layout position if it exists
|
||||
const temporaryLayout = temporaryLayouts[id];
|
||||
y = temporaryLayout?.top ?? Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
gw = gw ? gw : gridWidth;
|
||||
|
||||
|
|
@ -187,15 +195,20 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
newWidth = 43 - posX;
|
||||
}
|
||||
}
|
||||
setComponentLayout({
|
||||
[id]: {
|
||||
height: height ? height : GRID_HEIGHT,
|
||||
width: newWidth ? newWidth : 1,
|
||||
top: y,
|
||||
left: Math.round(x / gw),
|
||||
},
|
||||
});
|
||||
|
||||
// Add to batched layouts instead of calling setComponentLayout immediately
|
||||
batchedLayouts[id] = {
|
||||
height: height ? height : GRID_HEIGHT,
|
||||
width: newWidth ? newWidth : 1,
|
||||
top: y,
|
||||
left: Math.round(x / gw),
|
||||
};
|
||||
});
|
||||
|
||||
// Call setComponentLayout once with all updates
|
||||
if (Object.keys(batchedLayouts).length > 0) {
|
||||
setComponentLayout(batchedLayouts);
|
||||
}
|
||||
},
|
||||
[canvasWidth, gridWidth, setComponentLayout]
|
||||
);
|
||||
|
|
@ -203,21 +216,24 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const configHandleWhenMultipleComponentSelected = (id) => {
|
||||
return (
|
||||
<div
|
||||
id="multiple-components-config-handle"
|
||||
className={'multiple-components-config-handle'}
|
||||
// This is to handle dragging using config handle when multiple components are selected since dragGroup is not triggered
|
||||
onMouseUpCapture={() => {
|
||||
if (lastDraggedEventsRef.current) {
|
||||
// Creatint the same event object that matches what onDragGroupEnd expects
|
||||
const lastDraggedEvents = lastGroupDragEventRef.current;
|
||||
if (lastDraggedEvents?.length) {
|
||||
// Creating the same event object that matches what onDragGroupEnd expects
|
||||
const event = {
|
||||
clientX: lastDraggedEventsRef.current.events[0].clientX,
|
||||
clientY: lastDraggedEventsRef.current.events[0].clientY,
|
||||
events: lastDraggedEventsRef.current.events.map((ev) => ({
|
||||
clientX: lastDraggedEvents?.[0].clientX,
|
||||
clientY: lastDraggedEvents?.[0].clientY,
|
||||
events: lastDraggedEvents?.map((ev) => ({
|
||||
target: ev.target,
|
||||
lastEvent: {
|
||||
translate: [ev.translate[0], ev.translate[1]],
|
||||
},
|
||||
})),
|
||||
targets: lastDraggedEvents?.map((ev) => ev.target),
|
||||
};
|
||||
|
||||
handleDragGroupEnd(event);
|
||||
}
|
||||
|
||||
|
|
@ -226,28 +242,19 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
}}
|
||||
onMouseDownCapture={() => {
|
||||
lastDraggedEventsRef.current = null;
|
||||
lastGroupDragEventRef.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 id={id}>
|
||||
<ConfigHandleButton className="no-hover">Components</ConfigHandleButton>
|
||||
<MentionComponentInChat componentIds={selectedComponents} currentPageComponents={currentPageComponents} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//TO-DO -> Move this to moveableExtensions.js
|
||||
const MultiComponentHandle = {
|
||||
name: 'multiComponentHandle',
|
||||
|
|
@ -273,7 +280,6 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
mouseLeave(e) {
|
||||
e.props.target.classList.remove('hovered');
|
||||
e.controlBox.classList.remove('moveable-control-box-d-block');
|
||||
e.controlBox.classList.remove('moveable-horizonta-only');
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -337,21 +343,13 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
if (moveableRef.current) {
|
||||
safeUpdateMoveable();
|
||||
}
|
||||
}, [temporaryHeight, boxList]);
|
||||
}, [boxList, selectedComponents, mainCanvasWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadGrid();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedComponents, openModalWidgetId, boxList, currentLayout]);
|
||||
|
||||
const updateNewPosition = (events, parent = null) => {
|
||||
const posWithParent = {
|
||||
events,
|
||||
parent,
|
||||
};
|
||||
lastDraggedEventsRef.current = posWithParent;
|
||||
};
|
||||
|
||||
const isComponentVisible = (id) => {
|
||||
const component = getResolvedComponent(id, null, moduleId);
|
||||
const componentExposedVisibility = getExposedValueOfComponent(id, moduleId)?.isVisible;
|
||||
|
|
@ -403,7 +401,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const parentId = parent.includes('-') ? parent.split('-').slice(0, -1).join('-') : parent;
|
||||
const componentType = boxList.find((box) => box.id === parentId)?.component.component;
|
||||
const parentHeight = parentElem?.clientHeight || _height;
|
||||
if (_height > parentHeight && ['Tabs', 'Listview'].includes(componentType)) {
|
||||
if (_height > parentHeight && ['Listview'].includes(componentType)) {
|
||||
_height = parentHeight;
|
||||
y = 0;
|
||||
}
|
||||
|
|
@ -423,13 +421,12 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
return layouts;
|
||||
}, {});
|
||||
setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: true });
|
||||
// Only set updateParent to true when the parent actually changed
|
||||
// This avoids unnecessary batch updates for simple drag operations within the same parent
|
||||
const hasParentChanged = newParent !== oldParent;
|
||||
setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: hasParentChanged });
|
||||
|
||||
// const currentWidget = boxList.find((box) => box.id === id);
|
||||
updateContainerAutoHeight(newParent);
|
||||
updateContainerAutoHeight(oldParent);
|
||||
|
||||
toggleCanvasUpdater();
|
||||
incrementCanvasUpdater();
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[boxList, currentLayout, gridWidth]
|
||||
|
|
@ -442,20 +439,13 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const showConfigHandle = (e) => {
|
||||
const targetId = e.target.offsetParent.getAttribute('target-id');
|
||||
const componentType = getComponentTypeFromId(targetId);
|
||||
|
||||
if (componentType === 'ModuleContainer') {
|
||||
return;
|
||||
}
|
||||
useStore.getState().setHoveredComponentBoundaryId(targetId);
|
||||
const isHorizontallyExpandable = checkHoveredComponentDynamicHeight(targetId);
|
||||
const moveableControlBox = document.querySelector(`.moveable-control-box[target-id="${targetId}"]`);
|
||||
if (moveableControlBox) {
|
||||
if (isHorizontallyExpandable) {
|
||||
moveableControlBox.classList.add('moveable-horizontal-only');
|
||||
} else {
|
||||
moveableControlBox.classList.remove('moveable-horizontal-only');
|
||||
}
|
||||
}
|
||||
setIsVerticalExpansionRestricted(!!isHorizontallyExpandable);
|
||||
|
||||
updateDashedBordersOnHover(targetId);
|
||||
};
|
||||
const hideConfigHandle = () => {
|
||||
useStore.getState().setHoveredComponentBoundaryId('');
|
||||
|
|
@ -468,12 +458,28 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
moveableBox.removeEventListener('mouseover', showConfigHandle);
|
||||
moveableBox.removeEventListener('mouseout', hideConfigHandle);
|
||||
};
|
||||
}, [moveableRef?.current?._elementTargets?.length]);
|
||||
}, [moveableRef?.current?._elementTargets?.length, checkHoveredComponentDynamicHeight, getComponentTypeFromId]);
|
||||
|
||||
const handleDragGroupEnd = (e) => {
|
||||
try {
|
||||
const scrollDelta = getScrollDelta();
|
||||
// Stop autoscroll monitoring for group drag
|
||||
stopAutoScroll();
|
||||
hideGridLines();
|
||||
// setIsGroupDragging(false);
|
||||
handleDeactivateTargets();
|
||||
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
||||
if (isGroupDraggingRef.current) {
|
||||
useStore.getState().setIsGroupDragging(false);
|
||||
isGroupDraggingRef.current = false;
|
||||
}
|
||||
e.targets.forEach((targetWidget) => {
|
||||
if (!targetWidget) return;
|
||||
targetWidget.classList.remove('show-ghost-group-dragging-resizing');
|
||||
const moveableControlBox = document.getElementsByClassName(`sc-${targetWidget.id}`)[0];
|
||||
if (moveableControlBox) {
|
||||
moveableControlBox.style.setProperty('visibility', 'visible', 'important');
|
||||
}
|
||||
});
|
||||
const { events, clientX, clientY } = e;
|
||||
const initialParent = events[0].target.closest('.real-canvas');
|
||||
// Get potential new parent using same logic as onDragEnd
|
||||
|
|
@ -579,11 +585,11 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const snappedX = Math.round(posX / _gridWidth) * _gridWidth;
|
||||
const snappedY = Math.round(posY / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
ev.target.style.transform = `translate(${snappedX}px, ${snappedY}px)`;
|
||||
ev.target.style.transform = `translate(${snappedX + scrollDelta.x}px, ${snappedY + scrollDelta.y}px)`;
|
||||
return {
|
||||
id: ev.target.id,
|
||||
x: posX,
|
||||
y: posY,
|
||||
x: posX + scrollDelta.x || 0,
|
||||
y: posY + scrollDelta.y || 0,
|
||||
parent: draggedOverElemId,
|
||||
};
|
||||
})
|
||||
|
|
@ -597,7 +603,6 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
useGroupedTargetsScrollHandler(groupedTargets, boxList, moveableRef);
|
||||
if (mode !== 'edit') return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Moveable
|
||||
|
|
@ -614,16 +619,14 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
origin={false}
|
||||
individualGroupable={virtualTarget ? false : groupedTargets.length <= 1}
|
||||
draggable={!shouldFreeze}
|
||||
resizable={!shouldFreeze ? isWidgetResizable : false && mode !== 'view'}
|
||||
resizable={!shouldFreeze ? isWidgetResizable : false}
|
||||
keepRatio={false}
|
||||
individualGroupableProps={individualGroupableProps}
|
||||
onResize={(e) => {
|
||||
const temporaryLayouts = getTemporaryLayouts();
|
||||
const currentWidget = boxList.find(({ id }) => id === e.target.id);
|
||||
const resizingComponentId = useStore.getState().resizingComponentId;
|
||||
if (resizingComponentId !== e.target.id) {
|
||||
useStore.getState().setResizingComponentId(e.target.id);
|
||||
showGridLines();
|
||||
}
|
||||
|
||||
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
||||
|
|
@ -638,9 +641,12 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const isLeftChanged = e.direction[0] === -1;
|
||||
const isTopChanged = e.direction[1] === -1;
|
||||
|
||||
// Calculate positions considering temporary layouts'
|
||||
// Get scroll delta from autoscroll hook
|
||||
const scrollDelta = getScrollDelta();
|
||||
|
||||
// Calculate positions with scroll delta adjustment
|
||||
let transformX = currentWidget.left * _gridWidth;
|
||||
let transformY = temporaryLayouts[currentWidget.id]?.top ?? currentWidget.top;
|
||||
let transformY = currentWidget.top;
|
||||
|
||||
if (isLeftChanged) {
|
||||
// Left resize
|
||||
|
|
@ -653,7 +659,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
// Apply container bounds
|
||||
const elemContainer = e.target.closest('.real-canvas');
|
||||
const containerHeight = elemContainer.clientHeight;
|
||||
const containerHeight = elemContainer.scrollHeight;
|
||||
const containerWidth = elemContainer.clientWidth;
|
||||
const maxY = containerHeight - e.target.clientHeight;
|
||||
const maxLeft = containerWidth - e.target.clientWidth;
|
||||
|
|
@ -670,10 +676,11 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
if (!maxHeightHit || e.height < e.target.clientHeight) {
|
||||
e.target.style.height = `${e.height}px`;
|
||||
}
|
||||
e.target.style.transform = `translate(${transformX}px, ${transformY}px)`;
|
||||
e.target.style.transform = `translate(${transformX + scrollDelta.x}px, ${transformY + scrollDelta.y}px)`;
|
||||
if (e.width > 0) e.target.style.width = `${e.width}px`;
|
||||
if (e.height > 0) e.target.style.height = `${e.height}px`;
|
||||
positionGhostElement(e.target, 'resize-ghost-widget');
|
||||
|
||||
positionGhostElement(e.target, 'moveable-ghost-widget');
|
||||
}}
|
||||
onResizeStart={(e) => {
|
||||
if (
|
||||
|
|
@ -688,19 +695,23 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
return false;
|
||||
}
|
||||
handleActivateNonDraggingComponents();
|
||||
updateDashedBordersOnDragResize(e.target.id, e?.moveable?.controlBox?.classList);
|
||||
e.setMin([gridWidth, GRID_HEIGHT]);
|
||||
}}
|
||||
onResizeEnd={(e) => {
|
||||
try {
|
||||
handleDeactivateTargets();
|
||||
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
||||
useStore.getState().setResizingComponentId(null);
|
||||
const currentWidget = boxList.find(({ id }) => {
|
||||
return id === e.target.id;
|
||||
});
|
||||
hideGridLines();
|
||||
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
||||
const directions = e.lastEvent?.direction;
|
||||
if (!e.lastEvent) {
|
||||
return;
|
||||
}
|
||||
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 / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
const currentWidth = currentWidget.width * _gridWidth;
|
||||
|
|
@ -740,10 +751,10 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
const resizeData = {
|
||||
id: e.target.id,
|
||||
height: height,
|
||||
height: directions[1] !== 0 ? height : currentWidget.height,
|
||||
width: width,
|
||||
x: transformX,
|
||||
y: transformY,
|
||||
x: transformX || 0,
|
||||
y: transformY || 0,
|
||||
};
|
||||
if (currentWidget.component?.parent) {
|
||||
resizeData.gw = _gridWidth;
|
||||
|
|
@ -753,18 +764,28 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
} catch (error) {
|
||||
console.error('ResizeEnd error ->', error);
|
||||
}
|
||||
handleDeactivateTargets();
|
||||
toggleCanvasUpdater();
|
||||
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
||||
incrementCanvasUpdater();
|
||||
}}
|
||||
onResizeGroupStart={({ events }) => {
|
||||
showGridLines();
|
||||
handleActivateNonDraggingComponents();
|
||||
events.forEach((ev) => {
|
||||
ev.target.classList.add('show-ghost-group-dragging-resizing');
|
||||
const moveableControlBox = document.getElementsByClassName(`sc-${ev.target.id}`)[0];
|
||||
if (moveableControlBox) {
|
||||
moveableControlBox.style.setProperty('visibility', 'hidden', 'important');
|
||||
}
|
||||
});
|
||||
}}
|
||||
onResizeGroup={({ events }) => {
|
||||
onResizeGroup={(e) => {
|
||||
const { events } = e;
|
||||
if (!isGroupResizingRef.current) {
|
||||
useStore.getState().setIsGroupResizing(true);
|
||||
isGroupResizingRef.current = true;
|
||||
}
|
||||
const parentElm = events[0].target.closest('.real-canvas');
|
||||
const parentWidth = parentElm?.clientWidth;
|
||||
const parentHeight = parentElm?.clientHeight;
|
||||
const parentHeight = parentElm?.scrollHeight;
|
||||
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
|
||||
const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight);
|
||||
events.forEach((ev) => {
|
||||
|
|
@ -772,10 +793,10 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
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;
|
||||
}
|
||||
positionGroupGhostElement(events, 'moveable-ghost-widget');
|
||||
}}
|
||||
onResizeGroupEnd={(e) => {
|
||||
try {
|
||||
|
|
@ -783,7 +804,17 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const newBoxs = [];
|
||||
|
||||
hideGridLines();
|
||||
|
||||
if (isGroupResizingRef.current) {
|
||||
useStore.getState().setIsGroupResizing(false);
|
||||
isGroupResizingRef.current = false;
|
||||
}
|
||||
events.forEach((ev) => {
|
||||
ev.target.classList.remove('show-ghost-group-dragging-resizing');
|
||||
const moveableControlBox = document.getElementsByClassName(`sc-${ev.target.id}`)[0];
|
||||
if (moveableControlBox) {
|
||||
moveableControlBox.style.setProperty('visibility', 'visible', 'important');
|
||||
}
|
||||
});
|
||||
// TODO: Logic needs to be relooked post go live P2
|
||||
groupResizeDataRef.current.forEach((ev) => {
|
||||
const currentWidget = boxList.find(({ id }) => {
|
||||
|
|
@ -840,19 +871,23 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
console.error('Error resizing group', error);
|
||||
}
|
||||
handleDeactivateTargets();
|
||||
toggleCanvasUpdater();
|
||||
incrementCanvasUpdater();
|
||||
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
||||
}}
|
||||
checkInput
|
||||
onDragStart={(e) => {
|
||||
if (e.target.id === 'moveable-virtual-ghost-element') {
|
||||
startAutoScroll(e.clientX, e.clientY, e.target);
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is to prevent parent component from being dragged and the stop the propagation of the event
|
||||
if (getHoveredComponentForGrid() !== e.target.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
|
||||
// Reset per-drag-session flag
|
||||
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
|
||||
|
||||
const box = boxList.find((box) => box.id === e.target.id);
|
||||
|
|
@ -860,6 +895,42 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
if (SUBCONTAINER_WIDGETS.includes(box?.component?.component) && e.inputEvent.shiftKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentId = boxList.find((box) => box.id === e.target.id)?.parent?.substring(0, 36);
|
||||
const parentComponent = boxList.find((box) => box.id === parentId);
|
||||
const isParentLegacyModal = parentComponent?.componentType === 'Modal';
|
||||
|
||||
// Special case for Modal. This is added to prevent widget from being dragged out of the modal.
|
||||
if (parentComponent?.componentType === 'ModalV2' || parentComponent?.componentType === 'Modal') {
|
||||
const modalContainer = e.target.closest('.tj-modal--container');
|
||||
const mainCanvas = document.getElementById('real-canvas');
|
||||
const mainRect = mainCanvas.getBoundingClientRect();
|
||||
const modalRect = modalContainer.getBoundingClientRect();
|
||||
const relativePosition = {
|
||||
top: modalRect.top - mainRect.top + (isParentLegacyModal ? 56 : 0), // 56 is the height of the legacy modal header
|
||||
right: mainRect.right - modalRect.right + modalContainer.offsetWidth,
|
||||
bottom: modalRect.height + (modalRect.top - mainRect.top),
|
||||
left: modalRect.left - mainRect.left,
|
||||
};
|
||||
setCanvasBounds({ ...relativePosition });
|
||||
} else if (isModuleEditor) {
|
||||
const moduleContainer = e.target.closest('.module-container-canvas');
|
||||
const mainCanvas = document.getElementById('real-canvas');
|
||||
|
||||
const mainRect = mainCanvas.getBoundingClientRect();
|
||||
const modalRect = moduleContainer.getBoundingClientRect();
|
||||
const relativePosition = {
|
||||
top: modalRect.top - mainRect.top,
|
||||
// right: mainRect.right - modalRect.right + moduleContainer.offsetWidth,
|
||||
|
||||
right: modalRect.left - mainRect.left + moduleContainer.offsetWidth,
|
||||
bottom: modalRect.height + (modalRect.top - mainRect.top),
|
||||
left: modalRect.left - mainRect.left,
|
||||
};
|
||||
|
||||
setCanvasBounds({ ...relativePosition });
|
||||
}
|
||||
|
||||
// This flag indicates whether the drag event originated on a child element within a component
|
||||
// (e.g., inside a Table's columns, Calendar's dates, or Kanban's cards).
|
||||
// When true, it prevents the parent component from being dragged, allowing the inner elements
|
||||
|
|
@ -900,7 +971,13 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
stopAutoScroll();
|
||||
handleDeactivateTargets();
|
||||
setCanvasBounds({ ...CANVAS_BOUNDS });
|
||||
hideGridLines();
|
||||
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
||||
|
||||
/* if the drag end is on the virtual ghost element(component drop), return */
|
||||
if (e.target.id === 'moveable-virtual-ghost-element') {
|
||||
return;
|
||||
}
|
||||
|
|
@ -929,16 +1006,12 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
// Compute new position
|
||||
let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged);
|
||||
const componentParentType = target?.widget?.componentType;
|
||||
|
||||
const isModalToCanvas = source.isModal && source.id !== target.id;
|
||||
// For now, only doing it for container and form, we need to check it for other components later
|
||||
let scrollDelta =
|
||||
componentParentType === 'Form' || componentParentType === 'Container'
|
||||
? document.getElementById(`canvas-${target.slotId}`)?.scrollTop || 0
|
||||
: computeScrollDelta({ source });
|
||||
const isModalToCanvas = isDraggingModalToCanvas(source, target, boxList);
|
||||
|
||||
let scrollDelta = computeScrollDeltaOnDrag(target.slotId);
|
||||
|
||||
if (isParentChangeAllowed && !isModalToCanvas && !isParentModuleContainer) {
|
||||
// Special case for Modal; If source widget is modal, prevent drops to canvas
|
||||
const parent = target.slotId === 'real-canvas' ? null : target.slotId;
|
||||
handleDragEnd([{ id: e.target.id, x: left, y: top + scrollDelta, parent }]);
|
||||
} else {
|
||||
|
|
@ -946,7 +1019,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
left = dragged.left * sourcegridWidth;
|
||||
top = dragged.top;
|
||||
!isModalToCanvas ??
|
||||
!isModalToCanvas &&
|
||||
toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`);
|
||||
isParentModuleContainer ? toast.error('Modules cannot be edited inside an app') : null;
|
||||
}
|
||||
|
|
@ -964,35 +1037,45 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
} catch (error) {
|
||||
console.error('Error in onDragEnd:', error);
|
||||
}
|
||||
setCanvasBounds({ ...CANVAS_BOUNDS });
|
||||
hideGridLines();
|
||||
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
||||
toggleCanvasUpdater();
|
||||
|
||||
incrementCanvasUpdater();
|
||||
}}
|
||||
onDrag={(e) => {
|
||||
if (e.target.id === 'moveable-virtual-ghost-element') {
|
||||
showGridLines();
|
||||
const _gridWidth = useGridStore.getState().subContainerWidths[currentDragCanvasId] || gridWidth;
|
||||
let left = e.translate[0];
|
||||
let top = e.translate[1];
|
||||
const scrollDelta = getScrollDelta();
|
||||
let left = e.translate[0] + scrollDelta.x;
|
||||
let top = e.translate[1] + scrollDelta.y;
|
||||
|
||||
if (currentDragCanvasId === 'canvas') {
|
||||
left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
|
||||
top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
left = Math.round(e.translate[0] / _gridWidth) * _gridWidth + scrollDelta.x || 0;
|
||||
top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT + scrollDelta.y || 0;
|
||||
|
||||
const _canvasWidth = NO_OF_GRIDS * _gridWidth;
|
||||
left = Math.max(0, Math.min(left, _canvasWidth - e.target.clientWidth));
|
||||
top = Math.max(0, top);
|
||||
}
|
||||
|
||||
// Apply bounds clamping to prevent widget from going out of canvas
|
||||
useGridStore.getState().actions.setGhostDragPosition({ left, top, e });
|
||||
const draggingWidgetWidth = getDraggingWidgetWidth(currentDragCanvasId, e.target.clientWidth);
|
||||
e.target.style.width = `${draggingWidgetWidth}px`;
|
||||
|
||||
e.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
|
||||
// Update autoscroll with current mouse position and target
|
||||
updateMousePosition(e.clientX, e.clientY, e.target);
|
||||
return false;
|
||||
}
|
||||
// Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again
|
||||
if (!isDraggingRef.current) {
|
||||
// Start autoscroll monitoring
|
||||
startAutoScroll(e.clientX, e.clientY, e.target);
|
||||
useStore.getState().setDraggingComponentId(e.target.id);
|
||||
showGridLines();
|
||||
handleActivateNonDraggingComponents();
|
||||
updateDashedBordersOnDragResize(e.target.id, e?.moveable?.controlBox?.classList);
|
||||
isDraggingRef.current = true;
|
||||
}
|
||||
const currentWidget = boxList.find((box) => box.id === e.target.id);
|
||||
|
|
@ -1001,128 +1084,160 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const _dragParentId = newDragParentId.current === null ? 'canvas' : newDragParentId.current;
|
||||
const _gridWidth = useGridStore.getState().subContainerWidths[_dragParentId] || gridWidth;
|
||||
|
||||
// Snap to grid
|
||||
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
|
||||
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
// Get scroll delta from autoscroll hook
|
||||
const scrollDelta = getScrollDelta();
|
||||
// Snap to grid + add scroll delta to keep widget under cursor
|
||||
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth + scrollDelta.x || 0;
|
||||
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT + scrollDelta.y || 0;
|
||||
const draggingWidgetWidth = getDraggingWidgetWidth(_dragParentId, e.target.clientWidth);
|
||||
e.target.style.width = `${draggingWidgetWidth}px`;
|
||||
|
||||
// This logic is to handle the case when the dragged element is over a new canvas
|
||||
if (_dragParentId !== currentParentId) {
|
||||
left = e.translate[0];
|
||||
top = e.translate[1];
|
||||
left = e.translate[0] + scrollDelta.x || 0;
|
||||
top = e.translate[1] + scrollDelta.y || 0;
|
||||
}
|
||||
|
||||
// Special case for Modal
|
||||
const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent;
|
||||
const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId;
|
||||
const parentComponent = boxList.find((box) => box.id === parentId);
|
||||
const parentWidgetType = parentComponent?.component?.component;
|
||||
const isOnHeaderOrFooter = oldParentId
|
||||
? oldParentId.includes('-header') || oldParentId.includes('-footer')
|
||||
: false;
|
||||
const isParentModalSlot = parentWidgetType === 'ModalV2' && isOnHeaderOrFooter;
|
||||
const isParentNewModal = parentComponent?.component?.component === 'ModalV2';
|
||||
const isParentLegacyModal = parentComponent?.component?.component === 'Modal';
|
||||
const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot;
|
||||
|
||||
if (isParentModal) {
|
||||
const modalContainer = e.target.closest('.tj-modal--container');
|
||||
const mainCanvas = document.getElementById('real-canvas');
|
||||
const mainRect = mainCanvas.getBoundingClientRect();
|
||||
const modalRect = modalContainer.getBoundingClientRect();
|
||||
const relativePosition = {
|
||||
top: modalRect.top - mainRect.top + (isParentLegacyModal ? 56 : 0), // 56 is the height of the legacy modal header
|
||||
right: mainRect.right - modalRect.right + modalContainer.offsetWidth,
|
||||
bottom: modalRect.height + (modalRect.top - mainRect.top),
|
||||
left: modalRect.left - mainRect.left,
|
||||
};
|
||||
setCanvasBounds({ ...relativePosition });
|
||||
} else if (isModuleEditor) {
|
||||
const moduleContainer = e.target.closest('.module-container-canvas');
|
||||
const mainCanvas = document.getElementById('real-canvas');
|
||||
|
||||
const mainRect = mainCanvas.getBoundingClientRect();
|
||||
const modalRect = moduleContainer.getBoundingClientRect();
|
||||
const relativePosition = {
|
||||
top: modalRect.top - mainRect.top,
|
||||
right: mainRect.right - modalRect.right + moduleContainer.offsetWidth,
|
||||
bottom: modalRect.height + (modalRect.top - mainRect.top),
|
||||
left: modalRect.left - mainRect.left,
|
||||
};
|
||||
setCanvasBounds({ ...relativePosition });
|
||||
let newParentId = getDroppableSlotIdOnScreen(e, boxList) || 'canvas';
|
||||
if (parentComponent?.component?.component === 'Modal') {
|
||||
// Never update parentId for Modal
|
||||
newParentId = parentComponent?.id;
|
||||
e.target.style.width = `${e.target.clientWidth}px`;
|
||||
}
|
||||
|
||||
// This block is to show grid lines on the canvas when the dragged element is over a new canvas
|
||||
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'));
|
||||
|
||||
// Determine potential new parent
|
||||
let newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
|
||||
if (newParentId === e.target.id) {
|
||||
newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent;
|
||||
} else if (parentComponent?.component?.component === 'Modal') {
|
||||
// Never update parentId for Modal
|
||||
newParentId = parentComponent?.id;
|
||||
e.target.style.width = `${e.target.clientWidth}px`;
|
||||
}
|
||||
|
||||
if (newParentId !== prevDragParentId.current) {
|
||||
// setDragParentId(newParentId === 'canvas' ? null : newParentId);
|
||||
newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
|
||||
prevDragParentId.current = newParentId;
|
||||
handleActivateTargets(newParentId);
|
||||
}
|
||||
if (newParentId !== prevDragParentId.current) {
|
||||
// setDragParentId(newParentId === 'canvas' ? null : newParentId);
|
||||
newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
|
||||
prevDragParentId.current = newParentId;
|
||||
handleActivateTargets(newParentId);
|
||||
}
|
||||
|
||||
// Build the drag context from the event
|
||||
const source = { slotId: oldParentId };
|
||||
let scrollDelta = computeScrollDeltaOnDrag({ source });
|
||||
// Apply bounds clamping to prevent widget from going out of main canvas
|
||||
if (newParentId === 'canvas' && currentParentId === 'canvas') {
|
||||
const _canvasWidth = NO_OF_GRIDS * _gridWidth;
|
||||
left = Math.max(0, Math.min(left, _canvasWidth - e.target.clientWidth));
|
||||
top = Math.max(0, top);
|
||||
}
|
||||
|
||||
e.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
|
||||
e.target.style.transform = `translate(${left}px, ${top - scrollDelta}px)`;
|
||||
e.target.setAttribute(
|
||||
'widget-pos2',
|
||||
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
|
||||
);
|
||||
|
||||
positionGhostElement(e.target, 'moveable-drag-ghost');
|
||||
positionGhostElement(e.target, 'moveable-ghost-widget');
|
||||
|
||||
// Update autoscroll with current mouse position and target
|
||||
updateMousePosition(e.clientX, e.clientY, e.target);
|
||||
}}
|
||||
onDragGroup={(ev) => {
|
||||
const { events } = ev;
|
||||
lastGroupDragEventRef.current = events;
|
||||
const parentElm = events[0]?.target?.closest('.real-canvas');
|
||||
if (parentElm && !parentElm.classList.contains('show-grid')) {
|
||||
parentElm?.classList?.add('show-grid');
|
||||
if (!isGroupDraggingRef.current) {
|
||||
useStore.getState().setIsGroupDragging(true);
|
||||
isGroupDraggingRef.current = true;
|
||||
// Add the class to the targets that are being dragged to hide the group selection
|
||||
lastGroupDragEventRef?.current?.forEach((ev) => {
|
||||
if (!ev?.target) return;
|
||||
ev.target.classList.add('show-ghost-group-dragging-resizing');
|
||||
const moveableControlBox = document.getElementsByClassName(`sc-${ev.target.id}`)[0];
|
||||
if (moveableControlBox) {
|
||||
moveableControlBox.style.setProperty('visibility', 'hidden', 'important');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get scroll delta from autoscroll hook for group drag
|
||||
const scrollDelta = getScrollDelta();
|
||||
|
||||
// First pass: calculate all positions and find group bounds
|
||||
const positions = [];
|
||||
let groupMinLeft = Infinity;
|
||||
let groupMinTop = Infinity;
|
||||
let groupMaxRight = -Infinity;
|
||||
|
||||
events.forEach((ev) => {
|
||||
const currentWidget = boxList.find(({ id }) => id === ev.target.id);
|
||||
const _gridWidth =
|
||||
useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth;
|
||||
|
||||
let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth;
|
||||
let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
// Add scroll delta to position for smooth scrolling during group drag
|
||||
let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth + scrollDelta.x || 0;
|
||||
let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT + scrollDelta.y || 0;
|
||||
|
||||
positions.push({ ev, left, top, currentWidget });
|
||||
|
||||
// Track group bounds for widgets on main canvas
|
||||
if (!currentWidget?.component?.parent) {
|
||||
groupMinLeft = Math.min(groupMinLeft, left);
|
||||
groupMinTop = Math.min(groupMinTop, top);
|
||||
groupMaxRight = Math.max(groupMaxRight, left + ev.target.clientWidth);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate offset needed to keep entire group within canvas bounds
|
||||
const realCanvas = document.getElementById('real-canvas');
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
||||
if (realCanvas && groupMinTop !== Infinity) {
|
||||
const canvasWidth = realCanvas.clientWidth;
|
||||
|
||||
// Top bound: group's topmost edge should not go below 0
|
||||
if (groupMinTop < 0) {
|
||||
offsetY = -groupMinTop;
|
||||
}
|
||||
// Left bound: group's leftmost edge should not go below 0
|
||||
if (groupMinLeft < 0) {
|
||||
offsetX = -groupMinLeft;
|
||||
}
|
||||
// Right bound: group's rightmost edge should not exceed canvas width
|
||||
if (groupMaxRight > canvasWidth) {
|
||||
offsetX = canvasWidth - groupMaxRight;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: apply positions with group offset
|
||||
positions.forEach(({ ev, left, top, currentWidget }) => {
|
||||
// Apply group offset only to widgets on main canvas
|
||||
if (!currentWidget?.component?.parent) {
|
||||
left += offsetX;
|
||||
top += offsetY;
|
||||
}
|
||||
ev.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
});
|
||||
|
||||
// Position single ghost for entire group
|
||||
positionGroupGhostElement(events, 'moveable-ghost-widget');
|
||||
|
||||
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
|
||||
updateNewPosition(events);
|
||||
|
||||
// Update autoscroll with current mouse position and all targets for group drag
|
||||
const targets = events.map((e) => e.target);
|
||||
updateMousePosition(ev.clientX, ev.clientY, targets);
|
||||
}}
|
||||
onDragGroupStart={({ events }) => {
|
||||
onDragGroupStart={(e) => {
|
||||
showGridLines();
|
||||
// setIsGroupDragging(true);
|
||||
handleActivateNonDraggingComponents();
|
||||
// Don't start autoscroll if dragging via config handle
|
||||
if (isGroupHandleHoverd) return;
|
||||
// Start autoscroll for group drag with all target elements
|
||||
const targets = e.targets || [];
|
||||
if (targets.length > 0) {
|
||||
startAutoScroll(e.clientX, e.clientY, targets, 'groupDrag');
|
||||
}
|
||||
}}
|
||||
onDragGroupEnd={(e) => {
|
||||
// IMP --> This function is not called when group components are dragged using config Handle, hence we have separate handler
|
||||
handleDragGroupEnd(e);
|
||||
handleDeactivateTargets();
|
||||
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
|
||||
toggleCanvasUpdater();
|
||||
incrementCanvasUpdater();
|
||||
}}
|
||||
onClickGroup={(e) => {
|
||||
const targetId =
|
||||
|
|
@ -1144,7 +1259,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
snapGap={false}
|
||||
isDisplaySnapDigit={false}
|
||||
// snapThreshold={GRID_HEIGHT}
|
||||
bounds={canvasBounds}
|
||||
bounds={virtualTarget ? CANVAS_BOUNDS : canvasBounds}
|
||||
// Guidelines configuration
|
||||
elementGuidelines={elementGuidelines}
|
||||
snapDirections={{
|
||||
|
|
@ -1176,6 +1291,19 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
}}
|
||||
snapGridAll={true}
|
||||
onClick={(e) => {
|
||||
// Check if the click is on a config handle button
|
||||
const configHandleButton = e.inputEvent?.target?.closest('.config-handle-button');
|
||||
|
||||
// Only execute if clicked on the first child (component-name-btn) span, not on inspect, properties, or delete buttons
|
||||
if (configHandleButton && !configHandleButton.classList.contains('component-name-btn')) {
|
||||
// Clicked on inspect, properties, or delete buttons - don't execute
|
||||
return;
|
||||
}
|
||||
|
||||
useStore.getState().setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
|
||||
useStore.getState().setRightSidebarOpen(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -288,15 +288,7 @@ export const handleWidgetResize = (e, list, boxes, gridWidth) => {
|
|||
};
|
||||
|
||||
export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) {
|
||||
let parentDiv = id
|
||||
? typeof id === 'string'
|
||||
? document.getElementById(id)
|
||||
: id
|
||||
: document.getElementsByClassName('real-canvas')[0];
|
||||
parentDiv = id === 'real-canvas' ? document.getElementById('real-canvas') : document.getElementById('canvas-' + id);
|
||||
if (parentWidgetType === 'Container' || parentWidgetType === 'Modal') {
|
||||
parentDiv = document.getElementById('canvas-' + id);
|
||||
}
|
||||
let parentDiv = document.getElementById('canvas-' + id) || document.getElementById('real-canvas');
|
||||
// Get the bounding rectangle of the parent div.
|
||||
const parentDivRect = parentDiv.getBoundingClientRect();
|
||||
const targetDivRect = event.target.getBoundingClientRect();
|
||||
|
|
@ -541,12 +533,10 @@ export const handleDeactivateTargets = () => {
|
|||
component.classList.remove('non-dragging-component');
|
||||
});
|
||||
};
|
||||
export const computeScrollDelta = ({ source }) => {
|
||||
export const computeScrollDeltaOnDrag = (canvasId) => {
|
||||
// Only need to calculate scroll delta when moving from a sub-container
|
||||
if (source.slotId !== 'real-canvas') {
|
||||
const subContainerWrap = document
|
||||
.querySelector(`#canvas-${source.slotId}`)
|
||||
?.closest('.sub-container-overflow-wrap');
|
||||
if (canvasId !== 'real-canvas') {
|
||||
const subContainerWrap = document.getElementById(`canvas-${canvasId}`);
|
||||
|
||||
return subContainerWrap?.scrollTop || 0;
|
||||
}
|
||||
|
|
@ -555,13 +545,8 @@ export const computeScrollDelta = ({ source }) => {
|
|||
return 0;
|
||||
};
|
||||
|
||||
export const computeScrollDeltaOnDrag = computeScrollDelta;
|
||||
|
||||
export const getDraggingWidgetWidth = (canvasParentId, widgetWidth) => {
|
||||
const transformedCanvasParentId = canvasParentId?.substring(0, 36);
|
||||
const targetCanvasWidth =
|
||||
document.getElementById(`canvas-${transformedCanvasParentId}`)?.offsetWidth ||
|
||||
document.getElementById('real-canvas')?.offsetWidth;
|
||||
const targetCanvasWidth = document.getElementById(`canvas-${canvasParentId}`)?.offsetWidth || 0;
|
||||
const gridUnitWidth = targetCanvasWidth / NO_OF_GRIDS;
|
||||
const gridUnits = Math.round(widgetWidth / gridUnitWidth);
|
||||
const draggingWidgetWidth = gridUnits * gridUnitWidth;
|
||||
|
|
@ -595,6 +580,68 @@ export const positionGhostElement = (targetElement, ghostElementId) => {
|
|||
ghostElement.style.height = `${targetRect.height}px`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the unified bounding box for a group of elements
|
||||
* @param {HTMLElement[]} targetElements - Array of elements being dragged as a group
|
||||
* @returns {Object} - Bounding box with left, top, width, height relative to main canvas
|
||||
*/
|
||||
export const calculateGroupBoundingBox = (targetElements) => {
|
||||
if (!targetElements || targetElements.length === 0) return null;
|
||||
|
||||
const mainCanvas = document.getElementById('real-canvas');
|
||||
if (!mainCanvas) return null;
|
||||
|
||||
const mainCanvasRect = mainCanvas.getBoundingClientRect();
|
||||
|
||||
// Initialize with extreme values
|
||||
let minLeft = Infinity;
|
||||
let minTop = Infinity;
|
||||
let maxRight = -Infinity;
|
||||
let maxBottom = -Infinity;
|
||||
|
||||
// Find the bounds of all elements
|
||||
targetElements.forEach((element) => {
|
||||
if (!element) return;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
const relativeLeft = rect.left - mainCanvasRect.left;
|
||||
const relativeTop = rect.top - mainCanvasRect.top;
|
||||
const relativeRight = relativeLeft + rect.width;
|
||||
const relativeBottom = relativeTop + rect.height;
|
||||
|
||||
minLeft = Math.min(minLeft, relativeLeft);
|
||||
minTop = Math.min(minTop, relativeTop);
|
||||
maxRight = Math.max(maxRight, relativeRight);
|
||||
maxBottom = Math.max(maxBottom, relativeBottom);
|
||||
});
|
||||
|
||||
return {
|
||||
left: minLeft,
|
||||
top: minTop,
|
||||
width: maxRight - minLeft,
|
||||
height: maxBottom - minTop,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Positions a ghost element to cover the entire bounding box of a group
|
||||
* @param {Object} boundingBox - Bounding box with left, top, width, height
|
||||
* @param {string} ghostElementId - The ID of the ghost element to position
|
||||
*/
|
||||
export const positionGroupGhostElement = (events, ghostElementId, gridWidth) => {
|
||||
if (!events || events.length === 0) return;
|
||||
|
||||
const boundingBox = calculateGroupBoundingBox(events.map((e) => e.target));
|
||||
const ghostElement = document.getElementById(ghostElementId);
|
||||
|
||||
if (!ghostElement || !boundingBox) return;
|
||||
ghostElement.style.width = `${boundingBox.width}px`;
|
||||
ghostElement.style.height = `${boundingBox.height}px`;
|
||||
ghostElement.style.willChange = 'transform';
|
||||
|
||||
ghostElement.style.transform = `translate(${boundingBox.left}px, ${boundingBox.top}px)`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the new parent ID based on the current mouse position during drag operations
|
||||
* @param {number} clientX - The X coordinate of the mouse position
|
||||
|
|
@ -631,3 +678,45 @@ export const clearActiveTargetClassNamesAfterSnapping = (selectedComponents) =>
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const updateDashedBordersOnHover = (targetId) => {
|
||||
const dynamicHeight = useStore.getState().checkHoveredComponentDynamicHeight(targetId);
|
||||
const targetMoveableBox = document.querySelector(`.moveable-control-box[target-id="${targetId}"]`);
|
||||
if (targetMoveableBox && dynamicHeight && !targetMoveableBox.classList.contains('moveable-dynamic-height')) {
|
||||
targetMoveableBox.classList.add('moveable-dynamic-height');
|
||||
} else if (targetMoveableBox && !dynamicHeight) {
|
||||
targetMoveableBox.classList.remove('moveable-dynamic-height');
|
||||
}
|
||||
};
|
||||
|
||||
// Check if dropping from modal to canvas, including nested containers within modals
|
||||
export const isDraggingModalToCanvas = (source, target, boxList) => {
|
||||
if (!source.isModal) return false;
|
||||
|
||||
// If target is the same as source, it's not modal to canvas
|
||||
if (source.id === target.id) return false;
|
||||
|
||||
// Check if target or any of its parents is a modal
|
||||
let currentTargetId = target.id;
|
||||
while (currentTargetId && currentTargetId !== 'canvas') {
|
||||
const targetComponent = boxList.find((b) => b.id === currentTargetId);
|
||||
if (!targetComponent) break;
|
||||
|
||||
// If we find a modal in the parent chain, it's not modal to canvas
|
||||
if (source.id === targetComponent.parent) return false;
|
||||
|
||||
currentTargetId = targetComponent.parent;
|
||||
}
|
||||
|
||||
// If we've reached canvas without finding a modal parent, it's modal to canvas
|
||||
return currentTargetId === 'canvas' || currentTargetId === null;
|
||||
};
|
||||
|
||||
export const updateDashedBordersOnDragResize = (targetId, moveableControlBoxClassList) => {
|
||||
const hasDynamicHeight = useStore.getState().checkHoveredComponentDynamicHeight(targetId);
|
||||
if (hasDynamicHeight && !moveableControlBoxClassList?.contains('moveable-dynamic-height')) {
|
||||
moveableControlBoxClassList?.add('moveable-dynamic-height');
|
||||
} else if (moveableControlBoxClassList?.contains('moveable-dynamic-height') && !hasDynamicHeight) {
|
||||
moveableControlBoxClassList?.remove('moveable-dynamic-height');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -212,20 +212,19 @@ export function dragContextBuilder({ event, widgets, isModuleEditor = false }) {
|
|||
* Given an event, finds the **nearest valid droppable slot**.
|
||||
*/
|
||||
export const getDroppableSlotIdOnScreen = (event, widgets) => {
|
||||
// Hack: This is a temporary solution. We need to find a better way to handle this.
|
||||
// We have added this solution to fix dragging widget not being correctly dropped when it is there is scroll
|
||||
const widgetType = getWidgetById(widgets, event.target.id)?.component?.component || CANVAS_ID;
|
||||
if (!DROPPABLE_PARENTS.has(widgetType) && widgetType !== 'ModuleViewer') {
|
||||
// TO:DO - Have to remove this condition for ModuleViewer
|
||||
if (widgetType !== 'ModuleViewer') {
|
||||
const targetElems = document.elementsFromPoint(event.clientX, event.clientY);
|
||||
const draggedOverElements = targetElems.filter(
|
||||
(ele) => (ele.id !== event.target.id && ele.classList.contains('target')) || ele.classList.contains('real-canvas')
|
||||
(ele) => ele.id.replace('canvas-', '')?.slice(0, 36) !== event.target.id && ele.classList.contains('real-canvas')
|
||||
);
|
||||
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
|
||||
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
|
||||
|
||||
// Determine potential new parent
|
||||
const newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
|
||||
return newParentId == 'canvas' ? undefined : newParentId;
|
||||
return newParentId === 'canvas' ? undefined : newParentId;
|
||||
} else {
|
||||
const [slotId] = document
|
||||
.elementsFromPoint(event.clientX, event.clientY)
|
||||
|
|
@ -238,6 +237,7 @@ export const getDroppableSlotIdOnScreen = (event, widgets) => {
|
|||
const widgetType = getWidgetById(widgets, slotId.slice(0, 36))?.component?.component || CANVAS_ID;
|
||||
return DROPPABLE_PARENTS.has(widgetType);
|
||||
});
|
||||
|
||||
return slotId;
|
||||
}
|
||||
};
|
||||
|
|
@ -267,9 +267,6 @@ const extractSlotId = (element) => {
|
|||
* @returns {Object} { left, top } - The computed position.
|
||||
*/
|
||||
export const getAdjustedDropPosition = (event, target, isParentChangeAllowed, gridWidth, dragged) => {
|
||||
let left = event.lastEvent?.translate[0];
|
||||
let top = event.lastEvent?.translate[1];
|
||||
|
||||
if (isParentChangeAllowed) {
|
||||
// Compute the relative position inside the new container
|
||||
const { left: adjustedLeft, top: adjustedTop } = getMouseDistanceFromParentDiv(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,403 @@
|
|||
import { useRef, useCallback, useEffect } from 'react';
|
||||
import { positionGhostElement } from '@/AppBuilder/AppCanvas/Grid/gridUtils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useGridStore } from '@/_stores/gridStore';
|
||||
import { GRID_HEIGHT } from '../../appCanvasConstants';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
threshold: 30, // Distance from edge to trigger scrolling (px)
|
||||
scrollSpeed: 10, // Scroll speed per frame (px)
|
||||
verticalContainerSelector: '.canvas-content',
|
||||
horizontalContainerSelector: '.canvas-container',
|
||||
canvasHeightIncrement: 50, // Pixels to increase canvas height when at bottom
|
||||
};
|
||||
|
||||
const RIGHT_SIDEBAR_WIDTH = 300; // Width of the right sidebar when open
|
||||
const LEFT_SIDEBAR_WIDTH_DEFAULT = 350; // Default width of the left sidebar when open
|
||||
|
||||
/**
|
||||
* Parse transform translate values from an element's transform style
|
||||
*/
|
||||
const parseTransform = (element) => {
|
||||
if (!element) return { x: 0, y: 0 };
|
||||
const transform = element.style.transform;
|
||||
const match = transform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/);
|
||||
if (match) {
|
||||
return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
|
||||
}
|
||||
return { x: 0, y: 0 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for auto-scrolling the canvas when dragging widgets near edges
|
||||
* Supports two modes:
|
||||
* - 'drag': Updates element position as canvas scrolls (for single widget drag operations)
|
||||
* - 'groupDrag': Updates multiple elements' positions as canvas scrolls (for group drag operations)
|
||||
*
|
||||
* @param {Object} config - Configuration options
|
||||
* @param {number} config.threshold - Distance from edge to trigger scrolling
|
||||
* @param {number} config.scrollSpeed - Pixels to scroll per animation frame
|
||||
* @param {string} config.verticalContainerSelector - CSS selector for vertical scroll container
|
||||
* @param {string} config.horizontalContainerSelector - CSS selector for horizontal scroll container
|
||||
* @param {number} config.canvasHeightIncrement - Pixels to increase canvas height when at bottom
|
||||
* @param {Array} boxList - List of widget boxes for height calculations
|
||||
* @param {HTMLElement} virtualTarget - Virtual target element (if any)
|
||||
* @param {React.RefObject} moveableRef - Reference to the Moveable instance
|
||||
* @returns {Object} - { startAutoScroll, stopAutoScroll, updateMousePosition, getScrollDelta }
|
||||
*/
|
||||
export const useCanvasAutoScroll = (config = {}, boxList = [], virtualTarget = null, moveableRef = null) => {
|
||||
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
|
||||
const isLeftSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
|
||||
|
||||
const { threshold, scrollSpeed, verticalContainerSelector, horizontalContainerSelector, canvasHeightIncrement } = {
|
||||
...DEFAULT_CONFIG,
|
||||
...config,
|
||||
};
|
||||
|
||||
const rafIdRef = useRef(null);
|
||||
const isScrollingRef = useRef(false);
|
||||
const mousePositionRef = useRef({ clientX: 0, clientY: 0 });
|
||||
const canvasHeightRef = useRef(null);
|
||||
const scrollDeltaRef = useRef({ x: 0, y: 0 }); // Cumulative scroll delta
|
||||
const targetElementRef = useRef(null); // Track the dragged element (single)
|
||||
const targetElementsRef = useRef([]); // Track multiple dragged elements (for group drag)
|
||||
const modeRef = useRef('drag'); // 'drag' or 'groupDrag' - controls position update behavior
|
||||
|
||||
/**
|
||||
* Check if widget(s) are near the bottom of canvas and need to extend it
|
||||
* Supports both single element and group drag modes
|
||||
*/
|
||||
const extendCanvasIfNeeded = useCallback(
|
||||
(container, scrollY) => {
|
||||
if (scrollY <= 0) return; // Only extend when scrolling down
|
||||
|
||||
const realCanvas = document.getElementById('real-canvas');
|
||||
if (!realCanvas) return;
|
||||
|
||||
const isGroupDragMode = modeRef.current === 'groupDrag';
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const canvasHeight = realCanvas.offsetHeight;
|
||||
|
||||
let maxWidgetBottomOnCanvas = 0;
|
||||
let maxComponentHeight = 0;
|
||||
|
||||
if (isGroupDragMode) {
|
||||
// For group drag, find the maximum bottom position across all elements
|
||||
const elements = targetElementsRef.current;
|
||||
if (!elements || elements.length === 0) return;
|
||||
|
||||
elements.forEach((element) => {
|
||||
if (!element) return;
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const widgetBottomOnCanvas = container.scrollTop + (elementRect.bottom - containerRect.top);
|
||||
maxWidgetBottomOnCanvas = Math.max(maxWidgetBottomOnCanvas, widgetBottomOnCanvas);
|
||||
|
||||
// Get the height of this component for extension calculation
|
||||
const componentHeight = boxList.find((box) => box.id === element.id)?.height || 0;
|
||||
maxComponentHeight = Math.max(maxComponentHeight, componentHeight);
|
||||
});
|
||||
} else {
|
||||
// Single element mode
|
||||
const element = targetElementRef.current;
|
||||
const elementRect = element?.getBoundingClientRect();
|
||||
|
||||
if (!elementRect) return;
|
||||
|
||||
maxComponentHeight = boxList.find((box) => box.id === element?.id)?.height || 0;
|
||||
maxWidgetBottomOnCanvas = container.scrollTop + (elementRect.bottom - containerRect.top);
|
||||
}
|
||||
|
||||
// Check if widget's (or group's) bottom is approaching the canvas bottom
|
||||
const isNearBottom = maxWidgetBottomOnCanvas >= canvasHeight - threshold;
|
||||
|
||||
if (isNearBottom) {
|
||||
// Get current canvas height and increase it
|
||||
const currentHeight = realCanvas.offsetHeight;
|
||||
const newHeight = currentHeight + maxComponentHeight + canvasHeightIncrement;
|
||||
|
||||
// Store the increased height
|
||||
if (!canvasHeightRef.current) {
|
||||
canvasHeightRef.current = currentHeight;
|
||||
}
|
||||
|
||||
// Directly set the height on the canvas element for immediate effect
|
||||
realCanvas.style.height = `${newHeight}px`;
|
||||
}
|
||||
},
|
||||
[threshold, canvasHeightIncrement, boxList]
|
||||
);
|
||||
|
||||
/**
|
||||
* Immediately update the dragged element's position when scrolling
|
||||
* This provides smooth UX even when mouse is stationary
|
||||
*/
|
||||
const updateElementPositionOnScroll = useCallback((scrollX, scrollY) => {
|
||||
const element = targetElementRef.current;
|
||||
if (!element) return;
|
||||
|
||||
// Get canvas bounds for clamping
|
||||
const realCanvas = document.getElementById('real-canvas');
|
||||
if (!realCanvas) return;
|
||||
|
||||
const canvasWidth = realCanvas.clientWidth;
|
||||
const elementWidth = element.clientWidth;
|
||||
const _gridWidth = useGridStore.getState().subContainerWidths['canvas'];
|
||||
// Get current transform and add scroll delta
|
||||
const currentPos = parseTransform(element);
|
||||
let newX = currentPos.x;
|
||||
let newY = currentPos.y;
|
||||
newX = Math.round(newX / _gridWidth) * _gridWidth + scrollX;
|
||||
newY = Math.round(newY / GRID_HEIGHT) * GRID_HEIGHT + scrollY;
|
||||
|
||||
// Clamp position to stay within canvas bounds
|
||||
// Left bound: newX >= 0
|
||||
// Top bound: newY >= 0
|
||||
// Right bound: newX <= canvasWidth - elementWidth
|
||||
newX = Math.max(0, Math.min(newX, canvasWidth - elementWidth));
|
||||
newY = Math.max(0, newY);
|
||||
|
||||
// Update element transform immediately
|
||||
element.style.transform = `translate(${newX}px, ${newY}px)`;
|
||||
positionGhostElement(element, 'moveable-ghost-widget');
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Core scroll logic - checks MOUSE POSITION and scrolls if near viewport edges
|
||||
* Note: Vertical scroll is on .canvas-content, horizontal scroll is on .canvas-container
|
||||
* Supports drag and groupDrag modes
|
||||
*/
|
||||
const scrollIfNeeded = useCallback(() => {
|
||||
const verticalContainer = document.querySelector(verticalContainerSelector);
|
||||
const horizontalContainer = document.querySelector(horizontalContainerSelector);
|
||||
if ((!verticalContainer && !horizontalContainer) || !isScrollingRef.current) return;
|
||||
|
||||
const { clientX, clientY } = mousePositionRef.current;
|
||||
|
||||
const verticalRect = verticalContainer?.getBoundingClientRect();
|
||||
const horizontalRect = horizontalContainer?.getBoundingClientRect();
|
||||
|
||||
let scrollX = 0;
|
||||
let scrollY = 0;
|
||||
|
||||
// Check vertical boundaries using MOUSE POSITION (on .canvas-content)
|
||||
if (verticalContainer && clientY <= verticalRect.top + threshold) {
|
||||
// Mouse within threshold of top edge - scroll up
|
||||
const remainingTopScroll = Math.floor(verticalContainer.scrollTop);
|
||||
if (remainingTopScroll > 1) {
|
||||
scrollY = -Math.min(scrollSpeed, remainingTopScroll);
|
||||
}
|
||||
} else if (verticalContainer && clientY >= verticalRect.bottom - threshold) {
|
||||
// Mouse within threshold of bottom edge - scroll down
|
||||
scrollY = scrollSpeed;
|
||||
}
|
||||
|
||||
// Check horizontal boundaries using MOUSE POSITION (on .canvas-container)
|
||||
if (horizontalContainer) {
|
||||
let leftBoundary = horizontalRect.left + threshold;
|
||||
if (isLeftSidebarOpen) {
|
||||
const leftSidebar = document.querySelector('.left-sidebar-scrollbar');
|
||||
if (leftSidebar) {
|
||||
const leftSidebarRect = leftSidebar.getBoundingClientRect();
|
||||
leftBoundary = Math.max(leftSidebarRect.right + threshold, leftBoundary);
|
||||
} else {
|
||||
leftBoundary = Math.max(LEFT_SIDEBAR_WIDTH_DEFAULT + threshold, leftBoundary);
|
||||
}
|
||||
}
|
||||
|
||||
if (clientX <= leftBoundary) {
|
||||
// Mouse within threshold of left boundary - scroll left
|
||||
const remainingLeftScroll = Math.floor(horizontalContainer.scrollLeft);
|
||||
if (remainingLeftScroll > 1) {
|
||||
scrollX = -Math.min(scrollSpeed, remainingLeftScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (horizontalContainer && scrollX === 0) {
|
||||
let rightBoundary = horizontalRect.right - threshold;
|
||||
|
||||
if (isRightSidebarOpen) {
|
||||
const sidebar = document.querySelector('.editor-sidebar');
|
||||
if (sidebar) {
|
||||
const sidebarRect = sidebar.getBoundingClientRect();
|
||||
rightBoundary = sidebarRect.left - threshold;
|
||||
} else {
|
||||
rightBoundary = horizontalRect.right - RIGHT_SIDEBAR_WIDTH - threshold;
|
||||
}
|
||||
}
|
||||
|
||||
if (clientX >= rightBoundary) {
|
||||
// Mouse within threshold of right boundary - scroll right
|
||||
const maxScrollLeft = horizontalContainer.scrollWidth - horizontalContainer.clientWidth;
|
||||
const remainingScroll = Math.floor(maxScrollLeft - horizontalContainer.scrollLeft);
|
||||
if (remainingScroll > 1) {
|
||||
scrollX = Math.min(scrollSpeed, remainingScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform vertical scroll if needed
|
||||
if (scrollY !== 0 && verticalContainer) {
|
||||
extendCanvasIfNeeded(verticalContainer, scrollY);
|
||||
|
||||
const scrollTopBefore = verticalContainer.scrollTop;
|
||||
verticalContainer.scrollBy({ top: scrollY });
|
||||
const actualScrollY = verticalContainer.scrollTop - scrollTopBefore;
|
||||
|
||||
if (actualScrollY !== 0) {
|
||||
scrollDeltaRef.current.y += actualScrollY;
|
||||
// Update element position(s) based on mode
|
||||
if (modeRef.current === 'drag') {
|
||||
updateElementPositionOnScroll(0, actualScrollY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform horizontal scroll if needed
|
||||
if (scrollX !== 0 && horizontalContainer) {
|
||||
const scrollLeftBefore = horizontalContainer.scrollLeft;
|
||||
horizontalContainer.scrollBy({ left: scrollX });
|
||||
const actualScrollX = horizontalContainer.scrollLeft - scrollLeftBefore;
|
||||
|
||||
if (actualScrollX !== 0) {
|
||||
scrollDeltaRef.current.x += actualScrollX;
|
||||
// Update element position(s) based on mode
|
||||
if (modeRef.current === 'drag') {
|
||||
updateElementPositionOnScroll(actualScrollX, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update Moveable rect if any scrolling occurred
|
||||
if ((scrollX !== 0 || scrollY !== 0) && moveableRef?.current) {
|
||||
moveableRef.current.updateRect();
|
||||
}
|
||||
|
||||
// Continue animation loop while dragging
|
||||
if (isScrollingRef.current) {
|
||||
rafIdRef.current = requestAnimationFrame(scrollIfNeeded);
|
||||
}
|
||||
}, [
|
||||
verticalContainerSelector,
|
||||
threshold,
|
||||
scrollSpeed,
|
||||
moveableRef,
|
||||
extendCanvasIfNeeded,
|
||||
updateElementPositionOnScroll,
|
||||
isRightSidebarOpen,
|
||||
isLeftSidebarOpen,
|
||||
horizontalContainerSelector,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Update mouse position and target element(s)
|
||||
* Call this from onDrag/onDragGroup handlers
|
||||
* @param {number} clientX - Mouse X position
|
||||
* @param {number} clientY - Mouse Y position
|
||||
* @param {HTMLElement|HTMLElement[]} target - The element(s) being dragged (single or array for group)
|
||||
*/
|
||||
const updateMousePosition = useCallback(
|
||||
(clientX, clientY, target = null) => {
|
||||
mousePositionRef.current = { clientX, clientY };
|
||||
|
||||
// Update target(s) if provided
|
||||
if (target) {
|
||||
if (Array.isArray(target)) {
|
||||
// Group mode - update all targets
|
||||
targetElementsRef.current = target;
|
||||
targetElementRef.current = target[0] || null;
|
||||
} else {
|
||||
// Single element mode
|
||||
targetElementRef.current = target;
|
||||
}
|
||||
}
|
||||
|
||||
// Start scroll loop if not already running
|
||||
if (isScrollingRef.current && !rafIdRef.current) {
|
||||
rafIdRef.current = requestAnimationFrame(scrollIfNeeded);
|
||||
}
|
||||
},
|
||||
[scrollIfNeeded]
|
||||
);
|
||||
|
||||
/**
|
||||
* Start auto-scroll monitoring
|
||||
* Call this from onDragStart or onDragGroupStart handlers
|
||||
* @param {number} clientX - Mouse X position
|
||||
* @param {number} clientY - Mouse Y position
|
||||
* @param {HTMLElement|HTMLElement[]} target - The element(s) being dragged (single element or array for group)
|
||||
* @param {string} mode - 'drag' or 'groupDrag' - controls position update behavior
|
||||
*/
|
||||
const startAutoScroll = useCallback(
|
||||
(clientX, clientY, target = null, mode = 'drag') => {
|
||||
isScrollingRef.current = true;
|
||||
mousePositionRef.current = { clientX, clientY };
|
||||
scrollDeltaRef.current = { x: 0, y: 0 }; // Reset delta on start
|
||||
modeRef.current = mode === 'groupDrag' ? 'groupDrag' : 'drag'; // Normalize mode for this scroll session
|
||||
|
||||
// Store the target element(s)
|
||||
if (target) {
|
||||
if (Array.isArray(target)) {
|
||||
// Group drag mode - store all targets
|
||||
targetElementsRef.current = target;
|
||||
// Also set first element as primary target for compatibility
|
||||
targetElementRef.current = target[0] || null;
|
||||
} else {
|
||||
// Single element mode
|
||||
targetElementRef.current = target;
|
||||
targetElementsRef.current = [];
|
||||
}
|
||||
}
|
||||
|
||||
rafIdRef.current = requestAnimationFrame(scrollIfNeeded);
|
||||
},
|
||||
[scrollIfNeeded]
|
||||
);
|
||||
|
||||
/**
|
||||
* Stop auto-scroll monitoring
|
||||
* Call this from onDragEnd/onDragGroupEnd handlers
|
||||
*/
|
||||
const stopAutoScroll = useCallback(() => {
|
||||
isScrollingRef.current = false;
|
||||
if (rafIdRef.current) {
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
rafIdRef.current = null;
|
||||
}
|
||||
|
||||
// Reset the inline canvas height style - the actual height will be
|
||||
// recalculated by updateCanvasBottomHeight via incrementCanvasUpdater
|
||||
const realCanvas = document.getElementById('real-canvas');
|
||||
if (realCanvas && canvasHeightRef.current) {
|
||||
canvasHeightRef.current = null;
|
||||
}
|
||||
|
||||
// Reset refs
|
||||
scrollDeltaRef.current = { x: 0, y: 0 };
|
||||
targetElementRef.current = null;
|
||||
targetElementsRef.current = []; // Reset group targets
|
||||
modeRef.current = 'drag'; // Reset mode to default
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!virtualTarget) {
|
||||
stopAutoScroll();
|
||||
}
|
||||
}, [virtualTarget, stopAutoScroll]);
|
||||
|
||||
/**
|
||||
* Get current accumulated scroll delta
|
||||
* Use this in onDrag to adjust widget position
|
||||
*/
|
||||
const getScrollDelta = useCallback(() => {
|
||||
return { ...scrollDeltaRef.current };
|
||||
}, []);
|
||||
|
||||
return {
|
||||
startAutoScroll,
|
||||
stopAutoScroll,
|
||||
updateMousePosition,
|
||||
getScrollDelta,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { findHighestLevelofSelection } from '../gridUtils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
function isInViewport(el, root = window) {
|
||||
if (!el) return false;
|
||||
|
|
@ -18,9 +16,165 @@ function isInViewport(el, root = window) {
|
|||
elementRect.left < containerRect.right // element left left-of container's visible right
|
||||
);
|
||||
}
|
||||
|
||||
// Simple rule: Never filter extremes in any direction (top, bottom, left, right)
|
||||
function getEssentialElements(boxes, selectedComponentIds = []) {
|
||||
if (boxes.length <= 2) {
|
||||
return boxes.map((box) => `.ele-${box.id}`);
|
||||
}
|
||||
|
||||
const nonSelectedBoxes = boxes.filter((box) => !selectedComponentIds.includes(box.id));
|
||||
|
||||
if (nonSelectedBoxes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Find global extremes in all 4 directions
|
||||
const minLeft = Math.min(...nonSelectedBoxes.map((box) => box.left));
|
||||
const maxRight = Math.max(...nonSelectedBoxes.map((box) => box.left + box.width));
|
||||
const minTop = Math.min(...nonSelectedBoxes.map((box) => box.top));
|
||||
const maxBottom = Math.max(...nonSelectedBoxes.map((box) => box.top + box.height));
|
||||
|
||||
const essentialElements = new Set();
|
||||
|
||||
// Step 1: Keep global extremes
|
||||
nonSelectedBoxes.forEach((box) => {
|
||||
const isLeftExtreme = box.left === minLeft;
|
||||
const isRightExtreme = box.left + box.width === maxRight;
|
||||
const isTopExtreme = box.top === minTop;
|
||||
const isBottomExtreme = box.top + box.height === maxBottom;
|
||||
|
||||
if (isLeftExtreme || isRightExtreme || isTopExtreme || isBottomExtreme) {
|
||||
essentialElements.add(box.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: Keep local extremes for each alignment line
|
||||
|
||||
// For each unique TOP position, keep leftmost and rightmost elements
|
||||
const topGroups = new Map();
|
||||
nonSelectedBoxes.forEach((box) => {
|
||||
if (!topGroups.has(box.top)) topGroups.set(box.top, []);
|
||||
topGroups.get(box.top).push(box);
|
||||
});
|
||||
|
||||
topGroups.forEach((elements, topPos) => {
|
||||
if (elements.length > 1) {
|
||||
const minLeft = Math.min(...elements.map((box) => box.left));
|
||||
const maxRight = Math.max(...elements.map((box) => box.left + box.width));
|
||||
|
||||
elements.forEach((box) => {
|
||||
if (box.left === minLeft || box.left + box.width === maxRight) {
|
||||
if (!essentialElements.has(box.id)) {
|
||||
essentialElements.add(box.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// For each unique BOTTOM position, keep leftmost and rightmost elements
|
||||
const bottomGroups = new Map();
|
||||
nonSelectedBoxes.forEach((box) => {
|
||||
const bottom = box.top + box.height;
|
||||
if (!bottomGroups.has(bottom)) bottomGroups.set(bottom, []);
|
||||
bottomGroups.get(bottom).push(box);
|
||||
});
|
||||
|
||||
bottomGroups.forEach((elements, bottomPos) => {
|
||||
if (elements.length > 1) {
|
||||
const minLeft = Math.min(...elements.map((box) => box.left));
|
||||
const maxRight = Math.max(...elements.map((box) => box.left + box.width));
|
||||
|
||||
elements.forEach((box) => {
|
||||
if (box.left === minLeft || box.left + box.width === maxRight) {
|
||||
if (!essentialElements.has(box.id)) {
|
||||
essentialElements.add(box.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const leftGroups = new Map();
|
||||
nonSelectedBoxes.forEach((box) => {
|
||||
if (!leftGroups.has(box.left)) leftGroups.set(box.left, []);
|
||||
leftGroups.get(box.left).push(box);
|
||||
});
|
||||
|
||||
leftGroups.forEach((elements, leftPos) => {
|
||||
if (elements.length > 1) {
|
||||
const minTop = Math.min(...elements.map((box) => box.top));
|
||||
const maxBottom = Math.max(...elements.map((box) => box.top + box.height));
|
||||
|
||||
elements.forEach((box) => {
|
||||
if (box.top === minTop || box.top + box.height === maxBottom) {
|
||||
if (!essentialElements.has(box.id)) {
|
||||
essentialElements.add(box.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// For each unique RIGHT position, keep topmost and bottommost elements
|
||||
|
||||
const rightGroups = new Map();
|
||||
nonSelectedBoxes.forEach((box) => {
|
||||
const right = box.left + box.width;
|
||||
if (!rightGroups.has(right)) rightGroups.set(right, []);
|
||||
rightGroups.get(right).push(box);
|
||||
});
|
||||
|
||||
rightGroups.forEach((elements, rightPos) => {
|
||||
if (elements.length > 1) {
|
||||
const minTop = Math.min(...elements.map((box) => box.top));
|
||||
const maxBottom = Math.max(...elements.map((box) => box.top + box.height));
|
||||
|
||||
elements.forEach((box) => {
|
||||
if (box.top === minTop || box.top + box.height === maxBottom) {
|
||||
if (!essentialElements.has(box.id)) {
|
||||
essentialElements.add(box.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// For remaining elements, keep those that provide unique alignment opportunities
|
||||
const remainingElements = nonSelectedBoxes.filter((box) => !essentialElements.has(box.id));
|
||||
|
||||
// Collect all unique edge positions from kept extremes
|
||||
const keptElements = nonSelectedBoxes.filter((box) => essentialElements.has(box.id));
|
||||
const keptLeftEdges = new Set(keptElements.map((box) => box.left));
|
||||
const keptRightEdges = new Set(keptElements.map((box) => box.left + box.width));
|
||||
const keptTopEdges = new Set(keptElements.map((box) => box.top));
|
||||
const keptBottomEdges = new Set(keptElements.map((box) => box.top + box.height));
|
||||
|
||||
// Keep remaining elements that provide unique alignment opportunities
|
||||
remainingElements.forEach((box) => {
|
||||
const providesUniqueLeft = !keptLeftEdges.has(box.left);
|
||||
const providesUniqueRight = !keptRightEdges.has(box.left + box.width);
|
||||
const providesUniqueTop = !keptTopEdges.has(box.top);
|
||||
const providesUniqueBottom = !keptBottomEdges.has(box.top + box.height);
|
||||
|
||||
if (providesUniqueLeft || providesUniqueRight || providesUniqueTop || providesUniqueBottom) {
|
||||
essentialElements.add(box.id);
|
||||
|
||||
// Update kept edges
|
||||
keptLeftEdges.add(box.left);
|
||||
keptRightEdges.add(box.left + box.width);
|
||||
keptTopEdges.add(box.top);
|
||||
keptBottomEdges.add(box.top + box.height);
|
||||
}
|
||||
});
|
||||
|
||||
const result = Array.from(essentialElements).map((id) => `.ele-${id}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const useElementGuidelines = (boxList, selectedComponents, getResolvedValue, virtualTarget) => {
|
||||
const [elementGuidelines, setElementGuidelines] = useState([]);
|
||||
// const draggingComponentId = useStore((state) => state.draggingComponentId, shallow);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedSet = new Set(selectedComponents);
|
||||
|
|
@ -30,38 +184,31 @@ export const useElementGuidelines = (boxList, selectedComponents, getResolvedVal
|
|||
const selectedParent = firstSelectedParent;
|
||||
const isAnyModalOpen = document.querySelector('#modal-container') ? true : false;
|
||||
|
||||
const guidelines = boxList
|
||||
.filter((box) => {
|
||||
const isVisible =
|
||||
getResolvedValue(box?.component?.definition?.properties?.visibility?.value) ||
|
||||
getResolvedValue(box?.component?.definition?.styles?.visibility?.value);
|
||||
const filteredBoxes = boxList.filter((box) => {
|
||||
const isVisible =
|
||||
getResolvedValue(box?.component?.definition?.properties?.visibility?.value) ||
|
||||
getResolvedValue(box?.component?.definition?.styles?.visibility?.value);
|
||||
|
||||
// Early return for non-visible elements
|
||||
if (!isVisible) return false;
|
||||
if (!isVisible) return false;
|
||||
if (!virtualTarget && selectedSet.has(box.id)) return false;
|
||||
|
||||
// // If component is selected, don't show its guidelines
|
||||
if (!virtualTarget && selectedSet.has(box.id)) return false;
|
||||
if (isAnyModalOpen) {
|
||||
if (box.parent === 'canvas' || !box.parent) return false;
|
||||
}
|
||||
|
||||
// If component is a child of the dragging component, don't show its guidelines
|
||||
// if (box.parent?.slice(0, 36) === draggingComponentId?.slice(0, 36)) return false;
|
||||
if (isGrouped) {
|
||||
if (selectedSet.has(box.id)) return false;
|
||||
return selectedParent ? box.parent === selectedParent : !box.parent;
|
||||
}
|
||||
|
||||
// Don't show guidelines for components which are outside the modal specially on main canvas
|
||||
if (isAnyModalOpen) {
|
||||
if (box.parent === 'canvas' || !box.parent) return false;
|
||||
}
|
||||
const element = document.querySelector(`.ele-${box.id}`);
|
||||
const container = document.getElementsByClassName('canvas-content')?.[0];
|
||||
if (!element) return false;
|
||||
return isInViewport(element, container);
|
||||
});
|
||||
|
||||
if (isGrouped) {
|
||||
// If component is selected, don't show its guidelines
|
||||
if (selectedSet.has(box.id)) return false;
|
||||
return selectedParent ? box.parent === selectedParent : !box.parent;
|
||||
}
|
||||
|
||||
const element = document.querySelector(`.ele-${box.id}`);
|
||||
const container = document.getElementsByClassName('canvas-content')?.[0];
|
||||
if (!element) return false;
|
||||
return isInViewport(element, container);
|
||||
})
|
||||
.map((box) => `.ele-${box.id}`);
|
||||
// OPTIMIZATION: Only keep essential elements for guidelines
|
||||
const guidelines = getEssentialElements(filteredBoxes, selectedComponents);
|
||||
setElementGuidelines(guidelines);
|
||||
}, [boxList, selectedComponents, getResolvedValue, virtualTarget]);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import useKeyHooks from '@/_hooks/useKeyHooks';
|
|||
import { shallow } from 'zustand/shallow';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }) => {
|
||||
export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth, isModuleMode }) => {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const canvasRef = useRef(null);
|
||||
const focusedParentId = useStore((state) => state.focusedParentId, shallow);
|
||||
|
|
@ -138,6 +138,7 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }
|
|||
maxWidth: canvasMaxWidth,
|
||||
margin: '0 auto',
|
||||
transform: 'translateZ(0)',
|
||||
...(isModuleMode && { height: '100%' }),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const NoComponentCanvasContainer = () => {
|
|||
const createDataQuery = useStore((state) => state.dataQuery.createDataQuery, shallow);
|
||||
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData, shallow);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const expandQueryPaneIfNeeded = useStore((state) => state.queryPanel.expandQueryPaneIfNeeded);
|
||||
|
||||
const queryBoxText = sampleDataSource
|
||||
? 'Connect to your data source or use our sample data source to start playing around!'
|
||||
|
|
@ -28,6 +29,7 @@ const NoComponentCanvasContainer = () => {
|
|||
const handleConnectSampleDB = () => {
|
||||
const source = sampleDataSource;
|
||||
const query = `SELECT tablename \nFROM pg_catalog.pg_tables \nWHERE schemaname='public';`;
|
||||
expandQueryPaneIfNeeded();
|
||||
createDataQuery(source, true, { query });
|
||||
setPreviewData(null);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,7 +36,15 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [
|
|||
'Form',
|
||||
'FilePicker',
|
||||
'Tabs',
|
||||
'RangeSliderV2'
|
||||
'RangeSliderV2',
|
||||
'Statistics',
|
||||
'StarRating',
|
||||
'PopoverMenu',
|
||||
'Tags',
|
||||
'CircularProgressBar',
|
||||
'Kanban',
|
||||
'AudioRecorder',
|
||||
'Camera',
|
||||
];
|
||||
|
||||
const RenderWidget = ({
|
||||
|
|
@ -50,17 +58,23 @@ const RenderWidget = ({
|
|||
inCanvas = false,
|
||||
darkMode,
|
||||
moduleId,
|
||||
currentMode,
|
||||
}) => {
|
||||
const component = useStore((state) => state.getComponentDefinition(id, moduleId)?.component, shallow);
|
||||
const getDefaultStyles = useStore((state) => state.debugger.getDefaultStyles, shallow);
|
||||
const adjustComponentPositions = useStore((state) => state.adjustComponentPositions, shallow);
|
||||
const componentCount = useStore((state) => state.getContainerChildrenMapping(id)?.length || 0, shallow);
|
||||
const getExposedPropertyForAdditionalActions = useStore(
|
||||
(state) => state.getExposedPropertyForAdditionalActions,
|
||||
shallow
|
||||
);
|
||||
const componentName = component?.name;
|
||||
const [key, setKey] = useState(Math.random());
|
||||
const resolvedProperties = useStore(
|
||||
(state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.properties,
|
||||
shallow
|
||||
);
|
||||
|
||||
const resolvedStyles = useStore(
|
||||
(state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.styles,
|
||||
shallow
|
||||
|
|
@ -95,18 +109,26 @@ const RenderWidget = ({
|
|||
|
||||
const isDisabled = useStore((state) => {
|
||||
const component = state.getResolvedComponent(id, subContainerIndex, moduleId);
|
||||
const componentExposedDisabled = state.getExposedValueOfComponent(id, moduleId)?.isDisabled;
|
||||
if (typeof componentExposedDisabled === 'boolean') return componentExposedDisabled;
|
||||
if (component?.properties?.disabledState === true || component?.styles?.disabledState === true) return true;
|
||||
return false;
|
||||
const componentExposedDisabled = getExposedPropertyForAdditionalActions(
|
||||
id,
|
||||
subContainerIndex,
|
||||
'isDisabled',
|
||||
moduleId
|
||||
);
|
||||
if (componentExposedDisabled !== undefined) return componentExposedDisabled;
|
||||
return component?.properties?.disabledState || component?.styles?.disabledState;
|
||||
});
|
||||
|
||||
const isLoading = useStore((state) => {
|
||||
const component = state.getResolvedComponent(id, subContainerIndex, moduleId);
|
||||
const componentExposedLoading = state.getExposedValueOfComponent(id, moduleId)?.isLoading;
|
||||
if (typeof componentExposedLoading === 'boolean') return componentExposedLoading;
|
||||
if (component?.properties?.loadingState === true || component?.styles?.loadingState === true) return true;
|
||||
return false;
|
||||
const componentExposedLoading = getExposedPropertyForAdditionalActions(
|
||||
id,
|
||||
subContainerIndex,
|
||||
'isLoading',
|
||||
moduleId
|
||||
);
|
||||
if (componentExposedLoading !== undefined) return componentExposedLoading;
|
||||
return component?.properties?.loadingState || component?.styles?.loadingState;
|
||||
});
|
||||
|
||||
const obj = {
|
||||
|
|
@ -121,11 +143,11 @@ const RenderWidget = ({
|
|||
validateWidget({
|
||||
...{ widgetValue: value },
|
||||
...{ validationObject: unResolvedValidation },
|
||||
customResolveObjects: customResolvables,
|
||||
customResolveObjects: customResolvables?.[subContainerIndex] ?? {},
|
||||
componentType,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[validateWidget, customResolvables, unResolvedValidation, resolvedValidation]
|
||||
[validateWidget, customResolvables, subContainerIndex, unResolvedValidation, resolvedValidation, moduleId]
|
||||
);
|
||||
|
||||
const resetComponent = useCallback(() => {
|
||||
|
|
@ -135,12 +157,12 @@ const RenderWidget = ({
|
|||
const ComponentToRender = useMemo(() => getComponentToRender(componentType), [componentType]);
|
||||
const setExposedVariable = useCallback(
|
||||
(key, value) => {
|
||||
setExposedValue(id, key, value, moduleId);
|
||||
// Trigger an update when the child components is directly linked to any component
|
||||
updateDependencyValues(`components.${id}.${key}`, moduleId);
|
||||
|
||||
// Check if the component is inside the subcontainer and it has its own onOptionChange(setExposedValue) function
|
||||
if (onOptionChange === null) {
|
||||
setExposedValue(id, key, value, moduleId);
|
||||
// Trigger an update when the child components is directly linked to any component
|
||||
updateDependencyValues(`components.${id}.${key}`, moduleId);
|
||||
} else {
|
||||
if (onOptionChange !== null) {
|
||||
onOptionChange(key, value, id, subContainerIndex);
|
||||
}
|
||||
},
|
||||
|
|
@ -148,9 +170,9 @@ const RenderWidget = ({
|
|||
);
|
||||
const setExposedVariables = useCallback(
|
||||
(exposedValues) => {
|
||||
if (onOptionsChange === null) {
|
||||
setExposedValues(id, 'components', exposedValues, moduleId);
|
||||
} else {
|
||||
setExposedValues(id, 'components', exposedValues, moduleId);
|
||||
|
||||
if (onOptionsChange !== null) {
|
||||
onOptionsChange(exposedValues, id, subContainerIndex);
|
||||
}
|
||||
},
|
||||
|
|
@ -182,17 +204,18 @@ const RenderWidget = ({
|
|||
? null
|
||||
: ['hover', 'focus']
|
||||
: !resolvedGeneralProperties?.tooltip?.toString().trim()
|
||||
? null
|
||||
: ['hover', 'focus']
|
||||
? null
|
||||
: ['hover', 'focus']
|
||||
}
|
||||
overlay={(props) =>
|
||||
renderTooltip({
|
||||
props,
|
||||
text: inCanvas
|
||||
? `${SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY.includes(component?.component)
|
||||
? resolvedProperties?.tooltip
|
||||
: resolvedGeneralProperties?.tooltip
|
||||
}`
|
||||
? `${
|
||||
SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY.includes(component?.component)
|
||||
? resolvedProperties?.tooltip
|
||||
: resolvedGeneralProperties?.tooltip
|
||||
}`
|
||||
: `${t(`widget.${component?.name}.description`, component?.description)}`,
|
||||
})
|
||||
}
|
||||
|
|
@ -202,9 +225,13 @@ const RenderWidget = ({
|
|||
height: '100%',
|
||||
padding: resolvedStyles?.padding == 'none' ? '0px' : `${BOX_PADDING}px`, //chart and image has a padding property other than container padding
|
||||
}}
|
||||
role={'Box'}
|
||||
className={`canvas-component ${inCanvas ? `_tooljet-${component?.component} _tooljet-${component?.name}` : ''
|
||||
} ${!['Modal', 'ModalV2'].includes(component.component) && (isDisabled || isLoading) ? 'disabled' : ''}`} //required for custom CSS
|
||||
className={`canvas-component ${
|
||||
inCanvas ? `_tooljet-${component?.component} _tooljet-${component?.name}` : ''
|
||||
} ${
|
||||
!['Modal', 'ModalV2', 'CircularProgressBar'].includes(component.component) && (isDisabled || isLoading)
|
||||
? 'disabled'
|
||||
: ''
|
||||
}`} //required for custom CSS
|
||||
>
|
||||
<ComponentToRender
|
||||
id={id}
|
||||
|
|
@ -224,6 +251,8 @@ const RenderWidget = ({
|
|||
adjustComponentPositions={adjustComponentPositions}
|
||||
componentCount={componentCount}
|
||||
dataCy={`draggable-widget-${componentName}`}
|
||||
currentMode={currentMode}
|
||||
subContainerIndex={subContainerIndex}
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useCallback, useRef } from 'react';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import Selecto from 'react-selecto';
|
||||
import './selecto.scss';
|
||||
import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { findHighestLevelofSelection } from './Grid/gridUtils';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
|
@ -78,9 +77,23 @@ export const EditorSelecto = () => {
|
|||
? [...getSelectedComponents().filter((id) => !allSelectedIds.includes(id)), ...allSelectedIds]
|
||||
: allSelectedIds;
|
||||
|
||||
setSelectedComponents(
|
||||
!isCanvasSelectStartEndSame ? newSelection : filterSelectedComponentsByHighestLevel(newSelection)
|
||||
);
|
||||
const isCanvasModal =
|
||||
getComponentDefinition(canvasStartId.current, moduleId)?.component?.component === 'Modal' ||
|
||||
getComponentDefinition(canvasStartId.current, moduleId)?.component?.component === 'ModalV2';
|
||||
|
||||
const _selectedComponents = !isCanvasSelectStartEndSame
|
||||
? newSelection
|
||||
: filterSelectedComponentsByHighestLevel(newSelection);
|
||||
|
||||
if (isCanvasModal) {
|
||||
setSelectedComponents(
|
||||
_selectedComponents.filter(
|
||||
(id) => getComponentDefinition(id, moduleId)?.component?.parent === canvasStartId.current
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setSelectedComponents(_selectedComponents);
|
||||
}
|
||||
}
|
||||
canvasStartId.current = null;
|
||||
},
|
||||
|
|
@ -119,6 +132,7 @@ export const EditorSelecto = () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[setSelectedComponents, setActiveRightSideBarTab, getSelectedComponents]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import React, { memo } from 'react';
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { ConfigHandle } from './ConfigHandle/ConfigHandle';
|
||||
import cx from 'classnames';
|
||||
import RenderWidget from './RenderWidget';
|
||||
import { NO_OF_GRIDS } from './appCanvasConstants';
|
||||
import { isTruthyOrZero } from '@/_helpers/appUtils';
|
||||
|
||||
const DYNAMIC_HEIGHT_AUTO_LIST = ['CodeEditor', 'Listview', 'TextArea', 'TagsInput'];
|
||||
|
||||
const WidgetWrapper = memo(
|
||||
({
|
||||
|
|
@ -22,15 +25,24 @@ const WidgetWrapper = memo(
|
|||
parentId,
|
||||
}) => {
|
||||
const calculateMoveableBoxHeightWithId = useStore((state) => state.calculateMoveableBoxHeightWithId, shallow);
|
||||
const incrementCanvasUpdater = useStore((state) => state.incrementCanvasUpdater, shallow);
|
||||
const stylesDefinition = useStore(
|
||||
(state) => state.getComponentDefinition(id, moduleId)?.component?.definition?.styles,
|
||||
shallow
|
||||
);
|
||||
const layoutData = useStore(
|
||||
(state) => state.getComponentDefinition(id, moduleId)?.layouts?.[currentLayout],
|
||||
const layoutData = useStore((state) => state.getComponentDefinition(id, moduleId)?.layouts?.[currentLayout]);
|
||||
const temporaryLayouts = useStore((state) => {
|
||||
let transformedId = id;
|
||||
if (subContainerIndex || subContainerIndex === 0) {
|
||||
transformedId = `${id}-${subContainerIndex}`;
|
||||
}
|
||||
return state.temporaryLayouts?.[transformedId];
|
||||
}, shallow);
|
||||
const getExposedPropertyForAdditionalActions = useStore(
|
||||
(state) => state.getExposedPropertyForAdditionalActions,
|
||||
shallow
|
||||
);
|
||||
const temporaryLayouts = useStore((state) => state.temporaryLayouts?.[id], shallow);
|
||||
|
||||
const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow);
|
||||
const isDragging = useStore((state) => state.draggingComponentId === id);
|
||||
const isResizing = useStore((state) => state.resizingComponentId === id);
|
||||
|
|
@ -38,19 +50,39 @@ const WidgetWrapper = memo(
|
|||
(state) => state.getComponentDefinition(id, moduleId)?.component?.component,
|
||||
shallow
|
||||
);
|
||||
const isDynamicHeightEnabled = useStore(
|
||||
(state) => state.getResolvedComponent(id, subContainerIndex, moduleId)?.properties?.dynamicHeight,
|
||||
shallow
|
||||
);
|
||||
const isDynamicHeightEnabledInModeView = isDynamicHeightEnabled && mode === 'view';
|
||||
// Dont remove this is being used to re-render the height calculations
|
||||
const label = useStore(
|
||||
(state) => state.getComponentDefinition(id, moduleId)?.component?.definition?.properties?.label
|
||||
);
|
||||
|
||||
const setHoveredComponentForGrid = useStore((state) => state.setHoveredComponentForGrid, shallow);
|
||||
const canShowInCurrentLayout = useStore((state) => {
|
||||
const others = state.getResolvedComponent(id, subContainerIndex, moduleId)?.others;
|
||||
return others?.[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'];
|
||||
});
|
||||
|
||||
const visibility = useStore((state) => {
|
||||
const component = state.getResolvedComponent(id, subContainerIndex, moduleId);
|
||||
const componentExposedVisibility = state.getExposedValueOfComponent(id, moduleId)?.isVisible;
|
||||
if (componentExposedVisibility === false) return false;
|
||||
if (component?.properties?.visibility === false || component?.styles?.visibility === false) return false;
|
||||
return true;
|
||||
const componentExposedVisibility = getExposedPropertyForAdditionalActions(
|
||||
id,
|
||||
subContainerIndex,
|
||||
'isVisible',
|
||||
moduleId
|
||||
);
|
||||
if (componentExposedVisibility !== undefined) return componentExposedVisibility;
|
||||
return component?.properties?.visibility || component?.styles?.visibility;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
incrementCanvasUpdater();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visibility]);
|
||||
|
||||
if (!canShowInCurrentLayout || !layoutData) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -63,12 +95,23 @@ const WidgetWrapper = memo(
|
|||
|
||||
const width = gridWidth * newLayoutData?.width;
|
||||
const height = calculateMoveableBoxHeightWithId(id, currentLayout, stylesDefinition);
|
||||
|
||||
// Calculate the final height based on visibility and temporary layouts
|
||||
const finalHeight = visibility ? temporaryLayouts?.height ?? height : 10;
|
||||
|
||||
// Sets height to auto for subcontainer or listview if dynamic height is enabled
|
||||
const styles = {
|
||||
width: width + 'px',
|
||||
height: visibility === false ? '10px' : `${height}px`,
|
||||
height:
|
||||
isDynamicHeightEnabledInModeView &&
|
||||
(isTruthyOrZero(subContainerIndex) || DYNAMIC_HEIGHT_AUTO_LIST.includes(componentType))
|
||||
? 'auto'
|
||||
: finalHeight + 'px',
|
||||
transform: `translate(${newLayoutData.left * gridWidth}px, ${temporaryLayouts?.top ?? newLayoutData.top}px)`,
|
||||
WebkitFontSmoothing: 'antialiased',
|
||||
border: visibility === false && mode === 'edit' ? `1px solid var(--border-default)` : 'none',
|
||||
border: !visibility && mode === 'edit' ? `1px solid var(--border-default)` : 'none',
|
||||
boxSizing: 'content-box',
|
||||
display: !visibility && mode === 'view' ? 'none' : 'block',
|
||||
};
|
||||
|
||||
const isModuleContainer = componentType === 'ModuleContainer';
|
||||
|
|
@ -82,14 +125,16 @@ const WidgetWrapper = memo(
|
|||
[`widget-${id} nested-target`]: id !== 'canvas' && !readOnly,
|
||||
'position-absolute': readOnly,
|
||||
'active-target': isWidgetActive,
|
||||
'opacity-0': isDragging || isResizing,
|
||||
'opacity-0 pointer-events-none': isDragging || isResizing,
|
||||
'module-container': isModuleContainer,
|
||||
'dynamic-height-target': isDynamicHeightEnabled,
|
||||
})}
|
||||
data-id={`${id}`}
|
||||
id={id}
|
||||
widgetid={id}
|
||||
component-type={componentType}
|
||||
parent-id={parentId}
|
||||
subcontainer-id={subContainerIndex}
|
||||
style={{
|
||||
// zIndex: mode === 'view' && widget.component.component == 'Datepicker' ? 2 : null,
|
||||
...styles,
|
||||
|
|
@ -106,6 +151,7 @@ const WidgetWrapper = memo(
|
|||
{mode == 'edit' && (
|
||||
<ConfigHandle
|
||||
id={id}
|
||||
readOnly={readOnly}
|
||||
widgetTop={temporaryLayouts?.top ?? layoutData.top}
|
||||
widgetHeight={temporaryLayouts?.height ?? layoutData.height}
|
||||
showHandle={isWidgetActive}
|
||||
|
|
@ -114,6 +160,7 @@ const WidgetWrapper = memo(
|
|||
customClassName={isModuleContainer ? 'module-container' : ''}
|
||||
isModuleContainer={isModuleContainer}
|
||||
subContainerIndex={subContainerIndex}
|
||||
isDynamicHeightEnabled={isDynamicHeightEnabled}
|
||||
/>
|
||||
)}
|
||||
<RenderWidget
|
||||
|
|
@ -127,6 +174,7 @@ const WidgetWrapper = memo(
|
|||
darkMode={darkMode}
|
||||
onOptionsChange={onOptionsChange}
|
||||
moduleId={moduleId}
|
||||
currentMode={mode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.canvas-container{
|
||||
&:focus-visible{
|
||||
.canvas-container {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
|
@ -23,12 +23,12 @@
|
|||
// }
|
||||
// }
|
||||
|
||||
.empty-box-cont{
|
||||
.empty-box-cont {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: unset !important;
|
||||
|
||||
.dotted-cont{
|
||||
.dotted-cont {
|
||||
border: 1px dashed var(--indigo8);
|
||||
border-radius: 6px;
|
||||
margin: 0 10px 0 10px;
|
||||
|
|
@ -38,20 +38,20 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.title-text{
|
||||
.title-text {
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.title-desc{
|
||||
.title-desc {
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.box-link{
|
||||
.box-link {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
|
||||
|
|
@ -60,8 +60,8 @@
|
|||
width: auto;
|
||||
}
|
||||
|
||||
.link-but{
|
||||
|
||||
.link-but {
|
||||
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #3E63DD;
|
||||
|
|
@ -70,5 +70,42 @@
|
|||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: none; // Rewriting it to replace outline coming from browser styles
|
||||
}
|
||||
outline: none; // Rewriting it to replace outline coming from browser styles
|
||||
}
|
||||
|
||||
// This is common scrollbar classused for all subcontainers
|
||||
// Show scrollbar only on hover in subcontainers
|
||||
|
||||
.real-canvas:not(.has-no-scroll) {
|
||||
overflow: hidden auto;
|
||||
scrollbar-width: none;
|
||||
|
||||
&:hover {
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This is required to maintain the height of the subcontainer when dragging a widget inside it
|
||||
.real-canvas.is-child-being-dragged:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 500%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.scrollbar-hidden {
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ export const NO_OF_GRIDS = 43;
|
|||
|
||||
export const GRID_HEIGHT = 10;
|
||||
|
||||
export const HIDDEN_COMPONENT_HEIGHT = 10;
|
||||
|
||||
export const CANVAS_WIDTHS = Object.freeze({
|
||||
deviceWindowWidth: 450,
|
||||
leftSideBarWidth: 48,
|
||||
|
|
@ -12,20 +14,27 @@ export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanba
|
|||
|
||||
export const DEFAULT_CANVAS_WIDTH = 1292;
|
||||
|
||||
export const APP_HEADER_HEIGHT = 47;
|
||||
export const APP_HEADER_HEIGHT = 48;
|
||||
|
||||
export const LEFT_SIDEBAR_WIDTH = 350;
|
||||
export const QUERY_PANE_HEIGHT = 40; // This represents pane that contains trigger to toggle query panel
|
||||
|
||||
export const LEFT_SIDEBAR_WIDTH = {
|
||||
tooljetai: 440,
|
||||
default: 350,
|
||||
};
|
||||
|
||||
export const RIGHT_SIDEBAR_WIDTH = 300;
|
||||
|
||||
export const PAGES_SIDEBAR_WIDTH_EXPANDED = 226;
|
||||
export const PAGES_SIDEBAR_WIDTH_EXPANDED = 256;
|
||||
|
||||
export const PAGES_SIDEBAR_WIDTH_COLLAPSED = 44;
|
||||
export const PAGES_SIDEBAR_WIDTH_COLLAPSED = 54;
|
||||
|
||||
export const SUBCONTAINER_WIDGETS = ['Container', 'Tabs', 'Listview', 'Kanban', 'Form'];
|
||||
|
||||
export const CONTAINER_FORM_CANVAS_PADDING = 7;
|
||||
|
||||
export const WIDGET_BORDER_WIDTH = 1;
|
||||
|
||||
export const SUBCONTAINER_CANVAS_BORDER_WIDTH = 1;
|
||||
|
||||
export const BOX_PADDING = 2;
|
||||
|
|
@ -42,8 +51,10 @@ export const DROPPABLE_PARENTS = new Set([
|
|||
'Table',
|
||||
'ModuleContainer',
|
||||
]);
|
||||
export const TAB_CANVAS_PADDING = 7.5;
|
||||
export const TAB_CANVAS_PADDING = 8;
|
||||
|
||||
export const MODAL_CANVAS_PADDING = 5;
|
||||
|
||||
export const LISTVIEW_CANVAS_PADDING = 7;
|
||||
|
||||
export const decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16));
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ export const addNewWidgetToTheEditor = (
|
|||
parentId === 'canvas' ? 'real-canvas' : parentId,
|
||||
parentCanvasType
|
||||
);
|
||||
let [left, top] = snapToGrid(subContainerWidth, _left, _top);
|
||||
const scrollTop = realCanvasRef?.scrollTop;
|
||||
let [left, top] = snapToGrid(subContainerWidth, _left, _top + scrollTop);
|
||||
|
||||
const gridWidth = subContainerWidth / NO_OF_GRIDS;
|
||||
left = Math.round(left / gridWidth);
|
||||
|
|
@ -355,7 +356,7 @@ export const copyComponents = ({ isCut = false, isCloning = false }) => {
|
|||
}
|
||||
useStore.getState().setLastCanvasClickPosition(null);
|
||||
if (isCloning) {
|
||||
const parentId = allComponents[selectedComponents[0]?.id]?.parent ?? undefined;
|
||||
const parentId = allComponents[selectedComponents[0]?.id]?.component?.parent ?? undefined;
|
||||
debouncedPasteComponents(parentId, newComponentObj);
|
||||
toast.success('Component cloned succesfully');
|
||||
} else if (isCut) {
|
||||
|
|
@ -487,6 +488,9 @@ function calculateGroupPosition(components, existingComponents, layout, targetPa
|
|||
|
||||
// Create a virtual component representing the entire group
|
||||
const virtualGroupComponent = {
|
||||
component: {
|
||||
parent: targetParentId,
|
||||
},
|
||||
layouts: {
|
||||
[layout]: {
|
||||
top: bounds.minTop,
|
||||
|
|
@ -611,6 +615,12 @@ export function pasteComponents(targetParentId, copiedComponentObj) {
|
|||
// Adjust width if parent changed
|
||||
let width = component.layouts[currentLayout].width;
|
||||
|
||||
if (!isCloning && targetParentId !== component.component?.parent) {
|
||||
const containerWidth = useGridStore.getState().subContainerWidths[targetParentId || 'canvas'];
|
||||
const oldContainerWidth = useGridStore.getState().subContainerWidths[component?.component?.parent || 'canvas'];
|
||||
width = Math.round((width * oldContainerWidth) / containerWidth);
|
||||
}
|
||||
|
||||
component.layouts[currentLayout] = {
|
||||
...component.layouts[currentLayout],
|
||||
width,
|
||||
|
|
@ -798,6 +808,9 @@ export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, com
|
|||
if (componentType === 'Listview') {
|
||||
padding = 2 * LISTVIEW_CANVAS_PADDING + 5; // 5 is accounting for scrollbar
|
||||
}
|
||||
if (componentType === 'Tabs') {
|
||||
padding = 2 * TAB_CANVAS_PADDING + 2 * BOX_PADDING;
|
||||
}
|
||||
return canvasWidth - padding;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,65 @@
|
|||
// .active-target::after{
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// top: 0px;
|
||||
// left: 0px;
|
||||
// right: 0px;
|
||||
// bottom: 0px;
|
||||
// border: 1px solid #4af !important;
|
||||
// pointer-events: none; /* So it doesn't block clicks */
|
||||
// box-sizing: border-box;
|
||||
// inset: -1px;
|
||||
// &.dynamic-height-target::after{
|
||||
// border-bottom: 1px dashed #4af !important;
|
||||
// border-top: 1px dashed #4af !important;
|
||||
// }
|
||||
// }
|
||||
// .main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover {
|
||||
// z-index: 4 !important;
|
||||
// &::after{
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// top: 0px;
|
||||
// left: 0px;
|
||||
// right: 0px;
|
||||
// bottom: 0px;
|
||||
// border: 1px solid #4af !important;
|
||||
// pointer-events: none; /* So it doesn't block clicks */
|
||||
// box-sizing: border-box;
|
||||
// inset: -1px;
|
||||
// }
|
||||
// }
|
||||
// .main-editor-canvas .widget-target:not(:has(.widget-target:hover)).dynamic-height-target::after{
|
||||
// border-bottom: 1px dashed #4af !important;
|
||||
// border-top: 1px dashed #4af !important;
|
||||
// }
|
||||
// .main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
|
||||
// // outline: 1px solid #4af;
|
||||
// z-index: 4 !important;
|
||||
// }
|
||||
// .main-editor-canvas .widget-target.module-container {
|
||||
// border: dotted 2px #CCD1D5 !important;
|
||||
// }
|
||||
// // .main-editor-canvas .widget-target:hover {
|
||||
// // outline: 1px solid #4af;
|
||||
// // }
|
||||
|
||||
|
||||
|
||||
|
||||
.active-target {
|
||||
outline: 1px solid #4af !important;
|
||||
&.dynamic-height-target {
|
||||
outline: 1px solid #9747FF !important;
|
||||
}
|
||||
}
|
||||
|
||||
.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover {
|
||||
outline: 1px solid #4af;
|
||||
z-index: 4 !important;
|
||||
&.dynamic-height-target {
|
||||
outline: 1px solid #9747FF !important;
|
||||
}
|
||||
}
|
||||
|
||||
.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import toast from 'react-hot-toast';
|
|||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { handleDeactivateTargets, hideGridLines } from '../AppCanvas/Grid/gridUtils';
|
||||
|
||||
const BUFFER_OFFSET = 15;
|
||||
|
||||
export const useCanvasDropHandler = () => {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
|
||||
|
|
@ -32,7 +34,6 @@ export const useCanvasDropHandler = () => {
|
|||
const isParentModuleContainer = realCanvasRef?.getAttribute('component-type') === 'ModuleContainer';
|
||||
handleDeactivateTargets();
|
||||
hideGridLines();
|
||||
|
||||
setShowModuleBorder(false); // Hide the module border when dropping
|
||||
|
||||
if (isModuleEditor && canvasId === 'canvas') {
|
||||
|
|
@ -50,6 +51,9 @@ export const useCanvasDropHandler = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
|
||||
setRightSidebarOpen(true);
|
||||
|
||||
// IMPORTANT: This logic needs to be changed when we implement the module versioning
|
||||
const moduleInfo = component?.moduleId
|
||||
? {
|
||||
|
|
@ -89,27 +93,24 @@ export const useCanvasDropHandler = () => {
|
|||
await addComponentToCurrentPage(addedComponent);
|
||||
}
|
||||
|
||||
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
|
||||
setRightSidebarOpen(true);
|
||||
// const canvas = document.querySelector('.canvas-container');
|
||||
// const sidebar = document.querySelector('.editor-sidebar');
|
||||
// const droppedElem = document.getElementById(addedComponent?.[0]?.id);
|
||||
|
||||
const canvas = document.querySelector('.canvas-container');
|
||||
const sidebar = document.querySelector('.editor-sidebar');
|
||||
const droppedElem = document.getElementById(addedComponent?.[0]?.id);
|
||||
// if (!canvas || !sidebar || !droppedElem) return;
|
||||
|
||||
if (!canvas || !sidebar || !droppedElem) return;
|
||||
// const droppedRect = droppedElem.getBoundingClientRect();
|
||||
// const sidebarRect = sidebar.getBoundingClientRect();
|
||||
|
||||
const droppedRect = droppedElem.getBoundingClientRect();
|
||||
const sidebarRect = sidebar.getBoundingClientRect();
|
||||
// const isOverlapping = droppedRect.right > sidebarRect.left && droppedRect.left < sidebarRect.right;
|
||||
|
||||
const isOverlapping = droppedRect.right > sidebarRect.left && droppedRect.left < sidebarRect.right;
|
||||
|
||||
if (isOverlapping) {
|
||||
const overlap = droppedRect.right - sidebarRect.left;
|
||||
canvas.scrollTo({
|
||||
left: canvas.scrollLeft + overlap,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
// if (isOverlapping) {
|
||||
// const overlap = droppedRect.right - sidebarRect.left;
|
||||
// canvas.scrollTo({
|
||||
// left: canvas.scrollLeft + overlap + BUFFER_OFFSET,
|
||||
// behavior: 'smooth',
|
||||
// });
|
||||
// }
|
||||
// Reset canvas ID when dropping
|
||||
setCurrentDragCanvasId(null);
|
||||
};
|
||||
|
|
|
|||
59
frontend/src/AppBuilder/AppCanvas/useCanvasMinWidth.js
Normal file
59
frontend/src/AppBuilder/AppCanvas/useCanvasMinWidth.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import {
|
||||
LEFT_SIDEBAR_WIDTH,
|
||||
RIGHT_SIDEBAR_WIDTH,
|
||||
PAGES_SIDEBAR_WIDTH_EXPANDED,
|
||||
PAGES_SIDEBAR_WIDTH_COLLAPSED,
|
||||
} from './appCanvasConstants';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
export default function useCanvasMinWidth({ currentMode, position, isModuleMode, isViewerSidebarPinned }) {
|
||||
const { moduleId } = useModuleContext();
|
||||
const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
|
||||
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
|
||||
const selectedSidebarItem = useStore((state) => state.selectedSidebarItem);
|
||||
const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility(moduleId), shallow);
|
||||
|
||||
const getMinWidth = () => {
|
||||
if (isModuleMode) return '100%';
|
||||
|
||||
const isLeftSidebarOpenInEditor = currentMode === 'edit' ? isSidebarOpen : false;
|
||||
|
||||
const shouldAdjust = isSidebarOpen || (isRightSidebarOpen && currentMode === 'edit');
|
||||
|
||||
if (!shouldAdjust) return '';
|
||||
let offset;
|
||||
const currentSideBarWidth = LEFT_SIDEBAR_WIDTH[selectedSidebarItem] ?? LEFT_SIDEBAR_WIDTH.default;
|
||||
|
||||
if (isViewerSidebarPinned && !isPagesSidebarHidden) {
|
||||
if (position === 'side' && isLeftSidebarOpenInEditor && isRightSidebarOpen && !isPagesSidebarHidden) {
|
||||
offset = `${currentSideBarWidth + RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`;
|
||||
} else if (position === 'side' && isLeftSidebarOpenInEditor && !isRightSidebarOpen && !isPagesSidebarHidden) {
|
||||
offset = `${currentSideBarWidth - PAGES_SIDEBAR_WIDTH_EXPANDED}px`;
|
||||
} else if (position === 'side' && isRightSidebarOpen && !isLeftSidebarOpenInEditor && !isPagesSidebarHidden) {
|
||||
offset = `${RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`;
|
||||
}
|
||||
} else {
|
||||
if (position === 'side' && isLeftSidebarOpenInEditor && isRightSidebarOpen && !isPagesSidebarHidden) {
|
||||
offset = `${currentSideBarWidth + RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`;
|
||||
} else if (position === 'side' && isLeftSidebarOpenInEditor && !isRightSidebarOpen && !isPagesSidebarHidden) {
|
||||
offset = `${currentSideBarWidth - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`;
|
||||
} else if (position === 'side' && isRightSidebarOpen && !isLeftSidebarOpenInEditor && !isPagesSidebarHidden) {
|
||||
offset = `${RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentMode === 'edit') {
|
||||
if ((position === 'top' || isPagesSidebarHidden) && isLeftSidebarOpenInEditor && isRightSidebarOpen) {
|
||||
offset = `${currentSideBarWidth + RIGHT_SIDEBAR_WIDTH}px`;
|
||||
} else if ((position === 'top' || isPagesSidebarHidden) && isLeftSidebarOpenInEditor && !isRightSidebarOpen) {
|
||||
offset = `${currentSideBarWidth}px`;
|
||||
} else if ((position === 'top' || isPagesSidebarHidden) && isRightSidebarOpen && !isLeftSidebarOpenInEditor) {
|
||||
offset = `${RIGHT_SIDEBAR_WIDTH}px`;
|
||||
}
|
||||
}
|
||||
return `calc(100% + ${offset})`;
|
||||
};
|
||||
return getMinWidth();
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
// It is used to show the scrollbar of the main canvas only when the user is scrolling
|
||||
export default function useEnableMainCanvasScroll({ canvasContentRef }) {
|
||||
const scrollTimeoutRef = useRef(null);
|
||||
const [isScrolling, setIsScrolling] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolling(true);
|
||||
|
||||
if (scrollTimeoutRef.current) {
|
||||
clearTimeout(scrollTimeoutRef.current);
|
||||
}
|
||||
|
||||
scrollTimeoutRef.current = setTimeout(() => {
|
||||
setIsScrolling(false);
|
||||
}, 600);
|
||||
};
|
||||
|
||||
const element = canvasContentRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
element.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [canvasContentRef]);
|
||||
|
||||
return isScrolling;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef, use } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
|
@ -8,22 +8,47 @@ import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
|||
const useSidebarMargin = (canvasContainerRef) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const [editorMarginLeft, setEditorMarginLeft] = useState(0);
|
||||
const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
|
||||
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
|
||||
const isLeftSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
|
||||
const selectedSidebarItem = useStore((state) => state.selectedSidebarItem);
|
||||
const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
|
||||
const scrollLeftRef = useRef(0);
|
||||
const appliedLeftSidebarWidthRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'view') setEditorMarginLeft(isSidebarOpen ? LEFT_SIDEBAR_WIDTH : 0);
|
||||
else setEditorMarginLeft(0);
|
||||
}, [isSidebarOpen, mode]);
|
||||
const handleScroll = () => {
|
||||
if (canvasContainerRef.current) {
|
||||
scrollLeftRef.current = canvasContainerRef.current.scrollLeft;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(canvasContainerRef?.current) && isSidebarOpen && canvasContainerRef.current.scrollLeft === 0) {
|
||||
canvasContainerRef.current.scrollLeft += editorMarginLeft;
|
||||
const canvasContainer = canvasContainerRef.current;
|
||||
if (canvasContainer) {
|
||||
canvasContainer.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
canvasContainer.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}
|
||||
}, [editorMarginLeft, canvasContainerRef, isSidebarOpen]);
|
||||
}, [canvasContainerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'view' && !isEmpty(canvasContainerRef?.current)) {
|
||||
const leftSidebarWidth = LEFT_SIDEBAR_WIDTH[selectedSidebarItem] ?? LEFT_SIDEBAR_WIDTH.default;
|
||||
const delta = isLeftSidebarOpen
|
||||
? leftSidebarWidth - appliedLeftSidebarWidthRef.current
|
||||
: -appliedLeftSidebarWidthRef.current;
|
||||
|
||||
const nextScrollLeft = scrollLeftRef.current + delta;
|
||||
canvasContainerRef.current.scrollTo({ left: nextScrollLeft, behavior: 'instant' });
|
||||
|
||||
appliedLeftSidebarWidthRef.current = isLeftSidebarOpen ? leftSidebarWidth : 0;
|
||||
setEditorMarginLeft(isLeftSidebarOpen ? leftSidebarWidth : 0);
|
||||
} else {
|
||||
setEditorMarginLeft(0);
|
||||
appliedLeftSidebarWidthRef.current = 0;
|
||||
}
|
||||
}, [isLeftSidebarOpen, mode, selectedSidebarItem, canvasContainerRef]);
|
||||
|
||||
return editorMarginLeft;
|
||||
};
|
||||
|
||||
export default useSidebarMargin;
|
||||
|
|
|
|||
|
|
@ -93,7 +93,10 @@ export const BoxShadow = ({ value, onChange, cyLabel }) => {
|
|||
|
||||
const eventPopover = () => {
|
||||
return (
|
||||
<Popover className={`${darkMode && 'dark-theme'}`} style={{ width: '350px', maxWidth: '350px' }}>
|
||||
<Popover
|
||||
className={`${darkMode && 'dark-theme'} boxshadow-picker-popover`}
|
||||
style={{ width: '350px', maxWidth: '350px' }}
|
||||
>
|
||||
<Popover.Body className={`${darkMode && 'dark-theme'}`}>
|
||||
<>
|
||||
{input.map((item) => (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
function Checkbox({ value, onChange }) {
|
||||
function Checkbox({ value, onChange, label, meta }) {
|
||||
const [isChecked, setIsChecked] = useState(value); // Initial state of the checkbox
|
||||
useEffect(() => {
|
||||
setIsChecked(value);
|
||||
}, [value]);
|
||||
|
||||
// Use checkboxLabel from meta if provided, otherwise fall back to label prop or default
|
||||
const checkboxLabel = meta?.checkboxLabel ?? label ?? 'Auto width';
|
||||
|
||||
return (
|
||||
<div className="d-flex align-items-center color-slate12" style={{ width: '142px', marginTop: '16px' }}>
|
||||
<div className="d-flex align-items-center color-slate12" style={{ width: '142px' }}>
|
||||
<input
|
||||
data-cy={`auto-width-checkbox`}
|
||||
type="checkbox"
|
||||
|
|
@ -20,7 +23,7 @@ function Checkbox({ value, onChange }) {
|
|||
style={{ height: '16px', width: '16px' }}
|
||||
/>
|
||||
<span className="tj-text-xsm" style={{ marginLeft: '8px' }} data-cy={`auto-width-label`}>
|
||||
Auto width
|
||||
{checkboxLabel}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
|||
import Popover from 'react-bootstrap/Popover';
|
||||
import classNames from 'classnames';
|
||||
import { computeColor } from '@/_helpers/utils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
export const Color = ({
|
||||
value,
|
||||
|
|
@ -19,8 +20,14 @@ export const Color = ({
|
|||
componentType = 'color',
|
||||
CustomOptionList = () => {},
|
||||
SwatchesToggle = () => {},
|
||||
componentId,
|
||||
}) => {
|
||||
value = component == 'Button' ? computeColor(styleDefinition, value, meta) : value;
|
||||
const computeColorForPopoverMenu = useStore((state) => state.computeColorForPopoverMenu);
|
||||
if (component == 'PopoverMenu') {
|
||||
value = computeColorForPopoverMenu(value, meta, componentId);
|
||||
} else if (component == 'Button') {
|
||||
value = computeColor(styleDefinition, value, meta);
|
||||
}
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const colorPickerPosition = meta?.colorPickerPosition ?? '';
|
||||
|
|
@ -56,6 +63,7 @@ export const Color = ({
|
|||
return (
|
||||
<Popover
|
||||
className={classNames(
|
||||
'color-picker-popover',
|
||||
{ 'dark-theme': darkMode },
|
||||
// This is fix when color picker don't have much space to open in bottom side
|
||||
{ 'inspector-color-input-popover': colorPickerPosition === 'top' }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import React, { useRef, useEffect } from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import DataSourceSelect from '@/AppBuilder/QueryManager/Components/DataSourceSelect';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { FileCode2 } from 'lucide-react';
|
||||
|
||||
const AddQueryBtn = ({ darkMode, disabled: _disabled, onQueryCreate, showMenu, setShowMenu }) => {
|
||||
const selectRef = useRef();
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const disabled = _disabled || shouldFreeze;
|
||||
|
||||
useEffect(() => {
|
||||
if (showMenu) {
|
||||
selectRef.current.focus();
|
||||
}
|
||||
}, [showMenu]);
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
show={showMenu && !disabled}
|
||||
placement="left"
|
||||
arrowOffsetTop={90}
|
||||
arrowOffsetLeft={90}
|
||||
overlay={
|
||||
<Popover
|
||||
key={'page.i'}
|
||||
id="component-data-query-add-popover"
|
||||
className={`${darkMode && 'popover-dark-themed dark-theme tj-dark-mode'}`}
|
||||
style={{ width: '244px', maxWidth: '246px' }}
|
||||
>
|
||||
<DataSourceSelect
|
||||
selectRef={selectRef}
|
||||
darkMode={darkMode}
|
||||
closePopup={() => setShowMenu(false)}
|
||||
onQueryCreate={onQueryCreate}
|
||||
skipClosePopup={true}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<span className="col-auto" id="component-data-query-add-popover-btn">
|
||||
<div
|
||||
onMouseEnter={(e) => {
|
||||
e.stopPropagation();
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
setShowMenu(true);
|
||||
}}
|
||||
className="tw-flex tw-items-center tw-w-full tw-text-left dropdown-menu-item"
|
||||
>
|
||||
<span style={{ width: '16px', height: '16px' }} />
|
||||
<span className="icon-image tw-flex tw-items-center">
|
||||
<FileCode2 color="var(--icon-weak)" width={16} height={16} />
|
||||
</span>
|
||||
<span>Add new query</span>
|
||||
<span style={{ marginLeft: 'auto' }}>
|
||||
<SolidIcon name="rightarrrow" width={16} height={16} />
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddQueryBtn;
|
||||
|
|
@ -3,34 +3,73 @@ import useStore from '@/AppBuilder/_stores/store';
|
|||
import { shallow } from 'zustand/shallow';
|
||||
import DataSourceIcon from '@/AppBuilder/QueryManager/Components/DataSourceIcon';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { LabeledDivider } from '@/AppBuilder/RightSideBar/Inspector/Components/Form/_components';
|
||||
import cx from 'classnames';
|
||||
import Fuse from 'fuse.js';
|
||||
import './styles.scss';
|
||||
import AddQueryBtn from './AddQueryBtn';
|
||||
import InputComponent from '@/components/ui/Input/Index';
|
||||
import useShowPopover from '@/_hooks/useShowPopover';
|
||||
import { FileCode2, Braces } from 'lucide-react';
|
||||
|
||||
export const DropdownMenu = (props) => {
|
||||
const { value, onChange, forceCodeBox } = props;
|
||||
|
||||
const { value, onChange, darkMode, meta } = props;
|
||||
const { disableCreateQuery = false } = meta;
|
||||
const expandQueryPaneIfNeeded = useStore((state) => state.queryPanel.expandQueryPaneIfNeeded);
|
||||
const dataQueries = useStore((state) => state.dataQuery.queries.modules.canvas, shallow);
|
||||
|
||||
// Simple emoji/text icons instead of lucide icons
|
||||
const sourceOptions = useMemo(
|
||||
() => [
|
||||
{ id: 'rawJson', label: 'Raw JSON', icon: <SolidIcon name="curlybraces" /> },
|
||||
{ id: 'jsonSchema', label: 'JSON schema', icon: <SolidIcon name="curlybraces" /> },
|
||||
// { id: 'json-schema', label: 'JSON schema' },
|
||||
],
|
||||
[]
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [filteredQueries, setFilteredQueries] = useState(dataQueries);
|
||||
const [showMenu, setShowMenu] = useShowPopover(
|
||||
false,
|
||||
'#component-data-query-add-popover',
|
||||
'#component-data-query-add-popover-btn'
|
||||
);
|
||||
|
||||
// Simple emoji/text icons instead of lucide icons
|
||||
const sourceOptions = useMemo(() => {
|
||||
const options = props.meta.options;
|
||||
return options.map((option) => ({
|
||||
id: option.value,
|
||||
label: option.name,
|
||||
icon: <Braces color="var(--icon-weak)" width={16} height={16} />,
|
||||
}));
|
||||
}, [props.meta.options]);
|
||||
|
||||
const queryOptions = useMemo(() => {
|
||||
return dataQueries.map((query) => ({
|
||||
return filteredQueries.map((query) => ({
|
||||
id: query.id,
|
||||
value: `{{queries.${query.id}.data}}`,
|
||||
label: query.name,
|
||||
icon: <DataSourceIcon source={query} height={16} />,
|
||||
type: 'query',
|
||||
}));
|
||||
}, [dataQueries]);
|
||||
}, [filteredQueries]);
|
||||
|
||||
const closeMenu = () => {
|
||||
if (!showMenu) {
|
||||
return;
|
||||
}
|
||||
setShowMenu(false);
|
||||
};
|
||||
|
||||
const filterQueries = (value, queries) => {
|
||||
if (value) {
|
||||
const fuse = new Fuse(queries, { keys: ['name'], shouldSort: true, threshold: 0.3 });
|
||||
const results = fuse.search(value);
|
||||
let filterDataQueries = [];
|
||||
results.every((result) => {
|
||||
if (result.item.name === value) {
|
||||
filterDataQueries = [];
|
||||
filterDataQueries.push(result.item);
|
||||
return false;
|
||||
}
|
||||
filterDataQueries.push(result.item);
|
||||
return true;
|
||||
});
|
||||
setFilteredQueries(filterDataQueries);
|
||||
} else {
|
||||
setFilteredQueries(queries);
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedSource = (value) => {
|
||||
if (!value) return null;
|
||||
|
|
@ -38,7 +77,7 @@ export const DropdownMenu = (props) => {
|
|||
if (selectedItem) {
|
||||
return selectedItem;
|
||||
}
|
||||
if (!value.startsWith('{{queries.')) {
|
||||
if (typeof value !== 'string' || !value.startsWith('{{queries.')) {
|
||||
return null;
|
||||
}
|
||||
const queryName = value.split('.')[1]?.replace('}}', '');
|
||||
|
|
@ -49,10 +88,35 @@ export const DropdownMenu = (props) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const handleChange = (data) => {
|
||||
const { id, name, kind } = data;
|
||||
const option = {
|
||||
id: id,
|
||||
label: name,
|
||||
value: `{{queries.${id}.data}}`,
|
||||
icon: <DataSourceIcon source={{ id, name, kind }} height={16} />,
|
||||
type: 'query',
|
||||
};
|
||||
setSelectedSource(option);
|
||||
expandQueryPaneIfNeeded();
|
||||
onChange(`{{queries.${id}.data}}`);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedSource, setSelectedSource] = useState(() => getSelectedSource(value));
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
// Initialize filtered queries when dataQueries change
|
||||
useEffect(() => {
|
||||
setFilteredQueries(dataQueries);
|
||||
}, [dataQueries]);
|
||||
|
||||
// Filter queries when search value changes
|
||||
useEffect(() => {
|
||||
filterQueries(searchValue, dataQueries);
|
||||
}, [searchValue, dataQueries]);
|
||||
|
||||
// Handle outside clicks
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
|
|
@ -83,13 +147,17 @@ export const DropdownMenu = (props) => {
|
|||
onChange(source.id);
|
||||
} else if (source.type === 'query') {
|
||||
onChange(source.value);
|
||||
forceCodeBox();
|
||||
}
|
||||
};
|
||||
|
||||
const renderCheckIcon = ({ id }) => {
|
||||
if (value === id) {
|
||||
return <SolidIcon name="check" width="16" height="16" fill="#4368E3" viewBox="0 0 16 16" />;
|
||||
const renderCheckIcon = (props) => {
|
||||
let transformedValue = props.value;
|
||||
const { id, label } = props;
|
||||
if (typeof transformedValue === 'string' && transformedValue.startsWith('{{queries.')) {
|
||||
transformedValue = transformedValue.replace(id, label);
|
||||
}
|
||||
if (value === props.id || value === transformedValue) {
|
||||
return <SolidIcon name="check" width="16" height="16" fill="#4368E3" viewBox="0 0 18 18" />;
|
||||
} else {
|
||||
return <div style={{ width: '16px', height: '16px' }}></div>;
|
||||
}
|
||||
|
|
@ -108,18 +176,20 @@ export const DropdownMenu = (props) => {
|
|||
}
|
||||
)}
|
||||
>
|
||||
<div className="tw-flex tw-items-center">
|
||||
<div className="dropdown-menu-trigger-content">
|
||||
{selectedSource ? (
|
||||
<>
|
||||
<span className="tw-mr-2">{selectedSource.icon}</span>
|
||||
<span>{selectedSource.label}</span>
|
||||
<span className="dropdown-menu-trigger-label">{selectedSource.label}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="tw-mr-2 tw-text-gray-400">
|
||||
<SolidIcon name="code" width="16" height="16" fill="#CCD1D5" />
|
||||
</span>
|
||||
<span className="tw-text-gray-400 dropdown-menu-placeholder">Select a source</span>
|
||||
<span className="tw-text-gray-400 dropdown-menu-placeholder dropdown-menu-trigger-label">
|
||||
Select a source
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -134,43 +204,88 @@ export const DropdownMenu = (props) => {
|
|||
|
||||
{/* Dropdown menu */}
|
||||
{isOpen && (
|
||||
<div className="tw-absolute tw-z-10 tw-w-full tw-mt-1 tw-rounded-md tw-shadow-lg tw-p-2 dropdown-menu-container">
|
||||
{/* Source options section */}
|
||||
<div className="tw-py-1 dropdown-menu-items">
|
||||
{sourceOptions.map((option) => (
|
||||
<div
|
||||
key={option.id}
|
||||
onClick={() => selectSource(option)}
|
||||
className="tw-flex tw-items-center tw-w-full tw-px-4 tw-py-2 tw-text-left dropdown-menu-item"
|
||||
>
|
||||
{renderCheckIcon(option)}
|
||||
<span className="icon-image">{option.icon}</span>
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="tw-absolute tw-z-10 tw-w-full tw-mt-1 tw-rounded-md dropdown-menu-container">
|
||||
<div className="dropdown-menu-header">
|
||||
<InputComponent
|
||||
leadingIcon="search01"
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onClear={() => setSearchValue('')}
|
||||
size="medium"
|
||||
placeholder="Search for queries"
|
||||
value={searchValue}
|
||||
{...(searchValue && { trailingAction: 'clear' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{dataQueries.length > 0 && (
|
||||
{filteredQueries.length > 0 ? (
|
||||
<>
|
||||
{/* Divider with "From query" text */}
|
||||
<LabeledDivider label="From query" />
|
||||
|
||||
{/* Query options section */}
|
||||
<div className="tw-py-1 dropdown-menu-items">
|
||||
<div className="dropdown-menu-items dropdown-menu-body dropdown-menu-body-transparent-scrollbar">
|
||||
{queryOptions.map((option) => (
|
||||
<div
|
||||
key={option.id}
|
||||
onClick={() => selectSource(option)}
|
||||
className="tw-flex tw-items-center tw-w-full tw-px-4 tw-py-2 tw-text-left dropdown-menu-item"
|
||||
className="tw-flex tw-items-center tw-w-full tw-text-left dropdown-menu-item"
|
||||
onMouseEnter={() => closeMenu()}
|
||||
>
|
||||
{renderCheckIcon(option)}
|
||||
<span className="icon-image">{option.icon}</span>
|
||||
<span>{option.label}</span>
|
||||
<span className="dropdown-menu-item-label">{option.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="dropdown-empty-state">
|
||||
<div className="dropdown-empty-state-content">
|
||||
<div className="dropdown-empty-state-content-icon">
|
||||
<FileCode2 color="var(--icon-default)" width={20} height={20} />
|
||||
</div>
|
||||
{dataQueries.length === 0 && (
|
||||
<span className="dropdown-empty-state-content-title">No queries created</span>
|
||||
)}
|
||||
{dataQueries.length === 0 ? (
|
||||
<span className="dropdown-empty-state-content-description">
|
||||
Create your first query to fetch and display data in your table
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="dropdown-empty-state-content-description" style={{ marginTop: '8px' }}>
|
||||
No queries found matching
|
||||
</span>
|
||||
<span
|
||||
className="dropdown-empty-state-content-description"
|
||||
style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
‘{searchValue}’
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Source options section */}
|
||||
<div className="dropdown-menu-items dropdown-menu-footer">
|
||||
{sourceOptions.map((option) => (
|
||||
<div
|
||||
key={option.id}
|
||||
onClick={() => selectSource(option)}
|
||||
className="tw-flex tw-items-center tw-w-full tw-text-left dropdown-menu-item"
|
||||
onMouseEnter={() => closeMenu()}
|
||||
>
|
||||
{renderCheckIcon(option)}
|
||||
<span className="icon-image">{option.icon}</span>
|
||||
<span className="dropdown-menu-item-label">{option.label}</span>
|
||||
</div>
|
||||
))}
|
||||
{!disableCreateQuery && (
|
||||
<AddQueryBtn
|
||||
onQueryCreate={handleChange}
|
||||
darkMode={darkMode}
|
||||
showMenu={showMenu}
|
||||
setShowMenu={setShowMenu}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
.dropdown-menu-container {
|
||||
background-color: var(--background-surface-layer-01, #FFFFFF);
|
||||
border: 1px solid var(--border-default, #CCD1D5);
|
||||
box-shadow: var(--elevation-400-box-shadow);
|
||||
}
|
||||
|
||||
.dropdown-menu-trigger {
|
||||
|
|
@ -21,6 +21,21 @@
|
|||
&.is-open {
|
||||
border: 2px solid var(--interactive-focus-outline, #4368E3);
|
||||
}
|
||||
|
||||
.dropdown-menu-trigger-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.dropdown-menu-trigger-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-placeholder {
|
||||
|
|
@ -29,14 +44,114 @@
|
|||
|
||||
.dropdown-menu-items {
|
||||
color: var(--text-default, #1B1F24);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.dropdown-menu-footer {
|
||||
border-top: 1px solid var(--border-weak, #E4E7EB);
|
||||
}
|
||||
|
||||
.dropdown-menu-body {
|
||||
max-height: 286px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dropdown-empty-state {
|
||||
height: 182px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
|
||||
.dropdown-empty-state-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
.dropdown-empty-state-content-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: var(--background-surface-layer-02);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dropdown-empty-state-content-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-default);
|
||||
margin-top: 8px;
|
||||
width: 228px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropdown-empty-state-content-description {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--text-placeholder);
|
||||
width: 228px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dropdown-menu-body-transparent-scrollbar {
|
||||
scrollbar-color: #8890991F transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px !important;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #8890991F;
|
||||
border-radius: 10px;
|
||||
border: 1.5px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #8890991F;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.dropdown-menu-header {
|
||||
padding: 4px 0px;
|
||||
border-bottom: 1px solid var(--border-weak, #E4E7EB);
|
||||
input {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-item {
|
||||
border-radius: 6px;
|
||||
height: 30px;
|
||||
padding: 7px 8px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--interactive-hover, #ACB2B959);
|
||||
background-color: var(--interactive-default, #88909914);
|
||||
}
|
||||
|
||||
.dropdown-menu-item-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const Icon = ({
|
|||
styleDefinition,
|
||||
component,
|
||||
isVisibilityEnabled = true,
|
||||
iconVisibility,
|
||||
}) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [showPopOver, setPopOverVisibility] = useState(false);
|
||||
|
|
@ -40,6 +41,7 @@ export const Icon = ({
|
|||
id="popover-basic"
|
||||
style={{ width: '460px', maxWidth: '460px' }}
|
||||
className={`icon-widget-popover ${darkMode && 'dark-theme theme-dark'}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Popover.Header>
|
||||
<SearchBox onSubmit={searchIcon} width="100%" />
|
||||
|
|
@ -100,13 +102,13 @@ export const Icon = ({
|
|||
overlay={eventPopover()}
|
||||
>
|
||||
<div className="d-flex align-items-center" role="button">
|
||||
<div className="" style={{ marginRight: '2px' }}>
|
||||
<div className="" style={{ marginRight: '2px', marginLeft: '6px', height: '20px', width: '18px' }}>
|
||||
<IconElement
|
||||
data-cy={`icon-on-side-panel`}
|
||||
color={`${darkMode ? '#fff' : '#000'}`}
|
||||
stroke={1.5}
|
||||
strokeLinejoin="miter"
|
||||
style={{ width: '24px', height: '24px' }}
|
||||
style={{ width: '18px', height: '18px' }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -125,6 +127,7 @@ export const Icon = ({
|
|||
onVisibilityChange={onVisibilityChange}
|
||||
component={component}
|
||||
styleDefinition={styleDefinition}
|
||||
iconVisibility={iconVisibility}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,16 @@ import {
|
|||
DeprecatedColumnTooltip,
|
||||
checkIfTableColumnDeprecated,
|
||||
} from '@/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/DeprecatedColumnTypeMsg';
|
||||
import { checkIfInputWidgetTypeIsDeprecated } from '@/AppBuilder/Widgets/BaseComponents/hooks/useInput';
|
||||
import { checkIfStarRatingLabelTypeIsDeprecated } from '@/AppBuilder/Widgets/Rating/Rating';
|
||||
|
||||
export const Option = (props) => {
|
||||
const isDeprecated = checkIfTableColumnDeprecated(props.value);
|
||||
const isDeprecatedStyle =
|
||||
checkIfInputWidgetTypeIsDeprecated(props.value) || checkIfStarRatingLabelTypeIsDeprecated(props.value);
|
||||
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<DeprecatedColumnTooltip columnType={props.value}>
|
||||
<DeprecatedColumnTooltip columnType={props.value} isDeprecatedStyle={isDeprecatedStyle}>
|
||||
<div className="d-flex justify-content-between">
|
||||
<span>{props.label}</span>
|
||||
{props.isSelected && (
|
||||
|
|
@ -20,7 +24,7 @@ export const Option = (props) => {
|
|||
<Check width={'20'} fill={'#3E63DD'} />
|
||||
</span>
|
||||
)}
|
||||
{isDeprecated && (
|
||||
{(checkIfTableColumnDeprecated(props.value) || isDeprecatedStyle) && (
|
||||
<span>
|
||||
<Icon name={'warning'} height={16} width={16} fill="#DB4324" />
|
||||
</span>
|
||||
|
|
@ -66,6 +70,7 @@ const selectCustomStyles = (width) => {
|
|||
}),
|
||||
singleValue: (provided) => ({
|
||||
...provided,
|
||||
paddingLeft: '0px',
|
||||
color: 'var(--slate12)',
|
||||
}),
|
||||
};
|
||||
|
|
@ -89,7 +94,7 @@ export const Select = ({ value, onChange, meta, width = '144px' }) => {
|
|||
height={32}
|
||||
styles={selectCustomStyles(width)}
|
||||
useCustomStyles={true}
|
||||
classNamePrefix="inspector-select"
|
||||
customClassPrefix="inspector-select"
|
||||
components={{
|
||||
IndicatorSeparator: () => null,
|
||||
Option,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import * as Slider from '@radix-ui/react-slider';
|
|||
import './Slider.scss';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
function Slider1({ value, onChange, component, styleDefinition }) {
|
||||
function Slider1(props) {
|
||||
const { value, onChange, component, styleDefinition, meta } = props;
|
||||
const { min = 0, max = 100, step = 1, parseType = 'number', staticInputText = '%' } = meta;
|
||||
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;
|
||||
|
|
@ -24,28 +26,35 @@ function Slider1({ value, onChange, component, styleDefinition }) {
|
|||
|
||||
// debounce function to handle input changes
|
||||
const onInputChange = (e) => {
|
||||
let inputValue = parseInt(e.target.value, 10) || 0;
|
||||
inputValue = Math.min(inputValue, 100);
|
||||
let inputValue = 0;
|
||||
if (parseType === 'float') {
|
||||
inputValue = parseFloat(e.target.value) || 0;
|
||||
} else {
|
||||
inputValue = parseInt(e.target.value, 10) || 0;
|
||||
}
|
||||
inputValue = Math.min(inputValue, max);
|
||||
setSliderValue(inputValue);
|
||||
debouncedOnChange(inputValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column " style={{ width: '142px', marginBottom: '16px', position: 'relative' }}>
|
||||
<div className="d-flex flex-column" style={{ width: '142px', marginBottom: '16px', position: 'relative' }}>
|
||||
<CustomInput
|
||||
disabled={isDisabled}
|
||||
value={sliderValue}
|
||||
staticText="% of the field"
|
||||
staticText={staticInputText}
|
||||
onInputChange={onInputChange}
|
||||
dataCy="width"
|
||||
type="number"
|
||||
min={min}
|
||||
/>
|
||||
<div style={{ position: 'absolute', top: '34px' }}>
|
||||
<Slider.Root
|
||||
className="SliderRoot"
|
||||
defaultValue={sliderValue ? [sliderValue] : [33]}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={[sliderValue]}
|
||||
onValueChange={handleSliderChange}
|
||||
onValueCommit={(value) => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||
import cx from 'classnames';
|
||||
|
||||
const Switch = ({ value, onChange, cyLabel, meta, paramName, isIcon, component }) => {
|
||||
const options = meta?.options;
|
||||
const options = meta?.options || [];
|
||||
const defaultValue =
|
||||
paramName == 'defaultValue' && (component == 'Checkbox' || component == 'ToggleSwitchV2') ? `{{${value}}}` : value;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import React from 'react';
|
|||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { resolveReferences } from '@/_helpers/utils';
|
||||
|
||||
export const Visibility = ({ onVisibilityChange, styleDefinition }) => {
|
||||
const iconVisibility = resolveReferences(styleDefinition?.iconVisibility?.value) || false;
|
||||
export const Visibility = ({ onVisibilityChange, styleDefinition, iconVisibility: _iconVisibility }) => {
|
||||
const iconVisibility = !!_iconVisibility || resolveReferences(styleDefinition?.iconVisibility?.value) || false;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const Portal = ({ children, ...restProps }) => {
|
|||
const PopupIcon = ({ callback, icon, tip, position, isMultiEditor = false, isQueryManager = false }) => {
|
||||
const size = 16;
|
||||
const topRef = isNumber(position?.height) ? Math.floor(position?.height) - 30 : 32;
|
||||
let top = isMultiEditor ? 270 : topRef > 32 ? topRef : 0;
|
||||
let top = topRef > 32 ? topRef : 0;
|
||||
// for query manager we allow the height of query manager to be dynamic, so we need to render the popup icon at the bottom of code editor
|
||||
const renderAtBottom = isQueryManager && (isMultiEditor || topRef > 32);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React, { useContext, useEffect, useMemo, useRef } from 'react';
|
|||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { keymap } from '@codemirror/view';
|
||||
import { keymap, tooltips } from '@codemirror/view';
|
||||
import { completionKeymap, acceptCompletion, autocompletion, completionStatus } from '@codemirror/autocomplete';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { sql } from '@codemirror/lang-sql';
|
||||
|
|
@ -11,21 +11,20 @@ import _ from 'lodash';
|
|||
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 { getSuggestionsForMultiLine } from './autocompleteExtensionConfig';
|
||||
import ErrorBoundary from '@/_ui/ErrorBoundary';
|
||||
import CodeHinter from './CodeHinter';
|
||||
import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext';
|
||||
import { createReferencesLookup } from '@/_stores/utils';
|
||||
import { PreviewBox } from './PreviewBox';
|
||||
import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import { search, searchKeymap, searchPanelOpen } from '@codemirror/search';
|
||||
import { handleSearchPanel } from './SearchBox';
|
||||
import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks';
|
||||
import { isInsideParent } from './utils';
|
||||
import { CodeHinterBtns } from './CodehinterOverlayTriggers';
|
||||
import useWorkflowStore from '@/_stores/workflowStore';
|
||||
|
||||
const langSupport = Object.freeze({
|
||||
javascript: javascript(),
|
||||
|
|
@ -55,6 +54,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
editable = true,
|
||||
renderCopilot,
|
||||
setCodeEditorView,
|
||||
onInputChange, // Added this prop to immediately handle value changes
|
||||
} = props;
|
||||
const editorRef = useRef(null);
|
||||
|
||||
|
|
@ -74,6 +74,8 @@ const MultiLineCodeEditor = (props) => {
|
|||
|
||||
const context = useContext(CodeHinterContext);
|
||||
|
||||
const { workflowSuggestions } = useWorkflowStore((state) => ({ workflowSuggestions: state.suggestions }), shallow);
|
||||
|
||||
const { suggestionList: paramList } = createReferencesLookup(context, true);
|
||||
|
||||
const currentValueRef = useRef(initialValue);
|
||||
|
|
@ -118,7 +120,10 @@ const MultiLineCodeEditor = (props) => {
|
|||
};
|
||||
}, [editorView]);
|
||||
|
||||
const handleChange = (val) => (currentValueRef.current = val);
|
||||
const handleChange = (val) => {
|
||||
currentValueRef.current = val;
|
||||
onInputChange && onInputChange(val);
|
||||
};
|
||||
|
||||
const handleOnBlur = () => {
|
||||
if (!delayOnChange) return onChange(currentValueRef.current);
|
||||
|
|
@ -147,17 +152,9 @@ const MultiLineCodeEditor = (props) => {
|
|||
};
|
||||
|
||||
function autoCompleteExtensionConfig(context) {
|
||||
const currentCursor = context.pos;
|
||||
|
||||
const currentString = context.state.doc.text ||
|
||||
(context.state.doc.children && context.state.doc.children.flatMap(child => child.text || []));
|
||||
|
||||
const inputStr = currentString.join(' ');
|
||||
const currentCurosorPos = currentCursor;
|
||||
const nearestSubstring = removeNestedDoubleCurlyBraces(findNearestSubstring(inputStr, currentCurosorPos));
|
||||
|
||||
const hints = getSuggestions();
|
||||
|
||||
const hasWorkflowSuggestions =
|
||||
workflowSuggestions?.appHints?.length > 0 || workflowSuggestions?.jsHints?.length > 0;
|
||||
const hints = hasWorkflowSuggestions ? workflowSuggestions : getSuggestions();
|
||||
const serverHints = getServerSideGlobalResolveSuggestions(isInsideQueryManager);
|
||||
|
||||
const allHints = {
|
||||
|
|
@ -165,123 +162,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
appHints: [...hints.appHints, ...serverHints],
|
||||
};
|
||||
|
||||
let JSLangHints = [];
|
||||
if (lang === 'javascript') {
|
||||
JSLangHints = Object.keys(allHints['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 = allHints['appHints'];
|
||||
|
||||
let autoSuggestionList = appHints.filter((suggestion) => {
|
||||
return suggestion.hint.includes(nearestSubstring);
|
||||
});
|
||||
|
||||
const localVariables = new Set();
|
||||
|
||||
// Traverse the syntax tree to extract variable declarations
|
||||
syntaxTree(context.state).iterate({
|
||||
enter: (node) => {
|
||||
// JavaScript: Detect variable declarations (var, let, const)
|
||||
if (node.name === 'VariableDefinition') {
|
||||
const varName = context.state.sliceDoc(node.from, node.to);
|
||||
if (varName && varName.startsWith(nearestSubstring)) localVariables.add(varName);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Convert Set to an array of completion suggestions
|
||||
const localVariableSuggestions = [...localVariables].map((varName) => ({
|
||||
hint: varName,
|
||||
type: 'variable',
|
||||
}));
|
||||
|
||||
const suggestionList = paramList.filter((paramSuggestion) => paramSuggestion.hint.includes(nearestSubstring));
|
||||
|
||||
const suggestions = generateHints(
|
||||
[...localVariableSuggestions, ...JSLangHints, ...autoSuggestionList, ...suggestionList],
|
||||
null,
|
||||
nearestSubstring
|
||||
)
|
||||
// Apply depth-based sorting (like SingleLineCodeEditor's filterHintsByDepth)
|
||||
.sort((a, b) => {
|
||||
// Calculate depth based on the original hint property (not label)
|
||||
const aDepth = (a.info?.split('.') || []).length;
|
||||
const bDepth = (b.info?.split('.') || []).length;
|
||||
|
||||
// Sort by depth first (shallow suggestions first)
|
||||
return aDepth - bDepth;
|
||||
})
|
||||
.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],
|
||||
filter: false,
|
||||
};
|
||||
return getSuggestionsForMultiLine(context, allHints, hints, lang, paramList);
|
||||
}
|
||||
|
||||
const customKeyMaps = [
|
||||
|
|
@ -374,6 +255,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
tip="Pop out code editor into a new window"
|
||||
isMultiEditor={true}
|
||||
isQueryManager={isInsideQueryPane}
|
||||
position={{ height: height }}
|
||||
/>
|
||||
|
||||
<CodeHinter.Portal
|
||||
|
|
@ -395,7 +277,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
ref={editorRef}
|
||||
value={initialValueWithReplacedIds}
|
||||
placeholder={placeholder}
|
||||
height={'100%'}
|
||||
height={heightInPx}
|
||||
minHeight={heightInPx}
|
||||
{...(isInsideQueryPane ? { maxHeight: '100%' } : {})}
|
||||
width="100%"
|
||||
|
|
@ -405,6 +287,9 @@ const MultiLineCodeEditor = (props) => {
|
|||
search({
|
||||
createPanel: handleSearchPanel,
|
||||
}),
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
}),
|
||||
javascriptLanguage.data.of({
|
||||
autocomplete: overRideFunction,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { reservedKeywordReplacer } from '@/_lib/reserved-keyword-replacer';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { Overlay } from 'react-bootstrap';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
import { findDefault } from '../_utils/component-properties-validation';
|
||||
|
|
@ -336,6 +337,16 @@ const RenderResolvedValue = ({
|
|||
);
|
||||
};
|
||||
|
||||
function FixIssueTooltipContent() {
|
||||
return (
|
||||
<>
|
||||
<h5 className="tw-font-medium">Auto-fix</h5>
|
||||
|
||||
<p className="tw-text-base tw-mb-0">Diagnose and resolve errors instantly to keep your apps running smoothly</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const PreviewContainer = ({
|
||||
children,
|
||||
isFocused,
|
||||
|
|
@ -459,7 +470,7 @@ const PreviewContainer = ({
|
|||
const popover = (
|
||||
<Popover
|
||||
bsPrefix="codehinter-preview-popover"
|
||||
id="popover-basic"
|
||||
id="codehinter-preview-box-popover"
|
||||
className={`${darkMode && 'dark-theme'}`}
|
||||
style={{
|
||||
zIndex: 1400,
|
||||
|
|
@ -492,15 +503,21 @@ const PreviewContainer = ({
|
|||
</div>
|
||||
|
||||
{aiFeaturesEnabled && (
|
||||
<Button
|
||||
size="medium"
|
||||
variant="outline"
|
||||
leadingIcon="tooljetai"
|
||||
className="mt-2"
|
||||
onClick={handleFixErrorWithAI}
|
||||
<ToolTip
|
||||
placement="left"
|
||||
message={<FixIssueTooltipContent />}
|
||||
tooltipClassName="[&_.tooltip-inner]:tw-text-left [&_.tooltip-inner]:tw-p-3"
|
||||
>
|
||||
Fix with AI
|
||||
</Button>
|
||||
<Button
|
||||
size="medium"
|
||||
variant="outline"
|
||||
leadingIcon="tooljetai"
|
||||
className="mt-2"
|
||||
onClick={handleFixErrorWithAI}
|
||||
>
|
||||
Auto-fix
|
||||
</Button>
|
||||
</ToolTip>
|
||||
)}
|
||||
</Alert>
|
||||
</div>
|
||||
|
|
@ -621,6 +638,48 @@ const PreviewContainer = ({
|
|||
offset: [0, 3],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'detectViewportOverflowObserver',
|
||||
enabled: true,
|
||||
phase: 'write',
|
||||
effect: ({ state, instance }) => {
|
||||
const popperEl = state?.elements?.popper;
|
||||
|
||||
if (!popperEl || typeof IntersectionObserver === 'undefined') return;
|
||||
|
||||
let rafId = null;
|
||||
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const ent = entries[0];
|
||||
if (!ent) return;
|
||||
|
||||
// intersectionRatio < 1 => partially/fully out of viewport
|
||||
if (ent.intersectionRatio < 1) {
|
||||
if (rafId) return;
|
||||
|
||||
rafId = requestAnimationFrame(() => {
|
||||
try {
|
||||
instance.update();
|
||||
} catch (e) {
|
||||
/* error */
|
||||
} finally {
|
||||
rafId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{ threshold: [0, 0.01, 0.5, 1] }
|
||||
);
|
||||
|
||||
io.observe(popperEl);
|
||||
|
||||
return () => {
|
||||
io.disconnect();
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
onFirstUpdate: (state) => {
|
||||
// Force position update on first render
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable import/no-unresolved */
|
||||
import React, { useEffect, useMemo, useRef, useState, useContext } from 'react';
|
||||
import { PreviewBox } from './PreviewBox';
|
||||
import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip';
|
||||
import { ToolTip } from '@/AppBuilder/RightSideBar/Inspector/Elements/Components/ToolTip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { camelCase, isEmpty, noop, get } from 'lodash';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
|
|
@ -14,26 +14,26 @@ import {
|
|||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { keymap } from '@codemirror/view';
|
||||
import { keymap, tooltips } from '@codemirror/view';
|
||||
import FxButton from '../CodeBuilder/Elements/FxButton';
|
||||
import cx from 'classnames';
|
||||
import { DynamicFxTypeRenderer } from './DynamicFxTypeRenderer';
|
||||
import { isInsideParent, resolveReferences } from './utils';
|
||||
import { okaidia } from '@uiw/codemirror-theme-okaidia';
|
||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
import { getAutocompletion } from './autocompleteExtensionConfig';
|
||||
import { getAutocompletion, getSuggestionsForMultiLine } from './autocompleteExtensionConfig';
|
||||
import ErrorBoundary from '@/_ui/ErrorBoundary';
|
||||
import CodeHinter from './CodeHinter';
|
||||
// import { EditorContext } from '../Context/EditorContextWrapper';
|
||||
import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getCssVarValue } from '@/Editor/Components/utils';
|
||||
import { getCssVarValue } from '@/AppBuilder/Widgets/utils';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext';
|
||||
import { createReferencesLookup } from '@/_stores/utils';
|
||||
import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks';
|
||||
import Icon from '@/_ui/Icon/solidIcons/index';
|
||||
import useWorkflowStore from '@/_stores/workflowStore';
|
||||
|
||||
const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
|
|
@ -49,7 +49,6 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
|
|||
const componentDefinition = useStore((state) => state.getComponentDefinition(componentId, moduleId), shallow);
|
||||
const parentId = componentDefinition?.component?.parent;
|
||||
const customResolvables = useStore((state) => state.resolvedStore.modules.canvas?.customResolvables, shallow);
|
||||
|
||||
const customVariables = customResolvables?.[parentId]?.[0] || {};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -85,10 +84,6 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
|
|||
newInitialValue = replaceIdsWithName(initialValue);
|
||||
}
|
||||
|
||||
//! Re render the component when the componentName changes as the initialValue is not updated
|
||||
|
||||
// const { variablesExposedForPreview } = useContext(EditorContext) || {};
|
||||
|
||||
// const customVariables = variablesExposedForPreview?.[componentId] ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -238,57 +233,63 @@ const EditorInput = ({
|
|||
|
||||
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline');
|
||||
|
||||
const { workflowSuggestions } = useWorkflowStore((state) => ({ workflowSuggestions: state.suggestions }), shallow);
|
||||
|
||||
const isInsideQueryManager = useMemo(
|
||||
() => isInsideParent(wrapperRef?.current, 'query-manager'),
|
||||
[wrapperRef.current]
|
||||
);
|
||||
function autoCompleteExtensionConfig(context) {
|
||||
const hintsWithoutParamHints = getSuggestions();
|
||||
const hasWorkflowSuggestions =
|
||||
workflowSuggestions?.appHints?.length > 0 || workflowSuggestions?.jsHints?.length > 0;
|
||||
const hintsWithoutParamHints = hasWorkflowSuggestions ? workflowSuggestions : getSuggestions();
|
||||
const serverHints = getServerSideGlobalResolveSuggestions(isInsideQueryManager);
|
||||
|
||||
let word = context.matchBefore(/\w*/);
|
||||
|
||||
const hints = {
|
||||
...hintsWithoutParamHints,
|
||||
appHints: [...hintsWithoutParamHints.appHints, ...serverHints, ...paramHints],
|
||||
};
|
||||
|
||||
const totalReferences = (context.state.doc.toString().match(/{{/g) || []).length;
|
||||
if (!hasWorkflowSuggestions) {
|
||||
let word = context.matchBefore(/\w*/);
|
||||
|
||||
let queryInput = context.state.doc.toString();
|
||||
const originalQueryInput = queryInput;
|
||||
const totalReferences = (context.state.doc.toString().match(/{{/g) || []).length;
|
||||
|
||||
if (totalReferences > 0) {
|
||||
const currentCursor = context.state.selection.main.head;
|
||||
const currentCursorPos = context.pos;
|
||||
let queryInput = context.state.doc.toString();
|
||||
const originalQueryInput = queryInput;
|
||||
|
||||
let currentWord = queryInput.substring(currentCursor, currentCursorPos);
|
||||
if (totalReferences > 0) {
|
||||
const currentCursor = context.state.selection.main.head;
|
||||
const currentCursorPos = context.pos;
|
||||
|
||||
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 = removeNestedDoubleCurlyBraces(currentWord);
|
||||
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 = removeNestedDoubleCurlyBraces(currentWord);
|
||||
}
|
||||
|
||||
if (currentWord.includes(' ')) {
|
||||
currentWord = currentWord.split(' ').pop();
|
||||
}
|
||||
|
||||
// remove \n from the current word if it is present
|
||||
currentWord = currentWord.replace(/\n/g, '');
|
||||
|
||||
queryInput = '{{' + currentWord + '}}';
|
||||
}
|
||||
|
||||
if (currentWord.includes(' ')) {
|
||||
currentWord = currentWord.split(' ').pop();
|
||||
}
|
||||
let completions = getAutocompletion(queryInput, validationType, hints, totalReferences, originalQueryInput);
|
||||
|
||||
// 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: /^\{\{.*\}\}$/,
|
||||
filter: false,
|
||||
};
|
||||
return {
|
||||
from: word.from,
|
||||
options: completions,
|
||||
validFor: /^\{\{.*\}\}$/,
|
||||
filter: false,
|
||||
};
|
||||
} else return getSuggestionsForMultiLine(context, hints, hintsWithoutParamHints, lang, paramHints); //Need multiline behaviour inside workflows editor, where suggestions are shown on each keystroke
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -362,7 +363,6 @@ const EditorInput = ({
|
|||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const theme = darkMode ? okaidia : githubLight;
|
||||
|
||||
|
||||
// when full screen editor is closed, show the preview box
|
||||
useEffect(() => {
|
||||
if (isFocused && !isOpen) {
|
||||
|
|
@ -425,7 +425,7 @@ const EditorInput = ({
|
|||
<div
|
||||
ref={currentEditorHeightRef}
|
||||
className={`cm-codehinter ${darkMode && 'cm-codehinter-dark-themed'} ${disabled ? 'disabled-cursor' : ''}`}
|
||||
data-cy={`${cyLabel.replace(/_/g, '-')}-input-field`}
|
||||
data-cy={`${cyLabel}-input-field`}
|
||||
>
|
||||
{/* sticky element to position the preview box correctly on top without flowing out of container */}
|
||||
{usePortalEditor && (
|
||||
|
|
@ -477,12 +477,20 @@ const EditorInput = ({
|
|||
extensions={
|
||||
showSuggestions
|
||||
? [
|
||||
javascript({ jsx: lang === 'jsx' }),
|
||||
autoCompleteConfig,
|
||||
keymap.of([...customKeyMaps]),
|
||||
customTabKeymap,
|
||||
]
|
||||
: [javascript({ jsx: lang === 'jsx' })]
|
||||
javascript({ jsx: lang === 'jsx' }),
|
||||
autoCompleteConfig,
|
||||
keymap.of([...customKeyMaps]),
|
||||
customTabKeymap,
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
}),
|
||||
]
|
||||
: [
|
||||
javascript({ jsx: lang === 'jsx' }),
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
}),
|
||||
]
|
||||
}
|
||||
onChange={(val) => {
|
||||
setFirstTimeFocus(false);
|
||||
|
|
@ -536,6 +544,8 @@ const DynamicEditorBridge = (props) => {
|
|||
component,
|
||||
onVisibilityChange,
|
||||
isEventManagerParam = false,
|
||||
iconVisibility,
|
||||
componentId,
|
||||
} = props;
|
||||
|
||||
const [forceCodeBox, setForceCodeBox] = React.useState(fxActive);
|
||||
|
|
@ -547,11 +557,10 @@ const DynamicEditorBridge = (props) => {
|
|||
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
|
||||
let newInitialValue = initialValue,
|
||||
shouldResolve = true;
|
||||
|
||||
// This is to handle the case when the initial value is a string and contains components or queries
|
||||
// and we need to replace the ids with names
|
||||
// but we don't want to resolve the references as it needs to be displayed as it is
|
||||
if (paramName === 'generateFormFrom') {
|
||||
if (paramName === 'generateFormFrom' || paramName === 'dataSourceSelector') {
|
||||
if (
|
||||
typeof initialValue === 'string' &&
|
||||
(initialValue?.includes('components') || initialValue?.includes('queries'))
|
||||
|
|
@ -580,8 +589,9 @@ const DynamicEditorBridge = (props) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`col-auto pt-0 fx-common fx-button-container ${(isEventManagerParam || codeShow) && 'show-fx-button-container'
|
||||
}`}
|
||||
className={`col-auto pt-0 fx-common fx-button-container ${
|
||||
(isEventManagerParam || codeShow) && 'show-fx-button-container'
|
||||
} ${paramType === 'slider' ? 'slider-fx-button-container' : ''}`}
|
||||
>
|
||||
<FxButton
|
||||
active={codeShow}
|
||||
|
|
@ -609,12 +619,16 @@ const DynamicEditorBridge = (props) => {
|
|||
return (
|
||||
<>
|
||||
{paramLabel !== ' ' && !HIDDEN_CODE_HINTER_LABELS.includes(paramLabel) && (
|
||||
<div className={`field ${className}`} data-cy={`${cyLabel}-widget-parameter-label`}>
|
||||
<div
|
||||
className={`field ${paramType === 'slider' ? 'slider-code-editor-label' : ''} ${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'
|
||||
}`}
|
||||
labelClass={`tj-text-xsm color-slate12 ${codeShow ? 'mb-2' : 'mb-0'} ${
|
||||
darkMode && 'color-whitish-darkmode'
|
||||
}`}
|
||||
/>
|
||||
{isDeprecated && (
|
||||
<span className={'list-item-deprecated-column-type'}>
|
||||
|
|
@ -645,6 +659,9 @@ const DynamicEditorBridge = (props) => {
|
|||
styleDefinition={styleDefinition}
|
||||
component={component}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
iconVisibility={iconVisibility}
|
||||
componentId={componentId}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,24 @@
|
|||
/* eslint-disable import/no-unresolved */
|
||||
import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils';
|
||||
import { getLastDepth, getLastSubstring } from './autocompleteUtils';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
|
||||
/**
|
||||
* Collects all unique JS method hints from all field types
|
||||
* @param {Object} hints - The hints object containing jsHints
|
||||
* @returns {Array} Array of unique JS method hints with type 'js_method'
|
||||
*/
|
||||
const collectUniqueJSMethodHints = (hints) => {
|
||||
const uniqueHints = new Set();
|
||||
Object.values(hints['jsHints']).forEach((fieldType) => {
|
||||
fieldType['methods'].forEach((hint) => uniqueHints.add(hint));
|
||||
});
|
||||
|
||||
return Array.from(uniqueHints, (hint) => ({
|
||||
hint,
|
||||
type: 'js_method',
|
||||
}));
|
||||
};
|
||||
|
||||
export const getAutocompletion = (input, fieldType, hints, totalReferences = 1, originalQueryInput = null) => {
|
||||
if (!input.startsWith('{{') || !input.endsWith('}}')) return [];
|
||||
|
|
@ -15,14 +34,8 @@ export const getAutocompletion = (input, fieldType, hints, totalReferences = 1,
|
|||
type: 'js_method',
|
||||
}));
|
||||
} else {
|
||||
JSLangHints = Object.keys(hints['jsHints'])
|
||||
.map((key) => {
|
||||
return hints['jsHints'][key]['methods'].map((hint) => ({
|
||||
hint: hint,
|
||||
type: 'js_method',
|
||||
}));
|
||||
})
|
||||
.flat();
|
||||
// Collect all unique JS method hints from all field types
|
||||
JSLangHints = collectUniqueJSMethodHints(hints);
|
||||
}
|
||||
|
||||
const deprecatedWorkspaceVarsHints = ['client', 'server'];
|
||||
|
|
@ -53,11 +66,14 @@ export const getAutocompletion = (input, fieldType, hints, totalReferences = 1,
|
|||
return suggestion.hint.includes(actualInput);
|
||||
});
|
||||
|
||||
const lastCharsAfterDot = actualInput.split('.').pop();
|
||||
const jsHints = JSLangHints.filter((cm) => {
|
||||
const lastCharsAfterDot = actualInput.split('.').pop();
|
||||
if (cm.hint.includes(lastCharsAfterDot)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (autoSuggestionList.length === 0 && !cm.hint.includes(actualInput)) return true;
|
||||
jsHints.sort((a, b) => {
|
||||
return a.hint.startsWith(lastCharsAfterDot) ? -1 : 1;
|
||||
});
|
||||
|
||||
const searchInput = removeNestedDoubleCurlyBraces(input);
|
||||
|
|
@ -85,7 +101,7 @@ 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;
|
||||
let displayedHint = type === 'js_method' || (type === 'Function' && !(hint.endsWith('.run()') || hint.endsWith('.reset()'))) ? `${hint}()` : hint;
|
||||
|
||||
const currentWord = input.split('{{').pop().split('}}')[0];
|
||||
const hasDepth = currentWord.includes('.');
|
||||
|
|
@ -146,13 +162,11 @@ export const generateHints = (hints, totalReferences = 1, input, searchText) =>
|
|||
changes: pickedCompletionConfig,
|
||||
};
|
||||
|
||||
const actualInput = removeNestedDoubleCurlyBraces(doc.toString());
|
||||
|
||||
if (actualInput.length === 0) {
|
||||
dispatchConfig.selection = {
|
||||
anchor: anchorSelection,
|
||||
};
|
||||
}
|
||||
dispatchConfig.selection = {
|
||||
anchor:
|
||||
pickedCompletionConfig.from +
|
||||
(completion.type === 'js_methods' ? completion.label.length - 1 : completion.label.length),
|
||||
};
|
||||
|
||||
view.dispatch(dispatchConfig);
|
||||
},
|
||||
|
|
@ -188,10 +202,6 @@ export function findNearestSubstring(inputStr, currentCurosorPos) {
|
|||
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] === ' ') {
|
||||
|
|
@ -202,3 +212,135 @@ export function findNearestSubstring(inputStr, currentCurosorPos) {
|
|||
|
||||
return substring;
|
||||
}
|
||||
|
||||
export const getSuggestionsForMultiLine = (context, allHints, hints = {}, lang, paramList = []) => {
|
||||
const currentCursor = context.pos;
|
||||
|
||||
const currentString =
|
||||
context.state.doc.text ||
|
||||
(context.state.doc.children && context.state.doc.children.flatMap((child) => child.text || []));
|
||||
|
||||
const inputStr = currentString.join(' ');
|
||||
const currentCurosorPos = currentCursor;
|
||||
const nearestSubstring = removeNestedDoubleCurlyBraces(findNearestSubstring(inputStr, currentCurosorPos));
|
||||
|
||||
let JSLangHints = [];
|
||||
if (lang === 'javascript') {
|
||||
// Collect all unique JS method hints from all field types
|
||||
JSLangHints = collectUniqueJSMethodHints(hints);
|
||||
|
||||
JSLangHints = JSLangHints.filter((cm) => {
|
||||
let lastWordAfterDot = nearestSubstring.split('.');
|
||||
|
||||
lastWordAfterDot = lastWordAfterDot[lastWordAfterDot.length - 1];
|
||||
|
||||
if (cm.hint.includes(lastWordAfterDot)) return true;
|
||||
});
|
||||
}
|
||||
|
||||
const appHints = allHints['appHints'];
|
||||
|
||||
let autoSuggestionList = appHints.filter((suggestion) => {
|
||||
return suggestion.hint.includes(nearestSubstring);
|
||||
});
|
||||
|
||||
const localVariables = new Set();
|
||||
|
||||
// Traverse the syntax tree to extract variable declarations
|
||||
syntaxTree(context.state).iterate({
|
||||
enter: (node) => {
|
||||
// JavaScript: Detect variable declarations (var, let, const)
|
||||
if (node.name === 'VariableDefinition') {
|
||||
const varName = context.state.sliceDoc(node.from, node.to);
|
||||
if (varName && varName.startsWith(nearestSubstring)) localVariables.add(varName);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Convert Set to an array of completion suggestions
|
||||
const localVariableSuggestions = [...localVariables].map((varName) => ({
|
||||
hint: varName,
|
||||
type: 'variable',
|
||||
}));
|
||||
|
||||
const suggestionList = paramList.filter((paramSuggestion) => paramSuggestion.hint.includes(nearestSubstring));
|
||||
|
||||
const suggestions = generateHints(
|
||||
[...localVariableSuggestions, ...JSLangHints, ...autoSuggestionList, ...suggestionList],
|
||||
null,
|
||||
nearestSubstring
|
||||
)
|
||||
// Apply depth-based sorting (like SingleLineCodeEditor's filterHintsByDepth)
|
||||
.sort((a, b) => {
|
||||
// Calculate depth based on the original hint property (not label)
|
||||
const aDepth = (a.info?.split('.') || []).length;
|
||||
const bDepth = (b.info?.split('.') || []).length;
|
||||
|
||||
// Sort by depth first (shallow suggestions first)
|
||||
return aDepth - bDepth;
|
||||
})
|
||||
.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') {
|
||||
let limit = Math.max(1, fromIndex === 1 ? 0 : fromIndex);
|
||||
for (let i = to; i > limit; i--) {
|
||||
if (inputStr[i - 1] === '.') {
|
||||
pickedCompletionConfig.from = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispacthConfig.selection = {
|
||||
anchor:
|
||||
pickedCompletionConfig.from +
|
||||
(completion.type === 'js_methods' ? completion.label.length - 1 : completion.label.length),
|
||||
};
|
||||
|
||||
view.dispatch(dispacthConfig);
|
||||
};
|
||||
return hint;
|
||||
});
|
||||
|
||||
return {
|
||||
from: context.pos,
|
||||
options: [...suggestions],
|
||||
filter: false,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
}
|
||||
|
||||
|
||||
.cm-base-hint-info {
|
||||
.cm-base-hint-info, .cm-completionInfo {
|
||||
color: var(--text-default, #1B1F24) !important;
|
||||
background-color: var(--surfaces-surface-02);
|
||||
border: 1px solid var(--borders-disabled-on-white, #E4E7EB) !important;
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cm-base-autocomplete {
|
||||
.cm-base-autocomplete, .cm-tooltip-autocomplete {
|
||||
// height: 300px !important;
|
||||
color: var(--text-default, #1B1F24);
|
||||
background: var(--slate1) !important;
|
||||
|
|
@ -299,8 +299,8 @@
|
|||
height: 100%;
|
||||
|
||||
.cm-editor {
|
||||
min-height: 300px;
|
||||
height: 300px;
|
||||
// min-height: 300px;
|
||||
// height: 300px;
|
||||
max-height: fit-content !important;
|
||||
|
||||
.cm-gutters {
|
||||
|
|
@ -362,11 +362,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.cm-scroller {
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
|
|
@ -390,6 +385,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.codehinter-popup{
|
||||
.cm-editor{
|
||||
border-radius: 0 0 4px 4px !important;
|
||||
//box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rest-api-tab-content {
|
||||
.fields-container {
|
||||
.rest-api-codehinter-key-field {
|
||||
|
|
@ -413,9 +415,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.runjs-editor .cm-editor {
|
||||
border: none !important;
|
||||
}
|
||||
//.runjs-editor .cm-editor {
|
||||
// border: none !important;
|
||||
//}
|
||||
|
||||
.preview-alert-banner {
|
||||
height: fit-content;
|
||||
|
|
@ -488,8 +490,6 @@
|
|||
.codehinter-input {
|
||||
height: 100%;
|
||||
border: none !important;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ function traverseAST(node, callback) {
|
|||
export const isInsideParent = (element, className) => {
|
||||
while (element) {
|
||||
if (element.classList?.contains(className)) {
|
||||
console.log('element.classList', element.classList);
|
||||
return true;
|
||||
}
|
||||
element = element.parentElement;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ export function setPatToken(patToken) {
|
|||
|
||||
export function getPatToken() {
|
||||
if (inMemoryPatToken) return inMemoryPatToken;
|
||||
// Fallback to window.name (persists across same-tab navigations)
|
||||
if (window.name && window.name.length > 0) {
|
||||
inMemoryPatToken = window.name;
|
||||
return inMemoryPatToken;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default function EmbedAppRedirect() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
|
@ -7,7 +7,17 @@ import { shallow } from 'zustand/shallow';
|
|||
|
||||
const AppCanvasBanner = ({ appId = '' }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
|
||||
const { fetchDevelopmentVersions, currentMode, environments } = useStore(
|
||||
(state) => ({
|
||||
fetchDevelopmentVersions: state.fetchDevelopmentVersions,
|
||||
currentMode: state.modeStore.modules[moduleId].currentMode,
|
||||
environments: state.environments,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
useEffect(() => {
|
||||
fetchDevelopmentVersions(appId);
|
||||
}, [appId, environments]);
|
||||
const renderBanner = () => {
|
||||
if (currentMode === 'edit') {
|
||||
return <FreezeVersionInfo hide={false} />;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ToolTip } from '@/_components/ToolTip';
|
|||
import { decodeEntities } from '@/_helpers/utils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import './style.scss';
|
||||
|
||||
const appVersionLoadingStatus = Object.freeze({
|
||||
|
|
@ -15,8 +16,10 @@ const appVersionLoadingStatus = Object.freeze({
|
|||
error: 'error',
|
||||
});
|
||||
|
||||
export const AppVersionsManager = function ({ darkMode }) {
|
||||
export const AppVersionsManager = ({ darkMode }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [appVersionStatus, setGetAppVersionStatus] = useState(appVersionLoadingStatus.loading);
|
||||
|
||||
const [deleteVersion, setDeleteVersion] = useState({
|
||||
|
|
@ -70,7 +73,7 @@ export const AppVersionsManager = function ({ darkMode }) {
|
|||
shallow
|
||||
);
|
||||
|
||||
let appCreationMode = creationMode;
|
||||
const appCreationMode = creationMode;
|
||||
const isEditable = currentMode === 'edit';
|
||||
|
||||
// useEffect(() => {
|
||||
|
|
@ -98,11 +101,29 @@ export const AppVersionsManager = function ({ darkMode }) {
|
|||
});
|
||||
}
|
||||
|
||||
// Close menu when selecting a version
|
||||
setForceMenuOpen(false);
|
||||
|
||||
changeEditorVersionAction(
|
||||
appId,
|
||||
id,
|
||||
(newDeff) => {
|
||||
setCurrentVersionId(id);
|
||||
|
||||
if (isViewer) {
|
||||
const selectedVersionObj = versionsPromotedToEnvironment.find((v) => v.id === id);
|
||||
if (selectedVersionObj) {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
searchParams.set('version', selectedVersionObj.name);
|
||||
navigate(
|
||||
{
|
||||
pathname: location.pathname,
|
||||
search: searchParams.toString(),
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
toast.error(error.message);
|
||||
|
|
@ -194,7 +215,17 @@ export const AppVersionsManager = function ({ darkMode }) {
|
|||
await lazyLoadAppVersions(appId);
|
||||
setGetAppVersionStatus(appVersionLoadingStatus.loaded);
|
||||
}
|
||||
setForceMenuOpen(!forceMenuOpen);
|
||||
};
|
||||
|
||||
const handleToggleMenu = async () => {
|
||||
if (!forceMenuOpen && !appVersionsLazyLoaded) {
|
||||
setGetAppVersionStatus(appVersionLoadingStatus.loading);
|
||||
await lazyLoadAppVersions(appId);
|
||||
setGetAppVersionStatus(appVersionLoadingStatus.loaded);
|
||||
}
|
||||
setForceMenuOpen((prev) => {
|
||||
return !prev;
|
||||
});
|
||||
};
|
||||
|
||||
const customSelectProps = {
|
||||
|
|
@ -212,7 +243,7 @@ export const AppVersionsManager = function ({ darkMode }) {
|
|||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (clickedOutsideRef.current && !clickedOutsideRef.current.contains(event.target)) {
|
||||
if (!forceMenuOpen) {
|
||||
if (forceMenuOpen) {
|
||||
setForceMenuOpen(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -221,14 +252,9 @@ export const AppVersionsManager = function ({ darkMode }) {
|
|||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [clickedOutsideRef]);
|
||||
}, [forceMenuOpen]);
|
||||
return (
|
||||
<div
|
||||
className="d-flex align-items-center p-0"
|
||||
style={{ margin: isViewer && currentLayout === 'mobile' ? '0px' : '0 24px' }}
|
||||
ref={clickedOutsideRef}
|
||||
>
|
||||
<div className="d-flex align-items-center p-0" ref={clickedOutsideRef}>
|
||||
<div
|
||||
className={cx('d-flex version-manager-container p-0', {
|
||||
'w-100': isViewer && currentLayout === 'mobile',
|
||||
|
|
@ -247,7 +273,7 @@ export const AppVersionsManager = function ({ darkMode }) {
|
|||
onChange={(id) => selectVersion(id)}
|
||||
{...customSelectProps}
|
||||
onMenuOpen={onMenuOpen}
|
||||
onMenuClose={() => setForceMenuOpen(false)}
|
||||
onToggleMenu={handleToggleMenu}
|
||||
menuIsOpen={forceMenuOpen}
|
||||
currentEnvironment={selectedEnvironment}
|
||||
isEditable={isEditable}
|
||||
|
|
@ -257,4 +283,4 @@ export const AppVersionsManager = function ({ darkMode }) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
import React, { useState } from 'react';
|
||||
import AlertDialog from '@/_ui/AlertDialog';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
|
||||
const EditVersionModal = ({ showEditAppVersion, setShowEditAppVersion, versionToEdit }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const [isEditingVersion, setIsEditingVersion] = useState(false);
|
||||
const textareaRef = React.useRef(null);
|
||||
|
||||
const handleDescriptionInput = (e) => {
|
||||
const textarea = textareaRef.current || (e && e.target);
|
||||
if (!textarea) return;
|
||||
textarea.style.height = 'auto';
|
||||
const lineHeight = 24;
|
||||
const maxLines = 4;
|
||||
const maxHeight = lineHeight * maxLines;
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, maxHeight) + 'px';
|
||||
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
||||
};
|
||||
|
||||
const { updateVersionNameAction, selectedVersion, appId } = useStore(
|
||||
(state) => ({
|
||||
updateVersionNameAction: state.updateVersionNameAction,
|
||||
selectedVersion: state.selectedVersion,
|
||||
appId: state.appStore.modules[moduleId].app.appId,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
// Use versionToEdit if provided, otherwise fall back to selectedVersion
|
||||
const editingVersion = versionToEdit || selectedVersion;
|
||||
|
||||
const [versionName, setVersionName] = useState(editingVersion?.name || '');
|
||||
const [versionDescription, setVersionDescription] = useState(editingVersion?.description || '');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [descriptionError, setDescriptionError] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const validateVersionName = (value) => {
|
||||
if (value.trim() === '') {
|
||||
return t('editor.appVersionManager.emptyNameError', 'Version name should not be empty');
|
||||
} else if (value.length > 25) {
|
||||
return t('editor.appVersionManager.maxLengthError', 'Version name cannot exceed 25 characters');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const validateVersionDescription = (value) => {
|
||||
if (value.length > 500) {
|
||||
return t('editor.appVersionManager.maxDescriptionLengthError', 'Description cannot exceed 500 characters');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// Update form when modal opens or when the version to edit changes
|
||||
if (showEditAppVersion && editingVersion) {
|
||||
setVersionName(editingVersion?.name || '');
|
||||
setVersionDescription(editingVersion?.description || '');
|
||||
setNameError('');
|
||||
setDescriptionError('');
|
||||
}
|
||||
}, [editingVersion?.id, editingVersion?.name, editingVersion?.description, showEditAppVersion, editingVersion]);
|
||||
|
||||
const editVersion = () => {
|
||||
setNameError('');
|
||||
setDescriptionError('');
|
||||
|
||||
let hasError = false;
|
||||
const hasNameError = validateVersionName(versionName);
|
||||
const hasDescriptionError = validateVersionDescription(versionDescription);
|
||||
|
||||
if (hasDescriptionError) {
|
||||
setDescriptionError(hasDescriptionError);
|
||||
toast.error(hasDescriptionError);
|
||||
hasError = true;
|
||||
}
|
||||
if (hasNameError) {
|
||||
setNameError(hasNameError);
|
||||
toast.error(hasNameError);
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (hasError) return;
|
||||
|
||||
setIsEditingVersion(true);
|
||||
updateVersionNameAction(
|
||||
appId,
|
||||
editingVersion?.id,
|
||||
versionName,
|
||||
versionDescription,
|
||||
() => {
|
||||
toast.success('Version details updated successfully!');
|
||||
setIsEditingVersion(false);
|
||||
setShowEditAppVersion(false);
|
||||
},
|
||||
(error) => {
|
||||
setIsEditingVersion(false);
|
||||
const errorMessage = error?.error || t('editor.appVersionManager.updateFailed', 'Failed to update version');
|
||||
setNameError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
show={showEditAppVersion}
|
||||
closeModal={() => {
|
||||
setVersionName(editingVersion?.name || '');
|
||||
setVersionDescription(editingVersion?.description || '');
|
||||
setNameError('');
|
||||
setDescriptionError('');
|
||||
setShowEditAppVersion(false);
|
||||
}}
|
||||
checkForBackground={true}
|
||||
title={'Edit version'}
|
||||
customClassName="edit-version-modal"
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
editVersion();
|
||||
}}
|
||||
>
|
||||
<div className="row mb-3">
|
||||
<div className="col modal-main tj-app-input version-name">
|
||||
<label className="form-label" data-cy="version-name-label">
|
||||
{t('editor.appVersionManager.versionName', 'Version Name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setVersionName(value);
|
||||
setNameError(validateVersionName(value));
|
||||
}}
|
||||
className="form-control"
|
||||
data-cy="edit-version-name-input-field"
|
||||
placeholder={t('editor.appVersionManager.enterVersionName', 'Enter version name')}
|
||||
disabled={isEditingVersion}
|
||||
value={versionName}
|
||||
maxLength={25}
|
||||
/>
|
||||
<small className={`version-description-helper-text ${nameError ? 'text-danger' : ''}`}>
|
||||
{nameError
|
||||
? nameError
|
||||
: t('editor.appVersionManager.versionNameHelper', 'Version name must be unique and max 25 characters')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="col modal-main tj-app-input version-description">
|
||||
<label className="form-label" data-cy="version-description-label">
|
||||
{t('editor.appVersionManager.versionDescription', 'Version description')}
|
||||
</label>
|
||||
<textarea
|
||||
type="text"
|
||||
ref={textareaRef}
|
||||
onInput={handleDescriptionInput}
|
||||
onChange={(e) => {
|
||||
setVersionDescription(e.target.value);
|
||||
setDescriptionError(validateVersionDescription(e.target.value));
|
||||
}}
|
||||
className="form-control edit-version-description"
|
||||
data-cy="edit-version-description-input-field"
|
||||
placeholder={t('editor.appVersionManager.enterVersionDescription', 'Enter version description')}
|
||||
disabled={isEditingVersion}
|
||||
value={versionDescription}
|
||||
maxLength={500}
|
||||
rows={1}
|
||||
/>
|
||||
<small className={`version-description-helper-text ${descriptionError ? 'text-danger' : ''}`}>
|
||||
{descriptionError
|
||||
? descriptionError
|
||||
: t('editor.appVersionManager.versionDescriptionHelper', 'Description must be max 500 characters')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="edit-version-footer">
|
||||
<hr className="section-divider" style={{ marginLeft: '-1.5rem', marginRight: '-1.5rem' }} />
|
||||
|
||||
<div className="col d-flex justify-content-end">
|
||||
<ButtonSolid
|
||||
size="lg"
|
||||
data-cy="cancel-button"
|
||||
type="button"
|
||||
variant="tertiary"
|
||||
className="mx-2"
|
||||
onClick={() => {
|
||||
setVersionName(editingVersion?.name || '');
|
||||
setVersionDescription(editingVersion?.description || '');
|
||||
setNameError('');
|
||||
setDescriptionError('');
|
||||
setShowEditAppVersion(false);
|
||||
}}
|
||||
>
|
||||
{t('globals.cancel', 'Cancel')}{' '}
|
||||
</ButtonSolid>
|
||||
|
||||
<ButtonSolid size="lg" data-cy="save-button" type="submit" variant="primary">
|
||||
{t('editor.update', 'Update')}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
export default EditVersionModal;
|
||||
313
frontend/src/AppBuilder/Header/CreateDraftVersionModal.jsx
Normal file
313
frontend/src/AppBuilder/Header/CreateDraftVersionModal.jsx
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import AlertDialog from '@/_ui/AlertDialog';
|
||||
import { Alert } from '@/_ui/Alert';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Select from '@/_ui/Select';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import '../../_styles/version-modal.scss';
|
||||
|
||||
const CreateDraftVersionModal = ({
|
||||
showCreateAppVersion,
|
||||
setShowCreateAppVersion,
|
||||
handleCommitEnableChange,
|
||||
canCommit,
|
||||
orgGit,
|
||||
fetchingOrgGit,
|
||||
handleCommitOnVersionCreation = () => { },
|
||||
}) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
|
||||
const [versionName, setVersionName] = useState('');
|
||||
const [isGitSyncEnabled, setIsGitSyncEnabled] = useState(false);
|
||||
const {
|
||||
createNewVersionAction,
|
||||
changeEditorVersionAction,
|
||||
fetchDevelopmentVersions,
|
||||
developmentVersions,
|
||||
appId,
|
||||
selectedVersion,
|
||||
} = useStore(
|
||||
(state) => ({
|
||||
createNewVersionAction: state.createNewVersionAction,
|
||||
changeEditorVersionAction: state.changeEditorVersionAction,
|
||||
selectedEnvironment: state.selectedEnvironment,
|
||||
fetchDevelopmentVersions: state.fetchDevelopmentVersions,
|
||||
developmentVersions: state.developmentVersions,
|
||||
featureAccess: state.license.featureAccess,
|
||||
editingVersion: state.currentVersionId,
|
||||
appId: state.appStore.modules[moduleId].app.appId,
|
||||
currentVersionId: state.currentVersionId,
|
||||
selectedVersion: state.selectedVersion,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
// Filter out draft versions - show all saved versions (PUBLISHED + any released)
|
||||
const savedVersions = developmentVersions.filter((version) => version.status !== 'DRAFT');
|
||||
useEffect(() => {
|
||||
const gitSyncEnabled =
|
||||
orgGit?.git_ssh?.is_enabled ||
|
||||
orgGit?.git_https?.is_enabled ||
|
||||
orgGit?.git_lab?.is_enabled;
|
||||
setIsGitSyncEnabled(gitSyncEnabled);
|
||||
}, [orgGit]);
|
||||
|
||||
const [selectedVersionForCreation, setSelectedVersionForCreation] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (appId) {
|
||||
fetchDevelopmentVersions(appId);
|
||||
}
|
||||
}, [appId, fetchDevelopmentVersions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedVersionForCreation) {
|
||||
return;
|
||||
}
|
||||
// If savedVersions is empty but we have a selectedVersion that is not DRAFT, use it
|
||||
if (!savedVersions?.length) {
|
||||
if (selectedVersion && selectedVersion.status !== 'DRAFT') {
|
||||
setSelectedVersionForCreation(selectedVersion);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If selectedVersion exists in savedVersions, use it
|
||||
if (selectedVersion?.id) {
|
||||
const selected = savedVersions.find((version) => version?.id === selectedVersion?.id);
|
||||
if (selected) {
|
||||
setSelectedVersionForCreation(selected);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, default to the first saved version
|
||||
if (savedVersions.length > 0) {
|
||||
setSelectedVersionForCreation(savedVersions[0]);
|
||||
}
|
||||
}, [savedVersions, selectedVersion, selectedVersionForCreation]);
|
||||
|
||||
// Update version name when selectedVersionForCreation changes or when modal opens
|
||||
useEffect(() => {
|
||||
if (showCreateAppVersion && selectedVersionForCreation?.name) {
|
||||
setVersionName(selectedVersionForCreation.name);
|
||||
}
|
||||
}, [selectedVersionForCreation, showCreateAppVersion]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Create options from savedVersions (all non-draft versions)
|
||||
const options =
|
||||
savedVersions.length > 0
|
||||
? savedVersions.map((version) => ({ label: version.name, value: version.id }))
|
||||
: selectedVersion && selectedVersion.status !== 'DRAFT'
|
||||
? [{ label: selectedVersion.name, value: selectedVersion.id }]
|
||||
: [];
|
||||
|
||||
const createVersion = () => {
|
||||
if (versionName.trim().length > 25) {
|
||||
toast.error('Version name should not be longer than 25 characters');
|
||||
return;
|
||||
}
|
||||
if (versionName.trim() == '') {
|
||||
toast.error('Version name should not be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedVersionForCreation || selectedVersionForCreation === undefined) {
|
||||
toast.error('Please select a version from.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreatingVersion(true);
|
||||
|
||||
//TODO: pass environmentId to the func
|
||||
createNewVersionAction(
|
||||
appId,
|
||||
versionName,
|
||||
selectedVersionForCreation.id,
|
||||
'',
|
||||
(newVersion) => {
|
||||
toast.success('Version Created');
|
||||
setVersionName('');
|
||||
setIsCreatingVersion(false);
|
||||
setShowCreateAppVersion(false);
|
||||
// Refresh development versions to update the list with the new draft
|
||||
fetchDevelopmentVersions(appId);
|
||||
// Use changeEditorVersionAction to properly switch to the new draft version
|
||||
// This will update selectedVersion with all fields including status
|
||||
changeEditorVersionAction(
|
||||
appId,
|
||||
newVersion.id,
|
||||
(data) => {
|
||||
handleCommitOnVersionCreation(data);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error switching to new draft version:', error);
|
||||
toast.error('Draft created but failed to switch to it');
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
if (error?.data?.code === '23505') {
|
||||
toast.error('Version name already exists.');
|
||||
} else {
|
||||
toast.error(error);
|
||||
}
|
||||
setIsCreatingVersion(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
show={showCreateAppVersion}
|
||||
closeModal={() => {
|
||||
setVersionName('');
|
||||
setShowCreateAppVersion(false);
|
||||
}}
|
||||
title={t('editor.appVersionManager.createDraftVersion', 'Create draft version')}
|
||||
customClassName="create-draft-version-modal"
|
||||
>
|
||||
{fetchingOrgGit ? (
|
||||
<div className="loader-container">
|
||||
<div className="primary-spin-loader"></div>
|
||||
</div>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
createVersion();
|
||||
}}
|
||||
>
|
||||
<div className="create-draft-version-body">
|
||||
<div className="mb-3">
|
||||
<div className="col">
|
||||
<label className="form-label mb-1 ms-1" data-cy="version-name-label">
|
||||
{t('editor.appVersionManager.versionName', 'Version Name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => setVersionName(e.target.value)}
|
||||
className="form-control"
|
||||
data-cy="version-name-input-field"
|
||||
placeholder={t('editor.appVersionManager.enterVersionName', 'Enter version name')}
|
||||
disabled={isCreatingVersion}
|
||||
value={versionName}
|
||||
autoFocus={true}
|
||||
minLength="1"
|
||||
maxLength="25"
|
||||
style={{ height: '32px' }}
|
||||
/>
|
||||
<small className="version-name-helper-text" data-cy="version-name-helper-text">
|
||||
{t('editor.appVersionManager.versionNameHelper', 'Version name must be unique and max 25 characters')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 mb-3 version-select">
|
||||
<div className="col">
|
||||
<label className="form-label mb-1 ms-1" data-cy="create-draft-version-from-label">
|
||||
{t('editor.appVersionManager.createVersionFrom', 'Create from version')}
|
||||
</label>
|
||||
<div className="ts-control" data-cy="create-draft-version-from-input-field">
|
||||
<Select
|
||||
options={options}
|
||||
value={selectedVersionForCreation?.id}
|
||||
onChange={(versionId) => {
|
||||
const version = savedVersions.find((v) => v.id === versionId);
|
||||
setSelectedVersionForCreation(version);
|
||||
}}
|
||||
useMenuPortal={false}
|
||||
width="100%"
|
||||
maxMenuHeight={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
placeSvgTop={true}
|
||||
svg="warning-icon"
|
||||
cls={`create-draft-version-alert ${isGitSyncEnabled ? 'git-sync-enabled' : 'git-sync-disabled'}`}
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
marginRight: '6px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="create-draft-version-helper-text"
|
||||
style={{ marginBottom: '12px' }}
|
||||
data-cy="create-draft-version-helper-text"
|
||||
>
|
||||
Draft version can only be created from saved versions.{' '}
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
{isGitSyncEnabled && (
|
||||
<div className="commit-changes mb-3">
|
||||
<div>
|
||||
<input
|
||||
className="form-check-input"
|
||||
checked={canCommit}
|
||||
type="checkbox"
|
||||
onChange={handleCommitEnableChange}
|
||||
data-cy="git-commit-input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="tj-text tj-text-xsm" data-cy="commit-changes-label">
|
||||
Commit changes
|
||||
</div>
|
||||
<div className="tj-text-xxsm" data-cy="commit-helper-text">
|
||||
This will commit the creation of the new version to the git repo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="create-draft-version-footer">
|
||||
<hr className="section-divider" style={{ marginLeft: '-1.5rem', marginRight: '-1.5rem' }} />
|
||||
<div className="col d-flex justify-content-end">
|
||||
<ButtonSolid
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
setVersionName('');
|
||||
setShowCreateAppVersion(false);
|
||||
}}
|
||||
variant="tertiary"
|
||||
className="mx-2"
|
||||
data-cy="create-draft-version-cancel-button"
|
||||
>
|
||||
{t('globals.cancel', 'Cancel')}
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
size="lg"
|
||||
variant="primary"
|
||||
className=""
|
||||
type="submit"
|
||||
disabled={!selectedVersionForCreation}
|
||||
data-cy="create-draft-version-create-button"
|
||||
>
|
||||
{t('editor.appVersionManager.createVersion', 'Create Version')}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDraftVersionModal;
|
||||
|
|
@ -8,6 +8,8 @@ import Select from '@/_ui/Select';
|
|||
import { shallow } from 'zustand/shallow';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import '../../_styles/version-modal.scss';
|
||||
|
||||
const CreateVersionModal = ({
|
||||
showCreateAppVersion,
|
||||
|
|
@ -16,27 +18,31 @@ const CreateVersionModal = ({
|
|||
canCommit,
|
||||
orgGit,
|
||||
fetchingOrgGit,
|
||||
handleCommitOnVersionCreation = () => {},
|
||||
handleCommitOnVersionCreation = () => { },
|
||||
versionId,
|
||||
onVersionCreated,
|
||||
}) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const setResolvedGlobals = useStore((state) => state.setResolvedGlobals, shallow);
|
||||
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
|
||||
const [versionName, setVersionName] = useState('');
|
||||
const isGitSyncEnabled =
|
||||
orgGit?.org_git?.git_ssh?.is_enabled ||
|
||||
orgGit?.org_git?.git_https?.is_enabled ||
|
||||
orgGit?.org_git?.git_lab?.is_enabled;
|
||||
|
||||
const [versionDescription, setVersionDescription] = useState('');
|
||||
const isGitSyncEnabled = orgGit?.git_ssh?.is_enabled || orgGit?.git_https?.is_enabled || orgGit?.git_lab?.is_enabled;
|
||||
const {
|
||||
createNewVersionAction,
|
||||
changeEditorVersionAction,
|
||||
environmentChangedAction,
|
||||
fetchDevelopmentVersions,
|
||||
developmentVersions,
|
||||
appId,
|
||||
setCurrentVersionId,
|
||||
selectedVersion,
|
||||
currentMode,
|
||||
currentEnvironment,
|
||||
environments,
|
||||
setIsEditorFreezed,
|
||||
} = useStore(
|
||||
(state) => ({
|
||||
createNewVersionAction: state.createNewVersionAction,
|
||||
changeEditorVersionAction: state.changeEditorVersionAction,
|
||||
environmentChangedAction: state.environmentChangedAction,
|
||||
selectedEnvironment: state.selectedEnvironment,
|
||||
fetchDevelopmentVersions: state.fetchDevelopmentVersions,
|
||||
developmentVersions: state.developmentVersions,
|
||||
|
|
@ -44,76 +50,192 @@ const CreateVersionModal = ({
|
|||
editingVersion: state.currentVersionId,
|
||||
appId: state.appStore.modules[moduleId].app.appId,
|
||||
currentVersionId: state.currentVersionId,
|
||||
setCurrentVersionId: state.setCurrentVersionId,
|
||||
selectedVersion: state.selectedVersion,
|
||||
currentMode: state.currentMode,
|
||||
currentEnvironment: state.selectedEnvironment,
|
||||
environments: state.environments,
|
||||
setIsEditorFreezed: state.setIsEditorFreezed,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
const [selectedVersionForCreation, setSelectedVersionForCreation] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchDevelopmentVersions(appId);
|
||||
}, []);
|
||||
const textareaRef = React.useRef(null);
|
||||
|
||||
const handleDescriptionInput = (e) => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
textarea.style.height = 'auto';
|
||||
const lineHeight = 24;
|
||||
const maxLines = 4;
|
||||
const maxHeight = lineHeight * maxLines;
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, maxHeight) + 'px';
|
||||
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (developmentVersions?.length && selectedVersion?.id) {
|
||||
const selected = developmentVersions.find((version) => version?.id === selectedVersion?.id) || null;
|
||||
setSelectedVersionForCreation(selected);
|
||||
if (appId) {
|
||||
fetchDevelopmentVersions(appId);
|
||||
}
|
||||
}, [developmentVersions, selectedVersion]);
|
||||
}, [appId, fetchDevelopmentVersions]);
|
||||
|
||||
// Set the version to promote when modal opens or when developmentVersions/versionId changes
|
||||
useEffect(() => {
|
||||
// Only run when modal is open
|
||||
if (!showCreateAppVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for developmentVersions to be loaded
|
||||
if (!developmentVersions?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If versionId prop is provided, ONLY use that specific version
|
||||
if (versionId) {
|
||||
const versionToPromote = developmentVersions.find((version) => version?.id === versionId);
|
||||
if (versionToPromote) {
|
||||
setSelectedVersionForCreation(versionToPromote);
|
||||
setVersionName(versionToPromote.name);
|
||||
setVersionDescription(versionToPromote.description || '');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If no versionId prop, use selectedVersion from store
|
||||
if (selectedVersion?.id) {
|
||||
const selected = developmentVersions.find((version) => version?.id === selectedVersion?.id);
|
||||
if (selected) {
|
||||
setSelectedVersionForCreation(selected);
|
||||
setVersionName(selected.name);
|
||||
setVersionDescription(selected.description || '');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no version is selected or found, use the first development version
|
||||
if (developmentVersions.length > 0) {
|
||||
setSelectedVersionForCreation(developmentVersions[0]);
|
||||
setVersionName(developmentVersions[0].name);
|
||||
setVersionDescription(developmentVersions[0].description || '');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [developmentVersions, versionId, showCreateAppVersion]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const options = developmentVersions.map((version) => {
|
||||
return { label: version.name, value: version };
|
||||
});
|
||||
|
||||
const createVersion = () => {
|
||||
const createVersion = async () => {
|
||||
if (versionName.trim().length > 25) {
|
||||
toast.error('Version name should not be longer than 25 characters');
|
||||
return;
|
||||
}
|
||||
if (versionDescription.trim().length > 500) {
|
||||
toast.error('Version description should not be longer than 500 characters');
|
||||
return;
|
||||
}
|
||||
if (versionName.trim() == '') {
|
||||
toast.error('Version name should not be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedVersionForCreation === undefined) {
|
||||
toast.error('Please select a version from.');
|
||||
if (!selectedVersionForCreation) {
|
||||
toast.error('Please wait while versions are loading...');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreatingVersion(true);
|
||||
|
||||
//TODO: pass environmentId to the func
|
||||
createNewVersionAction(
|
||||
appId,
|
||||
versionName,
|
||||
selectedVersionForCreation.id,
|
||||
(newVersion) => {
|
||||
toast.success('Version Created');
|
||||
setVersionName('');
|
||||
setIsCreatingVersion(false);
|
||||
setShowCreateAppVersion(false);
|
||||
appVersionService
|
||||
.getAppVersionData(appId, newVersion.id, currentMode)
|
||||
.then((data) => {
|
||||
setCurrentVersionId(newVersion.id);
|
||||
handleCommitOnVersionCreation(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
if (error?.data?.code === '23505') {
|
||||
toast.error('Version name already exists.');
|
||||
} else {
|
||||
toast.error(error?.error);
|
||||
}
|
||||
setIsCreatingVersion(false);
|
||||
try {
|
||||
await appVersionService.save(appId, selectedVersionForCreation.id, {
|
||||
name: versionName,
|
||||
description: versionDescription,
|
||||
// need to add commit changes logic here
|
||||
status: 'PUBLISHED',
|
||||
});
|
||||
toast.success('Version Created successfully');
|
||||
setVersionName('');
|
||||
setVersionDescription('');
|
||||
setSelectedVersionForCreation(null);
|
||||
setIsCreatingVersion(false);
|
||||
setShowCreateAppVersion(false);
|
||||
|
||||
// Fetch versions after creation
|
||||
if (onVersionCreated) {
|
||||
onVersionCreated();
|
||||
}
|
||||
);
|
||||
// Refresh development versions to update the lock status
|
||||
fetchDevelopmentVersions(appId);
|
||||
// Switch to the newly created published version properly
|
||||
// The newly created version will be in the draft's environment (development)
|
||||
// but with PUBLISHED status. We may need to switch environment first.
|
||||
try {
|
||||
const newVersionData = await appVersionService.getAppVersionData(
|
||||
appId,
|
||||
selectedVersionForCreation.id,
|
||||
currentMode
|
||||
);
|
||||
|
||||
// Set editor freeze state based on should_freeze_editor
|
||||
if (newVersionData.should_freeze_editor !== undefined) {
|
||||
setIsEditorFreezed(newVersionData.should_freeze_editor);
|
||||
}
|
||||
|
||||
if (newVersionData.editing_version?.id) {
|
||||
const newVersionEnvironmentId = newVersionData.editing_version.currentEnvironmentId;
|
||||
const isDifferentEnvironment = newVersionEnvironmentId !== currentEnvironment?.id;
|
||||
|
||||
if (isDifferentEnvironment) {
|
||||
// Need to switch environment first, then switch to the version
|
||||
const targetEnvironment = environments.find((env) => env.id === newVersionEnvironmentId);
|
||||
if (targetEnvironment) {
|
||||
// First switch environment
|
||||
await environmentChangedAction(targetEnvironment, () => {
|
||||
// Then switch to the new version
|
||||
changeEditorVersionAction(
|
||||
appId,
|
||||
newVersionData.editing_version.id,
|
||||
() => {
|
||||
console.log('Successfully switched environment and version');
|
||||
handleCommitOnVersionCreation(newVersionData, selectedVersion);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error switching to newly created version:', error);
|
||||
toast.error('Version created but failed to switch to it');
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Same environment, just switch version
|
||||
await changeEditorVersionAction(
|
||||
appId,
|
||||
newVersionData.editing_version.id,
|
||||
() => {
|
||||
handleCommitOnVersionCreation(newVersionData, selectedVersion);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error switching to newly created version:', error);
|
||||
toast.error('Version created but failed to switch to it');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting new version data:', error);
|
||||
toast.error('Version created but failed to switch to it');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.data?.code === '23505') {
|
||||
toast.error('Version name already exists.');
|
||||
} else if (error?.error) {
|
||||
toast.error(error?.error);
|
||||
}
|
||||
else {
|
||||
toast.error('Error while creating version. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setIsCreatingVersion(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -121,10 +243,12 @@ const CreateVersionModal = ({
|
|||
show={showCreateAppVersion}
|
||||
closeModal={() => {
|
||||
setVersionName('');
|
||||
setVersionDescription('');
|
||||
setSelectedVersionForCreation(null);
|
||||
setShowCreateAppVersion(false);
|
||||
}}
|
||||
title={t('editor.appVersionManager.createVersion', 'Create new version')}
|
||||
customClassName="git-sync-modal"
|
||||
title={'Save version'}
|
||||
customClassName="create-version-modal"
|
||||
>
|
||||
{fetchingOrgGit ? (
|
||||
<div className="loader-container">
|
||||
|
|
@ -132,15 +256,15 @@ const CreateVersionModal = ({
|
|||
</div>
|
||||
) : (
|
||||
<form
|
||||
className="commit-form"
|
||||
className="create-version-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
createVersion();
|
||||
}}
|
||||
>
|
||||
<div className="mb-3 pb-2">
|
||||
<div className="create-version-body mb-3">
|
||||
<div className="col">
|
||||
<label className="form-label" data-cy="version-name-label">
|
||||
<label className="form-label mb-1 ms-1" data-cy="version-name-label">
|
||||
{t('editor.appVersionManager.versionName', 'Version Name')}
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -155,10 +279,35 @@ const CreateVersionModal = ({
|
|||
minLength="1"
|
||||
maxLength="25"
|
||||
/>
|
||||
<small className="version-name-helper-text" data-cy="version-name-helper-text">
|
||||
{t('editor.appVersionManager.versionNameHelper', 'Version name must be unique and max 25 characters')}
|
||||
</small>
|
||||
</div>
|
||||
<div className="col mt-2">
|
||||
<label className="form-label mb-1 ms-1" data-cy="version-description-label">
|
||||
{t('editor.appVersionManager.versionDescription', 'Version description')}
|
||||
</label>
|
||||
<textarea
|
||||
type="text"
|
||||
ref={textareaRef}
|
||||
onInput={handleDescriptionInput}
|
||||
onChange={(e) => setVersionDescription(e.target.value)}
|
||||
className="form-control app-version-description"
|
||||
data-cy="version-description-input-field"
|
||||
placeholder={t('editor.appVersionManager.enterVersionDescription', 'Enter version description')}
|
||||
disabled={isCreatingVersion}
|
||||
value={versionDescription}
|
||||
autoFocus={true}
|
||||
minLength="0"
|
||||
maxLength="500"
|
||||
rows={1}
|
||||
/>
|
||||
<small className="version-description-helper-text" data-cy="version-description-helper-text">
|
||||
{t('editor.appVersionManager.versionDescriptionHelper', 'Description must be max 500 characters')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 pb-2 version-select">
|
||||
{/* <div className="mb-4 pb-2 version-select">
|
||||
<label className="form-label" data-cy="create-version-from-label">
|
||||
{t('editor.appVersionManager.createVersionFrom', 'Create version from')}
|
||||
</label>
|
||||
|
|
@ -174,81 +323,74 @@ const CreateVersionModal = ({
|
|||
maxMenuHeight={100}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{isGitSyncEnabled && (
|
||||
<div className="commit-changes mt-3">
|
||||
<div>
|
||||
<input
|
||||
className="form-check-input"
|
||||
checked={canCommit}
|
||||
type="checkbox"
|
||||
onChange={handleCommitEnableChange}
|
||||
data-cy="git-commit-input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="tj-text tj-text-xsm" data-cy="commit-changes-label">
|
||||
Commit changes
|
||||
</div>
|
||||
<div className="tj-text-xxsm" data-cy="commit-helper-text">
|
||||
This will commit the creation of the new version to the git repo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3">
|
||||
<Alert placeSvgTop={true} svg="warning-icon" className="create-version-alert">
|
||||
<div
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="create-version-helper-text" data-cy="create-version-helper-text">
|
||||
Saving the version will lock it. To make any edits afterwards, you'll need to create a draft
|
||||
version.
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isGitSyncEnabled && (
|
||||
<div className="commit-changes" style={{ marginTop: '-1rem', marginBottom: '2rem' }}>
|
||||
<div>
|
||||
<input
|
||||
className="form-check-input"
|
||||
checked={canCommit}
|
||||
type="checkbox"
|
||||
onChange={handleCommitEnableChange}
|
||||
data-cy="git-commit-input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="tj-text tj-text-xsm" data-cy="commit-changes-label">
|
||||
Commit changes
|
||||
</div>
|
||||
<div className="tj-text-xxsm" data-cy="commit-helper-text">
|
||||
This will commit the creation of the new version to the git repo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Alert svg="tj-info">
|
||||
<div
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="" data-cy="workspace-constant-helper-text">
|
||||
The new version will be created in development environment
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="create-version-footer">
|
||||
<hr className="section-divider" style={{ marginLeft: '-1.5rem', marginRight: '-1.5rem' }} />
|
||||
<div className="col d-flex justify-content-end">
|
||||
<button
|
||||
className="btn mx-2"
|
||||
data-cy="cancel-button"
|
||||
<ButtonSolid
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
setVersionName('');
|
||||
setVersionDescription('');
|
||||
setShowCreateAppVersion(false);
|
||||
}}
|
||||
type="button"
|
||||
variant="tertiary"
|
||||
className="mx-2"
|
||||
data-cy="create-version-cancel-button"
|
||||
>
|
||||
{t('globals.cancel', 'Cancel')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-primary ${isCreatingVersion ? 'btn-loading' : ''}`}
|
||||
data-cy="create-new-version-button"
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
size="lg"
|
||||
variant="primary"
|
||||
className=""
|
||||
type="submit"
|
||||
disabled={!selectedVersionForCreation || isCreatingVersion}
|
||||
data-cy="create-version-save-button"
|
||||
>
|
||||
<svg
|
||||
className="icon"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.34375 4.42643C5.88351 4.42643 5.51042 4.79953 5.51042 5.25977C5.51042 5.72 5.88351 6.0931 6.34375 6.0931C6.80399 6.0931 7.17708 5.72 7.17708 5.25977C7.17708 4.79953 6.80399 4.42643 6.34375 4.42643ZM3.84375 5.25977C3.84375 3.87905 4.96304 2.75977 6.34375 2.75977C7.72446 2.75977 8.84375 3.87905 8.84375 5.25977C8.84375 6.34828 8.14808 7.27431 7.17708 7.61751V12.902C7.88743 13.1531 8.45042 13.7161 8.7015 14.4264H13.0104C13.2314 14.4264 13.4434 14.3386 13.5997 14.1824C13.756 14.0261 13.8438 13.8141 13.8438 13.5931V11.4383L12.7663 12.5157C12.4409 12.8411 11.9133 12.8411 11.5878 12.5157C11.2624 12.1903 11.2624 11.6626 11.5878 11.3372L14.0878 8.83718C14.4133 8.51174 14.9409 8.51174 15.2663 8.83718L17.7663 11.3372C18.0918 11.6626 18.0918 12.1903 17.7663 12.5157C17.4409 12.8411 16.9133 12.8411 16.5878 12.5157L15.5104 11.4383V13.5931C15.5104 14.2561 15.247 14.892 14.7782 15.3609C14.3093 15.8297 13.6735 16.0931 13.0104 16.0931H8.7015C8.3583 17.0641 7.43227 17.7598 6.34375 17.7598C4.96304 17.7598 3.84375 16.6405 3.84375 15.2598C3.84375 14.1712 4.53942 13.2452 5.51042 12.902V7.61751C4.53942 7.27431 3.84375 6.34828 3.84375 5.25977ZM14.6771 4.42643C14.2168 4.42643 13.8438 4.79953 13.8438 5.25977C13.8438 5.72 14.2168 6.0931 14.6771 6.0931C15.1373 6.0931 15.5104 5.72 15.5104 5.25977C15.5104 4.79953 15.1373 4.42643 14.6771 4.42643ZM12.1771 5.25977C12.1771 3.87905 13.2964 2.75977 14.6771 2.75977C16.0578 2.75977 17.1771 3.87905 17.1771 5.25977C17.1771 6.64048 16.0578 7.75977 14.6771 7.75977C13.2964 7.75977 12.1771 6.64048 12.1771 5.25977ZM6.34375 14.4264C5.88351 14.4264 5.51042 14.7995 5.51042 15.2598C5.51042 15.72 5.88351 16.0931 6.34375 16.0931C6.80399 16.0931 7.17708 15.72 7.17708 15.2598C7.17708 14.7995 6.80399 14.4264 6.34375 14.4264Z"
|
||||
fill="#FDFDFE"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{t('editor.appVersionManager.createVersion', 'Create Version')}
|
||||
</button>
|
||||
{t('editor.appVersionManager.saveVersion', 'Save version')}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,13 @@ import { ConfirmDialog } from '@/_components';
|
|||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import EditWhite from '@assets/images/icons/edit-white.svg';
|
||||
import { defaultAppEnvironments, decodeEntities } from '@/_helpers/utils';
|
||||
import { CreateVersionModal } from '@/modules/Appbuilder/components';
|
||||
import { CreateVersionModal, CreateDraftVersionModal } from '@/modules/Appbuilder/components';
|
||||
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
import { Tag } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button/Button';
|
||||
|
||||
// TODO: edit version modal and add version modal
|
||||
const Menu = (props) => {
|
||||
const isEditable = props.selectProps.isEditable;
|
||||
|
|
@ -95,36 +99,30 @@ const Menu = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const SingleValue = ({ selectProps }) => {
|
||||
export const SingleValue = ({ selectProps = {} }) => {
|
||||
const appVersionName = selectProps.value?.appVersionName;
|
||||
const { menuIsOpen, onToggleMenu } = selectProps;
|
||||
return (
|
||||
<div className="d-inline-flex align-items-center" data-cy="app-version-label" style={{ gap: '8px' }}>
|
||||
<div className="d-inline-flex align-items-center" style={{ gap: '2px' }}>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="layers">
|
||||
<g id="Union">
|
||||
<path
|
||||
d="M6.28275 1.32756C6.73436 1.10175 7.26594 1.10175 7.71756 1.32756L11.9856 3.4616C12.738 3.8378 12.738 4.91152 11.9856 5.28772L7.71756 7.42177C7.26594 7.64758 6.73436 7.64758 6.28275 7.42177L2.01466 5.28772C1.26226 4.91152 1.26226 3.8378 2.01466 3.4616L6.28275 1.32756Z"
|
||||
fill="#D6409F"
|
||||
/>
|
||||
<path
|
||||
d="M1.35032 10.0303C1.44845 9.80947 1.707 9.71003 1.92779 9.80816L6.70397 11.9309C6.8925 12.0147 7.10771 12.0147 7.29625 11.9309L12.0724 9.80816C12.2932 9.71003 12.5518 9.80947 12.6499 10.0303C12.748 10.2511 12.6486 10.5096 12.4278 10.6077L7.65162 12.7305C7.23684 12.9148 6.76338 12.9148 6.34859 12.7305L1.57242 10.6077C1.35162 10.5096 1.25218 10.2511 1.35032 10.0303Z"
|
||||
fill="#D6409F"
|
||||
/>
|
||||
<path
|
||||
d="M1.92779 7.18316C1.707 7.08503 1.44845 7.18447 1.35032 7.40527C1.25218 7.62607 1.35162 7.88461 1.57242 7.98275L6.34859 10.1055C6.76338 10.2898 7.23684 10.2898 7.65162 10.1055L12.4278 7.98275C12.6486 7.88461 12.748 7.62607 12.6499 7.40527C12.5518 7.18447 12.2932 7.08503 12.0724 7.18316L7.29625 9.3059C7.10771 9.3897 6.8925 9.3897 6.70397 9.3059L1.92779 7.18316Z"
|
||||
fill="#D6409F"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<p className="tj-app-version-text tj-text-xsm"> ver</p>
|
||||
</div>
|
||||
<div
|
||||
className={cx('app-version-name text-truncate', { 'color-light-green': selectProps.value.isReleasedVersion })}
|
||||
data-cy={`${selectProps.value?.appVersionName}-current-version-text`}
|
||||
<div className="d-inline-flex align-items-center tw-w-full" data-cy="app-version-label" style={{ gap: '8px' }}>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onToggleMenu && typeof onToggleMenu === 'function') {
|
||||
onToggleMenu();
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
className={`tw-w-full tw-min-w-[80px] ${menuIsOpen ? 'tw-bg-button-outline-hover' : ''}`}
|
||||
>
|
||||
{selectProps.value?.appVersionName && decodeEntities(selectProps.value?.appVersionName)}
|
||||
</div>
|
||||
<Tag width="16" height="16" className="tw-text-icon-success" />
|
||||
|
||||
<span
|
||||
className={cx('app-version-name text-truncate', { 'color-light-green': selectProps.value.isReleasedVersion })}
|
||||
data-cy={`${appVersionName}-current-version-text`}
|
||||
>
|
||||
{appVersionName && decodeEntities(appVersionName)}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -137,7 +135,7 @@ export const CustomSelect = ({ currentEnvironment, onSelectVersion, ...props })
|
|||
return (
|
||||
<>
|
||||
{isEditable && showCreateAppVersion && (
|
||||
<CreateVersionModal
|
||||
<CreateDraftVersionModal
|
||||
{...props}
|
||||
showCreateAppVersion={showCreateAppVersion}
|
||||
setShowCreateAppVersion={setShowCreateAppVersion}
|
||||
|
|
@ -157,9 +155,13 @@ export const CustomSelect = ({ currentEnvironment, onSelectVersion, ...props })
|
|||
{/* When we merge this code to EE update the defaultAppEnvironments object with rest of default environments (then delete this comment)*/}
|
||||
<ConfirmDialog
|
||||
show={deleteVersion.showModal}
|
||||
message={`Are you sure you want to delete this version - ${decodeEntities(deleteVersion.versionName)}?`}
|
||||
title={'Delete version'}
|
||||
message={`This version will be permanently deleted and cannot be recovered. Are you sure you want to continue?`}
|
||||
onConfirm={() => deleteAppVersion(deleteVersion.versionId, deleteVersion.versionName)}
|
||||
onCancel={resetDeleteModal}
|
||||
confirmButtonText={'Delete version'}
|
||||
cancelButtonText={'Cancel'}
|
||||
cancelButtonType="tertiary"
|
||||
/>
|
||||
<Select
|
||||
width={'100%'}
|
||||
|
|
@ -169,6 +171,7 @@ export const CustomSelect = ({ currentEnvironment, onSelectVersion, ...props })
|
|||
Menu: (props) => (
|
||||
<Menu
|
||||
{...props}
|
||||
className="!tw-w-44"
|
||||
currentEnvironment={currentEnvironment}
|
||||
setShowCreateAppVersion={setShowCreateAppVersion}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
// TODO: Clean up code
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { ToolTip } from '@/_components';
|
||||
import { appsService } from '@/_services';
|
||||
import { handleHttpErrorMessages, validateName } from '@/_helpers/utils';
|
||||
import { InfoOrErrorBox } from './InfoOrErrorBox';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { AppModal } from '@/_components/AppModal';
|
||||
import { PenLine } from 'lucide-react';
|
||||
|
||||
function EditAppName() {
|
||||
const { moduleId } = useModuleContext();
|
||||
|
|
@ -22,150 +20,68 @@ function EditAppName() {
|
|||
shallow
|
||||
);
|
||||
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const [name, setName] = useState(appName);
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [warningText, setWarningText] = useState('');
|
||||
const [showRenameModal, setShowRenameModal] = useState(false);
|
||||
|
||||
const inputRef = useRef(null);
|
||||
const handleRenameApp = async (newAppName, appId) => {
|
||||
const sanitizedName = newAppName?.trim().replace(/\s+/g, ' ');
|
||||
|
||||
useEffect(() => {
|
||||
setName(appName);
|
||||
}, [appName]);
|
||||
|
||||
const clearError = () => {
|
||||
setIsError(false);
|
||||
setErrorMessage('');
|
||||
};
|
||||
|
||||
const setError = (message) => {
|
||||
setIsError(true);
|
||||
setErrorMessage(message);
|
||||
};
|
||||
|
||||
const saveAppName = async (newName) => {
|
||||
const trimmedName = newName.trim();
|
||||
if (validateName(trimmedName, 'App', false, true)?.errorMsg) {
|
||||
setName(appName);
|
||||
clearError();
|
||||
setIsEditing(false);
|
||||
return;
|
||||
// Prevent unnecessary API call if the name effectively hasn't changed
|
||||
if (sanitizedName === appName) {
|
||||
setShowRenameModal(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trimmedName === appName) {
|
||||
setIsValid(true);
|
||||
setIsEditing(false);
|
||||
setName(appName);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await appsService.saveApp(appId, { name: trimmedName });
|
||||
setAppName(trimmedName);
|
||||
|
||||
setIsValid(true);
|
||||
setIsEditing(false);
|
||||
toast.success('App name successfully updated!');
|
||||
} catch (error) {
|
||||
if (error.statusCode === 409) {
|
||||
setError('App name already exists');
|
||||
} else {
|
||||
clearError();
|
||||
setName(appName);
|
||||
setIsEditing(false);
|
||||
handleHttpErrorMessages(error, 'app');
|
||||
await appsService.saveApp(appId, { name: sanitizedName });
|
||||
setAppName(sanitizedName);
|
||||
toast.success('App name has been updated!');
|
||||
return true;
|
||||
} catch (errorResponse) {
|
||||
if (errorResponse.statusCode === 409) {
|
||||
return false;
|
||||
}
|
||||
if (errorResponse.statusCode !== 451) {
|
||||
throw errorResponse;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown); // Clean up the event listener
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleBlur = () => {
|
||||
saveAppName(name);
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsValid(true);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleInput = (e) => {
|
||||
const newValue = e.target.value;
|
||||
setName(newValue);
|
||||
if (newValue.length >= 50) {
|
||||
setWarningText('Maximum length has been reached');
|
||||
} else {
|
||||
setWarningText('');
|
||||
clearError();
|
||||
}
|
||||
};
|
||||
|
||||
const borderColor = isError
|
||||
? 'var(--light-tomato-10, #DB4324)' // Apply error border color
|
||||
: darkMode
|
||||
? 'var(--dark-border-color, #2D3748)' // Change this to the appropriate dark border color
|
||||
: 'var(--light-border-color, #FFF0EE)';
|
||||
|
||||
// Define the message based on the pageType prop
|
||||
const messageType = 'App';
|
||||
|
||||
return (
|
||||
<div className={`app-name input-icon ${darkMode ? 'dark' : ''}`}>
|
||||
<ToolTip message={name} placement="bottom" isVisible={!isEditing && appCreationMode !== 'GIT'}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
onChange={() => {
|
||||
//this was quick fix. replace this with actual tooltip props and state later
|
||||
if (document.getElementsByClassName('tooltip').length) {
|
||||
document.getElementsByClassName('tooltip')[0].style.display = 'none';
|
||||
}
|
||||
}}
|
||||
onInput={handleInput}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
onClick={() => {
|
||||
inputRef.current.select();
|
||||
setIsEditing(true);
|
||||
}}
|
||||
className={`form-control-plaintext form-control-plaintext-sm ${
|
||||
(!isError && !isEditing) || isValid ? '' : 'is-invalid'
|
||||
} ${isError ? 'error' : ''}`} // Add the 'error' class when there's an error
|
||||
style={{ border: `1px solid ${borderColor}` }}
|
||||
value={name}
|
||||
maxLength={50}
|
||||
data-cy="app-name-input"
|
||||
<>
|
||||
<div className="tw-h-full tw-flex tw-items-start tw-justify-start">
|
||||
<ToolTip message={appName} placement="bottom" isVisible={appCreationMode !== 'GIT'}>
|
||||
<button
|
||||
className="edit-app-name-button tw-h-8 tw-min-w-[100px] tw-rounded-lg tw-pr-1 tw-w-auto tw-font-medium tw-cursor-pointer tw-outline-none tw-bg-transparent tw-border tw-border-transparent hover:tw-border-border-strong tw-shadow-none tw-group tw-transition-all tw-duration-300 tw-flex tw-items-center tw-relative tw-justify-start"
|
||||
type="button"
|
||||
data-cy="edit-app-name-button"
|
||||
onClick={() => setShowRenameModal(true)}
|
||||
>
|
||||
<span
|
||||
className="tw-font-title-large tw-truncate tw-w-full tw-block tw-text-start group-hover:tw-w-[calc(100%-24px)] tw-text-[var(--slate12)]"
|
||||
data-cy="editor-app-name-input"
|
||||
>
|
||||
{appName}
|
||||
</span>
|
||||
<span className="tw-absolute tw-right-0.5 tw-top-1 tw-text-icon-default tw-hidden group-hover:tw-block tw-w-7 tw-h-7 tw-items-center tw-justify-center">
|
||||
<PenLine width="16" height="16" name="pencil" />
|
||||
</span>
|
||||
</button>
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
{showRenameModal && (
|
||||
<AppModal
|
||||
show={showRenameModal}
|
||||
closeModal={() => setShowRenameModal(false)}
|
||||
processApp={handleRenameApp}
|
||||
selectedAppId={appId}
|
||||
selectedAppName={appName}
|
||||
title="Rename app"
|
||||
actionButton="Rename app"
|
||||
actionLoadingButton={'Renaming'}
|
||||
appType="app"
|
||||
/>
|
||||
</ToolTip>
|
||||
<InfoOrErrorBox
|
||||
active={isError || isEditing}
|
||||
message={
|
||||
errorMessage ||
|
||||
warningText ||
|
||||
(name.length >= 50
|
||||
? 'Maximum length has been reached'
|
||||
: `${messageType} name should be unique and max 50 characters`)
|
||||
}
|
||||
isWarning={warningText || name.length >= 50}
|
||||
isError={isError}
|
||||
darkMode={darkMode}
|
||||
additionalClassName={isError ? 'error' : ''}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,24 @@ import { useTranslation } from 'react-i18next';
|
|||
import { shallow } from 'zustand/shallow';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
|
||||
export const EditVersionModal = ({ setShowEditAppVersion, showEditAppVersion }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const [isEditingVersion, setIsEditingVersion] = useState(false);
|
||||
const textareaRef = React.useRef(null);
|
||||
|
||||
const handleDescriptionInput = (e) => {
|
||||
const textarea = textareaRef.current || (e && e.target);
|
||||
if (!textarea) return;
|
||||
textarea.style.height = 'auto';
|
||||
const lineHeight = 24;
|
||||
const maxLines = 4;
|
||||
const maxHeight = lineHeight * maxLines;
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, maxHeight) + 'px';
|
||||
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
||||
};
|
||||
|
||||
const {
|
||||
updateVersionNameAction,
|
||||
selectedVersion: editingVersion,
|
||||
|
|
@ -22,31 +36,71 @@ export const EditVersionModal = ({ setShowEditAppVersion, showEditAppVersion })
|
|||
shallow
|
||||
);
|
||||
const [versionName, setVersionName] = useState(editingVersion?.name || '');
|
||||
const [versionDescription, setVersionDescription] = useState(editingVersion?.description || '');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [descriptionError, setDescriptionError] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const validateVersionName = (value) => {
|
||||
if (value.trim() === '') {
|
||||
return t('editor.appVersionManager.emptyNameError', 'Version name should not be empty');
|
||||
} else if (value.length > 25) {
|
||||
return t('editor.appVersionManager.maxLengthError', 'Version name cannot exceed 25 characters');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const validateVersionDescription = (value) => {
|
||||
if (value.length > 500) {
|
||||
return t('editor.appVersionManager.maxDescriptionLengthError', 'Description cannot exceed 500 characters');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setVersionName(editingVersion?.name);
|
||||
}, [editingVersion?.name]);
|
||||
setVersionDescription(editingVersion?.description || '');
|
||||
setNameError('');
|
||||
setDescriptionError('');
|
||||
}, [editingVersion?.name, editingVersion]);
|
||||
|
||||
const editVersion = () => {
|
||||
if (versionName.trim() === '') {
|
||||
toast.error('Version name should not be empty');
|
||||
return;
|
||||
setNameError('');
|
||||
setDescriptionError('');
|
||||
|
||||
let hasError = false;
|
||||
const hasNameError = validateVersionName(versionName);
|
||||
const hasDescriptionError = validateVersionDescription(versionDescription);
|
||||
|
||||
if (hasDescriptionError) {
|
||||
setDescriptionError(hasDescriptionError);
|
||||
toast.error(hasDescriptionError);
|
||||
hasError = true;
|
||||
}
|
||||
if (hasNameError) {
|
||||
setNameError(hasNameError);
|
||||
toast.error(hasNameError);
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (hasError) return;
|
||||
|
||||
setIsEditingVersion(true);
|
||||
updateVersionNameAction(
|
||||
appId,
|
||||
editingVersion?.id,
|
||||
versionName,
|
||||
versionDescription,
|
||||
() => {
|
||||
toast.success('Version name updated');
|
||||
toast.success('Version details updated successfully!');
|
||||
setIsEditingVersion(false);
|
||||
setShowEditAppVersion(false);
|
||||
},
|
||||
(error) => {
|
||||
setIsEditingVersion(false);
|
||||
toast.error(error?.error);
|
||||
const errorMessage = error?.error || t('editor.appVersionManager.updateFailed', 'Failed to update version');
|
||||
setNameError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -56,10 +110,14 @@ export const EditVersionModal = ({ setShowEditAppVersion, showEditAppVersion })
|
|||
show={showEditAppVersion}
|
||||
closeModal={() => {
|
||||
setVersionName(editingVersion?.name || '');
|
||||
setVersionDescription(editingVersion?.description || '');
|
||||
setNameError('');
|
||||
setDescriptionError('');
|
||||
setShowEditAppVersion(false);
|
||||
}}
|
||||
checkForBackground={true}
|
||||
title={t('editor.appVersionManager.editVersion', 'Edit Version')}
|
||||
title={'Edit version'}
|
||||
customClassName="edit-version-modal"
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
|
|
@ -68,10 +126,17 @@ export const EditVersionModal = ({ setShowEditAppVersion, showEditAppVersion })
|
|||
}}
|
||||
>
|
||||
<div className="row mb-3">
|
||||
<div className="col modal-main tj-app-input">
|
||||
<div className="col modal-main tj-app-input version-name">
|
||||
<label className="form-label" data-cy="version-name-label">
|
||||
{t('editor.appVersionManager.versionName', 'Version Name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => setVersionName(e.target.value)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setVersionName(value);
|
||||
setNameError(validateVersionName(value));
|
||||
}}
|
||||
className="form-control"
|
||||
data-cy="edit-version-name-input-field"
|
||||
placeholder={t('editor.appVersionManager.enterVersionName', 'Enter version name')}
|
||||
|
|
@ -79,28 +144,66 @@ export const EditVersionModal = ({ setShowEditAppVersion, showEditAppVersion })
|
|||
value={versionName}
|
||||
maxLength={25}
|
||||
/>
|
||||
<small className={`version-description-helper-text ${nameError ? 'text-danger' : ''}`}>
|
||||
{nameError
|
||||
? nameError
|
||||
: t('editor.appVersionManager.versionNameHelper', 'Version name must be unique and max 25 characters')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="row mb-3">
|
||||
<div className="col modal-main tj-app-input version-description">
|
||||
<label className="form-label" data-cy="version-description-label">
|
||||
{t('editor.appVersionManager.versionDescription', 'Version description')}
|
||||
</label>
|
||||
<textarea
|
||||
type="text"
|
||||
ref={textareaRef}
|
||||
onInput={handleDescriptionInput}
|
||||
onChange={(e) => {
|
||||
setVersionDescription(e.target.value);
|
||||
setDescriptionError(validateVersionDescription(e.target.value));
|
||||
}}
|
||||
className="form-control edit-version-description"
|
||||
data-cy="edit-version-description-input-field"
|
||||
placeholder={t('editor.appVersionManager.enterVersionDescription', 'Enter version description')}
|
||||
disabled={isEditingVersion}
|
||||
value={versionDescription}
|
||||
maxLength={500}
|
||||
rows={1}
|
||||
/>
|
||||
<small className={`version-description-helper-text ${descriptionError ? 'text-danger' : ''}`}>
|
||||
{descriptionError
|
||||
? descriptionError
|
||||
: t('editor.appVersionManager.versionDescriptionHelper', 'Description must be max 500 characters')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="edit-version-footer">
|
||||
<hr className="section-divider" style={{ marginLeft: '-1.5rem', marginRight: '-1.5rem' }} />
|
||||
|
||||
<div className="col d-flex justify-content-end">
|
||||
<button
|
||||
className="btn mx-2"
|
||||
<ButtonSolid
|
||||
size="lg"
|
||||
data-cy="cancel-button"
|
||||
type="button"
|
||||
variant="tertiary"
|
||||
className="mx-2"
|
||||
onClick={() => {
|
||||
setVersionName(editingVersion?.name || '');
|
||||
setVersionDescription(editingVersion?.description || '');
|
||||
setNameError('');
|
||||
setDescriptionError('');
|
||||
setShowEditAppVersion(false);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{t('globals.cancel', 'Cancel')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-primary ${isEditingVersion ? 'btn-loading' : ''}`}
|
||||
data-cy="save-button"
|
||||
type="submit"
|
||||
>
|
||||
{t('globals.save', 'Save')}
|
||||
</button>
|
||||
{t('globals.cancel', 'Cancel')}{' '}
|
||||
</ButtonSolid>
|
||||
|
||||
<ButtonSolid size="lg" data-cy="save-button" type="submit" variant="primary">
|
||||
{t('editor.update', 'Update')}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -2,20 +2,18 @@ import React from 'react';
|
|||
import EditAppName from './EditAppName';
|
||||
import cx from 'classnames';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { LogoNavDropdown, AppEnvironments } from '@/modules/Appbuilder/components';
|
||||
import { LogoNavDropdown } from '@/modules/Appbuilder/components';
|
||||
import HeaderActions from './HeaderActions';
|
||||
import { AppVersionsManager } from './AppVersionsManager';
|
||||
import RealtimeAvatars from '@/Editor/RealtimeAvatars';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { VersionManagerDropdown, VersionManagerErrorBoundary } from './VersionManager';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import RightTopHeaderButtons from './RightTopHeaderButtons/RightTopHeaderButtons';
|
||||
import BuildSuggestions from './BuildSuggestions';
|
||||
import GitSyncManager from './GitSyncManager';
|
||||
import UpdatePresenceMultiPlayer from './UpdatePresenceMultiPlayer';
|
||||
import { ModuleEditorBanner } from '@/modules/Modules/components';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import './styles/style.scss';
|
||||
|
||||
import Steps from './Steps';
|
||||
import SaveIndicator from './SaveIndicator';
|
||||
|
||||
export const EditorHeader = ({ darkMode, isUserInZeroToOneFlow }) => {
|
||||
const { moduleId, isModuleEditor } = useModuleContext();
|
||||
|
|
@ -28,47 +26,23 @@ export const EditorHeader = ({ darkMode, isUserInZeroToOneFlow }) => {
|
|||
}),
|
||||
shallow
|
||||
);
|
||||
const shouldEnableMultiplayer = window.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true';
|
||||
|
||||
const getSaveIndicator = () => {
|
||||
if (isSaving) {
|
||||
return 'Saving...';
|
||||
} else if (saveError) {
|
||||
return (
|
||||
<div className="d-flex align-items-center" style={{ gap: '4px' }}>
|
||||
<SolidIcon name="cloudinvalid" width="14" />
|
||||
<p className="mb-0 text-center tj-text-xxsm">Could not save changes</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="d-flex align-items-center" style={{ gap: '4px' }}>
|
||||
<SolidIcon name="cloudvalid" width="14" />
|
||||
<p className="mb-0 text-center">Changes saved</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
const activeStepDetails = aiGenerationMetadata.steps?.find((step) => step.id === aiGenerationMetadata.active_step);
|
||||
const activeStep = activeStepDetails?.hidden ? activeStepDetails.parent_step_id : aiGenerationMetadata.active_step;
|
||||
|
||||
return (
|
||||
<div className={cx('header', { 'dark-theme theme-dark': darkMode })} style={{ width: '100%' }}>
|
||||
<header className="navbar navbar-expand-md d-print-none" style={{ zIndex: 12 }}>
|
||||
<header className="navbar navbar-expand-md d-print-none tw-h-12" style={{ zIndex: 12 }}>
|
||||
<div className="container-xl header-container">
|
||||
<div className="d-flex w-100">
|
||||
<h1 className="navbar-brand d-none-navbar-horizontal p-0" data-cy="editor-page-logo">
|
||||
<LogoNavDropdown darkMode={darkMode} />
|
||||
</h1>
|
||||
<div className="header-inner-wrapper d-flex" style={{ width: 'calc(100% - 348px)', background: '' }}>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '48px',
|
||||
margin: '0px',
|
||||
padding: '0px',
|
||||
width: 'calc(100% - 348px)',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
className="flex-grow-1 d-flex align-items-center"
|
||||
>
|
||||
<div className="d-flex w-100 tw-h-9 tw-justify-between">
|
||||
<div
|
||||
className="header-inner-wrapper d-flex"
|
||||
style={{
|
||||
width: isUserInZeroToOneFlow ? 'auto' : 'calc(100% - 348px)',
|
||||
background: '',
|
||||
}}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<div
|
||||
className="p-0 m-0 d-flex align-items-center"
|
||||
style={{
|
||||
|
|
@ -78,68 +52,63 @@ export const EditorHeader = ({ darkMode, isUserInZeroToOneFlow }) => {
|
|||
}}
|
||||
>
|
||||
<div className="global-settings-app-wrapper p-0 m-0 ">
|
||||
<div className="d-flex flex-row">
|
||||
<h1 className="navbar-brand d-none-navbar-horizontal p-0 tw-shrink-0" data-cy="editor-page-logo">
|
||||
<LogoNavDropdown darkMode={darkMode} />
|
||||
</h1>
|
||||
<div className="d-flex flex-row tw-mr-1">
|
||||
{isModuleEditor && <ModuleEditorBanner showBeta={true} />}
|
||||
<EditAppName />
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
className={cx('autosave-indicator tj-text-xsm', {
|
||||
'autosave-indicator-saving': isSaving,
|
||||
'text-danger': saveError,
|
||||
'd-none': isVersionReleased,
|
||||
})}
|
||||
data-cy="autosave-indicator"
|
||||
>
|
||||
<SaveIndicator isSaving={isSaving} saveError={saveError} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isUserInZeroToOneFlow && (
|
||||
<Steps
|
||||
steps={aiGenerationMetadata?.steps?.map((step) => ({ label: step.name, value: step.id })) ?? []}
|
||||
activeStep={aiGenerationMetadata?.active_step}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isUserInZeroToOneFlow && (
|
||||
<>
|
||||
<HeaderActions darkMode={darkMode} />
|
||||
<div className="d-flex align-items-center">
|
||||
<div style={{ width: '100px' }}>
|
||||
<span
|
||||
className={cx('autosave-indicator tj-text-xsm', {
|
||||
'autosave-indicator-saving': isSaving,
|
||||
'text-danger': saveError,
|
||||
'd-none': isVersionReleased,
|
||||
})}
|
||||
data-cy="autosave-indicator"
|
||||
>
|
||||
{getSaveIndicator()}
|
||||
</span>
|
||||
</div>
|
||||
{shouldEnableMultiplayer && (
|
||||
<div className="mx-2 p-2">
|
||||
<RealtimeAvatars />
|
||||
</div>
|
||||
)}
|
||||
{shouldEnableMultiplayer && <UpdatePresenceMultiPlayer />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isModuleEditor && !isUserInZeroToOneFlow && <div className="navbar-seperator"></div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isUserInZeroToOneFlow && (
|
||||
{isUserInZeroToOneFlow && (
|
||||
<Steps
|
||||
steps={
|
||||
aiGenerationMetadata.steps
|
||||
?.filter((step) => !step.hidden)
|
||||
?.map((step) => ({
|
||||
label: step.name,
|
||||
value: step.id,
|
||||
})) ?? []
|
||||
}
|
||||
activeStep={activeStep}
|
||||
classes={{ stepsContainer: 'tw-mx-auto' }}
|
||||
/>
|
||||
)}
|
||||
{!isUserInZeroToOneFlow && <HeaderActions darkMode={darkMode} />}
|
||||
|
||||
{!isUserInZeroToOneFlow && (
|
||||
<div className="tw-flex tw-flex-row tw-items-center tw-justify-end tw-grow-1 tw-w-full">
|
||||
<div className="d-flex align-items-center p-0">
|
||||
<div className="d-flex version-manager-container p-0 mx-2 align-items-center ">
|
||||
<div className="d-flex version-manager-container p-0 align-items-center gap-0">
|
||||
{!isModuleEditor && (
|
||||
<>
|
||||
<AppEnvironments darkMode={darkMode} />
|
||||
<AppVersionsManager darkMode={darkMode} />
|
||||
<GitSyncManager />
|
||||
<VersionManagerErrorBoundary>
|
||||
<VersionManagerDropdown darkMode={darkMode} />
|
||||
</VersionManagerErrorBoundary>
|
||||
<RightTopHeaderButtons isModuleEditor={isModuleEditor} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isUserInZeroToOneFlow && (
|
||||
<>
|
||||
<RightTopHeaderButtons isModuleEditor={isModuleEditor} />
|
||||
<BuildSuggestions />
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { capitalize } from 'lodash';
|
|||
import XenvSvg from '@assets/images/icons/x-env.svg';
|
||||
import '@/_styles/versions.scss';
|
||||
import { LicenseTooltip } from '@/LicenseTooltip';
|
||||
import { Layers } from 'lucide-react';
|
||||
|
||||
const EnvironmentSelectBox = React.memo(function EnvironmentSelectBox({ options, currentEnv, licenseValid }) {
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
|
|
@ -68,26 +69,8 @@ const EnvironmentSelectBox = React.memo(function EnvironmentSelectBox({ options,
|
|||
data-cy="env-container"
|
||||
>
|
||||
<div className="d-inline-flex align-items-center env-header" onClick={() => setShowOptions(!showOptions)}>
|
||||
<XenvSvg />
|
||||
<span className="tj-text-xsm env-switch-text">Env</span>
|
||||
<Layers width="16" height="16" />
|
||||
<div data-cy="list-current-env-name">{capitalize(currentEnv.name)}</div>
|
||||
<div className={`env-arrow ${showOptions ? 'env-arrow-roate' : ''}`}>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 6 10"
|
||||
fill={'var(--slate12)'}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-cy="env-arrow"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.942841 0.344988C0.673302 0.560619 0.629601 0.953927 0.845232 1.22347L3.86622 4.9997L0.845232 8.77593C0.629601 9.04547 0.673301 9.43878 0.94284 9.65441C1.21238 9.87004 1.60569 9.82634 1.82132 9.5568L5.15465 5.39013C5.33726 5.16187 5.33726 4.83753 5.15465 4.60926L1.82132 0.442596C1.60569 0.173058 1.21238 0.129357 0.942841 0.344988Z"
|
||||
fill={'var(--slate12)'}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{showOptions && (
|
||||
<div className={`env-popover ${darkMode ? 'theme-dark' : ''}`}>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import React, { useEffect, useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import Branch from '@assets/images/icons/branch.svg';
|
||||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
const FreezeVersionInfo = ({
|
||||
|
|
@ -12,22 +10,6 @@ const FreezeVersionInfo = ({
|
|||
const isViewOnly = useStore((state) => state.getShouldFreeze());
|
||||
const isAiOperationInProgress = useStore((state) => state?.ai?.isLoading);
|
||||
|
||||
// const { isUserEditingTheVersion, disableReleasedVersionPopupState } = useAppVersionStore(
|
||||
// (state) => ({
|
||||
// isUserEditingTheVersion: state.isUserEditingTheVersion,
|
||||
// disableReleasedVersionPopupState: state.actions.disableReleasedVersionPopupState,
|
||||
// }),
|
||||
// shallow
|
||||
// );
|
||||
// const changeBackTheState = useCallback(() => {
|
||||
// isUserEditingTheVersion && disableReleasedVersionPopupState();
|
||||
// }, [isUserEditingTheVersion, disableReleasedVersionPopupState]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const intervalId = setInterval(() => changeBackTheState(), 2000);
|
||||
// return () => intervalId && clearInterval(intervalId);
|
||||
// }, [isUserEditingTheVersion, changeBackTheState]);
|
||||
|
||||
if (!isViewOnly || hide || isAiOperationInProgress) return null;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,8 +4,14 @@ import { Tooltip } from 'react-tooltip';
|
|||
import { shallow } from 'zustand/shallow';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { Button } from '@/components/ui/Button/Button';
|
||||
import { Monitor, Smartphone, Play } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAppPreviewLink } from '@/_hooks/useAppPreviewLink';
|
||||
import { ToggleLayoutButtons } from './ToggleLayoutButtons';
|
||||
import { Button as ButtonComponent } from '@/components/ui/Button/Button';
|
||||
|
||||
const HeaderActions = function HeaderActions({ darkMode, showFullWidth }) {
|
||||
const HeaderActions = function HeaderActions({ darkMode, showFullWidth, showPreviewBtn = true }) {
|
||||
const {
|
||||
currentLayout,
|
||||
canUndo,
|
||||
|
|
@ -36,128 +42,50 @@ const HeaderActions = function HeaderActions({ darkMode, showFullWidth }) {
|
|||
for (const element of selectedElems) {
|
||||
element.classList.remove('active-target');
|
||||
}
|
||||
}, []);
|
||||
}, [clearSelectedComponents]);
|
||||
const appPreviewLink = useAppPreviewLink();
|
||||
return (
|
||||
<div className={cx('editor-header-actions', { 'w-100': showFullWidth })} data-cy="header-actions">
|
||||
<div
|
||||
className={cx('tw-flex tw-gap-2 tw-items-center tw-justify-center editor-header-actions', {
|
||||
'w-100': showFullWidth,
|
||||
})}
|
||||
data-cy="header-actions"
|
||||
>
|
||||
{showToggleLayoutBtn && (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
...(currentLayout === 'mobile' && {
|
||||
width: '100%',
|
||||
}),
|
||||
}}
|
||||
className={cx({ 'w-100': showFullWidth })}
|
||||
data-cy="layout-toggle-container"
|
||||
<ToggleLayoutButtons
|
||||
currentLayout={currentLayout}
|
||||
toggleCurrentLayout={toggleCurrentLayout}
|
||||
clearSelectionBorder={clearSelectionBorder}
|
||||
showFullWidth={showFullWidth}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
)}
|
||||
{showPreviewBtn && (
|
||||
|
||||
|
||||
<Link
|
||||
title="Preview"
|
||||
to={appPreviewLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
data-cy="preview-link-button"
|
||||
className="text-decoration-none"
|
||||
style={{ color: 'var(--text-default)' }}
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-center p-1 current-layout"
|
||||
style={{
|
||||
height: 28,
|
||||
background: darkMode ? '#202425' : '#F1F3F5',
|
||||
borderRadius: 6,
|
||||
}}
|
||||
role="tablist"
|
||||
aria-orientation="horizontal"
|
||||
data-cy="layout-toggle-buttons"
|
||||
<ButtonComponent
|
||||
isLucid
|
||||
size="default"
|
||||
variant="outline"
|
||||
leadingIcon="play"
|
||||
data-cy="preview-link-button"
|
||||
style={{ padding: "7px 12px" }}
|
||||
>
|
||||
<button
|
||||
className={cx('btn border-0 p-1', {
|
||||
'bg-transparent': currentLayout !== 'desktop',
|
||||
'bg-white opacity-100': currentLayout === 'desktop',
|
||||
'w-100': showFullWidth,
|
||||
'flex-grow-1': currentLayout === 'mobile',
|
||||
})}
|
||||
style={{ height: 20 }}
|
||||
role="tab"
|
||||
type="button"
|
||||
aria-selected="true"
|
||||
tabIndex="0"
|
||||
onClick={() => {
|
||||
toggleCurrentLayout('desktop');
|
||||
clearSelectionBorder();
|
||||
}}
|
||||
data-cy={`button-change-layout-to-desktop`}
|
||||
>
|
||||
<SolidIcon
|
||||
name="computer"
|
||||
width="14"
|
||||
fill={currentLayout === 'desktop' ? 'var(--slate12)' : 'var(--slate8)'}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className={cx('btn border-0 p-1', {
|
||||
'bg-transparent': currentLayout !== 'mobile',
|
||||
'bg-white opacity-100': currentLayout === 'mobile',
|
||||
'w-100': showFullWidth,
|
||||
'flex-grow-1': currentLayout === 'mobile',
|
||||
})}
|
||||
role="tab"
|
||||
type="button"
|
||||
style={{ height: 20 }}
|
||||
aria-selected="false"
|
||||
tabIndex="-1"
|
||||
onClick={() => {
|
||||
toggleCurrentLayout('mobile');
|
||||
clearSelectionBorder();
|
||||
}}
|
||||
data-cy={`button-change-layout-to-mobile`}
|
||||
>
|
||||
<SolidIcon
|
||||
name="mobile"
|
||||
width="14"
|
||||
fill={currentLayout !== 'desktop' ? 'var(--slate12)' : 'var(--slate8)'}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showUndoRedoBtn && (
|
||||
<div className="undo-redo-container" data-cy="undo-redo-container">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleUndo();
|
||||
}}
|
||||
className="tj-ghost-black-btn"
|
||||
data-tooltip-id="tooltip-for-undo"
|
||||
data-tooltip-content="Undo"
|
||||
data-cy={`editor-undo-button`}
|
||||
>
|
||||
<SolidIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill={darkMode ? '#fff' : '#2c3e50'}
|
||||
name="arrowforwardup"
|
||||
className={cx('cursor-pointer', {
|
||||
disabled: !canUndo,
|
||||
})}
|
||||
data-cy="undo-icon"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleRedo();
|
||||
}}
|
||||
className="tj-ghost-black-btn"
|
||||
data-tooltip-id="tooltip-for-redo"
|
||||
data-tooltip-content="Redo"
|
||||
data-cy={`editor-redo-button`}
|
||||
>
|
||||
<SolidIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill={darkMode ? '#fff' : '#2c3e50'}
|
||||
name="arrowbackup"
|
||||
className={cx('cursor-pointer', {
|
||||
disabled: !canRedo,
|
||||
})}
|
||||
data-cy="redo-icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Preview
|
||||
</ButtonComponent>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Tooltip id="tooltip-for-undo" className="tooltip" data-cy="undo-tooltip" />
|
||||
<Tooltip id="tooltip-for-redo" className="tooltip" data-cy="redo-tooltip" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,25 @@ import Modal from 'react-bootstrap/Modal';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import '@/_styles/versions.scss';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
export default function ReleaseConfirmation(props) {
|
||||
const { onClose, onConfirm, show } = props;
|
||||
const { t } = useTranslation();
|
||||
const darkMode = props.darkMode ?? (localStorage.getItem('darkMode') === 'true' || false);
|
||||
|
||||
const { name, releasedVersionId, versionsPromotedToEnvironment, developmentVersions } = useStore(
|
||||
(state) => ({
|
||||
name: state?.selectedVersion?.name,
|
||||
releasedVersionId: state.releasedVersionId,
|
||||
versionsPromotedToEnvironment: state.versionsPromotedToEnvironment,
|
||||
developmentVersions: state.developmentVersions,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
const releasedVersionName =
|
||||
versionsPromotedToEnvironment.find((v) => v.id === releasedVersionId)?.name ||
|
||||
developmentVersions.find((v) => v.id === releasedVersionId)?.name;
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
|
|
@ -19,7 +32,7 @@ export default function ReleaseConfirmation(props) {
|
|||
contentClassName={`release-confirm-dialogue-modal ${darkMode ? 'dark-theme' : ''}`}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title data-cy="modal-title">Release Version</Modal.Title>
|
||||
<Modal.Title data-cy="modal-title">Release {name}</Modal.Title>
|
||||
<svg
|
||||
onClick={onClose}
|
||||
className="cursor-pointer"
|
||||
|
|
@ -40,7 +53,11 @@ export default function ReleaseConfirmation(props) {
|
|||
</Modal.Header>
|
||||
|
||||
<Modal.Body className="env-confirm-dialogue-body" data-cy="confirm-dialogue-box-text">
|
||||
<div className="env-change-info">Are you sure you want to release this version?</div>
|
||||
<div className="env-change-info">
|
||||
{releasedVersionId === null
|
||||
? `Are you sure you want to release version ${name}?`
|
||||
: `Releasing version ${name} will override ${releasedVersionName}. Are you sure you want to continue?`}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="env-modal-footer">
|
||||
<ButtonSolid variant="tertiary" onClick={onClose} data-cy="cancel-button">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import InfoSvg from '@assets/images/info.svg';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import { useAppDataStore } from '@/_stores/appDataStore';
|
|||
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
|
||||
import InfoIcon from '@assets/images/icons/info.svg';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { Button } from '@/components/ui/Button/Button';
|
||||
import { Share2 } from 'lucide-react';
|
||||
|
||||
class ManageAppUsersComponent extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -188,22 +190,20 @@ class ManageAppUsersComponent extends React.Component {
|
|||
const { isHovered } = this.state.isHovered;
|
||||
|
||||
return (
|
||||
<div title={'Share'} className="manage-app-users" data-cy="share-button-link">
|
||||
<span
|
||||
className="manage-app-users tj-secondary-btn editor-header-icon cursor-pointer"
|
||||
onClick={() => {
|
||||
this.validateThePreExistingSlugs();
|
||||
this.setState({ showModal: true });
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cx('d-flex', {
|
||||
'share-disabled': false,
|
||||
})}
|
||||
<div className="manage-app-users" data-cy="share-button-link">
|
||||
<ToolTip message="Share" placement="bottom">
|
||||
<Button
|
||||
variant="ghost"
|
||||
iconOnly
|
||||
onClick={() => {
|
||||
this.validateThePreExistingSlugs();
|
||||
this.setState({ showModal: true });
|
||||
}}
|
||||
>
|
||||
<SolidIcon name="share" width="14" className="cursor-pointer" fill="#3E63DD" />
|
||||
</span>
|
||||
</span>
|
||||
<Share2 width="16" height="16" className="tw-text-icon-strong" />
|
||||
</Button>
|
||||
</ToolTip>
|
||||
|
||||
<Modal
|
||||
show={this.state.showModal}
|
||||
size="lg"
|
||||
|
|
|
|||
|
|
@ -5,17 +5,19 @@ import { ManageAppUsers } from './ManageAppUsers';
|
|||
import { shallow } from 'zustand/shallow';
|
||||
import queryString from 'query-string';
|
||||
import { isEmpty } from 'lodash';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import GitSyncManager from '../GitSyncManager';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { PromoteReleaseButton } from '@/modules/Appbuilder/components';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const RightTopHeaderButtons = ({ isModuleEditor }) => {
|
||||
return (
|
||||
<div className="d-flex justify-content-end navbar-right-section" style={{ width: '300px', paddingRight: '12px' }}>
|
||||
<div className=" release-buttons navbar-nav flex-row">
|
||||
<div className="d-flex justify-content-end navbar-right-section">
|
||||
<div className=" release-buttons">
|
||||
<GitSyncManager />
|
||||
<div className="tw-hidden navbar-seperator" />
|
||||
<PreviewAndShareIcons />
|
||||
{!isModuleEditor && <PromoteReleaseButton />}
|
||||
{/* {!isModuleEditor && <PromoteReleaseButton />} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -70,35 +72,25 @@ const PreviewAndShareIcons = () => {
|
|||
}, [slug, currentVersionId, editingVersion, selectedEnvironment?.id, currentPageHandle]);
|
||||
|
||||
return (
|
||||
<div className="preview-share-wrap navbar-nav flex-row" style={{ gap: '4px' }}>
|
||||
<div className="nav-item">
|
||||
{appId && (
|
||||
<ManageAppUsers
|
||||
currentEnvironment={selectedEnvironment}
|
||||
multiEnvironmentEnabled={featureAccess?.multiEnvironment}
|
||||
app={app}
|
||||
appId={appId}
|
||||
slug={slug}
|
||||
pageHandle={currentPageHandle}
|
||||
darkMode={darkMode}
|
||||
isVersionReleased={isVersionReleased}
|
||||
isPublic={isPublic ?? false}
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
<div className="preview-share-wrap navbar-nav flex-row tw-mr-1">
|
||||
<div className="nav-item">
|
||||
{appId && (
|
||||
<ManageAppUsers
|
||||
currentEnvironment={selectedEnvironment}
|
||||
multiEnvironmentEnabled={featureAccess?.multiEnvironment}
|
||||
app={app}
|
||||
appId={appId}
|
||||
slug={slug}
|
||||
pageHandle={currentPageHandle}
|
||||
darkMode={darkMode}
|
||||
isVersionReleased={isVersionReleased}
|
||||
isPublic={isPublic ?? false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="nav-item">
|
||||
<Link
|
||||
title="Preview"
|
||||
to={appPreviewLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
data-cy="preview-link-button"
|
||||
className="editor-header-icon tj-secondary-btn"
|
||||
>
|
||||
<SolidIcon name="eyeopen" width="14" fill="#3E63DD" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
38
frontend/src/AppBuilder/Header/SaveIndicator.jsx
Normal file
38
frontend/src/AppBuilder/Header/SaveIndicator.jsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { CloudCheck, CloudAlert } from 'lucide-react';
|
||||
import Loader from '@/ToolJetUI/Loader/Loader';
|
||||
import { ToolTip } from '@/_components';
|
||||
|
||||
const SaveIndicator = ({ isSaving, saveError }) => {
|
||||
if (isSaving) {
|
||||
return (
|
||||
<ToolTip message="Saving in progress! Don't close the app yet." placement="bottom">
|
||||
<div className="d-flex align-items-center" style={{ gap: '4px' }}>
|
||||
<div className="d-flex align-items-center" style={{ width: '16px', height: '16px' }}>
|
||||
<Loader width={16} height={16} reverse={true} />
|
||||
</div>
|
||||
<p className="mb-0 mx-1 text-center tw-text-text-default">Saving...</p>
|
||||
</div>
|
||||
</ToolTip>
|
||||
);
|
||||
}
|
||||
if (saveError) {
|
||||
return (
|
||||
<ToolTip message="Could not save changes" placement="bottom">
|
||||
<div className="d-flex align-items-center" style={{ gap: '4px' }}>
|
||||
<CloudAlert width={16} height={16} color="var(--icon-danger)" />
|
||||
<p className="mb-0 text-center tw-text-text-danger">Could not save changes</p>
|
||||
</div>
|
||||
</ToolTip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ToolTip message="Changes saved!" placement="bottom">
|
||||
<div className="d-flex align-items-center" style={{ gap: '4px' }}>
|
||||
<CloudCheck width={16} height={16} color="var(--icon-success)" />
|
||||
</div>
|
||||
</ToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveIndicator;
|
||||
|
|
@ -9,11 +9,11 @@ function Step({ stepNo, label, active, completed }) {
|
|||
return (
|
||||
<div className="tw-flex tw-items-center tw-gap-1.5 tw-px-2.5 tw-py-1">
|
||||
{completed ? (
|
||||
<CheckCircle />
|
||||
<CheckCircle width="16" height="16" />
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
'tw-bg-text-placeholder tw-text-white tw-text-[0.625rem] tw-rounded-full tw-size-3.5 tw-flex tw-justify-center tw-items-center',
|
||||
'tw-bg-text-placeholder tw-text-white tw-text-[0.625rem] tw-leading-none tw-rounded-full tw-size-3.5 tw-flex tw-justify-center tw-items-center',
|
||||
{ '!tw-bg-black': active }
|
||||
)}
|
||||
>
|
||||
|
|
@ -22,7 +22,7 @@ function Step({ stepNo, label, active, completed }) {
|
|||
)}
|
||||
|
||||
<p
|
||||
className={cn('tw-text-base tw-text-text-placeholder tw-font-medium tw-mb-0', {
|
||||
className={cn('tw-text-text-placeholder tw-font-title-default tw-mb-0', {
|
||||
'tw-text-text-primary': completed || active,
|
||||
})}
|
||||
>
|
||||
|
|
@ -39,12 +39,12 @@ function Connector({ completed }) {
|
|||
}
|
||||
|
||||
// sequential steps
|
||||
export default function Steps({ steps, activeStep }) {
|
||||
export default function Steps({ steps, activeStep, classes = null }) {
|
||||
const activeStepIndex = steps.findIndex((step) => step.value === activeStep);
|
||||
const currentStepIdx = activeStepIndex === -1 ? 0 : activeStepIndex;
|
||||
|
||||
return (
|
||||
<div className="tw-flex tw-items-center tw-gap-1 tw-py-2">
|
||||
<div className={cn('tw-flex tw-items-center tw-gap-1 tw-py-2', classes?.stepsContainer)}>
|
||||
{Children.toArray(
|
||||
steps.map((step, index) => {
|
||||
const isActive = index === currentStepIdx;
|
||||
|
|
|
|||
65
frontend/src/AppBuilder/Header/ToggleLayoutButtons.jsx
Normal file
65
frontend/src/AppBuilder/Header/ToggleLayoutButtons.jsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import { Button } from '@/components/ui/Button/Button';
|
||||
import { Monitor, Smartphone } from 'lucide-react';
|
||||
|
||||
export function ToggleLayoutButtons({
|
||||
currentLayout,
|
||||
toggleCurrentLayout,
|
||||
clearSelectionBorder,
|
||||
showFullWidth,
|
||||
darkMode,
|
||||
}) {
|
||||
return (
|
||||
<div className={cx({ '!tw-w-100': showFullWidth })} data-cy="layout-toggle-container">
|
||||
<div
|
||||
className="d-flex align-items-center p-1 current-layout tw-gap-0.5"
|
||||
role="tablist"
|
||||
aria-orientation="horizontal"
|
||||
data-cy="layout-toggle-buttons"
|
||||
>
|
||||
<OverlayTrigger placement="bottom" overlay={<Tooltip id="desktop-layout-tooltip">Desktop Layout</Tooltip>}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cx({
|
||||
'tw-pressed tw-bg-button-outline-pressed': currentLayout === 'desktop',
|
||||
})}
|
||||
iconOnly
|
||||
aria-label="Switch to desktop layout"
|
||||
aria-selected={currentLayout === 'desktop'}
|
||||
tabIndex={currentLayout === 'desktop' ? 0 : -1}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
toggleCurrentLayout('desktop');
|
||||
clearSelectionBorder();
|
||||
}}
|
||||
data-cy="button-change-layout-to-desktop"
|
||||
>
|
||||
<Monitor width="16" height="16" className="tw-text-icon-strong" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger placement="bottom" overlay={<Tooltip id="mobile-layout-tooltip">Mobile Layout</Tooltip>}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cx({
|
||||
'tw-pressed tw-bg-button-outline-pressed': currentLayout === 'mobile',
|
||||
})}
|
||||
iconOnly
|
||||
aria-label="Switch to mobile layout"
|
||||
aria-selected={currentLayout === 'mobile'}
|
||||
tabIndex={currentLayout === 'mobile' ? 0 : -1}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
toggleCurrentLayout('mobile');
|
||||
clearSelectionBorder();
|
||||
}}
|
||||
data-cy="button-change-layout-to-mobile"
|
||||
>
|
||||
<Smartphone width="16" height="16" className="tw-text-icon-strong" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import { Button } from '@/components/ui/Button/Button';
|
||||
import './style.scss';
|
||||
|
||||
const CreateDraftButton = ({ onClick, disabled = false, darkMode = false }) => {
|
||||
return (
|
||||
<div className={cx('create-draft-button', { 'dark-theme theme-dark': darkMode })} style={{ padding: '8px' }}>
|
||||
<ToolTip
|
||||
message={'Draft version can only be created from saved versions.'}
|
||||
tooltipClassName="create-draft-button-tooltip"
|
||||
placement="left"
|
||||
show={disabled}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
leadingIcon="plus"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className="tw-w-full"
|
||||
data-cy="create-draft-version-button"
|
||||
>
|
||||
Create draft version
|
||||
</Button>
|
||||
</ToolTip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDraftButton;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
|
||||
import BaseCreateDraftVersionModal from '@/modules/common/components/BaseCreateDraftVersionModal';
|
||||
const CreateDraftVersionModal = (props) => {
|
||||
return <BaseCreateDraftVersionModal {...props} />;
|
||||
};
|
||||
export default withEditionSpecificComponent(CreateDraftVersionModal, 'Appbuilder');
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Button } from 'react-bootstrap';
|
||||
import Select from '@/_ui/Select';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
// need to review this component -> discuss with Vijaykant
|
||||
|
||||
const CreateDraftVersionModal1 = ({ show, onClose, appId, versions, environments = [] }) => {
|
||||
const { t } = useTranslation();
|
||||
const { createDraftVersionAction } = useStore(
|
||||
(state) => ({
|
||||
createDraftVersionAction: state.createDraftVersionAction,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
const [draftDescription, setDraftDescription] = useState('');
|
||||
const [selectedVersionForCreation, setSelectedVersionForCreation] = useState(null);
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
// Filter out draft versions for "Create from" dropdown
|
||||
const finalizedVersions = versions.filter((v) => v.status !== 'DRAFT');
|
||||
|
||||
// Set default environment to development (priority = 1)
|
||||
useEffect(() => {
|
||||
if (environments.length > 0 && !selectedEnvironment) {
|
||||
const developmentEnv = environments.find((env) => env.priority === 1);
|
||||
if (developmentEnv) {
|
||||
setSelectedEnvironment(developmentEnv.id);
|
||||
}
|
||||
}
|
||||
}, [environments, selectedEnvironment]);
|
||||
|
||||
// Set default version to latest finalized version
|
||||
useEffect(() => {
|
||||
if (finalizedVersions.length > 0 && !selectedVersionForCreation) {
|
||||
const latestVersion = finalizedVersions[finalizedVersions.length - 1];
|
||||
setSelectedVersionForCreation(latestVersion);
|
||||
}
|
||||
}, [finalizedVersions, selectedVersionForCreation]);
|
||||
|
||||
const versionOptions = finalizedVersions.map((version) => ({
|
||||
label: version.name,
|
||||
value: version,
|
||||
}));
|
||||
|
||||
const environmentOptions = environments.map((env) => ({
|
||||
label: env.name,
|
||||
value: env.id,
|
||||
}));
|
||||
|
||||
const handleCreateDraft = async () => {
|
||||
// Validation
|
||||
if (!draftDescription.trim()) {
|
||||
toast.error('Draft description should not be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
if (draftDescription.trim().length > 500) {
|
||||
toast.error('Draft description should not be longer than 500 characters');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedVersionForCreation) {
|
||||
toast.error('Please select a version to create draft from');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
toast.error('Please select an environment');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
|
||||
try {
|
||||
await createDraftVersionAction(appId, {
|
||||
versionFromId: selectedVersionForCreation.id,
|
||||
environmentId: selectedEnvironment,
|
||||
versionDescription: draftDescription,
|
||||
});
|
||||
|
||||
toast.success('Draft version created successfully');
|
||||
setDraftDescription('');
|
||||
onClose();
|
||||
} catch (error) {
|
||||
if (error?.data?.message?.includes('draft version already exists')) {
|
||||
toast.error('A draft version already exists for this app');
|
||||
} else {
|
||||
toast.error(error?.data?.message || 'Failed to create draft version');
|
||||
}
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isCreating) {
|
||||
setDraftDescription('');
|
||||
setSelectedVersionForCreation(null);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
onHide={handleClose}
|
||||
centered
|
||||
className="create-draft-modal"
|
||||
backdrop={isCreating ? 'static' : true}
|
||||
keyboard={!isCreating}
|
||||
>
|
||||
<Modal.Header closeButton={!isCreating}>
|
||||
<Modal.Title className="tj-text-sm" style={{ fontWeight: 600 }}>
|
||||
Create Draft Version
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleCreateDraft();
|
||||
}}
|
||||
>
|
||||
{/* Create from Version */}
|
||||
<div className="mb-3">
|
||||
<label className="form-label tj-text-xsm" data-cy="create-draft-from-label">
|
||||
Create draft from
|
||||
</label>
|
||||
<Select
|
||||
options={versionOptions}
|
||||
value={selectedVersionForCreation}
|
||||
onChange={(version) => setSelectedVersionForCreation(version)}
|
||||
useMenuPortal={false}
|
||||
width="100%"
|
||||
maxMenuHeight={150}
|
||||
placeholder="Select version..."
|
||||
isDisabled={isCreating}
|
||||
/>
|
||||
<div className="helper-text">Select which version to use as a starting point for the draft</div>
|
||||
</div>
|
||||
|
||||
{/* Environment Selection */}
|
||||
<div className="mb-3">
|
||||
<label className="form-label tj-text-xsm" data-cy="draft-environment-label">
|
||||
Environment
|
||||
</label>
|
||||
<Select
|
||||
options={environmentOptions}
|
||||
value={environmentOptions.find((opt) => opt.value === selectedEnvironment)}
|
||||
onChange={(option) => setSelectedEnvironment(option?.value)}
|
||||
useMenuPortal={false}
|
||||
width="100%"
|
||||
placeholder="Select environment..."
|
||||
isDisabled={isCreating}
|
||||
/>
|
||||
<div className="helper-text">Select the environment for this draft version</div>
|
||||
</div>
|
||||
|
||||
{/* Draft Description */}
|
||||
<div className="mb-3">
|
||||
<label className="form-label tj-text-xsm" data-cy="draft-description-label">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control tj-text-xsm"
|
||||
data-cy="draft-description-input-field"
|
||||
placeholder="Enter draft description..."
|
||||
disabled={isCreating}
|
||||
value={draftDescription}
|
||||
onChange={(e) => setDraftDescription(e.target.value)}
|
||||
rows={4}
|
||||
minLength="1"
|
||||
maxLength="500"
|
||||
style={{
|
||||
fontFamily: 'IBM Plex Sans, sans-serif',
|
||||
resize: 'vertical',
|
||||
minHeight: '80px',
|
||||
}}
|
||||
/>
|
||||
<div className="helper-text">Add a description for this draft (max 500 characters)</div>
|
||||
</div>
|
||||
|
||||
{/* Info Alert */}
|
||||
<div
|
||||
className="alert alert-info d-flex align-items-center"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
padding: '12px',
|
||||
backgroundColor: 'var(--indigo1)',
|
||||
border: '1px solid var(--indigo3)',
|
||||
borderRadius: '6px',
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style={{ marginRight: '8px', flexShrink: 0 }}>
|
||||
<path
|
||||
d="M8 1C4.13438 1 1 4.13438 1 8C1 11.8656 4.13438 15 8 15C11.8656 15 15 11.8656 15 8C15 4.13438 11.8656 1 8 1ZM8.75 11.5C8.75 11.9125 8.4125 12.25 8 12.25C7.5875 12.25 7.25 11.9125 7.25 11.5V7.75C7.25 7.3375 7.5875 7 8 7C8.4125 7 8.75 7.3375 8.75 7.75V11.5ZM8 6C7.45 6 7 5.55 7 5C7 4.45 7.45 4 8 4C8.55 4 9 4.45 9 5C9 5.55 8.55 6 8 6Z"
|
||||
fill="var(--indigo6)"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
The draft version will be created and you can make changes before promoting it to a finalized version.
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleClose}
|
||||
disabled={isCreating}
|
||||
className="tj-text-xsm"
|
||||
data-cy="cancel-button"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCreateDraft}
|
||||
disabled={isCreating || !draftDescription.trim()}
|
||||
className="tj-text-xsm"
|
||||
data-cy="create-draft-button"
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create Draft'
|
||||
)}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDraftVersionModal1;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue