Merge pull request #10214 from ToolJet/chore/main-to-develop

Merge main back to develop (v2.63.0)
This commit is contained in:
Kavin Venkatachalam 2024-06-28 12:12:40 +05:30 committed by GitHub
commit 9e7c0e6005
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
170 changed files with 45366 additions and 7832 deletions

View file

@ -155,4 +155,4 @@ jobs:
message="Job '${{ env.JOB_NAME }}' failed! tooljet/tooljet-ce:${{ github.event.inputs.image }}"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}

View file

@ -1 +1 @@
2.61.3
2.63.0

View file

@ -296,7 +296,6 @@ describe(
});
});
cy.get(usersSelector.acceptInvite).click();
cy.get('[data-cy="draggable-widget-table1"]').should("be.visible");
});
}

View file

@ -43,7 +43,7 @@ describe("Password reset functionality", () => {
);
cy.get(commonSelectors.forgotPasswordPageSubHeader).should(($el) => {
expect($el.contents().first().text().trim()).to.eq(
commonText.newToTooljetText
"New to"
);
});
cy.get(commonSelectors.createAnAccountLink).verifyVisibleElement(

View file

@ -51,7 +51,6 @@ describe("User signup", () => {
it("Verify invalid invitation link", () => {
cy.log(invitationLink)
cy.visit(invitationLink);
cy.pause()
verifyInvalidInvitationLink();
cy.get(commonSelectors.backtoSignUpButton).click();
cy.get(commonSelectors.SignUpSectionHeader).should("be.visible");

View file

@ -21,7 +21,6 @@ import { buttonText } from "Texts/button";
describe("App Export Functionality", () => {
var data = {};
data.appName1 = `${fake.companyName}-App`;
let currentVersion = "";
let otherVersions = [];
beforeEach(() => {
@ -29,6 +28,7 @@ describe("App Export Functionality", () => {
});
it("Verify the elements of export dialog box", () => {
data.appName1 = `${fake.companyName}-App`;
cy.apiCreateApp(data.appName1);
cy.openApp();
cy.dragAndDropWidget(buttonText.defaultWidgetText);
@ -56,6 +56,9 @@ describe("App Export Functionality", () => {
});
it("Verify 'Export app' functionality of an application", () => {
data.appName1 = `${fake.companyName}-App`;
cy.apiCreateApp(data.appName1);
cy.visit("/");
cy.get(commonSelectors.appHeaderLable).should("be.visible");
@ -96,7 +99,7 @@ describe("App Export Functionality", () => {
.click();
createNewVersion((otherVersions = ["v2"]), (currentVersion = "v1"));
cy.wait(500);
cy.dragAndDropWidget("Toggle Switch", 50, 50);
cy.dragAndDropWidget("Text Input", 50, 50);
cy.waitForAutoSave();
cy.get(appVersionSelectors.currentVersionField((otherVersions = "v2")))
.should("be.visible")

View file

@ -34,9 +34,9 @@ describe("Bulk user upload", () => {
data.firstName = fake.firstName;
data.workspaceName = data.firstName.toLowerCase();
cy.apiLogin()
cy.apiLogin();
cy.apiCreateWorkspace(data.firstName, data.workspaceName);
cy.visit(`${data.workspaceName}`)
cy.visit(`${data.workspaceName}`);
common.navigateToManageUsers();
@ -122,9 +122,9 @@ describe("Bulk user upload", () => {
data.firstName = fake.firstName;
data.workspaceName = data.firstName.toLowerCase();
cy.apiLogin()
cy.apiLogin();
cy.apiCreateWorkspace(data.firstName, data.workspaceName);
cy.visit(`${data.workspaceName}`)
cy.visit(`${data.workspaceName}`);
common.navigateToManageUsers();
cy.get(usersSelector.buttonAddUsers).click();
@ -142,7 +142,10 @@ describe("Bulk user upload", () => {
force: true,
});
cy.get(usersSelector.buttonUploadUsers).click();
cy.wait(10000);
cy.wait(30000);
cy.get(".go2072408551")
.should("be.visible")
.and("have.text", "250 users are being added");
common.searchUser("test12@gmail.com");
cy.contains("td", "test12@gmail.com")
.parent()

View file

@ -131,7 +131,8 @@ describe("user invite flow cases", () => {
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceName);
cy.visit(`${data.workspaceName}`);
enableSignUp();
cy.wait(3000)
setSignupStatus(true, data.workspaceName);
logout();
cy.get(commonSelectors.createAnAccountLink).click();
@ -142,11 +143,6 @@ describe("user invite flow cases", () => {
cy.defaultWorkspaceLogin();
visitWorkspaceInvitation(data.email, data.workspaceName);
cy.clearAndType(commonSelectors.workEmailInputField, data.email);
cy.clearAndType(commonSelectors.passwordInputField, "password");
cy.get(commonSelectors.signInButton).click();
cy.get(usersSelector.acceptInvite).click();
cy.verifyToastMessage(commonSelectors.toastMessage, usersText.inviteToast);
logout();
});

View file

@ -552,21 +552,25 @@ export const defaultSSO = (enable) => {
});
};
export const setSignupStatus = (enable) => {
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request(
{
export const setSignupStatus = (enable, workspaceName = 'My workspace') => {
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: `SELECT id FROM organizations WHERE name = '${workspaceName}'`,
}).then((resp) => {
const workspaceId = resp.rows[0].id;
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request({
method: "PATCH",
url: "http://localhost:3000/api/organizations",
headers: {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
"Tj-Workspace-Id": workspaceId,
Cookie: `tj_auth_token=${cookie.value}`,
},
body: { enableSignUp: enable },
},
{ log: false }
).then((response) => {
expect(response.status).to.equal(200);
}).then((response) => {
expect(response.status).to.equal(200);
});
});
});
};

View file

@ -20,7 +20,7 @@
display: flex;
padding: 1.5rem;
flex: 1 1 auto;
align-items: start;
align-items: flex-start;
}
.card-info {

2
frontend/.gitignore vendored
View file

@ -21,3 +21,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
storybook-static

View file

@ -0,0 +1,14 @@
// storybookDecorators.js
import React from 'react';
export function withColorScheme(story, context) {
const darkMode = context?.globals?.backgrounds?.value === '#333333'; // Access theme mode from globals
const className = darkMode ? 'dark-theme' : '';
return (
<div className={className} style={{ backgroundColor: 'transparent' }}>
{story()}
</div>
);
}

View file

@ -1,6 +1,5 @@
/** @type { import('@storybook/react-webpack5').StorybookConfig } */
import custom from '../webpack.config'
const path = require('path');
import customWebpackConfig from '../webpack.config';
import path from 'path';
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
@ -17,16 +16,19 @@ const config = {
docs: {
autodocs: "tag",
},
webpackFinal: async (config) => {
webpackFinal: async (storybookConfig) => {
return {
...config,
module: { ...config.module, rules: [...config.module.rules, ...custom.module.rules] },
resolve : {
alias : {
'@': path.resolve(__dirname, 'src/')
...storybookConfig,
module: { ...storybookConfig.module, rules: [...storybookConfig.module.rules, ...customWebpackConfig.module.rules] },
resolve: {
...storybookConfig.resolve,
alias: {
...storybookConfig.resolve.alias,
'@': path.resolve(__dirname, '../src/')
}
}
};
},
};
export default config;

View file

@ -1,7 +1,8 @@
/** @type { import('@storybook/react').Preview } */
// import 'bootstrap/dist/css/bootstrap.min.css';
import '../src/_styles/theme.scss';
import './preview.scss'
import './preview.scss';
import { withColorScheme } from './decorators'; // Import the decorator
const preview = {
parameters: {
@ -13,6 +14,7 @@ const preview = {
},
},
},
decorators: [withColorScheme], // Adding the decorator to the decorators array
};
export default preview;

View file

@ -1,2 +1,2 @@
@import '~bootstrap/scss/bootstrap';
@import '../src/_styles/componentdesign.scss'

View file

@ -1 +1 @@
2.61.3
2.63.0

View file

@ -0,0 +1,131 @@
import { decodeEntities } from '@/_helpers/utils';
export const defaultWhiteLabellingSettings = {
WHITE_LABEL_LOGO: 'assets/images/rocket.svg',
WHITE_LABEL_TEXT: 'ToolJet',
WHITE_LABEL_FAVICON: 'assets/images/logo.svg',
};
export const whiteLabellingOptions = {
WHITE_LABEL_LOGO: 'App Logo',
WHITE_LABEL_TEXT: 'Page Title',
WHITE_LABEL_FAVICON: 'Favicon',
};
export async function fetchWhiteLabelDetails() {}
export async function checkWhiteLabelsDefaultState() {
return true;
}
export async function resetToDefaultWhiteLabels() {}
export function retrieveWhiteLabelText() {
return window.public_config?.WHITE_LABEL_TEXT || defaultWhiteLabellingSettings.WHITE_LABEL_TEXT;
}
export function retrieveWhiteLabelLogo() {
return window.public_config?.WHITE_LABEL_LOGO || defaultWhiteLabellingSettings.WHITE_LABEL_LOGO;
}
export function retrieveWhiteLabelFavicon() {
return window.public_config?.WHITE_LABEL_FAVICON || defaultWhiteLabellingSettings.WHITE_LABEL_FAVICON;
}
export const pageTitles = {
INSTANCE_SETTINGS: 'Settings',
WORKSPACE_SETTINGS: 'Workspace settings',
INTEGRATIONS: 'Marketplace',
WORKFLOWS: 'Workflows',
DATABASE: 'Database',
DATA_SOURCES: 'Data sources',
AUDIT_LOGS: 'Audit logs',
ACCOUNT_SETTINGS: 'Profile settings',
SETTINGS: 'Profile settings',
EDITOR: 'Editor',
WORKFLOW_EDITOR: 'workflowEditor',
VIEWER: 'Viewer',
DASHBOARD: 'Dashboard',
WORKSPACE_CONSTANTS: 'Workspace constants',
};
// to set favicon and title from router for individual pages
export async function setFaviconAndTitle(whiteLabelFavicon, whiteLabelText, location) {
if (!whiteLabelFavicon || !whiteLabelText) {
whiteLabelFavicon = await retrieveWhiteLabelFavicon();
whiteLabelText = await retrieveWhiteLabelText();
}
// Set favicon
let links = document.querySelectorAll("link[rel='icon']");
if (links.length === 0) {
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/svg+xml';
document.getElementsByTagName('head')[0].appendChild(link);
links = [link];
}
links.forEach((link) => {
link.href = `${whiteLabelFavicon || defaultWhiteLabellingSettings.WHITE_LABEL_FAVICON}`;
});
// Set title
const isEditorOrViewerGoingToRender = ['/apps/', '/applications/'].some((path) => location?.pathname.includes(path));
if (isEditorOrViewerGoingToRender) {
return;
}
const pathToTitle = {
'instance-settings': pageTitles.INSTANCE_SETTINGS,
'workspace-settings': pageTitles.WORKSPACE_SETTINGS,
integrations: pageTitles.INTEGRATIONS,
workflows: pageTitles.WORKFLOWS,
'data-sources': pageTitles.DATA_SOURCES,
'audit-logs': pageTitles.AUDIT_LOGS,
'account-settings': pageTitles.ACCOUNT_SETTINGS,
settings: pageTitles.INSTANCE_SETTINGS,
login: '',
signUp: '',
error: '',
signup: '',
'organization-invitations': '',
invitation: '',
'forgot-password': '',
'reset-password': '',
'workspace-constants': pageTitles.WORKSPACE_CONSTANTS,
setup: '',
};
const pageTitleKey = Object.keys(pathToTitle).find((path) => location?.pathname.includes(path));
const pageTitle = pathToTitle[pageTitleKey];
//For undefined routes
if (pageTitle === undefined) {
return;
}
if (pageTitleKey && !isEditorOrViewerGoingToRender) {
document.title = pageTitle
? `${decodeEntities(pageTitle)} | ${whiteLabelText || defaultWhiteLabellingSettings.WHITE_LABEL_TEXT}`
: `${decodeEntities(whiteLabelText) || defaultWhiteLabellingSettings.WHITE_LABEL_TEXT}`;
}
}
export async function fetchAndSetWindowTitle(pageDetails) {
const whiteLabelText = retrieveWhiteLabelText();
let pageTitleKey = pageDetails?.page || '';
let pageTitle = '';
switch (pageTitleKey) {
case pageTitles.VIEWER: {
const titlePrefix = pageDetails?.preview ? 'Preview - ' : '';
pageTitle = `${titlePrefix}${pageDetails?.appName || 'My App'}`;
break;
}
case pageTitles.EDITOR:
case pageTitles.WORKFLOW_EDITOR: {
pageTitle = pageDetails?.appName || 'My App';
break;
}
default: {
pageTitle = pageTitleKey;
break;
}
}
document.title = !(pageDetails?.preview === false) ? `${pageTitle} | ${whiteLabelText}` : `${pageTitle}`;
}

17
frontend/components.json Normal file
View file

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/theme.scss",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

9
frontend/netlify.toml Normal file
View file

@ -0,0 +1,9 @@
[build]
base = "frontend/"
publish = "storybook-static"
command = "npx storybook build"
[template.environment]
NODE_ENV = "production"
NODE_VERSION = "18.18.2"
NPM_VERSION = "9.8.1"

30523
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,6 +19,7 @@
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-popover": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toggle-group": "^1.0.4",
"@react-google-maps/api": "^2.18.1",
"@sentry/react": "^7.100.1",
@ -35,6 +36,7 @@
"acorn": "^8.11.3",
"axios": "^1.3.3",
"bootstrap": "^5.2.3",
"class-variance-authority": "^0.7.0",
"classnames": "^2.3.2",
"deep-object-diff": "^1.1.9",
"dompurify": "^3.0.0",
@ -106,10 +108,13 @@
"react-tooltip": "^5.8.1",
"react-virtuoso": "^4.1.0",
"react-zoom-pan-pinch": "^2.6.1",
"rfdc": "^1.3.1",
"rxjs": "^7.8.0",
"semver": "^7.3.8",
"string-hash": "^1.1.3",
"superstruct": "^1.0.3",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"tinycolor2": "^1.6.0",
"url-join": "^5.0.0",
"use-react-router-breadcrumbs": "^4.0.1",
@ -137,6 +142,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"autoprefixer": "^10.4.17",
"babel-loader": "^9.1.2",
"babel-plugin-console-source": "^2.0.5",
"babel-plugin-import": "^1.13.6",
@ -158,10 +164,13 @@
"jest": "^29.4.2",
"node-sass": "^8.0.0",
"path": "^0.12.7",
"postcss": "^8.4.35",
"postcss-loader": "^8.1.0",
"prettier": "^2.8.4",
"sass-loader": "^13.2.0",
"storybook": "^7.2.1",
"style-loader": "^3.3.1",
"tailwindcss": "^3.4.1",
"terser-webpack-plugin": "^5.3.6",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
@ -192,7 +201,7 @@
"format": "eslint . --fix '**/*.{js,jsx}'",
"test": "jest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"build-storybook": "npx storybook build"
},
"eslintConfig": {
"extends": "react-app"

View file

@ -0,0 +1,8 @@
// postcss.config.js
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
// Other PostCSS plugins if needed
],
};

View file

@ -39,6 +39,7 @@ import { ManageOrgUsers } from '@/ManageOrgUsers';
import { ManageGroupPermissions } from '@/ManageGroupPermissions';
import OrganizationLogin from '@/_components/OrganizationLogin/OrganizationLogin';
import { ManageOrgVars } from '@/ManageOrgVars';
import { setFaviconAndTitle } from '@white-label/whiteLabelling';
const AppWrapper = (props) => {
const { isAppDarkMode } = useAppDarkMode();
@ -78,6 +79,7 @@ class AppComponent extends React.Component {
componentDidMount() {
authorizeWorkspace();
this.fetchMetadata();
setFaviconAndTitle(null, null, this.props.location);
setInterval(this.fetchMetadata, 1000 * 60 * 60 * 1);
}
// check if its getting routed from editor

View file

@ -9,6 +9,12 @@ import Spinner from '@/_ui/Spinner';
import { withRouter } from '@/_hoc/withRouter';
import { onLoginSuccess } from '@/_helpers/platform/utils/auth.utils';
import { updateCurrentSession } from '@/_helpers/authorizeWorkspace';
import {
retrieveWhiteLabelText,
setFaviconAndTitle,
retrieveWhiteLabelFavicon,
checkWhiteLabelsDefaultState,
} from '@white-label/whiteLabelling';
class OrganizationInvitationPageComponent extends React.Component {
constructor(props) {
super(props);
@ -21,10 +27,18 @@ class OrganizationInvitationPageComponent extends React.Component {
this.organizationId = new URLSearchParams(props?.location?.search).get('oid');
this.organizationToken = new URLSearchParams(props?.location?.search).get('organizationToken');
this.source = new URLSearchParams(props?.location?.search).get('source');
this.whiteLabelText = retrieveWhiteLabelText();
this.whiteLabelFavicon = retrieveWhiteLabelFavicon();
}
componentDidMount() {
authenticationService.deleteLoginOrganizationId();
setFaviconAndTitle(this.whiteLabelText, this.whiteLabelFavicon, this.props?.location);
checkWhiteLabelsDefaultState(this.organizationId).then((res) => {
this.setState({ defaultState: res });
this.whiteLabelText = retrieveWhiteLabelText();
this.whiteLabelFavicon = retrieveWhiteLabelFavicon();
});
document.addEventListener('keydown', this.handleEnterKey);
}
@ -74,14 +88,14 @@ class OrganizationInvitationPageComponent extends React.Component {
<form action="." method="get" autoComplete="off">
<div className="common-auth-container-wrapper">
<h2 className="common-auth-section-header org-invite-header" data-cy="invite-page-header">
Join {organizationName ? organizationName : 'ToolJet'}
Join {organizationName ? organizationName : this.whiteLabelText}
</h2>
<div className="invite-sub-header" data-cy="invite-page-sub-header">
{`You are invited to ${
organizationName
? `a workspace ${organizationName}. Accept the invite to join the workspace.`
: 'ToolJet.'
: this.whiteLabelText
}`}
</div>

View file

@ -2,34 +2,15 @@ import React from 'react';
import HydrateWithResolveReferences from './Middlewares/HydrateWithResolveReferences';
import BoxUI from './BoxUI';
import _ from 'lodash';
import { useEditorStore } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
function deepEqualityCheckusingLoDash(obj1, obj2) {
return _.isEqual(obj1, obj2);
}
import { shouldUpdate } from './ControlledComponentToRender';
export const shouldUpdate = (prevProps, nextProps) => {
return (
deepEqualityCheckusingLoDash(prevProps?.id, nextProps?.id) &&
deepEqualityCheckusingLoDash(prevProps?.component?.definition, nextProps?.component?.definition) &&
prevProps?.width === nextProps?.width &&
prevProps?.height === nextProps?.height
);
};
export const Box = (props) => {
export const Box = React.memo((props) => {
const { id, component, mode, customResolvables } = props;
/**
* !This component does not consume the value returned from the below hook.
* Only purpose of the hook is to force one rerender the component
* */
useEditorStore((state) => state.componentsNeedsUpdateOnNextRender.find((compId) => compId === id), shallow);
return (
<HydrateWithResolveReferences id={id} mode={mode} component={component} customResolvables={customResolvables}>
<BoxUI {...props} />
</HydrateWithResolveReferences>
);
};
}, shouldUpdate);

View file

@ -1,6 +1,7 @@
import _ from 'lodash';
import { copilotService } from '@/_services/copilot.service';
import { toast } from 'react-hot-toast';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export async function getRecommendation(currentContext, query, lang = 'javascript') {
const words = query.split(' ');
@ -66,7 +67,7 @@ function getResult(suggestionList, query) {
}
export function getSuggestionKeys(refState) {
const state = _.cloneDeep(refState);
const state = deepClone(refState);
const queries = state['queries'];
const actions = [
'runQuery',

View file

@ -1,5 +1,5 @@
/* eslint-disable import/no-unresolved */
import React, { useContext, useEffect } from 'react';
import React, { useContext, useEffect, useRef } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { defaultKeymap } from '@codemirror/commands';
@ -16,6 +16,7 @@ import CodeHinter from './CodeHinter';
import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext';
import { createReferencesLookup } from '@/_stores/utils';
import { PreviewBox } from './PreviewBox';
import { debounce } from 'lodash';
const langSupport = Object.freeze({
javascript: javascript(),
@ -44,38 +45,22 @@ const MultiLineCodeEditor = (props) => {
delayOnChange = true, // Added this prop to immediately update the onBlurUpdate callback
} = props;
const [currentValue, setCurrentValue] = React.useState(() => initialValue);
const context = useContext(CodeHinterContext);
const { suggestionList } = createReferencesLookup(context, true);
const diffOfCurrentValue = React.useRef(null);
const currentValueRef = useRef(initialValue);
const handleChange = React.useCallback((val) => {
setCurrentValue(val);
const diff = val.length - currentValue.length;
if (diff > 0) {
diffOfCurrentValue.current = val.slice(-diff);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleChange = (val) => (currentValueRef.current = val);
const handleOnBlur = () => {
if (!delayOnChange) return onChange(currentValue);
if (!delayOnChange) return onChange(currentValueRef.current);
setTimeout(() => {
onChange(currentValue);
onChange(currentValueRef.current);
}, 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
};
useEffect(() => {
setCurrentValue(initialValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lang]);
const heightInPx = typeof height === 'string' && height?.includes('px') ? height : `${height}px`;
const theme = darkMode ? okaidia : githubLight;
@ -221,7 +206,7 @@ const MultiLineCodeEditor = (props) => {
<ErrorBoundary>
<div className="codehinter-container w-100 " data-cy={`${cyLabel}-input-field`} style={{ height: '100%' }}>
<CodeMirror
value={currentValue}
value={initialValue}
placeholder={placeholder}
height={'100%'}
minHeight={heightInPx}
@ -257,7 +242,7 @@ const MultiLineCodeEditor = (props) => {
{showPreview && (
<div className="multiline-previewbox-wrapper">
<PreviewBox
currentValue={currentValue}
currentValue={currentValueRef.current}
validationSchema={null}
setErrorStateActive={() => null}
componentId={null}

View file

@ -69,6 +69,7 @@ export const PreviewBox = ({
useEffect(() => {
const [valid, _error, newValue, resolvedValue] = resolveReferences(currentValue, validationSchema, customVariables);
if (isWorkspaceVariable || !validationSchema || isEmpty(validationSchema)) {
return setResolvedValue(newValue);
}
@ -364,6 +365,7 @@ const PreviewCodeBlock = ({ code, isExpectValue = false }) => {
rootName={false}
theme={darkMode ? 'dark' : 'light'}
groupArraysAfterLength={hasDeepChild ? 10 : 100}
maxDisplayLength={hasDeepChild ? 10 : 50}
/>
</div>
);

View file

@ -78,7 +78,7 @@
max-width: 100% !important;
width: 100% !important;
display: flex !important;
align-items: start !important;
align-items: flex-start !important;
justify-content: space-between !important;
padding: 0.35rem !important;
font-size: 11px !important;
@ -533,9 +533,10 @@
}
}
.disabled-cursor{
.disabled-cursor {
cursor: not-allowed;
}
.disabled-pointerevents{
.disabled-pointerevents {
pointer-events: none;
}

View file

@ -4,7 +4,8 @@ import _, { isEmpty } from 'lodash';
import { useCurrentStateStore } from '@/_stores/currentStateStore';
import { any } from 'superstruct';
import { generateSchemaFromValidationDefinition, validate } from '../component-properties-validation';
import { hasCircularDependency, resolveReferences as olderResolverMethod } from '@/_helpers/utils';
import { hasCircularDependency } from '@/_helpers/utils';
import { validateMultilineCode } from '@/_helpers/utility';
const acorn = require('acorn');
@ -240,6 +241,16 @@ export const resolveReferences = (query, validationSchema, customResolvers = {})
return resolveWorkspaceVariables(query);
}
if (query?.startsWith('{{') && query?.endsWith('}}')) {
const { status, data } = validateMultilineCode(query);
if (status === 'failed') {
const errMessage = `${data.message} - ${data.description}`;
return [false, errMessage, query, query];
}
}
if ((!validationSchema || isEmpty(validationSchema)) && (!query?.includes('{{') || !query?.includes('}}'))) {
return [true, error, resolvedValue];
}
@ -256,17 +267,10 @@ export const resolveReferences = (query, validationSchema, customResolvers = {})
useJSResolvers = true;
}
const customWidgetResolvers = ['listItem'];
const isCustomResolvers = customWidgetResolvers.some((resolver) => query.includes(resolver));
const { lookupTable } = useResolveStore.getState();
if (useJSResolvers) {
resolvedValue = resolveMultiDynamicReferences(query, lookupTable, queryHasJSCode);
} else if (isCustomResolvers && !_.isEmpty(customResolvers)) {
const currentState = useCurrentStateStore.getState();
const resolvedCode = olderResolverMethod(query, currentState, '', customResolvers);
resolvedValue = resolvedCode;
} else {
let value = query?.replace(/{{|}}/g, '').trim();

View file

@ -4,7 +4,8 @@ import Plotly from 'plotly.js-dist-min';
import createPlotlyComponent from 'react-plotly.js/factory';
import { isJson } from '@/_helpers/utils';
const Plot = createPlotlyComponent(Plotly);
import { isEqual, cloneDeep } from 'lodash';
import { isEqual } from 'lodash';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
var tinycolor = require('tinycolor2');
export const Chart = function Chart({
@ -232,7 +233,7 @@ const PlotComponent = memo(
return (
<Plot
data={data}
layout={cloneDeep(layout)} // Cloning the layout since the object is getting mutated inside the package
layout={deepClone(layout)} // Cloning the layout since the object is getting mutated inside the package
config={config}
onClick={(e) => {
onClick(e.points);

View file

@ -247,8 +247,6 @@ export const FilePicker = ({
}
});
});
setSelectedFiles(fileData);
onComponentOptionChanged(component, 'file', fileData, id);
onEvent('onFileSelected', filePickerEvents, { component })
.then(() => {
@ -263,7 +261,11 @@ export const FilePicker = ({
}, 600);
});
})
.then(() => onEvent('onFileLoaded', filePickerEvents, { component }));
.then(() => onEvent('onFileLoaded', filePickerEvents, { component }))
.then(() => {
setSelectedFiles(fileData);
onComponentOptionChanged(component, 'file', fileData, id);
});
}
if (fileRejections.length > 0) {

View file

@ -3,7 +3,7 @@ import { SubCustomDragLayer } from '@/Editor/SubCustomDragLayer';
import { SubContainer } from '@/Editor/SubContainer';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import _, { omit } from 'lodash';
import _, { debounce, omit } from 'lodash';
import { Box } from '@/Editor/Box';
import { generateUIComponents } from './FormUtils';
import { useMounted } from '@/_hooks/use-mount';
@ -14,6 +14,7 @@ import {
removeFunctionObjects,
} from '@/_helpers/appUtils';
import { useAppInfo } from '@/_stores/appDataStore';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const Form = function Form(props) {
const {
id,
@ -150,7 +151,7 @@ export const Form = function Form(props) {
// eslint-disable-next-line no-unused-vars
Object.entries(formattedChildData).map(([key, { formKey, ...rest }]) => [key, rest]) // removing formkey from final exposed data
);
const formattedChildDataClone = _.cloneDeep(formattedChildData);
const formattedChildDataClone = deepClone(formattedChildData);
const exposedVariables = {
...(!advanced && { children: formattedChildDataClone }),
data: removeFunctionObjects(formattedChildData),
@ -188,7 +189,9 @@ export const Form = function Form(props) {
};
const fireSubmissionEvent = () => {
if (isValid) {
onEvent('onSubmit', formEvents).then(() => resetComponent());
onEvent('onSubmit', formEvents).then(() => {
debounce(() => resetComponent(), 100)();
});
} else {
fireEvent('onInvalid');
}

View file

@ -2,7 +2,6 @@ import _ from 'lodash';
import React from 'react';
import Board from './Board';
import { isCardColoumnIdUpdated, updateCardData, updateColumnData, getData, isArray, isValidCardData } from './utils';
import { useCurrentState } from '@/_stores/currentStateStore';
export const BoardContext = React.createContext({});
@ -19,7 +18,6 @@ export const KanbanBoard = ({
dataCy,
}) => {
const { columns, cardData, enableAddCard } = properties;
const currentState = useCurrentState();
const { visibility, disabledState, width, minWidth, accentColor } = styles;
const [rawColumnData, setRawColumnData] = React.useState([]);
@ -109,9 +107,7 @@ export const KanbanBoard = ({
}
const darkMode = localStorage.getItem('darkMode') === 'true';
return (
<BoardContext.Provider
value={{ id, currentState, enableAddCard, accentColor, containerProps, removeComponent, darkMode }}
>
<BoardContext.Provider value={{ id, enableAddCard, accentColor, containerProps, removeComponent, darkMode }}>
<div
id={id}
style={{ display: visibility ? '' : 'none' }}

View file

@ -3,6 +3,7 @@ import { SubContainer } from '../SubContainer';
import { Pagination } from '@/_components/Pagination';
import { removeFunctionObjects } from '@/_helpers/appUtils';
import _ from 'lodash';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const Listview = function Listview({
id,
@ -79,7 +80,7 @@ export const Listview = function Listview({
}, [columns]);
useEffect(() => {
const childrenDataClone = _.cloneDeep(childrenData);
const childrenDataClone = deepClone(childrenData);
const exposedVariables = {
data: removeFunctionObjects(childrenDataClone),
children: childrenData,
@ -103,7 +104,7 @@ export const Listview = function Listview({
const componentNamesSet = new Set(
Object.values(childComponents ?? {}).map((component) => component.component.name)
);
const filteredData = _.cloneDeep(childrenData);
const filteredData = deepClone(childrenData);
if (filteredData?.[0]) {
Object.keys(filteredData?.[0]).forEach((item) => {
if (!componentNamesSet?.has(item)) {

View file

@ -5,6 +5,7 @@ import { Tooltip } from 'react-tooltip';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import cx from 'classnames';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export function AddNewRowComponent({
hideAddNewRowPopup,
@ -152,7 +153,7 @@ export function AddNewRowComponent({
<button
className="btn btn-light btn-sm m-2"
onClick={() => {
const rowData = _.cloneDeep(newRowsState);
const rowData = deepClone(newRowsState);
const index = rowData.length;
let newRow = getNewRowObject();
newRow = utilityForNestedNewRow(newRow);

View file

@ -49,6 +49,7 @@ export const GlobalFilter = ({
onClick={() => {
setGlobalFilter(undefined);
setValue('');
debouncedChange('');
onComponentOptionChanged(component, 'searchText', '');
}}
>

View file

@ -61,6 +61,7 @@ import { OverlayTriggerComponent } from './OverlayTriggerComponent';
import { diff } from 'deep-object-diff';
import { isRowInValid } from '../tableUtils';
import moment from 'moment';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
// utilityForNestedNewRow function is used to construct nested object while adding or updating new row when '.' is present in column key for adding new row
const utilityForNestedNewRow = (row) => {
@ -249,7 +250,7 @@ export function Table({
setIsCellValueChanged(true);
const dataUpdates = tableDetails.dataUpdates || [];
const clonedTableData = _.cloneDeep(tableData);
const clonedTableData = deepClone(tableData);
let obj = changeSet ? changeSet[index] || {} : {};
obj = _.set(obj, key, value);
@ -281,7 +282,7 @@ export function Table({
const copyOfTableDetails = useRef(tableDetails);
useEffect(() => {
copyOfTableDetails.current = _.cloneDeep(tableDetails);
copyOfTableDetails.current = deepClone(tableDetails);
}, [JSON.stringify(tableDetails)]);
function handleNewRowCellValueChange(index, key, value, rowData) {
@ -373,13 +374,13 @@ export function Table({
}
function handleChangesSaved() {
const clonedTableData = _.cloneDeep(tableData);
const clonedTableData = deepClone(tableData);
Object.keys(changeSet).forEach((key) => {
clonedTableData[key] = {
..._.merge(clonedTableData[key], changeSet[key]),
};
});
updatedDataReference.current = _.cloneDeep(clonedTableData);
updatedDataReference.current = deepClone(clonedTableData);
setExposedVariables({
changeSet: {},
@ -454,7 +455,7 @@ export function Table({
columnData = useMemo(
() =>
columnData.filter((column) => {
if (resolveReferences(column?.columnVisibility, currentState)) {
if (resolveReferences(column?.columnVisibility)) {
return column;
}
}),
@ -478,7 +479,7 @@ export function Table({
// Single-level nested property
const [nestedKey, subKey] = nestedKeys;
const nestedObject = transformedObject?.[nestedKey] || { ...row[nestedKey] }; // Retain existing nested object
const newValue = resolveReferences(transformation, currentState, row[key], {
const newValue = resolveReferences(transformation, row[key], {
cellValue: row?.[nestedKey]?.[subKey],
rowData: row,
});
@ -490,7 +491,7 @@ export function Table({
transformedObject[nestedKey] = nestedObject;
} else {
// Non-nested property
transformedObject[key] = resolveReferences(transformation, currentState, row[key], {
transformedObject[key] = resolveReferences(transformation, row[key], {
cellValue: row[key],
rowData: row,
});
@ -1283,7 +1284,7 @@ export function Table({
},
};
}
const isEditable = resolveReferences(column?.isEditable ?? false, currentState);
const isEditable = resolveReferences(column?.isEditable ?? false);
return (
<th
key={index}
@ -1421,15 +1422,13 @@ export function Table({
{page.map((row, index) => {
prepareRow(row);
let rowProps = { ...row.getRowProps() };
const contentWrap = resolveReferences(contentWrapProperty, currentState);
const contentWrap = resolveReferences(contentWrapProperty);
const isMaxRowHeightAuto = maxRowHeight === 'auto';
rowProps.style.minHeight = cellSize === 'condensed' ? '39px' : '45px'; // 1px is removed to accomodate 1px border-bottom
let cellMaxHeight;
let cellHeight;
if (contentWrap) {
cellMaxHeight = isMaxRowHeightAuto
? 'fit-content'
: resolveReferences(maxRowHeightValue, currentState) + 'px';
cellMaxHeight = isMaxRowHeightAuto ? 'fit-content' : resolveReferences(maxRowHeightValue) + 'px';
rowProps.style.maxHeight = cellMaxHeight;
} else {
cellMaxHeight = cellSize === 'condensed' ? 40 : 46;
@ -1523,25 +1522,25 @@ export function Table({
'multiselect',
'toggle',
].includes(cell?.column?.columnType)
? resolveReferences(cell.column?.cellBackgroundColor, currentState, '', {
? resolveReferences(cell.column?.cellBackgroundColor, '', {
cellValue,
rowData,
})
: '';
const cellTextColor = resolveReferences(cell.column?.textColor, currentState, '', {
const cellTextColor = resolveReferences(cell.column?.textColor, '', {
cellValue,
rowData,
});
const actionButtonsArray = actions.map((action) => {
return {
...action,
isDisabled: resolveReferences(action?.disableActionButton ?? false, currentState, '', {
isDisabled: resolveReferences(action?.disableActionButton ?? false, '', {
cellValue,
rowData,
}),
};
});
const isEditable = resolveReferences(cell.column?.isEditable ?? false, currentState, '', {
const isEditable = resolveReferences(cell.column?.isEditable ?? false, '', {
cellValue,
rowData,
});

View file

@ -51,8 +51,8 @@ export default function generateColumnsData({
columnType === 'image'
) {
columnOptions.selectOptions = [];
const values = resolveReferences(column.values, currentState, []);
const labels = resolveReferences(column.labels, currentState, []);
const values = resolveReferences(column.values, []);
const labels = resolveReferences(column.labels, []);
if (Array.isArray(labels) && Array.isArray(values)) {
columnOptions.selectOptions = labels.map((label, index) => {
@ -62,9 +62,9 @@ export default function generateColumnsData({
}
if (columnType === 'select' || columnType === 'newMultiSelect') {
columnOptions.selectOptions = [];
const useDynamicOptions = resolveReferences(column?.useDynamicOptions, currentState);
const useDynamicOptions = resolveReferences(column?.useDynamicOptions);
if (useDynamicOptions) {
const dynamicOptions = resolveReferences(column?.dynamicOptions || [], currentState);
const dynamicOptions = resolveReferences(column?.dynamicOptions || []);
columnOptions.selectOptions = Array.isArray(dynamicOptions) ? dynamicOptions : [];
} else {
const options = column?.options ?? [];
@ -103,7 +103,7 @@ export default function generateColumnsData({
const width = columnSize || defaultColumn.width;
return {
id: column.id,
Header: resolveReferences(column.name, currentState) ?? '',
Header: resolveReferences(column.name) ?? '',
accessor: column.key || column.name,
filter: customFilter,
width: width,
@ -150,7 +150,7 @@ export default function generateColumnsData({
case 'string':
case undefined:
case 'default': {
const cellTextColor = resolveReferences(column.textColor, currentState, '', { cellValue, rowData });
const cellTextColor = resolveReferences(column.textColor, '', { cellValue, rowData });
return (
<StringColumn
isEditable={isEditable}
@ -252,7 +252,7 @@ export default function generateColumnsData({
// );
}
case 'number': {
const textColor = resolveReferences(column.textColor, currentState, '', { cellValue, rowData });
const textColor = resolveReferences(column.textColor, '', { cellValue, rowData });
const cellStyles = {
color: textColor ?? '',
@ -303,7 +303,7 @@ export default function generateColumnsData({
const allowedDecimalPlaces = column?.decimalPlaces ?? null;
const removingExcessDecimalPlaces = (cellValue, allowedDecimalPlaces) => {
allowedDecimalPlaces = resolveReferences(allowedDecimalPlaces, currentState);
allowedDecimalPlaces = resolveReferences(allowedDecimalPlaces);
if (cellValue?.toString()?.includes('.')) {
const splittedCellValue = cellValue?.toString()?.split('.');
const decimalPlacesUnderLimit = splittedCellValue[1]
@ -486,8 +486,7 @@ export default function generateColumnsData({
isMulti={columnType === 'newMultiSelect' ? true : false}
containerWidth={width}
optionsLoadingState={
resolveReferences(column?.useDynamicOptions, currentState) &&
resolveReferences(column?.optionsLoadingState, currentState)
resolveReferences(column?.useDynamicOptions) && resolveReferences(column?.optionsLoadingState)
? true
: false
}
@ -610,12 +609,12 @@ export default function generateColumnsData({
);
}
case 'datepicker': {
const textColor = resolveReferences(column.textColor, currentState, '', { cellValue, rowData });
const isTimeChecked = resolveReferences(column?.isTimeChecked, currentState);
const isTwentyFourHrFormatEnabled = resolveReferences(column?.isTwentyFourHrFormatEnabled, currentState);
const disabledDates = resolveReferences(column?.disabledDates, currentState);
const parseInUnixTimestamp = resolveReferences(column?.parseInUnixTimestamp, currentState);
const isDateSelectionEnabled = resolveReferences(column?.isDateSelectionEnabled, currentState);
const textColor = resolveReferences(column.textColor, '', { cellValue, rowData });
const isTimeChecked = resolveReferences(column?.isTimeChecked);
const isTwentyFourHrFormatEnabled = resolveReferences(column?.isTwentyFourHrFormatEnabled);
const disabledDates = resolveReferences(column?.disabledDates);
const parseInUnixTimestamp = resolveReferences(column?.parseInUnixTimestamp);
const isDateSelectionEnabled = resolveReferences(column?.isDateSelectionEnabled);
const cellStyles = {
color: textColor ?? '',
};
@ -684,7 +683,7 @@ export default function generateColumnsData({
);
}
case 'link': {
const linkTarget = resolveReferences(column?.linkTarget ?? '{{true}}', currentState);
const linkTarget = resolveReferences(column?.linkTarget ?? '{{true}}');
column = {
...column,
linkColor: column?.linkColor ?? '#1B1F24',

View file

@ -1,5 +1,5 @@
/* eslint-disable import/no-named-as-default */
import React, { useCallback, useState, useEffect, useRef, useMemo, useContext } from 'react';
import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import cx from 'classnames';
import { useDrop, useDragLayer } from 'react-dnd';
import { ItemTypes, EditorConstants } from './editorConstants';
@ -17,7 +17,7 @@ import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useEditorStore } from '@/_stores/editorStore';
import { useAppInfo } from '@/_stores/appDataStore';
import { shallow } from 'zustand/shallow';
import _, { cloneDeep, isEmpty } from 'lodash';
import _, { isEmpty } from 'lodash';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import DragContainer from './DragContainer';
@ -32,6 +32,7 @@ import './editor.theme.scss';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import BulkIcon from '@/_ui/Icon/BulkIcons';
import { getSubpath } from '@/_helpers/routes';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
const deviceWindowWidth = EditorConstants.deviceWindowWidth;
@ -123,9 +124,9 @@ export const Container = ({
const mobLayouts = Object.keys(boxes)
.filter((key) => !boxes[key]?.component?.parent)
.map((key) => {
return { ...cloneDeep(boxes[key]?.layouts?.desktop), i: key };
return { ...deepClone(boxes[key]?.layouts?.desktop), i: key };
});
const updatedBoxes = cloneDeep(boxes);
const updatedBoxes = deepClone(boxes);
let newmMobLayouts = correctBounds(mobLayouts, { cols: 43 });
newmMobLayouts = compact(newmMobLayouts, 'vertical', 43);
Object.keys(boxes).forEach((id) => {
@ -182,9 +183,9 @@ export const Container = ({
const mobLayouts = Object.keys(components)
.filter((key) => !components[key]?.component?.parent)
.map((key) => {
return { ...cloneDeep(components[key]?.layouts?.desktop), i: key };
return { ...deepClone(components[key]?.layouts?.desktop), i: key };
});
const updatedBoxes = cloneDeep(components);
const updatedBoxes = deepClone(components);
let newmMobLayouts = correctBounds(mobLayouts, { cols: 43 });
newmMobLayouts = compact(newmMobLayouts, 'vertical', 43);
Object.keys(components).forEach((id) => {
@ -257,16 +258,18 @@ export const Container = ({
return;
}
if (!appDefinition.pages[currentPageId]?.components) return;
const definition = useEditorStore.getState().appDefinition;
if (!definition.pages[currentPageId]?.components) return;
const newDefinition = {
...appDefinition,
...definition,
pages: {
...appDefinition.pages,
...definition.pages,
[currentPageId]: {
...appDefinition.pages[currentPageId],
...definition.pages[currentPageId],
components: {
...appDefinition.pages[currentPageId]?.components,
...definition.pages[currentPageId]?.components,
...boxes,
},
},
@ -275,7 +278,7 @@ export const Container = ({
//need to check if a new component is added or deleted
const oldComponents = appDefinition.pages[currentPageId]?.components ?? {};
const oldComponents = definition.pages[currentPageId]?.components ?? {};
const newComponents = boxes;
const componendAdded = Object.keys(newComponents).length > Object.keys(oldComponents).length;
@ -288,7 +291,8 @@ export const Container = ({
opts.componentAdded = true;
}
const shouldUpdate = !_.isEmpty(diff(appDefinition, newDefinition));
const shouldUpdate = !_.isEmpty(diff(definition, newDefinition));
if (shouldUpdate) {
appDefinitionChanged(newDefinition, opts);
}
@ -366,7 +370,7 @@ export const Container = ({
}
const canvasBoundingRect = document.getElementsByClassName('real-canvas')[0].getBoundingClientRect();
const componentMeta = _.cloneDeep(
const componentMeta = deepClone(
componentTypes.find((component) => component.component === item.component.component)
);
@ -390,15 +394,13 @@ export const Container = ({
Listview: 'listItem',
});
const customResolverVariable = widgetResolvables[parentMeta?.component];
const defaultChildren = _.cloneDeep(parentMeta)['defaultChildren'];
const defaultChildren = deepClone(parentMeta)['defaultChildren'];
const parentId = newComponent.id;
defaultChildren.forEach((child) => {
const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child;
const componentMeta = _.cloneDeep(
componentTypes.find((component) => component.component === componentName)
);
const componentMeta = deepClone(componentTypes.find((component) => component.component === componentName));
const componentData = JSON.parse(JSON.stringify(componentMeta));
const width = layout.width ? layout.width : (componentMeta.defaultSize.width * 100) / noOfGrids;
@ -818,6 +820,8 @@ export const Container = ({
? 'Connect to your data source or use our sample data source to start playing around!'
: 'Connect to a data source to be able to create a query';
const showEmptyContainer = !appLoading && !isDragging && mode !== 'view';
return (
<ContainerWrapper
showComments={showComments}
@ -930,7 +934,7 @@ export const Container = ({
/>
</div>
</div>
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
{Object.keys(boxes).length === 0 && showEmptyContainer && (
<div style={{ paddingTop: '10%' }}>
<div className="row empty-box-cont">
<div className="col-md-4 dotted-cont">

View file

@ -31,6 +31,7 @@ export const shouldUpdate = (prevProps, nextProps) => {
return (
deepEqualityCheckusingLoDash(prevProps?.id, nextProps?.id) &&
deepEqualityCheckusingLoDash(prevProps?.component?.definition, nextProps?.component?.definition) &&
deepEqualityCheckusingLoDash(prevProps?.customResolvables, nextProps?.customResolvables) &&
prevProps?.width === nextProps?.width &&
prevProps?.height === nextProps?.height &&
prevProps?.darkMode === nextProps?.darkMode &&

View file

@ -14,6 +14,8 @@ import { useNoOfGrid, useGridStore } from '@/_stores/gridStore';
import WidgetBox from './WidgetBox';
import * as Sentry from '@sentry/react';
import { findHighestLevelofSelection } from './DragContainer';
import { useCurrentStateStore } from '@/_stores/currentStateStore';
import { useResolveStore } from '@/_stores/resolverStore';
function computeWidth(currentLayoutOptions) {
return `${currentLayoutOptions?.width}%`;
@ -143,6 +145,16 @@ const DraggableBox = React.memo(
[id]
);
const isEditorReady = useCurrentStateStore.getState().isEditorReady;
const isResolverStoreReady = useResolveStore.getState().storeReady;
const isCanvasReady = isEditorReady && isResolverStoreReady;
/**
* !This component does not consume the value returned from the below hook.
* Only purpose of the hook is to force one rerender the component
* */
useEditorStore((state) => state.componentsNeedsUpdateOnNextRender.find((compId) => compId === id));
return (
<div
className={
@ -228,6 +240,7 @@ const DraggableBox = React.memo(
onOptionsChanged={onComponentOptionsChanged}
isFromSubContainer={isFromSubContainer}
childComponents={childComponents}
isCanvasReady={isCanvasReady}
/>
</Sentry.ErrorBoundary>
</div>

View file

@ -46,13 +46,8 @@ import { withTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import Skeleton from 'react-loading-skeleton';
import EditorHeader from './Header';
import {
getWorkspaceId,
isValidUUID,
setWindowTitle,
defaultWhiteLabellingSettings,
pageTitles,
} from '@/_helpers/utils';
import { getWorkspaceId, isValidUUID } from '@/_helpers/utils';
import { fetchAndSetWindowTitle, pageTitles, defaultWhiteLabellingSettings } from '@white-label/whiteLabelling';
import '@/_styles/editor/react-select-search.scss';
import { withRouter } from '@/_hoc/withRouter';
import { ReleasedVersionError } from './AppVersionsManager/ReleasedVersionError';
@ -69,7 +64,7 @@ import {
resetAllStores,
} from '@/_stores/utils';
import { setCookie } from '@/_helpers/cookie';
import { EMPTY_ARRAY, flushComponentsToRender, useEditorActions, useEditorStore } from '@/_stores/editorStore';
import { EMPTY_ARRAY, useEditorActions, useEditorStore } from '@/_stores/editorStore';
import { useAppDataActions, useAppDataStore } from '@/_stores/appDataStore';
import { useNoOfGrid } from '@/_stores/gridStore';
import { useMounted } from '@/_hooks/use-mount';
@ -87,14 +82,10 @@ import { HotkeysProvider } from 'react-hotkeys-hook';
import { useResolveStore } from '@/_stores/resolverStore';
import { dfs } from '@/_stores/handleReferenceTransactions';
import { decimalToHex, EditorConstants } from './editorConstants';
import {
findComponentsWithReferences,
handleLowPriorityWork,
updateCanvasBackground,
clearAllQueuedTasks,
} from '@/_helpers/editorHelpers';
import { handleLowPriorityWork, updateCanvasBackground, clearAllQueuedTasks } from '@/_helpers/editorHelpers';
import { TJLoader } from '@/_ui/TJLoader/TJLoader';
import cx from 'classnames';
import { resolveReferences } from './CodeEditor/utils';
setAutoFreeze(false);
enablePatches();
@ -301,65 +292,14 @@ const EditorComponent = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]);
/**
** Async updates components in batches to optimize and processing efficiency.
* This function iterates over an array of component IDs, updating them in fixed-size batches,
* and introduces a delay after each batch to allow the UI thread to manage other tasks, such as rendering updates.
* After all batches are processed, it flushes the updates to clear any flags or temporary states indicating pending updates,
* ensuring the system is ready for the next cycle of updates.
*
* @param {Array} componentIds An array of component IDs that need updates.
* @returns {Promise<void>} A promise that resolves once all batches have been processed and flushed.
*/
async function batchUpdateComponents(componentIds) {
if (componentIds.length === 0) return;
let updatedComponentIds = [];
for (let i = 0; i < componentIds.length; i += 10) {
const batch = componentIds.slice(i, i + 10);
batch.forEach((id) => {
updatedComponentIds.push(id);
});
updateComponentsNeedsUpdateOnNextRender(batch);
// Delay to allow UI to process
await new Promise((resolve) => setTimeout(resolve, 0));
}
// Flush only updated components
flushComponentsToRender(updatedComponentIds);
}
const lastUpdatedRef = useResolveStore((state) => state.lastUpdatedRefs, shallow);
useEffect(() => {
if (lastUpdatedRef.length > 0) {
const currentComponents = useEditorStore.getState().appDefinition?.pages?.[currentPageId]?.components || {};
const directRenders = lastUpdatedRef.map((ref) => ref.includes('rerender') && ref.split(' ')[1]);
const toUpdateRefs = lastUpdatedRef.filter((ref) => !ref.includes('rerender'));
const componentIdsWithReferences = findComponentsWithReferences(currentComponents, toUpdateRefs);
if (directRenders.length > 0) {
componentIdsWithReferences.push(...directRenders);
}
if (componentIdsWithReferences.length > 0) {
batchUpdateComponents(componentIdsWithReferences);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastUpdatedRef]);
useEffect(
() => {
const components = appDefinition?.pages?.[currentPageId]?.components || {};
computeComponentState(components);
const isEditorReady = useCurrentStateStore.getState().isEditorReady;
const isResolverStoreReady = useResolveStore.getState().storeReady;
if (isEditorReady && isResolverStoreReady) {
const components = appDefinition?.pages?.[currentPageId]?.components || {};
computeComponentState(components);
}
const isPageSwitched = useResolveStore.getState().isPageSwitched;
@ -406,6 +346,11 @@ const EditorComponent = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentLayout, mounted]);
useEffect(() => {
updateEntityReferences(appDefinition, currentPageId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [events.length]);
const handleYmapEventUpdates = () => {
props.ymap?.set('eventHandlersUpdated', {
currentVersionId: currentVersionId,
@ -621,7 +566,7 @@ const EditorComponent = (props) => {
app.name = newName;
updateState({ appName: newName, app: app });
updateState({ appName: newName });
setWindowTitle({ page: pageTitles.EDITOR, appName: newName });
fetchAndSetWindowTitle({ page: pageTitles.EDITOR, appName: newName });
};
const onZoomChanged = (zoom) => {
@ -760,7 +705,7 @@ const EditorComponent = (props) => {
} = appData;
const startingPageHandle = props.params.pageHandle;
setWindowTitle({ page: pageTitles.EDITOR, appName });
fetchAndSetWindowTitle({ page: pageTitles.EDITOR, appName });
useAppVersionStore.getState().actions.updateEditingVersion(editing_version);
current_version_id && useAppVersionStore.getState().actions.updateReleasedVersionId(current_version_id);
await fetchOrgEnvironmentConstants();
@ -777,21 +722,23 @@ const EditorComponent = (props) => {
app: appData,
});
await useDataSourcesStore.getState().actions.fetchGlobalDataSources(organizationId);
await fetchDataSources(editing_version?.id);
await processNewAppDefinition(appData, startingPageHandle, false, ({ homePageId }) => {
handleLowPriorityWork(async () => {
handleLowPriorityWork(() => {
useResolveStore.getState().actions.updateLastUpdatedRefs(['constants', 'client']);
await useDataSourcesStore.getState().actions.fetchGlobalDataSources(organizationId);
await fetchDataSources(editing_version?.id);
commonLowPriorityActions(events, { homePageId });
});
});
};
const commonLowPriorityActions = async (events, { homePageId }) => {
const commonLowPriorityActions = (events, { homePageId }) => {
const currentPageEvents = events.filter((event) => event.target === 'page' && event.sourceId === homePageId);
const editorRef = getEditorRef();
await runQueries(useDataQueriesStore.getState().dataQueries, editorRef, true);
await handleEvent('onPageLoad', currentPageEvents, {}, true);
runQueries(useDataQueriesStore.getState().dataQueries, editorRef, true).then(() => {
handleEvent('onPageLoad', currentPageEvents, {}, true);
});
};
const processNewAppDefinition = async (data, startingPageHandle, versionSwitched = false, onComplete) => {
@ -1132,12 +1079,13 @@ const EditorComponent = (props) => {
isUpdatingEditorStateInProcess: false,
});
})
.catch(() => {
.catch((err) => {
updateEditorState({
saveError: true,
isUpdatingEditorStateInProcess: false,
});
toast.error('App could not save.');
// toast.error('App could not save.');
toast.error(err?.error ?? 'App could not save.');
})
.finally(() => {
if (appDiffOptions?.cloningComponent) {
@ -1509,6 +1457,7 @@ const EditorComponent = (props) => {
newGlobalSettings = dfs(newGlobalSettings, entity, value);
}
});
const [_, error, resolvedCanvasBackgroundColor] = resolveReferences(newGlobalSettings?.backgroundFxQuery, {});
const newAppDefinition = produce(appJson, (draft) => {
draft.globalSettings = newGlobalSettings;
@ -1517,7 +1466,7 @@ const EditorComponent = (props) => {
// Setting the canvas background to the editor store
setCanvasBackground({
backgroundFxQuery: newGlobalSettings?.backgroundFxQuery,
canvasBackgroundColor: newGlobalSettings?.canvasBackgroundColor,
canvasBackgroundColor: resolvedCanvasBackgroundColor || '',
});
updateEditorState({
@ -1818,7 +1767,9 @@ const EditorComponent = (props) => {
});
const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition));
const newCurrentPageId = isHomePage ? Object.keys(copyOfAppDefinition.pages)[0] : copyOfAppDefinition.homePageId;
setCurrentPageId(newCurrentPageId);
const toBeDeletedPage = copyOfAppDefinition.pages[pageId];
const newAppDefinition = {
@ -1826,9 +1777,6 @@ const EditorComponent = (props) => {
pages: omit(copyOfAppDefinition.pages, pageId),
};
const newCurrentPageId = isHomePage ? Object.keys(copyOfAppDefinition.pages)[0] : copyOfAppDefinition.homePageId;
setCurrentPageId(newCurrentPageId);
updateEditorState({
isUpdatingEditorStateInProcess: true,
});
@ -1840,8 +1788,6 @@ const EditorComponent = (props) => {
});
toast.success(`${toBeDeletedPage.name} page deleted.`);
switchPage(newCurrentPageId);
};
const disableEnablePage = ({ pageId, isDisabled }) => {
@ -1907,7 +1853,7 @@ const EditorComponent = (props) => {
setIsSaving(true);
appVersionService
.clonePage(appId, editingVersionId, pageId)
.then((data) => {
.then(async (data) => {
const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition));
const pages = data.pages.reduce((acc, page) => {
@ -1931,6 +1877,8 @@ const EditorComponent = (props) => {
events: data.events,
});
appDefinitionChanged(newAppDefinition);
await onEditorLoad(newAppDefinition, pageId, false);
updateEntityReferences(newAppDefinition, pageId);
})
.finally(() => setIsSaving(false));
};

View file

@ -366,7 +366,7 @@ export const GlobalSettings = ({
lineNumbers={false}
onChange={(color) => {
const options = {
canvasBackgroundColor: resolveReferences(color, realState),
canvasBackgroundColor: resolveReferences(color),
backgroundFxQuery: color,
};
globalSettingsChanged(options);

View file

@ -14,11 +14,13 @@ import cx from 'classnames';
import { ToolTip } from '@/_components/ToolTip';
import { TOOLTIP_MESSAGES } from '@/_helpers/constants';
import { useAppDataStore } from '@/_stores/appDataStore';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
class ManageAppUsersComponent extends React.Component {
constructor(props) {
super(props);
this.isUserAdmin = authenticationService.currentSessionValue?.admin;
this.whiteLabelText = retrieveWhiteLabelText();
this.state = {
showModal: false,
@ -175,7 +177,7 @@ class ManageAppUsersComponent extends React.Component {
const appLink = `${getHostURL()}/applications/`;
const shareableLink = appLink + (this.props.slug || appId);
const slugButtonClass = !_.isEmpty(newSlug.error) ? 'is-invalid' : 'is-valid';
const embeddableLink = `<iframe width="560" height="315" src="${appLink}${this.props.slug}" title="Tooljet app - ${this.props.slug}" frameborder="0" allowfullscreen></iframe>`;
const embeddableLink = `<iframe width="560" height="315" src="${appLink}${this.props.slug}" title="${this.whiteLabelText} app - ${this.props.slug}" frameborder="0" allowfullscreen></iframe>`;
const shouldWeDisableShareModal = !this.props.isVersionReleased;
return (

View file

@ -73,7 +73,7 @@ export default function EditorHeader({
: '';
setAppPreviewLink(appVersionPreviewLink);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [slug, currentVersionId, editingVersion]);
}, [slug, currentVersionId, editingVersion, pageHandle]);
return (
<div className={cx('header', { 'dark-theme theme-dark': darkMode })} style={{ width: '100%' }}>

View file

@ -112,7 +112,7 @@ export const baseComponentProperties = (
Layout: [],
};
if (component.component.component === 'Listview') {
if (!resolveReferences(component.component.definition.properties?.enablePagination?.value, currentState)) {
if (!resolveReferences(component.component.definition.properties?.enablePagination?.value)) {
properties = properties.filter((property) => property !== 'rowsPerPage');
}
}

View file

@ -20,10 +20,7 @@ export const FilePicker = ({ componentMeta, darkMode, ...restProps }) => {
return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
};
const conditionalAccordionItems = (component) => {
const parseContent = resolveReferences(
component.component.definition.properties.parseContent?.value ?? false,
currentState
);
const parseContent = resolveReferences(component.component.definition.properties.parseContent?.value ?? false);
const accordionItems = [];
const options = ['parseContent'];

View file

@ -21,8 +21,7 @@ export const Modal = ({ componentMeta, darkMode, ...restProps }) => {
};
const conditionalAccordionItems = (component) => {
const useDefaultButton = resolveReferences(
component.component.definition.properties.useDefaultButton?.value ?? false,
currentState
component.component.definition.properties.useDefaultButton?.value ?? false
);
const accordionItems = [];
const options = ['useDefaultButton'];

View file

@ -105,7 +105,7 @@ const DatepickerProperties = ({ column, index, darkMode, currentState, onColumnI
paramMeta={{ type: 'toggle', displayName: 'Enable date' }}
/>
</div>
{resolveReferences(column?.isDateSelectionEnabled, currentState) && (
{resolveReferences(column?.isDateSelectionEnabled) && (
<div
data-cy={`input-date-display-format`}
className="field mb-2 w-100"
@ -175,7 +175,7 @@ const DatepickerProperties = ({ column, index, darkMode, currentState, onColumnI
paramMeta={{ type: 'toggle', displayName: 'Enable time' }}
/>
</div>
{resolveReferences(column?.isTimeChecked, currentState) && (
{resolveReferences(column?.isTimeChecked) && (
<>
{!isDateDisplayFormatFxOn && (
<div className="field mb-2" onClick={(e) => e.stopPropagation()} style={{ padding: '0px 12px' }}>
@ -260,7 +260,7 @@ const DatepickerProperties = ({ column, index, darkMode, currentState, onColumnI
paramType="properties"
paramMeta={{ type: 'toggle', displayName: 'Parse in unix timestamp' }}
/>
{resolveReferences(column?.parseInUnixTimestamp, currentState) ? (
{resolveReferences(column?.parseInUnixTimestamp) ? (
<div className="mt-2">
<div className="field mb-2 tj-app-input">
<label data-cy={`label-date-parse-format`} className="form-label">
@ -283,7 +283,7 @@ const DatepickerProperties = ({ column, index, darkMode, currentState, onColumnI
</div>
) : (
<div className="mt-2">
{resolveReferences(column?.isDateSelectionEnabled, currentState) && (
{resolveReferences(column?.isDateSelectionEnabled) && (
<div data-cy={`input-parse-timezone`} className="field mb-2">
<div className="d-flex justify-content-between">
<label data-cy={`label-parse-timezone`} className="form-label">
@ -331,7 +331,7 @@ const DatepickerProperties = ({ column, index, darkMode, currentState, onColumnI
)}
</div>
)}
{resolveReferences(column?.isTimeChecked, currentState) && (
{resolveReferences(column?.isTimeChecked) && (
<>
{!isParseDateFormatFxOn && (
<div className="field mb-2" onClick={(e) => e.stopPropagation()}>

View file

@ -253,7 +253,7 @@ export const PropertiesTabElements = ({
paramType="properties"
/>
</div>
{resolveReferences(column?.isEditable, currentState) && (
{resolveReferences(column?.isEditable) && (
<ValidationProperties
column={column}
index={index}

View file

@ -111,9 +111,7 @@ export const ProgramaticallyHandleProperties = ({
const fxActiveFieldsPropExists = props?.hasOwnProperty('fxActiveFields') ?? false;
//to support backward compatibility, when fxActive is true for a particular column, we are passing all possible combinations which should render codehinter
const fxActive =
props?.fxActive && resolveReferences(props.fxActive, currentState)
? ['isEditable', 'columnVisibility', 'linkTarget']
: [];
props?.fxActive && resolveReferences(props.fxActive) ? ['isEditable', 'columnVisibility', 'linkTarget'] : [];
const checkFxActiveFieldIsArrray = (fxActiveFieldsProperty) => {
// adding error handling mechanism for fxActiveFieldsProperty , if props.fxActiveFields is array , then return props.fxActiveFields or else return [], this will make sure, fxActiveFields wil always be array

View file

@ -106,7 +106,7 @@ export const OptionsList = ({
const options = column.options;
options[optionIndex][property] = value;
column.options = options;
const isValueTruthy = !!resolveReferences(value, currentState);
const isValueTruthy = !!resolveReferences(value);
// This block is responsible for updating list of defaultOptions when makeDefaultOption prop is updated
if (property === 'makeDefaultOption') {
@ -235,7 +235,7 @@ export const OptionsList = ({
}}
paramType="properties"
/>
{resolveReferences(column?.useDynamicOptions, currentState) ? (
{resolveReferences(column?.useDynamicOptions) ? (
<div className="d-flex custom-gap-7 flex-column">
<CodeHinter
currentState={currentState}

View file

@ -18,7 +18,6 @@ import NoListItem from './NoListItem';
import { ProgramaticallyHandleProperties } from './ProgramaticallyHandleProperties';
import { ColumnPopoverContent } from './ColumnManager/ColumnPopover';
import { useAppDataStore } from '@/_stores/appDataStore';
import CodeHinter from '@/Editor/CodeEditor';
import { checkIfTableColumnDeprecated } from './ColumnManager/DeprecatedColumnTypeMsg';
@ -82,7 +81,7 @@ class TableComponent extends React.Component {
checkIfAllColumnsAreEditable = (component) => {
const isAllColumnsEditable = component.component?.definition?.properties?.columns?.value
?.filter((column) => !NON_EDITABLE_COLUMNS.includes(column.columnType))
.every((column) => resolveReferences(column.isEditable, this.props.currentState));
.every((column) => resolveReferences(column.isEditable));
return isAllColumnsEditable;
};
@ -92,7 +91,7 @@ class TableComponent extends React.Component {
if (prevPropsColumns !== currentPropsColumns) {
const isAllColumnsEditable = currentPropsColumns
.filter((column) => !NON_EDITABLE_COLUMNS.includes(column.columnType))
.every((column) => resolveReferences(column.isEditable, this.props.currentState));
.every((column) => resolveReferences(column.isEditable));
this.setState({ isAllColumnsEditable });
}
}
@ -172,7 +171,7 @@ class TableComponent extends React.Component {
className={`${this.props.darkMode && 'dark-theme'} shadow table-column-popover`}
style={{
width: '280px',
maxHeight: resolveReferences(column.isEditable, this.state.currentState) ? '100vh' : 'inherit',
maxHeight: resolveReferences(column.isEditable) ? '100vh' : 'inherit',
overflowY: 'auto',
zIndex: '9999',
}}
@ -470,10 +469,7 @@ class TableComponent extends React.Component {
`component/${this.props.component.component.name}/${column ?? 'default'}::${field}`;
handleMakeAllColumnsEditable = (value) => {
const columns = resolveReferences(
this.props.component.component.definition.properties.columns,
this.props.currentState
);
const columns = resolveReferences(this.props.component.component.definition.properties.columns);
this.setState({ isAllColumnsEditable: resolveReferences(value) });
@ -502,37 +498,37 @@ class TableComponent extends React.Component {
paramUpdated({ name: 'displaySearchBox' }, 'value', true, 'properties');
const displaySearchBox = component.component.definition.properties.displaySearchBox.value;
const displayServerSideFilter = component.component.definition.properties.showFilterButton?.value
? resolveReferences(component.component.definition.properties.showFilterButton?.value, currentState)
? resolveReferences(component.component.definition.properties.showFilterButton?.value)
: false;
const displayServerSideSearch = component.component.definition.properties.displaySearchBox?.value
? resolveReferences(component.component.definition.properties.displaySearchBox?.value, currentState)
? resolveReferences(component.component.definition.properties.displaySearchBox?.value)
: false;
const serverSidePagination = component.component.definition.properties.serverSidePagination?.value
? resolveReferences(component.component.definition.properties.serverSidePagination?.value, currentState)
? resolveReferences(component.component.definition.properties.serverSidePagination?.value)
: false;
const clientSidePagination = component.component.definition.properties.clientSidePagination?.value
? resolveReferences(component.component.definition.properties.clientSidePagination?.value, currentState)
? resolveReferences(component.component.definition.properties.clientSidePagination?.value)
: false;
let enablePagination = !has(component.component.definition.properties, 'enablePagination')
? clientSidePagination || serverSidePagination
: resolveReferences(component.component.definition.properties.enablePagination?.value, currentState);
: resolveReferences(component.component.definition.properties.enablePagination?.value);
const enabledSort = component.component.definition.properties.enabledSort?.value
? resolveReferences(component.component.definition.properties.enabledSort?.value, currentState)
? resolveReferences(component.component.definition.properties.enabledSort?.value)
: true;
const useDynamicColumn = component.component.definition.properties.useDynamicColumn?.value
? resolveReferences(component.component.definition.properties.useDynamicColumn?.value, currentState) ?? false
? resolveReferences(component.component.definition.properties.useDynamicColumn?.value) ?? false
: false;
//from app definition values are of string data type if defined or else,undefined
const allowSelection = component.component.definition.properties?.allowSelection?.value
? resolveReferences(component.component.definition.properties.allowSelection?.value, currentState)
: resolveReferences(component.component.definition.properties.highlightSelectedRow.value, currentState) ||
resolveReferences(component.component.definition.properties.showBulkSelector.value, currentState);
? resolveReferences(component.component.definition.properties.allowSelection?.value)
: resolveReferences(component.component.definition.properties.highlightSelectedRow.value) ||
resolveReferences(component.component.definition.properties.showBulkSelector.value);
const renderCustomElement = (param, paramType = 'properties') => {
return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType);
};
let items = [];
@ -569,8 +565,8 @@ class TableComponent extends React.Component {
{({ innerRef, droppableProps, placeholder }) => (
<div className="w-100 d-flex custom-gap-4 flex-column" {...droppableProps} ref={innerRef}>
{columns.value.map((item, index) => {
const resolvedItemName = resolveReferences(item.name, this.state.currentState);
const isEditable = resolveReferences(item.isEditable, this.state.currentState);
const resolvedItemName = resolveReferences(item.name);
const isEditable = resolveReferences(item.isEditable);
const columnVisibility = item?.columnVisibility ?? true;
const getSecondaryText = (text) => {
switch (text) {
@ -662,7 +658,7 @@ class TableComponent extends React.Component {
deleteIconOutsideMenu={true}
showCopyColumnOption={true}
showVisibilityIcon={true}
isColumnVisible={resolveReferences(columnVisibility, this.state.currentState)}
isColumnVisible={resolveReferences(columnVisibility)}
className={`${
this.state.activeColumnPopoverIndex === index && 'active-column-list'
}`}

View file

@ -20,7 +20,7 @@ const getTableDefinitionInitialValue = (
{ component: { component: { definition: { properties } = {} } = {} } = {} },
currentState
) => {
const resolveProperty = (propertyName) => resolveReferences(properties?.[propertyName]?.value, currentState);
const resolveProperty = (propertyName) => resolveReferences(properties?.[propertyName]?.value);
switch (param) {
case 'enablePagination':

View file

@ -26,6 +26,7 @@ import { diff } from 'deep-object-diff';
import { useEditorStore } from '@/_stores/editorStore';
import { handleLowPriorityWork } from '@/_helpers/editorHelpers';
import { appService } from '@/_services';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const EventManager = ({
sourceId,
@ -264,6 +265,9 @@ export const EventManager = ({
function getPageOptions(event) {
// If disabled page is already selected then don't remove from page options
if (!Array.isArray(pages) || pages.length === 0) return [];
if (pages.find((page) => page.id === event.pageId)?.disabled) {
return pages.map((page) => ({
name: page.name,
@ -279,7 +283,7 @@ export const EventManager = ({
}
function handleQueryChange(index, updates) {
let newEvents = _.cloneDeep(events);
let newEvents = deepClone(events);
let updatedEvent = newEvents[index];
updatedEvent.event = {
@ -301,7 +305,7 @@ export const EventManager = ({
}
function handlerChanged(index, param, value) {
let newEvents = _.cloneDeep(events);
let newEvents = deepClone(events);
let updatedEvent = newEvents[index];
updatedEvent.event[param] = value;
@ -341,7 +345,7 @@ export const EventManager = ({
}
function removeHandler(index) {
const eventsHandler = _.cloneDeep(events);
const eventsHandler = deepClone(events);
const eventId = eventsHandler[index].id;
setEventToDeleteLoaderIndex(index);
@ -520,7 +524,7 @@ export const EventManager = ({
{event.actionId === 'go-to-app' && (
<GotoApp
event={_.cloneDeep(event)}
event={deepClone(event)}
handlerChanged={handlerChanged}
eventIndex={index}
getAllApps={getAllApps}
@ -823,7 +827,7 @@ export const EventManager = ({
)}
{event.actionId === 'switch-page' && (
<SwitchPage
event={_.cloneDeep(event)}
event={deepClone(event)}
handlerChanged={handlerChanged}
eventIndex={index}
getPages={() => getPageOptions(event)}
@ -940,7 +944,7 @@ export const EventManager = ({
}
const reorderEvents = (startIndex, endIndex) => {
const result = _.cloneDeep(events);
const result = deepClone(events);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);

View file

@ -33,6 +33,7 @@ import Copy from '@/_ui/Icon/solidIcons/Copy';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import classNames from 'classnames';
import { useEditorStore, EMPTY_ARRAY } from '@/_stores/editorStore';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
const INSPECTOR_HEADER_OPTIONS = [
{
@ -169,7 +170,7 @@ export const Inspector = ({
function paramUpdated(param, attr, value, paramType, isParamFromTableColumn = false) {
let newComponent = JSON.parse(JSON.stringify(component));
let newDefinition = _.cloneDeep(newComponent.component.definition);
let newDefinition = deepClone(newComponent.component.definition);
let allParams = newDefinition[paramType] || {};
const paramObject = allParams[param.name];
if (!paramObject) {
@ -180,11 +181,7 @@ export const Inspector = ({
const defaultValue = getDefaultValue(value);
// This is needed to have enable pagination in Table as backward compatible
// Whenever enable pagination is false, we turn client and server side pagination as false
if (
component.component.component === 'Table' &&
param.name === 'enablePagination' &&
!resolveReferences(value, currentState)
) {
if (component.component.component === 'Table' && param.name === 'enablePagination' && !resolveReferences(value)) {
if (allParams?.['clientSidePagination']?.[attr]) {
allParams['clientSidePagination'][attr] = value;
}
@ -218,7 +215,7 @@ export const Inspector = ({
if (
component.component.component === 'Table' &&
param.name === 'contentWrap' &&
!resolveReferences(value, currentState) &&
!resolveReferences(value) &&
newDefinition.properties.columns.value.some((item) => item.columnType === 'image' && item.height !== '')
) {
const updatedColumns = newDefinition.properties.columns.value.map((item) => {
@ -587,11 +584,11 @@ const resolveConditionalStyle = (definition, condition, currentState) => {
if (conditionExistsInDefinition) {
switch (condition) {
case 'cellSize': {
const cellSize = resolveReferences(definition[condition]?.value ?? false, currentState) === 'hugContent';
const cellSize = resolveReferences(definition[condition]?.value ?? false) === 'hugContent';
return cellSize;
}
default:
return resolveReferences(definition[condition]?.value ?? false, currentState);
return resolveReferences(definition[condition]?.value ?? false);
}
}
};

View file

@ -53,7 +53,7 @@ export function renderCustomStyles(
const { conditionallyRender = null } = paramConfig;
const getResolvedValue = (key) => {
return paramTypeDefinition?.[key] && resolveReferences(paramTypeDefinition?.[key], currentState);
return paramTypeDefinition?.[key] && resolveReferences(paramTypeDefinition?.[key]);
};
const utilFuncForMultipleChecks = (conditionallyRender) => {
@ -139,7 +139,7 @@ export function renderElement(
if (conditionallyRender) {
const { key, value } = conditionallyRender;
if (paramTypeDefinition?.[key] ?? value) {
const resolvedValue = paramTypeDefinition?.[key] && resolveReferences(paramTypeDefinition?.[key], currentState);
const resolvedValue = paramTypeDefinition?.[key] && resolveReferences(paramTypeDefinition?.[key]);
if (resolvedValue?.value !== value) return;
}
}

View file

@ -60,20 +60,8 @@ export const LeftSidebarInspector = ({
}, [selectedComponents]);
const memoizedJSONData = React.useMemo(() => {
const updatedQueries = {};
const { queries: currentQueries } = currentState;
// if (!_.isEmpty(dataQueries)) {
// const copyCurrentQueies = JSON.parse(JSON.stringify(currentQueries));
// dataQueries.forEach((query) => {
// updatedQueries[query.name] = _.merge(copyCurrentQueies[query.name], {
// id: query.id,
// isLoading: false,
// data: [],
// rawData: [],
// });
// });
// }
// const data = _.merge(currentState, { queries: updatedQueries });
const jsontreeData = { ...currentState, queries: currentQueries };
delete jsontreeData.errors;
delete jsontreeData.client;

View file

@ -11,6 +11,7 @@ import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
const LeftSidebarPageSelector = ({
appDefinition,
@ -36,7 +37,7 @@ const LeftSidebarPageSelector = ({
}) => {
const pages = useMemo(
() =>
Object.entries(_.cloneDeep(appDefinition.pages))
Object.entries(deepClone(appDefinition.pages))
.map(([id, page]) => ({ id, ...page }))
.sort((a, b) => a.index - b.index) || [],
[JSON.stringify(appDefinition.pages)]

View file

@ -20,6 +20,7 @@ import { shallow } from 'zustand/shallow';
import useDebugger from './SidebarDebugger/useDebugger';
import { GlobalSettings } from '../Header/GlobalSettings';
import cx from 'classnames';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const LeftSidebar = forwardRef((props, ref) => {
const router = useRouter();
@ -159,7 +160,7 @@ export const LeftSidebar = forwardRef((props, ref) => {
updatePageHandle={updatePageHandle}
clonePage={clonePage}
pages={
Object.entries(_.cloneDeep(appDefinition).pages)
Object.entries(deepClone(appDefinition).pages)
.map(([id, page]) => ({ id, ...page }))
.sort((a, b) => a.index - b.index) || []
}

View file

@ -9,7 +9,7 @@ import { validateProperties } from '../component-properties-validation';
import { getComponentName, debuggerActions } from '@/_helpers/appUtils';
import { memoizeFunction } from '../../_helpers/editorHelpers';
import { componentTypes } from '../WidgetManager/components';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useCurrentStateStore } from '@/_stores/currentStateStore';
const shouldAddBoxShadowAndVisibility = ['TextInput', 'PasswordInput', 'NumberInput', 'Text'];
@ -18,26 +18,15 @@ const getComponentMetaData = memoizeFunction((componentType) => {
});
const HydrateWithResolveReferences = ({ id, mode, component, customResolvables, children }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const componentMeta = useMemo(() => getComponentMetaData(component?.component), []);
const currentState = useCurrentState();
const resolvedProperties = resolveProperties(component, {}, null, customResolvables, id);
const resolvedProperties = useMemo(() => {
return resolveProperties(component, currentState, null, customResolvables, id);
}, [component, currentState, customResolvables, id]);
const resolvedStyles = resolveStyles(component, {}, null, customResolvables);
const resolvedStyles = useMemo(() => {
return resolveStyles(component, currentState, null, customResolvables);
}, [component, currentState, customResolvables]);
const resolvedGeneralProperties = resolveGeneralProperties(component, {}, null, customResolvables);
const resolvedGeneralProperties = useMemo(() => {
return resolveGeneralProperties(component, currentState, null, customResolvables);
}, [component, currentState, customResolvables]);
const resolvedGeneralStyles = useMemo(() => {
return resolveGeneralStyles(component, currentState, null, customResolvables);
}, [component, currentState, customResolvables]);
const resolvedGeneralStyles = resolveGeneralStyles(component, {}, null, customResolvables);
const [validatedProperties, propertyErrors] =
mode === 'edit' && component.validate
@ -67,6 +56,11 @@ const HydrateWithResolveReferences = ({ id, mode, component, customResolvables,
: [resolvedGeneralStyles, []];
useEffect(() => {
const isEditorReady = useCurrentStateStore.getState().isEditorReady;
if (!isEditorReady) return;
const currentState = useCurrentStateStore.getState();
const currentPage = currentState?.page;
const componentName = getComponentName(currentState, id);
const errorLog = Object.fromEntries(

View file

@ -5,7 +5,7 @@ import PlusRectangle from '@/_ui/Icon/solidIcons/PlusRectangle';
import Remove from '@/_ui/Icon/bulkIcons/Remove';
import ParameterForm from './ParameterForm';
const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRemove, currentState, otherParams }) => {
const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRemove, otherParams }) => {
const [showModal, setShowModal] = useState(false);
const closeMenu = () => setShowModal(false);
@ -66,7 +66,6 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe
defaultValue={defaultValue}
onSubmit={handleSubmit}
showModal={showModal}
currentState={currentState}
/>
</Popover>
}

View file

@ -8,7 +8,6 @@ const ParameterList = ({
handleAddParameter,
handleParameterChange,
handleParameterRemove,
currentState,
darkMode,
containerRef,
}) => {
@ -68,7 +67,7 @@ const ParameterList = ({
name={parameter.name}
otherParams={formattedParameters.filter((p) => p.name !== parameter.name)}
defaultValue={parameter.defaultValue}
currentState={currentState}
// currentState={currentState}
darkMode={darkMode}
/>
);
@ -97,7 +96,6 @@ const ParameterList = ({
handleParameterChange(selectedParameter.index, param);
setSelectedParameter();
}}
currentState={currentState}
showModal={showMore}
/>
) : (
@ -136,12 +134,7 @@ const ParameterList = ({
)}
</span>
</OverlayTrigger>
<ParameterDetails
onSubmit={handleAddParameter}
currentState={currentState}
darkMode={darkMode}
otherParams={formattedParameters}
/>
<ParameterDetails onSubmit={handleAddParameter} darkMode={darkMode} otherParams={formattedParameters} />
</div>
);
};

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import cx from 'classnames';
import { cloneDeep, isEmpty } from 'lodash';
import { isEmpty } from 'lodash';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import { allSources, source } from '../QueryEditors';
@ -19,6 +19,7 @@ import { useSelectedQuery, useSelectedDataSource } from '@/_stores/queryPanelSto
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import SuccessNotificationInputs from './SuccessNotificationInputs';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const QueryManagerBody = ({
darkMode,
@ -87,7 +88,7 @@ export const QueryManagerBody = ({
const updatedOptions = cleanFocusedFields(newOptions);
setOptions((options) => ({ ...options, ...updatedOptions }));
updateDataQuery(cloneDeep({ ...options, ...updatedOptions }));
updateDataQuery(deepClone({ ...options, ...updatedOptions }));
};
const optionchanged = (option, value) => {

View file

@ -15,15 +15,14 @@ import {
useShowCreateQuery,
useNameInputFocussed,
} from '@/_stores/queryPanelStore';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useSelectedQueryLoadingState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { Tooltip } from 'react-tooltip';
import { Button } from 'react-bootstrap';
import { cloneDeep } from 'lodash';
import ParameterList from './ParameterList';
import { decodeEntities } from '@/_helpers/utils';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, setOptions }, ref) => {
const { renameQuery } = useDataQueriesActions();
@ -31,8 +30,7 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
const selectedDataSource = useSelectedDataSource();
const [showCreateQuery, setShowCreateQuery] = useShowCreateQuery();
const queryName = selectedQuery?.name ?? '';
const currentState = useCurrentState((state) => ({ queries: state.queries }), shallow);
const { queries } = currentState;
const isLoading = useSelectedQueryLoadingState();
const { isVersionReleased } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
@ -97,7 +95,6 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
};
const renderRunButton = () => {
const { isLoading } = queries[selectedQuery?.name] ?? false;
return (
<span
{...(isInDraft && {
@ -131,7 +128,6 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
const renderButtons = () => {
if (selectedQuery === null || showCreateQuery) return;
const { isLoading } = queries[selectedQuery?.name] ?? false;
return (
<>
<PreviewButton
@ -146,7 +142,7 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
const optionsChanged = (newOptions) => {
setOptions(newOptions);
updateDataQuery(cloneDeep(newOptions));
updateDataQuery(deepClone(newOptions));
};
const handleAddParameter = (newParameter) => {
@ -201,7 +197,6 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
handleAddParameter={handleAddParameter}
handleParameterChange={handleParameterChange}
handleParameterRemove={handleParameterRemove}
currentState={currentState}
darkMode={darkMode}
containerRef={paramListContainerRef}
/>

View file

@ -181,7 +181,6 @@ class Restapi extends React.Component {
>
<CodeHinter
type="basic"
currentState={this.props.currentState}
initialValue={options.url}
onChange={(value) => {
changeOption(this, 'url', value);

View file

@ -1,23 +1,13 @@
import React, { useState, useEffect } from 'react';
import { defaults } from 'lodash';
import { Card } from 'react-bootstrap';
import { useCurrentState } from '@/_stores/currentStateStore';
import ParameterList from '../../Components/ParameterList';
import CodeHinter from '@/Editor/CodeEditor';
const Runjs = (props) => {
const currentState = useCurrentState();
const [currStateForCodeHinter, setCurrStateForCodeHinter] = useState(currentState);
const initialOptions = defaults({ ...props.options }, { code: '//Type your JavaScript code here' });
const [options, setOptions] = useState(initialOptions);
useEffect(() => {
setCurrStateForCodeHinter({
...currentState,
parameters: options?.parameters?.reduce((params, param) => ({ ...params, [param.name]: param.defaultValue }), {}),
});
}, [currentState?.components, options?.parameters]);
useEffect(() => {
setOptions(props.options);
}, [props.options]);

View file

@ -9,10 +9,11 @@ import Remove from '@/_ui/Icon/solidIcons/Remove';
import Information from '@/_ui/Icon/solidIcons/Information';
import Icon from '@/_ui/Icon/solidIcons/index';
import set from 'lodash/set';
import { cloneDeep, isEmpty } from 'lodash';
import { isEmpty } from 'lodash';
import { getPrivateRoute } from '@/_helpers/routes';
import { useNavigate } from 'react-router-dom';
import useConfirm from './Confirm';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
const { selectedTableId, tables, joinOptions, findTableDetails, tableForeignKeyInfo } =
@ -134,7 +135,7 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
const adjacentTableForeignKeyDetails = checkIfAdjacentTableHasForeignKey(isChoosingLHStable, tableId);
if (isChoosingLHStable) {
if (adjacentTableForeignKeyDetails.length) {
const newData = cloneDeep({ ...data });
const newData = deepClone({ ...data });
const newConditionsList = adjacentTableForeignKeyDetails.map((adjacentTableForeignKey) => {
const { referenced_column_names = [], column_names = [] } = adjacentTableForeignKey;
const newCondition = {
@ -156,7 +157,7 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
set(newData, 'conditions.conditionsList', newConditionsList);
onChange(newData);
} else {
const newData = cloneDeep({ ...data });
const newData = deepClone({ ...data });
const { conditionsList = [{}] } = newData?.conditions || {};
const newConditionsList = conditionsList.map((condition) => {
const newCondition = { ...condition };
@ -170,7 +171,7 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
}
} else {
if (adjacentTableForeignKeyDetails.length) {
const newData = cloneDeep({ ...data });
const newData = deepClone({ ...data });
const newConditionsList = adjacentTableForeignKeyDetails.map((adjacentTableForeignKey) => {
const { referenced_column_names = [], column_names = [] } = adjacentTableForeignKey;
const newCondition = {
@ -193,7 +194,7 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
set(newData, 'table', tableId);
onChange(newData);
} else {
const newData = cloneDeep({ ...data });
const newData = deepClone({ ...data });
const { conditionsList = [] } = newData?.conditions || {};
const newConditionsList = conditionsList.map((condition) => {
const newCondition = { ...condition };
@ -341,7 +342,7 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
index={index}
groupOperator={operator}
onOperatorChange={(value) => {
const newData = cloneDeep(data);
const newData = deepClone(data);
set(newData, 'conditions.operator', value);
onChange(newData);
}}
@ -352,13 +353,13 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
}
return con;
});
const newData = cloneDeep(data);
const newData = deepClone(data);
set(newData, 'conditions.conditionsList', newConditionsList);
onChange(newData);
}}
onRemove={() => {
const newConditionsList = conditionsList.filter((_cond, i) => i !== index);
const newData = cloneDeep(data);
const newData = deepClone(data);
set(newData, 'conditions.conditionsList', newConditionsList);
onChange(newData);
}}

View file

@ -2,14 +2,14 @@ import React, { useContext } from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import DropDownSelect from './DropDownSelect';
import { cloneDeep } from 'lodash';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export default function JoinSelect({ darkMode }) {
const { joinOptions, tableInfo, joinTableOptions, joinTableOptionsChange, findTableDetails } =
useContext(TooljetDatabaseContext);
const joinSelectOptions = cloneDeep(joinTableOptions['fields']) || [];
const joinSelectOptions = deepClone(joinTableOptions['fields']) || [];
const setJoinSelectOptions = (fields) => {
joinTableOptionsChange('fields', fields);
};

View file

@ -9,18 +9,17 @@ import { DeleteRows } from './DeleteRows';
import { toast } from 'react-hot-toast';
import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
import { useMounted } from '@/_hooks/use-mount';
import { useCurrentState } from '@/_stores/currentStateStore';
import { JoinTable } from './JoinTable';
import { cloneDeep, difference } from 'lodash';
import { difference } from 'lodash';
import DropDownSelect from './DropDownSelect';
import { getPrivateRoute } from '@/_helpers/routes';
import { useNavigate } from 'react-router-dom';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLayout }) => {
const computeSelectStyles = (darkMode, width) => {
return queryManagerSelectComponentStyle(darkMode, width);
};
const currentState = useCurrentState();
const navigate = useNavigate();
const { current_organization_id: organizationId } = authenticationService.currentSessionValue;
const mounted = useMounted();
@ -64,7 +63,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
setJoinTableOptions((prevJoinOptions) => {
const { conditions, order_by = [], joins: currJoins, fields: currFields = [] } = prevJoinOptions;
const conditionsList = cloneDeep(conditions?.conditionsList || []);
const conditionsList = deepClone(conditions?.conditionsList || []);
const newConditionsList = conditionsList.filter((condition) => {
const { leftField } = condition || {};
if (tableSet.has(leftField?.table)) {
@ -244,7 +243,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
if (isNewTableAdded) {
setJoinTableOptions((joinOptions) => {
const { fields } = joinOptions;
const newFields = cloneDeep(fields).filter((field) => field.table !== tableId);
const newFields = deepClone(fields).filter((field) => field.table !== tableId);
newFields.push(
...(data?.result?.columns
? data.result.columns.map((col) => ({
@ -393,7 +392,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
if (isNewTableAdded) {
setJoinTableOptions((joinOptions) => {
const { fields } = joinOptions;
const newFields = cloneDeep(fields).filter((field) => field.table !== tableId);
const newFields = deepClone(fields).filter((field) => field.table !== tableId);
newFields.push(
...(data?.result?.columns
? data.result.columns.map((col) => ({
@ -525,14 +524,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
</div>
{/* component to render based on the operation */}
{ComponentToRender && (
<ComponentToRender
currentState={currentState}
options={options}
optionchanged={optionchanged}
darkMode={darkMode}
/>
)}
{ComponentToRender && <ComponentToRender options={options} optionchanged={optionchanged} darkMode={darkMode} />}
</TooljetDatabaseContext.Provider>
);
};

View file

@ -72,7 +72,7 @@ const QueryManager = ({ mode, appId, darkMode, apps, allComponents, appDefinitio
parameters: selectedQuery?.options?.parameters?.reduce(
(parameters, parameter) => ({
...parameters,
[parameter.name]: resolveReferences(parameter.defaultValue, {}, undefined),
[parameter.name]: resolveReferences(parameter.defaultValue, undefined),
}),
{}
),

View file

@ -7,9 +7,10 @@ import useWindowResize from '@/_hooks/useWindowResize';
import { useQueryPanelActions, useQueryPanelStore } from '@/_stores/queryPanelStore';
import { useDataQueriesStore, useDataQueries } from '@/_stores/dataQueriesStore';
import Maximize from '@/_ui/Icon/solidIcons/Maximize';
import { cloneDeep, isEmpty, isEqual } from 'lodash';
import { isEmpty, isEqual } from 'lodash';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import cx from 'classnames';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
const QueryPanel = ({
dataQueriesChanged,
@ -54,10 +55,10 @@ const QueryPanel = ({
}
//removing updated_at since this value changes whenever the data is updated in the BE
const formattedQuery = cloneDeep(selectedQuery);
const formattedQuery = deepClone(selectedQuery);
delete formattedQuery.updated_at;
const formattedPrevQuery = cloneDeep(prevState?.selectedQuery || {});
const formattedPrevQuery = deepClone(prevState?.selectedQuery || {});
delete formattedPrevQuery.updated_at;
if (!isEqual(formattedQuery, formattedPrevQuery)) {

View file

@ -15,7 +15,7 @@ import {
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { toast } from 'react-hot-toast';
import { restrictedWidgetsObj } from '@/Editor/WidgetManager/restrictedWidgetsConfig';
import { useCurrentState } from '@/_stores/currentStateStore';
import { getCurrentState } from '@/_stores/currentStateStore';
import { shallow } from 'zustand/shallow';
import { useEditorStore } from '@/_stores/editorStore';
@ -24,6 +24,7 @@ import { useEditorStore } from '@/_stores/editorStore';
import { diff } from 'deep-object-diff';
import { useGridStore, useResizingComponentId } from '@/_stores/gridStore';
import GhostWidget from './GhostWidget';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const SubContainer = ({
mode,
@ -58,7 +59,6 @@ export const SubContainer = ({
}) => {
const appDefinition = useEditorStore((state) => state.appDefinition, shallow);
const currentState = useCurrentState();
const { selectedComponents } = useEditorStore(
(state) => ({
selectedComponents: state.selectedComponents,
@ -190,22 +190,24 @@ export const SubContainer = ({
}, [containerWidth]);
useEffect(() => {
if (appDefinitionChanged) {
const definition = useEditorStore.getState().appDefinition;
if (definition) {
const newDefinition = {
...appDefinition,
...definition,
pages: {
...appDefinition.pages,
...definition.pages,
[currentPageId]: {
...appDefinition.pages[currentPageId],
...definition.pages[currentPageId],
components: {
...appDefinition.pages[currentPageId].components,
...definition.pages[currentPageId].components,
...childWidgets,
},
},
},
};
const oldComponents = appDefinition.pages[currentPageId]?.components ?? {};
const oldComponents = definition.pages[currentPageId]?.components ?? {};
const newComponents = newDefinition.pages[currentPageId]?.components ?? {};
const componendAdded = Object.keys(newComponents).length > Object.keys(oldComponents).length;
@ -216,7 +218,7 @@ export const SubContainer = ({
opts.componentAdded = true;
}
const shouldUpdate = !_.isEmpty(diff(appDefinition, newDefinition));
const shouldUpdate = !_.isEmpty(diff(definition, newDefinition));
if (shouldUpdate) {
appDefinitionChanged(newDefinition, opts);
@ -241,7 +243,7 @@ export const SubContainer = ({
return;
}
const componentMeta = _.cloneDeep(
const componentMeta = deepClone(
componentTypes.find((component) => component.component === item.component.component)
);
const canvasBoundingRect = parentRef.current.getElementsByClassName('real-canvas')[0].getBoundingClientRect();
@ -280,14 +282,14 @@ export const SubContainer = ({
Listview: 'listItem',
});
const customResolverVariable = widgetResolvables[parentMeta?.component];
const defaultChildren = _.cloneDeep(parentMeta)['defaultChildren'];
const defaultChildren = deepClone(parentMeta)['defaultChildren'];
const parentId = newComponent.id;
defaultChildren.forEach((child) => {
const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } =
child;
const componentMeta = _.cloneDeep(
const componentMeta = deepClone(
componentTypes.find((component) => component.component === componentName)
);
const componentData = JSON.parse(JSON.stringify(componentMeta));
@ -530,6 +532,7 @@ export const SubContainer = ({
}
const getContainerProps = (componentId) => {
const currentState = getCurrentState();
return {
mode,
snapToGrid,

View file

@ -25,7 +25,7 @@ import {
import queryString from 'query-string';
import ViewerLogoIcon from './Icons/viewer-logo.svg';
import { DataSourceTypes } from './DataSourceManager/SourceComponents';
import { resolveReferences, isQueryRunnable, setWindowTitle, pageTitles, isValidUUID } from '@/_helpers/utils';
import { resolveReferences, isQueryRunnable, isValidUUID } from '@/_helpers/utils';
import { withTranslation } from 'react-i18next';
import _ from 'lodash';
import { Navigate } from 'react-router-dom';
@ -50,6 +50,8 @@ import { findAllEntityReferences } from '@/_stores/utils';
import { dfs } from '@/_stores/handleReferenceTransactions';
import useAppDarkMode from '@/_hooks/useAppDarkMode';
import TooljetBanner from './Viewer/TooljetBanner';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
class ViewerComponent extends React.Component {
constructor(props) {
@ -491,7 +493,7 @@ class ViewerComponent extends React.Component {
useCurrentStateStore.getState().actions.initializeCurrentStateOnVersionSwitch();
this.setStateForApp(data, true);
this.setStateForContainer(data);
setWindowTitle({
fetchAndSetWindowTitle({
page: pageTitles.VIEWER,
appName: data.name,
preview,
@ -516,7 +518,7 @@ class ViewerComponent extends React.Component {
await appService
.fetchAppByVersion(appId, versionId)
.then((data) => {
setWindowTitle({
fetchAndSetWindowTitle({
page: pageTitles.VIEWER,
appName: data.name,
preview: true,
@ -703,7 +705,7 @@ class ViewerComponent extends React.Component {
const bgColor =
(this.props.canvasBackground?.backgroundFxQuery || this.props.canvasBackground?.canvasBackgroundColor) ??
'#2f3c4c';
const resolvedBackgroundColor = resolveReferences(bgColor, this.props.currentState);
const resolvedBackgroundColor = resolveReferences(bgColor);
if (['#2f3c4c', '#F2F2F5', '#edeff5'].includes(resolvedBackgroundColor)) {
return this.props.darkMode ? '#2f3c4c' : '#F2F2F5';
}
@ -851,7 +853,7 @@ class ViewerComponent extends React.Component {
const queryConfirmationList = this.props?.queryConfirmationList ?? [];
const canvasMaxWidth = this.computeCanvasMaxWidth();
const pages =
Object.entries(_.cloneDeep(appDefinition)?.pages)
Object.entries(deepClone(appDefinition)?.pages)
.map(([id, page]) => ({ id, ...page }))
.sort((a, b) => a.index - b.index) || [];
@ -1074,14 +1076,18 @@ const withStore = (Component) => (props) => {
}
React.useEffect(() => {
const currentComponentsDef = appDefinition?.pages?.[currentPageId]?.components || {};
const currentComponents = Object.keys(currentComponentsDef);
const isPageSwitched = useResolveStore.getState().isPageSwitched;
setTimeout(() => {
if (currentComponents.length > 0) {
batchUpdateComponents(currentComponents);
}
}, 400);
if (isPageSwitched) {
const currentComponentsDef = appDefinition?.pages?.[currentPageId]?.components || {};
const currentComponents = Object.keys(currentComponentsDef);
setTimeout(() => {
if (currentComponents.length > 0) {
batchUpdateComponents(currentComponents);
}
}, 400);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPageId]);

View file

@ -1,5 +1,17 @@
import { resolveReferences } from '@/_helpers/utils';
import { resolveReferences as newResolveReference } from './CodeEditor/utils';
const handleResolveReferences = (initialValue, defaultValue, customResolvers) => {
const [_, error, value] = newResolveReference(initialValue, {}, customResolvers);
if (error) {
return defaultValue;
}
return value;
};
export const resolveProperties = (component, currentState, defaultValue, customResolvables) => {
if (currentState) {
return Object.entries(component.definition.properties).reduce(
@ -8,7 +20,7 @@ export const resolveProperties = (component, currentState, defaultValue, customR
...{
[entry[0]]: entry[1]?.skipResolve
? entry[1].value
: resolveReferences(entry[1].value, currentState, defaultValue, customResolvables),
: handleResolveReferences(entry[1].value, defaultValue, customResolvables),
},
}),
{}
@ -23,7 +35,7 @@ export const resolveStyles = (component, currentState, defaultValue, customResol
const key = entry[0];
const value = entry[1]?.skipResolve
? entry[1].value
: resolveReferences(entry[1].value, currentState, defaultValue, customResolvables);
: handleResolveReferences(entry[1].value, defaultValue, customResolvables);
return {
...resolvedStyles,
...{ [key]: value },
@ -41,7 +53,7 @@ export const resolveGeneralProperties = (component, currentState, defaultValue,
const key = entry[0];
const value = entry[1]?.skipResolve
? entry[1].value
: resolveReferences(entry[1].value, currentState, defaultValue, customResolvables);
: handleResolveReferences(entry[1].value, defaultValue, customResolvables);
return {
...resolvedGeneral,
...{ [key]: value },
@ -59,7 +71,7 @@ export const resolveGeneralStyles = (component, currentState, defaultValue, cust
const key = entry[0];
const value = entry[1]?.skipResolve
? entry[1].value
: resolveReferences(entry[1].value, currentState, defaultValue, customResolvables);
: handleResolveReferences(entry[1].value, defaultValue, customResolvables);
return {
...resolvedGeneral,
...{ [key]: value },

View file

@ -14,6 +14,7 @@ const {
never,
} = require('superstruct');
import { validateMultilineCode } from '@/_helpers/utility';
import _ from 'lodash';
export const generateSchemaFromValidationDefinition = (definition, recursionDepth = 0) => {
@ -129,17 +130,17 @@ export const validateProperties = (resolvedProperties, propertyDefinitions) => {
? any()
: generateSchemaFromValidationDefinition(validationDefinition);
const reservedKeyword = ['app', 'window']; // Case-sensitive reserved keywords
const keywordRegex = new RegExp(`\\b(${reservedKeyword.join('|')})\\b`, 'i');
const hasReservedkeyword = keywordRegex.test(value);
if (typeof value === string && value.startsWith('{{') && value.endsWith('}}')) {
const { status, data } = validateMultilineCode(value);
if (hasReservedkeyword) {
allErrors.push({
property: propertyDefinitions[propertyName]?.displayName,
message: 'Code contains reserved keywords',
});
if (status === 'failed') {
allErrors.push({
property: propertyDefinitions[propertyName]?.displayName,
message: data,
});
return [propertyName, defaultValue];
return [propertyName, defaultValue];
}
}
const [_valid, errors, newValue] = propertyName ? validate(value, schema, defaultValue) : [true, []];

View file

@ -9,6 +9,7 @@ import { ButtonSolid } from '@/_components/AppButton';
import { withTranslation } from 'react-i18next';
import EnterIcon from '../../assets/images/onboardingassets/Icons/Enter';
import Spinner from '@/_ui/Spinner';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
class ForgotPasswordComponent extends React.Component {
constructor(props) {
super(props);
@ -21,6 +22,7 @@ class ForgotPasswordComponent extends React.Component {
};
}
darkMode = localStorage.getItem('darkMode') === 'true';
whiteLabelText = retrieveWhiteLabelText();
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value, emailError: '' });
@ -68,9 +70,10 @@ class ForgotPasswordComponent extends React.Component {
Forgot Password
</h2>
<p className="common-auth-sub-header" data-cy="forgot-password-sub-header">
New to ToolJet? &nbsp;
New to {this.whiteLabelText}? &nbsp;
<Link
to={'/signup'}
state={{ from: '/forgot-password' }}
tabIndex="-1"
style={{ color: this.darkMode && '#3E63DD' }}
data-cy="create-an-account-link"

View file

@ -20,7 +20,7 @@ import { SearchBox } from '@/_components';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { BreadCrumbContext } from '@/App';
import { pageTitles, setWindowTitle } from '@/_helpers/utils';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasource }) => {
const containerRef = useRef(null);
@ -69,7 +69,7 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
}, []);
useEffect(() => {
setWindowTitle({ page: `${selectedDataSource?.name || pageTitles.DATA_SOURCES}` });
fetchAndSetWindowTitle({ page: `${selectedDataSource?.name || pageTitles.DATA_SOURCES}` });
if (selectedDataSource) {
setModalProps({ ...modalProps, backdrop: false });
}
@ -328,6 +328,7 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
{datasources.map((item) => (
<Card
key={item.key}
darkMode={darkMode}
title={item.title}
src={item?.src}
usePluginIcon={isEmpty(item?.iconFile?.data)}

View file

@ -100,7 +100,7 @@ export const List = ({ updateSelectedDatasource }) => {
<EmptyFoldersIllustration />
</div>
<div className="tj-text-md text-secondary" data-cy="empty-ds-page-text">
No datasources added
{filteredData?.length === 0 && dataSources?.length !== 0 ? 'No results found' : 'No datasources added'}
</div>
</div>
);

View file

@ -6,6 +6,7 @@ import { GlobalDataSourcesPage } from './GlobalDataSourcesPage';
import { toast } from 'react-hot-toast';
import { BreadCrumbContext } from '@/App/App';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
export const GlobalDataSourcesContext = createContext({
showDataSourceManagerModal: false,
@ -39,6 +40,7 @@ export const GlobalDatasources = (props) => {
selectedDataSource
? updateSidebarNAV(selectedDataSource.name)
: !activeDatasourceList && updateSidebarNAV('Commonly used');
fetchAndSetWindowTitle({ page: `${selectedDataSource?.name || pageTitles.DATA_SOURCES}` });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(dataSources), JSON.stringify(selectedDataSource)]);

View file

@ -4,6 +4,7 @@ import EmptyIllustration from '@assets/images/no-apps.svg';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useNavigate } from 'react-router-dom';
import EmptyFoldersIllustration from '@assets/images/icons/no-queries-added.svg';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
export const BlankPage = function BlankPage({
readAndImport,
@ -17,6 +18,7 @@ export const BlankPage = function BlankPage({
canCreateApp,
}) {
const { t } = useTranslation();
const whiteLabelText = retrieveWhiteLabelText();
const [deploying, setDeploying] = useState(false);
const navigate = useNavigate();
@ -56,12 +58,17 @@ export const BlankPage = function BlankPage({
<div className="row homepage-empty-container">
<div className="col-7">
<h3 className="empty-welcome-header" data-cy="empty-homepage-welcome-header">
{t('blankPage.welcomeToToolJet', 'Welcome to your new ToolJet workspace')}
{t('blankPage.welcomeToToolJet', `Welcome to your new ${whiteLabelText} workspace`, {
whiteLabelText,
})}
</h3>
<p className={`empty-title`} data-cy="empty-homepage-description">
{t(
'blankPage.getStartedCreateNewApp',
'You can get started by creating a new application or by creating an application using a template in ToolJet Library.'
`You can get started by creating a new application or by creating an application using a template in ${whiteLabelText} Library.`,
{
whiteLabelText,
}
)}
</p>
<div className="row mt-4">

View file

@ -12,6 +12,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
const [versionId, setVersionId] = useState(undefined);
const [exportTjDb, setExportTjDb] = useState(true);
const [currentVersion, setCurrentVersion] = useState(undefined);
const [versionSelectLoading, setVersionSelectLoading] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
@ -19,8 +20,11 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
setLoading(true);
try {
const fetchVersions = await appsService.getVersions(app.id);
const fetchTables = await appsService.getTables(app.id); // this is used to get all tables
const { versions } = fetchVersions;
const { tables } = fetchTables;
setVersions(versions);
setAllTables(tables);
const currentEditingVersion = versions?.filter((version) => version?.isCurrentEditingVersion)[0];
if (currentEditingVersion) {
setCurrentVersion(currentEditingVersion);
@ -39,11 +43,9 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
useEffect(() => {
async function fetchAppTables() {
setLoading(true);
setVersionSelectLoading(true);
try {
if (!versionId) return;
const fetchTables = await appsService.getTables(app.id); // this is used to get all tables
const { tables } = fetchTables;
const tbl = await appsService.getAppByVersion(app.id, versionId); // this is used to get particular App by version
const { dataQueries } = tbl;
const extractedIdData = [];
@ -68,14 +70,13 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
const uniqueSet = new Set(extractedIdData);
const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item }));
setTables(selectedVersiontable);
setAllTables(tables);
} catch (error) {
toast.error('Could not fetch the tables.', {
position: 'top-center',
});
closeModal();
}
setLoading(false);
setVersionSelectLoading(false);
}
fetchAppTables();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -209,7 +210,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
Export All
</ButtonSolid>
<ButtonSolid
className="import-export-footer-btns"
className={`import-export-footer-btns ${versionSelectLoading ? 'btn-loading' : ''}`}
data-cy="export-selected-version-button"
onClick={() => exportApp(app, versionId, exportTjDb, tables)}
>

View file

@ -353,7 +353,7 @@ export const Folders = function Folders({
</div>
</div>
<div className="row">
<div className="col d-flex modal-footer-btn">
<div className="col d-flex modal-footer-btn justify-content-end">
<ButtonSolid variant="tertiary" onClick={closeModal} data-cy="cancel-button">
{t('globals.cancel', 'Cancel')}
</ButtonSolid>

View file

@ -20,12 +20,13 @@ import Footer from './Footer';
import { OrganizationList } from '@/_components/OrganizationManager/List';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import BulkIcon from '@/_ui/Icon/bulkIcons/index';
import { getWorkspaceId, pageTitles, setWindowTitle } from '@/_helpers/utils';
import { getWorkspaceId } from '@/_helpers/utils';
import { getQueryParams } from '@/_helpers/routes';
import { withRouter } from '@/_hoc/withRouter';
import FolderFilter from './FolderFilter';
import { APP_ERROR_TYPE } from '@/_helpers/error_constants';
import Skeleton from 'react-loading-skeleton';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
const { iconList, defaultIcon } = configs;
@ -87,7 +88,7 @@ class HomePageComponent extends React.Component {
};
componentDidMount() {
setWindowTitle({ page: pageTitles.DASHBOARD });
fetchAndSetWindowTitle({ page: pageTitles.DASHBOARD });
this.fetchApps(1, this.state.currentFolder.id);
this.fetchFolders();
this.setQueryParameter();
@ -238,7 +239,13 @@ class HomePageComponent extends React.Component {
};
event.target.value = null;
} catch (error) {
toast.error(error.message);
let errorMessage = 'Some Error Occured';
if (error?.error) {
errorMessage = error.error;
} else if (error?.message) {
errorMessage = error.message;
}
toast.error(errorMessage);
}
};
@ -271,7 +278,7 @@ class HomePageComponent extends React.Component {
if (error.statusCode === 409) {
return false;
}
toast.error(error?.error || 'App import failed');
toast.error(error?.error || error?.message || 'App import failed');
}
};
@ -720,7 +727,7 @@ class HomePageComponent extends React.Component {
</div>
</div>
<div className="row">
<div className="col d-flex modal-footer-btn">
<div className="col d-flex modal-footer-btn justify-content-end">
<ButtonSolid
variant="tertiary"
onClick={() => this.setState({ showAddToFolderModal: false, appOperations: {} })}
@ -750,7 +757,7 @@ class HomePageComponent extends React.Component {
</div>
</div>
<div className="row">
<div className="col d-flex modal-footer-btn">
<div className="col d-flex modal-footer-btn justify-content-end">
<ButtonSolid
onClick={() => this.setState({ showChangeIconModal: false, appOperations: {} })}
data-cy="cancel-button"

View file

@ -17,6 +17,7 @@ import { onLoginSuccess } from '@/_helpers/platform/utils/auth.utils';
import { updateCurrentSession } from '@/_helpers/authorizeWorkspace';
import cx from 'classnames';
import SSOLoginModule from './SSOLoginModule';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
class LoginPageComponent extends React.Component {
constructor(props) {
@ -31,6 +32,7 @@ class LoginPageComponent extends React.Component {
this.paramOrganizationSlug = props?.params?.organizationId;
}
darkMode = localStorage.getItem('darkMode') === 'true';
whiteLabelText = retrieveWhiteLabelText();
componentDidMount() {
/* remove login oranization's id and slug from the cookie */
@ -130,7 +132,9 @@ class LoginPageComponent extends React.Component {
const signUpCTA = workspaceSignUpEnabled ? 'Sign up' : 'Create an account';
const signupText = workspaceSignUpEnabled
? this.props.t('loginSignupPage.newToWorkspace', `New to this workspace?`)
: this.props.t('loginSignupPage.newToTooljet', `New to Tooljet?`);
: this.props.t('loginSignupPage.newToTooljet', ` New to ${this.whiteLabelText}?`, {
whiteLabelText: this.whiteLabelText,
});
const signUpUrl = `/signup${this.paramOrganizationSlug ? `/${this.paramOrganizationSlug}` : ''}${
redirectTo ? `?redirectTo=${redirectTo}` : ''
}`;

View file

@ -10,6 +10,7 @@ import ManageOrgVarsDrawer from './ManageOrgVarsDrawer';
import { Alert } from '@/_ui/Alert/Alert';
import { Button } from '@/_ui/LeftSidebar';
import { useNavigate, useParams } from 'react-router-dom';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
function useWorkspaceRouting() {
const navigate = useNavigate();
@ -89,7 +90,7 @@ class RawManageOrgVarsComponent extends React.Component {
});
orgEnvironmentVariableService.getVariables().then((data) => {
const variables = _.cloneDeep(data.variables)?.filter(({ variable_name }) => !/copilot_/.test(variable_name));
const variables = deepClone(data.variables)?.filter(({ variable_name }) => !/copilot_/.test(variable_name));
this.setState({
variables: variables,
isLoading: false,

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import useRouter from '@/_hooks/use-router';
import { authenticationService } from '@/_services';
import { appService, authenticationService } from '@/_services';
import { Navigate } from 'react-router-dom';
import Configs from './Configs/Config.json';
import { RedirectLoader } from '../_components';
@ -85,7 +85,22 @@ export function Authorize({ navigate }) {
const inviteeEmail = details?.inviteeEmail;
if (inviteeEmail) setInviteeEmail(inviteeEmail);
const errMessage = details?.message || err?.error || 'something went wrong';
setError(`${configs.name} login failed - ${errMessage}`);
if (!inviteeEmail && inviteFlowIdentifier) {
/* Some unexpected error happened from the provider side. Need to retreive email to continue */
appService
.getInviteeDetails(inviteFlowIdentifier)
.then((response) => {
setInviteeEmail(response.email);
})
.catch(() => {
console.error('Error while fetching invitee details');
})
.finally(() => {
setError(`${configs.name} login failed - ${errMessage}`);
});
} else {
setError(`${configs.name} login failed - ${errMessage}`);
}
});
};

View file

@ -6,6 +6,7 @@ import OnBoardingRadioInput from './OnBoardingRadioInput';
import ContinueButton from './ContinueButton';
import OnBoardingBubbles from './OnBoardingBubbles';
import { getuserName } from '@/_helpers/utils';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
import { redirectToDashboard } from '@/_helpers/routes';
import { ON_BOARDING_SIZE, ON_BOARDING_ROLES } from '@/_helpers/constants';
import LogoLightMode from '@assets/images/Logomark.svg';
@ -26,6 +27,7 @@ function OnBoardingForm({ userDetails = {}, token = '', organizationToken = '',
phoneNumber: '',
});
const source = new URLSearchParams(location?.search).get('source');
const whiteLabelText = retrieveWhiteLabelText();
const pageProps = {
formData,
@ -75,7 +77,7 @@ function OnBoardingForm({ userDetails = {}, token = '', organizationToken = '',
'Enter your phone number',
'Enter your phone number', //dummy for styling
];
const FormSubTitles = ['This information will help us improve ToolJet.'];
const FormSubTitles = [`This information will help us improve ${whiteLabelText}.`];
return (
<div className="flex">

View file

@ -4,6 +4,12 @@ import { useSessionManagement } from '@/_hooks/useSessionManagement';
import { getRedirectURL, pathnameToArray } from '@/_helpers/routes';
import { authenticationService } from '@/_services';
import { useLocation, useParams } from 'react-router-dom';
import {
resetToDefaultWhiteLabels,
retrieveWhiteLabelFavicon,
retrieveWhiteLabelText,
setFaviconAndTitle,
} from '@white-label/whiteLabelling';
export const AuthRoute = ({ children, navigate }) => {
const { isLoading, session, isValidSession, isInvalidSession, setLoading } = useSessionManagement({
@ -29,8 +35,7 @@ export const AuthRoute = ({ children, navigate }) => {
useEffect(
() => {
authenticationService.deleteAllAuthCookies();
fetchOrganizationDetails();
initialize();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[location.pathname]
@ -43,11 +48,30 @@ export const AuthRoute = ({ children, navigate }) => {
);
useEffect(() => {
const isComingFromPasswordReset = location?.state?.from === '/reset-password';
const isComingFromPasswordReset =
location?.state?.from === '/reset-password' || location?.state?.from === '/forgot-password';
if ((isInvalidSession || isComingFromPasswordReset) && !isGettingConfigs) setLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isInvalidSession, isGettingConfigs]);
const initialize = () => {
const pathname = location.pathname;
authenticationService.deleteAllAuthCookies();
fetchOrganizationDetails();
verifyWhiteLabeling(pathname);
};
const verifyWhiteLabeling = (pathname) => {
const signupRegex = /^\/signup\/[^/]+$/;
const loginRegex = /^\/login\/[^/]+$/;
if (!signupRegex.test(pathname) && !loginRegex.test(pathname)) {
resetToDefaultWhiteLabels();
}
const whiteLabelText = retrieveWhiteLabelText();
const whiteLabelFavicon = retrieveWhiteLabelFavicon();
setFaviconAndTitle(whiteLabelFavicon, whiteLabelText, location);
};
const fetchOrganizationDetails = () => {
authenticationService.getOrganizationConfigs(organizationSlug).then(
(configs) => {

View file

@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react';
import { RouteLoader } from './RouteLoader';
import { useLocation, useParams } from 'react-router-dom';
import { authenticationService } from '@/_services';
import { appService, authenticationService } from '@/_services';
import { authorizeUserAndHandleErrors, updateCurrentSession } from '@/_helpers/authorizeWorkspace';
import { toast } from 'react-hot-toast';
import { LinkExpiredPage } from '@/ConfirmationPage/LinkExpiredPage';
import { onLoginSuccess } from '@/_helpers/platform/utils/auth.utils';
export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite, navigate }) => {
/* Needed to pass invite token to signup page if the user doesn't exist */
@ -15,6 +16,8 @@ export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite,
const organizationToken = params.organizationToken || (isOrgazanizationOnlyInvite ? params.token : null);
const accountToken = !isOrgazanizationOnlyInvite ? params.token : null;
const [extraProps, setExtraProps] = useState({});
const searchParams = new URLSearchParams(location?.search);
const redirectTo = searchParams.get('redirectTo');
useEffect(() => {
getInvitedUserSession();
@ -33,6 +36,8 @@ export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite,
name,
organization_invite_url,
is_workspace_sign_up_invite,
source,
organization_user_source,
} = invitedUserSession;
/*
We should only run the authorization against the session if the user has active workspace
@ -42,6 +47,10 @@ export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite,
email,
name,
});
if (source === 'workspace_signup' || organization_user_source === 'signup') {
acceptInvite(accountToken, organizationToken, navigate, source, redirectTo);
return;
}
if (is_workspace_sign_up_invite) {
setLoading(false);
return;
@ -141,6 +150,41 @@ export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite,
);
};
const acceptInvite = (token, organizationToken, navigate, source, redirectTo) => {
if (token && organizationToken) {
authenticationService
.onboarding({
token,
organizationToken,
source,
})
.then((user) => {
onLoginSuccess(user, navigate, redirectTo);
})
.catch((res) => {
toast.error(res.error || 'Something went wrong', {
id: 'toast-login-auth-error',
position: 'top-center',
});
});
} else {
appService
.acceptInvite({
token: organizationToken,
})
.then((data) => {
toast.success(`Added to the workspace successfully.`);
updateCurrentSession({
isUserLoggingIn: true,
});
onLoginSuccess(data, navigate);
})
.catch(() => {
toast.error('Error while setting up your account.', { position: 'top-center' });
});
}
};
if (invalidLink) return <LinkExpiredPage />;
const clonedElement = React.cloneElement(children || <></>, extraProps);

View file

@ -17,6 +17,7 @@ import { extractErrorObj, onInvitedUserSignUpSuccess } from '@/_helpers/platform
import { isEmpty } from 'lodash';
import { EmailComponent } from './EmailComponent';
import SSOLoginModule from '@/LoginPage/SSOLoginModule';
import { checkWhiteLabelsDefaultState } from '@white-label/whiteLabelling';
class SignupPageComponent extends React.Component {
constructor(props) {
super(props);
@ -34,6 +35,7 @@ class SignupPageComponent extends React.Component {
emailError: '',
disableOnEdit: false,
email: this.inviteeEmail || '',
defaultState: false,
};
}
@ -42,6 +44,9 @@ class SignupPageComponent extends React.Component {
if (errorMessage) {
toast.error(errorMessage);
}
checkWhiteLabelsDefaultState(this.inviteOrganizationId).then((res) => {
this.setState({ defaultState: res });
});
}
backtoSignup = (email, name) => {
@ -122,7 +127,7 @@ class SignupPageComponent extends React.Component {
render() {
const { configs } = this.props;
const { isLoading, signupSuccess } = this.state;
const { isLoading, signupSuccess, defaultState } = this.state;
const comingFromInviteFlow = !!this.organizationToken;
const isSignUpButtonDisabled =
isLoading ||
@ -320,20 +325,22 @@ class SignupPageComponent extends React.Component {
</>
)}
<p className="signup-terms" data-cy="signup-terms-helper">
By signing up you are agreeing to the
<br />
<span>
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
Terms of Service{' '}
</a>
&
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
{' '}
Privacy Policy
</a>
</span>
</p>
{defaultState && (
<p className="signup-terms" data-cy="signup-terms-helper">
By signing up you are agreeing to the
<br />
<span>
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
Terms of Service{' '}
</a>
&
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
{' '}
Privacy Policy
</a>
</span>
</p>
)}
</div>
</>
)
@ -347,6 +354,8 @@ class SignupPageComponent extends React.Component {
name={this.state.name}
backtoSignup={this.backtoSignup}
darkMode={this.darkMode}
organizationId={this.inviteOrganizationId}
redirectTo={this.redirectTo}
/>
</div>
)}

View file

@ -1,8 +1,10 @@
import React from 'react';
import { ButtonSolid } from '@/_components/AppButton';
import { useNavigate } from 'react-router-dom';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
export const PasswordResetinfoScreen = function PasswordResetinfoScreen({ darkMode }) {
const whiteLabelText = retrieveWhiteLabelText();
const navigate = useNavigate();
return (
<div className="info-screen-wrapper">
@ -22,7 +24,7 @@ export const PasswordResetinfoScreen = function PasswordResetinfoScreen({ darkMo
Password has been reset
</h1>
<p className="info-screen-description" data-cy="reset-password-page-description">
Your password has been reset successfully, log into ToolJet to continue your session
Your password has been reset successfully, log into {whiteLabelText} to continue your session
</p>
<ButtonSolid
variant="secondary"

View file

@ -4,7 +4,14 @@ import { authenticationService } from '@/_services';
import { toast } from 'react-hot-toast';
import Spinner from '@/_ui/Spinner';
export const SignupInfoScreen = function SignupInfoScreen({ email, backtoSignup, name, darkMode }) {
export const SignupInfoScreen = function SignupInfoScreen({
email,
backtoSignup,
name,
darkMode,
organizationId,
redirectTo,
}) {
const [resendBtn, setResetBtn] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [buttonText, setButtonText] = useState('Resend verification mail in 30s');
@ -30,7 +37,7 @@ export const SignupInfoScreen = function SignupInfoScreen({ email, backtoSignup,
e.preventDefault();
authenticationService
.resendInvite(email)
.resendInvite(email, organizationId, redirectTo)
.then(() => {
setIsLoading(false);
setResetBtn(true);

View file

@ -1,10 +1,8 @@
import React, { useState, useEffect } from 'react';
import EnterIcon from '../../assets/images/onboardingassets/Icons/Enter';
import GoogleSSOLoginButton from '@ee/components/LoginPage/GoogleSSOLoginButton';
import GitSSOLoginButton from '@ee/components/LoginPage/GitSSOLoginButton';
import OnBoardingForm from '../OnBoardingForm/OnBoardingForm';
import { authenticationService } from '@/_services';
import { useLocation, useParams } from 'react-router-dom';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { LinkExpiredInfoScreen } from '@/SuccessInfoScreen';
import { ShowLoading } from '@/_components';
import { toast } from 'react-hot-toast';
@ -15,7 +13,9 @@ import EyeShow from '../../assets/images/onboardingassets/Icons/EyeShow';
import Spinner from '@/_ui/Spinner';
import { useTranslation } from 'react-i18next';
import { buildURLWithQuery } from '@/_helpers/utils';
import { onLoginSuccess } from '@/_helpers/platform/utils/auth.utils';
import { redirectToDashboard } from '@/_helpers/routes';
import { retrieveWhiteLabelText, setFaviconAndTitle, checkWhiteLabelsDefaultState } from '@white-label/whiteLabelling';
export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScreen() {
const [showOnboarding, setShowOnboarding] = useState(false);
@ -29,6 +29,7 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
const [showPassword, setShowPassword] = useState(false);
const [fallBack, setFallBack] = useState(false);
const { t } = useTranslation();
const [defaultState, setDefaultState] = useState(false);
const location = useLocation();
const params = useParams();
@ -39,6 +40,8 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
const source = searchParams.get('source');
const darkMode = localStorage.getItem('darkMode') === 'true';
const redirectTo = searchParams.get('redirectTo');
const navigate = useNavigate();
const whiteLabelText = retrieveWhiteLabelText();
const getUserDetails = () => {
setIsLoading(true);
@ -81,6 +84,7 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
(configs) => {
setIsGettingConfigs(false);
setConfigs(configs);
setFaviconAndTitle(null, null, location);
},
() => {
setIsGettingConfigs(false);
@ -89,6 +93,9 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
} else {
setIsGettingConfigs(false);
}
checkWhiteLabelsDefaultState(organizationId).then((res) => {
setDefaultState(res);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -121,7 +128,7 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
.then((user) => {
authenticationService.deleteLoginOrganizationId();
setIsLoading(false);
redirectToDashboard(user, redirectTo);
onLoginSuccess(user, navigate, redirectTo);
})
.catch((res) => {
setIsLoading(false);
@ -180,14 +187,14 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
) : (
<div className="common-auth-container-wrapper">
<h2 className="common-auth-section-header org-invite-header" data-cy="invite-page-header">
Join {configs?.name ? configs?.name : 'ToolJet'}
Join {configs?.name ? configs?.name : whiteLabelText}
</h2>
<div className="invite-sub-header" data-cy="invite-page-sub-header">
{`You are invited to ${
configs?.name
? `a workspace ${configs?.name}. Accept the invite to join the workspace.`
: 'ToolJet.'
: `${whiteLabelText}.`
}`}
</div>
@ -288,20 +295,22 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
)}
</ButtonSolid>
</div>
<p className="verification-terms" data-cy="signup-terms-helper">
By signing up you are agreeing to the
<br />
<span>
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
Terms of Service{' '}
</a>
&
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
{' '}
Privacy Policy
</a>
</span>
</p>
{defaultState && (
<p className="verification-terms" data-cy="signup-terms-helper">
By signing up you are agreeing to the
<br />
<span>
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
Terms of Service{' '}
</a>
&
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
{' '}
Privacy Policy
</a>
</span>
</p>
)}
</div>
)}
</form>
@ -331,7 +340,7 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
{t('verificationSuccessPage.successfullyVerifiedEmail', 'Successfully verified email')}
</h1>
<p className="info-screen-description" data-cy="onboarding-page-description">
Continue to set up your workspace to start using ToolJet.
Continue to set up your workspace to start using {whiteLabelText}.
</p>
<ButtonSolid
className="verification-success-info-btn "
@ -347,7 +356,10 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
</div>
) : (
<>
{t('verificationSuccessPage.setupTooljet', 'Set up ToolJet')}
{t('verificationSuccessPage.setupTooljet', `Set up ${whiteLabelText}`, {
whiteLabelText,
})}
<EnterIcon fill={'#fff'}></EnterIcon>
</>
)}

View file

@ -1,47 +0,0 @@
import React from 'react';
import List from './List';
import ListGroup from 'react-bootstrap/ListGroup';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: 'List',
component: List,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
// tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
};
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const WithIcon = {
render: (args) => (
<List>
<List.Item>Client side</List.Item>
<List.Item active>Server side</List.Item>
<List.Item>Client side</List.Item>
<List.Item disabled>Server side</List.Item>
<List.Item>Client side</List.Item>
<List.Item>Server side</List.Item>
<List.Item>Client side</List.Item>
<List.Item>Server side</List.Item>
</List>
),
};
export const WithIconAndEdit = {
render: (args) => (
<ListGroup>
<ListGroup.Item>Cras justo odio</ListGroup.Item>
<ListGroup.Item>Dapibus ac facilisis in</ListGroup.Item>
<ListGroup.Item>Morbi leo risus</ListGroup.Item>
<ListGroup.Item>Porta ac consectetur ac</ListGroup.Item>
<ListGroup.Item>Vestibulum at eros</ListGroup.Item>
</ListGroup>
),
};

View file

@ -5,7 +5,7 @@ import ToggleGroupItem from './ToggleGroupItem';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: 'ToggleGroup',
title: 'Components/ToggleGroup',
component: ToggleGroup,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout

View file

@ -1,45 +0,0 @@
import { Tabs } from './Tabs';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: 'Tabs',
component: Tabs,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
};
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
label: 'Button',
},
};
export const Large = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small = {
args: {
size: 'small',
label: 'Button',
},
};

View file

@ -11,6 +11,7 @@ import WarningInfo from '../Icons/Edit-information.svg';
import { ConfirmDialog } from '@/_components';
import { serialDataType } from '../constants';
import cx from 'classnames';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
const TableForm = ({
selectedTable = {},
@ -38,7 +39,7 @@ const TableForm = ({
const [showModal, setShowModal] = useState(false);
const [createForeignKeyInEdit, setCreateForeignKeyInEdit] = useState(false);
const [tableName, setTableName] = useState(selectedTable.table_name);
const [columns, setColumns] = useState(_.cloneDeep(selectedTableColumns));
const [columns, setColumns] = useState(deepClone(selectedTableColumns));
const { organizationId, foreignKeys, setForeignKeys } = useContext(TooljetDatabaseContext);
const { updateSidebarNAV } = useContext(BreadCrumbContext);

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