Merge pull request #8304 from ToolJet/main

Merge main back to develop (v2.26.0)
This commit is contained in:
Kavin Venkatachalam 2023-12-13 15:32:47 +05:30 committed by GitHub
commit 9a9a4d15cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 13415 additions and 979 deletions

View file

@ -1 +1 @@
2.25.0
2.26.0

View file

@ -44,7 +44,11 @@ Cypress.Commands.add("apiCreateGDS", (url, name, kind, options) => {
},
{ log: false }
).then((response) => {
{
log: false;
}
expect(response.status).to.equal(201);
Cypress.env(`${name}-id`, response.body.id);
Cypress.log({
name: "Create Data Source",
@ -79,6 +83,9 @@ Cypress.Commands.add("apiCreateApp", (appName = "testApp") => {
user_id: "",
},
}).then((response) => {
{
log: false;
}
expect(response.status).to.equal(201);
Cypress.env("appId", response.allRequestResponses[0]["Response Body"].id);
Cypress.log({

View file

@ -3,6 +3,8 @@ import { dashboardSelector } from "Selectors/dashboard";
import { ssoSelector } from "Selectors/manageSSO";
import { commonText, createBackspaceText } from "Texts/common";
import { passwordInputText } from "Texts/passwordInput";
import { importSelectors } from "Selectors/exportImport";
import { importText } from "Texts/exportImport";
Cypress.Commands.add(
"login",
@ -186,14 +188,14 @@ Cypress.Commands.add(
},
(subject, value) => {
cy.wrap(subject)
.click()
.realClick()
.find("pre.CodeMirror-line")
.invoke("text")
.then((text) => {
cy.wrap(subject).type(createBackspaceText(text)),
{
delay: 0,
};
cy.wrap(subject).realType(createBackspaceText(text)),
{
delay: 0,
};
});
}
);
@ -299,9 +301,71 @@ Cypress.Commands.add("hideTooltip", () => {
});
});
Cypress.Commands.add("importApp", (appFile) => {
cy.get(importSelectors.dropDownMenu).should("be.visible").click();
cy.get(importSelectors.importOptionInput).eq(0).selectFile(appFile, {
force: true,
});
cy.verifyToastMessage(
commonSelectors.toastMessage,
importText.appImportedToastMessage
);
});
Cypress.Commands.add("moveComponent", (componentName, x, y) => {
cy.get(`[data-cy="draggable-widget-${componentName}"]`, { log: false })
.trigger("mouseover", {
force: true,
log: false,
})
.trigger("mousedown", {
which: 1,
force: true,
log: false,
});
cy.get(commonSelectors.canvas, { log: false })
.trigger("mousemove", {
which: 1,
clientX: x,
ClientY: y,
clientX: x,
clientY: y,
pageX: x,
pageY: y,
screenX: x,
screenY: y,
log: false,
})
.trigger("mouseup", { log: false });
const log = Cypress.log({
name: "moveComponent",
displayName: "Component moved:",
message: `X: ${x}, Y:${y}`,
});
});
Cypress.Commands.add("getPosition", (componentName) => {
cy.get(commonWidgetSelector.draggableWidget(componentName)).then(
($element) => {
const element = $element[0];
const rect = element.getBoundingClientRect();
const clientX = Math.round(rect.left + window.scrollX + rect.width / 2);
const clientY = Math.round(rect.top + window.scrollY + rect.height / 2);
const log = Cypress.log({
name: "getPosition",
displayName: `${componentName}'s Position:\n`,
message: `\nX: ${clientX}, Y:${clientY}`,
});
return [clientX, clientY];
}
);
});
Cypress.Commands.add("defaultWorkspaceLogin", () => {
cy.apiLogin();
cy.visit('/my-workspace');
cy.get(commonSelectors.homePageLogo, { timeout: 10000 })
})
})

View file

@ -4,6 +4,7 @@ import { fake } from "Fixtures/fake";
import { commonWidgetText } from "Texts/common";
import { verifyControlComponentAction } from "Support/utils/button";
import { resizeQueryPanel } from "Support/utils/dataSource";
import {
openAccordion,
@ -39,6 +40,44 @@ describe("Editor- Test Button widget", () => {
cy.dragAndDropWidget(buttonText.defaultWidgetText, 500, 500);
});
it("should verify position of component after dragging", () => {
const data = {};
data.widgetName = buttonText.defaultWidgetName;
resizeQueryPanel(0);
cy.getPosition(data.widgetName).then((position) => {
const [clientX, clientY] = position;
expect(clientX).not.to.be.closeTo(100, 10);
expect(clientY).not.to.be.closeTo(100, 10);
});
cy.moveComponent(data.widgetName, 100, 100);
cy.waitForAutoSave();
cy.getPosition(data.widgetName).then((position) => {
const [clientX, clientY] = position;
expect(clientX).to.be.closeTo(100, 20);
expect(clientY).to.be.closeTo(100, 10);
});
cy.reload();
resizeQueryPanel(0);
cy.get(commonWidgetSelector.draggableWidget(data.widgetName)).should(
"be.visible"
);
cy.getPosition(data.widgetName).then((position) => {
const [clientX, clientY] = position;
expect(clientX).to.be.closeTo(100, 20);
expect(clientY).to.be.closeTo(100, 10);
});
cy.moveComponent(data.widgetName, 750, 750);
cy.getPosition(data.widgetName).then((position) => {
const [clientX, clientY] = position;
expect(clientX).to.be.closeTo(750, 20);
expect(clientY).to.be.closeTo(750, 10);
});
cy.apiDeleteApp(data.appName);
});
it("should verify the properties of the button widget", () => {
const data = {};
data.appName = `${fake.companyName}-App`;
@ -101,7 +140,7 @@ describe("Editor- Test Button widget", () => {
cy.apiDeleteApp(data.appName);
});
it("should verify the styles of the button widget", () => {
it("should verify the styles of the button component", () => {
const data = {};
data.appName = `${fake.companyName}-App`;
data.backgroundColor = fake.randomRgba;

View file

@ -40,6 +40,7 @@ describe("Basic components", () => {
cy.openApp();
cy.modifyCanvasSize(900, 900);
cy.intercept("GET", "/api/comments/*").as("loadComments");
resizeQueryPanel(0);
});
afterEach(() => {
cy.apiDeleteApp();
@ -409,6 +410,7 @@ describe("Basic components", () => {
verifyComponent("form2");
cy.go("back");
resizeQueryPanel(0);
deleteComponentAndVerify("form2");
});

View file

@ -351,7 +351,7 @@ describe("Table", () => {
cy.get('[data-index="0"]>.select-search-option:eq(1)').realClick();
verifyAndEnterColumnOptionInput("key", "name");
verifyAndEnterColumnOptionInput("Text color", "red");
verifyAndEnterColumnOptionInput("Cell Background color", "yellow");
verifyAndEnterColumnOptionInput("Cell background color", "yellow");
cy.get(
'[data-cy="input-and-label-cell-background-color"] > .form-label'
).click();

View file

@ -1,16 +1,128 @@
import {
verifyElemtsNoGds,
verifyElemtsWithGds,
} from "Support/utils/queryPanel/queryEditor";
import {
resizeQueryPanel,
query,
verifypreview,
} from "Support/utils/dataSource";
import { verifyNodeData, openNode, verifyValue } from "Support/utils/inspector";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import {
selectCSA,
selectEvent,
addSupportCSAData,
} from "Support/utils/events";
describe("Query Editor", () => {
beforeEach(() => {
cy.appUILogin();
cy.createApp();
cy.apiLogin();
});
it("should verify Elements on query editor", () => {});
it("should verify Elements on query editor", () => {
cy.apiCreateApp();
cy.openApp();
verifyElemtsNoGds();
verifyElemtsWithGds("cypress-psql");
cy.apiDeleteDS(`cypress-psql`);
});
it("should verify Functionality of query editor", () => {});
it("should verify Functionality of query editor", () => {
cy.apiCreateApp();
cy.openApp();
resizeQueryPanel("80");
it("should verify imported app's queries", () => {});
cy.get('[data-cy="restapi-add-query-card"]').click();
cy.get('[data-cy="query-rename-input"]').clear();
cy.forceClickOnCanvas();
cy.get('[data-cy="query-name-label"]').click();
cy.get('[data-cy="query-rename-input"]').clear().type("new name");
cy.forceClickOnCanvas();
cy.get('[data-cy="query-name-label"]').click();
cy.get('[data-cy="query-rename-input"]').clear().type("new_name");
cy.waitForAutoSave();
cy.wait(3000);
cy.get('[data-cy="-input-field"]:eq(0)').clearCodeMirror();
cy.get('[data-cy="-input-field"]:eq(0)').type(
"https://gorest.co.in/public/v2/users"
);
query("preview");
verifypreview("raw", '"gender":"male"');
});
it("should verify transformation", () => {});
it("should verify imported app's queries", () => {
cy.visit("/");
cy.window({ log: false }).then((win) => {
win.localStorage.setItem("walkthroughCompleted", "true");
});
cy.get('[data-testid="applicationFoldersList"]').should("be.visible");
cy.importApp(
"cypress/fixtures/templates/appbuilderApps/querytest-export-1695183432845.json"
);
resizeQueryPanel("70");
cy.get('[data-cy="draggable-widget-text1"]').verifyVisibleElement(
"have.text",
"dataPresent"
);
it("should verify Event Handler", () => {});
query("preview");
verifypreview("raw", "dataPresent");
});
it("should verify transformation", () => {
cy.apiCreateApp();
cy.openApp();
resizeQueryPanel("80");
cy.get('[data-cy="restapi-add-query-card"]').should("be.visible").click();
cy.get('[data-cy="query-rename-input"]').clear();
cy.get('[data-cy="query-rename-input"]').clear().type("new_name");
cy.waitForAutoSave();
cy.get('[data-cy="-input-field"]:eq(0)').clearCodeMirror();
cy.get('[data-cy="-input-field"]:eq(0)').type(
"https://gorest.co.in/public/v2/users"
);
cy.get("span.d-flex > .custom-toggle-switch > .switch > .slider").click();
cy.get('[data-cy="transformation-input-input-field"]').clearCodeMirror();
cy.get(
'[data-cy="transformation-input-input-field"]'
).clearAndTypeOnCodeMirror(
'return typeof(data.filter(person => person.gender === "female").length)==="number"'
);
query("preview");
verifypreview("raw", "true");
query("run");
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.hideTooltip();
openNode("queries");
openNode("new_name");
verifyValue("data", "Boolean", "true");
});
it("should verify Event Handler", () => {
cy.apiCreateApp();
cy.openApp();
resizeQueryPanel("80");
cy.get('[data-cy="restapi-add-query-card"]').should("be.visible").click();
cy.wait(500);
selectEvent("Query Failure", "Set variable");
addSupportCSAData("key", "globalVar");
addSupportCSAData("variable", "globalVar");
query("run");
cy.get(commonWidgetSelector.sidebarinspector).click();
openNode("variables");
verifyValue("globalVar", "String", `"globalVar"`);
cy.get('[data-cy="event-handler-card"] ')
.realHover()
.find(".tj-base-btn")
.click();
cy.notVisible('[data-cy="event-handler-card"] ');
});
});

View file

@ -5,6 +5,7 @@ import {
openEditorSidebar,
editAndVerifyWidgetName,
} from "Support/utils/commonWidget";
import { resizeQueryPanel } from "Support/utils/dataSource";
export const verifyComponent = (widgetName) => {
cy.get(commonWidgetSelector.draggableWidget(widgetName)).should("be.visible");
@ -46,5 +47,6 @@ export const verifyComponentWithOutLabel = (
verifyComponent(fakeName);
cy.go("back");
resizeQueryPanel(0);
deleteComponentAndVerify(fakeName);
};

View file

@ -18,32 +18,32 @@ export const verifyElemtsNoGds = (option) => {
);
cy.get('[data-cy="restapi-add-query-card"]').verifyVisibleElement(
"have.text",
"REST API"
" REST API"
);
cy.get('[data-cy="runjs-add-query-card"]').verifyVisibleElement(
"have.text",
"JavaScript"
" JavaScript"
);
cy.get('[data-cy="runpy-add-query-card"]').verifyVisibleElement(
"have.text",
"Python"
"contain.text",
" Python"
);
cy.get('[data-cy="tooljetdb-add-query-card"]').verifyVisibleElement(
"have.text",
"ToolJet DB"
" ToolJet DB"
);
cy.get('[data-cy="label-avilable-ds"]').verifyVisibleElement(
"have.text",
"Available Datasources 0"
"Available data sources 0"
);
cy.get('[data-cy="landing-page-add-new-ds-button"]').verifyVisibleElement(
"have.text",
"Add new"
);
cy.get('[data-cy="empty-banner-queryManager"]').verifyVisibleElement(
cy.get('[data-cy="label-no-ds-added"]').verifyVisibleElement(
"have.text",
"No global datasources have been added yet. Add new datasources to connect to your app! 🚀"
"No global data sources have been added yet."
);
};

View file

@ -1 +1 @@
2.25.0
2.26.0

View file

@ -301,73 +301,46 @@ export const Box = memo(
}}
role={preview ? 'BoxPreview' : 'Box'}
>
{inCanvas ? (
!resetComponent ? (
<ComponentToRender
onComponentClick={onComponentClick}
onComponentOptionChanged={onComponentOptionChanged}
currentState={currentState}
onEvent={onEvent}
id={id}
paramUpdated={paramUpdated}
width={width}
changeCanDrag={changeCanDrag}
onComponentOptionsChanged={onComponentOptionsChanged}
height={height}
component={component}
containerProps={containerProps}
darkMode={darkMode}
removeComponent={removeComponent}
canvasWidth={canvasWidth}
properties={validatedProperties}
exposedVariables={exposedVariables}
styles={{ ...validatedStyles, boxShadow: validatedGeneralStyles?.boxShadow }}
setExposedVariable={(variable, value) => onComponentOptionChanged(component, variable, value, id)}
setExposedVariables={(variableSet) => onComponentOptionsChanged(component, Object.entries(variableSet))}
fireEvent={fireEvent}
validate={validate}
parentId={parentId}
customResolvables={customResolvables}
variablesExposedForPreview={variablesExposedForPreview}
exposeToCodeHinter={exposeToCodeHinter}
setProperty={(property, value) => {
paramUpdated(id, property, { value });
}}
mode={mode}
resetComponent={() => setResetStatus(true)}
childComponents={childComponents}
dataCy={`draggable-widget-${String(component.name).toLowerCase()}`}
></ComponentToRender>
) : (
<></>
)
{!resetComponent ? (
<ComponentToRender
onComponentClick={onComponentClick}
onComponentOptionChanged={onComponentOptionChanged}
currentState={currentState}
onEvent={onEvent}
id={id}
paramUpdated={paramUpdated}
width={width}
changeCanDrag={changeCanDrag}
onComponentOptionsChanged={onComponentOptionsChanged}
height={height}
component={component}
containerProps={containerProps}
darkMode={darkMode}
removeComponent={removeComponent}
canvasWidth={canvasWidth}
properties={validatedProperties}
exposedVariables={exposedVariables}
styles={{ ...validatedStyles, boxShadow: validatedGeneralStyles?.boxShadow }}
setExposedVariable={(variable, value) => onComponentOptionChanged(component, variable, value, id)}
setExposedVariables={(variableSet) =>
onComponentOptionsChanged(component, Object.entries(variableSet), id)
}
fireEvent={fireEvent}
validate={validate}
parentId={parentId}
customResolvables={customResolvables}
variablesExposedForPreview={variablesExposedForPreview}
exposeToCodeHinter={exposeToCodeHinter}
setProperty={(property, value) => {
paramUpdated(id, property, { value });
}}
mode={mode}
resetComponent={() => setResetStatus(true)}
childComponents={childComponents}
dataCy={`draggable-widget-${String(component.name).toLowerCase()}`}
></ComponentToRender>
) : (
<div className="component-image-wrapper" style={{ height: '56px', width: '72px' }}>
<div
className="component-image-holder d-flex flex-column justify-content-center"
style={{ height: '100%' }}
data-cy={`widget-list-box-${component.displayName.toLowerCase().replace(/\s+/g, '-')}`}
>
<center>
<div
className="widget-svg-container"
style={{
width: '32px',
height: '32px',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
}}
>
<WidgetIcon
name={component.name.toLowerCase()}
width="32"
fill={darkMode ? '#3A3F42' : '#D7DBDF'}
/>
</div>
</center>
</div>
<div className="component-title">{t(`widget.${component.name}.displayName`, component.displayName)}</div>
</div>
<></>
)}
</div>
</OverlayTrigger>

View file

@ -35,7 +35,7 @@ import cx from 'classnames';
import { Alert } from '@/_ui/Alert/Alert';
import { useCurrentState } from '@/_stores/currentStateStore';
import ClientServerSwitch from './Elements/ClientServerSwitch';
import { validateProperty } from '../component-properties-validation';
const HIDDEN_CODE_HINTER_LABELS = ['Table data', 'Column data'];
const AllElements = {
@ -120,9 +120,49 @@ export function CodeHinter({
const { variablesExposedForPreview } = useContext(EditorContext);
const prevCountRef = useRef(false);
function getPropertyDefinition(paramName, component) {
if (component?.properties?.hasOwnProperty(`${paramName}`)) {
return component.properties?.[paramName];
} else if (component?.styles?.hasOwnProperty(`${paramName}`)) {
return component?.styles?.[paramName];
} else if (component?.general?.hasOwnProperty(`${paramName}`)) {
return component?.general?.[paramName];
} else if (component?.generalStyles?.hasOwnProperty(`${paramName}`)) {
return component?.generalStyles?.[paramName];
} else {
return {};
}
}
const checkTypeErrorInRunTime = (preview) => {
const propertyDefinition = getPropertyDefinition(paramName, component?.component);
const resolvedProperty = Object.keys(component?.component?.definition || {}).reduce((accumulator, currentKey) => {
if (
component?.component?.definition?.[currentKey]?.hasOwnProperty(paramName) ||
(paramName === 'tooltip' &&
currentKey === 'general' &&
!component?.component?.definition?.[currentKey]?.hasOwnProperty(paramName))
//added second condition because initilly general is empty object and hence it was not going inside if statement and thus codehinter was always receiving undefined for initial render and thus showing error message in the preview
) {
accumulator[`${paramName}`] = resolveReferences(preview, currentState);
}
return accumulator;
}, {});
const [_valid, errorMessages] = validateProperty(resolvedProperty, propertyDefinition, paramName);
return [_valid, errorMessages];
};
const getPreviewAndErrorFromValue = (value) => {
const customResolvables = getCustomResolvables();
const [preview, error] = resolveReferences(value, realState, null, customResolvables, true, true);
return [preview, error];
};
useEffect(() => {
setCurrentValue(initialValue);
const [preview, error] = getPreviewAndErrorFromValue(initialValue);
const [_valid] = checkTypeErrorInRunTime(preview);
if (!_valid || error) setResolvingError(true);
return () => {
setPrevCurrentValue(null);
setResolvedValue(null);
@ -160,25 +200,41 @@ export function CodeHinter({
}, [wrapperRef, isFocused, isPreviewFocused, currentValue, prevCountRef, isOpen]);
useEffect(() => {
let globalPreviewCopy = null;
let globalErrorCopy = null;
if (enablePreview && isFocused && JSON.stringify(currentValue) !== JSON.stringify(prevCurrentValue)) {
const customResolvables = getCustomResolvables();
const [preview, error] = resolveReferences(currentValue, realState, null, customResolvables, true, true);
setPrevCurrentValue(currentValue);
const [preview, error] = getPreviewAndErrorFromValue(currentValue);
// checking type error if any in run time
const [_valid, errorMessages] = checkTypeErrorInRunTime(preview);
if (error) {
setResolvingError(error);
setPrevCurrentValue(currentValue);
if (error || !_valid || typeof preview === 'function') {
globalPreviewCopy = null;
globalErrorCopy = error || errorMessages?.[errorMessages?.length - 1];
setResolvingError(error || errorMessages?.[errorMessages?.length - 1]);
setResolvedValue(null);
} else {
globalPreviewCopy = preview;
globalErrorCopy = null;
setResolvingError(null);
setResolvedValue(preview);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
return () => {
if (enablePreview && isFocused && JSON.stringify(currentValue) !== JSON.stringify(prevCurrentValue)) {
setPrevCurrentValue(null);
setResolvedValue(globalPreviewCopy);
setResolvingError(globalErrorCopy);
}
};
}, [JSON.stringify({ currentValue, realState, isFocused })]);
// eslint-disable-next-line react-hooks/exhaustive-deps
// }, [JSON.stringify({ currentValue, realState, isFocused })]);
function valueChanged(editor, onChange, ignoreBraces) {
if (editor.getValue()?.trim() !== currentValue) {
handleChange(editor, onChange, ignoreBraces, realState, componentName);
handleChange(editor, onChange, ignoreBraces, realState, componentName, getCustomResolvables());
setCurrentValue(editor.getValue()?.trim());
}
}
@ -222,14 +278,11 @@ export function CodeHinter({
const getPreview = () => {
if (!enablePreview) return;
// const customResolvables = getCustomResolvables();
// const [preview, error] = resolveReferences(currentValue, realState, null, customResolvables, true, true);
const themeCls = darkMode ? 'bg-dark py-1' : 'bg-light py-1';
const preview = resolvedValue;
const error = resolvingError;
if (error) {
if (resolvingError !== null && resolvedValue === null && error) {
const err = String(error);
const errorMessage = err.includes('.run()')
? `${err} in ${componentName ? componentName.split('::')[0] + "'s" : 'fx'} field`
@ -256,7 +309,6 @@ export function CodeHinter({
previewType = typeof previewContent;
}
const content = getPreviewContent(previewContent, previewType);
return (
<animated.div
className={isOpen ? themeCls : null}
@ -397,7 +449,9 @@ export function CodeHinter({
<div className={`${verticalLine && 'code-hinter-vertical-line'}`}></div>
<div className="code-hinter-wrapper position-relative" style={{ width: '100%' }}>
<div
className={`${defaultClassName} ${className || 'codehinter-default-input'}`}
className={`${defaultClassName} ${className || 'codehinter-default-input'} ${
resolvingError && 'border-danger'
}`}
key={componentName}
style={{
height: height || 'auto',
@ -437,10 +491,10 @@ export function CodeHinter({
height={'100%'}
onFocus={() => setFocused(true)}
onBlur={(editor, e) => {
e.stopPropagation();
const value = editor.getValue()?.trimEnd();
e?.stopPropagation();
const value = editor?.getValue()?.trimEnd();
onChange(value);
if (!isPreviewFocused.current) {
if (!isPreviewFocused?.current) {
setFocused(false);
}
}}
@ -460,6 +514,7 @@ export function CodeHinter({
);
}
// eslint-disable-next-line no-unused-vars
function CodeHinterInputField() {
return <></>;
}

View file

@ -68,7 +68,6 @@ function getResult(suggestionList, query) {
export function getSuggestionKeys(refState, refSource) {
const state = _.cloneDeep(refState);
const queries = state['queries'];
const actions = [
'runQuery',
'setVariable',
@ -143,6 +142,16 @@ export function getSuggestionKeys(refState, refSource) {
return suggestionList;
}
export function attachCustomResolvables(resolvables) {
const suggestionList = [];
for (const key in resolvables) {
for (const innerKey in resolvables[key]) {
suggestionList.push(`${key}.${innerKey}`);
}
}
return suggestionList;
}
export function generateHints(word, suggestions, isEnvironmentVariable = false, fromRunJs) {
if (word === '') {
return suggestions;
@ -239,8 +248,17 @@ export function canShowHint(editor, ignoreBraces = false) {
return value.slice(ch, ch + 2) === '}}' || value.slice(ch, ch + 2) === '%%';
}
export function handleChange(editor, onChange, ignoreBraces = false, currentState, editorSource = undefined) {
export function handleChange(
editor,
onChange,
ignoreBraces = false,
currentState,
editorSource = undefined,
resolvables = {}
) {
const suggestions = getSuggestionKeys(currentState, editorSource);
const resolvedSuggstions = attachCustomResolvables(resolvables); //attach custom resolved values to suggetsion list
suggestions.push(...resolvedSuggstions);
let state = editor.state.matchHighlighter;
editor.addOverlay((state.overlay = makeOverlay(state.options.style)));

View file

@ -3,7 +3,7 @@ import cx from 'classnames';
const tinycolor = require('tinycolor2');
export const Button = function Button(props) {
const { height, properties, styles, fireEvent, id, dataCy, setExposedVariable } = props;
const { height, properties, styles, fireEvent, id, dataCy, setExposedVariable, setExposedVariables } = props;
const { backgroundColor, textColor, borderRadius, loaderColor, disabledState, borderColor, boxShadow } = styles;
const [label, setLabel] = useState(properties.text);
@ -46,27 +46,28 @@ export const Button = function Button(props) {
};
useEffect(() => {
setExposedVariable('click', async function () {
if (!disable) {
fireEvent('onClick');
}
});
setExposedVariable('setText', async function (text) {
setLabel(text);
setExposedVariable('buttonText', text);
});
const exposedVariables = {
click: async function () {
if (!disable) {
fireEvent('onClick');
}
},
setText: async function (text) {
setLabel(text);
setExposedVariable('buttonText', text);
},
disable: async function (value) {
setDisable(value);
},
visibility: async function (value) {
setVisibility(value);
},
loading: async function (value) {
setLoading(value);
},
};
setExposedVariable('disable', async function (value) {
setDisable(value);
});
setExposedVariable('visibility', async function (value) {
setVisibility(value);
});
setExposedVariable('loading', async function (value) {
setLoading(value);
});
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disable, setLabel, setDisable, setVisibility, setLoading]);

View file

@ -6,6 +6,7 @@ export const Checkbox = function Checkbox({
styles,
fireEvent,
setExposedVariable,
setExposedVariables,
darkMode,
dataCy,
}) {
@ -36,11 +37,14 @@ export const Checkbox = function Checkbox({
}
setChecked(status);
};
const exposedVariables = {
value: defaultValueFromProperties,
setChecked: setCheckedAndNotify,
};
setExposedVariable('value', defaultValueFromProperties);
setDefaultvalue(defaultValueFromProperties);
setChecked(defaultValueFromProperties);
setExposedVariable('setChecked', setCheckedAndNotify);
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultValueFromProperties, setChecked]);

View file

@ -6,6 +6,7 @@ export const ColorPicker = function ({
properties,
styles,
setExposedVariable,
setExposedVariables,
darkMode,
height,
fireEvent,
@ -46,19 +47,28 @@ export const ColorPicker = function ({
};
useEffect(() => {
let exposedVariables = {};
setExposedVariable('setColor', async function (colorCode) {
if (/^#(([\dA-Fa-f]{3}){1,2}|([\dA-Fa-f]{4}){1,2})$/.test(colorCode)) {
if (colorCode !== color) {
setColor(colorCode);
setExposedVariable('selectedColorHex', `${colorCode}`);
setExposedVariable('selectedColorRGB', hexToRgb(colorCode));
setExposedVariable('selectedColorRGBA', hexToRgba(colorCode));
exposedVariables = {
selectedColorHex: colorCode,
selectedColorRGB: hexToRgb(colorCode),
selectedColorRGBA: hexToRgba(colorCode),
};
setExposedVariables(exposedVariables);
fireEvent('onChange');
}
} else {
setExposedVariable('selectedColorHex', 'undefined');
setExposedVariable('selectedColorRGB', 'undefined');
setExposedVariable('selectedColorRGBA', 'undefined');
exposedVariables = {
selectedColorHex: undefined,
selectedColorRGB: undefined,
selectedColorRGBA: undefined,
};
setExposedVariables(exposedVariables);
fireEvent('onChange');
setColor('Invalid Color');
}
@ -67,30 +77,43 @@ export const ColorPicker = function ({
}, [setColor]);
useEffect(() => {
let exposedVariables = {};
if (/^#(([\dA-Fa-f]{3}){1,2}|([\dA-Fa-f]{4}){1,2})$/.test(defaultColor)) {
if (defaultColor !== color) {
setExposedVariable('selectedColorHex', `${defaultColor}`);
setExposedVariable('selectedColorRGB', hexToRgb(defaultColor));
setExposedVariable('selectedColorRGBA', hexToRgba(defaultColor));
exposedVariables = {
selectedColorHex: defaultColor,
selectedColorRGB: hexToRgb(defaultColor),
selectedColorRGBA: hexToRgba(defaultColor),
};
setExposedVariables(exposedVariables);
setColor(defaultColor);
}
} else {
setExposedVariable('selectedColorHex', 'undefined');
setExposedVariable('selectedColorRGB', 'undefined');
setExposedVariable('selectedColorRGBA', 'undefined');
exposedVariables = {
selectedColorHex: undefined,
selectedColorRGB: undefined,
selectedColorRGBA: undefined,
};
setExposedVariables(exposedVariables);
setColor(`Invalid Color`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultColor]);
const handleColorChange = (colorCode) => {
let exposedVariables = {};
const { r, g, b, a } = colorCode.rgb;
const { hex: hexColor } = colorCode;
if (hexColor !== color) {
setColor(hexColor);
setExposedVariable('selectedColorHex', `${hexColor}`);
setExposedVariable('selectedColorRGB', `rgb(${r},${g},${b})`);
setExposedVariable('selectedColorRGBA', `rgb(${r},${g},${b},${a})`);
exposedVariables = {
selectedColorHex: hexColor,
selectedColorRGB: `rgb(${r},${g},${b})`,
selectedColorRGBA: `rgb(${r},${g},${b},${a})`,
};
setExposedVariables(exposedVariables);
fireEvent('onChange');
}
};

View file

@ -19,6 +19,7 @@ export const Form = function Form(props) {
removeComponent,
styles,
setExposedVariable,
setExposedVariables,
darkMode,
currentState,
fireEvent,
@ -57,16 +58,19 @@ export const Form = function Form(props) {
const mounted = useMounted();
useEffect(() => {
setExposedVariable('resetForm', async function () {
resetComponent();
});
setExposedVariable('submitForm', async function () {
if (isValid) {
onEvent('onSubmit', formEvents).then(() => resetComponent());
} else {
fireEvent('onInvalid');
}
});
const exposedVariables = {
resetForm: async function () {
resetComponent();
},
submitForm: async function () {
if (isValid) {
onEvent('onSubmit', formEvents).then(() => resetComponent());
} else {
fireEvent('onInvalid');
}
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isValid]);
@ -115,9 +119,13 @@ export const Form = function Form(props) {
let childValidation = true;
if (childComponents === null) {
setExposedVariable('data', formattedChildData);
!advanced && setExposedVariable('children', formattedChildData);
setExposedVariable('isValid', childValidation);
const exposedVariables = {
data: formattedChildData,
isValid: childValidation,
...(!advanced && { children: formattedChildData }),
};
setExposedVariables(exposedVariables);
return setValidation(childValidation);
}
@ -127,7 +135,7 @@ export const Form = function Form(props) {
} else {
Object.keys(childComponents).forEach((childId) => {
if (childrenData[childId]?.name) {
formattedChildData[childrenData[childId].name] = omit(childrenData[childId], 'name');
formattedChildData[childrenData[childId].name] = { ...omit(childrenData[childId], 'name'), id: childId };
childValidation = childValidation && (childrenData[childId]?.isValid ?? true);
}
});
@ -137,11 +145,13 @@ export const Form = function Form(props) {
Object.entries(formattedChildData).map(([key, { formKey, ...rest }]) => [key, rest]) // removing formkey from final exposed data
);
const formattedChildDataClone = _.cloneDeep(formattedChildData);
!advanced && setExposedVariable('children', formattedChildDataClone);
setExposedVariable('data', removeFunctionObjects(formattedChildData));
setExposedVariable('isValid', childValidation);
const exposedVariables = {
...(!advanced && { children: formattedChildDataClone }),
data: removeFunctionObjects(formattedChildData),
isValid: childValidation,
};
setValidation(childValidation);
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [childrenData, childComponents, advanced, JSON.stringify(JSONSchema)]);
@ -187,6 +197,7 @@ export const Form = function Form(props) {
fireSubmissionEvent();
}
};
//for custom json
function onComponentOptionChangedForSubcontainer(component, optionName, value, componentId = '') {
if (typeof value === 'function' && _.findKey({}, optionName)) {
return Promise.resolve();

View file

@ -10,6 +10,7 @@ export const Icon = ({
width,
height,
setExposedVariable,
setExposedVariables,
darkMode,
dataCy,
component,
@ -31,12 +32,15 @@ export const Icon = ({
}, [visibility]);
useEffect(() => {
setExposedVariable('setVisibility', async function (visibility) {
setIconVisibility(visibility);
});
setExposedVariable('click', async function () {
fireEvent('onClick');
});
const exposedVariables = {
setVisibility: async function (visibility) {
setIconVisibility(visibility);
},
click: async function () {
fireEvent('onClick');
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIconVisibility]);

View file

@ -15,6 +15,7 @@ export const Listview = function Listview({
styles,
fireEvent,
setExposedVariable,
setExposedVariables,
darkMode,
dataCy,
}) {
@ -52,15 +53,21 @@ export const Listview = function Listview({
function onRecordClicked(index) {
setSelectedRowIndex(index);
setExposedVariable('selectedRecordId', index);
setExposedVariable('selectedRecord', childrenData[index]);
const exposedVariables = {
selectedRecordId: index,
selectedRecord: childrenData[index],
};
setExposedVariables(exposedVariables);
fireEvent('onRecordClicked');
// eslint-disable-next-line react-hooks/exhaustive-deps
}
function onRowClicked(index) {
setSelectedRowIndex(index);
setExposedVariable('selectedRowId', index);
setExposedVariable('selectedRow', childrenData[index]);
const exposedVariables = {
selectedRowId: index,
selectedRow: childrenData[index],
};
setExposedVariables(exposedVariables);
fireEvent('onRowClicked');
// eslint-disable-next-line react-hooks/exhaustive-deps
}
@ -73,11 +80,18 @@ export const Listview = function Listview({
useEffect(() => {
const childrenDataClone = _.cloneDeep(childrenData);
setExposedVariable('data', removeFunctionObjects(childrenDataClone));
setExposedVariable('children', childrenData);
const exposedVariables = {
data: removeFunctionObjects(childrenDataClone),
children: childrenData,
};
setExposedVariables(exposedVariables);
if (selectedRowIndex != undefined) {
setExposedVariable('selectedRowId', selectedRowIndex);
setExposedVariable('selectedRow', childrenData[selectedRowIndex]);
const exposedVariables = {
selectedRowId: selectedRowIndex,
selectedRow: childrenData[selectedRowIndex],
};
setExposedVariables(exposedVariables);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [childrenData]);
@ -154,13 +168,17 @@ export const Listview = function Listview({
style={{ border: '1px solid', borderColor, margin: '1px', borderTop: 0 }}
>
<div style={{ backgroundColor }}>
<Pagination
darkMode={darkMode}
currentPage={currentPage}
pageChanged={pageChanged}
count={data?.length}
itemsPerPage={rowPerPageValue}
/>
{data?.length > 0 ? (
<Pagination
darkMode={darkMode}
currentPage={currentPage}
pageChanged={pageChanged}
count={data?.length}
itemsPerPage={rowPerPageValue}
/>
) : (
<div style={{ height: '61px' }}></div>
)}
</div>
</div>
)}

View file

@ -14,6 +14,7 @@ export const Modal = function Modal({
styles,
exposedVariables,
setExposedVariable,
setExposedVariables,
fireEvent,
dataCy,
height,
@ -46,14 +47,17 @@ export const Modal = function Modal({
const size = properties.size ?? 'lg';
useEffect(() => {
setExposedVariable('open', async function () {
setExposedVariable('show', true);
setShowModal(true);
});
setExposedVariable('close', async function () {
setShowModal(false);
setExposedVariable('show', false);
});
const exposedVariables = {
open: async function () {
setExposedVariable('show', true);
setShowModal(true);
},
close: async function () {
setShowModal(false);
setExposedVariable('show', false);
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setShowModal]);

View file

@ -17,6 +17,7 @@ export const Multiselect = function Multiselect({
styles,
exposedVariables,
setExposedVariable,
setExposedVariables,
onComponentClick,
darkMode,
fireEvent,
@ -75,56 +76,56 @@ export const Multiselect = function Multiselect({
};
useEffect(() => {
setExposedVariable('selectOption', async function (value) {
if (
selectOptions.some((option) => option.value === value) &&
!selected.some((option) => option.value === value)
) {
const newSelected = [
...selected,
...selectOptions.filter(
(option) =>
option.value === value && !selected.map((selectedOption) => selectedOption.value).includes(value)
),
];
setSelected(newSelected);
setExposedVariable(
'values',
newSelected.map((item) => item.value)
);
fireEvent('onSelect');
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected, setSelected]);
const exposedVariables = {
selectOption: async function (value) {
if (
selectOptions.some((option) => option.value === value) &&
!selected.some((option) => option.value === value)
) {
const newSelected = [
...selected,
...selectOptions.filter(
(option) =>
option.value === value && !selected.map((selectedOption) => selectedOption.value).includes(value)
),
];
setSelected(newSelected);
setExposedVariable(
'values',
newSelected.map((item) => item.value)
);
fireEvent('onSelect');
}
},
deselectOption: async function (value) {
if (
selectOptions.some((option) => option.value === value) &&
selected.some((option) => option.value === value)
) {
const newSelected = [
...selected.filter(function (item) {
return item.value !== value;
}),
];
setSelected(newSelected);
setExposedVariable(
'values',
newSelected.map((item) => item.value)
);
fireEvent('onSelect');
}
},
clearSelections: async function () {
if (selected.length >= 1) {
setSelected([]);
setExposedVariable('values', []);
fireEvent('onSelect');
}
},
};
useEffect(() => {
setExposedVariable('deselectOption', async function (value) {
if (selectOptions.some((option) => option.value === value) && selected.some((option) => option.value === value)) {
const newSelected = [
...selected.filter(function (item) {
return item.value !== value;
}),
];
setSelected(newSelected);
setExposedVariable(
'values',
newSelected.map((item) => item.value)
);
fireEvent('onSelect');
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected, setSelected]);
setExposedVariables(exposedVariables);
useEffect(() => {
setExposedVariable('clearSelections', async function () {
if (selected.length >= 1) {
setSelected([]);
setExposedVariable('values', []);
fireEvent('onSelect');
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected, setSelected]);

View file

@ -46,7 +46,7 @@ export const NumberInput = function NumberInput({
const handleBlur = (e) => {
if (!isNaN(parseFloat(properties.minValue)) && parseFloat(e.target.value) < parseFloat(properties.minValue)) {
setValue(Number(parseFloat(properties.minValue)));
} else setValue(Number(parseFloat(e.target.value).toFixed(properties.decimalPlaces)));
} else setValue(Number(parseFloat(e.target.value ? e.target.value : 0).toFixed(properties.decimalPlaces)));
};
useEffect(() => {

View file

@ -8,6 +8,7 @@ export const RadioButton = function RadioButton({
styles,
fireEvent,
setExposedVariable,
setExposedVariables,
darkMode,
dataCy,
}) {
@ -36,10 +37,13 @@ export const RadioButton = function RadioButton({
}
useEffect(() => {
setExposedVariable('value', value);
setExposedVariable('selectOption', async function (option) {
onSelect(option);
});
const exposedVariables = {
value: value,
selectOption: async function (option) {
onSelect(option);
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, setValue]);

View file

@ -1,9 +1,10 @@
import React from 'react';
import React, { useMemo } from 'react';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { debounce } from 'lodash';
// Table Search
export const GlobalFilter = ({
globalFilter,
useAsyncDebounce,
setGlobalFilter,
onComponentOptionChanged,
component,
@ -13,13 +14,14 @@ export const GlobalFilter = ({
tableEvents,
}) => {
const [value, setValue] = React.useState(globalFilter);
const onChange = useAsyncDebounce((filterValue) => {
setValue(filterValue);
const onChange = (filterValue) => {
setGlobalFilter(filterValue || undefined);
onComponentOptionChanged(component, 'searchText', filterValue).then(() => {
onEvent('onSearch', tableEvents, { component, data: {} });
});
}, 500);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedChange = useMemo(() => debounce(onChange, 500), []);
return (
<div
@ -31,8 +33,11 @@ export const GlobalFilter = ({
<input
type="text"
className={`align-self-center bg-transparent tj-text tj-text-xsm mx-lg-1`}
defaultValue={value || ''}
onChange={(e) => onChange(e.target.value)}
value={value || ''}
onChange={(e) => {
setValue(e.target.value);
debouncedChange(e.target.value);
}}
placeholder="Search"
data-cy="search-input-field"
style={{
@ -45,6 +50,7 @@ export const GlobalFilter = ({
onClick={() => {
setGlobalFilter(undefined);
setValue('');
onComponentOptionChanged(component, 'searchText', '');
}}
>
<SolidIcon name="remove" width="16" height="16px" fill={darkMode ? '#3E63DD' : '#3E63DD'} />

View file

@ -148,11 +148,18 @@ export function Table({
top: 'auto',
borderRadius: '4px',
...(isDragging && {
marginLeft: '-280px', // hack changing marginLeft to -280px to bring the draggable header to the correct position at the start of drag
// marginLeft: '-280px', // hack changing marginLeft to -280px to bring the draggable header to the correct position at the start of drag
display: 'flex',
alignItems: 'center',
paddingLeft: '10px',
height: '30px',
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0',
zIndex: '9999',
width: '60px',
}),
...(!isDragging && { transform: 'translate(0,0)', width: '100%' }),
...(isDropAnimating && { transitionDuration: '0.001s' }),
@ -707,9 +714,9 @@ export function Table({
useEffect(() => {
setExposedVariable('discardNewlyAddedRows', async function () {
if (
tableDetails.addNewRowsDetails.addingNewRows &&
(Object.keys(tableDetails.addNewRowsDetails.newRowsChangeSet || {}).length > 0 ||
Object.keys(tableDetails.addNewRowsDetails.newRowsDataUpdates || {}).length > 0)
!_.isEmpty(exposedVariables.newRows) ||
!_.isEmpty(tableDetails.addNewRowsDetails.newRowsChangeSet) ||
!_.isEmpty(tableDetails.addNewRowsDetails.newRowsChangeSet)
) {
setExposedVariables({
newRows: [],
@ -735,8 +742,9 @@ export function Table({
mergeToTableDetails({ selectedRowsDetails });
}
if (
(!showBulkSelector && !highlightSelectedRow) ||
(showBulkSelector && !highlightSelectedRow && preSelectRow.current)
allowSelection &&
((!showBulkSelector && !highlightSelectedRow) ||
(showBulkSelector && !highlightSelectedRow && preSelectRow.current))
) {
const selectedRow = selectedFlatRows?.[0]?.original ?? {};
const selectedRowId = selectedFlatRows?.[0]?.id ?? null;
@ -781,12 +789,11 @@ export function Table({
['currentData', data],
['selectedRow', []],
['selectedRowId', null],
]).then(() => {
if (tableDetails.selectedRowId || !_.isEmpty(tableDetails.selectedRowDetails)) {
toggleAllRowsSelected(false);
mergeToTableDetails({ selectedRow: {}, selectedRowId: null, selectedRowDetails: [] });
}
});
]);
if (tableDetails.selectedRowId || !_.isEmpty(tableDetails.selectedRowDetails)) {
toggleAllRowsSelected(false);
mergeToTableDetails({ selectedRow: {}, selectedRowId: null, selectedRowDetails: [] });
}
}
}, [tableData.length, _.toString(page), pageIndex, _.toString(data)]);
@ -1057,7 +1064,6 @@ export function Table({
{displaySearchBox && !loadingState && (
<GlobalFilter
globalFilter={state.globalFilter}
useAsyncDebounce={useAsyncDebounce}
setGlobalFilter={setGlobalFilter}
onComponentOptionChanged={onComponentOptionChanged}
component={component}
@ -1088,7 +1094,7 @@ export function Table({
onDragStart={() => {
currentColOrder.current = allColumns?.map((o) => o.id);
}}
onDragUpdate={(dragUpdateObj) => {
onDragEnd={(dragUpdateObj) => {
const colOrder = [...currentColOrder.current];
const sIndex = dragUpdateObj.source.index;
const dIndex = dragUpdateObj.destination && dragUpdateObj.destination.index;

View file

@ -155,7 +155,7 @@ export default function generateColumnsData({
<div className="h-100 d-flex flex-column justify-content-center">
<input
type="text"
style={{ ...cellStyles, maxWidth: width }}
style={{ ...cellStyles }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (e.target.defaultValue !== e.target.value) {
@ -228,7 +228,7 @@ export default function generateColumnsData({
<div className="h-100 d-flex flex-column justify-content-center">
<input
type="number"
style={{ ...cellStyles, maxWidth: width }}
style={{ ...cellStyles }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (e.target.defaultValue !== e.target.value) {
@ -278,7 +278,6 @@ export default function generateColumnsData({
darkMode ? 'text-light textarea-dark-theme' : 'text-muted'
}`}
readOnly={!isEditable}
style={{ maxWidth: width }}
onBlur={(e) => {
if (isEditable && e.target.defaultValue !== e.target.value) {
handleCellValueChange(cell.row.index, column.key || column.name, e.target.value, cell.row.original);
@ -358,7 +357,11 @@ export default function generateColumnsData({
case 'badge':
case 'badges': {
return (
<div className="h-100 d-flex align-items-center">
<div
className={`h-100 d-flex align-items-center justify-content-${determineJustifyContentValue(
horizontalAlignment
)}`}
>
<CustomSelect
options={columnOptions.selectOptions}
value={cellValue}

View file

@ -12,6 +12,7 @@ export const Tabs = function Tabs({
currentState,
removeComponent,
setExposedVariable,
setExposedVariables,
fireEvent,
styles,
darkMode,
@ -92,14 +93,17 @@ export const Tabs = function Tabs({
}
useEffect(() => {
setExposedVariable('setTab', async function (id) {
if (id) {
setCurrentTab(id);
setExposedVariable('currentTab', id);
fireEvent('onTabSwitch');
}
});
setExposedVariable('currentTab', currentTab);
const exposedVariables = {
setTab: async function (id) {
if (id) {
setCurrentTab(id);
setExposedVariable('currentTab', id);
fireEvent('onTabSwitch');
}
},
currentTab: currentTab,
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setCurrentTab, currentTab]);

View file

@ -1,7 +1,15 @@
import React, { useState, useEffect } from 'react';
import DOMPurify from 'dompurify';
export const Text = function Text({ height, properties, styles, darkMode, setExposedVariable, dataCy }) {
export const Text = function Text({
height,
properties,
styles,
darkMode,
setExposedVariable,
setExposedVariables,
dataCy,
}) {
let {
textSize,
textColor,
@ -29,20 +37,22 @@ export const Text = function Text({ height, properties, styles, darkMode, setExp
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [styles.visibility]);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
const text = computeText();
setText(text);
setExposedVariable('text', text);
setExposedVariable('setText', async function (text) {
setText(text);
setExposedVariable('text', text);
});
setExposedVariable('visibility', async function (value) {
setVisibility(value);
});
const exposedVariables = {
text: text,
setText: async function (text) {
setText(text);
setExposedVariable('text', text);
},
visibility: async function (value) {
setVisibility(value);
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.text, setText, setVisibility]);
@ -75,7 +85,7 @@ export const Text = function Text({ height, properties, styles, darkMode, setExp
{!loadingState && (
<div
style={{ width: '100%', fontSize: textSize }}
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(text) }}
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(text || '0') }}
/>
)}
{loadingState === true && (

View file

@ -1,20 +1,30 @@
import React, { useState, useEffect } from 'react';
export const TextArea = function TextArea({ height, properties, styles, setExposedVariable, dataCy }) {
export const TextArea = function TextArea({
height,
properties,
styles,
setExposedVariable,
setExposedVariables,
dataCy,
}) {
const [value, setValue] = useState(properties.value);
useEffect(() => {
setValue(properties.value);
setExposedVariable('value', properties.value);
const exposedVariables = {
value: properties.value,
setText: async function (text) {
setValue(text);
setExposedVariable('value', text);
},
clear: async function (text) {
setValue('');
setExposedVariable('value', '');
},
};
setExposedVariables(exposedVariables);
setExposedVariable('setText', async function (text) {
setValue(text);
setExposedVariable('value', text);
});
setExposedVariable('clear', async function () {
setValue('');
setExposedVariable('value', '');
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.value, setValue]);

View file

@ -6,6 +6,7 @@ export const TextInput = function TextInput({
properties,
styles,
setExposedVariable,
setExposedVariables,
fireEvent,
component,
darkMode,
@ -50,30 +51,37 @@ export const TextInput = function TextInput({
}, [properties.value]);
useEffect(() => {
setExposedVariable('setFocus', async function () {
textInputRef.current.focus();
});
setExposedVariable('setBlur', async function () {
textInputRef.current.blur();
});
setExposedVariable('disable', async function (value) {
setDisable(value);
});
setExposedVariable('visibility', async function (value) {
setVisibility(value);
});
const exposedVariables = {
setFocus: async function () {
textInputRef.current.focus();
},
setBlur: async function () {
textInputRef.current.blur();
},
disable: async function (value) {
setDisable(value);
},
visibility: async function (value) {
setVisibility(value);
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setExposedVariable('setText', async function (text) {
setValue(text);
setExposedVariable('value', text).then(fireEvent('onChange'));
});
setExposedVariable('clear', async function () {
setValue('');
setExposedVariable('value', '').then(fireEvent('onChange'));
});
const exposedVariables = {
setText: async function (text) {
setValue(text);
setExposedVariable('value', text).then(fireEvent('onChange'));
},
clear: async function () {
setValue('');
setExposedVariable('value', '').then(fireEvent('onChange'));
},
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setValue]);

View file

@ -5,7 +5,16 @@ import CheckboxTree from 'react-checkbox-tree';
import 'react-checkbox-tree/lib/react-checkbox-tree.css';
import { isExpectedDataType } from '@/_helpers/utils.js';
export const TreeSelect = ({ height, properties, styles, setExposedVariable, fireEvent, darkMode, dataCy }) => {
export const TreeSelect = ({
height,
properties,
styles,
setExposedVariable,
setExposedVariables,
fireEvent,
darkMode,
dataCy,
}) => {
const { label } = properties;
const { visibility, disabledState, checkboxColor, boxShadow } = styles;
const textColor = darkMode && styles.textColor === '#000' ? '#fff' : styles.textColor;
@ -32,13 +41,18 @@ export const TreeSelect = ({ height, properties, styles, setExposedVariable, fir
};
updateCheckedArr(data, checkedData);
setChecked(checkedArr);
setExposedVariable('checked', checkedArr);
checkedArr.forEach((item) => {
checkedPathArray.push(pathObj[item]);
checkedPathString.push(pathObj[item].join('-'));
});
setExposedVariable('checkedPathArray', checkedPathArray);
setExposedVariable('checkedPathStrings', checkedPathString);
const exposedVariables = {
checkedPathArray: checkedPathArray,
checkedPathStrings: checkedPathString,
checked: checkedArr,
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(checkedData), JSON.stringify(data)]);
@ -70,9 +84,14 @@ export const TreeSelect = ({ height, properties, styles, setExposedVariable, fir
checkedPathArray.push(pathObj[item]);
checkedPathString.push(pathObj[item].join('-'));
});
setExposedVariable('checkedPathArray', checkedPathArray);
setExposedVariable('checkedPathStrings', checkedPathString);
setExposedVariable('checked', checked);
const exposedVariables = {
checkedPathArray: checkedPathArray,
checkedPathStrings: checkedPathString,
checked: checked,
};
setExposedVariable(exposedVariables);
updatedNode.checked ? fireEvent('onCheck') : fireEvent('onUnCheck');
fireEvent('onChange');
setChecked(checked);

View file

@ -51,11 +51,10 @@ export const Container = ({
// redundant save on app definition load
const firstUpdate = useRef(true);
const { showComments, currentLayout, selectedComponents } = useEditorStore(
const { showComments, currentLayout } = useEditorStore(
(state) => ({
showComments: state?.showComments,
currentLayout: state?.currentLayout,
selectedComponents: state?.selectedComponents,
}),
shallow
);
@ -280,7 +279,6 @@ export const Container = ({
const componentMeta = _.cloneDeep(
componentTypes.find((component) => component.component === item.component.component)
);
console.log('adding new component');
const newComponent = addNewWidgetToTheEditor(
componentMeta,
monitor,
@ -336,7 +334,7 @@ export const Container = ({
let newBoxes = { ...boxes };
for (const selectedComponent of selectedComponents) {
for (const selectedComponent of useEditorStore.getState().selectedComponents) {
newBoxes = produce(newBoxes, (draft) => {
if (draft[selectedComponent.id]) {
const topOffset = draft[selectedComponent.id].layouts[currentLayout].top;
@ -351,7 +349,7 @@ export const Container = ({
setBoxes(newBoxes);
updateCanvasHeight(newBoxes);
},
[isVersionReleased, enableReleasedVersionPopupState, boxes, setBoxes, selectedComponents, updateCanvasHeight]
[isVersionReleased, enableReleasedVersionPopupState, boxes, setBoxes, updateCanvasHeight]
);
const onResizeStop = useCallback(
@ -582,7 +580,6 @@ export const Container = ({
removeComponent,
currentLayout,
deviceWindowWidth,
selectedComponents,
darkMode,
sideBarDebugger,
currentPageId,
@ -604,7 +601,6 @@ export const Container = ({
removeComponent,
currentLayout,
deviceWindowWidth,
selectedComponents,
darkMode,
sideBarDebugger,
currentPageId,

View file

@ -12,6 +12,8 @@ import ErrorBoundary from './ErrorBoundary';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useEditorStore } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
import WidgetBox from './WidgetBox';
import * as Sentry from '@sentry/react';
const NO_OF_GRIDS = 43;
@ -69,8 +71,6 @@ export const DraggableBox = React.memo(
className,
mode,
title,
_left,
_top,
parent,
allComponents,
component,
@ -121,7 +121,6 @@ export const DraggableBox = React.memo(
shallow
);
const currentState = useCurrentState();
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: ItemTypes.BOX,
@ -291,7 +290,13 @@ export const DraggableBox = React.memo(
configWidgetHandlerForModalComponent={configWidgetHandlerForModalComponent}
/>
)}
<ErrorBoundary showFallback={mode === 'edit'}>
{/* Adding a sentry's error boundary to differentiate between our generic error boundary and one from editor's component */}
<Sentry.ErrorBoundary
fallback={<h2>Something went wrong.</h2>}
beforeCapture={(scope) => {
scope.setTag('errorType', 'component');
}}
>
<Box
component={component}
id={id}
@ -305,7 +310,6 @@ export const DraggableBox = React.memo(
onComponentOptionChanged={onComponentOptionChanged}
onComponentOptionsChanged={onComponentOptionsChanged}
onComponentClick={onComponentClick}
currentState={currentState}
containerProps={containerProps}
darkMode={darkMode}
removeComponent={removeComponent}
@ -317,30 +321,14 @@ export const DraggableBox = React.memo(
sideBarDebugger={sideBarDebugger}
childComponents={childComponents}
/>
</ErrorBoundary>
</Sentry.ErrorBoundary>
</div>
</Rnd>
</div>
) : (
<div ref={drag} role="DraggableBox" className="draggable-box" style={{ height: '100%' }}>
<ErrorBoundary showFallback={mode === 'edit'}>
<Box
component={component}
id={id}
mode={mode}
inCanvas={inCanvas}
onEvent={onEvent}
paramUpdated={paramUpdated}
onComponentOptionChanged={onComponentOptionChanged}
onComponentOptionsChanged={onComponentOptionsChanged}
onComponentClick={onComponentClick}
currentState={currentState}
darkMode={darkMode}
removeComponent={removeComponent}
sideBarDebugger={sideBarDebugger}
customResolvables={customResolvables}
containerProps={containerProps}
/>
<WidgetBox component={component} darkMode={darkMode} />
</ErrorBoundary>
</div>
)}

View file

@ -10,7 +10,7 @@ import {
} from '@/_services';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import _, { cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash';
import _, { cloneDeep, isEqual, isEmpty, debounce, omit, noop } from 'lodash';
import { Container } from './Container';
import { EditorKeyHooks } from './EditorKeyHooks';
import { CustomDragLayer } from './CustomDragLayer';
@ -43,7 +43,6 @@ import { createWebsocketConnection } from '@/_helpers/websocketConnection';
import RealtimeCursors from '@/Editor/RealtimeCursors';
import { initEditorWalkThrough } from '@/_helpers/createWalkThrough';
import { EditorContextWrapper } from './Context/EditorContextWrapper';
import Selecto from 'react-selecto';
import { withTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import Skeleton from 'react-loading-skeleton';
@ -53,19 +52,22 @@ import '@/_styles/editor/react-select-search.scss';
import { withRouter } from '@/_hoc/withRouter';
import { ReleasedVersionError } from './AppVersionsManager/ReleasedVersionError';
import { useDataSourcesStore } from '@/_stores/dataSourcesStore';
import { useDataQueries, useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useAppVersionStore, useAppVersionActions, useAppVersionState } from '@/_stores/appVersionStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import { useCurrentStateStore, useCurrentState, getCurrentState } from '@/_stores/currentStateStore';
import { computeAppDiff, computeComponentPropertyDiff, isParamFromTableColumn, resetAllStores } from '@/_stores/utils';
import { setCookie } from '@/_helpers/cookie';
import { useEditorActions, useEditorState, useEditorStore } from '@/_stores/editorStore';
import { EMPTY_ARRAY, useEditorActions, useEditorStore } from '@/_stores/editorStore';
import { useAppDataActions, useAppInfo, useAppDataStore } from '@/_stores/appDataStore';
import { useMounted } from '@/_hooks/use-mount';
import EditorSelecto from './EditorSelecto';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import useDebouncedArrowKeyPress from '@/_hooks/useDebouncedArrowKeyPress';
import RightSidebarTabManager from './RightSidebarTabManager';
import { shallow } from 'zustand/shallow';
setAutoFreeze(false);
enablePatches();
@ -86,25 +88,26 @@ const EditorComponent = (props) => {
updateAppVersion,
setIsSaving,
createAppVersionEventHandlers,
setAppPreviewLink,
autoUpdateEventStore,
} = useAppDataActions();
const { updateEditorState, updateQueryConfirmationList, setSelectedComponents, setCurrentPageId } =
useEditorActions();
const { setAppVersions } = useAppVersionActions();
const { isVersionReleased, editingVersion, releasedVersionId } = useAppVersionState();
const { isVersionReleased, editingVersionId, releasedVersionId } = useAppVersionStore(
(state) => ({
isVersionReleased: state?.isVersionReleased,
editingVersionId: state?.editingVersion?.id,
releasedVersionId: state?.releasedVersionId,
}),
shallow
);
const {
appDefinition,
selectedComponents,
currentLayout,
canUndo,
canRedo,
isUpdatingEditorStateInProcess,
saveError,
scrollOptions,
currentSidebarTab,
isLoading,
defaultComponentStateComputed,
showComments,
@ -112,10 +115,24 @@ const EditorComponent = (props) => {
queryConfirmationList,
currentPageId,
currentSessionId,
} = useEditorState();
const dataQueries = useDataQueries();
} = useEditorStore(
(state) => ({
appDefinition: state.appDefinition,
currentLayout: state.currentLayout,
canUndo: state.canUndo,
canRedo: state.canRedo,
isLoading: state.isLoading,
defaultComponentStateComputed: state.defaultComponentStateComputed,
showComments: state.showComments,
showLeftSidebar: state.showLeftSidebar,
queryConfirmationList: state.queryConfirmationList,
currentPageId: state.currentPageId,
currentSessionId: state.currentSessionId,
}),
shallow
);
const dataQueries = useDataQueriesStore((state) => state.dataQueries, shallow);
const {
isMaintenanceOn,
appId,
@ -128,15 +145,28 @@ const EditorComponent = (props) => {
appDiffOptions,
events,
areOthersOnSameVersionAndPage,
} = useAppInfo();
} = useAppDataStore(
(state) => ({
isMaintenanceOn: state.isMaintenanceOn,
appId: state.appId,
app: state.app,
appName: state.appName,
slug: state.slug,
currentUser: state.currentUser,
currentVersionId: state.currentVersionId,
appDefinitionDiff: state.appDefinitionDiff,
appDiffOptions: state.appDiffOptions,
events: state.events,
areOthersOnSameVersionAndPage: state.areOthersOnSameVersionAndPage,
}),
shallow
);
const currentState = useCurrentState();
const [zoomLevel, setZoomLevel] = useState(1);
const [isQueryPaneDragging, setIsQueryPaneDragging] = useState(false);
const [isQueryPaneExpanded, setIsQueryPaneExpanded] = useState(false); //!check where this is used
const [selectionInProgress, setSelectionInProgress] = useState(false);
const [hoveredComponent, setHoveredComponent] = useState(null);
const [editorMarginLeft, setEditorMarginLeft] = useState(0);
const [isDragging, setIsDragging] = useState(false);
@ -144,15 +174,12 @@ const EditorComponent = (props) => {
const [showPageDeletionConfirmation, setShowPageDeletionConfirmation] = useState(null);
const [isDeletingPage, setIsDeletingPage] = useState(false);
// const [currentSessionId, setCurrentSessionId] = useState(null);
const [undoStack, setUndoStack] = useState([]);
const [redoStack, setRedoStack] = useState([]);
const [optsStack, setOptsStack] = useState({
undo: [],
redo: [],
});
// refs
const canvasContainerRef = useRef(null);
const dataSourceModalRef = useRef(null);
@ -207,8 +234,6 @@ const EditorComponent = (props) => {
$componentDidMount();
// setCurrentSessionId(() => uuid());
// 6. Unsubscribe from the observable when the component is unmounted
return () => {
document.title = 'Tooljet - Dashboard';
@ -235,7 +260,7 @@ const EditorComponent = (props) => {
computeComponentState(components);
if (isUpdatingEditorStateInProcess) {
if (useEditorStore.getState().isUpdatingEditorStateInProcess) {
autoSave();
}
}
@ -432,7 +457,6 @@ const EditorComponent = (props) => {
initRealtimeSave();
initEventListeners();
updateEditorState({
currentSidebarTab: 2,
selectedComponents: [],
scrollOptions: {
container: canvasContainerRef.current,
@ -489,7 +513,7 @@ const EditorComponent = (props) => {
})
);
} else {
fetchDataSources(editingVersion?.id);
fetchDataSources(editingVersionId);
}
};
@ -506,20 +530,10 @@ const EditorComponent = (props) => {
})
);
} else {
fetchDataQueries(editingVersion?.id);
fetchDataQueries(editingVersionId);
}
};
const switchSidebarTab = (tabIndex) => {
updateEditorState({
currentSidebarTab: tabIndex,
});
};
const handleInspectorView = () => {
switchSidebarTab(2);
};
const onNameChanged = (newName) => {
updateState({ appName: newName });
setWindowTitle(newName);
@ -556,12 +570,6 @@ const EditorComponent = (props) => {
updateEditorState({
selectedComponent: { id, component },
});
switchSidebarTab(1);
};
const handleComponentHover = (id) => {
if (selectionInProgress) return;
setHoveredComponent(id);
};
const sideBarDebugger = {
@ -588,56 +596,20 @@ const EditorComponent = (props) => {
return onEvent(getEditorRef(), eventName, event, options, 'edit');
};
const handleRunQuery = (queryId, queryName) => runQuery(editorRef, queryId, queryName);
const handleRunQuery = (queryId, queryName) => runQuery(getEditorRef(), queryId, queryName);
const dataSourceModalHandler = () => {
dataSourceModalRef.current.dataSourceModalToggleStateHandler();
};
const onAreaSelectionStart = (e) => {
const isMultiSelect = e.inputEvent.shiftKey || selectedComponents.length > 0;
setSelectionInProgress(true);
const prevSelectedComponents = [...selectedComponents];
updateEditorState({
selectedComponents: [...(isMultiSelect ? prevSelectedComponents : [])],
});
};
const onAreaSelection = (e) => {
e.added.forEach((el) => {
el.classList.add('resizer-select');
});
if (selectionInProgress) {
e.removed.forEach((el) => {
el.classList.remove('resizer-select');
});
}
};
const setSelectedComponent = (id, component, multiSelect = false) => {
if (selectedComponents.length === 0 || !multiSelect) {
switchSidebarTab(1);
} else {
switchSidebarTab(2);
}
const isAlreadySelected = selectedComponents.find((component) => component.id === id);
const isAlreadySelected = useEditorStore.getState()?.selectedComponents.find((component) => component.id === id);
if (!isAlreadySelected) {
setSelectedComponents([{ id, component }], multiSelect);
}
};
const onAreaSelectionEnd = (e) => {
setSelectionInProgress(false);
e.selected.forEach((el, index) => {
const id = el.getAttribute('widgetid');
const component = appDefinition?.pages[currentPageId].components[id].component;
const isMultiSelect = e.inputEvent.shiftKey || (!e.isClick && index != 0);
setSelectedComponent(id, component, isMultiSelect);
});
};
const onVersionRelease = (versionId) => {
useAppVersionStore.getState().actions.updateReleasedVersionId(versionId);
@ -657,26 +629,6 @@ const EditorComponent = (props) => {
return canvasBackgroundColor;
};
const onAreaSelectionDragStart = (e) => {
if (e.inputEvent.target.getAttribute('id') !== 'real-canvas') {
selectionDragRef.current = true;
} else {
selectionDragRef.current = false;
}
};
const onAreaSelectionDrag = (e) => {
if (selectionDragRef.current) {
e.stop();
selectionInProgress && setSelectionInProgress(false);
}
};
const onAreaSelectionDragEnd = () => {
selectionDragRef.current = false;
selectionInProgress && setSelectionInProgress(false);
};
const getPagesWithIds = () => {
//! Needs attention
return Object.entries(appDefinition?.pages).map(([id, page]) => ({ ...page, id }));
@ -786,7 +738,7 @@ const EditorComponent = (props) => {
const setAppDefinitionFromVersion = (appData) => {
const version = appData?.editing_version?.id;
if (version?.id !== editingVersion?.id) {
if (version?.id !== editingVersionId) {
if (version?.id === currentVersionId) {
updateEditorState({
canUndo: false,
@ -813,7 +765,6 @@ const EditorComponent = (props) => {
const appDefinitionChanged = async (newDefinition, opts = {}) => {
if (opts?.versionChanged) {
setCurrentPageId(newDefinition.homePageId);
return new Promise((resolve) => {
updateEditorState({
isUpdatingEditorStateInProcess: true,
@ -965,6 +916,7 @@ const EditorComponent = (props) => {
};
const saveEditingVersion = (isUserSwitchedVersion = false) => {
const editingVersion = useAppVersionStore.getState().editingVersion;
if (isVersionReleased && !isUserSwitchedVersion) {
updateEditorState({
isUpdatingEditorStateInProcess: false,
@ -982,14 +934,13 @@ const EditorComponent = (props) => {
toast(toastMessage, {
icon: '🚫',
});
return updateEditorState({
saveError: true,
isUpdatingEditorStateInProcess: false,
});
}
updateAppVersion(appId, editingVersion?.id, currentPageId, updateDiff, isUserSwitchedVersion)
updateAppVersion(appId, editingVersion.id, currentPageId, updateDiff, isUserSwitchedVersion)
.then(() => {
const _editingVersion = {
...editingVersion,
@ -1000,7 +951,7 @@ const EditorComponent = (props) => {
if (config.ENABLE_MULTIPLAYER_EDITING) {
props.ymap?.set('appDef', {
newDefinition: appDefinition,
editingVersionId: editingVersion?.id,
editingVersionId: editingVersion.id,
currentSessionId,
areOthersOnSameVersionAndPage,
opts: appDiffOptions,
@ -1022,7 +973,6 @@ const EditorComponent = (props) => {
events: updatedEvents,
});
}
updateEditorState({
saveError: false,
isUpdatingEditorStateInProcess: false,
@ -1050,7 +1000,6 @@ const EditorComponent = (props) => {
}
});
}
updateEditorState({
saveError: false,
isUpdatingEditorStateInProcess: false,
@ -1124,7 +1073,6 @@ const EditorComponent = (props) => {
updateEditorState({
appDefinition: updatedAppDefinition,
currentSidebarTab: 2,
isUpdatingEditorStateInProcess: true,
});
}
@ -1182,7 +1130,6 @@ const EditorComponent = (props) => {
// Update the component definition in the copy
updatedAppDefinition.pages[currentPageId].components[componentDefinition.id].component =
componentDefinition.component;
updateEditorState({
isUpdatingEditorStateInProcess: true,
});
@ -1194,7 +1141,6 @@ const EditorComponent = (props) => {
}
}
};
const removeComponent = (componentId) => {
if (!isVersionReleased) {
let newDefinition = cloneDeep(appDefinition);
@ -1230,7 +1176,6 @@ const EditorComponent = (props) => {
componentDefinitionChanged: true,
componentDeleted: true,
});
handleInspectorView();
} else {
useAppVersionStore.getState().actions.enableReleasedVersionPopupState();
}
@ -1240,6 +1185,7 @@ const EditorComponent = (props) => {
const gridWidth = (1 * 100) / 43; // width of the canvas grid in percentage
const _appDefinition = _.cloneDeep(appDefinition);
let newComponents = _appDefinition?.pages[currentPageId].components;
const selectedComponents = useEditorStore.getState()?.selectedComponents;
for (const selectedComponent of selectedComponents) {
let top = newComponents[selectedComponent.id].layouts[currentLayout].top;
@ -1270,7 +1216,13 @@ const EditorComponent = (props) => {
};
const copyComponents = () =>
cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false);
cloneComponents(
useEditorStore.getState()?.selectedComponents,
appDefinition,
currentPageId,
appDefinitionChanged,
false
);
const cutComponents = () => {
if (isVersionReleased) {
@ -1279,28 +1231,41 @@ const EditorComponent = (props) => {
return;
}
cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, false, true);
cloneComponents(
useEditorStore.getState()?.selectedComponents,
appDefinition,
currentPageId,
appDefinitionChanged,
false,
true
);
};
const cloningComponents = () => {
if (isVersionReleased) {
useAppVersionStore.getState().actions.enableReleasedVersionPopupState();
return;
}
cloneComponents(selectedComponents, appDefinition, currentPageId, appDefinitionChanged, true, false);
cloneComponents(
useEditorStore.getState()?.selectedComponents,
appDefinition,
currentPageId,
appDefinitionChanged,
true,
false
);
};
const handleEditorEscapeKeyPress = () => {
if (selectedComponents?.length > 0) {
if (useEditorStore.getState()?.selectedComponents?.length > 0) {
updateEditorState({
selectedComponents: [],
});
handleInspectorView();
}
};
const removeComponents = () => {
const selectedComponents = useEditorStore.getState()?.selectedComponents;
if (!isVersionReleased && selectedComponents?.length > 1) {
let newDefinition = cloneDeep(appDefinition);
@ -1315,8 +1280,6 @@ const EditorComponent = (props) => {
icon: '🗑️',
});
}
handleInspectorView();
} else if (isVersionReleased) {
useAppVersionStore.getState().actions.enableReleasedVersionPopupState();
}
@ -1375,9 +1338,7 @@ const EditorComponent = (props) => {
};
setCurrentPageId(newPageId);
setHoveredComponent(null);
updateEditorState({
currentSidebarTab: 2,
selectedComponents: [],
});
@ -1393,8 +1354,6 @@ const EditorComponent = (props) => {
},
});
const { globals: existingGlobals } = currentState;
const page = {
id: newPageId,
name,
@ -1403,7 +1362,7 @@ const EditorComponent = (props) => {
};
const globals = {
...existingGlobals,
...currentState.globals,
};
useCurrentStateStore.getState().actions.setCurrentState({ globals, page });
};
@ -1430,8 +1389,6 @@ const EditorComponent = (props) => {
},
});
const { globals: existingGlobals } = currentState;
const page = {
id: pageId,
name,
@ -1440,13 +1397,12 @@ const EditorComponent = (props) => {
};
const globals = {
...existingGlobals,
...currentState.globals,
urlparams: JSON.parse(JSON.stringify(queryString.parse(queryParamsString))),
};
useCurrentStateStore.getState().actions.setCurrentState({ globals, page });
setCurrentPageId(pageId);
handleInspectorView();
const currentPageEvents = events.filter((event) => event.target === 'page' && event.sourceId === page.id);
@ -1563,7 +1519,7 @@ const EditorComponent = (props) => {
const clonePage = (pageId) => {
setIsSaving(true);
appVersionService
.clonePage(appId, editingVersion?.id, pageId)
.clonePage(appId, editingVersionId, pageId)
.then((data) => {
const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition));
@ -1667,29 +1623,17 @@ const EditorComponent = (props) => {
});
};
useEffect(() => {
const previewQuery = queryString.stringify({ version: editingVersion?.name });
const appVersionPreviewLink = editingVersion
? `/applications/${slug || appId}/${currentState.page.handle}${
!_.isEmpty(previewQuery) ? `?${previewQuery}` : ''
}`
: '';
setAppPreviewLink(appVersionPreviewLink);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [slug, currentVersionId]);
const handleCanvasContainerMouseUp = (e) => {
if (
['real-canvas', 'modal'].includes(e.target.className) &&
useEditorStore.getState()?.selectedComponents?.length
) {
setSelectedComponents(EMPTY_ARRAY);
}
};
const deviceWindowWidth = 450;
const editorRef = {
appDefinition: appDefinition,
queryConfirmationList: queryConfirmationList,
updateQueryConfirmationList: updateQueryConfirmationList,
navigate: props.navigate,
switchPage: switchPage,
currentPageId: currentPageId,
};
if (isLoading) {
return (
<div className="apploader">
@ -1715,21 +1659,13 @@ const EditorComponent = (props) => {
</div>
);
}
//! Need to move conditionally rendered components to separate components => Widget Manger or Widget Inspector
const shouldrenderWidgetInspector =
currentSidebarTab === 1 &&
selectedComponents?.length === 1 &&
!isEmpty(appDefinition?.pages[currentPageId]?.components) &&
!isEmpty(appDefinition?.pages[currentPageId]?.components[selectedComponents[0]?.id]);
return (
<div className="editor wrapper">
<Confirm
show={queryConfirmationList?.length > 0}
message={`Do you want to run this query - ${queryConfirmationList[0]?.queryName}?`}
onConfirm={(queryConfirmationData) => onQueryConfirmOrCancel(editorRef, queryConfirmationData, true)}
onCancel={() => onQueryConfirmOrCancel(editorRef, queryConfirmationList[0])}
onConfirm={(queryConfirmationData) => onQueryConfirmOrCancel(getEditorRef(), queryConfirmationData, true)}
onCancel={() => onQueryConfirmOrCancel(getEditorRef(), queryConfirmationList[0])}
queryConfirmationData={queryConfirmationList[0]}
darkMode={props.darkMode}
key={queryConfirmationList[0]?.queryName}
@ -1748,12 +1684,11 @@ const EditorComponent = (props) => {
<EditorHeader
darkMode={props.darkMode}
appDefinition={_.cloneDeep(appDefinition)}
editingVersion={editingVersion}
canUndo={canUndo}
canRedo={canRedo}
handleUndo={handleUndo}
handleRedo={handleRedo}
saveError={saveError}
// saveError={saveError}
onNameChanged={onNameChanged}
setAppDefinitionFromVersion={setAppDefinitionFromVersion}
onVersionRelease={onVersionRelease}
@ -1768,8 +1703,6 @@ const EditorComponent = (props) => {
<div className="sub-section">
<LeftSidebar
globalSettingsChanged={globalSettingsChanged}
errorLogs={currentState.errors}
components={currentState.components}
appId={appId}
darkMode={props.darkMode}
dataSourcesChanged={dataSourcesChanged}
@ -1780,7 +1713,6 @@ const EditorComponent = (props) => {
debuggerActions={sideBarDebugger}
appDefinition={{
components: appDefinition?.pages[currentPageId]?.components ?? {},
selectedComponent: selectedComponents ? selectedComponents[selectedComponents.length - 1] : {},
pages: appDefinition?.pages ?? {},
homePageId: appDefinition?.homePageId ?? null,
showViewerNavigation: appDefinition?.showViewerNavigation,
@ -1790,7 +1722,6 @@ const EditorComponent = (props) => {
removeComponent={removeComponent}
runQuery={(queryId, queryName) => handleRunQuery(queryId, queryName)}
ref={dataSourceModalRef}
isSaving={isUpdatingEditorStateInProcess}
currentPageId={currentPageId}
addNewPage={addNewPage}
switchPage={switchPage}
@ -1809,23 +1740,13 @@ const EditorComponent = (props) => {
toggleAppMaintenance={toggleAppMaintenance}
/>
{!showComments && (
<Selecto
dragContainer={'.canvas-container'}
selectableTargets={['.react-draggable']}
hitRate={0}
selectByClick={true}
toggleContinueSelect={['shift']}
ref={selectionRef}
scrollOptions={scrollOptions}
onSelectStart={onAreaSelectionStart}
onSelectEnd={onAreaSelectionEnd}
onSelect={onAreaSelection}
onDragStart={onAreaSelectionDragStart}
onDrag={onAreaSelectionDrag}
onDragEnd={onAreaSelectionDragEnd}
onScroll={(e) => {
canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10);
}}
<EditorSelecto
selectionRef={selectionRef}
canvasContainerRef={canvasContainerRef}
setSelectedComponent={setSelectedComponent}
selectionDragRef={selectionDragRef}
appDefinition={appDefinition}
currentPageId={currentPageId}
/>
)}
<div
@ -1842,15 +1763,7 @@ const EditorComponent = (props) => {
height: computeCanvasContainerHeight(),
background: !props.darkMode ? '#EBEBEF' : '#2E3035',
}}
onMouseUp={(e) => {
if (['real-canvas', 'modal'].includes(e.target.className)) {
updateEditorState({
currentSidebarTab: 2,
selectedComponents: [],
});
setHoveredComponent(null);
}
}}
onMouseUp={handleCanvasContainerMouseUp}
ref={canvasContainerRef}
onScroll={() => {
selectionRef.current.checkScroll();
@ -1869,7 +1782,7 @@ const EditorComponent = (props) => {
}}
>
{config.ENABLE_MULTIPLAYER_EDITING && (
<RealtimeCursors editingVersionId={editingVersion?.id} editingPageId={currentPageId} />
<RealtimeCursors editingVersionId={editingVersionId} editingPageId={currentPageId} />
)}
{isLoading && (
<div className="apploader">
@ -1906,7 +1819,6 @@ const EditorComponent = (props) => {
mode={'edit'}
zoomLevel={zoomLevel}
deviceWindowWidth={deviceWindowWidth}
selectedComponents={selectedComponents}
appLoading={isLoading}
onEvent={handleEvent}
onComponentOptionChanged={handleOnComponentOptionChanged}
@ -1915,9 +1827,7 @@ const EditorComponent = (props) => {
handleUndo={handleUndo}
handleRedo={handleRedo}
removeComponent={removeComponent}
onComponentClick={handleComponentClick}
onComponentHover={handleComponentHover}
hoveredComponent={hoveredComponent}
onComponentClick={noop} // Prop is used in Viewer hence using a dummy function to prevent error in editor
sideBarDebugger={sideBarDebugger}
currentPageId={currentPageId}
/>
@ -1941,7 +1851,7 @@ const EditorComponent = (props) => {
appId={appId}
appDefinition={appDefinition}
dataSourceModalHandler={dataSourceModalHandler}
editorRef={editorRef}
editorRef={getEditorRef()}
/>
<ReactTooltip id="tooltip-for-add-query" className="tooltip" />
</div>
@ -1954,24 +1864,24 @@ const EditorComponent = (props) => {
handleEditorEscapeKeyPress={handleEditorEscapeKeyPress}
removeMultipleComponents={removeComponents}
/>
{shouldrenderWidgetInspector ? (
<div className="pages-container">
<Inspector
moveComponents={moveComponents}
componentDefinitionChanged={componentDefinitionChanged}
removeComponent={removeComponent}
selectedComponentId={selectedComponents[0].id}
allComponents={appDefinition?.pages[currentPageId]?.components}
key={selectedComponents[0].id}
switchSidebarTab={switchSidebarTab}
darkMode={props.darkMode}
pages={getPagesWithIds()}
/>
</div>
) : (
<WidgetManager componentTypes={componentTypes} zoomLevel={zoomLevel} darkMode={props.darkMode} />
)}
<RightSidebarTabManager
inspectorTab={
<div className="pages-container">
<Inspector
moveComponents={moveComponents}
componentDefinitionChanged={componentDefinitionChanged}
removeComponent={removeComponent}
allComponents={appDefinition?.pages[currentPageId]?.components}
darkMode={props.darkMode}
pages={getPagesWithIds()}
/>
</div>
}
widgetManagerTab={
<WidgetManager componentTypes={componentTypes} zoomLevel={zoomLevel} darkMode={props.darkMode} />
}
allComponents={appDefinition.pages[currentPageId]?.components}
/>
</div>
{config.COMMENT_FEATURE_ENABLE && showComments && (
<CommentNotifications socket={socket} pageId={currentPageId} />

View file

@ -21,7 +21,6 @@ export const EditorKeyHooks = ({
cloneComponents();
break;
case 'KeyC':
console.log('copyComponent');
copyComponents();
break;
case 'KeyX':

View file

@ -0,0 +1,104 @@
import React, { useCallback, memo } from 'react';
import Selecto from 'react-selecto';
import { useEditorStore, EMPTY_ARRAY } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
const EditorSelecto = ({
selectionRef,
canvasContainerRef,
currentPageId,
setSelectedComponent,
appDefinition,
selectionDragRef,
}) => {
const { setSelectionInProgress, setSelectedComponents, scrollOptions } = useEditorStore(
(state) => ({
setSelectionInProgress: state?.actions?.setSelectionInProgress,
setSelectedComponents: state?.actions?.setSelectedComponents,
scrollOptions: state.scrollOptions,
}),
shallow
);
const onAreaSelectionStart = useCallback(
(e) => {
const isMultiSelect = e.inputEvent.shiftKey || useEditorStore.getState().selectedComponents.length > 0;
setSelectionInProgress(true);
setSelectedComponents([...(isMultiSelect ? useEditorStore.getState().selectedComponents : EMPTY_ARRAY)]);
},
[setSelectionInProgress, setSelectedComponents]
);
const onAreaSelection = useCallback((e) => {
e.added.forEach((el) => {
el.classList.add('resizer-select');
});
if (useEditorStore.getState().selectionInProgress) {
e.removed.forEach((el) => {
el.classList.remove('resizer-select');
});
}
}, []);
const onAreaSelectionEnd = useCallback(
(e) => {
setSelectionInProgress(false);
e.selected.forEach((el, index) => {
const id = el.getAttribute('widgetid');
const component = appDefinition.pages[currentPageId].components[id].component;
const isMultiSelect = e.inputEvent.shiftKey || (!e.isClick && index != 0);
setSelectedComponent(id, component, isMultiSelect);
});
},
[appDefinition, currentPageId, setSelectedComponent, setSelectionInProgress]
);
const onAreaSelectionDragStart = useCallback(
(e) => {
if (e.inputEvent.target.getAttribute('id') !== 'real-canvas') {
selectionDragRef.current = true;
} else {
selectionDragRef.current = false;
}
},
[selectionDragRef]
);
const onAreaSelectionDrag = useCallback(
(e) => {
if (selectionDragRef.current) {
e.stop();
useEditorStore.getState().selectionInProgress && setSelectionInProgress(false);
}
},
[setSelectionInProgress, selectionDragRef]
);
const onAreaSelectionDragEnd = () => {
selectionDragRef.current = false;
useEditorStore.getState().selectionInProgress && setSelectionInProgress(false);
};
return (
<Selecto
dragContainer={'.canvas-container'}
selectableTargets={['.react-draggable']}
hitRate={0}
selectByClick={true}
toggleContinueSelect={['shift']}
ref={selectionRef}
scrollOptions={scrollOptions}
onSelectStart={onAreaSelectionStart}
onSelectEnd={onAreaSelectionEnd}
onSelect={onAreaSelection}
onDragStart={onAreaSelectionDragStart}
onDrag={onAreaSelectionDrag}
onDragEnd={onAreaSelectionDragEnd}
onScroll={(e) => {
canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10);
}}
/>
);
};
export default memo(EditorSelecto);

View file

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { withTranslation } from 'react-i18next';
import * as Sentry from '@sentry/react';
class ErrorBoundary extends Component {
constructor(props) {
@ -18,12 +19,11 @@ class ErrorBoundary extends Component {
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.showFallback ? <h2>{this.props.t('errorBoundary', 'Something went wrong.')}</h2> : <div></div>;
}
return this.props.children;
return (
<Sentry.ErrorBoundary fallback={<h2>{this.props.t('errorBoundary', 'Something went wrong.')}</h2>}>
{this.props.children}
</Sentry.ErrorBoundary>
);
}
}

View file

@ -64,6 +64,19 @@ function EditAppName({ appId, appName = '', onNameChanged }) {
}
};
const handleKeyDown = (e) => {
if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
}
};
React.useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown); // Clean up the event listener
};
}, []);
const handleBlur = () => {
saveAppName(name);
};

View file

@ -12,11 +12,13 @@ import config from 'config';
// eslint-disable-next-line import/no-unresolved
import { useUpdatePresence } from '@y-presence/react';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useCurrentStateStore } from '@/_stores/currentStateStore';
import { shallow } from 'zustand/shallow';
import { useAppInfo, useCurrentUser } from '@/_stores/appDataStore';
import { useAppDataActions, useAppInfo, useCurrentUser } from '@/_stores/appDataStore';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { redirectToDashboard } from '@/_helpers/routes';
import queryString from 'query-string';
import { isEmpty } from 'lodash';
export default function EditorHeader({
M,
@ -35,8 +37,8 @@ export default function EditorHeader({
}) {
const currentUser = useCurrentUser();
const { isSaving, appId, appName, app, isPublic, appVersionPreviewLink } = useAppInfo();
const { isSaving, appId, appName, app, isPublic, appVersionPreviewLink, currentVersionId } = useAppInfo();
const { setAppPreviewLink } = useAppDataActions();
const { isVersionReleased, editingVersion } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
@ -44,7 +46,12 @@ export default function EditorHeader({
}),
shallow
);
const currentState = useCurrentState();
const { pageHandle } = useCurrentStateStore(
(state) => ({
pageHandle: state?.page?.handle,
}),
shallow
);
const updatePresence = useUpdatePresence();
@ -69,6 +76,15 @@ export default function EditorHeader({
redirectToDashboard();
};
useEffect(() => {
const previewQuery = queryString.stringify({ version: editingVersion.name });
const appVersionPreviewLink = editingVersion.id
? `/applications/${slug || appId}/${pageHandle}${!isEmpty(previewQuery) ? `?${previewQuery}` : ''}`
: '';
setAppPreviewLink(appVersionPreviewLink);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [slug, currentVersionId, editingVersion]);
return (
<div className="header" style={{ width: '100%' }}>
<header className="navbar navbar-expand-md d-print-none">
@ -147,34 +163,37 @@ export default function EditorHeader({
className="d-flex justify-content-end navbar-right-section"
style={{ width: '300px', paddingRight: '12px' }}
>
<div className="navbar-nav flex-row order-md-last release-buttons ">
<div className="nav-item">
{appId && (
<ManageAppUsers
app={app}
appId={appId}
slug={slug}
darkMode={darkMode}
isVersionReleased={isVersionReleased}
pageHandle={currentState?.page?.handle}
M={M}
isPublic={isPublic ?? false}
/>
)}
<div className=" release-buttons navbar-nav flex-row">
<div className="preview-share-wrap navbar-nav flex-row" style={{ gap: '4px' }}>
<div className="nav-item">
{appId && (
<ManageAppUsers
app={app}
appId={appId}
slug={slug}
darkMode={darkMode}
isVersionReleased={isVersionReleased}
pageHandle={pageHandle}
M={M}
isPublic={isPublic ?? false}
/>
)}
</div>
<div className="nav-item">
<Link
title="Preview"
to={appVersionPreviewLink}
target="_blank"
rel="noreferrer"
data-cy="preview-link-button"
className="editor-header-icon tj-secondary-btn"
>
<SolidIcon name="eyeopen" width="14" fill="#3E63DD" />
</Link>
</div>
</div>
<div className="nav-item">
<Link
title="Preview"
to={appVersionPreviewLink}
target="_blank"
rel="noreferrer"
data-cy="preview-link-button"
className="editor-header-icon tj-secondary-btn"
>
<SolidIcon name="eyeopen" width="14" fill="#3E63DD" />
</Link>
</div>
<div className="nav-item dropdown">
<div className="nav-item dropdown promote-release-btn">
<ReleaseVersionButton
appId={appId}
appName={appName}

View file

@ -108,6 +108,7 @@ export const baseComponentProperties = (
apps={apps}
darkMode={darkMode}
pages={pages}
component={component}
/>
),
});

View file

@ -14,7 +14,7 @@ import defaultStyles from '@/_ui/Select/styles';
import { useTranslation } from 'react-i18next';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import RunjsParameters from './ActionConfigurationPanels/RunjsParamters';
import { useAppDataActions, useAppInfo } from '@/_stores/appDataStore';
import { useAppDataActions, useAppDataStore } from '@/_stores/appDataStore';
import { isQueryRunnable } from '@/_helpers/utils';
import { shallow } from 'zustand/shallow';
import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton';
@ -33,6 +33,7 @@ export const EventManager = ({
hideEmptyEventsAlert,
callerQueryId,
customEventRefs = undefined,
component,
}) => {
const dataQueries = useDataQueriesStore(({ dataQueries = [] }) => {
if (callerQueryId) {
@ -41,12 +42,21 @@ export const EventManager = ({
}
return dataQueries;
}, shallow);
const { apps, appId, events: allAppEvents } = useAppInfo();
const {
appId,
apps,
events: allAppEvents,
} = useAppDataStore((state) => ({
appId: state.appId,
apps: state.apps,
events: state.events,
}));
const { updateAppVersionEventHandlers, createAppVersionEventHandlers, deleteAppVersionEventHandler } =
useAppDataActions();
const currentEvents = allAppEvents.filter((event) => {
const currentEvents = allAppEvents?.filter((event) => {
if (customEventRefs) {
if (event.event.ref !== customEventRefs.ref) {
return false;
@ -391,6 +401,7 @@ export const EventManager = ({
initialValue={event.runOnlyIf}
onChange={(value) => handlerChanged(index, 'runOnlyIf', value)}
usePortalEditor={false}
component={component}
/>
</div>
</div>
@ -413,6 +424,7 @@ export const EventManager = ({
initialValue={event.message}
onChange={(value) => handlerChanged(index, 'message', value)}
usePortalEditor={false}
component={component}
/>
</div>
</div>
@ -445,6 +457,7 @@ export const EventManager = ({
initialValue={event.url}
onChange={(value) => handlerChanged(index, 'url', value)}
usePortalEditor={false}
component={component}
/>
</div>
)}
@ -509,6 +522,7 @@ export const EventManager = ({
initialValue={event.contentToCopy}
onChange={(value) => handlerChanged(index, 'contentToCopy', value)}
usePortalEditor={false}
component={component}
/>
</div>
)}
@ -564,6 +578,7 @@ export const EventManager = ({
onChange={(value) => handlerChanged(index, 'key', value)}
enablePreview={true}
usePortalEditor={false}
component={component}
/>
</div>
</div>
@ -576,6 +591,7 @@ export const EventManager = ({
onChange={(value) => handlerChanged(index, 'value', value)}
enablePreview={true}
usePortalEditor={false}
component={component}
/>
</div>
</div>
@ -613,6 +629,7 @@ export const EventManager = ({
initialValue={event.fileName}
onChange={(value) => handlerChanged(index, 'fileName', value)}
enablePreview={true}
component={component}
/>
</div>
</div>
@ -624,6 +641,7 @@ export const EventManager = ({
initialValue={event.data}
onChange={(value) => handlerChanged(index, 'data', value)}
enablePreview={true}
component={component}
/>
</div>
</div>
@ -658,6 +676,7 @@ export const EventManager = ({
onChange={(value) => handlerChanged(index, 'pageIndex', value)}
enablePreview={true}
usePortalEditor={false}
component={component}
/>
</div>
</div>
@ -674,6 +693,7 @@ export const EventManager = ({
onChange={(value) => handlerChanged(index, 'key', value)}
enablePreview={true}
cyLabel={`key`}
component={component}
/>
</div>
</div>
@ -686,6 +706,7 @@ export const EventManager = ({
onChange={(value) => handlerChanged(index, 'value', value)}
enablePreview={true}
cyLabel={`variable`}
component={component}
/>
</div>
</div>
@ -701,6 +722,7 @@ export const EventManager = ({
initialValue={event.key}
onChange={(value) => handlerChanged(index, 'key', value)}
enablePreview={true}
component={component}
/>
</div>
</div>
@ -717,6 +739,7 @@ export const EventManager = ({
onChange={(value) => handlerChanged(index, 'key', value)}
enablePreview={true}
cyLabel={`key`}
component={component}
/>
</div>
</div>
@ -729,6 +752,7 @@ export const EventManager = ({
onChange={(value) => handlerChanged(index, 'value', value)}
enablePreview={true}
cyLabel={`variable`}
component={component}
/>
</div>
</div>
@ -745,6 +769,7 @@ export const EventManager = ({
onChange={(value) => handlerChanged(index, 'key', value)}
enablePreview={true}
cyLabel={`key`}
component={component}
/>
</div>
</div>
@ -841,7 +866,8 @@ export const EventManager = ({
enablePreview={true}
type={param?.type}
fieldMeta={{ options: param?.options }}
cyLabel={param?.displayName}
cyLabel={param.displayName}
component={component}
/>
</div>
)}
@ -857,6 +883,7 @@ export const EventManager = ({
initialValue={event.debounce}
onChange={(value) => handlerChanged(index, 'debounce', value)}
usePortalEditor={false}
component={component}
/>
</div>
</div>

View file

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { componentTypes } from '../WidgetManager/components';
import { Table } from './Components/Table/Table.jsx';
import { Chart } from './Components/Chart';
@ -16,7 +16,7 @@ import { Icon } from './Components/Icon';
import useFocus from '@/_hooks/use-focus';
import Accordion from '@/_ui/Accordion';
import { useTranslation } from 'react-i18next';
import _ from 'lodash';
import _, { isEmpty } from 'lodash';
import { useMounted } from '@/_hooks/use-mount';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useDataQueries } from '@/_stores/dataQueriesStore';
@ -33,6 +33,7 @@ import Edit from '@/_ui/Icon/bulkIcons/Edit';
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';
const INSPECTOR_HEADER_OPTIONS = [
{
@ -53,29 +54,34 @@ const INSPECTOR_HEADER_OPTIONS = [
];
export const Inspector = ({
selectedComponentId,
componentDefinitionChanged,
allComponents,
darkMode,
switchSidebarTab,
removeComponent,
pages,
cloneComponents,
}) => {
const dataQueries = useDataQueries();
const currentState = useCurrentState();
const { selectedComponentId, setSelectedComponents } = useEditorStore(
(state) => ({
selectedComponentId: state.selectedComponents[0]?.id,
setSelectedComponents: state.actions.setSelectedComponents,
}),
shallow
);
const component = {
id: selectedComponentId,
component: JSON.parse(JSON.stringify(allComponents[selectedComponentId].component)),
component: JSON.parse(JSON.stringify(allComponents?.[selectedComponentId]?.component)),
layouts: allComponents[selectedComponentId].layouts,
parent: allComponents[selectedComponentId].parent,
};
const currentState = useCurrentState();
const [showWidgetDeleteConfirmation, setWidgetDeleteConfirmation] = useState(false);
const componentNameRef = useRef(null);
const [newComponentName, setNewComponentName] = useState(component.component.name);
// eslint-disable-next-line no-unused-vars
const [newComponentName, setNewComponentName] = useState('');
const [inputRef, setInputFocus] = useFocus();
// const [selectedTab, setSelectedTab] = useState('properties');
const [showHeaderActionsMenu, setShowHeaderActionsMenu] = useState(false);
const { isVersionReleased } = useAppVersionStore(
(state) => ({
@ -84,27 +90,27 @@ export const Inspector = ({
shallow
);
const { t } = useTranslation();
useHotkeys('backspace', () => {
if (isVersionReleased) return;
setWidgetDeleteConfirmation(true);
});
useHotkeys('escape', () => switchSidebarTab(2));
useHotkeys('escape', () => setSelectedComponents(EMPTY_ARRAY));
const componentMeta = _.cloneDeep(componentTypes.find((comp) => component.component.component === comp.component));
const isMounted = useMounted();
//
useEffect(() => {
componentNameRef.current = newComponentName;
}, [newComponentName]);
setNewComponentName(allComponents[selectedComponentId]?.component?.name);
}, [selectedComponentId, allComponents]);
const validateComponentName = (name) => {
const isValid = !Object.values(allComponents)
.map((component) => component.component.name)
.map((component) => component?.component?.name)
.includes(name);
if (component.component.name === name) {
if (component?.component.name === name) {
return true;
}
return isValid;
@ -220,7 +226,7 @@ export const Inspector = ({
componentDefinitionChanged(newComponent, { layoutPropertyChanged: true });
// Child components should also have a mobile layout
const childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent === component.id);
const childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent === component?.id);
childComponents.forEach((componentId) => {
let newChild = {
@ -302,7 +308,6 @@ export const Inspector = ({
/>
</div>
);
const stylesTab = (
<div style={{ marginBottom: '6rem' }} className={`${isVersionReleased && 'disabled'}`}>
<div className="p-3">
@ -339,15 +344,15 @@ export const Inspector = ({
show={showWidgetDeleteConfirmation}
message={'Widget will be deleted, do you want to continue?'}
onConfirm={() => {
switchSidebarTab(2);
setSelectedComponents(EMPTY_ARRAY);
removeComponent(component.id);
}}
onCancel={() => setWidgetDeleteConfirmation(false)}
darkMode={darkMode}
/>
<div>
<div className={`row inspector-component-title-input-holder ${isVersionReleased && 'disabled'}`}>
<div className="col-1" onClick={() => switchSidebarTab(2)}>
<div className="row inspector-component-title-input-holder">
<div className="col-1" onClick={() => setSelectedComponents(EMPTY_ARRAY)}>
<span data-cy={`inspector-close-icon`} className="cursor-pointer">
<ArrowLeft fill={'var(--slate12)'} width={'14'} />
</span>
@ -452,7 +457,7 @@ const widgetsWithStyleConditions = {
const RenderStyleOptions = ({ componentMeta, component, paramUpdated, dataQueries, currentState, allComponents }) => {
return Object.keys(componentMeta.styles).map((style) => {
const conditionWidget = widgetsWithStyleConditions[component.component.component] ?? null;
const conditionWidget = widgetsWithStyleConditions[component?.component?.component] ?? null;
const condition = conditionWidget?.conditions.find((condition) => condition.property) ?? {};
if (conditionWidget && conditionWidget.conditions.find((condition) => condition.conditionStyles.includes(style))) {
@ -468,7 +473,7 @@ const RenderStyleOptions = ({ componentMeta, component, paramUpdated, dataQuerie
allComponents,
style,
propertyConditon,
component.component?.definition[widgetPropertyDefinition]
component?.component?.definition[widgetPropertyDefinition]
);
}

View file

@ -43,7 +43,7 @@ export const LeftSidebarComment = forwardRef(({ selectedSidebarItem, currentPage
selectedSidebarItem={selectedSidebarItem}
title={appVersionsId ? 'Toggle comments' : 'Comments section will be available once you save this application'}
icon={'comments'}
className={cx(`left-sidebar-item left-sidebar-layout`, {
className={cx(`left-sidebar-item sidebar-comments left-sidebar-layout`, {
disabled: !appVersionsId,
active: isActive,
})}

View file

@ -16,7 +16,7 @@ function Logs({ logProps, idx }) {
? 'Completed'
: logProps?.type === 'component'
? `Invalid property detected: ${logProps?.message}.`
: `${startCase(logProps?.type)} failed: ${logProps?.message ?? ''}`;
: `${startCase(logProps?.type)} failed: ${logProps?.message ? logProps?.message : logProps?.error?.message}`;
const defaultStyles = {
transform: open ? 'rotate(90deg)' : 'rotate(0deg)',

View file

@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { HeaderSection } from '@/_ui/LeftSidebar';
import JSONTreeViewer from '@/_ui/JSONTreeViewer';
import _ from 'lodash';
import _, { isEmpty } from 'lodash';
import { toast } from 'react-hot-toast';
import { getSvgIcon } from '@/_helpers/appUtils';
@ -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 { useEditorStore } from '@/_stores/editorStore';
const staticDataSources = [
{ kind: 'tooljetdb', id: 'null', name: 'Tooljet Database' },
@ -37,16 +38,27 @@ export const LeftSidebarInspector = ({
}),
shallow
);
const componentDefinitions = JSON.parse(JSON.stringify(appDefinition))['components'];
const selectedComponent = React.useMemo(() => {
return {
id: appDefinition['selectedComponent']?.id,
component: appDefinition['selectedComponent']?.component?.name,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appDefinition['selectedComponent']]);
const { selectedComponents } = useEditorStore(
(state) => ({
selectedComponents: state.selectedComponents,
}),
shallow
);
const currentState = useCurrentState();
const componentDefinitions = JSON.parse(JSON.stringify(appDefinition))['components'];
const selectedComponent = React.useMemo(() => {
const _selectedComponent = selectedComponents[selectedComponents.length - 1];
return {
id: _selectedComponent?.id,
component: _selectedComponent?.component?.name,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedComponents]);
const memoizedJSONData = React.useMemo(() => {
const updatedQueries = {};
const { queries: currentQueries } = currentState;

View file

@ -166,6 +166,7 @@ const PageHandleField = ({ page, updatePageHandle }) => {
const Field = ({ id, text, iconSrc, customClass = '', closeMenu, disabled = false, callback = () => null }) => {
const handleOnClick = (e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
callback(id);

View file

@ -80,8 +80,6 @@ export const LeftSidebar = forwardRef((props, ref) => {
const currentState = useCurrentState();
const [pinned, setPinned] = useState(!!localStorage.getItem('selectedSidebarItem'));
const [realState, setRealState] = useState(currentState);
const { errorLogs, clearErrorLogs, unReadErrorCount, allLog } = useDebugger({
currentPageId,
isDebuggerOpen: !!selectedSidebarItem,
@ -139,10 +137,6 @@ export const LeftSidebar = forwardRef((props, ref) => {
const setSideBarBtnRefs = (page) => (ref) => {
sideBarBtnRefs.current[page] = ref;
};
useEffect(() => {
setRealState(currentState); //!ceck this
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState.components]);
const backgroundFxQuery = appDefinition?.globalSettings?.backgroundFxQuery;

View file

@ -55,7 +55,12 @@ function DataSourcePicker({ dataSources, staticDataSources, darkMode, globalData
<p className="mb-3" style={{ textAlign: 'center' }}>
Select a Data source to start creating a new query. To know more about queries in ToolJet, you can read our
&nbsp;
<a target="_blank" href="https://docs.tooljet.com/docs/app-builder/query-panel" rel="noreferrer">
<a
data-cy="querymanager-doc-link"
target="_blank"
href="https://docs.tooljet.com/docs/app-builder/query-panel"
rel="noreferrer"
>
documentation
</a>
</p>

View file

@ -66,6 +66,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
className="py-2 px-2 rounded option-nested-datasource-selector small text-truncate"
data-tooltip-id="tooltip-for-add-query-dd-option"
data-tooltip-content={source.name}
data-cy={`ds-${source.name.toLowerCase()}`}
>
{source.name}
<Tooltip id="tooltip-for-add-query-dd-option" className="tooltip query-manager-ds-select-tooltip" />
@ -83,7 +84,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
const DataSourceOptions = [
{
label: (
<span className="color-slate9" style={{ fontWeight: 500 }}>
<span data-cy="ds-section-header-default" className="color-slate9" style={{ fontWeight: 500 }}>
Defaults
</span>
),
@ -92,7 +93,10 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup }) {
...staticDataSources.map((source) => ({
label: (
<div>
<DataSourceIcon source={source} height={16} /> <span className="ms-1 small">{source.name}</span>
<DataSourceIcon source={source} height={16} />{' '}
<span data-cy={`ds-${source.name.toLowerCase()}`} className="ms-1 small">
{source.name}
</span>
</div>
),
value: source.id,

View file

@ -14,7 +14,6 @@ const Preview = ({ darkMode }) => {
const previewLoading = usePreviewLoading();
const { setPreviewData } = useQueryPanelActions();
const previewPanelRef = useRef();
useEffect(() => {
setTheme(() => getTheme(darkMode));
}, [darkMode]);
@ -40,7 +39,7 @@ const Preview = ({ darkMode }) => {
const renderRawData = () => {
if (!queryPreviewData) {
return `${queryPreviewData}`;
return queryPreviewData === null ? '' : `${queryPreviewData}`;
} else {
return isJson ? JSON.stringify(queryPreviewData).toString() : queryPreviewData.toString();
}

View file

@ -100,7 +100,11 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef },
})}
>
<button
onClick={() => runQuery(editorRef, selectedQuery?.id, selectedQuery?.name)}
onClick={() =>
runQuery(editorRef, selectedQuery?.id, selectedQuery?.name, undefined, 'edit', {
shouldSetPreviewData: true,
})
}
className={`border-0 default-secondary-button float-right1 ${buttonLoadingState(isLoading)}`}
data-cy="query-run-button"
disabled={isInDraft}

View file

@ -259,6 +259,7 @@ const DataSourceSelector = ({
onChange={(e) => setSearch(e.target.value)}
ref={searchBoxRef}
value={search}
data-cy="input-query-ds-filter"
/>
</div>
</div>
@ -279,7 +280,10 @@ const DataSourceSelector = ({
label={
<div className="d-flex align-items-center">
<DataSourceIcon source={source} height={12} styles={{ minWidth: 12 }} />
&nbsp;<span className="ms-1 text-truncate">{source.name}</span>
&nbsp;
<span className="ms-1 text-truncate" data-cy={`ds-filter-${source.name.toLowerCase()}`}>
{source.name}
</span>
</div>
}
/>

View file

@ -202,7 +202,7 @@ const EmptyDataSource = () => (
<FolderEmpty style={{ height: '16px' }} />
</span>
</div>
<span>No queries have been added. </span>
<span data-cy="label-no-queries">No queries have been added. </span>
</div>
);

View file

@ -11,8 +11,10 @@ const RealtimeAvatars = ({ darkMode }) => {
const others = useOthers();
const othersOnSameVersionAndPage = others.filter(
(other) =>
other?.presence?.editingVersionId === self?.presence.editingVersionId &&
other?.presence?.editingPageId === self?.presence.editingPageId
other?.presence &&
self?.presence &&
other?.presence?.editingVersionId === self?.presence?.editingVersionId &&
other?.presence?.editingPageId === self?.presence?.editingPageId
);
const getAvatarText = (presence) => presence.firstName?.charAt(0) + presence.lastName?.charAt(0);

View file

@ -0,0 +1,25 @@
import { useEditorStore } from '@/_stores/editorStore';
import React from 'react';
import { shallow } from 'zustand/shallow';
import { isEmpty } from 'lodash';
const RightSidebarTabManager = ({ inspectorTab, widgetManagerTab, allComponents }) => {
const { selectedComponents } = useEditorStore(
(state) => ({
selectedComponents: state?.selectedComponents,
}),
shallow
);
const currentTab = selectedComponents.length === 1 ? 1 : 2;
const showInspectorTab =
currentTab === 1 &&
selectedComponents.length === 1 &&
!isEmpty(allComponents) &&
!isEmpty(allComponents[selectedComponents[0]?.id]);
return <>{showInspectorTab ? inspectorTab : widgetManagerTab}</>;
};
export default RightSidebarTabManager;

View file

@ -15,6 +15,7 @@ import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { useMounted } from '@/_hooks/use-mount';
import { useEditorStore } from '@/_stores/editorStore';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
@ -46,7 +47,6 @@ export const SubContainer = ({
onComponentHover,
hoveredComponent,
sideBarDebugger,
selectedComponents,
onOptionChange,
exposedVariables,
addDefaultChildren = false,
@ -402,6 +402,7 @@ export const SubContainer = ({
let newBoxes = { ...boxes };
const subContainerHeight = canvasBounds.height - 30;
const selectedComponents = useEditorStore.getState().selectedComponents;
if (selectedComponents) {
for (const selectedComponent of selectedComponents) {
@ -522,6 +523,7 @@ export const SubContainer = ({
backgroundSize: `${gridWidth}px 10px`,
};
//check if parent is listview or form return false is so
const checkParent = (box) => {
let isListView = false,
isForm = false;
@ -590,7 +592,19 @@ export const SubContainer = ({
onOptionChange && onOptionChange({ component, optionName, value, componentId });
}
}
onComponentOptionsChanged={onComponentOptionsChanged}
onComponentOptionsChanged={(component, variableSet, id) => {
checkParent(box)
? onComponentOptionsChanged(component, variableSet)
: variableSet.map((item) => {
onOptionChange &&
onOptionChange({
component,
optionName: item[0],
value: item[1],
componentId: id,
});
});
}}
key={key}
onResizeStop={onResizeStop}
onDragStop={onDragStop}
@ -634,7 +648,6 @@ export const SubContainer = ({
removeComponent,
currentLayout,
deviceWindowWidth,
selectedComponents,
darkMode,
readOnly,
onComponentHover,

View file

@ -0,0 +1,36 @@
import React from 'react';
import WidgetIcon from '@/../assets/images/icons/widgets';
import { useTranslation } from 'react-i18next';
const WidgetBox = ({ component, darkMode }) => {
const { t } = useTranslation();
return (
<div style={{ height: '100%' }}>
<div className="component-image-wrapper" style={{ height: '56px', width: '72px' }}>
<div
className="component-image-holder d-flex flex-column justify-content-center"
style={{ height: '100%' }}
data-cy={`widget-list-box-${component.displayName.toLowerCase().replace(/\s+/g, '-')}`}
>
<center>
<div
className="widget-svg-container"
style={{
width: '24px',
height: '24px',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
}}
>
<WidgetIcon name={component.name.toLowerCase()} fill={darkMode ? '#3A3F42' : '#D7DBDF'} />
</div>
</center>
</div>
<div className="component-title">{t(`widget.${component.name}.displayName`, component.displayName)}</div>
</div>
</div>
);
};
export default WidgetBox;

View file

@ -83,6 +83,7 @@ export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel,
const commonItems = ['Table', 'Chart', 'Button', 'Text', 'Datepicker'];
const formItems = [
'Form',
'TextInput',
'NumberInput',
'PasswordInput',

View file

@ -536,7 +536,7 @@ export const widgets = [
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
cellSize: { value: 'regular' },
borderRadius: { value: '0' },
borderRadius: { value: '4' },
tableType: { value: 'table-classic' },
},
},
@ -676,7 +676,7 @@ export const widgets = [
textColor: { value: '#fff' },
loaderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
borderColor: { value: '#375FCF' },
disabledState: { value: '{{false}}' },
},
@ -1049,6 +1049,217 @@ export const widgets = [
},
},
},
{
name: 'Form',
displayName: 'Form',
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
},
defaultChildren: [
{
componentName: 'Text',
layout: {
top: 40,
left: 10,
height: 30,
width: 17,
},
properties: ['text'],
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'User Details',
fontWeight: 'bold',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Text',
layout: {
top: 90,
left: 10,
height: 30,
},
properties: ['text'],
defaultValue: {
text: 'Name',
},
},
{
componentName: 'Text',
layout: {
top: 160,
left: 10,
height: 30,
},
properties: ['text'],
defaultValue: {
text: 'Age',
},
},
{
componentName: 'TextInput',
layout: {
top: 120,
left: 10,
height: 30,
width: 25,
},
properties: ['placeholder'],
defaultValue: {
placeholder: 'Enter your name',
},
},
{
componentName: 'NumberInput',
layout: {
top: 190,
left: 10,
height: 30,
width: 25,
},
properties: ['value'],
styles: ['borderColor'],
defaultValue: {
value: 24,
borderColor: '#dadcde',
},
},
{
componentName: 'Button',
layout: {
top: 240,
left: 10,
height: 30,
width: 10,
},
properties: ['text'],
defaultValue: {
text: 'Submit',
},
},
],
component: 'Form',
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
properties: {
buttonToSubmit: {
type: 'select',
displayName: 'Button To Submit Form',
options: [{ name: 'None', value: 'none' }],
validation: {
schema: { type: 'string' },
},
conditionallyRender: {
key: 'advanced',
value: false,
},
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
validation: {
schema: { type: 'boolean' },
},
},
advanced: {
type: 'toggle',
displayName: ' Use custom schema',
},
JSONSchema: {
type: 'code',
displayName: 'JSON Schema',
conditionallyRender: {
key: 'advanced',
value: true,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
backgroundColor: {
type: 'color',
displayName: 'Background color',
validation: {
schema: { type: 'string' },
},
},
borderRadius: {
type: 'code',
displayName: 'Border Radius',
validation: {
schema: {
type: 'union',
schemas: [{ type: 'string' }, { type: 'number' }],
},
},
},
borderColor: {
type: 'color',
displayName: 'Border color',
validation: {
schema: { type: 'string' },
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
},
},
},
exposedVariables: {
data: {},
isValid: true,
},
actions: [
{
handle: 'submitForm',
displayName: 'Submit Form',
},
{
handle: 'resetForm',
displayName: 'Reset Form',
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
properties: {
loadingState: { value: '{{false}}' },
advanced: { value: '{{false}}' },
JSONSchema: {
value:
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
},
},
{
name: 'TextInput',
displayName: 'Text Input',
@ -1173,7 +1384,7 @@ export const widgets = [
textColor: { value: '#000' },
borderColor: { value: '#dadcde' },
errTextColor: { value: '#ff0000' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
backgroundColor: { value: '#fff' },
@ -1299,7 +1510,7 @@ export const widgets = [
styles: {
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
backgroundColor: { value: '#ffffffff' },
borderColor: { value: '#fff' },
textColor: { value: '#232e3c' },
@ -1388,7 +1599,7 @@ export const widgets = [
styles: {
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
backgroundColor: { value: '#ffffff' },
},
},
@ -1496,7 +1707,7 @@ export const widgets = [
styles: {
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
},
},
},
@ -1871,7 +2082,7 @@ export const widgets = [
styles: {
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
},
},
},
@ -1967,7 +2178,7 @@ export const widgets = [
},
events: [],
styles: {
borderRadius: { value: '0' },
borderRadius: { value: '4' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
@ -2355,7 +2566,7 @@ export const widgets = [
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderRadius: { value: '4' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
@ -2549,7 +2760,7 @@ export const widgets = [
},
events: [],
styles: {
borderRadius: { value: '0' },
borderRadius: { value: '4' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
justifyContent: { value: 'left' },
@ -2678,7 +2889,7 @@ export const widgets = [
},
events: [],
styles: {
borderRadius: { value: '0' },
borderRadius: { value: '4' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
@ -3277,7 +3488,7 @@ export const widgets = [
styles: {
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
},
},
},
@ -3525,7 +3736,7 @@ export const widgets = [
styles: {
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
},
},
},
@ -3976,7 +4187,7 @@ export const widgets = [
borderColor: { value: '#dadcde' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
},
},
},
@ -4876,7 +5087,7 @@ ReactDOM.render(<ConnectedComponent />, document.body);`,
backgroundColor: { value: '' },
textColor: { value: '' },
visibility: { value: '{{true}}' },
borderRadius: { value: '{{0}}' },
borderRadius: { value: '{{4}}' },
disabledState: { value: '{{false}}' },
selectedTextColor: { value: '' },
selectedBackgroundColor: { value: '' },
@ -5577,217 +5788,6 @@ ReactDOM.render(<ConnectedComponent />, document.body);`,
},
},
},
{
name: 'Form',
displayName: 'Form',
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
},
defaultChildren: [
{
componentName: 'Text',
layout: {
top: 40,
left: 10,
height: 30,
width: 17,
},
properties: ['text'],
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'User Details',
fontWeight: 'bold',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Text',
layout: {
top: 90,
left: 10,
height: 30,
},
properties: ['text'],
defaultValue: {
text: 'Name',
},
},
{
componentName: 'Text',
layout: {
top: 160,
left: 10,
height: 30,
},
properties: ['text'],
defaultValue: {
text: 'Age',
},
},
{
componentName: 'TextInput',
layout: {
top: 120,
left: 10,
height: 30,
width: 25,
},
properties: ['placeholder'],
defaultValue: {
placeholder: 'Enter your name',
},
},
{
componentName: 'NumberInput',
layout: {
top: 190,
left: 10,
height: 30,
width: 25,
},
properties: ['value'],
styles: ['borderColor'],
defaultValue: {
value: 24,
borderColor: '#dadcde',
},
},
{
componentName: 'Button',
layout: {
top: 240,
left: 10,
height: 30,
width: 10,
},
properties: ['text'],
defaultValue: {
text: 'Submit',
},
},
],
component: 'Form',
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
properties: {
buttonToSubmit: {
type: 'select',
displayName: 'Button To Submit Form',
options: [{ name: 'None', value: 'none' }],
validation: {
schema: { type: 'string' },
},
conditionallyRender: {
key: 'advanced',
value: false,
},
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
validation: {
schema: { type: 'boolean' },
},
},
advanced: {
type: 'toggle',
displayName: ' Use custom schema',
},
JSONSchema: {
type: 'code',
displayName: 'JSON Schema',
conditionallyRender: {
key: 'advanced',
value: true,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
backgroundColor: {
type: 'color',
displayName: 'Background color',
validation: {
schema: { type: 'string' },
},
},
borderRadius: {
type: 'code',
displayName: 'Border Radius',
validation: {
schema: {
type: 'union',
schemas: [{ type: 'string' }, { type: 'number' }],
},
},
},
borderColor: {
type: 'color',
displayName: 'Border color',
validation: {
schema: { type: 'string' },
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
},
},
},
exposedVariables: {
data: {},
isValid: true,
},
actions: [
{
handle: 'submitForm',
displayName: 'Submit Form',
},
{
handle: 'resetForm',
displayName: 'Reset Form',
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
properties: {
loadingState: { value: '{{false}}' },
advanced: { value: '{{false}}' },
JSONSchema: {
value:
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
},
},
{
name: 'BoundedBox',
displayName: 'Bounded Box',

View file

@ -96,3 +96,15 @@ export const validateProperties = (resolvedProperties, propertyDefinitions) => {
);
return [coercedProperties, allErrors];
};
export const validateProperty = (resolvedProperty, propertyDefinitions, paramName) => {
const validationDefinition = propertyDefinitions?.validation?.schema;
const value = resolvedProperty?.[paramName];
const defaultValue = propertyDefinitions?.validation?.defaultValue;
const schema = _.isUndefined(validationDefinition)
? any()
: generateSchemaFromValidationDefinition(validationDefinition);
const [_valid, errors] = paramName ? validate(value, schema, defaultValue) : [true, []];
return [_valid, errors];
};

View file

@ -69,7 +69,7 @@ export default function AppCard({
}
return (
<div className="card homepage-app-card animation-fade">
<div className="card homepage-app-card">
<div key={app?.id} ref={hoverRef} data-cy={`${app?.name.toLowerCase().replace(/\s+/g, '-')}-card`}>
<div className="row home-app-card-header">
<div className="col-12 d-flex justify-content-between">

View file

@ -75,12 +75,14 @@ export const SearchBox = forwardRef(
autoFocus={autoFocus}
ref={ref}
/>
{(isFocused || showClearButton) && (
{searchText.length > 0 ? (
<span className="input-icon-addon end" onMouseDown={clearSearchText}>
<div className="d-flex tj-common-search-input-clear-icon" title="clear">
<SolidIcon name="remove" />
</div>
</span>
) : (
''
)}
</div>
</div>

View file

@ -571,11 +571,27 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'control-component': {
const component = Object.values(getCurrentState()?.components ?? {}).filter(
let component = Object.values(getCurrentState()?.components ?? {}).filter(
(component) => component.id === event.componentId
)[0];
const action = component?.[event.componentSpecificActionHandle];
const actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
let action = '';
let actionArguments = '';
// check if component id not found then try to find if its available as child widget else continue
// with normal flow finding action
if (component == undefined) {
component = _ref.appDefinition.pages[getCurrentState()?.page?.id].components[event.componentId].component;
const parent = Object.values(getCurrentState()?.components ?? {}).find(
(item) => item.id === component.parent
);
const child = Object.values(parent?.children).find((item) => item.id === event.componentId);
if (child) {
action = child[event.componentSpecificActionHandle];
}
} else {
//normal component outside a container ex : form
action = component?.[event.componentSpecificActionHandle];
}
actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
...param,
value: resolveReferences(param.value, getCurrentState(), undefined, customVariables),
}));
@ -814,8 +830,14 @@ export function getQueryVariables(options, state) {
export function previewQuery(_ref, query, calledFromQuery = false, parameters = {}, hasParamSupport = false) {
const options = getQueryVariables(query.options, getCurrentState());
const { setPreviewLoading, setPreviewData } = useQueryPanelStore.getState().actions;
const queryPanelState = useQueryPanelStore.getState();
const { queryPreviewData } = queryPanelState;
const { setPreviewLoading, setPreviewData } = queryPanelState.actions;
setPreviewLoading(true);
if (queryPreviewData) {
setPreviewData('');
}
return new Promise(function (resolve, reject) {
let queryExecutionPromise = null;
@ -929,6 +951,15 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
let dataQuery = {};
// const { setPreviewLoading, setPreviewData } = useQueryPanelStore.getState().actions;
const queryPanelState = useQueryPanelStore.getState();
const { queryPreviewData } = queryPanelState;
const { setPreviewLoading, setPreviewData } = queryPanelState.actions;
if (parameters?.shouldSetPreviewData) {
setPreviewLoading(true);
queryPreviewData && setPreviewData('');
}
if (query) {
dataQuery = JSON.parse(JSON.stringify(query));
} else {
@ -1030,6 +1061,10 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
errorData = data;
break;
}
if (parameters?.shouldSetPreviewData) {
setPreviewLoading(false);
setPreviewData(errorData);
}
// errorData = query.kind === 'runpy' ? data.data : data;
useCurrentStateStore.getState().actions.setErrors({
[queryName]: {
@ -1061,14 +1096,19 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
resolve(data);
onEvent(_self, 'onDataQueryFailure', queryEvents);
if (mode !== 'view') {
const err = query.kind == 'tooljetdb' ? data?.error || data : _.isEmpty(data.data) ? data : data.data;
toast.error(err?.message);
const err = query.kind == 'tooljetdb' ? data?.error || data : data;
toast.error(err?.message ? err?.message : 'Something went wrong');
}
return;
} else {
let rawData = data.data;
let finalData = data.data;
if (parameters?.shouldSetPreviewData) {
setPreviewLoading(false);
setPreviewData(finalData);
}
if (dataQuery.options.enableTransformation) {
finalData = await runTransformation(
_ref,

View file

@ -31,22 +31,13 @@ export const initEditorWalkThrough = () => {
closeBtnText: 'Skip (1/6)',
},
},
{
element: '.sidebar-datasources',
popover: {
title: 'Connect to data sources',
description: 'You can manage your data sources from here.',
position: 'right',
closeBtnText: 'Skip (2/6)',
},
},
{
element: '.left-sidebar-inspector',
popover: {
title: 'Inspector',
description: 'Inspector lets you check the properties of components, results of queries etc.',
position: 'right',
closeBtnText: 'Skip (3/6)',
closeBtnText: 'Skip (2/6)',
},
},
{
@ -56,16 +47,26 @@ export const initEditorWalkThrough = () => {
description:
'Create queries to interact with your data sources, run JavaScript snippets and to make API requests.',
position: 'top',
closeBtnText: 'Skip (3/6)',
},
},
{
element: '.preview-share-wrap',
popover: {
title: 'Preview & share',
description:
'Click on preview to view the current changes on app viewer. Click on share button to view the sharing options.',
position: 'left',
closeBtnText: 'Skip (4/6)',
},
},
{
element: '.release-buttons',
element: '.promote-release-btn',
popover: {
title: 'Preview, release & share',
title: 'Release',
description:
'Click on preview to view the current changes on app viewer. Click on share button to view the sharing options. Release the editing version to make the changes live. Released versions cannot be modified, you will have to create another version to make more changes.',
position: 'bottom',
' Release the editing version to make the changes live. Released versions cannot be modified, you will have to create another version to make more changes.',
position: 'left',
closeBtnText: 'Skip (5/6)',
},
},
@ -74,7 +75,7 @@ export const initEditorWalkThrough = () => {
popover: {
title: 'Collaborate',
description: 'Add comments on canvas and tag your team members to collaborate.',
position: 'right',
position: 'top',
closeBtnText: 'Skip (6/6)',
},
},

View file

@ -90,7 +90,7 @@ function resolveCode(code, state, customObjects = {}, withError = false, reserve
);
} catch (err) {
error = err;
console.log('eval_error', err);
// console.log('eval_error', err);
}
}

View file

@ -24,7 +24,7 @@ class WebSocketConnection {
addListeners(appId) {
// Connection opened
this.socket.addEventListener('open', (event) => {
console.log('connection established', event);
// console.log('connection established', event);
//TODO: verify if the socket functionality is working or not
this.socket.send(
@ -43,12 +43,12 @@ class WebSocketConnection {
// Connection closed
this.socket.addEventListener('close', (event) => {
console.log('connection closed', event);
// console.log('connection closed', event);
});
// Listen for possible errors
this.socket.addEventListener('error', (event) => {
console.log('WebSocket error: ', event);
// console.log('WebSocket error: ', event);
});
}
}

View file

@ -1,5 +1,6 @@
import { appVersionService } from '@/_services';
import { create, zustandDevTools } from './utils';
import { shallow } from 'zustand/shallow';
const initialState = {
editingVersion: null,
@ -127,9 +128,9 @@ export const useAppDataStore = create(
)
);
export const useEditingVersion = () => useAppDataStore((state) => state.editingVersion);
export const useIsSaving = () => useAppDataStore((state) => state.isSaving);
export const useUpdateEditingVersion = () => useAppDataStore((state) => state.actions);
export const useCurrentUser = () => useAppDataStore((state) => state.currentUser);
export const useEditingVersion = () => useAppDataStore((state) => state.editingVersion, shallow);
export const useIsSaving = () => useAppDataStore((state) => state.isSaving, shallow);
export const useUpdateEditingVersion = () => useAppDataStore((state) => state.actions, shallow);
export const useCurrentUser = () => useAppDataStore((state) => state.currentUser, shallow);
export const useAppInfo = () => useAppDataStore((state) => state);
export const useAppDataActions = () => useAppDataStore((state) => state.actions);
export const useAppDataActions = () => useAppDataStore((state) => state.actions, shallow);

View file

@ -26,10 +26,10 @@ export const useCurrentStateStore = create(
...initialState,
actions: {
setCurrentState: (currentState) => {
set({ ...currentState }), false, { type: 'SET_CURRENT_STATE', currentState };
set({ ...currentState }, false, { type: 'SET_CURRENT_STATE', currentState });
},
setErrors: (error) => {
set({ errors: { ...get().errors, ...error } }), false, { type: 'SET_ERRORS', error };
set({ errors: { ...get().errors, ...error } }, false, { type: 'SET_ERRORS', error });
},
},
}),

View file

@ -10,6 +10,7 @@ import { v4 as uuidv4 } from 'uuid';
import { toast } from 'react-hot-toast';
import _, { isEmpty, throttle } from 'lodash';
import { useEditorStore } from './editorStore';
import { shallow } from 'zustand/shallow';
import { useCurrentStateStore } from './currentStateStore';
const initialState = {
@ -359,7 +360,7 @@ const sortByAttribute = (data, sortBy, order) => {
}
};
export const useDataQueries = () => useDataQueriesStore((state) => state.dataQueries);
export const useDataQueries = () => useDataQueriesStore((state) => state.dataQueries, shallow);
export const useDataQueriesActions = () => useDataQueriesStore((state) => state.actions);
export const useQueryCreationLoading = () => useDataQueriesStore((state) => !!state.creatingQueryInProcessId);
export const useQueryUpdationLoading = () => useDataQueriesStore((state) => state.isUpdatingQueryInProcess);
export const useQueryCreationLoading = () => useDataQueriesStore((state) => !!state.creatingQueryInProcessId, shallow);
export const useQueryUpdationLoading = () => useDataQueriesStore((state) => state.isUpdatingQueryInProcess, shallow);

View file

@ -1,7 +1,9 @@
import { create, zustandDevTools } from './utils';
import { create } from './utils';
import { v4 as uuid } from 'uuid';
const STORE_NAME = 'Editor';
export const EMPTY_ARRAY = [];
const ACTIONS = {
SET_SHOW_COMMENTS: 'SET_SHOW_COMMENTS',
SET_HOVERED_COMPONENT: 'SET_HOVERED_COMPONENT',
@ -17,9 +19,8 @@ const initialState = {
showComments: false,
hoveredComponent: '',
selectionInProgress: false,
selectedComponents: [],
selectedComponents: EMPTY_ARRAY,
isEditorActive: false,
currentSidebarTab: 2,
selectedComponent: null,
scrollOptions: {
container: null,
@ -31,7 +32,6 @@ const initialState = {
currentVersion: {},
noOfVersionsSupported: 100,
appDefinition: {},
// isSaving: false,
isUpdatingEditorStateInProcess: false,
saveError: false,
isLoading: true,
@ -43,7 +43,7 @@ const initialState = {
};
export const useEditorStore = create(
// Dev tools for this store are disabled comments since its freezing chrome tab
//Redux Dev tools for this store are disabled since its freezing chrome tab
(set, get) => ({
...initialState,
actions: {

View file

@ -11826,8 +11826,6 @@ tbody {
}
#popover-basic-2 {
z-index: 10 !important;
.sketch-picker {
left: 7px;
width: 170px !important;

View file

@ -91,7 +91,7 @@ const UnstyledButton = ({ children, onClick, classNames = '', styles = {}, disab
type="button"
style={{ ...styles, ...(disabled ? defaultDisabledStyles : {}), ...cursorNotPointer }}
className={`unstyled-button ${classNames} ${disabled && 'disabled'} ${darkMode && 'dark'}`}
onClick={onClick}
onMouseDown={onClick}
>
{children}
</div>

View file

@ -1 +1 @@
2.25.0
2.26.0

View file

@ -93,6 +93,7 @@ async function bootstrap() {
'https://unpkg.com/react-dom@16.7.0/umd/react-dom.production.min.js',
'cdn.skypack.dev',
'cdn.jsdelivr.net',
'https://esm.sh',
],
'default-src': [
'maps.googleapis.com',

View file

@ -636,53 +636,57 @@ export class AppsService {
.where('data_query.appVersionId = :appVersionId', { appVersionId: versionFrom?.id })
.andWhere('dataSource.scope = :scope', { scope: DataSourceScopes.GLOBAL })
.getMany();
const dataSources = versionFrom?.dataSources;
const dataSources = versionFrom?.dataSources; //Local data sources
const globalDataSources = [...new Map(globalQueries.map((gq) => [gq.dataSource.id, gq.dataSource])).values()];
const dataSourceMapping = {};
const newDataQueries = [];
const allEvents = await manager.find(EventHandler, {
where: { appVersionId: versionFrom?.id, target: 'data_query' },
});
if (dataSources?.length > 0) {
for (const dataSource of dataSources) {
const dataSourceParams: Partial<DataSource> = {
name: dataSource.name,
kind: dataSource.kind,
type: dataSource.type,
appVersionId: appVersion.id,
};
const newDataSource = await manager.save(manager.create(DataSource, dataSourceParams));
dataSourceMapping[dataSource.id] = newDataSource.id;
const dataQueries = versionFrom?.dataSources?.find((ds) => ds.id === dataSource.id).dataQueries;
for (const dataQuery of dataQueries) {
const dataQueryParams = {
name: dataQuery.name,
options: dataQuery.options,
dataSourceId: newDataSource.id,
if (dataSources?.length > 0 || globalDataSources?.length > 0) {
if (dataSources?.length > 0) {
for (const dataSource of dataSources) {
const dataSourceParams: Partial<DataSource> = {
name: dataSource.name,
kind: dataSource.kind,
type: dataSource.type,
appVersionId: appVersion.id,
};
const newQuery = await manager.save(manager.create(DataQuery, dataQueryParams));
const newDataSource = await manager.save(manager.create(DataSource, dataSourceParams));
dataSourceMapping[dataSource.id] = newDataSource.id;
const dataQueryEvents = allEvents.filter((event) => event.sourceId === dataQuery.id);
const dataQueries = versionFrom?.dataSources?.find((ds) => ds.id === dataSource.id).dataQueries;
dataQueryEvents.forEach(async (event, index) => {
const newEvent = new EventHandler();
for (const dataQuery of dataQueries) {
const dataQueryParams = {
name: dataQuery.name,
options: dataQuery.options,
dataSourceId: newDataSource.id,
appVersionId: appVersion.id,
};
const newQuery = await manager.save(manager.create(DataQuery, dataQueryParams));
newEvent.id = uuid.v4();
newEvent.name = event.name;
newEvent.sourceId = newQuery.id;
newEvent.target = event.target;
newEvent.event = event.event;
newEvent.index = event.index ?? index;
newEvent.appVersionId = appVersion.id;
const dataQueryEvents = allEvents.filter((event) => event.sourceId === dataQuery.id);
await manager.save(newEvent);
});
dataQueryEvents.forEach(async (event, index) => {
const newEvent = new EventHandler();
oldDataQueryToNewMapping[dataQuery.id] = newQuery.id;
newDataQueries.push(newQuery);
newEvent.id = uuid.v4();
newEvent.name = event.name;
newEvent.sourceId = newQuery.id;
newEvent.target = event.target;
newEvent.event = event.event;
newEvent.index = event.index ?? index;
newEvent.appVersionId = appVersion.id;
await manager.save(newEvent);
});
oldDataQueryToNewMapping[dataQuery.id] = newQuery.id;
newDataQueries.push(newQuery);
}
}
}