Merge branch 'main' into fix/workspace-constants-mapping

This commit is contained in:
parthy007 2025-07-01 18:55:20 +05:30
commit 39d6d14287
504 changed files with 22206 additions and 5280 deletions

View file

@ -1,6 +1,6 @@
{
"[javascript, typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"eslint.validate": [
"javascript",
@ -8,8 +8,8 @@
"typescript",
"typescriptreact"
],
"eslint.format.enable": false,
"editor.formatOnSave": false,
"eslint.format.enable": true,
"editor.formatOnSave": true,
"json.schemas": [
{
"fileMatch": [

View file

@ -98,8 +98,12 @@ module.exports = defineConfig({
configFile: environment.configFile,
specPattern: [
"cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js",
// Exclude specific files from ceTestcases/apps and ceTestcases/workspace
"cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js",
"cypress/e2e/happyPath/platform/ceTestcases/**/!(*appSlug).cy.js",
"cypress/e2e/happyPath/platform/ceTestcases/**/!(*appSlug|appImport|privateAndpublicApps|version).cy.js",
// Exclude workspaceConstants.cy.js explicitly
"cypress/e2e/happyPath/platform/ceTestcases/workspace/!(*groupDuplication|workspaceConstants).cy.js",
"!cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js",
"cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
],
numTestsKeptInMemory: 1,

View file

@ -572,6 +572,7 @@
"styles": "Styles",
"general": "General",
"validation": "Validation",
"structure": "Structure",
"documentation": "Read documentation for {{componentMeta}}",
"widgetNameEmptyError": "Widget name cannot be empty",
"componentNameExistsError": "Component name already exists",

@ -1 +1 @@
Subproject commit aa521205455afd59e85762716a0012c1e44986e1
Subproject commit 9da4f776915e328120c3024e551ef6b8032f9f63

View file

@ -63,6 +63,7 @@
"dotenv": "^16.0.3",
"draft-js": "^0.11.7",
"draft-js-export-html": "^1.4.1",
"draft-js-import-html": "^1.4.1",
"driver.js": "^0.9.8",
"emoji-mart": "^5.5.2",
"file-loader": "^6.2.0",
@ -105,7 +106,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-dropzone": "^14.3.8",
"react-highlight-words": "^0.21.0",
"react-hot-toast": "^2.4.0",
"react-hotkeys-hook": "^4.3.5",
@ -16948,6 +16949,31 @@
"immutable": "3.x.x"
}
},
"node_modules/draft-js-import-element": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz",
"integrity": "sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg==",
"dependencies": {
"draft-js-utils": "^1.4.0",
"synthetic-dom": "^1.4.0"
},
"peerDependencies": {
"draft-js": ">=0.10.0",
"immutable": "3.x.x"
}
},
"node_modules/draft-js-import-html": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/draft-js-import-html/-/draft-js-import-html-1.4.1.tgz",
"integrity": "sha512-KOZmtgxZriCDgg5Smr3Y09TjubvXe7rHPy/2fuLSsL+aSzwUDwH/aHDA/k47U+WfpmL4qgyg4oZhqx9TYJV0tg==",
"dependencies": {
"draft-js-import-element": "^1.4.0"
},
"peerDependencies": {
"draft-js": ">=0.10.0",
"immutable": "3.x.x"
}
},
"node_modules/draft-js-utils": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.4.1.tgz",
@ -32645,6 +32671,11 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/synthetic-dom": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.4.0.tgz",
"integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg=="
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",

View file

@ -101,7 +101,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-dropzone": "^14.3.8",
"react-highlight-words": "^0.21.0",
"react-hot-toast": "^2.4.0",
"react-hotkeys-hook": "^4.3.5",

View file

@ -14,6 +14,7 @@ import EditorHeader from '@/AppBuilder/Header';
import LeftSidebar from '@/AppBuilder/LeftSidebar';
import Popups from './Popups';
import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
import RightSidebarToggle from '@/AppBuilder/RightSideBar/RightSidebarToggle';
import { shallow } from 'zustand/shallow';
// const EditorHeader = lazy(() => import('@/AppBuilder/Header'));
@ -25,6 +26,7 @@ import { shallow } from 'zustand/shallow';
// TODO: split Loader into separate component and remove editor loading state from Editor
export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMode, appType = 'front-end' }) => {
useAppData(appId, moduleId, darkMode);
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen);
const isEditorLoading = useStore((state) => state.loaderStore.modules[moduleId].isEditorLoading, shallow);
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
const isModuleEditor = appType === 'module';
@ -54,9 +56,10 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
</Suspense>
{window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && <RealtimeCursors />}
<DndProvider backend={HTML5Backend}>
<AppCanvas appId={appId} />
<AppCanvas moduleId={moduleId} appId={appId} switchDarkMode={switchDarkMode} darkMode={darkMode} />
<QueryPanel darkMode={darkMode} />
<RightSideBar darkMode={darkMode} />
<RightSidebarToggle darkMode={darkMode} />
{isRightSidebarOpen && <RightSideBar darkMode={darkMode} />}{' '}
</DndProvider>
<Popups darkMode={darkMode} />
</ModuleProvider>

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { Container } from './Container';
import Grid from './Grid';
import { EditorSelecto } from './Selecto';
@ -17,9 +17,13 @@ import useAppDarkMode from '@/_hooks/useAppDarkMode';
import useAppCanvasMaxWidth from './useAppCanvasMaxWidth';
import { DeleteWidgetConfirmation } from './DeleteWidgetConfirmation';
import useSidebarMargin from './useSidebarMargin';
import PagesSidebarNavigation from '../RightSideBar/PageSettingsTab/PageMenu/PagesSidebarNavigation';
import { resolveReferences } from '@/_helpers/utils';
import useRightSidebarMargin from './userRightSidebarMargin';
import { DragGhostWidget } from './GhostWidgets';
import AppCanvasBanner from '../../AppBuilder/Header/AppCanvasBanner';
export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) => {
export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode }) => {
const { moduleId, isModuleMode, appType } = useModuleContext();
const canvasContainerRef = useRef();
const handleCanvasContainerMouseUp = useStore((state) => state.handleCanvasContainerMouseUp, shallow);
@ -42,9 +46,33 @@ export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) =>
const setIsComponentLayoutReady = useStore((state) => state.setIsComponentLayoutReady, shallow);
const canvasMaxWidth = useAppCanvasMaxWidth({ mode: currentMode });
const editorMarginLeft = useSidebarMargin(canvasContainerRef);
const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow);
// const editorMarginRight = useRightSidebarMargin(canvasContainerRef);
// const isPagesSidebarHidden = useStore((state) => state.getPagesSidebarVisibility('canvas'), shallow);
const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
const getPageId = useStore((state) => state.getCurrentPageId, shallow);
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned, shallow);
const currentPageId = useStore((state) => state.modules[moduleId].currentPageId);
const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId);
const [isViewerSidebarPinned, setIsSidebarPinned] = useState(
localStorage.getItem('isPagesSidebarPinned') !== 'false'
);
const { globalSettings, pages, pageSettings, switchPage } = useStore(
(state) => ({
globalSettings: state.globalSettings,
pages: state.modules.canvas.pages,
pageSettings: state.pageSettings,
switchPage: state.switchPage,
}),
shallow
);
const showHeader = !globalSettings?.hideHeader;
const { definition: { styles = {}, properties = {} } = {} } = pageSettings ?? {};
const { position, disableMenu, showOnDesktop } = properties ?? {};
const isPagesSidebarHidden = resolveReferences(disableMenu?.value);
const hideSidebar = isModuleMode || isPagesSidebarHidden || appType === 'module';
@ -79,9 +107,11 @@ export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) =>
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, [currentLayout, canvasMaxWidth, isViewerSidebarPinned, moduleId]);
}, [currentLayout, canvasMaxWidth, isViewerSidebarPinned, moduleId, isRightSidebarOpen]);
const styles = useMemo(() => {
useEffect(() => {}, [isViewerSidebarPinned]);
const canvasContainerStyles = useMemo(() => {
const canvasBgColor =
currentMode === 'view'
? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor)
@ -101,24 +131,37 @@ export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) =>
borderLeft: currentMode === 'edit' && editorMarginLeft + 'px solid',
height: currentMode === 'edit' ? canvasContainerHeight : '100%',
background: canvasBgColor,
marginLeft:
isViewerSidebarPinned && !hideSidebar && currentLayout !== 'mobile' && currentMode !== 'edit'
? pageSidebarStyle === 'icon'
? '65px'
: '210px'
: 'auto',
width: currentMode === 'edit' ? `calc(100% - 96px)` : '100%',
alignItems: 'unset',
justifyContent: 'unset',
borderRight: currentMode === 'edit' && isRightSidebarOpen && '299' + 'px solid',
padding: currentMode === 'edit' && '8px',
paddingBottom: currentMode === 'edit' && '2px',
};
}, [
currentMode,
isAppDarkMode,
isModuleMode,
editorMarginLeft,
canvasContainerHeight,
isViewerSidebarPinned,
hideSidebar,
currentLayout,
pageSidebarStyle,
]);
}, [currentMode, isAppDarkMode, isModuleMode, editorMarginLeft, canvasContainerHeight, isRightSidebarOpen]);
const toggleSidebarPinned = useCallback(() => {
const newValue = !isViewerSidebarPinned;
setIsSidebarPinned(newValue);
localStorage.setItem('isPagesSidebarPinned', JSON.stringify(newValue));
}, [isViewerSidebarPinned]);
function getMinWidth() {
if (isModuleMode) return '100%';
const shouldAdjust = isSidebarOpen || (isRightSidebarOpen && currentMode === 'edit');
if (!shouldAdjust) return '';
let offset;
if (isViewerSidebarPinned) {
offset = position === 'side' ? '352px' : '126px';
} else {
offset = position === 'side' ? '171px' : '126px';
}
return `calc(100vw - ${offset})`;
}
return (
<div
@ -127,48 +170,71 @@ export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) =>
onMouseUp={handleCanvasContainerMouseUp}
>
<AppCanvasBanner appId={appId} />
<div
ref={canvasContainerRef}
className={cx(
'canvas-container align-items-center page-container',
{ 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned },
{ 'overflow-x-auto': (currentMode === 'edit' && isSidebarOpen) || currentMode === 'view' },
{ 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode
)}
style={styles}
>
<div id="sidebar-page-navigation" className="areas d-flex flex-rows">
<div
style={{
minWidth: isModuleMode ? '100%' : `calc((100vw - 300px) - 48px)`,
}}
className={`app-${appId} _tooljet-page-${getPageId()}`}
>
{currentMode === 'edit' && (
<AutoComputeMobileLayoutAlert currentLayout={currentLayout} darkMode={isAppDarkMode} />
ref={canvasContainerRef}
className={cx(
'canvas-container d-flex page-container',
{ 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned },
{ 'overflow-x-auto': currentMode === 'edit' },
{ 'position-top': position === 'top' },
{ 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode
)}
<DeleteWidgetConfirmation darkMode={isAppDarkMode} />
<HotkeyProvider mode={currentMode} canvasMaxWidth={canvasMaxWidth} currentLayout={currentLayout}>
{environmentLoadingState !== 'loading' && (
<div>
<Container
id={moduleId}
gridWidth={gridWidth}
canvasWidth={canvasWidth}
canvasHeight={canvasHeight}
darkMode={isAppDarkMode}
canvasMaxWidth={canvasMaxWidth}
isViewerSidebarPinned={isViewerSidebarPinned}
pageSidebarStyle={pageSidebarStyle}
appType={appType}
/>
{appType !== 'module' && <div id="component-portal" />}
</div>
style={canvasContainerStyles}
>
{showOnDesktop && (
<PagesSidebarNavigation
showHeader={showHeader}
isMobileDevice={currentLayout === 'mobile'}
pages={pages}
currentPageId={currentPageId ?? homePageId}
switchPage={switchPage}
height={currentMode === 'edit' ? canvasContainerHeight : '100%'}
switchDarkMode={switchDarkMode}
isSidebarPinned={isViewerSidebarPinned}
toggleSidebarPinned={toggleSidebarPinned}
darkMode={darkMode}
/>
)}
<div
style={{
minWidth: getMinWidth(),
scrollbarWidth: 'none',
overflow: 'auto',
width: currentMode === 'view' ? `calc(100% - ${isViewerSidebarPinned ? '0px' : '0px'})` : '100%',
}}
className={`app-${appId} _tooljet-page-${getPageId()}`}
>
{currentMode === 'edit' && (
<AutoComputeMobileLayoutAlert currentLayout={currentLayout} darkMode={isAppDarkMode} />
)}
<DeleteWidgetConfirmation darkMode={isAppDarkMode} />
<HotkeyProvider mode={currentMode} canvasMaxWidth={canvasMaxWidth} currentLayout={currentLayout}>
{environmentLoadingState !== 'loading' && (
<div>
<Container
id="canvas"
gridWidth={gridWidth}
canvasWidth={canvasWidth}
canvasHeight={canvasHeight}
darkMode={isAppDarkMode}
canvasMaxWidth={canvasMaxWidth}
isViewerSidebarPinned={isViewerSidebarPinned}
pageSidebarStyle={pageSidebarStyle}
pagePositionType={position}
appType={appType}
/>
<DragGhostWidget />
<div id="component-portal" />
{appType !== 'module' && <div id="component-portal" />}
</div>
)}
{currentMode === 'view' || (currentLayout === 'mobile' && isAutoMobileLayout) ? null : (
<Grid currentLayout={currentLayout} gridWidth={gridWidth} />
)}
</HotkeyProvider>
{currentMode === 'view' || (currentLayout === 'mobile' && isAutoMobileLayout) ? null : (
<Grid currentLayout={currentLayout} gridWidth={gridWidth} />
)}
</HotkeyProvider>
</div>
</div>
</div>
{currentMode === 'edit' && <EditorSelecto />}

View file

@ -4,6 +4,7 @@ import './configHandle.scss';
import useStore from '@/AppBuilder/_stores/store';
import { findHighestLevelofSelection } from '../Grid/gridUtils';
import SolidIcon from '@/_ui/Icon/solidIcons/index';
import { ToolTip } from '@/_components/ToolTip';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { DROPPABLE_PARENTS } from '../appCanvasConstants';
@ -52,7 +53,40 @@ export const ConfigHandle = ({
);
}, shallow);
const currentPageIndex = useStore((state) => state.modules.canvas.currentPageIndex);
const component = useStore((state) => state.modules.canvas.pages[currentPageIndex].components[id]);
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
const isRestricted = component.permissions && component.permissions.length !== 0;
const draggingComponentId = useStore((state) => state.draggingComponentId);
let height = visibility === false ? 10 : widgetHeight;
const getTooltip = () => {
const permission = component.permissions?.[0];
if (!permission) return null;
const users = permission.groups || permission.users || [];
if (users.length === 0) return null;
const isSingle = permission.type === 'SINGLE';
const isGroup = permission.type === 'GROUP';
if (isSingle) {
return users.length === 1
? `Access restricted to ${users[0].user.email}`
: `Access restricted to ${users.length} users`;
}
if (isGroup) {
return users.length === 1
? `Access restricted to ${users[0].permission_group?.name || users[0].permissionGroup?.name} group`
: `Access restricted to ${users.length} user groups`;
}
return null;
};
return (
<div
className={`config-handle ${customClassName}`}
@ -78,6 +112,22 @@ export const ConfigHandle = ({
}
}}
>
{licenseValid && isRestricted && (
<ToolTip message={getTooltip()} show={licenseValid && isRestricted && !draggingComponentId}>
<span
style={{
background:
visibility === false ? '#c6cad0' : componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
border: position === 'bottom' ? '1px solid white' : 'none',
color: visibility === false && 'var(--text-placeholder)',
marginRight: '4px',
}}
className="badge handle-content"
>
<SolidIcon width="12" name="lock" fill="var(--icon-on-solid)" />
</span>
</ToolTip>
)}
<span
style={{
background:

View file

@ -10,6 +10,7 @@ import {
addNewWidgetToTheEditor,
computeViewerBackgroundColor,
getSubContainerWidthAfterPadding,
addDefaultButtonIdToForm,
} from './appCanvasUtils';
import {
CANVAS_WIDTHS,
@ -52,6 +53,7 @@ export const Container = React.memo(
canvasMaxWidth,
isViewerSidebarPinned,
pageSidebarStyle,
pagePositionType,
componentType,
appType,
}) => {
@ -84,18 +86,12 @@ export const Container = React.memo(
item.canvasWidth = getContainerCanvasWidth();
},
drop: async ({ componentType, component }, monitor) => {
setShowModuleBorder(false); // Hide the module border when dropping
setShowModuleBorder(false);
if (currentMode === 'view' || (appType === 'module' && componentType !== 'ModuleContainer')) return;
const didDrop = monitor.didDrop();
if (didDrop) return;
if (componentType === 'PDF' && !isPDFSupported()) {
toast.error(
'PDF is not supported in this version of browser. We recommend upgrading to the latest version for full support.'
);
return;
}
// IMPORTANT: This logic needs to be changed when we implement the module versioning
const moduleInfo = component?.moduleId
? {
moduleId: component.moduleId,
@ -106,8 +102,10 @@ export const Container = React.memo(
}
: undefined;
let addedComponent;
if (WIDGETS_WITH_DEFAULT_CHILDREN.includes(componentType)) {
const parentComponent = addNewWidgetToTheEditor(
let parentComponent = addNewWidgetToTheEditor(
componentType,
monitor,
currentLayout,
@ -116,10 +114,11 @@ export const Container = React.memo(
moduleInfo
);
const childComponents = addChildrenWidgetsToParent(componentType, parentComponent?.id, currentLayout);
const newComponents = [parentComponent, ...childComponents];
await addComponentToCurrentPage(newComponents);
// setSelectedComponents([parentComponent?.id]);
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
if (componentType === 'Form') {
parentComponent = addDefaultButtonIdToForm(parentComponent, childComponents);
}
addedComponent = [parentComponent, ...childComponents];
await addComponentToCurrentPage(addedComponent);
} else {
const newComponent = addNewWidgetToTheEditor(
componentType,
@ -129,11 +128,32 @@ export const Container = React.memo(
id,
moduleInfo
);
await addComponentToCurrentPage([newComponent]);
// setSelectedComponents([newComponent?.id]);
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
addedComponent = [newComponent];
await addComponentToCurrentPage(addedComponent);
}
setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
const canvas = document.querySelector('.canvas-container');
const sidebar = document.querySelector('.editor-sidebar');
const droppedElem = document.getElementById(addedComponent?.[0]?.id);
if (!canvas || !sidebar || !droppedElem) return;
const droppedRect = droppedElem.getBoundingClientRect();
const sidebarRect = sidebar.getBoundingClientRect();
const isOverlapping = droppedRect.right > sidebarRect.left && droppedRect.left < sidebarRect.right;
if (isOverlapping) {
const overlap = droppedRect.right - sidebarRect.left;
canvas.scrollTo({
left: canvas.scrollLeft + overlap,
behavior: 'smooth',
});
}
},
collect: (monitor) => ({
isOverCurrent: monitor.isOver({ shallow: true }),
}),
@ -149,10 +169,11 @@ export const Container = React.memo(
if (canvasWidth !== undefined) {
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
if (id === 'canvas') return canvasWidth;
return getSubContainerWidthAfterPadding(canvasWidth, componentType, id);
return getSubContainerWidthAfterPadding(canvasWidth, componentType, id, realCanvasRef);
}
return realCanvasRef?.current?.offsetWidth;
}
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
useEffect(() => {
useGridStore.getState().actions.setSubContainerWidths(id, gridWidth);
@ -160,18 +181,27 @@ export const Container = React.memo(
}, [canvasWidth, listViewMode, columns]);
const getCanvasWidth = useCallback(() => {
if (
id === 'canvas' &&
!isPagesSidebarHidden &&
isViewerSidebarPinned &&
currentLayout !== 'mobile' &&
currentMode !== 'edit' &&
appType !== 'module'
) {
return `calc(100% - ${pageSidebarStyle === 'icon' ? '65px' : '210px'})`;
}
// if (
// id === 'canvas' &&
// !isPagesSidebarHidden &&
// isViewerSidebarPinned &&
// currentLayout !== 'mobile' &&
// pagePositionType == 'side' &&
// appType !== 'module'
// ) {
// return `calc(100% - ${pageSidebarStyle === 'icon' ? '85px' : '226px'})`;
// }
// if (
// id === 'canvas' &&
// !isPagesSidebarHidden &&
// !isViewerSidebarPinned &&
// currentLayout !== 'mobile' &&
// pagePositionType == 'side'
// ) {
// return `calc(100% - ${'44px'})`;
// }
return '100%';
}, [isViewerSidebarPinned, currentLayout, id, currentMode, pageSidebarStyle]);
}, [id, isPagesSidebarHidden, isViewerSidebarPinned, currentLayout, pagePositionType, pageSidebarStyle]);
const handleCanvasClick = useCallback(
(e) => {
@ -223,7 +253,7 @@ export const Container = React.memo(
: id === 'canvas'
? canvasBgColor
: '#f0f0f0',
width: getCanvasWidth(),
width: '100%',
maxWidth: (() => {
// For Main Canvas
if (id === 'canvas') {

View file

@ -1,17 +1,24 @@
import React from 'react';
import useStore from '@/AppBuilder/_stores/store';
export const DragGhostWidget = () => {
const draggingComponentId = useStore((state) => state.draggingComponentId);
if (!draggingComponentId) return null;
export const DragGhostWidget = ({ isDragging }) => {
if (!isDragging) return '';
return (
<div
id={'moveable-drag-ghost'}
id="moveable-drag-ghost"
style={{
zIndex: 4,
position: 'absolute',
background: '#D9E2FC',
opacity: '0.7',
pointerEvents: 'none',
left: 0,
top: 0,
}}
></div>
/>
);
};

View file

@ -1,176 +1,207 @@
.target, .nested-target {
position: absolute;
box-sizing: border-box;
.target,
.nested-target {
position: absolute;
box-sizing: border-box;
}
.target.hovered{
z-index: 2;
.target.hovered {
z-index: 2;
}
.moveable-control-box>.moveable-control-box:not(.moveable-control-box-d-block, .moveable-dragging, .selected-component){
visibility: hidden !important;
}
.moveable-control-box>.moveable-control-box:hover, .selected-component{
visibility: visible !important;
}
.moveable-control-box>.moveable-control-box:hover, .moveable-control-box>.moveable-dragging{
visibility: visible !important;
}
.moveable-control-box.modal-moveable{
z-index: 3001 !important;
.moveable-control-box
> .moveable-control-box:not(
.moveable-control-box-d-block,
.moveable-dragging,
.selected-component
) {
visibility: hidden !important;
}
.moveable-control-box > .moveable-control-box:hover,
.selected-component {
visibility: visible !important;
}
.moveable-e.moveable-control{
/* height: 24px !important;
.moveable-control-box > .moveable-control-box:hover,
.moveable-control-box > .moveable-dragging {
visibility: visible !important;
}
.moveable-control-box.modal-moveable {
z-index: 3001 !important;
}
.moveable-e.moveable-control {
/* height: 24px !important;
top: -5px !important; */
border-radius: 2px !important;
border: 1px solid #3E63DD !important;
background: #fff !important;
width: 6px !important;
left: 4px !important;
border-radius: 2px !important;
border: 1px solid #3e63dd !important;
background: #fff !important;
width: 6px !important;
left: 4px !important;
}
.moveable-w.moveable-control{
/* height: 24px !important;
.moveable-w.moveable-control {
/* height: 24px !important;
top: -5px !important; */
border-radius: 2px !important;
border: 1px solid #3E63DD !important;
background: #fff !important;
width: 6px !important;
left: 4px !important;
border-radius: 2px !important;
border: 1px solid #3e63dd !important;
background: #fff !important;
width: 6px !important;
left: 4px !important;
}
.moveable-n.moveable-control{
/* height: 24px !important; */
top: 4px !important;
border-radius: 2px !important;
border: 1px solid #3E63DD !important;
background: #fff !important;
height: 6px !important;
/* left: 3px !important; */
.moveable-horizontal-only {
.moveable-direction.moveable-w:not(.moveable-edge),
.moveable-direction.moveable-e:not(.moveable-edge) {
height: 20px !important;
width: 7.5px !important;
opacity: 1 !important;
background-color: #fff !important;
border-radius: 10px !important;
}
.moveable-direction.moveable-w:not(.moveable-edge) {
left: 1px !important;
top: -6.5px !important;
}
.moveable-direction.moveable-e:not(.moveable-edge) {
left: 1px !important;
top: -6.5px !important;
}
}
.moveable-s.moveable-control{
/* height: 24px !important; */
top: 4px !important;
border-radius: 2px !important;
border: 1px solid #3E63DD !important;
background: #fff !important;
height: 6px !important;
/* left: 3px !important; */
.moveable-n.moveable-control {
/* height: 24px !important; */
top: 4px !important;
border-radius: 2px !important;
border: 1px solid #3e63dd !important;
background: #fff !important;
height: 6px !important;
/* left: 3px !important; */
}
.moveable-s.moveable-control {
/* height: 24px !important; */
top: 4px !important;
border-radius: 2px !important;
border: 1px solid #3e63dd !important;
background: #fff !important;
height: 6px !important;
/* left: 3px !important; */
}
.grid-guide-lines {
background: #8DA4EF !important;
background: #8da4ef !important;
}
.moveable-control-box:not([data-able-groupable]) .moveable-control-box:not(:hover) {
opacity: 0;
.moveable-control-box:not([data-able-groupable])
.moveable-control-box:not(:hover) {
opacity: 0;
}
.dragged-movable-control-box, [data-hovered-control="true"] {
opacity: 1 !important;
.dragged-movable-control-box,
[data-hovered-control="true"] {
opacity: 1 !important;
}
.moveable-line.moveable-e,
.moveable-line.moveable-w {
border: 5px solid #fff0;
border: 5px solid #fff0;
}
.moveable-line.moveable-n {
border-bottom: 5px solid #fff0;
border-bottom: 5px solid #fff0;
}
.moveable-line.moveable-s {
border-bottom: 5px solid #fff0;
border-bottom: 5px solid #fff0;
}
.moveable-control[data-rotation="0"], .moveable-control[data-rotation="90"],
.moveable-around-control[data-rotation="0"], .moveable-around-control[data-rotation="90"] {
opacity: 0;
width: 0px !important;
height: 0px !important;
.moveable-control[data-rotation="0"],
.moveable-control[data-rotation="90"],
.moveable-around-control[data-rotation="0"],
.moveable-around-control[data-rotation="90"] {
opacity: 0;
width: 0px !important;
height: 0px !important;
}
.moveable-control {
width: 8px !important;
height: 8px !important;
border: 1px solid var(--moveable-color) !important;
background: #fff !important;
margin-top: -4px !important;
margin-left: -4px !important;
width: 8px !important;
height: 8px !important;
border: 1px solid var(--moveable-color) !important;
background: #fff !important;
margin-top: -4px !important;
margin-left: -4px !important;
}
.moveable-around-control{
height: 10px !important;
width: 10px !important;
.moveable-around-control {
height: 10px !important;
width: 10px !important;
}
.moveable-around-control[data-direction*="nw"] {
left: -11px;
top: -11px;
left: -11px;
top: -11px;
}
.moveable-around-control[data-direction*="ne"] {
top: -11px;
top: -11px;
}
.moveable-around-control[data-direction*="ne"] {
top: -11px;
top: -11px;
}
.moveable-around-control[data-direction*="sw"] {
left: -11px;
top: -1px;
left: -11px;
top: -1px;
}
.moveable-draggable-dragging {
opacity: 1 !important;
opacity: 1 !important;
}
[data-off-screen="true"] {
display: none;
display: none;
}
.moveable-guideline {
background: #97AEFC !important;
opacity: 0.8;
z-index: 9999;
background: #97aefc !important;
opacity: 0.8;
z-index: 9999;
}
.moveable-guideline.moveable-horizontal {
height: 1px !important;
width: 100% !important;
background: #97AEFC !important;
left: 0 !important;
height: 1px !important;
width: 100% !important;
background: #97aefc !important;
left: 0 !important;
}
.moveable-guideline.moveable-vertical {
width: 1px !important;
height: 100% !important;
background: #97AEFC !important;
top: 0 !important;
width: 1px !important;
height: 100% !important;
background: #97aefc !important;
top: 0 !important;
}
.moveable-guideline-group {
z-index: 9999;
z-index: 9999;
}
.dragging-component-canvas {
outline: 1px solid var(--border-accent-strong) !important;
outline-offset: 0px; /* Creates space between element and outline */
z-index: 999 !important;
outline: 1px solid var(--border-accent-strong) !important;
outline-offset: 0px;
/* Creates space between element and outline */
z-index: 999 !important;
}
.non-dragging-component {
outline: 1px dotted var(--border-accent-weak) !important;
outline-offset: 0px; /* Creates space between element and outline */
z-index: 999 !important;
.non-dragging-component {
outline: 1px dotted var(--border-accent-weak) !important;
outline-offset: 0px;
/* Creates space between element and outline */
z-index: 999 !important;
}

View file

@ -24,10 +24,13 @@ import {
handleActivateNonDraggingComponents,
computeScrollDelta,
computeScrollDeltaOnDrag,
getDraggingWidgetWidth,
positionDragGhostWidget,
} from './gridUtils';
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
import useStore from '@/AppBuilder/_stores/store';
import './Grid.css';
import { useGroupedTargetsScrollHandler } from './hooks/useGroupedTargetsScrollHandler';
import { DROPPABLE_PARENTS, NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, position: 'css' };
@ -35,6 +38,12 @@ const RESIZABLE_CONFIG = {
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
};
const HORIZONTAL_CONFIG = {
edge: ['e', 'w'],
renderDirections: ['w', 'e'],
};
export const GRID_HEIGHT = 10;
export default function Grid({ gridWidth, currentLayout }) {
@ -49,8 +58,9 @@ export default function Grid({ gridWidth, currentLayout }) {
const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow);
const getComponentTypeFromId = useStore((state) => state.getComponentTypeFromId, shallow);
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
const temporaryHeight = useStore((state) => state.temporaryLayouts?.[selectedComponents?.[0]]?.height, shallow);
const isGroupHandleHoverd = useIsGroupHandleHoverd();
const checkHoveredComponentDynamicHeight = useStore((state) => state.checkHoveredComponentDynamicHeight, shallow);
const openModalWidgetId = useOpenModalWidgetId();
const moveableRef = useRef(null);
const triggerCanvasUpdater = useStore((state) => state.triggerCanvasUpdater, shallow);
@ -60,9 +70,10 @@ export default function Grid({ gridWidth, currentLayout }) {
const canvasWidth = NO_OF_GRIDS * gridWidth;
const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow);
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
const getTemporaryLayouts = useStore((state) => state.getTemporaryLayouts, shallow);
const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow);
const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
const draggingComponentId = useStore((state) => state.draggingComponentId, shallow);
const draggingComponentId = useGridStore((state) => state.draggingComponentId, shallow);
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
const [dragParentId, setDragParentId] = useState(null);
const [elementGuidelines, setElementGuidelines] = useState([]);
@ -73,6 +84,8 @@ export default function Grid({ gridWidth, currentLayout }) {
const checkIfAnyWidgetVisibilityChanged = useStore((state) => state.checkIfAnyWidgetVisibilityChanged(), shallow);
const getExposedValueOfComponent = useStore((state) => state.getExposedValueOfComponent, shallow);
const setReorderContainerChildren = useStore((state) => state.setReorderContainerChildren, shallow);
const [isVerticalExpansionRestricted, setIsVerticalExpansionRestricted] = useState(false);
const toggleRightSidebar = useStore((state) => state.toggleRightSidebar, shallow);
useEffect(() => {
const selectedSet = new Set(selectedComponents);
@ -121,6 +134,7 @@ export default function Grid({ gridWidth, currentLayout }) {
top: widget?.layouts?.[currentLayout]?.top,
width: widget?.layouts?.[currentLayout]?.width,
parent: widget?.component?.parent,
componentType: widget?.component?.component,
component: widget?.component,
};
})
@ -143,24 +157,26 @@ export default function Grid({ gridWidth, currentLayout }) {
const handleResizeStop = useCallback(
(boxList) => {
const transformedBoxes = boxList.reduce((acc, box) => {
acc[box.id] = box;
return acc;
}, {});
const temporaryLayouts = getTemporaryLayouts();
boxList.forEach(({ id, height, width, x, y, gw }) => {
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
// Consider temporary layout position if it exists
const temporaryLayout = temporaryLayouts[id];
y = temporaryLayout?.top ?? Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
gw = gw ? gw : gridWidth;
const parent = transformedBoxes[id]?.component?.parent;
const parent = boxList.find((box) => box.id === id)?.component?.parent;
if (y < 0) {
y = 0;
}
if (parent) {
const parentElem = document.getElementById(`canvas-${parent}`);
const parentId = parent.includes('-') ? parent?.split('-').slice(0, -1).join('-') : parent;
const componentType = transformedBoxes.find((box) => box.id === parentId)?.component.component;
const componentType = boxList.find((box) => box.id === parentId)?.component.component;
var parentHeight = parentElem?.clientHeight || height;
if (height > parentHeight && ['Tabs', 'Listview'].includes(componentType)) {
height = parentHeight;
@ -253,10 +269,16 @@ export default function Grid({ gridWidth, currentLayout }) {
}
e.props.target.classList.add('hovered');
e.controlBox.classList.add('moveable-control-box-d-block');
const isHorizontallyExpandable = checkHoveredComponentDynamicHeight();
if (isHorizontallyExpandable) {
e.controlBox.classList.add('moveable-horizontal-only');
}
setIsVerticalExpansionRestricted(!!isHorizontallyExpandable);
},
mouseLeave(e) {
e.props.target.classList.remove('hovered');
e.controlBox.classList.remove('moveable-control-box-d-block');
e.controlBox.classList.remove('moveable-horizonta-only');
},
};
@ -321,6 +343,11 @@ export default function Grid({ gridWidth, currentLayout }) {
const groupedTargets = [...findHighestLevelofSelection().map((component) => '.ele-' + component.id)];
useEffect(() => {
if (moveableRef.current) {
moveableRef.current.updateTarget();
}
}, [temporaryHeight]);
useEffect(() => {
reloadGrid();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -580,6 +607,8 @@ export default function Grid({ gridWidth, currentLayout }) {
}
}, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]);
useGroupedTargetsScrollHandler(groupedTargets, boxList, moveableRef);
if (mode !== 'edit') return null;
return (
@ -598,23 +627,34 @@ export default function Grid({ gridWidth, currentLayout }) {
origin={false}
individualGroupable={groupedTargets.length <= 1}
draggable={!shouldFreeze && mode !== 'view'}
resizable={!shouldFreeze ? RESIZABLE_CONFIG : false && mode !== 'view'}
resizable={
!shouldFreeze
? isVerticalExpansionRestricted
? HORIZONTAL_CONFIG
: RESIZABLE_CONFIG
: false && mode !== 'view'
}
keepRatio={false}
individualGroupableProps={individualGroupableProps}
onResize={(e) => {
const temporaryLayouts = getTemporaryLayouts();
if (resizingComponentId !== e.target.id) {
useGridStore.getState().actions.setResizingComponentId(e.target.id);
showGridLines();
}
const currentWidget = boxList.find(({ id }) => id === e.target.id);
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
// Show grid during resize
if (currentWidget.component?.parent) {
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
setDragParentId(currentWidget.component?.parent);
} else {
document.getElementById('real-canvas').classList.add('show-grid');
}
handleActivateTargets(currentWidget.component?.parent);
const currentWidth = currentWidget.width * _gridWidth;
const diffWidth = e.width - currentWidth;
@ -622,20 +662,30 @@ export default function Grid({ gridWidth, currentLayout }) {
const isLeftChanged = e.direction[0] === -1;
const isTopChanged = e.direction[1] === -1;
// Calculate positions considering temporary layouts'
let transformX = currentWidget.left * _gridWidth;
let transformY = currentWidget.top;
let transformY = temporaryLayouts[currentWidget.id]?.top ?? currentWidget.top;
if (isLeftChanged) {
transformX = currentWidget.left * _gridWidth - diffWidth;
// Left resize
transformX = transformX - diffWidth;
}
if (isTopChanged) {
transformY = currentWidget.top - diffHeight;
// Top resize
transformY = transformY - diffHeight;
}
// Apply container bounds
const elemContainer = e.target.closest('.real-canvas');
const containerHeight = elemContainer.clientHeight;
const containerWidth = elemContainer.clientWidth;
const maxY = containerHeight - e.target.clientHeight;
const maxLeft = containerWidth - e.target.clientWidth;
transformY = Math.max(0, Math.min(transformY, maxY));
transformX = Math.max(0, Math.min(transformX, maxLeft));
// Update element style
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
const maxHeightHit = transformY < 0 || transformY >= maxY;
if (!maxWidthHit || e.width < e.target.clientWidth) {
@ -645,14 +695,8 @@ export default function Grid({ gridWidth, currentLayout }) {
e.target.style.height = `${e.height}px`;
}
e.target.style.transform = `translate(${transformX}px, ${transformY}px)`;
// Postion ghost element exactly with respect to resizing element
if (document.getElementById('resize-ghost-widget')) {
document.getElementById(
'resize-ghost-widget'
).style.transform = `translate(${transformX}px, ${transformY}px)`;
document.getElementById('resize-ghost-widget').style.width = `${e.target.clientWidth}px`;
document.getElementById('resize-ghost-widget').style.height = `${e.target.clientHeight}px`;
}
if (e.width > 0) e.target.style.width = `${e.width}px`;
if (e.height > 0) e.target.style.height = `${e.height}px`;
}}
onResizeStart={(e) => {
if (
@ -827,6 +871,7 @@ export default function Grid({ gridWidth, currentLayout }) {
if (getHoveredComponentForGrid() !== e.target.id) {
return false;
}
toggleRightSidebar();
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
@ -949,6 +994,9 @@ export default function Grid({ gridWidth, currentLayout }) {
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
const draggingWidgetWidth = getDraggingWidgetWidth(_dragParentId, e.target.clientWidth);
e.target.style.width = `${draggingWidgetWidth}px`;
// This logic is to handle the case when the dragged element is over a new canvas
if (_dragParentId !== currentParentId) {
left = e.translate[0];
@ -1014,6 +1062,7 @@ export default function Grid({ gridWidth, currentLayout }) {
} else if (parentComponent?.component?.component === 'Modal') {
// Never update parentId for Modal
newParentId = parentComponent?.id;
e.target.style.width = `${e.target.clientWidth}px`;
}
if (newParentId !== prevDragParentId.current) {
@ -1034,12 +1083,7 @@ export default function Grid({ gridWidth, currentLayout }) {
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
);
// Postion ghost element exactly as same at dragged element
if (document.getElementById(`moveable-drag-ghost`)) {
document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`;
document.getElementById(`moveable-drag-ghost`).style.width = `${e.target.clientWidth}px`;
document.getElementById(`moveable-drag-ghost`).style.height = `${e.target.clientHeight}px`;
}
positionDragGhostWidget(e.target);
}}
onDragGroup={(ev) => {
const { events } = ev;

View file

@ -2,6 +2,8 @@ import { useGridStore } from '@/_stores/gridStore';
import { isEmpty } from 'lodash';
import useStore from '@/AppBuilder/_stores/store';
import { getTabId, getSubContainerIdWithSlots } from '../appCanvasUtils';
import { NO_OF_GRIDS } from '../appCanvasConstants';
export function correctBounds(layout, bounds) {
layout = scaleLayouts(layout);
const collidesWith = [];
@ -517,3 +519,35 @@ export const computeScrollDelta = ({ source }) => {
};
export const computeScrollDeltaOnDrag = computeScrollDelta;
export const getDraggingWidgetWidth = (canvasParentId, widgetWidth) => {
const targetCanvasWidth =
document.getElementById(`canvas-${canvasParentId}`)?.offsetWidth ||
document.getElementById('real-canvas')?.offsetWidth;
const gridUnitWidth = targetCanvasWidth / NO_OF_GRIDS;
const gridUnits = Math.round(widgetWidth / gridUnitWidth);
const draggingWidgetWidth = gridUnits * gridUnitWidth;
return draggingWidgetWidth;
};
export const positionDragGhostWidget = (draggedElement) => {
const ghostElement = document.getElementById('moveable-drag-ghost');
if (!ghostElement || !draggedElement) return;
const mainCanvas = document.getElementById('real-canvas');
if (!mainCanvas) return;
const mainCanvasRect = mainCanvas.getBoundingClientRect();
const draggedRect = draggedElement.getBoundingClientRect();
// Calculate position relative to main canvas
const relativeLeft = draggedRect.left - mainCanvasRect.left;
const relativeTop = draggedRect.top - mainCanvasRect.top;
// Apply the position
ghostElement.style.left = `${relativeLeft}px`;
ghostElement.style.top = `${relativeTop}px`;
ghostElement.style.width = `${draggedRect.width}px`;
ghostElement.style.height = `${draggedRect.height}px`;
};

View file

@ -0,0 +1,49 @@
import { useEffect, useMemo, useCallback, useRef } from 'react';
export const useGroupedTargetsScrollHandler = (groupedTargets, boxList, moveableRef) => {
const scrollRAF = useRef(null); // // Stores the requestAnimationFrame ID
const parentCanvasId = useMemo(() => {
if (!groupedTargets?.[0] || groupedTargets.length === 0) return null;
const targetId = groupedTargets[0].replace('.ele-', '');
const targetBox = boxList.find((box) => box.id === targetId);
return targetBox?.parent || null;
}, [groupedTargets, boxList]);
const containerId = useMemo(() => {
return parentCanvasId ? `canvas-${parentCanvasId}` : null;
}, [parentCanvasId]);
const scrollHandler = useCallback(() => {
if (!scrollRAF.current) {
scrollRAF.current = requestAnimationFrame(() => {
if (groupedTargets.length > 1 && moveableRef.current) {
moveableRef.current.updateRect();
}
scrollRAF.current = null;
});
}
}, [groupedTargets.length, moveableRef]);
useEffect(() => {
// Early return if no container ID or not enough grouped targets
if (!containerId || groupedTargets.length <= 1) {
return;
}
const canvasContainer = document.getElementById(containerId);
if (!canvasContainer) {
return;
}
canvasContainer.addEventListener('scroll', scrollHandler, { passive: true });
return () => {
canvasContainer.removeEventListener('scroll', scrollHandler);
if (scrollRAF.current) {
cancelAnimationFrame(scrollRAF.current);
}
};
}, [containerId, groupedTargets.length, scrollHandler]);
};

View file

@ -35,6 +35,7 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [
'VerticalDivider',
'Link',
'Form',
'FilePicker',
];
const RenderWidget = ({
@ -51,6 +52,8 @@ const RenderWidget = ({
const { moduleId } = useModuleContext();
const componentDefinition = useStore((state) => state.getComponentDefinition(id, moduleId), shallow);
const getDefaultStyles = useStore((state) => state.debugger.getDefaultStyles, shallow);
const adjustComponentPositions = useStore((state) => state.adjustComponentPositions, shallow);
const componentCount = useStore((state) => state.getContainerChildrenMapping(id)?.length || 0, shallow);
const component = componentDefinition?.component;
const componentName = component?.name;
const [key, setKey] = useState(Math.random());
@ -152,6 +155,9 @@ const RenderWidget = ({
}, []);
if (!componentDefinition?.component) return null;
const disabledState = resolvedProperties?.disabledState;
const loadingState = resolvedProperties?.loadingState;
return (
<ErrorBoundary>
<OverlayTrigger
@ -185,7 +191,9 @@ const RenderWidget = ({
padding: resolvedStyles?.padding == 'none' ? '0px' : `${BOX_PADDING}px`, //chart and image has a padding property other than container padding
}}
role={'Box'}
className={inCanvas ? `_tooljet-${component?.component} _tooljet-${component?.name}` : ''} //required for custom CSS
className={`canvas-component ${
inCanvas ? `_tooljet-${component?.component} _tooljet-${component?.name}` : ''
} ${disabledState || loadingState ? 'disabled' : ''}`} //required for custom CSS
>
<ComponentToRender
id={id}
@ -202,6 +210,8 @@ const RenderWidget = ({
onComponentClick={onComponentClick}
darkMode={darkMode}
componentName={componentName}
adjustComponentPositions={adjustComponentPositions}
componentCount={componentCount}
dataCy={`draggable-widget-${componentName}`}
/>
</div>

View file

@ -32,6 +32,7 @@ const WidgetWrapper = memo(
(state) => state.getComponentDefinition(id, moduleId)?.layouts?.[currentLayout],
shallow
);
const temporaryLayouts = useStore((state) => state.temporaryLayouts?.[id], shallow);
const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow);
const isDragging = useStore((state) => state.draggingComponentId === id);
const isResizing = useGridStore((state) => state.resizingComponentId === id);
@ -106,8 +107,8 @@ const WidgetWrapper = memo(
{mode == 'edit' && (
<ConfigHandle
id={id}
widgetTop={newLayoutData.top}
widgetHeight={newLayoutData.height}
widgetTop={temporaryLayouts?.top ?? layoutData.top}
widgetHeight={temporaryLayouts?.height ?? layoutData.height}
showHandle={isWidgetActive}
componentType={componentType}
visibility={visibility}
@ -128,7 +129,6 @@ const WidgetWrapper = memo(
onOptionsChange={onOptionsChange}
/>
</div>
<DragGhostWidget isDragging={isDragging} />
<ResizeGhostWidget isResizing={isResizing} />
</>
);

View file

@ -2,6 +2,12 @@
&:focus-visible{
outline: none;
}
&.page-container {
&.position-top {
flex-direction: column;
}
}
}
.modal-backdrop {

View file

@ -16,6 +16,8 @@ export const APP_HEADER_HEIGHT = 47;
export const LEFT_SIDEBAR_WIDTH = 348; // exclusive of border
export const RIGHT_SIDEBAR_WIDTH = 299;
export const SUBCONTAINER_WIDGETS = ['Container', 'Tabs', 'Listview', 'Kanban', 'Form'];
export const CONTAINER_FORM_CANVAS_PADDING = 7;
@ -39,3 +41,5 @@ export const DROPPABLE_PARENTS = new Set([
export const TAB_CANVAS_PADDING = 7.5;
export const MODAL_CANVAS_PADDING = 5;
export const LISTVIEW_CANVAS_PADDING = 7;

View file

@ -15,6 +15,7 @@ import {
BOX_PADDING,
TAB_CANVAS_PADDING,
MODAL_CANVAS_PADDING,
LISTVIEW_CANVAS_PADDING,
} from './appCanvasConstants';
export function snapToGrid(canvasWidth, x, y) {
@ -776,7 +777,7 @@ export const getSubContainerIdWithSlots = (parentId) => {
return cleanParentId;
};
export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, componentId) => {
export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, componentId, realCanvasRef) => {
let padding = 2; //Need to update this 2 to correct value for other subcontainers
if (componentType === 'Container' || componentType === 'Form') {
padding = 2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING;
@ -789,11 +790,19 @@ export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, com
if (isModalHeader) {
const isModalHeaderCloseBtnEnabled = !useStore.getState().getResolvedComponent(componentId)?.properties
?.hideCloseButton;
console.log('isModalHeaderCloseBtnEnabled', isModalHeaderCloseBtnEnabled);
padding = 2 * (MODAL_CANVAS_PADDING + (isModalHeaderCloseBtnEnabled ? 56 : 0));
} else {
padding = 2 * MODAL_CANVAS_PADDING;
}
}
if (componentType === 'Listview') {
padding = 2 * LISTVIEW_CANVAS_PADDING + 5; // 5 is accounting for scrollbar
}
return canvasWidth - padding;
};
export const addDefaultButtonIdToForm = (formComponent, defaultChildComponents) => {
const { id } = defaultChildComponents[defaultChildComponents.length - 1]; // Assuming the last child is the button
formComponent.component.definition.properties.buttonToSubmit = { value: id };
return formComponent;
};

View file

@ -7,12 +7,15 @@ import debounce from 'lodash/debounce';
const useAppCanvasMaxWidth = ({ mode }) => {
const canvasMaxWidth = useStore((state) => state.globalSettings.canvasMaxWidth, shallow);
const canvasMaxWidthType = useStore((state) => state.globalSettings.canvasMaxWidthType, shallow);
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned, shallow);
let [maxWidth, setMaxWidth] = useState(0);
const getEditorCanvasWidth = useCallback(() => {
let _maxWidth;
const windowWidth = window.innerWidth;
const widthInPx = windowWidth - (CANVAS_WIDTHS.leftSideBarWidth + CANVAS_WIDTHS.rightSideBarWidth);
const widthInPx = windowWidth - CANVAS_WIDTHS.leftSideBarWidth;
if (canvasMaxWidthType === 'px') {
_maxWidth = +canvasMaxWidth;
}
@ -51,7 +54,7 @@ const useAppCanvasMaxWidth = ({ mode }) => {
debouncedGetCanvasWidth.cancel(); // Cancel any pending debounced calls
}
};
}, [debouncedGetCanvasWidth, getEditorCanvasWidth, getViewerWidth, mode]);
}, [debouncedGetCanvasWidth, getEditorCanvasWidth, getViewerWidth, mode, isRightSidebarOpen, isRightSidebarPinned]);
return maxWidth;
};

View file

@ -0,0 +1,28 @@
import { useState, useEffect } from 'react';
import { isEmpty } from 'lodash';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { RIGHT_SIDEBAR_WIDTH } from './appCanvasConstants';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
const useRightSidebarMargin = (canvasContainerRef) => {
const { moduleId } = useModuleContext();
const [editorMarginRight, setEditorMarginRight] = useState(0);
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
useEffect(() => {
if (mode !== 'view') setEditorMarginRight(isRightSidebarOpen ? RIGHT_SIDEBAR_WIDTH : 0);
else setEditorMarginRight(0);
}, [isRightSidebarOpen, mode]);
useEffect(() => {
if (!isEmpty(canvasContainerRef?.current)) {
canvasContainerRef.current.scrollRight += editorMarginRight;
}
}, [editorMarginRight, canvasContainerRef]);
return editorMarginRight;
};
export default useRightSidebarMargin;

View file

@ -0,0 +1,179 @@
import React, { useMemo, useState, useRef, useEffect } from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import DataSourceIcon from '@/AppBuilder/QueryManager/Components/DataSourceIcon';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { LabeledDivider } from '@/AppBuilder/RightSideBar/Inspector/Components/Form/_components';
import cx from 'classnames';
import './styles.scss';
export const DropdownMenu = (props) => {
const { value, onChange, forceCodeBox } = props;
const dataQueries = useStore((state) => state.dataQuery.queries.modules.canvas, shallow);
// Simple emoji/text icons instead of lucide icons
const sourceOptions = useMemo(
() => [
{ id: 'rawJson', label: 'Raw JSON', icon: <SolidIcon name="curlybraces" /> },
{ id: 'jsonSchema', label: 'JSON schema', icon: <SolidIcon name="curlybraces" /> },
// { id: 'json-schema', label: 'JSON schema' },
],
[]
);
const queryOptions = useMemo(() => {
return dataQueries.map((query) => ({
id: query.id,
value: `{{queries.${query.id}.data}}`,
label: query.name,
icon: <DataSourceIcon source={query} height={16} />,
type: 'query',
}));
}, [dataQueries]);
const getSelectedSource = (value) => {
if (!value) return null;
const selectedItem = sourceOptions.find((option) => option.id === value);
if (selectedItem) {
return selectedItem;
}
if (!value.startsWith('{{queries.')) {
return null;
}
const queryName = value.split('.')[1]?.replace('}}', '');
const selectedQuery = queryOptions.find((option) => option.label === queryName);
if (selectedQuery) {
return selectedQuery;
}
return null;
};
const [isOpen, setIsOpen] = useState(false);
const [selectedSource, setSelectedSource] = useState(() => getSelectedSource(value));
const dropdownRef = useRef(null);
// Handle outside clicks
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
const toggleDropdown = () => {
setIsOpen(!isOpen);
};
const selectSource = (source) => {
setSelectedSource(source);
setIsOpen(false);
if (source.id === 'rawJson' || source.id === 'jsonSchema') {
onChange(source.id);
} else if (source.type === 'query') {
onChange(source.value);
forceCodeBox();
}
};
const renderCheckIcon = ({ id }) => {
if (value === id) {
return <SolidIcon name="check" width="16" height="16" fill="#4368E3" viewBox="0 0 16 16" />;
} else {
return <div style={{ width: '16px', height: '16px' }}></div>;
}
};
return (
<div className="tw-w-full tw-max-w-md dropdown-menu-inspector" ref={dropdownRef}>
<div className="tw-relative">
{/* Dropdown trigger div */}
<button
onClick={toggleDropdown}
className={cx(
'tw-flex tw-items-center tw-justify-between tw-w-full tw-px-4 tw-py-2 tw-text-left tw-bg-white dropdown-menu-trigger',
{
'is-open': isOpen,
}
)}
>
<div className="tw-flex tw-items-center">
{selectedSource ? (
<>
<span className="tw-mr-2">{selectedSource.icon}</span>
<span>{selectedSource.label}</span>
</>
) : (
<>
<span className="tw-mr-2 tw-text-gray-400">
<SolidIcon name="code" width="16" height="16" fill="#CCD1D5" />
</span>
<span className="tw-text-gray-400 dropdown-menu-placeholder">Select a source</span>
</>
)}
</div>
<span className="tw-ml-2">
{isOpen ? (
<SolidIcon name="TriangleDownCenter" width={16} />
) : (
<SolidIcon name="TriangleUpCenter" width={16} />
)}
</span>
</button>
{/* Dropdown menu */}
{isOpen && (
<div className="tw-absolute tw-z-10 tw-w-full tw-mt-1 tw-bg-white tw-border tw-border-gray-300 tw-rounded-md tw-shadow-lg tw-p-2">
{/* Source options section */}
<div className="tw-py-1 dropdown-menu-items">
{sourceOptions.map((option) => (
<div
key={option.id}
onClick={() => selectSource(option)}
className="tw-flex tw-items-center tw-w-full tw-px-4 tw-py-2 tw-text-left tw-hover:bg-gray-100"
>
{renderCheckIcon(option)}
<span className="icon-image">{option.icon}</span>
<span>{option.label}</span>
</div>
))}
</div>
{dataQueries.length > 0 && (
<>
{/* Divider with "From query" text */}
<LabeledDivider label="From query" />
{/* Query options section */}
<div className="tw-py-1 dropdown-menu-items">
{queryOptions.map((option) => (
<div
key={option.id}
onClick={() => selectSource(option)}
className="tw-flex tw-items-center tw-w-full tw-px-4 tw-py-2 tw-text-left tw-hover:bg-gray-100"
>
{renderCheckIcon(option)}
<span className="icon-image">{option.icon}</span>
<span>{option.label}</span>
</div>
))}
</div>
</>
)}
</div>
)}
</div>
</div>
);
};

View file

@ -0,0 +1 @@
export { DropdownMenu as default } from './DropdownMenu';

View file

@ -0,0 +1,52 @@
.dropdown-menu-inspector {
margin-top: 2px;
font-size: 12px;
.dropdown-menu-trigger {
height: 34px;
padding: 7px 12px;
align-items: center;
flex-shrink: 0;
align-self: stretch;
border-radius: 6px;
border: 1px solid var(--border-default, #CCD1D5);
&.is-open {
border: 2px solid var(--interactive-focus-outline, #4368E3);
}
}
.dropdown-menu-placeholder {
color: var(--text-placeholder, #6A727C);
}
.dropdown-menu-items {
color: var(--text-default, #1B1F24);
>div {
border-radius: 6px;
&:hover {
cursor: pointer;
background-color: var(--slate4);
}
}
}
.icon-image {
margin: 0px 6px;
width: 16px;
height: 16px;
}
.custom-line {
border-color: var(--border-default, #CCD1D5);
border-top: 0px
}
.separator-text {
color: var(--text-placeholder, #6A727C);
background-color: white;
padding: 0 6px;
}
}

View file

@ -7,7 +7,14 @@ import * as Icons from '@tabler/icons-react';
import { VirtuosoGrid } from 'react-virtuoso';
import { Visibility } from './Visibility';
export const Icon = ({ value, onChange, onVisibilityChange, styleDefinition, component }) => {
export const Icon = ({
value,
onChange,
onVisibilityChange,
styleDefinition,
component,
isVisibilityEnabled = true,
}) => {
const [searchText, setSearchText] = useState('');
const [showPopOver, setPopOverVisibility] = useState(false);
const iconList = useRef(Object.keys(Icons));
@ -111,13 +118,15 @@ export const Icon = ({ value, onChange, onVisibilityChange, styleDefinition, com
>
{String(value)}
</div>
<Visibility
value={value}
onChange={onChange}
onVisibilityChange={onVisibilityChange}
component={component}
styleDefinition={styleDefinition}
/>
{isVisibilityEnabled && (
<Visibility
value={value}
onChange={onChange}
onVisibilityChange={onVisibilityChange}
component={component}
styleDefinition={styleDefinition}
/>
)}
</div>
</OverlayTrigger>
</div>

View file

@ -10,6 +10,7 @@ export const Input = ({ value, onChange, cyLabel, meta }) => {
className="tj-input-element tj-text-xsm"
value={value}
placeholder=""
key={`${String(cyLabel)}-input`}
id="labelId"
onChange={(e) => {
onChange(e.target.value);

View file

@ -1,13 +1,18 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
export const Number = ({ value, onChange, cyLabel }) => {
const [number, setNumber] = useState(value ? value : 0);
useEffect(() => {
setNumber(value);
}, [value]);
return (
<>
<div className="field tj-app-input" style={{ padding: '0.225rem 0.35rem' }}>
<input
className={'inspector-field-number'}
key={`${String(cyLabel)}-input`}
type="number"
onChange={(e) => {
setNumber(e.target.value);

View file

@ -1,3 +1,5 @@
import { drop } from 'lodash';
export const TypeMapping = {
text: 'Text',
string: 'Text',
@ -20,5 +22,6 @@ export const TypeMapping = {
visibility: 'Visibility',
numberInput: 'NumberInput',
tableRowHeightInput: 'TableRowHeightInput',
dropdownMenu: 'DropdownMenu',
query: 'Query',
};

View file

@ -20,7 +20,15 @@ const CODE_EDITOR_TYPE = {
tjdbHinter: TJDBCodeEditor,
};
const CodeHinter = ({ type = 'basic', initialValue, componentName, disabled, renderCopilot, ...restProps }) => {
const CodeHinter = ({
type = 'basic',
initialValue,
componentName,
disabled,
renderCopilot,
setCodeEditorView,
...restProps
}) => {
const darkMode = localStorage.getItem('darkMode') === 'true';
const [isOpen, setIsOpen] = React.useState(false);
@ -71,6 +79,7 @@ const CodeHinter = ({ type = 'basic', initialValue, componentName, disabled, ren
}}
componentName={componentName}
disabled={disabled}
setCodeEditorView={setCodeEditorView}
{...restProps}
/>
);

View file

@ -17,6 +17,7 @@ import { Visibility } from '../CodeBuilder/Elements/Visibility';
import { NumberInput } from '../CodeBuilder/Elements/NumberInput';
import { Datepicker } from '../CodeBuilder/Elements/Datepicker';
import TableRowHeightInput from '../CodeBuilder/Elements/TableRowHeightInput';
import DropdownMenu from '../CodeBuilder/Elements/DropdownMenu';
import { TimePicker } from '../CodeBuilder/Elements/TimePicker';
import { Query } from '../CodeBuilder/Elements/Query';
import { ColorSwatches } from '@/modules/Appbuilder/components';
@ -41,6 +42,7 @@ const AllElements = {
TableRowHeightInput,
Datepicker,
TimePicker,
DropdownMenu,
Query,
};

View file

@ -54,11 +54,15 @@ const MultiLineCodeEditor = (props) => {
readOnly = false,
editable = true,
renderCopilot,
setCodeEditorView,
} = props;
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
const wrapperRef = useRef(null);
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow);
const getServerSideGlobalResolveSuggestions = useStore(
(state) => state.getServerSideGlobalResolveSuggestions,
shallow
);
const isInsideQueryPane = !!document.querySelector('.code-hinter-wrapper')?.closest('.query-details');
const isInsideQueryManager = useMemo(
@ -72,13 +76,48 @@ const MultiLineCodeEditor = (props) => {
const currentValueRef = useRef(initialValue);
const handleChange = (val) => (currentValueRef.current = val);
const [editorView, setEditorView] = React.useState(null);
const [isSearchPanelOpen, setIsSearchPanelOpen] = React.useState(false);
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onChange, currentValueRef, 'multiline');
// Add state for tracking autocomplete visibility
const [showSuggestions, setShowSuggestions] = React.useState(true);
const currentLineObserverRef = useRef(null);
const isObserverTriggeredRef = useRef(false);
// Intersection observer to detect when current line goes out of view
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.intersectionRatio < 1) {
setShowSuggestions(false);
isObserverTriggeredRef.current = true;
// Close autocomplete dropdown by dispatching a selection change
if (editorView) {
editorView.dispatch({
selection: editorView.state.selection,
});
}
} else {
setShowSuggestions(true);
isObserverTriggeredRef.current = false;
}
},
{ root: null, threshold: [1] }
);
currentLineObserverRef.current = observer;
return () => {
if (currentLineObserverRef.current) {
currentLineObserverRef.current.disconnect();
}
};
}, [editorView]);
const handleChange = (val) => (currentValueRef.current = val);
const handleOnBlur = () => {
if (!delayOnChange) return onChange(currentValueRef.current);
setTimeout(() => {
@ -116,7 +155,7 @@ const MultiLineCodeEditor = (props) => {
const hints = getSuggestions();
const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager);
const serverHints = getServerSideGlobalResolveSuggestions(isInsideQueryManager);
const allHints = {
...hints,
@ -276,6 +315,21 @@ const MultiLineCodeEditor = (props) => {
return initialValue;
}, [initialValue, replaceIdsWithName]);
function updateCurrentLineObserver(editorView) {
if (!editorView || !editorView?.view?.dom) return;
const cursorPos = editorView.state.selection.main.head;
const line = editorView.state.doc.lineAt(cursorPos);
const lineNumber = line.number;
const cmLines = editorView.view.dom.querySelectorAll('.cm-line');
const currentLineDiv = cmLines[lineNumber - 1] || null;
// Update intersection observer to watch the current line
if (currentLineObserverRef.current && currentLineDiv && !isObserverTriggeredRef.current) {
currentLineObserverRef.current.disconnect();
currentLineObserverRef.current.observe(currentLineDiv);
}
}
return (
<div
className={`code-hinter-wrapper position-relative ${isInsideQueryPane ? 'code-editor-query-panel' : ''}`}
@ -349,8 +403,16 @@ const MultiLineCodeEditor = (props) => {
indentWithTab={false}
readOnly={readOnly}
editable={editable} //for transformations in query manager
onCreateEditor={(view) => setEditorView(view)}
onUpdate={(view) => setIsSearchPanelOpen(searchPanelOpen(view.state))}
onCreateEditor={(view) => {
setEditorView(view);
if (setCodeEditorView) {
setCodeEditorView(view);
}
}}
onUpdate={(view) => {
setIsSearchPanelOpen(searchPanelOpen(view.state));
updateCurrentLineObserver(view);
}}
/>
</div>
{showPreview && (

View file

@ -98,7 +98,7 @@ export const PreviewBox = ({
const [largeDataset, setLargeDataset] = useState(false);
const globals = useStore((state) => state.getAllExposedValues(moduleId).constants || {}, shallow);
const secrets = useStore((state) => state.getSecrets(), shallow);
const globalServerConstantsRegex = /^\{\{.*globals\.server.*\}\}$/;
const globalServerConstantsRegex = /\{\{.*globals\.server.*\}\}/;
const getPreviewContent = (content, type) => {
if (content === undefined || content === null) return currentValue;
@ -251,7 +251,10 @@ const RenderResolvedValue = ({
isServerConstant = false,
isLargeDataset,
}) => {
const isServerSideGlobalEnabled = useStore((state) => !!state?.license?.featureAccess?.serverSideGlobal, shallow);
const isServerSideGlobalResolveEnabled = useStore(
(state) => !!state?.license?.featureAccess?.serverSideGlobalResolve,
shallow
);
const computeCoersionPreview = (resolvedValue, coersionData) => {
if (coersionData?.typeBeforeCoercion === coersionData?.typeAfterCoercion) return resolvedValue;
@ -276,7 +279,7 @@ const RenderResolvedValue = ({
: previewType;
const previewContent = isServerConstant
? isServerSideGlobalEnabled
? isServerSideGlobalResolveEnabled
? 'Server variables would be resolved at runtime'
: 'Server variables are only available in paid plans'
: isSecretConstant
@ -486,7 +489,14 @@ const PreviewContainer = ({
};
const PreviewCodeBlock = ({ code, isExpectValue = false, isLargeDataset }) => {
let preview = code && code.trim ? code?.trim() : `${code}`;
let preview;
if (typeof code === 'string') {
preview = code.trim();
} else if (typeof code === 'symbol') {
preview = code.toString();
} else {
preview = String(code);
}
const shouldTrim = preview.length > 35;
let showJSONTree = false;

View file

@ -28,10 +28,12 @@ import CodeHinter from './CodeHinter';
import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { getCssVarValue } from '@/Editor/Components/utils';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext';
import { createReferencesLookup } from '@/_stores/utils';
import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks';
import Icon from '@/_ui/Icon/solidIcons/index';
const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => {
const { moduleId } = useModuleContext();
@ -79,7 +81,6 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
let newInitialValue = initialValue;
if (typeof initialValue === 'string' && (initialValue?.includes('components') || initialValue?.includes('queries'))) {
newInitialValue = replaceIdsWithName(initialValue);
}
@ -209,6 +210,7 @@ const EditorInput = ({
onInputChange,
wrapperRef,
showSuggestions,
setCodeEditorView = null, // Function to set the CodeMirror view
}) => {
const codeHinterContext = useContext(CodeHinterContext);
const { suggestionList: paramHints } = createReferencesLookup(codeHinterContext, true);
@ -216,7 +218,10 @@ const EditorInput = ({
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
const [codeMirrorView, setCodeMirrorView] = useState(undefined);
const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow);
const getServerSideGlobalResolveSuggestions = useStore(
(state) => state.getServerSideGlobalResolveSuggestions,
shallow
);
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline');
@ -226,7 +231,7 @@ const EditorInput = ({
);
function autoCompleteExtensionConfig(context) {
const hintsWithoutParamHints = getSuggestions();
const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager);
const serverHints = getServerSideGlobalResolveSuggestions(isInsideQueryManager);
let word = context.matchBefore(/\w*/);
@ -274,7 +279,10 @@ const EditorInput = ({
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [isInsideQueryManager, paramHints]);
const overRideFunction = React.useCallback(
(context) => autoCompleteExtensionConfig(context),
[isInsideQueryManager, paramHints]
);
const autoCompleteConfig = autocompletion({
override: [overRideFunction],
@ -443,6 +451,9 @@ const EditorInput = ({
<CodeMirror
onCreateEditor={(view) => {
setCodeMirrorView(view);
if (setCodeEditorView) {
setCodeEditorView(view);
}
}}
value={currentValue}
placeholder={placeholder}
@ -451,11 +462,11 @@ const EditorInput = ({
extensions={
showSuggestions
? [
javascript({ jsx: lang === 'jsx' }),
autoCompleteConfig,
keymap.of([...customKeyMaps]),
customTabKeymap,
]
javascript({ jsx: lang === 'jsx' }),
autoCompleteConfig,
keymap.of([...customKeyMaps]),
customTabKeymap,
]
: [javascript({ jsx: lang === 'jsx' })]
}
onChange={(val) => {
@ -487,9 +498,9 @@ const EditorInput = ({
}}
/>
</div>
</ErrorBoundary >
</CodeHinter.Portal >
</div >
</ErrorBoundary>
</CodeHinter.Portal>
</div>
);
};
@ -514,24 +525,49 @@ const DynamicEditorBridge = (props) => {
const [forceCodeBox, setForceCodeBox] = React.useState(fxActive);
const codeShow = paramType === 'code' || forceCodeBox;
const HIDDEN_CODE_HINTER_LABELS = ['Table data', 'Column data', 'Text Format'];
const { isFxNotRequired } = fieldMeta;
const HIDDEN_CODE_HINTER_LABELS = ['Table data', 'Column data', 'Text Format', 'Slider type'];
const { isFxNotRequired, newLine = false, section = '' } = fieldMeta;
const isDeprecated = section === 'deprecated';
const { t } = useTranslation();
const [_, error, value] = type === 'fxEditor' ? resolveReferences(initialValue) : [];
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
let newInitialValue = initialValue,
shouldResolve = true;
// This is to handle the case when the initial value is a string and contains components or queries
// and we need to replace the ids with names
// but we don't want to resolve the references as it needs to be displayed as it is
if (paramName === 'generateFormFrom') {
if (
typeof initialValue === 'string' &&
(initialValue?.includes('components') || initialValue?.includes('queries'))
) {
newInitialValue = replaceIdsWithName(initialValue);
shouldResolve = false;
}
}
const [_, error, value] =
type === 'fxEditor' ? (shouldResolve ? resolveReferences(newInitialValue) : [false, '', newInitialValue]) : [];
let cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : props.cyLabel;
useEffect(() => {
setForceCodeBox(fxActive);
}, [component, fxActive]);
let modifiedValue = initialValue;
if (paramType === 'colorSwatches' && typeof initialValue === 'string' && initialValue?.includes('var(')) {
modifiedValue = getCssVarValue(document.documentElement, initialValue);
}
const renderFx = () => {
if (paramType === 'query' || !(paramLabel !== 'Type' && isFxNotRequired === undefined)) {
return null;
}
return (
<div
className={`col-auto pt-0 fx-common fx-button-container ${(isEventManagerParam || codeShow) && 'show-fx-button-container'
}`}
className={`col-auto pt-0 fx-common fx-button-container ${
(isEventManagerParam || codeShow) && 'show-fx-button-container'
}`}
>
<FxButton
active={codeShow}
@ -539,6 +575,9 @@ const DynamicEditorBridge = (props) => {
if (codeShow) {
setForceCodeBox(false);
onFxPress(false);
if (paramType === 'colorSwatches') {
onChange(modifiedValue);
}
} else {
setForceCodeBox(true);
onFxPress(true);
@ -551,48 +590,69 @@ const DynamicEditorBridge = (props) => {
};
const fxClass = isEventManagerParam ? 'justify-content-start' : 'justify-content-end';
return (
<div className={cx({ 'codeShow-active': codeShow }, 'wrapper-div-code-editor')}>
<div className={cx('d-flex align-items-center justify-content-between code-flex-wrapper')}>
const renderedLabel = () => {
return (
<>
{paramLabel !== ' ' && !HIDDEN_CODE_HINTER_LABELS.includes(paramLabel) && (
<div className={`field ${className}`} data-cy={`${cyLabel}-widget-parameter-label`}>
<ToolTip
label={t(`widget.commonProperties.${camelCase(paramLabel)}`, paramLabel)}
meta={fieldMeta}
labelClass={`tj-text-xsm color-slate12 ${codeShow ? 'mb-2' : 'mb-0'} ${darkMode && 'color-whitish-darkmode'
}`}
labelClass={`tj-text-xsm color-slate12 ${codeShow ? 'mb-2' : 'mb-0'} ${
darkMode && 'color-whitish-darkmode'
}`}
/>
{isDeprecated && (
<span className={'list-item-deprecated-column-type'}>
<Icon name={'warning'} height={14} width={14} fill="#DB4324" />
</span>
)}
</div>
)}
</>
);
};
const renderDynamicFx = () => {
if (codeShow) return null;
return (
<DynamicFxTypeRenderer
value={!error ? value : ''}
onChange={onChange}
paramName={paramName}
paramLabel={paramLabel}
paramType={paramType}
forceCodeBox={() => {
setForceCodeBox(true);
onFxPress(true);
}}
meta={fieldMeta}
cyLabel={cyLabel}
styleDefinition={styleDefinition}
component={component}
onVisibilityChange={onVisibilityChange}
/>
);
};
return (
<div className={cx({ 'codeShow-active': codeShow }, 'wrapper-div-code-editor')}>
<div className={cx('d-flex align-items-center justify-content-between code-flex-wrapper')}>
{renderedLabel()}
<div className={`${(paramType ?? 'code') === 'code' ? 'd-none' : ''} flex-grow-1`}>
<div style={{ marginBottom: codeShow ? '0.5rem' : '0px' }} className={`d-flex align-items-center ${fxClass}`}>
{renderFx()}
</div>
</div>
{!codeShow && (
<DynamicFxTypeRenderer
value={!error ? value : ''}
onChange={onChange}
paramName={paramName}
paramLabel={paramLabel}
paramType={paramType}
forceCodeBox={() => {
setForceCodeBox(true);
onFxPress(true);
}}
meta={fieldMeta}
cyLabel={cyLabel}
styleDefinition={styleDefinition}
component={component}
onVisibilityChange={onVisibilityChange}
/>
)}
{!newLine && renderDynamicFx()}
</div>
{newLine && renderDynamicFx()}
{codeShow && (
<div className={`row custom-row`} style={{ display: codeShow ? 'flex' : 'none' }}>
<div className={`col code-hinter-col`}>
<div className="d-flex">
<SingleLineCodeEditor initialValue {...props} />
<SingleLineCodeEditor {...props} initialValue={modifiedValue} />
</div>
</div>
</div>

View file

@ -660,6 +660,13 @@
}
}
.code-editor-component {
.cm-editor {
min-height: 0 !important;
}
}
.cm-searchMatch.cm-searchMatch-selected {
background-color: #F28F2D !important;
}
@ -673,4 +680,4 @@
.cm-theme{
height: 100% ;
}
}
}

View file

@ -367,6 +367,7 @@ export const FxParamTypeMapping = Object.freeze({
visibility: 'Visibility',
numberInput: 'NumberInput',
tableRowHeightInput: 'TableRowHeightInput',
dropdownMenu: 'DropdownMenu',
query: 'Query',
});

View file

@ -16,15 +16,11 @@ const CreateVersionModal = ({
canCommit,
orgGit,
fetchingOrgGit,
handleCommitOnVersionCreation = () => {},
handleCommitOnVersionCreation = () => { },
}) => {
const { moduleId } = useModuleContext();
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
const [versionName, setVersionName] = useState('');
const gitSyncEnabled =
orgGit?.org_git?.git_https?.is_enabled ||
orgGit?.org_git?.git_ssh?.is_enabled ||
orgGit?.org_git?.git_lab?.is_enabled;
const {
createNewVersionAction,
@ -33,6 +29,7 @@ const CreateVersionModal = ({
appId,
setCurrentVersionId,
selectedVersion,
currentMode,
} = useStore(
(state) => ({
createNewVersionAction: state.createNewVersionAction,
@ -45,6 +42,7 @@ const CreateVersionModal = ({
currentVersionId: state.currentVersionId,
setCurrentVersionId: state.setCurrentVersionId,
selectedVersion: state.selectedVersion,
currentMode: state.currentMode,
}),
shallow
);
@ -94,7 +92,7 @@ const CreateVersionModal = ({
setIsCreatingVersion(false);
setShowCreateAppVersion(false);
appVersionService
.getAppVersionData(appId, newVersion.id)
.getAppVersionData(appId, newVersion.id, currentMode)
.then((data) => {
setCurrentVersionId(newVersion.id);
handleCommitOnVersionCreation(data);
@ -104,8 +102,8 @@ const CreateVersionModal = ({
});
},
(error) => {
if (error?.data?.code === '23505') {
toast.error('Version name already exists.');
if (error?.data?.code === "23505") {
toast.error("Version name already exists.");
} else {
toast.error(error?.error);
}
@ -174,7 +172,7 @@ const CreateVersionModal = ({
</div>
</div>
{gitSyncEnabled && (
{orgGit?.org_git?.is_enabled && (
<div className="commit-changes" style={{ marginTop: '-1rem', marginBottom: '2rem' }}>
<div>
<input

View file

@ -10,8 +10,10 @@ import { resolveReferences } from '@/_helpers/utils';
import FxButton from '@/Editor/CodeBuilder/Elements/FxButton';
import { useTranslation } from 'react-i18next';
import { Confirm } from '@/Editor/Viewer/Confirm';
import { ColorSwatches } from '@/modules/Appbuilder/components';
import { shallow } from 'zustand/shallow';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { getCssVarValue } from '@/Editor/Components/utils';
const CanvasSettings = ({ darkMode }) => {
const { moduleId } = useModuleContext();
@ -117,77 +119,64 @@ const CanvasSettings = ({ darkMode }) => {
</div>
</div>
<div className="d-flex justify-content-between mb-3">
<div className="d-flex mb-3" style={{ height: '42px', gap: '20px' }}>
<span className="pt-2" data-cy={`label-bg-canvas`}>
{t('leftSidebar.Settings.backgroundColorOfCanvas', 'Canvas bavkground')}
</span>
<div className="canvas-codehinter-container">
{showPicker && (
<div>
<div style={coverStyles} onClick={() => setShowPicker(false)} />
<SketchPicker
data-cy={`color-picker-canvas`}
className="canvas-background-picker"
onFocus={() => setShowPicker(true)}
color={canvasBackgroundColor}
onChangeComplete={(color) => {
<div className={`fx-canvas `}>
<FxButton
dataCy={`canvas-bg-color`}
active={!forceCodeBox ? true : false}
onPress={async () => {
if (typeof canvasBackgroundColor === 'string' && canvasBackgroundColor?.includes('var(')) {
const value = getCssVarValue(document.documentElement, canvasBackgroundColor);
const options = {
canvasBackgroundColor: [color.hex, color.rgb],
backgroundFxQuery: '',
canvasBackgroundColor: value,
backgroundFxQuery: value,
};
globalSettingsChanged(options);
resolveOthers('canvas', true, { canvasBackgroundColor: [color.hex, color.rgb] });
}}
/>
</div>
)}
await Promise.resolve(globalSettingsChanged(options));
await Promise.resolve(resolveOthers('canvas', true, { canvasBackgroundColor: value }));
}
setForceCodeBox(!forceCodeBox);
}}
/>
</div>
{forceCodeBox && (
<div className="row mx-0 color-picker-input d-flex" onClick={() => setShowPicker(true)} style={outerStyles}>
<div
data-cy={`canvas-bg-color-picker`}
className="col-auto"
style={{
float: 'right',
width: '24px',
height: '24px',
backgroundColor: canvasBackgroundColor,
borderRadius: ' 6px',
border: `1px solid var(--slate7, #D7DBDF)`,
boxShadow: `0px 1px 2px 0px rgba(16, 24, 40, 0.05)`,
}}
></div>
<div style={{ height: '20px' }} className="col">
{canvasBackgroundColor}
</div>
</div>
<ColorSwatches
data-cy={`color-picker-canvas`}
outerWidth="155px"
value={canvasBackgroundColor}
onChange={(color) => {
const options = {
canvasBackgroundColor: resolveReferences(color),
backgroundFxQuery: color,
};
globalSettingsChanged(options);
resolveOthers('canvas', true, { canvasBackgroundColor: color });
}}
/>
)}
<div className={`${!forceCodeBox && 'hinter-canvas-input'} `}>
{!forceCodeBox && (
<CodeHinter
cyLabel={`canvas-bg-colour`}
initialValue={backgroundFxQuery ? backgroundFxQuery : canvasBackgroundColor}
lang="javascript"
className="canvas-hinter-wrap"
lineNumbers={false}
onChange={(color) => {
const options = {
canvasBackgroundColor: resolveReferences(color),
backgroundFxQuery: color,
};
globalSettingsChanged(options);
resolveOthers('canvas', true, { canvasBackgroundColor: color });
}}
/>
<div className="canvas-hinter-wrap-container">
<CodeHinter
cyLabel={`canvas-bg-colour`}
initialValue={backgroundFxQuery ? backgroundFxQuery : canvasBackgroundColor}
lang="javascript"
className="canvas-hinter-wrap"
lineNumbers={false}
onChange={(color) => {
const options = {
canvasBackgroundColor: resolveReferences(color),
backgroundFxQuery: color,
};
globalSettingsChanged(options);
resolveOthers('canvas', true, { canvasBackgroundColor: color });
}}
/>
</div>
)}
<div className={`fx-canvas `}>
<FxButton
dataCy={`canvas-bg-color`}
active={!forceCodeBox ? true : false}
onPress={() => {
setForceCodeBox(!forceCodeBox);
}}
/>
</div>
</div>
</div>
</div>

View file

@ -10,12 +10,13 @@ import AppModeToggle from './AppModeToggle';
import { ThemeSelect } from '@/modules/Appbuilder/components';
import MaintenanceMode from './MaintenanceMode';
import HideHeaderToggle from './HideHeaderToggle';
import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
const GlobalSettings = ({ darkMode }) => {
const shouldFreeze = useStore((state) => state.getShouldFreeze());
return (
<>
<ModuleProvider moduleId={'canvas'}>
<div>
<div bsPrefix="global-settings-popover" className="global-settings-panel">
<HeaderSection>
@ -44,7 +45,7 @@ const GlobalSettings = ({ darkMode }) => {
</div>
</div>
</div>
</>
</ModuleProvider>
);
};

View file

@ -5,13 +5,14 @@ import cx from 'classnames';
import { shallow } from 'zustand/shallow';
import { DarkModeToggle } from '@/_components';
import Popover from '@/_ui/Popover';
import { PageMenu } from './PageMenu';
// import { PageMenu } from './PageMenu';
import LeftSidebarInspector from './LeftSidebarInspector/LeftSidebarInspector';
import GlobalSettings from './GlobalSettings';
import '../../_styles/left-sidebar.scss';
import Debugger from './Debugger/Debugger';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
import { PageMenu } from '../RightSideBar/PageSettingsTab/PageMenu';
// TODO: remove passing refs to LeftSidebarItem and use state
// TODO: need to add datasources to the sidebar.
@ -58,7 +59,6 @@ export const BaseLeftSidebar = ({
const sideBarBtnRefs = useRef({});
const handleSelectedSidebarItem = (item) => {
pinned && localStorage.setItem('selectedSidebarItem', item);
if (item === 'debugger') resetUnreadErrorCount();
setSelectedSidebarItem(item);
if (item === selectedSidebarItem && !pinned) {
@ -211,15 +211,6 @@ export const BaseLeftSidebar = ({
tip: 'Build with AI',
ref: setSideBarBtnRefs('tooljetai'),
})}
<SidebarItem
selectedSidebarItem={selectedSidebarItem}
onClick={() => handleSelectedSidebarItem('page')}
darkMode={darkMode}
icon="page"
className={`left-sidebar-item left-sidebar-layout left-sidebar-page-selector`}
tip="Pages"
ref={setSideBarBtnRefs('page')}
/>
{renderCommonItems()}
<SidebarItem
icon="settings"

View file

@ -103,7 +103,9 @@ export const Node = (props) => {
marginTop: level === 1 ? 4 : 0,
marginBottom: level === 1 ? 4 : 0,
// borderLeft: level > 1 ? '1px solid var(--slate6, #D7DBDF)' : 'none',
cursor: level === 1 ? 'pointer' : 'default',
}}
{...(level === 1 && { onClick: () => onExpand(props) })}
>
{/* {!['queries', 'globals', 'variables'].includes(type) && ( */}
<div className="node-expansion-icon">

View file

@ -1,317 +0,0 @@
import React, { memo, useRef, useState, useCallback } from 'react';
import cx from 'classnames';
// import { RenameInput } from './RenameInput';
// import { PagehandlerMenu } from './PagehandlerMenu';
// import { EditModal } from './EditModal';
// import { SettingsModal } from './SettingsModal';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import EyeDisable from '@/_ui/Icon/solidIcons/EyeDisable';
import FileRemove from '@/_ui/Icon/solidIcons/FIleRemove';
import Home from '@/_ui/Icon/solidIcons/Home';
import useStore from '@/AppBuilder/_stores/store';
import _ from 'lodash';
import { toast } from 'react-hot-toast';
import { RenameInput } from './RenameInput';
import IconSelector from './IconSelector';
import { withRouter } from '@/_hoc/withRouter';
import OverflowTooltip from '@/_components/OverflowTooltip';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { shallow } from 'zustand/shallow';
import { ToolTip } from '@/_components/ToolTip';
export const PageMenuItem = withRouter(
memo(({ darkMode, page, navigate }) => {
const { moduleId } = useModuleContext();
const homePageId = useStore((state) => state.appStore.modules[moduleId].app.homePageId);
const isHomePage = page.id === homePageId;
const currentPageId = useStore((state) => state.modules[moduleId].currentPageId);
const isSelected = page.id === currentPageId;
const isHidden = page?.hidden ?? false;
const isDisabled = page?.disabled ?? false;
const [isHovered, setIsHovered] = useState(false);
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
const showEditingPopover = useStore((state) => state.showEditingPopover);
const restricted = page?.permissions && page?.permissions?.length > 0;
const {
definition: { styles, properties },
} = useStore((state) => state.pageSettings);
const setCurrentPageHandle = useStore((state) => state.setCurrentPageHandle);
// only update when the page is being edited
const editingPage = useStore(
(state) => state.editingPage,
(prev, next) => {
if (next?.id === page?.id) return false;
if (prev?.id === page?.id) return false;
return true;
}
);
const editingPageName = useStore((state) => state.showEditPageNameInput);
const popoverRef = useRef(null);
const openPageEditPopover = useStore((state) => state.openPageEditPopover);
const toggleEditPageNameInput = useStore((state) => state.toggleEditPageNameInput);
const isEditingPage = editingPage?.id === page?.id;
const icon = () => {
const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon;
if (!isDisabled && !isHidden) {
return <IconSelector iconColor={computedStyles?.icon?.color} iconName={iconName} pageId={page.id} />;
}
if (isDisabled || (isDisabled && isHidden)) {
return (
<FileRemove fill={computedStyles?.icon?.fill} className=" " width={16} height={16} viewBox={'0 0 16 16'} />
);
}
if (isHidden && !isDisabled) {
return <EyeDisable className="" width={16} height={16} />;
}
};
const computeStyles = useCallback(() => {
const baseStyles = {
pill: {
borderRadius: `${styles.pillRadius.value}px`,
},
icon: {
color: !styles.iconColor.isDefault && styles.iconColor.value,
fill: !styles.iconColor.isDefault && styles.iconColor.value,
},
};
switch (true) {
case isSelected: {
return {
...baseStyles,
text: {
color: !styles.selectedTextColor.isDefault && styles.selectedTextColor.value,
},
icon: {
stroke: !styles.selectedIconColor.isDefault && styles.selectedIconColor.value,
color: !styles.selectedIconColor.isDefault && styles.selectedIconColor.value,
fill: !styles.selectedIconColor.isDefault && styles.selectedIconColor.value,
},
pill: {
background: !styles.pillSelectedBackgroundColor.isDefault && styles.pillSelectedBackgroundColor.value,
...(page.id === editingPage?.id && {
backgroundColor: 'var(--slate1)',
}),
...baseStyles.pill,
},
};
}
case isHovered: {
return {
...baseStyles,
pill: {
background: !styles.pillHoverBackgroundColor.isDefault && styles.pillHoverBackgroundColor.value,
...baseStyles.pill,
},
};
}
default: {
return {
text: {
color: !styles.textColor.isDefault && styles.textColor.value,
},
icon: {
color: !styles.iconColor.isDefault && styles.iconColor.value,
fill: !styles.iconColor.isDefault && styles.iconColor.value,
},
};
}
}
}, [styles, isSelected, isHovered, page.id, editingPage?.id]);
const computedStyles = computeStyles();
const labelStyle = {
icon: {
hidden: properties.style === 'text',
},
label: {
hidden: properties.style === 'icon',
},
};
const switchPage = useStore((state) => state.switchPage);
const handlePageSwitch = useCallback(() => {
if (currentPageId === page.id) {
return;
}
switchPage(page.id, page.handle, [], moduleId);
setCurrentPageHandle(page.handle, moduleId);
}, [currentPageId, page.id, page.handle, switchPage, setCurrentPageHandle, moduleId]);
const handlePageMenuSettings = useCallback(
(event) => {
event.stopPropagation();
openPageEditPopover(page, popoverRef);
},
[popoverRef.current, page]
);
function getTooltip() {
const permission = page?.permissions?.length ? page?.permissions[0] : null;
if (!permission) return '';
const users = permission.users || [];
const isSingle = permission.type === 'SINGLE';
const isGroup = permission.type === 'GROUP';
if (users.length === 0) return null;
if (isSingle) {
if (users.length === 1) {
const email = users[0].user.email;
return `Access restricted to ${email}`;
} else {
return `Access restricted to ${users.length} users`;
}
}
if (isGroup) {
if (users.length === 1) {
const groupName = users[0].permissionGroup?.name ?? 'Group';
return `Access restricted to ${groupName} group`;
} else {
return `Access restricted to ${users.length} groups`;
}
}
return '';
}
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
width: '100%',
}}
>
<>
<div
onClick={handlePageSwitch}
className={`page-menu-item ${isSelected && 'is-selected'} ${darkMode && 'dark-theme'}`}
style={{
position: 'relative',
width: '100%',
...computedStyles?.pill,
}}
>
{editingPageName && editingPage?.id === page?.id ? (
<>
{' '}
<div className="left">{icon()}</div>
<RenameInput
page={page}
updaterCallback={() => {
toggleEditPageNameInput(false);
}}
/>
</>
) : (
<>
{' '}
<div className="left" data-cy={`pages-name-${page.name.toLowerCase()}`}>
{icon()}
<OverflowTooltip childrenClassName="page-name" style={{ ...computedStyles?.text }}>
{page.name}
</OverflowTooltip>
<span
style={{
marginLeft: '8px',
}}
className="color-slate09 meta-text"
>
{isHomePage && 'Home'}
{isDisabled && 'Disabled'}
{isHidden && !isDisabled && 'Hidden'}
</span>
</div>
<div style={{ marginLeft: '8px', marginRight: 'auto' }}>
{licenseValid && restricted && (
<ToolTip message={getTooltip()}>
<div>
<SolidIcon width="16" name="lock" fill="var(--icon-strong)" />
</div>
</ToolTip>
)}
</div>
<div className={cx('right', { 'handler-menu-open': showEditingPopover })}>
{!shouldFreeze && (
<button
style={{
backgroundColor: 'transparent',
border: 'none',
color: 'var(--color-slate12)',
cursor: 'pointer',
padding: '0',
...((isEditingPage || currentPageId === page?.id) && {
opacity: 1,
}),
}}
className="edit-page-overlay-toggle"
onClick={handlePageMenuSettings}
ref={popoverRef}
id={`edit-popover-${page.id}`}
>
<SolidIcon width="20" dataCy={`page-menu`} name="morevertical" />
</button>
)}
</div>
</>
)}
</div>
</>
</div>
);
})
);
export const AddingPageHandler = ({ darkMode }) => {
const toggleShowAddNewPageInput = useStore((state) => state.toggleShowAddNewPageInput);
const addNewPage = useStore((state) => state.addNewPage);
const isPageGroup = useStore((state) => state.isPageGroup);
const handleAddingNewPage = (pageName) => {
if (pageName.trim().length === 0) {
toast(`${isPageGroup ? 'Page group' : 'Page'} name should have at least 1 character`, {
icon: '⚠️',
});
} else if (pageName.trim().length > 32) {
toast(`${isPageGroup ? 'Page group' : 'Page'} name cannot exceed 32 characters`, {
icon: '⚠️',
});
} else {
addNewPage(pageName, _.kebabCase(pageName.toLowerCase()), isPageGroup);
}
toggleShowAddNewPageInput(false);
};
return (
<div role="button" style={{ marginTop: '2px' }}>
<div>
<input
type="text"
className={`form-control page-name-input color-slate12 ${darkMode && 'bg-transparent'}`}
autoFocus
onBlur={(event) => {
const name = event.target.value;
handleAddingNewPage(name);
event.stopPropagation();
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
const name = event.target.value;
handleAddingNewPage(name);
event.stopPropagation();
}
}}
/>
</div>
</div>
);
};

View file

@ -277,7 +277,11 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
};
const aggFxOptions = [
{ label: 'Sum', value: 'sum', description: 'Sum of all values in this column' },
{
label: 'Sum',
value: 'sum',
description: 'Sum of all values in this column',
},
{
label: 'Count',
value: 'count',
@ -402,7 +406,11 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
/>
</div>
<div
style={{ width: '32px', minWidth: '32px', borderRadius: '0 4px 4px 0' }}
style={{
width: '32px',
minWidth: '32px',
borderRadius: '0 4px 4px 0',
}}
className="d-flex justify-content-center align-items-center border"
onClick={() => handleDeleteAggregate(aggregateKey)}
>

View file

@ -1,10 +1,10 @@
import React, { useState, useCallback } from 'react';
import React, { useState } from 'react';
import { Tooltip } from 'react-tooltip';
import { ToolTip } from '@/_components/ToolTip';
import { updateQuerySuggestions } from '@/_helpers/appUtils';
// import { Confirm } from '../Viewer/Confirm';
import { toast } from 'react-hot-toast';
import { shallow } from 'zustand/shallow';
import Copy from '@/_ui/Icon/solidIcons/Copy';
import DataSourceIcon from '../QueryManager/Components/DataSourceIcon';
import { isQueryRunnable, decodeEntities } from '@/_helpers/utils';
import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers';
@ -12,13 +12,10 @@ import useStore from '@/AppBuilder/_stores/store';
//TODO: Remove this
import { Confirm } from '@/Editor/Viewer/Confirm';
// TODO: enable delete query confirmation popup
import { debounce } from 'lodash';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx';
import SolidIcon from '@/_ui/Icon/SolidIcons';
export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
const { moduleId } = useModuleContext();
const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
const isQuerySelected = useStore((state) => state.queryPanel.isQuerySelected(dataQuery.id), shallow);
const setSelectedQuery = useStore((state) => state.queryPanel.setSelectedQuery);
const checkExistingQueryName = useStore((state) => state.dataQuery.checkExistingQueryName);
@ -26,9 +23,16 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
const isDeletingQueryInProcess = useStore((state) => state.dataQuery.isDeletingQueryInProcess);
const renameQuery = useStore((state) => state.dataQuery.renameQuery);
const deleteDataQueries = useStore((state) => state.dataQuery.deleteDataQueries);
const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery);
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const renamingQueryId = useStore((state) => state.queryPanel.renamingQueryId);
const deletingQueryId = useStore((state) => state.queryPanel.deletingQueryId);
const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery);
const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery);
const isRenaming = renamingQueryId === dataQuery.id;
const isDeleting = deletingQueryId === dataQuery.id;
const hasPermissions =
selectedDataSourceScope === 'global'
? canUpdateDataSource(dataQuery?.data_source_id) ||
@ -36,57 +40,77 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
canDeleteDataSource()
: true;
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const [renamingQuery, setRenamingQuery] = useState(false);
const deleteDataQuery = (e) => {
e.stopPropagation();
setShowDeleteConfirmation(true);
};
const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu);
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
const isRestricted = dataQuery.permissions && dataQuery.permissions.length !== 0;
const updateQueryName = (dataQuery, newName) => {
const { name } = dataQuery;
if (name === newName) {
return setRenamingQuery(false);
return setRenamingQuery(null);
}
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName);
if (newName && !isNewQueryNameAlreadyExists) {
renameQuery(dataQuery?.id, newName);
setRenamingQuery(false);
setRenamingQuery(null);
updateQuerySuggestions(name, newName);
} else {
if (isNewQueryNameAlreadyExists) {
toast.error('Query name already exists');
}
setRenamingQuery(false);
setRenamingQuery(null);
}
};
const executeDataQueryDeletion = () => {
setShowDeleteConfirmation(false);
deleteDataQuery(null);
deleteDataQueries(dataQuery?.id);
setPreviewData(null);
};
// To prevent user clicking from continuous clicks
const debouncedDuplicateQuery = useCallback(
debounce((queryId, appId) => {
duplicateQuery(queryId, appId);
setPreviewData(null);
}, 500),
[duplicateQuery]
);
const getTooltip = () => {
const permission = dataQuery.permissions?.[0];
if (!permission) return null;
const users = permission.groups || permission.users || [];
if (users.length === 0) return null;
const isSingle = permission.type === 'SINGLE';
const isGroup = permission.type === 'GROUP';
if (isSingle) {
return users.length === 1
? `Access restricted to ${users[0].user.email}`
: `Access restricted to ${users.length} users`;
}
if (isGroup) {
return users.length === 1
? `Access restricted to ${users[0].permission_group?.name || users[0].permissionGroup?.name} group`
: `Access restricted to ${users.length} user groups`;
}
return null;
};
return (
<>
<div
className={`row query-row pe-2 ${darkMode && 'dark-theme'}` + (isQuerySelected ? ' query-row-selected' : '')}
key={dataQuery.id}
onClick={() => {
onClick={(e) => {
if (isQuerySelected) return;
setSelectedQuery(dataQuery?.id);
setPreviewData(null);
const menuBtn = document.getElementById(`query-handler-menu-${dataQuery?.id}`);
if (menuBtn.contains(e.target)) {
e.stopPropagation();
} else {
toggleQueryHandlerMenu(false);
}
setTimeout(() => {
setSelectedQuery(dataQuery?.id);
setPreviewData(null);
}, 0);
}}
role="button"
>
@ -94,7 +118,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
<DataSourceIcon source={dataQuery} height={16} />
</div>
<div className="col query-row-query-name">
{renamingQuery ? (
{isRenaming ? (
<input
data-cy={`query-edit-input-field`}
className={`query-name query-name-input-field border-indigo-09 bg-transparent ${
@ -121,7 +145,12 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
data-tooltip-dynamic="true"
>
{decodeEntities(dataQuery.name)}
</span>{' '}
</span>
<ToolTip message={getTooltip()} show={licenseValid && isRestricted}>
<div className="d-flex align-items-center" style={{ marginLeft: '8px', marginRight: 'auto' }}>
{licenseValid && isRestricted && <SolidIcon width="16" name="lock" fill="var(--icon-strong)" />}
</div>
</ToolTip>{' '}
{!isQueryRunnable(dataQuery) && <small className="mx-2 text-secondary">Draft</small>}
{localDs && (
<>
@ -143,80 +172,24 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
</div>
)}
</div>
{!shouldFreeze && isQuerySelected && (
<div className="col-auto query-rename-delete-btn">
<div
className={`col-auto ${(renamingQuery || !hasPermissions) && 'd-none'} rename-query`}
onClick={() => setRenamingQuery(true)}
>
<span className="d-flex" data-tooltip-id="query-card-btn-tooltip" data-tooltip-content="Rename query">
<svg
data-cy={`edit-query-${dataQuery.name.toLowerCase()}`}
width="100%"
height="100%"
viewBox="0 0 19 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.7087 1.40712C14.29 0.826221 15.0782 0.499893 15.9 0.499893C16.7222 0.499893 17.5107 0.82651 18.0921 1.40789C18.6735 1.98928 19.0001 2.7778 19.0001 3.6C19.0001 4.42197 18.6737 5.21028 18.0926 5.79162C18.0924 5.79178 18.0928 5.79145 18.0926 5.79162L16.8287 7.06006C16.7936 7.11191 16.753 7.16118 16.7071 7.20711C16.6621 7.25215 16.6138 7.292 16.563 7.32665L9.70837 14.2058C9.52073 14.3942 9.26584 14.5 9 14.5H6C5.44772 14.5 5 14.0523 5 13.5V10.5C5 10.2342 5.10585 9.97927 5.29416 9.79163L12.1733 2.93697C12.208 2.88621 12.2478 2.83794 12.2929 2.79289C12.3388 2.74697 12.3881 2.70645 12.4399 2.67132L13.7079 1.40789C13.7082 1.40763 13.7084 1.40738 13.7087 1.40712ZM13.0112 4.92545L7 10.9153V12.5H8.58474L14.5745 6.48876L13.0112 4.92545ZM15.9862 5.07202L14.428 3.51376L15.1221 2.82211C15.3284 2.6158 15.6082 2.49989 15.9 2.49989C16.1918 2.49989 16.4716 2.6158 16.6779 2.82211C16.8842 3.02842 17.0001 3.30823 17.0001 3.6C17.0001 3.89177 16.8842 4.17158 16.6779 4.37789L15.9862 5.07202ZM0.87868 5.37868C1.44129 4.81607 2.20435 4.5 3 4.5H4C4.55228 4.5 5 4.94772 5 5.5C5 6.05228 4.55228 6.5 4 6.5H3C2.73478 6.5 2.48043 6.60536 2.29289 6.79289C2.10536 6.98043 2 7.23478 2 7.5V16.5C2 16.7652 2.10536 17.0196 2.29289 17.2071C2.48043 17.3946 2.73478 17.5 3 17.5H12C12.2652 17.5 12.5196 17.3946 12.7071 17.2071C12.8946 17.0196 13 16.7652 13 16.5V15.5C13 14.9477 13.4477 14.5 14 14.5C14.5523 14.5 15 14.9477 15 15.5V16.5C15 17.2957 14.6839 18.0587 14.1213 18.6213C13.5587 19.1839 12.7957 19.5 12 19.5H3C2.20435 19.5 1.44129 19.1839 0.87868 18.6213C0.31607 18.0587 0 17.2957 0 16.5V7.5C0 6.70435 0.31607 5.94129 0.87868 5.37868Z"
fill="#11181C"
/>
</svg>
</span>
</div>
<div
className={`col-auto rename-query ${!hasPermissions && 'd-none'}`}
onClick={() => debouncedDuplicateQuery(dataQuery?.id, appId)}
>
<span className="d-flex" data-tooltip-id="query-card-btn-tooltip" data-tooltip-content="Duplicate query">
<Copy height={16} width={16} viewBox="0 5 20 20" />
</span>
</div>
<div className="col-auto">
{isDeletingQueryInProcess ? (
<div className="px-2">
<div className="text-center spinner-border spinner-border-sm" role="status"></div>
</div>
) : (
<span
className={`delete-query ${!hasPermissions && 'd-none'}`}
onClick={deleteDataQuery}
data-tooltip-id="query-card-btn-tooltip"
data-tooltip-content="Delete query"
>
<span className="d-flex">
<svg
data-cy={`delete-query-${dataQuery.name.toLowerCase()}`}
width="100%"
height="100%"
viewBox="0 0 18 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.58579 0.585786C5.96086 0.210714 6.46957 0 7 0H11C11.5304 0 12.0391 0.210714 12.4142 0.585786C12.7893 0.960859 13 1.46957 13 2V4H15.9883C15.9953 3.99993 16.0024 3.99993 16.0095 4H17C17.5523 4 18 4.44772 18 5C18 5.55228 17.5523 6 17 6H16.9201L15.9997 17.0458C15.9878 17.8249 15.6731 18.5695 15.1213 19.1213C14.5587 19.6839 13.7957 20 13 20H5C4.20435 20 3.44129 19.6839 2.87868 19.1213C2.32687 18.5695 2.01223 17.8249 2.00035 17.0458L1.07987 6H1C0.447715 6 0 5.55228 0 5C0 4.44772 0.447715 4 1 4H1.99054C1.9976 3.99993 2.00466 3.99993 2.0117 4H5V2C5 1.46957 5.21071 0.960859 5.58579 0.585786ZM3.0868 6L3.99655 16.917C3.99885 16.9446 4 16.9723 4 17C4 17.2652 4.10536 17.5196 4.29289 17.7071C4.48043 17.8946 4.73478 18 5 18H13C13.2652 18 13.5196 17.8946 13.7071 17.7071C13.8946 17.5196 14 17.2652 14 17C14 16.9723 14.0012 16.9446 14.0035 16.917L14.9132 6H3.0868ZM11 4H7V2H11V4ZM6.29289 10.7071C5.90237 10.3166 5.90237 9.68342 6.29289 9.29289C6.68342 8.90237 7.31658 8.90237 7.70711 9.29289L9 10.5858L10.2929 9.29289C10.6834 8.90237 11.3166 8.90237 11.7071 9.29289C12.0976 9.68342 12.0976 10.3166 11.7071 10.7071L10.4142 12L11.7071 13.2929C12.0976 13.6834 12.0976 14.3166 11.7071 14.7071C11.3166 15.0976 10.6834 15.0976 10.2929 14.7071L9 13.4142L7.70711 14.7071C7.31658 15.0976 6.68342 15.0976 6.29289 14.7071C5.90237 14.3166 5.90237 13.6834 6.29289 13.2929L7.58579 12L6.29289 10.7071Z"
fill="#DB4324"
/>
</svg>
</span>
</span>
)}
</div>
<Tooltip id="query-card-btn-tooltip" className="tooltip" />
</div>
)}
<div className={`col-auto query-rename-delete-btn ${!shouldFreeze && isQuerySelected ? 'd-flex' : 'd-none'}`}>
<ButtonComponent
iconOnly
leadingIcon="morevertical01"
onClick={(e) => toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)}
size="small"
variant="outline"
className=""
id={`query-handler-menu-${dataQuery?.id}`}
/>
</div>
</div>
<Confirm
show={showDeleteConfirmation}
show={isDeleting}
message={'Do you really want to delete this query?'}
confirmButtonLoading={isDeletingQueryInProcess}
onConfirm={executeDataQueryDeletion}
onCancel={() => setShowDeleteConfirmation(false)}
onCancel={() => deleteDataQuery(null)}
darkMode={darkMode}
/>
</>

View file

@ -0,0 +1,174 @@
import React, { useCallback } from 'react';
import { Overlay, Popover } from 'react-bootstrap';
import useStore from '@/AppBuilder/_stores/store';
import classNames from 'classnames';
import Edit from '@/_ui/Icon/bulkIcons/Edit';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import Copy from '@/_ui/Icon/solidIcons/Copy';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { shallow } from 'zustand/shallow';
import { ToolTip } from '@/_components/ToolTip';
import { debounce } from 'lodash';
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
const QueryCardMenu = ({ darkMode }) => {
const { moduleId } = useModuleContext();
const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
const selectedQuery = useStore((state) => state.queryPanel.selectedQuery);
const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal);
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
const targetBtnForMenu = useStore((state) => state.queryPanel.targetBtnForMenu);
const targetElement = document.getElementById(targetBtnForMenu);
const showQueryHandlerMenu = useStore((state) => state.queryPanel.showQueryHandlerMenu);
const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu);
const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery);
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery);
const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery);
const QUERY_MENU_OPTIONS = [
{
label: 'Rename',
value: 'rename',
icon: <Edit width={16} />,
showTooltip: false,
},
{
label: 'Duplicate',
value: 'duplicate',
icon: <Copy width={16} />,
showTooltip: false,
},
{
label: 'Query permission',
value: 'permission',
icon: (
<img
alt="permission-icon"
src="assets/images/icons/editor/left-sidebar/authorization.svg"
width="16"
height="16"
/>
),
trailingIcon: <SolidIcon width={16} name="enterprisecrown" className="mx-1" />,
},
{
label: 'Delete',
value: 'delete',
icon: <Trash width={16} fill={'#E54D2E'} />,
showTooltip: false,
},
];
// To prevent user clicking from continuous clicks
const debouncedDuplicateQuery = useCallback(
debounce((queryId, appId) => {
duplicateQuery(queryId, appId);
setPreviewData(null);
}, 500),
[duplicateQuery]
);
const handleQueryMenuActions = (value) => {
if (value === 'rename') {
setRenamingQuery(selectedQuery?.id);
}
if (value === 'duplicate') {
debouncedDuplicateQuery(selectedQuery?.id, appId);
}
if (value === 'permission') {
if (!licenseValid) return;
toggleQueryPermissionModal(true);
}
if (value === 'delete') {
deleteDataQuery(selectedQuery?.id);
}
toggleQueryHandlerMenu(false);
};
usePopoverObserver(
document.getElementsByClassName('query-list')[0],
targetElement,
document.getElementById('query-list-menu'),
showQueryHandlerMenu,
() => (document.getElementById('query-list-menu').style.display = 'block'),
() => (document.getElementById('query-list-menu').style.display = 'none')
);
return (
<Overlay
placement="bottom-start"
target={targetElement}
show={showQueryHandlerMenu}
rootClose
onHide={() => toggleQueryHandlerMenu(false)}
popperConfig={{
modifiers: [
{
name: 'flip',
options: {
fallbackPlacements: ['top-start'],
flipVariations: true,
allowedAutoPlacements: ['top', 'bottom'],
boundary: 'viewport',
},
},
{
name: 'offset',
options: {
offset: [0, 3],
},
},
],
}}
>
{(props) => (
<Popover {...props} id="query-list-menu" className={darkMode && 'dark-theme'}>
<Popover.Body bsPrefix="list-item-popover-body">
{QUERY_MENU_OPTIONS.map((option) => {
const optionBody = (
<div
data-cy={`component-inspector-${String(option?.value).toLowerCase()}-button`}
className="list-item-popover-option"
key={option?.value}
onClick={(e) => {
e.stopPropagation();
handleQueryMenuActions(option.value);
}}
>
<div className="list-item-popover-menu-option-icon">{option.icon}</div>
<div
className={classNames('list-item-option-menu-label', {
'color-tomato9': option.value === 'delete',
'color-disabled': option.value === 'permission' && !licenseValid,
})}
>
{option?.label}
</div>
{option.value === 'permission' && !licenseValid && option.trailingIcon && option.trailingIcon}
</div>
);
return option.value === 'permission' ? (
<ToolTip
key={option.value}
message={'Component permissions are available only in paid plans'}
placement="left"
show={!licenseValid}
>
{optionBody}
</ToolTip>
) : (
optionBody
);
})}
</Popover.Body>
</Popover>
)}
</Overlay>
);
};
export default QueryCardMenu;

View file

@ -16,6 +16,10 @@ import DataSourceSelect from '../QueryManager/Components/DataSourceSelect';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import FolderEmpty from '@/_ui/Icon/solidIcons/FolderEmpty';
import useStore from '@/AppBuilder/_stores/store';
import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal';
import { shallow } from 'zustand/shallow';
import { appPermissionService } from '@/_services';
import QueryCardMenu from './QueryCardMenu';
export const QueryDataPane = ({ darkMode }) => {
const { t } = useTranslation();
@ -34,6 +38,12 @@ export const QueryDataPane = ({ darkMode }) => {
function isDataSourceLocal(dataQuery) {
return dataSources.some((dataSource) => dataSource.id === dataQuery.data_source_id);
}
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
const selectedQuery = useStore((state) => state.queryPanel.selectedQuery);
const showQueryPermissionModal = useStore((state) => state.queryPanel.showQueryPermissionModal);
const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal);
const setQueries = useStore((state) => state.dataQuery.setQueries);
useEffect(() => {
setQueryPanelSearchTerm(searchTermForFilters);
@ -171,6 +181,33 @@ export const QueryDataPane = ({ darkMode }) => {
{filteredQueries.map((query) => (
<QueryCard key={query.id} dataQuery={query} darkMode={darkMode} localDs={!!isDataSourceLocal(query)} />
))}
<QueryCardMenu darkMode={darkMode} />
{licenseValid && (
<AppPermissionsModal
modalType="query"
resourceId={selectedQuery?.id}
resourceName={selectedQuery?.name}
showModal={showQueryPermissionModal}
toggleModal={toggleQueryPermissionModal}
darkMode={darkMode}
fetchPermission={(id, appId) => appPermissionService.getQueryPermission(appId, id)}
createPermission={(id, appId, body) => appPermissionService.createQueryPermission(appId, id, body)}
updatePermission={(id, appId, body) => appPermissionService.updateQueryPermission(appId, id, body)}
deletePermission={(id, appId) => appPermissionService.deleteQueryPermission(appId, id)}
onSuccess={(data) => {
const updatedDataQueries = dataQueries.map((query) => {
if (query.id === selectedQuery.id) {
return {
...query,
permissions: data.length === 0 || data.length === undefined ? [] : [data[0]],
};
}
return query;
});
setQueries(updatedDataQueries);
}}
/>
)}
</div>
<Tooltip
id="query-card-name-tooltip"

View file

@ -4,12 +4,33 @@ import { Inspector } from '@/AppBuilder/RightSideBar/Inspector/Inspector';
import useStore from '@/AppBuilder/_stores/store';
import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants';
import { shallow } from 'zustand/shallow';
import SolidIcon from '@/_ui/Icon/SolidIcons';
export const ComponentConfigurationTab = ({ darkMode, isModuleEditor }) => {
const selectedComponentId = useStore((state) => state.selectedComponents?.[0], shallow);
const activeTab = useStore((state) => state.activeRightSideBarTab, shallow);
const toggleRightSidebarPin = useStore((state) => state.toggleRightSidebarPin);
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned);
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab);
if (!selectedComponentId) {
return setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.COMPONENTS);
if (!selectedComponentId && activeTab !== RIGHT_SIDE_BAR_TAB.PAGES) {
// return setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.COMPONENTS);
return (
<>
<div className="empty-configuration-header">
<div className="header">Component properties</div>
<div className="icon-btn cursor-pointer" onClick={() => toggleRightSidebarPin()}>
<SolidIcon fill="var(--icon-strong)" name={isRightSidebarPinned ? 'unpin' : 'pin'} width="16" />
</div>
</div>
<div className="d-flex align-items-center justify-content-center no-component-selected">
<SolidIcon name="cursorclick" width="28" />
<div className="tj-text-sm font-weight-500 heading">No component selected</div>
<div className="tj-text-xsm sub-heading">
Click a component on the canvas to view and edit its properties.
</div>
</div>
</>
);
}
return (
<Inspector

View file

@ -4,6 +4,8 @@
height: 36px;
margin-bottom: 8px;
margin-top: 16px;
margin-left: 16px;
margin-right: 16px;
}
.tj-tabs-container {

View file

@ -7,6 +7,9 @@ import Fuse from 'fuse.js';
import { SearchBox } from '@/_components';
import { DragLayer } from './DragLayer';
import useStore from '@/AppBuilder/_stores/store';
import Accordion from '@/_ui/Accordion';
import sectionConfig from './sectionConfig';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { ModuleManager } from '@/modules/Modules/components';
import { ComponentModuleTab } from '@/modules/Appbuilder/components';
@ -28,12 +31,11 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
const _shouldFreeze = useStore((state) => state.getShouldFreeze());
const isAutoMobileLayout = useStore((state) => state.currentLayout === 'mobile' && state.getIsAutoMobileLayout());
const shouldFreeze = _shouldFreeze || isAutoMobileLayout;
const toggleRightSidebarPin = useStore((state) => state.toggleRightSidebarPin);
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned);
const handleSearchQueryChange = useCallback(
debounce((e) => {
const { value } = e.target;
debounce((value) => {
setSearchQuery(value);
if (activeTab === 1) {
filterComponents(value);
}
@ -78,11 +80,10 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
);
}
function renderList(header, items) {
function renderList(items) {
if (isEmpty(items)) return null;
return (
<div className="component-card-group-container">
<span className="widget-header">{header}</span>
<div className="component-card-group-wrapper">
{items.map((component, i) => renderComponentCard(component, i))}
</div>
@ -105,6 +106,7 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
className=" btn-sm tj-tertiary-btn mt-3"
onClick={() => {
setFilteredComponents([]);
handleSearchQueryChange('');
}}
>
{t('widgetManager.clearQuery', 'clear query')}
@ -113,62 +115,31 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
);
}
if (filteredComponents.length != componentList.length) {
return <>{renderList(undefined, filteredComponents)}</>;
} else {
const commonSection = { title: t('widgetManager.commonlyUsed', 'commonly used'), items: [] };
const layoutsSection = { title: t('widgetManager.layouts', 'layouts'), items: [] };
const formSection = { title: t('widgetManager.forms', 'forms'), items: [] };
const integrationSection = { title: t('widgetManager.integrations', 'integrations'), items: [] };
const otherSection = { title: t('widgetManager.others', 'others'), items: [] };
const legacySection = { title: 'Legacy', items: [] };
const commonItems = ['Table', 'Button', 'Text', 'TextInput', 'DatetimePickerV2', 'Form'];
const formItems = [
'Form',
'TextInput',
'NumberInput',
'PasswordInput',
'TextArea',
'EmailInput',
'PhoneInput',
'CurrencyInput',
'ToggleSwitchV2',
'DropdownV2',
'MultiselectV2',
'RichTextEditor',
'Checkbox',
'RadioButtonV2',
'DatetimePickerV2',
'DatePickerV2',
'TimePicker',
'DaterangePicker',
'FilePicker',
'StarRating',
];
const integrationItems = ['Map'];
const layoutItems = ['Container', 'Listview', 'Tabs', 'ModalV2'];
filteredComponents.forEach((f) => {
if (commonItems.includes(f)) commonSection.items.push(f);
if (formItems.includes(f)) formSection.items.push(f);
else if (integrationItems.includes(f)) integrationSection.items.push(f);
else if (LEGACY_ITEMS.includes(f)) legacySection.items.push(f);
else if (layoutItems.includes(f)) layoutsSection.items.push(f);
else otherSection.items.push(f);
});
return (
<>
{renderList(commonSection.title, commonSection.items)}
{renderList(layoutsSection.title, layoutsSection.items)}
{renderList(formSection.title, formSection.items)}
{renderList(otherSection.title, otherSection.items)}
{renderList(integrationSection.title, integrationSection.items)}
{renderList(legacySection.title, legacySection.items)}
</>
);
if (filteredComponents.length !== componentList.length) {
return <>{renderList(filteredComponents)}</>;
}
const sections = Object.entries(sectionConfig).map(([key, config]) => ({
title: config.title,
items: filteredComponents.filter((component) => config.valueSet.has(component)),
}));
const items = [];
sections.forEach((section) => {
if (section.items.length > 0) {
items.push({
title: section.title,
isOpen: true,
children: renderList(section.items),
});
}
});
return (
<div className="mt-3">
<Accordion items={items} isTitleCase={false} />
</div>
);
}
const handleChangeTab = (tab) => {
@ -195,7 +166,7 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
<SearchBox
dataCy={`widget-search-box`}
initialValue={''}
callBack={(e) => handleSearchQueryChange(e)}
callBack={(e) => handleSearchQueryChange(e.target.value)}
onClearCallback={() => {
setSearchQuery('');
if (activeTab === 1) {

View file

@ -11,6 +11,11 @@ import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { noop } from 'lodash';
export const DragLayer = ({ index, component, isModuleTab = false }) => {
const [isRightSidebarOpen, toggleRightSidebar] = useStore(
(state) => [state.isRightSidebarOpen, state.toggleRightSidebar],
shallow
);
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned);
const { isModuleEditor } = useModuleContext();
const setShowModuleBorder = useStore((state) => state.setShowModuleBorder, shallow) || noop;
const [{ isDragging }, drag, preview] = useDrag(
@ -28,11 +33,14 @@ export const DragLayer = ({ index, component, isModuleTab = false }) => {
useEffect(() => {
if (isDragging && !isModuleEditor) {
if (!isRightSidebarPinned) {
toggleRightSidebar(!isRightSidebarOpen);
}
setShowModuleBorder(true);
} else {
setShowModuleBorder(false);
}
}, [isDragging, setShowModuleBorder, isModuleEditor]);
}, [isDragging, setShowModuleBorder, isModuleEditor, toggleRightSidebar]);
// const size = isModuleTab
// ? component.module_container.layouts[currentLayout]
@ -55,36 +63,43 @@ const CustomDragLayer = ({ size }) => {
currentOffset: monitor.getSourceClientOffset(),
item: monitor.getItem(),
}));
console.log(currentOffset, 'currentOffset');
if (!currentOffset) return null;
const canvasWidth = item?.canvasWidth;
const canvasBounds = item?.canvasRef?.getBoundingClientRect();
const height = size.height;
const mainCanvasWidth = document.getElementById('real-canvas')?.offsetWidth || 0;
const appCanvasWidth = document.getElementById('real-canvas')?.offsetWidth || 0;
// Calculate width based on the app canvas's grid
let width = (appCanvasWidth * size.width) / NO_OF_GRIDS;
let width = (mainCanvasWidth * size.width) / NO_OF_GRIDS;
// Calculate position relative to the current canvas (parent or child)
const left = currentOffset.x - (canvasBounds?.left || 0);
const top = currentOffset.y - (canvasBounds?.top || 0);
// Adjust position and width if exceeding grid bounds
if (width >= canvasWidth) {
// Ensure width doesn't exceed the current container's width
if (width > canvasWidth) {
width = canvasWidth;
}
// Snap width to grid (round to nearest grid unit)
const gridUnitWidth = canvasWidth / NO_OF_GRIDS;
const gridUnits = Math.round(width / gridUnitWidth);
width = gridUnits * gridUnitWidth;
const [x, y] = snapToGrid(canvasWidth, left, top);
return (
<div
style={{
position: 'fixed',
pointerEvents: 'none',
zIndex: 1000,
left: canvasBounds?.left || 0,
top: canvasBounds?.top || 0,
height: `${height}px`,
width: `${width}px`,
zIndex: -1,
}}
>
<div

View file

@ -0,0 +1,70 @@
const sectionConfig = {
commonlyUsed: {
title: 'Commonly used',
valueSet: new Set(['Table', 'Button', 'Text', 'TextInput', 'DatetimePickerV2', 'Form']),
},
buttons: {
title: 'Buttons',
valueSet: new Set(['Button', 'ButtonGroup']),
},
data: {
title: 'Data',
valueSet: new Set(['Table', 'Chart']),
},
layouts: {
title: 'Layouts',
valueSet: new Set(['Form', 'ModalV2', 'Container', 'Tabs', 'Listview', 'Kanban', 'Calendar']),
},
textInputs: {
title: 'Text inputs',
valueSet: new Set(['TextInput', 'TextArea', 'EmailInput', 'PasswordInput', 'RichTextEditor']),
},
numberInputs: {
title: 'Number inputs',
valueSet: new Set(['NumberInput', 'PhoneInput', 'CurrencyInput', 'RangeSlider', 'StarRating']),
},
selectInputs: {
title: 'Select inputs',
valueSet: new Set(['DropdownV2', 'MultiselectV2', 'ToggleSwitchV2', 'RadioButtonV2', 'Checkbox', 'TreeSelect']),
},
dateTimeInputs: {
title: 'Date and time inputs',
valueSet: new Set(['DaterangePicker', 'DatePickerV2', 'TimePicker', 'DatetimePickerV2']),
},
navigation: {
title: 'Navigation',
valueSet: new Set(['Link', 'Pagination', 'Steps']),
},
media: {
title: 'Media',
valueSet: new Set(['Icon', 'Image', 'SvgImage', 'PDF', 'Map']),
},
presentation: {
title: 'Presentation',
valueSet: new Set([
'Text',
'Tags',
'CircularProgressBar',
'Timeline',
'Divider',
'VerticalDivider',
'Spinner',
'Statistics',
'Timer',
]),
},
custom: {
title: 'Custom',
valueSet: new Set(['CustomComponent', 'Html', 'IFrame']),
},
miscellaneous: {
title: 'Miscellaneous',
valueSet: new Set(['FilePicker', 'CodeEditor', 'ColorPicker', 'BoundedBox', 'QrScanner']),
},
legacy: {
title: 'Legacy',
valueSet: new Set(['Modal', 'Datepicker', 'RadioButton', 'ToggleSwitch', 'DropDown', 'Multiselect']),
},
};
export default sectionConfig;

View file

@ -27,10 +27,18 @@ const SHOW_ADDITIONAL_ACTIONS = [
'Button',
'RichTextEditor',
'Image',
'CodeEditor',
'TextArea',
'Container',
'Form',
'Divider',
'VerticalDivider',
'ModalV2',
'Tabs',
'RangeSlider',
'Link',
'FilePicker',
'Listview',
];
const PROPERTIES_VS_ACCORDION_TITLE = {
Text: 'Data',
@ -46,6 +54,8 @@ const PROPERTIES_VS_ACCORDION_TITLE = {
Divider: 'Data',
VerticalDivider: 'Data',
ModalV2: 'Data',
Tabs: 'Data',
RangeSlider: 'Data',
Link: 'Data',
};
@ -144,9 +154,12 @@ export const baseComponentProperties = (
'DropdownV2',
'MultiselectV2',
'Image',
'RangeSlider',
'Divider',
'VerticalDivider',
'Link',
'FilePicker',
'Tabs',
],
Layout: [],
};

View file

@ -1,8 +1,114 @@
import React from 'react';
import React, { useState } from 'react';
import Accordion from '@/_ui/Accordion';
import { renderElement } from '../Utils';
import { baseComponentProperties } from './DefaultComponent';
import { resolveReferences } from '@/_helpers/utils';
import cx from 'classnames';
import styles from '@/_ui/Select/styles';
import useStore from '@/AppBuilder/_stores/store';
import Select from '@/_ui/Select';
import CodeHinter from '@/AppBuilder/CodeEditor';
import FxButton from '@/AppBuilder/CodeBuilder/Elements/FxButton';
const FILE_TYPE_OPTIONS = [
{ value: '*/*', label: 'Any Files' },
{ value: 'image/*', label: 'Image files' },
{ value: '.pdf,.doc,.docx,.ppt,.pptx', label: 'Document files' },
{ value: '.xls,.xlsx,.csv,.ods', label: 'Spreadsheet files' },
{ value: 'text/*,.md,.json,.xml,.yaml', label: 'Text files' },
{ value: 'audio/*', label: 'Audio files' },
{ value: 'video/*', label: 'Video files' },
{ value: '.zip,.rar,.7z,.tar,.gz', label: 'Archive/Compressed files' },
];
const FxSelect = ({ label, paramName, initialValue, darkMode, paramUpdated, options, onValueChange }) => {
const [isFxActive, setIsFxActive] = useState(false);
const handleFxButtonClick = () => {
paramUpdated({ name: paramName }, 'fxActive', !isFxActive, 'properties');
setIsFxActive(!isFxActive);
};
return (
<div
data-cy={`input-date-display-format`}
className="field mb-2 w-100 input-date-display-format"
onClick={(e) => e.stopPropagation()}
>
<div className="field mb-2" onClick={(e) => e.stopPropagation()}>
<div className="d-flex justify-content-between mb-1">
<label className="form-label">{label}</label>
<div className={cx({ 'hide-fx': !isFxActive })}>
<FxButton active={isFxActive} onPress={handleFxButtonClick} />
</div>
</div>
{isFxActive ? (
<CodeHinter
initialValue={initialValue}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
onChange={onValueChange}
/>
) : (
<Select
options={options}
value={initialValue ?? '*/*'}
search={true}
closeOnSelect={true}
onChange={onValueChange}
fuzzySearch
placeholder="Select.."
useCustomStyles={true}
styles={styles(darkMode, '100%', 32, { fontSize: '12px' })}
/>
)}
</div>
</div>
);
};
/** Remove minFileCount and maxFileCount validations if multiple file selection is disabled */
const getValidations = (componentMeta, component) => {
const validations = Object.keys(componentMeta.validation || {});
const enableMultipleValue = resolveReferences(component.component.definition.properties.enableMultiple?.value ?? false);
const enableMultipleFxActive = component.component.definition.properties.enableMultiple?.fxActive;
if (!enableMultipleValue && !enableMultipleFxActive) {
return validations.filter((validation) => !['minFileCount', 'maxFileCount'].includes(validation));
}
return validations;
};
const getPropertiesBySection = (propertiesMeta) => {
const properties = [];
const additionalActions = [];
const dataProperties = [];
for (const [key, value] of Object.entries(propertiesMeta)) {
if (value?.section === 'additionalActions') {
additionalActions.push(key);
} else if (value?.accordian === 'Data') {
dataProperties.push(key);
} else {
properties.push(key);
}
}
return { properties, additionalActions, dataProperties };
};
const getConditionalAccordionItems = (component, renderCustomElement) => {
const parseContent = resolveReferences(component.component.definition.properties.parseContent?.value ?? false);
const options = ['parseContent'];
let renderOptions = options.map((option) => renderCustomElement(option));
const conditionalOptions = [{ name: 'parseFileType', condition: parseContent }];
conditionalOptions.forEach(({ name, condition }) => {
if (condition) renderOptions.push(renderCustomElement(name));
});
return renderOptions;
};
export const FilePicker = ({ componentMeta, darkMode, ...restProps }) => {
const {
@ -16,38 +122,22 @@ export const FilePicker = ({ componentMeta, darkMode, ...restProps }) => {
allComponents,
} = restProps;
const renderCustomElement = (param, paramType = 'properties') => {
return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
};
const conditionalAccordionItems = (component) => {
const parseContent = resolveReferences(component.component.definition.properties.parseContent?.value ?? false);
const accordionItems = [];
const options = ['parseContent'];
const resolvedValidations = useStore((state) => state.getResolvedComponent(component.id)?.validation);
const fileTypeValue = resolvedValidations?.fileType;
let renderOptions = [];
const renderCustomElement = (param, paramType = 'properties') =>
renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
options.map((option) => renderOptions.push(renderCustomElement(option)));
// Debug logs
// console.log('component.component.definition', component.component.definition);
const conditionalOptions = [{ name: 'parseFileType', condition: parseContent }];
conditionalOptions.map(({ name, condition }) => {
if (condition) renderOptions.push(renderCustomElement(name));
});
accordionItems.push({
title: 'Options',
children: renderOptions,
});
return accordionItems;
};
const properties = Object.keys(componentMeta.properties);
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
const validations = getValidations(componentMeta, component);
const filteredProperties = properties.filter(
(property) => property !== 'parseContent' && property !== 'parseFileType'
);
// console.log('validations', validations, enableMultipleValue, component.component.definition.properties.enableMultiple?.value, enableMultipleFxActive);
const { additionalActions, dataProperties } = getPropertiesBySection(componentMeta?.properties);
const filteredProperties = [...dataProperties];
const accordionItems = baseComponentProperties(
filteredProperties,
@ -62,10 +152,26 @@ export const FilePicker = ({ componentMeta, darkMode, ...restProps }) => {
apps,
allComponents,
validations,
darkMode
darkMode,
[],
additionalActions
);
accordionItems.splice(1, 0, ...conditionalAccordionItems(component));
// Insert conditional accordion items
accordionItems[0].children.push(...getConditionalAccordionItems(component, renderCustomElement));
// Insert FxSelect for file type
accordionItems[2].children[1] = (
<FxSelect
label={'File type'}
paramName="fileType"
initialValue={fileTypeValue}
darkMode={darkMode}
paramUpdated={paramUpdated}
options={FILE_TYPE_OPTIONS}
onValueChange={(value) => paramUpdated({ name: 'fileType' }, 'value', value, 'validation')}
/>
);
return <Accordion items={accordionItems} />;
};

View file

@ -0,0 +1,86 @@
import React from 'react';
import Accordion from '@/_ui/Accordion';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { useFormLogic } from './_hooks';
import { processComponentMeta } from './utils/componentMetaUtils';
import { createAccordionItems } from './config/accordionConfig';
import { DataSection } from './_components';
import './styles.scss';
export const Form = ({
componentMeta,
darkMode,
layoutPropertyChanged,
component,
paramUpdated,
dataQueries,
currentState,
eventsChanged,
apps,
allComponents,
pages,
}) => {
const resolveReferences = useStore((state) => state.resolveReferences, shallow);
// Use the combined form logic hook
const formLogic = useFormLogic(component, paramUpdated);
// Get resolved custom schema
const resolvedCustomSchema = resolveReferences('canvas', component.component.definition.properties.advanced.value);
// Process component metadata
const { tempComponentMeta, properties, additionalActions, deprecatedProperties, events, validations } =
processComponentMeta(componentMeta, component, allComponents, resolvedCustomSchema);
// Create render data element function
const renderDataElement = DataSection({
component,
componentMeta,
paramUpdatedInterceptor: formLogic.paramUpdatedInterceptor,
dataQueries,
currentState,
allComponents,
darkMode,
resolvedCustomSchema,
source: formLogic.source,
JSONData: formLogic.JSONData,
setCodeEditorView: formLogic.setCodeEditorView,
currentStatusRef: formLogic.currentStatusRef,
saveDataSection: formLogic.saveDataSection,
openModal: formLogic.openModal,
setParentModalState: formLogic.setOpenModal,
performColumnMapping: formLogic.performColumnMapping,
existingResolvedJsonData: formLogic.existingResolvedJsonData,
savedSourceValue: formLogic.savedSourceValue.current,
resolveReferences,
isLoading: formLogic.isLoading,
});
// Create accordion items
const accordionItems = createAccordionItems({
properties,
events,
component,
componentMeta: tempComponentMeta,
layoutPropertyChanged,
paramUpdated: formLogic.paramUpdatedInterceptor,
dataQueries,
currentState,
eventsChanged,
apps,
allComponents,
validations,
darkMode,
pages,
additionalActions,
deprecatedProperties,
renderDataElement,
});
return (
<>
<Accordion items={accordionItems} />
</>
);
};

View file

@ -0,0 +1,530 @@
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/Button/Button';
import Checkbox from '@/components/ui/Checkbox/Index';
import cx from 'classnames';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import Modal from 'react-bootstrap/Modal';
import Dropdown from '@/components/ui/Dropdown/Index';
import Input from '@/components/ui/Input/Index';
import { getInputTypeOptions, isTrueValue, isPropertyFxControlled } from '../utils/utils';
import { FORM_STATUS } from '../constants';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast';
import Loader from '@/ToolJetUI/Loader/Loader';
import { useColumnBuilder, useGroupedColumns, useCheckboxStates } from './hooks/useColumnMapping';
// Constants for section display names
const SECTION_DISPLAY_NAMES = {
existing: 'Existing',
isCustomField: 'Custom fields',
isNew: 'New',
isRemoved: 'Removed',
};
/**
* Reusable editable icon component
*/
const EditableIcon = ({ darkMode }) => (
<div className="tw-mr-2 editable-icon">
<SolidIcon name="editable" width="12" height="12" fill={darkMode ? '#4C5155' : '#C1C8CD'} viewBox="0 0 12 12" />
</div>
);
/**
* Modal header component
*/
const ModalHeader = ({ currentStatus, onClose }) => (
<div className="column-mapping-modal-header tw-flex tw-p-4 tw-flex-col tw-items-start tw-gap-2 tw-self-stretch tw-border-b bg-white">
<div className="tw-flex tw-justify-between tw-items-center tw-w-full" style={{ height: '28px' }}>
<h4 className="text-default tw-font-ibmplex tw-font-medium tw-leading-5 tw-m-0">
{currentStatus !== FORM_STATUS.GENERATE_FIELDS ? 'Manage fields' : 'Map columns'}
</h4>
<button className="tw-bg-transparent tw-border-0 tw-p-0 tw-cursor-pointer hover:tw-opacity-70" onClick={onClose}>
<SolidIcon name="remove" width="16" height="16" fill="#6A727C" />
</button>
</div>
</div>
);
/**
* Modal footer component
*/
const ModalFooter = ({ currentStatus, refreshData, handleSubmit, isSaving, allSectionsEmpty }) => (
<div
className={`tw-flex ${
currentStatus !== FORM_STATUS.GENERATE_FIELDS ? 'tw-justify-between' : 'tw-justify-end'
} tw-items-center`}
>
{currentStatus !== FORM_STATUS.GENERATE_FIELDS && (
<Button fill={'#ACB2B9'} leadingIcon={'arrowdirectionloop'} variant="outline" onClick={refreshData}>
Refresh data
</Button>
)}
<Button
variant="primary"
onClick={handleSubmit}
disabled={isSaving || allSectionsEmpty}
leadingIcon={currentStatus !== FORM_STATUS.GENERATE_FIELDS ? 'save' : 'plus'}
isLoading={isSaving}
loaderText={currentStatus !== FORM_STATUS.GENERATE_FIELDS ? 'Saving' : 'Generating'}
>
{currentStatus !== FORM_STATUS.GENERATE_FIELDS ? 'Save' : 'Generate form'}
</Button>
</div>
);
/**
* Loader component
*/
const LoaderComponent = () => (
<div className="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center tw-z-10">
<Loader width="32" absolute={false} />
</div>
);
/**
* Disable the checkbox if the property is fx controlled and it will not be included while selectAll is called.
* This is to prevent users from changing the state of fx controlled properties directly.
* Instead, they should use the fx editor to manage these properties.
*/
const ColumnMappingRow = ({
column,
onChange,
onCheckboxChange,
index,
darkMode = false,
disabled = false,
sectionType,
}) => {
if (!column) return null;
const inputTypeOptions = getInputTypeOptions(darkMode);
const isMandatoryFxControlled = isPropertyFxControlled(column.mandatory);
const handleLabelChange = (e) => {
onChange?.({
...column,
label: e.target.value,
});
};
const handleMandatoryChange = (checked) => {
if (typeof column.mandatory === 'object') {
onChange?.({
...column,
mandatory: {
...column.mandatory,
value: checked,
},
});
} else {
onChange?.({
...column,
mandatory: checked,
});
}
};
const handleInputTypeChange = (value) => {
onChange?.({
...column,
componentType: value,
});
};
const shouldHideCheckbox = sectionType === 'isCustomField';
return (
<div className="tw-flex tw-items-center tw-w-full tw-py-3 tw-px-2 tw-border-b tw-border-border-lighter column-mapping-row">
{/* Checkbox */}
<div className={cx(`tw-w-6`, { 'tw-invisible': disabled || shouldHideCheckbox })}>
<Checkbox checked={column.selected} onCheckedChange={onCheckboxChange} />
</div>
{/* Column Name and Type */}
<div className="name-column tw-flex tw-items-center base-regular tw-justify-between">
{column.key ? (
<>
<span className="base-regular">{column.key}</span>
<span className="tw-ml-2 data-type">{column.dataType}</span>
</>
) : (
<span className="no-mapped-column small-medium">No mapped columns</span>
)}
</div>
{/* Mapped To */}
<div
className={cx('arrow-column tw-flex tw-justify-center', {
'tw-invisible': column.key === undefined,
})}
>
<SolidIcon name="arrowright" width="24" height="24" fill="#CCD1D5" />
</div>
{/* Input Type Selector */}
<div className="mapped-column tw-relative hide-border">
<Dropdown
options={inputTypeOptions}
name={`dropdown-${index}`}
id={`dropdown-${index}`}
size="small"
zIndex={9999}
value={column.componentType || 'TextInput'}
leadingIcon={inputTypeOptions[column.componentType || 'TextInput'].leadingIcon}
onChange={handleInputTypeChange}
width="140px"
disabled={disabled}
/>
</div>
{/* Input Label */}
<div className="type-column rows tw-flex-1 hide-border">
<Input
value={column.label}
onChange={handleLabelChange}
placeholder="Input label"
size="small"
disabled={disabled}
/>
</div>
{/* Mandatory Checkbox */}
<div className={cx(`mandatory-column rows tw-flex tw-justify-end`, { 'tw-invisible': disabled })}>
<Checkbox
checked={isTrueValue(column.mandatory.value)}
onCheckedChange={handleMandatoryChange}
disabled={isMandatoryFxControlled} // Disable if fx controlled
/>
</div>
</div>
);
};
const RenderSection = ({
mappedColumns = [],
setMappedColumns,
darkMode,
sectionType,
sectionDisplayName,
disabled = false,
}) => {
const columnsArray = useMemo(() => {
return Array.isArray(mappedColumns) ? mappedColumns : [];
}, [mappedColumns]);
const checkboxStates = useCheckboxStates(columnsArray);
const { isAllSelected, isIntermediateSelected, isAllSelectedMandatory, isIntermediateMandatory } = checkboxStates;
const handleSelectAll = useCallback(
(checked) => {
if (columnsArray.length > 0) {
const updatedColumns = columnsArray.map((col) => ({
...col,
selected: checked,
}));
setMappedColumns(updatedColumns);
}
},
[columnsArray, setMappedColumns]
);
const handleSelectAllMandatory = useCallback(
(checked) => {
if (columnsArray.length > 0) {
const updatedColumns = columnsArray.map((col) => {
if (isPropertyFxControlled(col.mandatory)) {
return col;
}
return {
...col,
mandatory: {
...col.mandatory,
value: checked,
},
};
});
setMappedColumns(updatedColumns);
}
},
[columnsArray, setMappedColumns]
);
const handleColumnSelect = useCallback(
(columnName, checked) => {
if (columnsArray.length > 0) {
const updatedColumns = columnsArray.map((col) => {
if (col.name !== columnName) {
return col;
}
return {
...col,
selected: checked,
};
});
setMappedColumns(updatedColumns);
}
},
[columnsArray, setMappedColumns]
);
const handleColumnChange = useCallback(
(columnName, changes) => {
if (columnsArray.length > 0) {
const updatedColumns = columnsArray.map((col) => (col.name === columnName ? { ...col, ...changes } : col));
setMappedColumns(updatedColumns);
}
},
[columnsArray, setMappedColumns]
);
const shouldHideSelectAll = sectionType === 'isCustomField';
const renderHeader = () => {
return (
<div className="tw-flex tw-items-center tw-w-full tw-py-[10px] tw-px-2 header-row column-mapping-row">
<div className={cx(`tw-w-6 header-column`, { 'tw-invisible': disabled || shouldHideSelectAll })}>
<Checkbox
checked={isAllSelected || isIntermediateSelected}
onCheckedChange={handleSelectAll}
intermediate={isIntermediateSelected}
/>
</div>
<div className="name-column header-column">
<span className="text-default small-medium">Column name</span>
</div>
<div className="arrow-column header-column" />
<div className="mapped-column header-column tw-flex">
<EditableIcon darkMode={darkMode} />
<span className="text-default small-medium">Mapped to</span>
</div>
<div className="type-column tw-flex-1 header-column tw-flex">
<EditableIcon darkMode={darkMode} />
<span className="text-default small-medium">Input label</span>
</div>
<div className="mandatory-column header-column tw-flex tw-justify-end">
<span className="text-default small-medium tw-mr-2">Mandatory?</span>
<div className={cx({ 'tw-invisible': disabled })}>
<Checkbox
checked={isAllSelectedMandatory || isIntermediateMandatory}
onCheckedChange={handleSelectAllMandatory}
intermediate={!isAllSelectedMandatory && isIntermediateMandatory}
/>
</div>
</div>
</div>
);
};
return (
<div className="tw-w-full column-mapping-modal-body-content">
<div
className={cx('large-medium column-mapping-modal-title', {
new: sectionType === 'isNew',
removed: sectionType === 'isRemoved',
'tw-hidden': sectionDisplayName === '',
})}
>
{sectionDisplayName}
</div>
{renderHeader()}
<div>
{columnsArray.length > 0 ? (
columnsArray.map((column, index) => (
<ColumnMappingRow
key={column.name}
column={columnsArray.find((c) => c.name === column.name)}
onCheckboxChange={(checked) => handleColumnSelect(column.name, checked)}
onChange={(changes) => handleColumnChange(column.name, changes)}
index={index}
darkMode={darkMode}
disabled={disabled}
sectionType={sectionType}
/>
))
) : (
<div className="tw-py-4 tw-text-center tw-text-gray-500">No {sectionDisplayName.toLowerCase()} available</div>
)}
</div>
</div>
);
};
const ColumnMappingComponent = ({
isOpen,
onClose,
darkMode = false,
onSubmit,
currentStatusRef,
component,
newResolvedJsonData,
existingResolvedJsonData,
source,
isDataLoading,
}) => {
const { resolveReferences, getComponentDefinition, getFormFields } = useStore(
(state) => ({
resolveReferences: state.resolveReferences,
getComponentDefinition: state.getComponentDefinition,
getFormFields: state.getFormFields,
}),
shallow
);
const componentNameIdMapping = useStore((state) => state.modules.canvas.componentNameIdMapping, shallow);
const queryNameIdMapping = useStore((state) => state.modules.canvas.queryNameIdMapping, shallow);
const runQuery = useStore((state) => state.queryPanel.runQuery, shallow);
const [isSaving, setIsSaving] = useState(false);
const [refreshedColumns, setRefreshedColumns] = useState([]);
const [showLoader, setShowLoader] = useState(false);
const bodyContainerRef = useRef(null);
const lastBodyHeightRef = useRef(60);
useEffect(() => {
setShowLoader(isDataLoading);
}, [isDataLoading]);
// Track body height when content is loaded
useEffect(() => {
if (!showLoader && bodyContainerRef.current) {
// Use setTimeout to ensure DOM is fully rendered
setTimeout(() => {
if (bodyContainerRef.current) {
const height = bodyContainerRef.current.scrollHeight;
if (height > 0) {
lastBodyHeightRef.current = height;
}
}
}, 0);
}
}, [showLoader, groupedColumns]);
const currentStatus = currentStatusRef.current;
console.log('here--- existingResolvedJsonData--- ', existingResolvedJsonData);
const columnsToUse = useColumnBuilder(
component,
currentStatus,
newResolvedJsonData,
existingResolvedJsonData,
refreshedColumns?.length === 0 || Object.keys(refreshedColumns).length === 0
? newResolvedJsonData
: refreshedColumns,
getFormFields,
getComponentDefinition
);
const { groupedColumns, sectionTypes, updateSectionColumns } = useGroupedColumns(columnsToUse, currentStatus);
const refreshData = useCallback(async () => {
setShowLoader(true);
currentStatusRef.current = FORM_STATUS.REFRESH_FIELDS;
const res = extractAndReplaceReferencesFromString(source.value, componentNameIdMapping, queryNameIdMapping);
const { allRefs, valueWithBrackets } = res;
const queryRefs = allRefs
.filter((ref) => ref.entityType === 'queries')
.filter((ref, index, self) => index === self.findIndex((r) => r.entityNameOrId === ref.entityNameOrId));
await Promise.all(
queryRefs.map(async (ref) => {
const queryId = ref.entityNameOrId;
await runQuery(queryId, '', false, 'edit');
})
);
const resolvedValue = resolveReferences('canvas', valueWithBrackets);
setRefreshedColumns(resolvedValue);
setShowLoader(false);
}, [source.value, componentNameIdMapping, queryNameIdMapping, runQuery, resolveReferences, currentStatusRef]);
const handleSubmit = useCallback(() => {
setIsSaving(true);
const flatColumns = Object.entries(groupedColumns)
.flatMap(([, columns]) => columns)
.filter((col) => !col.isCustomField);
const combinedColumns = flatColumns.map((column) => {
if (!column.selected) {
return {
...column,
isRemoved: true,
};
} else return column;
});
onSubmit?.(combinedColumns);
}, [groupedColumns, onSubmit]);
// Get display name for section type
const getSectionDisplayName = useCallback((sectionType) => {
return SECTION_DISPLAY_NAMES[sectionType] || '';
}, []);
const allSectionsEmpty = useMemo(() => {
return Object.values(groupedColumns).every((sectionColumns) => {
return Array.isArray(sectionColumns) ? sectionColumns.every((col) => !col.selected) : true;
});
}, [groupedColumns]);
const modalBody = (
<>
<div
ref={bodyContainerRef}
className="tw-w-full column-mapping-modal-body-container tw-max-h-[500px] tw-overflow-y-auto tw-p-4 tw-pb-0 tw-relative"
style={showLoader && lastBodyHeightRef.current ? { minHeight: `${lastBodyHeightRef.current}px` } : undefined}
>
{showLoader && <LoaderComponent />}
{!showLoader && (
<div>
{sectionTypes.map((sectionType) => {
return (
groupedColumns[sectionType]?.length > 0 && (
<RenderSection
key={sectionType}
mappedColumns={groupedColumns[sectionType]}
setMappedColumns={(updatedColumns) => updateSectionColumns(sectionType, updatedColumns)}
darkMode={darkMode}
sectionType={sectionType}
sectionDisplayName={
currentStatus !== FORM_STATUS.GENERATE_FIELDS ? getSectionDisplayName(sectionType) : ''
}
disabled={sectionType === 'isRemoved'}
/>
)
);
})}
</div>
)}
</div>
<div className="tw-p-4 tw-border-t tw-border-border-lighter">
<ModalFooter
currentStatus={currentStatus}
refreshData={refreshData}
handleSubmit={handleSubmit}
isSaving={isSaving}
allSectionsEmpty={allSectionsEmpty}
/>
</div>
</>
);
return (
<Modal show={isOpen} onHide={onClose} size="lg">
<ModalHeader currentStatus={currentStatus} onClose={onClose} />
<div className="column-mapping-modal-body">{modalBody}</div>
</Modal>
);
};
export default ColumnMappingComponent;

View file

@ -0,0 +1,72 @@
import React from 'react';
import { renderElement } from '../../../Utils';
import { DataSectionWrapper } from './index';
export const DataSection = ({
component,
componentMeta,
paramUpdatedInterceptor,
dataQueries,
currentState,
allComponents,
darkMode,
resolvedCustomSchema,
source,
JSONData,
setCodeEditorView,
currentStatusRef,
saveDataSection,
openModal,
setParentModalState,
performColumnMapping,
existingResolvedJsonData,
savedSourceValue,
resolveReferences,
isLoading = false,
}) => {
return () => (
<div className={`${resolvedCustomSchema ? 'tw-pointer-events-none opacity-60' : ''}`}>
{componentMeta?.properties &&
Object.keys(componentMeta.properties).map((property) => {
if (componentMeta?.properties[property]?.section !== 'data') return null;
// Mutating the component definition properties to set the generateFormFrom source
component.component.definition.properties.generateFormFrom = source;
component.component.definition.properties.JSONData = JSONData;
const focusCodeEditor = property === 'JSONData' ? setCodeEditorView : undefined;
return renderElement(
component,
componentMeta,
paramUpdatedInterceptor,
dataQueries,
property,
'properties',
currentState,
allComponents,
darkMode,
'',
null,
focusCodeEditor
);
})}
{source.value !== 'jsonSchema' && (
<DataSectionWrapper
currentStatusRef={currentStatusRef}
source={source}
JSONData={JSONData}
component={component}
darkMode={darkMode}
saveDataSection={saveDataSection}
openModalFromParent={openModal}
setParentModalState={setParentModalState}
performColumnMapping={performColumnMapping}
newResolvedJsonData={resolveReferences('canvas', JSONData.value)}
existingResolvedJsonData={existingResolvedJsonData}
savedSourceValue={savedSourceValue}
isLoading={isLoading}
/>
)}
</div>
);
};

View file

@ -0,0 +1,201 @@
import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react';
import { Button } from '@/components/ui/Button/Button';
import { LabeledDivider, ColumnMappingComponent, FormFieldsList, FieldPopoverContent } from './index';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import { useDropdownState } from '../_hooks/useDropdownState';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { findNextElementTop, mergeFieldsWithComponentDefinition } from '../utils/utils';
import { createNewComponentFromMeta } from '../utils/fieldOperations';
import { FORM_STATUS, COMPONENT_LAYOUT_DETAILS } from '../constants';
import { checkDiff } from '@/AppBuilder/Widgets/componentUtils';
/* IMPORTANT - mandatory and selected (visibility) properties are objects with value and fxActive
This is to support dynamic values and fx expressions in the form fields.
When using these properties, ensure to access the value like so: field.mandatory.value
or field.selected.value.
Rest all the fields are directly accessible as strings or booleans.
For example: field.label, field.name, field.value, etc.
*/
const DataSectionUI = ({
component,
darkMode = false,
currentStatusRef,
openModalFromParent = false,
setParentModalState,
performColumnMapping,
newResolvedJsonData,
existingResolvedJsonData,
source,
JSONData,
isLoading: isDataLoading,
savedSourceValue = '',
}) => {
const { getChildComponents, currentLayout, getComponentDefinition, performBatchComponentOperations, saveFormFields } =
useStore(
(state) => ({
getChildComponents: state.getChildComponents,
currentLayout: state.currentLayout,
getComponentDefinition: state.getComponentDefinition,
performBatchComponentOperations: state.performBatchComponentOperations,
saveFormFields: state.saveFormFields,
}),
shallow
);
const formFields = useStore((state) => state.getFormFields(component.id), checkDiff);
const formFieldsWithComponentDefinition = useMemo(
() => mergeFieldsWithComponentDefinition(formFields, getComponentDefinition),
[formFields, getComponentDefinition]
);
const { handleDropdownOpen, handleDropdownClose, shouldPreventPopoverClose } = useDropdownState();
const [isModalOpen, setIsModalOpen] = useState(false);
const [showAddFieldPopover, setShowAddFieldPopover] = useState(false);
const addFieldButtonRef = useRef(null);
const hideManageFields = formFields.length === 0 || savedSourceValue === 'rawJson';
useEffect(() => {
if (openModalFromParent && openModalFromParent !== isModalOpen) {
setIsModalOpen(true);
} else if (!openModalFromParent) setIsModalOpen(false);
}, [openModalFromParent, isModalOpen]);
const handleDeleteField = (field) => {
const updatedFields = formFields.filter((f) => f.componentId !== field.componentId);
let operations = {
updated: {},
added: {},
deleted: [field.componentId],
};
performBatchComponentOperations(operations);
saveFormFields(component.id, updatedFields, 'canvas');
};
const handleAddField = (newField) => {
const updatedFields = {
componentType: newField.componentType,
name: 'custom',
mandatory: newField.mandatory,
label: newField.label,
value: '',
placeholder: newField.placeholder,
selected: true,
isCustomField: true,
};
const childComponents = getChildComponents(component?.id);
// Get the last position of the child components
const nextElementsTop = findNextElementTop(childComponents, currentLayout);
const { added = {} } = createNewComponentFromMeta(
updatedFields,
component.id,
nextElementsTop + COMPONENT_LAYOUT_DETAILS.spacing
);
let operations = {
updated: {},
added: {},
deleted: [],
};
operations.added[added.id] = added;
performBatchComponentOperations(operations);
saveFormFields(component.id, [...formFields, { componentId: added.id, isCustomField: true }], 'canvas');
setShowAddFieldPopover(false);
};
const renderManageFieldsIcon = () => {
return (
<Button
iconOnly
leadingIcon="sliders"
variant="ghost"
size="small"
onClick={() => {
currentStatusRef.current = FORM_STATUS.MANAGE_FIELDS;
setParentModalState(true);
setIsModalOpen(true);
}}
/>
);
};
const renderAddCustomFieldButton = () => {
return (
<OverlayTrigger
trigger="click"
placement="left"
show={showAddFieldPopover}
onToggle={(show) => {
if (!show && shouldPreventPopoverClose) {
return;
}
setShowAddFieldPopover(show);
}}
rootClose
overlay={
<Popover id="add-field-popover" className="shadow form-fields-column-popover">
<FieldPopoverContent
field={undefined}
onChange={handleAddField}
onClose={() => setShowAddFieldPopover(false)}
darkMode={darkMode}
mode="add"
onDropdownOpen={handleDropdownOpen}
onDropdownClose={handleDropdownClose}
shouldPreventPopoverClose={shouldPreventPopoverClose}
/>
</Popover>
}
>
<Button ref={addFieldButtonRef} iconOnly leadingIcon="plus" variant="ghost" size="small" />
</OverlayTrigger>
);
};
const closeModal = useCallback(() => {
setIsModalOpen(false);
setParentModalState(false);
}, [setIsModalOpen]);
return (
<>
<div className="tw-flex tw-justify-between tw-items-center tw-gap-1.5">
<div className="tw-flex-1">
<LabeledDivider label="Fields" rightContentCount={hideManageFields ? 1 : 2} />
</div>
<div className="tw-flex tw-items-center">
{!hideManageFields && renderManageFieldsIcon()}
{renderAddCustomFieldButton()}
</div>
</div>
<FormFieldsList
fields={formFieldsWithComponentDefinition}
onDeleteField={handleDeleteField}
currentStatusRef={currentStatusRef}
onSave={performColumnMapping}
/>
{isModalOpen && (
<ColumnMappingComponent
isOpen={isModalOpen}
onClose={closeModal}
darkMode={darkMode}
currentStatusRef={currentStatusRef}
onSubmit={performColumnMapping}
// Add new props for buildColumns
component={component}
newResolvedJsonData={newResolvedJsonData}
existingResolvedJsonData={existingResolvedJsonData}
source={source}
JSONData={JSONData}
isDataLoading={isDataLoading}
/>
)}
</>
);
};
export default DataSectionUI;

View file

@ -0,0 +1,61 @@
import React, { useEffect } from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { DataSectionUI } from './index';
import { isEqual } from 'lodash';
import { FORM_STATUS } from '../constants';
const DataSectionWrapper = ({
source,
JSONData,
component,
performColumnMapping,
currentStatusRef,
savedSourceValue,
...restProps
}) => {
const getFormDataSectionData = useStore((state) => state.getFormDataSectionData, shallow);
useEffect(() => {
const existingData = getFormDataSectionData(component?.id);
const isFormGenerated = existingData && existingData.generateFormFrom && existingData.JSONData;
const sourceChanged = !isEqual(savedSourceValue, source?.value);
const JSONDataChanged = !isEqual(existingData?.JSONData?.value, JSONData?.value);
// Case: Form not generated yet
if (!isFormGenerated) {
currentStatusRef.current = FORM_STATUS.GENERATE_FIELDS;
}
// Case: Form is already generated
else {
// Source changed - need to regenerate form
if (sourceChanged) {
currentStatusRef.current = FORM_STATUS.GENERATE_FIELDS;
}
// Source is same but JSON data changed - refresh form
else if (JSONDataChanged) {
currentStatusRef.current = FORM_STATUS.REFRESH_FIELDS;
}
// No changes
else {
currentStatusRef.current = FORM_STATUS.REFRESH_FIELDS;
}
}
}, [source, JSONData, component?.id, getFormDataSectionData, currentStatusRef, savedSourceValue]);
// You'll need to return the actual button component here instead of null
return (
<DataSectionUI
component={component}
{...restProps}
currentStatusRef={currentStatusRef}
performColumnMapping={performColumnMapping}
source={source}
JSONData={JSONData}
savedSourceValue={savedSourceValue}
/>
);
};
export default DataSectionWrapper;

View file

@ -0,0 +1,233 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { isEqual } from 'lodash';
import CodeHinter from '@/AppBuilder/CodeEditor';
import Dropdown from '@/components/ui/Dropdown/Index';
import Popover from 'react-bootstrap/Popover';
import { Button } from '@/components/ui/Button/Button';
import { getInputTypeOptions, isPropertyFxControlled, isTrueValue } from '../utils/utils';
const FieldPopoverContent = ({
field,
onChange,
onClose,
darkMode = false,
mode = 'edit',
onDropdownOpen,
onDropdownClose,
setSelectedComponents,
}) => {
const [localField, setLocalField] = useState(field ?? {});
useEffect(() => {
setLocalField({ ...field });
}, [field]);
// Memoize expensive computations
const isVisibilityFxControlled = useMemo(
() => (mode === 'edit' ? isPropertyFxControlled(localField.visibility) : false),
[mode, localField.visibility]
);
const isCurrentlyVisibility = useMemo(
() => (mode === 'edit' ? isTrueValue(localField.visibility?.value) : false),
[mode, localField.visibility?.value]
);
const inputTypeOptions = useMemo(() => getInputTypeOptions(darkMode), [darkMode]);
const handleFieldChange = useCallback((property, value) => {
if (property === 'mandatory' || property === 'visibility') {
return setLocalField((prevField) => ({
...prevField,
[property]: { ...prevField[property], value },
}));
}
setLocalField((prevField) => ({ ...prevField, [property]: value }));
}, []);
const handleFxChange = useCallback((property, fxActive) => {
setLocalField((prevField) => ({
...prevField,
[property]: { ...prevField[property], fxActive },
}));
}, []);
const handleSubmit = useCallback(() => {
onChange?.(localField);
if (mode !== 'edit') {
onClose?.();
}
}, [localField, onChange, onClose, mode]);
const renderPlaceholder = () => {
if (
[
'Checkbox',
'RadioButtonV2',
'Datepicker',
'DatetimePickerV2',
'Checkbox',
'ToggleSwitchV2',
'DatePickerV2',
'TimePicker',
'DaterangePicker',
].includes(localField.componentType)
)
return null;
return (
<div>
<label className="tw-text-text-default base-medium">Placeholder</label>
<CodeHinter
type={'basic'}
initialValue={localField.placeholder || ''}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
onChange={(value) => handleFieldChange('placeholder', value)}
/>
</div>
);
};
const renderDefaultValue = () => {
if (['RadioButtonV2', 'DropdownV2', 'MultiselectV2'].includes(localField.componentType)) return null;
return (
<div>
<label className="tw-text-text-default base-medium">Default value</label>
<CodeHinter
type={'basic'}
initialValue={localField.value || ''}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
onChange={(value) => handleFieldChange('value', value)}
/>
</div>
);
};
return (
<>
<Popover.Header className="d-flex justify-content-between align-items-center tw-px-4 tw-py-2 form-field-popover-header bg-white">
<span className="tw-text-text-default base-medium">{mode === 'edit' ? 'Edit field' : 'New custom field'}</span>
<div className="tw-flex">
{mode === 'edit' ? (
<>
<Button
iconOnly
leadingIcon="inspect"
variant="ghost"
size="medium"
onClick={() => setSelectedComponents([field.componentId])}
/>
<Button
iconOnly
leadingIcon={isCurrentlyVisibility ? 'eye' : 'eyedisable'}
variant="ghost"
size="medium"
disabled={isVisibilityFxControlled}
className={`${isVisibilityFxControlled ? 'tw-opacity-50' : ''}`}
onClick={() => {
handleFieldChange('visibility', !isCurrentlyVisibility);
}}
/>
</>
) : (
<Button iconOnly leadingIcon="remove" variant="ghost" size="medium" onClick={onClose} />
)}
</div>
</Popover.Header>
<Popover.Body className="bg-white tw-p-4 form-field-popover-body">
<div className="tw-space-y-[12px]">
<div>
<Dropdown
options={inputTypeOptions}
name="field-type"
id="field-type"
size="medium"
zIndex={9999}
value={localField.componentType || 'TextInput'}
leadingIcon={inputTypeOptions[localField.componentType || 'TextInput'].leadingIcon}
onChange={(value) => {
handleFieldChange('componentType', value);
}}
width="100%"
label="Component"
onOpen={onDropdownOpen}
onClose={onDropdownClose}
/>
</div>
<div>
<label className="tw-text-text-default base-medium">Label</label>
<CodeHinter
type={'basic'}
initialValue={localField.label || ''}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'Email id'}
onChange={(value) => handleFieldChange('label', value)}
/>
</div>
{renderPlaceholder()}
{renderDefaultValue()}
<div className="field mb-2">
<CodeHinter
initialValue={localField.mandatory?.value ?? false}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
type={'fxEditor'}
paramLabel={'Make this field mandatory'}
paramName={'isMandatory'}
fxActive={localField.mandatory?.fxActive ?? false}
fieldMeta={{
type: 'toggle',
displayName: 'Make editable',
}}
paramType={'toggle'}
onChange={(value) => handleFieldChange('mandatory', value)}
onFxPress={(active) => handleFxChange('mandatory', active)}
/>
</div>
{mode === 'edit' && (
<div className="field m-0">
<CodeHinter
initialValue={localField.visibility?.value ?? true}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
type={'fxEditor'}
paramLabel={'Visibility'}
paramName={'visible'}
fxActive={localField.visibility?.fxActive ?? false}
fieldMeta={{
type: 'toggle',
displayName: 'Make editable',
}}
paramType={'toggle'}
onChange={(value) => handleFieldChange('visibility', value)}
onFxPress={(active) => handleFxChange('visibility', active)}
/>
</div>
)}
<Button
leadingIcon={mode === 'edit' ? 'save' : 'plus'}
variant="secondary"
onClick={handleSubmit}
className="tw-w-full tw-rounded-[6px]"
disabled={field && isEqual(localField, field)}
>
{mode === 'edit' ? 'Save' : 'Add Field'}
</Button>
</div>
</Popover.Body>
</>
);
};
export default React.memo(FieldPopoverContent);

View file

@ -0,0 +1,166 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button/Button';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import { FieldPopoverContent } from './index';
import { useDropdownState } from '../_hooks/useDropdownState';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { isTrueValue, isPropertyFxControlled, getComponentIcon } from '../utils/utils';
export const FormField = ({ field, onDelete, activeMenu, onMenuToggle, onSave, darkMode = false }) => {
const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow);
const [showPopover, setShowPopover] = useState(false);
const [fieldData, setFieldData] = useState(field);
const { handleDropdownOpen, handleDropdownClose, shouldPreventPopoverClose } = useDropdownState();
useEffect(() => {
if (activeMenu && activeMenu !== fieldData.name) {
setShowPopover(false);
}
}, [activeMenu, fieldData.name]);
useEffect(() => {
setFieldData(field);
}, [field]);
const handleFieldChange = (updatedField) => {
setFieldData(updatedField);
onSave([updatedField], true);
};
const isMandatoryFxControlled = isPropertyFxControlled(fieldData.mandatory);
const isCurrentlyMandatory = isTrueValue(fieldData.mandatory?.value);
const mainPopover = (
<Popover id="popover-basic" className="shadow form-fields-column-popover">
<FieldPopoverContent
field={fieldData}
mode="edit"
onClose={() => setShowPopover(false)}
onChange={handleFieldChange}
onDropdownOpen={handleDropdownOpen}
onDropdownClose={handleDropdownClose}
shouldPreventPopoverClose={shouldPreventPopoverClose}
setSelectedComponents={setSelectedComponents}
/>
</Popover>
);
const menuPopover = (
<Popover id="menu-popover" className="shadow">
<Popover.Body className="tw-p-2">
<div className="tw-flex tw-flex-col">
<Button
variant="ghost"
size="default"
onClick={(e) => {
e.stopPropagation();
const newValue = !isCurrentlyMandatory;
handleFieldChange({
...fieldData,
mandatory:
typeof fieldData.mandatory === 'object'
? { ...fieldData.mandatory, value: `{{${newValue}}}` }
: `{{${newValue}}}`,
});
onMenuToggle(null);
}}
disabled={isMandatoryFxControlled}
className={`base-regular ${isMandatoryFxControlled ? 'tw-opacity-50' : ''}`}
leadingIcon="asterix"
fill="#CCD1D5"
>
{isCurrentlyMandatory ? 'Make optional' : 'Make mandatory'}
</Button>
<Button
variant="ghost"
size="default"
onClick={(e) => {
e.stopPropagation();
onMenuToggle(null);
setSelectedComponents([field.componentId]);
}}
className="base-regular"
leadingIcon="inspect"
fill="#CCD1D5"
>
View properties and styles
</Button>
<Button
variant="ghost"
size="default"
onClick={(e) => {
e.stopPropagation();
onDelete(fieldData);
onMenuToggle(null);
}}
className="base-regular"
leadingIcon="remove"
fill="#CCD1D5"
>
Remove from form
</Button>
</div>
</Popover.Body>
</Popover>
);
return (
<div className="tw-relative tw-group">
<OverlayTrigger
trigger="click"
placement="left"
show={showPopover}
onToggle={(show) => {
if (!show && shouldPreventPopoverClose) {
return;
}
if (show) onMenuToggle(null);
setShowPopover(show);
}}
rootClose
overlay={mainPopover}
>
<div
className={`field-item tw-flex tw-items-center tw-justify-between tw-gap-2 hover:tw-cursor-pointer ${
(fieldData.name === activeMenu || showPopover) && 'selected'
}`}
>
<div className="tw-flex tw-items-center tw-gap-[6px] tw-flex-1" style={{ width: 'calc(100% - 100px)' }}>
<div className="field-icon tw-w-6 tw-h-6 tw-flex tw-items-center tw-justify-center tw-rounded tw-bg-gray-100">
{getComponentIcon(fieldData.componentType, darkMode)}
</div>
<span className="field-name tw-text-sm base-regular">{fieldData.name}</span>
</div>
<OverlayTrigger
trigger="click"
placement="bottom-start"
show={activeMenu === fieldData.name}
onToggle={(show) => {
setShowPopover(false);
if (show) {
onMenuToggle(fieldData.name);
} else {
onMenuToggle(null);
}
}}
rootClose
overlay={menuPopover}
>
<Button
iconOnly
leadingIcon="morevertical"
variant="ghost"
size="default"
className="tw-opacity-0 group-hover:tw-opacity-100 more-btn"
onClick={(e) => e.stopPropagation()}
/>
</OverlayTrigger>
</div>
</OverlayTrigger>
</div>
);
};

View file

@ -0,0 +1,36 @@
import React, { useState } from 'react';
import { FormField } from './index';
export const FormFieldsList = ({ fields, onDeleteField, currentStatusRef, onSave }) => {
const [activeMenuField, setActiveMenuField] = useState(null);
if (fields.length === 0) {
return (
<span className="base-regular text-placeholder tw-block tw-p-3 tw-text-center">
No fields yet. Generate a form from a data source or add custom fields.
</span>
);
}
return (
<div className="tw-flex tw-flex-col" style={{ maxHeight: '400px' }}>
<div className="tw-flex-grow tw-overflow-y-auto tw-max-h-[calc(100%-50px)]">
<div className="tw-flex tw-flex-col tw-gap-1">
{fields.map((field) => (
<FormField
key={field.name}
field={field}
activeMenu={activeMenuField}
onMenuToggle={(fieldName) => {
currentStatusRef.current = null;
setActiveMenuField(fieldName);
}}
onDelete={onDeleteField}
onSave={onSave}
/>
))}
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,39 @@
import React from 'react';
const LabeledDivider = ({ label, rightContentCount = 0 }) => {
return (
<div className="tw-relative tw-flex-1" style={{ paddingTop: 4, paddingBottom: 8 }}>
{/* Background line */}
<div className="tw-absolute tw-inset-0 tw-flex tw-items-center tw-w-full">
<div
className="tw-w-full"
style={{
borderTop: '1px dashed var(--border-default, #CCD1D5)',
height: '1px',
}}
></div>
</div>
{/* Label container - centered accounting for right content */}
<div
className="tw-relative tw-flex tw-w-full"
style={{
justifyContent: 'center',
}}
>
<span
className="base-medium tw-px-3"
style={{
color: 'var(--text-placeholder, #6A727C)',
background: 'var(--base)',
transform: rightContentCount ? `translateX(${rightContentCount * 11.5}px)` : 'none', // Adjust for typical icon width + gap
}}
>
{label}
</span>
</div>
</div>
);
};
export default LabeledDivider;

View file

@ -0,0 +1,144 @@
import { useEffect, useState, useCallback, useMemo } from 'react';
import {
isTrueValue,
isPropertyFxControlled,
parseDataAndBuildFields,
analyzeJsonDifferences,
mergeFieldsWithComponentDefinition,
mergeFormFieldsWithNewData,
mergeArrays,
} from '../../utils/utils';
import { FORM_STATUS } from '../../constants';
import { merge } from 'lodash';
// Constants for section order preference
const SECTION_ORDER = ['isNew', 'isRemoved', 'existing', 'isCustomField'];
/**
* Custom hook for managing column building logic
*/
export const useColumnBuilder = (
component,
currentStatus,
newResolvedJsonData,
existingResolvedJsonData,
refreshedColumns,
getFormFields,
getComponentDefinition
) => {
return useMemo(() => {
const formFields = getFormFields(component.id);
const formFieldsWithComponentDefinition = mergeFieldsWithComponentDefinition(formFields, getComponentDefinition);
if (currentStatus === FORM_STATUS.MANAGE_FIELDS) {
const allColumnsFromJsonData = parseDataAndBuildFields(newResolvedJsonData);
return mergeArrays(allColumnsFromJsonData, formFieldsWithComponentDefinition);
} else if (currentStatus === FORM_STATUS.REFRESH_FIELDS) {
const jsonDifferences = analyzeJsonDifferences(refreshedColumns, existingResolvedJsonData);
const mergedJsonData = merge({}, existingResolvedJsonData, refreshedColumns);
const parsedFields = parseDataAndBuildFields(mergedJsonData, jsonDifferences);
const mergedFields = mergeFormFieldsWithNewData(formFieldsWithComponentDefinition, parsedFields);
const enhancedFieldsWithComponentDefinition = mergeFieldsWithComponentDefinition(
mergedFields,
getComponentDefinition
);
return [
...enhancedFieldsWithComponentDefinition,
...formFieldsWithComponentDefinition.filter((f) => f.isCustomField),
];
}
return parseDataAndBuildFields(newResolvedJsonData || []);
}, [
component.id,
currentStatus,
newResolvedJsonData,
existingResolvedJsonData,
refreshedColumns,
getFormFields,
getComponentDefinition,
]);
};
/**
* Custom hook for managing grouped columns state
*/
export const useGroupedColumns = (columnsToUse, currentStatus) => {
const [groupedColumns, setGroupedColumns] = useState({});
const [sectionTypes, setSectionTypes] = useState([]);
useEffect(() => {
const grouped = {};
const isGenerateFieldsMode = currentStatus === FORM_STATUS.GENERATE_FIELDS;
const isRefreshFormMode = currentStatus === FORM_STATUS.REFRESH_FIELDS;
const shouldSelectByDefault = isGenerateFieldsMode || isRefreshFormMode;
columnsToUse.forEach((col) => {
let sectionType = 'existing';
if (col.isNew) {
sectionType = 'isNew';
} else if (col.isRemoved) {
sectionType = 'isRemoved';
} else if (col.isCustomField) {
sectionType = 'isCustomField';
}
if (!grouped[sectionType]) {
grouped[sectionType] = [];
}
// Auto-select columns based on mode
if (
shouldSelectByDefault &&
sectionType !== 'isRemoved' &&
(isGenerateFieldsMode || (isRefreshFormMode && sectionType === 'isNew'))
) {
grouped[sectionType].push({ ...col, selected: true });
} else {
grouped[sectionType].push(col);
}
});
const types = SECTION_ORDER.filter((type) => grouped[type] && grouped[type].length > 0);
setGroupedColumns(grouped);
setSectionTypes(types);
}, [columnsToUse, currentStatus]);
const updateSectionColumns = useCallback((sectionType, updatedColumns) => {
setGroupedColumns((prev) => ({
...prev,
[sectionType]: updatedColumns,
}));
}, []);
return { groupedColumns, sectionTypes, updateSectionColumns };
};
/**
* Hook for checkbox state calculations
*/
export const useCheckboxStates = (columnsArray) => {
return useMemo(() => {
const mandatorySettableColumns = columnsArray.filter((col) => !isPropertyFxControlled(col.mandatory));
const isAllSelected = columnsArray.length > 0 ? columnsArray.every((col) => col.selected) : false;
const isIntermediateSelected = !isAllSelected && columnsArray.some((col) => col.selected);
const isAllSelectedMandatory =
mandatorySettableColumns.length > 0
? mandatorySettableColumns.every((col) => isTrueValue(col.mandatory.value))
: false;
const isIntermediateMandatory =
!isAllSelectedMandatory && mandatorySettableColumns.some((col) => isTrueValue(col.mandatory.value));
return {
isAllSelected,
isIntermediateSelected,
isAllSelectedMandatory,
isIntermediateMandatory,
mandatorySettableColumns,
};
}, [columnsArray]);
};

View file

@ -0,0 +1,9 @@
// Component exports for cleaner imports
export { DataSection } from './DataSection';
export { default as DataSectionWrapper } from './DataSectionWrapper';
export { default as DataSectionUI } from './DataSectionUI';
export { default as ColumnMappingComponent } from './ColumnMappingComponent';
export { default as FieldPopoverContent } from './FieldPopoverContent';
export { FormField } from './FormField';
export { FormFieldsList } from './FormFieldsList';
export { default as LabeledDivider } from './LabeledDivider';

View file

@ -0,0 +1,4 @@
export { useFormState } from './useFormState';
export { useFormData } from './useFormData';
export { useFormLogic } from './useFormLogic';
export { useDropdownState } from './useDropdownState';

View file

@ -0,0 +1,23 @@
import { useState, useCallback } from 'react';
export const useDropdownState = () => {
const [dropdownState, setDropdownState] = useState('closed'); // 'closed' | 'opening' | 'open'
const handleDropdownOpen = useCallback(() => {
setDropdownState('open');
}, []);
const handleDropdownClose = useCallback(() => {
setDropdownState('closing');
setTimeout(() => setDropdownState('closed'), 100);
}, []);
const shouldPreventPopoverClose = dropdownState !== 'closed';
return {
dropdownState,
handleDropdownOpen,
handleDropdownClose,
shouldPreventPopoverClose,
};
};

View file

@ -0,0 +1,37 @@
import React from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { mergeFieldsWithComponentDefinition } from '../utils/utils';
export const useFormData = (component) => {
const resolveReferences = useStore((state) => state.resolveReferences, shallow);
const getFormDataSectionData = useStore((state) => state.getFormDataSectionData, shallow);
const getComponentDefinition = useStore((state) => state.getComponentDefinition, shallow);
const formFields = useStore((state) => state.getFormFields(component.id), shallow);
// Get form data and process it
const existingData = getFormDataSectionData(component?.id);
let isFormGenerated = existingData?.generateFormFrom?.value ?? false;
// Memoized form fields with component definition
const formFieldsWithComponentDefinition = React.useMemo(
() => mergeFieldsWithComponentDefinition(formFields, getComponentDefinition),
[formFields, getComponentDefinition]
);
// Process JSON data
let existingResolvedJsonData = existingData?.JSONData?.value;
existingResolvedJsonData = resolveReferences('canvas', existingResolvedJsonData);
const newJSONValue = component.component.definition.properties['JSONData']?.value;
const newResolvedJsonData = resolveReferences('canvas', newJSONValue);
return {
existingData,
isFormGenerated,
formFieldsWithComponentDefinition,
existingResolvedJsonData,
newJSONValue,
newResolvedJsonData,
};
};

View file

@ -0,0 +1,114 @@
import { useEffect } from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { useFormState } from './useFormState';
import { useFormData } from './useFormData';
import { createParamUpdatedInterceptor, createColumnMappingHandler, createJSONDataBlurHandler } from '../handlers';
export const useFormLogic = (component, paramUpdated) => {
// Store selectors
const resolveReferences = useStore((state) => state.resolveReferences, shallow);
const getFormDataSectionData = useStore((state) => state.getFormDataSectionData, shallow);
const saveFormDataSectionData = useStore((state) => state.saveFormDataSectionData, shallow);
const componentNameIdMapping = useStore((state) => state.modules.canvas.componentNameIdMapping, shallow);
const queryNameIdMapping = useStore((state) => state.modules.canvas.queryNameIdMapping, shallow);
const getChildComponents = useStore((state) => state.getChildComponents, shallow);
const runQuery = useStore((state) => state.queryPanel.runQuery, shallow);
const getExposedValueOfQuery = useStore((state) => state.getExposedValueOfQuery, shallow);
const currentLayout = useStore((state) => state.currentLayout, shallow);
const getComponentDefinition = useStore((state) => state.getComponentDefinition, shallow);
const performBatchComponentOperations = useStore((state) => state.performBatchComponentOperations, shallow);
// Custom hooks
const formState = useFormState(component);
const formData = useFormData(component);
// Save data section function
const saveDataSection = (fields) => {
formState.savedSourceValue.current = formState.source.value;
const newJsonData = formState.JSONData;
if (newJsonData?.value === undefined) {
newJsonData.value = resolveReferences('canvas', formState.source.value);
}
saveFormDataSectionData(
component?.id,
{
generateFormFrom: formState.source,
JSONData: formState.JSONData,
},
fields
);
};
// Create column mapping handler
const performColumnMapping = createColumnMappingHandler({
component,
isFormGenerated: formData.isFormGenerated,
currentStatusRef: formState.currentStatusRef,
formFields: useStore((state) => state.getFormFields(component.id), shallow),
formFieldsWithComponentDefinition: formData.formFieldsWithComponentDefinition,
getChildComponents,
currentLayout,
performBatchComponentOperations,
saveDataSection,
setOpenModal: formState.setOpenModal,
});
// Create JSON data blur handler
const handleJSONDataBlur = createJSONDataBlurHandler({
component,
currentStatusRef: formState.currentStatusRef,
resolveReferences,
getFormDataSectionData,
savedSourceValue: formState.savedSourceValue,
source: formState.source,
formFieldsWithComponentDefinition: formData.formFieldsWithComponentDefinition,
existingResolvedJsonData: formData.existingResolvedJsonData,
getComponentDefinition,
performColumnMapping,
saveDataSection,
codeEditorView: formState.codeEditorView,
});
// Create parameter updated interceptor
const paramUpdatedInterceptor = createParamUpdatedInterceptor({
component,
paramUpdated,
source: formState.source,
setSource: formState.setSource,
setJSONData: formState.setJSONData,
setOpenModal: formState.setOpenModal,
shouldFocusJSONDataEditor: formState.shouldFocusJSONDataEditor,
shouldInvokeBlurEvent: formState.shouldInvokeBlurEvent,
savedSourceValue: formState.savedSourceValue,
componentNameIdMapping,
queryNameIdMapping,
getFormDataSectionData,
getExposedValueOfQuery,
runQuery,
resolveReferences,
setLoading: formState.setLoading,
});
// Effect for handling JSON data blur
useEffect(() => {
if (formState.shouldInvokeBlurEvent.current) {
formState.shouldInvokeBlurEvent.current = false;
handleJSONDataBlur(formState.JSONData.value);
}
}, [formState.shouldInvokeBlurEvent, formState.JSONData, handleJSONDataBlur]);
return {
...formState,
...formData,
paramUpdatedInterceptor,
performColumnMapping,
handleJSONDataBlur,
saveDataSection,
closeModal: () => {
formState.setOpenModal(false);
},
};
};

View file

@ -0,0 +1,78 @@
import { useState, useRef, useEffect } from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { INPUT_COMPONENTS_FOR_FORM } from '../constants';
export const useFormState = (component) => {
const getChildComponents = useStore((state) => state.getChildComponents, shallow);
const saveFormFields = useStore((state) => state.saveFormFields, shallow);
const resolveReferences = useStore((state) => state.resolveReferences, shallow);
const [source, setSource] = useState({
value: component.component.definition.properties?.generateFormFrom?.value,
fxActive: component.component.definition.properties?.generateFormFrom?.fxActive,
});
const resolvedSource = resolveReferences(
'canvas',
component.component.definition.properties?.generateFormFrom?.value
);
const [JSONData, setJSONData] = useState({
value: resolvedSource === 'rawJson' ? component.component.definition.properties?.JSONData?.value : resolvedSource,
});
const [openModal, setOpenModal] = useState(false);
const [isLoading, setLoading] = useState(false);
const [codeEditorView, setCodeEditorView] = useState(null);
// Refs for managing component state
const shouldFocusJSONDataEditor = useRef(false);
const currentStatusRef = useRef(null);
const shouldInvokeBlurEvent = useRef(false);
const savedSourceValue = useRef(component.component.definition.properties?.generateFormFrom?.value);
// Backfill fields if not present
const fields = component.component.definition.properties?.fields;
if (fields === undefined) {
const newFields = [];
const childComponents = getChildComponents(component.id);
Object.keys(childComponents).forEach((childId) => {
if (INPUT_COMPONENTS_FOR_FORM.includes(childComponents[childId].component.component.component)) {
newFields.push({
componentId: childId,
isCustomField: true,
});
}
});
saveFormFields(component.id, newFields, 'canvas');
}
// Focus management effect
useEffect(() => {
if (codeEditorView && shouldFocusJSONDataEditor.current) {
codeEditorView.focus();
// Add 'focused' class to the parent of codeEditorView.dom
if (codeEditorView.dom && codeEditorView.dom.parentNode) {
codeEditorView.dom.parentNode.classList.add('focused');
}
}
}, [codeEditorView, shouldFocusJSONDataEditor]);
return {
source,
setSource,
JSONData,
setJSONData,
openModal,
setOpenModal,
codeEditorView,
setCodeEditorView,
shouldFocusJSONDataEditor,
currentStatusRef,
shouldInvokeBlurEvent,
savedSourceValue,
isLoading,
setLoading,
};
};

View file

@ -1,93 +1,10 @@
import React from 'react';
import Accordion from '@/_ui/Accordion';
import { EventManager } from '../EventManager';
import { renderElement } from '../Utils';
// eslint-disable-next-line import/no-unresolved
import i18next from 'i18next';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { EventManager } from '../../../EventManager';
import { renderElement } from '../../../Utils';
export const Form = ({
componentMeta,
darkMode,
layoutPropertyChanged,
component,
paramUpdated,
dataQueries,
currentState,
eventsChanged,
apps,
allComponents,
pages,
}) => {
const tempComponentMeta = deepClone(componentMeta);
let properties = [];
let additionalActions = [];
let dataProperties = [];
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
for (const [key] of Object.entries(componentMeta?.properties)) {
if (componentMeta?.properties[key]?.section === 'additionalActions') {
additionalActions.push(key);
} else if (componentMeta?.properties[key]?.accordian === 'Data') {
dataProperties.push(key);
} else {
properties.push(key);
}
}
const { id } = component;
const newOptions = [{ name: 'None', value: 'none' }];
Object.entries(allComponents).forEach(([componentId, _component]) => {
const validParent =
_component.component.parent === id ||
_component.component.parent === `${id}-footer` ||
_component.component.parent === `${id}-header`;
if (validParent && _component?.component?.component === 'Button') {
newOptions.push({ name: _component.component.name, value: componentId });
}
});
tempComponentMeta.properties.buttonToSubmit.options = newOptions;
// Hide header footer if custom schema is turned on
if (component.component.definition.properties.advanced.value === '{{true}}') {
component.component.properties.showHeader = {
...component.component.properties.headerHeight,
isHidden: true,
};
component.component.properties.showFooter = {
...component.component.properties.headerHeight,
isHidden: true,
};
}
const accordionItems = baseComponentProperties(
properties,
events,
component,
tempComponentMeta,
layoutPropertyChanged,
paramUpdated,
dataQueries,
currentState,
eventsChanged,
apps,
allComponents,
validations,
darkMode,
pages,
additionalActions
);
return <Accordion items={accordionItems} />;
};
export const baseComponentProperties = (
export const createAccordionItems = ({
properties,
events,
component,
@ -102,12 +19,16 @@ export const baseComponentProperties = (
validations,
darkMode,
pages,
additionalActions
) => {
additionalActions,
deprecatedProperties,
renderDataElement,
}) => {
let items = [];
// Structure section
if (properties.length > 0) {
items.push({
title: `${i18next.t('widget.common.properties', 'Properties')}`,
title: `${i18next.t('widget.common.structure', 'Structure')}`,
children: properties.map((property) =>
renderElement(
component,
@ -124,6 +45,14 @@ export const baseComponentProperties = (
});
}
// Data section
items.push({
title: 'Data',
isOpen: true,
children: renderDataElement(),
});
// Events section
if (events.length > 0) {
items.push({
title: `${i18next.t('widget.common.events', 'Events')}`,
@ -145,6 +74,7 @@ export const baseComponentProperties = (
});
}
// Additional actions section
items.push({
title: 'Additional actions',
isOpen: true,
@ -163,6 +93,7 @@ export const baseComponentProperties = (
),
});
// Validation section
if (validations.length > 0) {
items.push({
title: `${i18next.t('widget.common.validation', 'Validation')}`,
@ -182,6 +113,7 @@ export const baseComponentProperties = (
});
}
// Devices section
items.push({
title: `${i18next.t('widget.common.devices', 'Devices')}`,
isOpen: true,
@ -211,5 +143,24 @@ export const baseComponentProperties = (
),
});
// Deprecated section
items.push({
title: 'Deprecated',
isOpen: true,
children: deprecatedProperties?.map((property) =>
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
property,
'properties',
currentState,
allComponents,
darkMode
)
),
});
return items;
};

View file

@ -0,0 +1,47 @@
export const DATATYPE_TO_COMPONENT = {
string: 'TextInput',
number: 'NumberInput',
date: 'DatePickerV2',
boolean: 'Checkbox',
array: 'DropdownV2',
};
export const COMPONENT_WITH_OPTIONS = ['DropdownV2', 'MultiselectV2', 'RadioButtonV2'];
export const INPUT_COMPONENTS_FOR_FORM = [
'TextInput',
'PasswordInput',
'EmailInput',
'PhoneInput',
'CurrencyInput',
'NumberInput',
'DropdownV2',
'MultiselectV2',
'RadioButtonV2',
'DatetimePickerV2',
'Checkbox',
'ToggleSwitchV2',
'DatePickerV2',
'TimePicker',
'DaterangePicker',
'TextArea',
];
export const JSON_DIFFERENCE = {
isExisting: [],
isNew: [],
isRemoved: [],
};
export const FORM_STATUS = {
MANAGE_FIELDS: 'manageFields',
GENERATE_FIELDS: 'generateFields',
REFRESH_FIELDS: 'refreshFields',
};
export const COMPONENT_LAYOUT_DETAILS = {
spacing: 40,
defaultWidth: 37,
defaultHeight: 30,
defaultLeft: 3,
};

View file

@ -0,0 +1,135 @@
import { isEqual } from 'lodash';
import { FORM_STATUS, COMPONENT_LAYOUT_DETAILS } from '../constants';
import { findNextElementTop, cleanupFormFields } from '../utils/utils';
import { updateFormFieldComponent } from '../utils/fieldOperations';
export const createColumnMappingHandler = ({
component,
isFormGenerated,
currentStatusRef,
formFields,
formFieldsWithComponentDefinition,
getChildComponents,
currentLayout,
performBatchComponentOperations,
saveDataSection,
setOpenModal,
}) => {
return (columns, isSingleUpdate = false) => {
const newColumns = isSingleUpdate ? formFields.filter((field) => field.componentId !== columns[0].componentId) : [];
let operations = {
updated: {},
added: {},
deleted: [],
},
componentsToBeRemoved = [];
const isFormRegeneration = isFormGenerated && currentStatusRef.current === FORM_STATUS.GENERATE_FIELDS;
if (!isSingleUpdate) {
if (isFormRegeneration) {
formFields.forEach((field) => {
if (!field.isCustomField) {
componentsToBeRemoved.push(field.componentId);
operations.deleted.push(field.componentId);
} else {
newColumns.push(field);
}
});
} else if (currentStatusRef.current === FORM_STATUS.GENERATE_FIELDS) {
newColumns.push(...formFields);
} else {
formFields.forEach((field) => {
if (field.isCustomField) {
newColumns.push(field);
}
});
columns.forEach((column) => {
if (column.isRemoved) {
componentsToBeRemoved.push(column.componentId);
}
});
}
}
const childComponents = getChildComponents(component?.id);
// Get the last position of the child components
const nextElementsTop = findNextElementTop(childComponents, currentLayout, componentsToBeRemoved);
// Create form field components from columns
if (columns && Array.isArray(columns) && columns.length > 0) {
let nextTop = nextElementsTop + COMPONENT_LAYOUT_DETAILS.spacing;
columns.forEach((column, index) => {
if (column.isRemoved) return operations.deleted.push(column.componentId);
if (currentStatusRef.current === FORM_STATUS.REFRESH_FIELDS) {
delete column.isRemoved;
delete column.isNew;
delete column.isExisting;
if (
isEqual(
column,
formFieldsWithComponentDefinition.find((field) => field.componentId === column.componentId)
)
) {
return newColumns.push(column);
}
}
if (
currentStatusRef.current === FORM_STATUS.MANAGE_FIELDS &&
isEqual(
column,
formFieldsWithComponentDefinition.find((field) => field.componentId === column.componentId)
)
) {
return newColumns.push(column);
}
const {
added = {},
updated = {},
deleted = false,
} = updateFormFieldComponent(column, {}, component.id, nextTop);
if (Object.keys(updated).length !== 0) {
operations.updated[column.componentId] = updated;
newColumns.push(column);
}
if (Object.keys(added).length !== 0) {
operations.added[added.id] = added;
if (added.component.component === 'Checkbox') {
nextTop = nextTop + added.layouts['desktop'].height + 10;
} else {
nextTop = nextTop + added.layouts['desktop'].height + COMPONENT_LAYOUT_DETAILS.spacing;
}
// Create simplified column structure with only the required fields
const simplifiedColumn = {
componentId: added.id,
isCustomField: column.isCustomField ?? false,
dataType: column.dataType,
key: column.key || column.name,
};
columns[index] = simplifiedColumn; // Replace with simplified structure
newColumns.push(simplifiedColumn);
}
if (deleted) {
operations.deleted.push(column.componentId);
}
});
if (
Object.keys(operations.updated).length > 0 ||
Object.keys(operations.added).length > 0 ||
operations.deleted.length > 0
) {
performBatchComponentOperations(operations);
saveDataSection(cleanupFormFields(newColumns));
}
setOpenModal(false);
}
};
};

View file

@ -0,0 +1,3 @@
export { createParamUpdatedInterceptor } from './parameterHandlers';
export { createColumnMappingHandler } from './columnMappingHandlers';
export { createJSONDataBlurHandler } from './jsonDataHandlers';

View file

@ -0,0 +1,79 @@
import { isEqual, merge } from 'lodash';
import { FORM_STATUS } from '../constants';
import {
parseDataAndBuildFields,
analyzeJsonDifferences,
mergeFormFieldsWithNewData,
mergeFieldsWithComponentDefinition,
} from '../utils/utils';
export const createJSONDataBlurHandler = ({
component,
currentStatusRef,
resolveReferences,
getFormDataSectionData,
savedSourceValue,
source,
formFieldsWithComponentDefinition,
existingResolvedJsonData,
getComponentDefinition,
performColumnMapping,
saveDataSection,
codeEditorView,
}) => {
return async (newJSONValue = null) => {
if (codeEditorView.dom && codeEditorView.dom.parentNode) {
codeEditorView.dom.parentNode.classList.remove('focused');
}
const existingData = getFormDataSectionData(component?.id);
const isFormGenerated = existingData && existingData.generateFormFrom && existingData.JSONData;
// Resolve both values to compare actual data, not just string comparison
const resolvedNewJSONValue = resolveReferences('canvas', newJSONValue);
const existingResolvedValue = existingData?.JSONData?.value
? resolveReferences('canvas', existingData.JSONData.value)
: null;
// Use deep comparison to check if there's actual content change
const hasDataChanged = !isEqual(resolvedNewJSONValue, existingResolvedValue);
// Only proceed if there's actual data and changes
if (!resolvedNewJSONValue || !newJSONValue) {
return;
}
if (!isFormGenerated) {
currentStatusRef.current = FORM_STATUS.GENERATE_FIELDS;
const columns = parseDataAndBuildFields(resolvedNewJSONValue);
if (columns && columns.length > 0) {
performColumnMapping(columns);
}
return;
}
if (hasDataChanged) {
const sourceChanged = !isEqual(savedSourceValue.current, source?.value);
currentStatusRef.current = sourceChanged ? FORM_STATUS.GENERATE_FIELDS : FORM_STATUS.REFRESH_FIELDS;
const jsonDifferences = analyzeJsonDifferences(
resolvedNewJSONValue,
sourceChanged ? null : existingResolvedJsonData
);
const mergedJsonData = merge({}, sourceChanged ? {} : existingResolvedJsonData, resolvedNewJSONValue);
const parsedFields = parseDataAndBuildFields(mergedJsonData, jsonDifferences);
const mergedFields = mergeFormFieldsWithNewData(formFieldsWithComponentDefinition, parsedFields);
const enhancedFieldsWithComponentDefinition = mergeFieldsWithComponentDefinition(
mergedFields,
getComponentDefinition
);
if (enhancedFieldsWithComponentDefinition && enhancedFieldsWithComponentDefinition.length > 0) {
performColumnMapping(enhancedFieldsWithComponentDefinition);
}
} else if (savedSourceValue.current === 'jsonSchema') {
return saveDataSection(formFieldsWithComponentDefinition);
}
};
};

View file

@ -0,0 +1,105 @@
import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast';
import { findFirstKeyValuePairWithPath } from '../utils/utils';
export const createParamUpdatedInterceptor = ({
component,
paramUpdated,
source,
setSource,
setJSONData,
setOpenModal,
shouldFocusJSONDataEditor,
shouldInvokeBlurEvent,
savedSourceValue,
componentNameIdMapping,
queryNameIdMapping,
getFormDataSectionData,
getExposedValueOfQuery,
runQuery,
resolveReferences,
setLoading,
}) => {
return async (param, attr, value, paramType, ...restArgs) => {
// Handle generateFormFrom parameter
if (param?.name === 'generateFormFrom') {
shouldFocusJSONDataEditor.current = false;
if (attr === 'value') {
const res = extractAndReplaceReferencesFromString(value, componentNameIdMapping, queryNameIdMapping);
let { valueWithId: selectedQuery, allRefs, valueWithBrackets } = res;
const { generateFormFrom, JSONData } = getFormDataSectionData(component?.id);
if (value === generateFormFrom?.value) {
setSource((prev) => ({ ...prev, value }));
return setJSONData({ value: JSONData.value });
}
if (value === 'jsonSchema') {
setSource({ value: 'jsonSchema' });
savedSourceValue.current = 'jsonSchema';
return paramUpdated(param, attr, value, paramType, ...restArgs);
} else if (value === 'rawJson') {
shouldFocusJSONDataEditor.current = true;
setJSONData({
value:
"{{{ 'name': 'John Doe', 'age': 35, 'isActive': true, 'dob': '01-01-1990', 'hobbies': ['reading', 'gaming', 'cycling'], 'address': { 'street': '123 Main Street', 'city': 'New York' } }}}",
});
return setSource((prev) => ({ ...prev, value }));
} else if (value !== 'rawJson' && value !== 'jsonSchema') {
// Set the source value to the selected query until the query is run
setSource((prev) => ({ ...prev, value: selectedQuery }));
setLoading(true);
const queryRefs = allRefs
.filter((ref) => ref.entityType === 'queries')
.filter((ref, index, self) => index === self.findIndex((r) => r.entityNameOrId === ref.entityNameOrId));
setOpenModal(true);
await Promise.all(
queryRefs.map(async (ref) => {
const queryId = ref.entityNameOrId;
const resolvedValueofQuery = getExposedValueOfQuery(queryId, 'canvas');
const hasMetadata =
resolvedValueofQuery && typeof resolvedValueofQuery === 'object' && 'metadata' in resolvedValueofQuery;
if (!hasMetadata && queryId && runQuery) {
await runQuery(queryId, '', false, 'edit');
}
})
);
let resolvedValue;
resolvedValue = resolveReferences('canvas', valueWithBrackets);
setLoading(false);
if (!source?.fxActive) {
const transformedData = findFirstKeyValuePairWithPath(resolvedValue, selectedQuery);
setJSONData({ value: transformedData.value });
return setSource((prev) => ({ ...prev, value: transformedData.path }));
}
setJSONData({ value: resolvedValue });
setOpenModal(true);
}
setSource((prev) => ({ ...prev, value: selectedQuery }));
} else if (attr === 'fxActive') {
setSource((prev) => ({ ...prev, fxActive: value }));
}
return;
}
// Handle JSONData parameter
if (param.name === 'JSONData') {
if (attr === 'value') {
if (source.value === 'rawJson') {
shouldInvokeBlurEvent.current = true;
}
setJSONData({ value });
}
return;
}
// Default parameter update
paramUpdated(param, attr, value, paramType, ...restArgs);
};
};

View file

@ -0,0 +1 @@
export { Form as default } from './Form';

View file

@ -0,0 +1,258 @@
.form-generate-form-btn {
button {
&:disabled {
border: 1px solid var(--border-weak, #E4E7EB) !important;
box-shadow: none;
}
}
}
.column-mapping-modal-header {
background-color: var(--primary-white) !important;
border-bottom: 1px solid var(--border-medium, rgba(106, 114, 124, 0.26));
}
.column-mapping-modal-body {
background: var(--page-page-default, #F6F8FA);
box-shadow: 0px 0px 1px 0px var(--dropshadow-100700-layer-1, rgba(48, 50, 51, 0.05)), 0px 8px 16px 0px var(--dropshadow-100400-layer-2, rgba(48, 50, 51, 0.10));
.column-mapping-modal-body-content {
background-color: var(--primary-white) !important;
border-radius: 8px;
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0px;
}
.column-mapping-modal-title {
padding-left: 10px;
padding-top: 10px;
color: var(--text-default, #1B1F24);
&.new {
color: var(--text-success, #1E823B);
}
&.removed {
color: var(--text-danger, #D92D2A);
}
}
.header-row {
height: 36px;
.header-column {
height: 16px;
span {
vertical-align: top;
}
button[role="checkbox"] {
margin-top: 0px;
}
.editable-icon svg {
vertical-align: initial;
}
}
}
.name-column {
width: 230px;
margin-right: 6px;
}
.arrow-column {
width: 40px
}
.mapped-column {
width: 160px;
padding: 0px 10px;
}
.type-column {
width: 76px;
padding: 0px 10px;
&.rows {
width: 160px;
}
}
.mandatory-column {
width: 84px;
margin-left: 8px;
&.rows {
width: 16px;
}
}
.column-mapping-row {
height: 40px;
color: var(--text-default, #1B1F24);
border-bottom: 1px solid var(--border-weak, #E4E7EB);
span.base-regular {
color: var(--text-default, #1B1F24);
}
&:last-child {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.data-type {
color: var(--text-placeholder, #6A727C);
font-family: monospace;
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
.hide-border {
button[role="combobox"],
input[type="text"] {
border-color: transparent;
border-radius: 6px;
&:hover {
border-color: var(--border-strong, #ACB2B9);
}
}
}
.no-mapped-column {
border-radius: 9px;
background-color: var(--interactive-default);
height: 18px;
padding: 0px 6px;
color: var(--text-placeholder, #6A727C);
}
}
}
}
.field-item {
background-color: var(--interactive-default);
border-radius: 6px;
padding: 7px 8px;
height: 32px;
&:hover {
background-color: var(--interactive-hover)
}
&.selected {
background-color: var(--interactive-selected) !important;
}
.field-name {
overflow: hidden;
color: var(--text-default, #1B1F24);
text-overflow: ellipsis;
}
.more-btn {
width: 22px;
height: 22px;
padding: 4px;
border-radius: 4px;
border: 1px solid var(--border-weak, #E4E7EB);
background: var(--button-secondary, #FFF);
/* Elevations/100 */
box-shadow: 0px 0px 1px 0px var(--dropshadow-100700-layer-1, rgba(48, 50, 51, 0.05)), 0px 1px 1px 0px var(--dropshadow-100400-layer-2, rgba(48, 50, 51, 0.10));
}
}
.field-icon {
background-color: inherit;
color: #6b7280;
}
.field-popover {
animation: popoverFade 0.2s ease-in-out;
}
@keyframes popoverFade {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#menu-popover {
.popover-body {
min-width: 200px;
}
button {
width: 100%;
text-align: left;
justify-content: flex-start !important;
border-radius: 6px;
padding: 6px 8px;
color: var(--text-default, #1B1F24);
&:hover {
background-color: var(--interactive-default);
}
}
}
.form-fields-column-popover {
border-radius: 8px;
width: 303px;
.form-field-popover-header {
border-bottom: 1px solid var(--border-weak, #E4E7EB);
}
.form-field-popover-body {
button[role="combobox"] {
border-radius: 6px;
}
label {
margin-bottom: 2px;
font-family: "IBM Plex Sans";
}
}
}
.refresh-data-section {
border-radius: 6px;
background: var(--background-surface-layer-02);
margin-top: 12px;
.neutral-light-color {
color: var(--neutral-light-n-900, #091E42);
}
.refresh-data-button {
width: 147px;
margin: 0px 24px;
}
}
.custom-schema-fields-section {
background: var(--background-warning-weak, #FAEFE7);
border-radius: 6px;
margin-top: 12px;
}

View file

@ -0,0 +1,66 @@
import { deepClone } from '@/_helpers/utilities/utils.helpers';
export const processComponentMeta = (componentMeta, component, allComponents, resolvedCustomSchema) => {
const tempComponentMeta = deepClone(componentMeta);
let properties = [];
let additionalActions = [];
let dataProperties = [];
let deprecatedProperties = [];
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
// Categorize properties
for (const [key] of Object.entries(componentMeta?.properties)) {
if (componentMeta?.properties[key]?.section === 'additionalActions') {
additionalActions.push(key);
} else if (componentMeta?.properties[key]?.section === 'data') {
dataProperties.push(key);
} else if (componentMeta?.properties[key]?.section === 'deprecated') {
deprecatedProperties.push(key);
} else {
// Skip the fields property as it is handled separately
if (key === 'fields') continue;
properties.push(key);
}
}
// Process button to submit options
const { id } = component;
const newOptions = [{ name: 'None', value: 'none' }];
Object.entries(allComponents).forEach(([componentId, _component]) => {
const validParent =
_component.component.parent === id ||
_component.component.parent === `${id}-footer` ||
_component.component.parent === `${id}-header`;
if (validParent && _component?.component?.component === 'Button') {
newOptions.push({ name: _component.component.name, value: componentId });
}
});
tempComponentMeta.properties.buttonToSubmit.options = newOptions;
// Hide header footer if custom schema is turned on
if (resolvedCustomSchema) {
component.component.properties.showHeader = {
...component.component.properties.headerHeight,
isHidden: true,
};
component.component.properties.showFooter = {
...component.component.properties.headerHeight,
isHidden: true,
};
}
return {
tempComponentMeta,
properties,
additionalActions,
dataProperties,
deprecatedProperties,
events,
validations,
};
};

View file

@ -0,0 +1,292 @@
import { merge, set } from 'lodash';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { v4 as uuidv4 } from 'uuid';
import { componentTypes } from '@/AppBuilder/WidgetManager';
import useStore from '@/AppBuilder/_stores/store';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import { ensureHandlebars, buildOptions } from './utils';
import { COMPONENT_LAYOUT_DETAILS, COMPONENT_WITH_OPTIONS } from '../constants';
export const createNewComponentFromMeta = (column, parentId, nextTop) => {
const currentLayout = useStore.getState().currentLayout;
const componentType = column.componentType || 'TextInput';
const fieldId = uuidv4();
const componentMeta = componentTypes.find((comp) => comp.component === componentType);
if (!componentMeta) {
console.error(`Component type ${componentType} not found in componentTypes`);
return;
}
const defaultHeight = componentMeta.defaultSize?.height || COMPONENT_LAYOUT_DETAILS.defaultHeight;
const componentData = deepClone(componentMeta);
const componentName = useStore.getState().generateUniqueComponentNameFromBaseName(column.name);
const formField = {
id: fieldId,
name: componentName,
component: {
...componentData,
type: componentType,
name: componentName,
parent: parentId,
definition: merge({}, componentData.definition, {
properties: {
label: {
value: column.label,
},
},
styles: {
alignment: { value: 'top' },
},
validation: {
mandatory: column.mandatory,
},
others: {
showOnDesktop: {
value: currentLayout === 'desktop' ? '{{true}}' : '{{false}}',
},
showOnMobile: {
value: currentLayout === 'mobile' ? '{{true}}' : '{{false}}',
},
},
}),
},
layouts: {
desktop: {
top: nextTop,
left: COMPONENT_LAYOUT_DETAILS.defaultLeft,
width: COMPONENT_LAYOUT_DETAILS.defaultWidth,
height: defaultHeight,
},
mobile: {
top: nextTop,
left: COMPONENT_LAYOUT_DETAILS.defaultLeft,
width: COMPONENT_LAYOUT_DETAILS.defaultWidth,
height: defaultHeight,
},
},
};
setValuesBasedOnType(column, componentType, formField, false);
return {
deleted: false,
added: formField,
updated: {},
};
};
/**
* Updates an existing form field component with new values
* @param {string} componentId - ID of the component to update
* @param {Object} updatedField - New field values to apply
* @param {Object} currentField - Current field data
* @returns {Object} Updated component definition
*/
export const updateFormFieldComponent = (updatedField, currentField, parentId, nextTop = 0) => {
const componentId = updatedField?.componentId;
if (!componentId) {
// componentId is not available, create a new component
return createNewComponentFromMeta(updatedField, parentId, nextTop);
}
// Get the current component from the store
const componentToUpdate = useStore.getState().getComponentDefinition(componentId);
if (!componentToUpdate) {
console.error(`Component with ID ${componentId} not found`);
return null;
}
if (updatedField.componentType !== componentToUpdate.component.component) {
return handleComponentTypeChange(componentToUpdate, updatedField);
}
// Create a deep clone of the component to avoid reference issues
const updatedComponent = deepClone(componentToUpdate);
// Update label if changed
if (updatedField.label !== currentField.label) {
set(updatedComponent.component.definition.properties, 'label.value', updatedField.label);
}
// Update mandatory status
if (updatedField.mandatory !== currentField.mandatory) {
set(updatedComponent.component.definition.validation, 'mandatory', updatedField.mandatory);
}
// Update visibility status
if (updatedField.visibility !== currentField.visibility) {
set(updatedComponent.component.definition.properties, 'visibility', updatedField.visibility);
}
// Update component type specific properties
const componentType = updatedField.componentType || componentToUpdate.component.component;
setValuesBasedOnType(updatedField, componentType, updatedComponent, false);
return { updated: diff(componentToUpdate, updatedComponent) };
};
const handleComponentTypeChange = (componentToUpdate, updatedField) => {
const newComponentId = uuidv4();
const addOptions =
COMPONENT_WITH_OPTIONS.includes(updatedField.componentType) &&
COMPONENT_WITH_OPTIONS.includes(componentToUpdate.component.component);
const currentLayout = useStore.getState().currentLayout;
const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
const componentMeta = componentTypes.find((comp) => comp.component === updatedField.componentType);
if (!componentMeta) {
console.error(`Component type ${updatedField.componentType} not found in componentTypes`);
return null;
}
const existingLayouts = componentToUpdate.layouts || {};
const componentName = useStore
.getState()
.generateUniqueComponentNameFromBaseName(updatedField.name || componentToUpdate.component.name);
const componentData = deepClone(componentMeta);
const newComponent = {
id: newComponentId,
name: componentName,
component: {
...componentData,
type: updatedField.componentType,
name: componentName,
parent: componentToUpdate.component.parent,
definition: merge({}, componentData.definition, {
properties: {
label: {
value: updatedField.label || componentToUpdate.component.definition.properties.label?.value,
},
...(addOptions && { options: componentToUpdate.component.definition.properties.options }),
},
styles: {
alignment: { value: 'top' },
},
validation: {
mandatory: updatedField.mandatory || componentToUpdate.component.definition.validation.mandatory,
},
others: {
showOnDesktop: componentToUpdate.component.definition.others?.showOnDesktop || { value: '{{true}}' },
showOnMobile: componentToUpdate.component.definition.others?.showOnMobile || { value: '{{false}}' },
},
}),
},
layouts: {
[currentLayout]: existingLayouts[currentLayout] || { top: 0, left: 3, width: 37, height: 30 },
[nonActiveLayout]: existingLayouts[nonActiveLayout] || { top: 0, left: 3, width: 37, height: 30 },
},
};
setValuesBasedOnType(updatedField, updatedField.componentType, newComponent, true);
// Return an object that indicates to:
// 1. Delete the old component
// 2. Add the new component
return {
deleted: true,
added: newComponent,
updated: {},
};
};
const setValuesBasedOnType = (column, componentType, formField, isTypeChange = false) => {
if (column.value !== undefined && column.value !== null) {
if (componentType === 'TextInput' || componentType === 'PasswordInput' || componentType === 'TextArea') {
set(formField.component.definition.properties, 'value.value', column.value);
}
if (componentType === 'NumberInput') {
set(formField.component.definition.properties, 'value.value', ensureHandlebars(column.value));
} else if (componentType === 'Checkbox' || componentType === 'DatePickerV2' || componentType === 'ToggleSwitchV2') {
set(formField.component.definition.properties, 'defaultValue.value', column.value);
} else if (
componentType === 'DropdownV2' ||
componentType === 'MultiselectV2' ||
componentType === 'RadioButtonV2'
) {
if (!isTypeChange) {
set(formField.component.definition.properties, 'options.value', buildOptions(column.value));
} else if (Array.isArray(formField.component.definition.properties?.options)) {
set(
formField.component.definition.properties,
'options.value',
buildOptions(formField.component.definition.properties.options)
);
}
}
}
if (isTypeChange && componentType === 'TextArea') {
set(formField, 'layouts.desktop.height', 50);
set(formField, 'layouts.mobile.height', 50);
}
if (
column.placeholder &&
componentType !== 'Checkbox' &&
componentType !== 'DatePickerV2' &&
componentType !== 'ToggleSwitchV2' &&
componentType !== 'DaterangePicker'
) {
set(formField.component.definition.properties, 'placeholder.value', column.placeholder);
}
};
/**
* Retrieves field data from a component definition in the store
* @param {string} componentId - Component ID to fetch definition for
* @param {Function} getComponentDefinition - Function to get component definition
* @returns {Object} Field data with merged component definition values
*/
export const getFieldDataFromComponent = (componentId, getComponentDefinition) => {
if (!componentId) {
return null;
}
const component = getComponentDefinition(componentId);
if (!component) return null;
const componentType = component.component.component;
const definition = component.component.definition;
// Get values from component definition
const label = definition.properties?.label?.value || '';
const name = component.component.name;
// Different components store values in different properties
let value;
if (componentType === 'Checkbox' || componentType === 'DatePickerV2') {
value = definition.properties?.defaultValue?.value;
} else {
value = definition.properties?.value?.value;
}
const mandatory = definition.validation?.mandatory;
const visibility = definition.properties?.visibility;
const selected = true;
const placeholder = definition.properties?.placeholder?.value || '';
return {
label,
name,
value,
mandatory,
visibility,
selected,
placeholder,
componentType,
};
};

View file

@ -0,0 +1,333 @@
import React from 'react';
import WidgetIcon from '@/../assets/images/icons/widgets';
import { DATATYPE_TO_COMPONENT, JSON_DIFFERENCE, INPUT_COMPONENTS_FOR_FORM } from '../constants';
import { startCase, omit, uniqBy } from 'lodash';
import { getFieldDataFromComponent } from './fieldOperations';
import { componentTypeDefinitionMap } from '@/AppBuilder/WidgetManager';
export const buildOptions = (options = []) => {
if (Array.isArray(options))
return options.map((option, index) => ({
label: option,
value: index,
disable: { value: false },
visible: { value: true },
default: { value: false },
}));
};
export const ensureHandlebars = (value) => {
if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) {
return value; // Already has handlebars
}
return `{{${value}}}`;
};
// Helper function to check if a value is considered "true"
export const isTrueValue = (value) => {
if (value === true) return true;
if (typeof value === 'string') {
const trimmedValue = value.trim().toLowerCase();
// Check for "{{true}}" format or just "true"
return trimmedValue === '{{true}}' || trimmedValue === 'true';
}
return false;
};
export const isPropertyFxControlled = (property) => {
return property && typeof property === 'object' && property.fxActive === true;
};
export const isValidJSONObject = (jsonString) => {
try {
const parsed = JSON.parse(jsonString);
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed);
} catch (e) {
return false;
}
};
export const getDataType = (value) => {
if (Array.isArray(value)) return 'array';
if (typeof value === 'string') {
const date = new Date(value);
if (!isNaN(date.getTime())) return 'date';
return 'string';
}
if (typeof value === 'object' && value !== null) return 'object';
return typeof value;
};
export const buildFieldObject = (key, value, label, jsonDifferences) => {
const dataType = getDataType(value);
return {
key,
name: key,
label: startCase(label) || startCase(key),
value: dataType === 'number' || dataType === 'boolean' ? ensureHandlebars(value) : value,
dataType,
componentType: DATATYPE_TO_COMPONENT[dataType] || 'TextInput',
mandatory: { value: false },
selected: false,
isCustomField: false,
isNew: jsonDifferences.isNew.includes(key),
isRemoved: jsonDifferences.isRemoved.includes(key),
isExisting: jsonDifferences.isExisting.includes(key),
};
};
export const parseDataAndBuildFields = (data, jsonDifferences = JSON_DIFFERENCE) => {
const obj = data || {};
const result = [];
Object.entries(obj).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const nestedKeys = Object.keys(value);
if (nestedKeys.length === 0) {
return;
}
nestedKeys.forEach((nestedKey) => {
const nestedValue = value[nestedKey];
if (
typeof nestedValue === 'object' &&
nestedValue !== null &&
!Array.isArray(nestedValue) &&
Object.keys(nestedValue).length === 0
) {
return;
}
result.push(buildFieldObject(`${key}.${nestedKey}`, nestedValue, nestedKey, jsonDifferences));
});
} else {
result.push(buildFieldObject(key, value, key, jsonDifferences));
}
});
return result;
};
export const findNextElementTop = (childComponents, currentLayout = 'desktop', componentsToBeIgnored = []) => {
const defaultTop = 0;
if (!childComponents || typeof childComponents !== 'object' || Object.keys(childComponents).length === 0) {
return defaultTop;
}
try {
let highestTop = -1;
let lastComponent = null;
Object.entries(childComponents).forEach(([componentId, component]) => {
if (componentsToBeIgnored.includes(componentId)) {
return;
}
const currentTop = component?.component?.layouts?.[currentLayout]?.top || 0;
if (currentTop > highestTop) {
highestTop = currentTop;
lastComponent = component;
}
});
if (
lastComponent &&
lastComponent.component &&
lastComponent.component.layouts &&
lastComponent.component.layouts[currentLayout]
) {
const { top = 0, height = 0 } = lastComponent.component.layouts[currentLayout];
return top + height;
}
return defaultTop;
} catch (error) {
console.error('Error finding last element position:', error);
return defaultTop;
}
};
export const getComponentIcon = (componentType, darkMode) => {
if (!componentType) return null;
const component = componentTypeDefinitionMap[componentType];
const iconName = component.name.toLowerCase();
return <WidgetIcon name={iconName} version={component?.version} fill={darkMode ? '#3A3F42' : '#D7DBDF'} width="16" />;
};
export const getInputTypeOptions = (darkMode) => {
const constructOptions = (component) => {
return {
label: component.displayName,
value: component.component,
leadingIcon: (
<WidgetIcon
name={component.name.toLowerCase()}
version={component?.version}
fill={darkMode ? '#3A3F42' : '#D7DBDF'}
width="16"
/>
),
};
};
return INPUT_COMPONENTS_FOR_FORM.reduce((options, component) => {
options[component] = constructOptions(componentTypeDefinitionMap[component]);
return options;
}, {});
};
export const constructFeildForSave = (field) => {
const { key, value, dataType, componentType, mandatory, selected, isCustomField } = field;
return {
key,
value: dataType === 'number' || dataType === 'boolean' ? ensureHandlebars(value) : value,
dataType,
componentType,
mandatory: mandatory?.value || false,
selected: selected?.value || false,
isCustomField: isCustomField || false,
};
};
const extractKeys = (json, parentKey = '') => {
if (!json || typeof json !== 'object') return [];
return Object.keys(json).reduce((keys, key) => {
const currentKey = parentKey ? `${parentKey}.${key}` : key;
const value = json[key];
if (value && typeof value === 'object' && !Array.isArray(value)) {
return [...keys, currentKey, ...extractKeys(value, currentKey)];
}
return [...keys, currentKey];
}, []);
};
export const analyzeJsonDifferences = (newJson, existingJson) => {
if (!newJson) return JSON_DIFFERENCE;
const newKeys = extractKeys(newJson);
const existingKeys = extractKeys(existingJson);
return {
isExisting: newKeys.filter((key) => existingKeys.includes(key)),
isNew: newKeys.filter((key) => !existingKeys.includes(key)),
isRemoved: existingKeys.filter((key) => !newKeys.includes(key)),
};
};
export const mergeFieldsWithComponentDefinition = (fields, getComponentDefinition) => {
return fields
.map((field) => {
if (field.componentId) {
const componentData = getFieldDataFromComponent(field.componentId, getComponentDefinition);
if (!componentData) {
return null;
}
return {
...field,
label: componentData?.label || field.label || '',
name: componentData?.name || field.name || '',
value: componentData?.value || field.value || '',
mandatory: componentData?.mandatory || field.mandatory || false,
visibility: componentData?.visibility || field.visibility || false,
selected: componentData?.selected || field.selected || false,
placeholder: componentData?.placeholder || field.placeholder || '',
componentType: componentData?.componentType || field.componentType || 'TextInput',
};
}
return field;
})
.filter((field) => field !== null);
};
export const mergeFormFieldsWithNewData = (existingFields, newFields) => {
if (!existingFields || existingFields.length === 0) return newFields;
const existingFieldsMap = {};
existingFields.forEach((field) => {
if (field.key) {
existingFieldsMap[field.key] = field;
}
});
return newFields.map((newField) => {
if (newField.isNew || !existingFieldsMap[newField.key]) {
return newField;
}
return {
...newField,
...omit(existingFieldsMap[newField.key], ['isNew']),
};
});
};
export const cleanupFormFields = (fields) => {
return uniqBy(
fields.filter((field) => !!field.componentId),
'componentId'
).map((field) => ({
componentId: field.componentId,
isCustomField: field.isCustomField,
dataType: field.dataType,
key: field.key,
}));
};
export const findFirstKeyValuePairWithPath = (data, basePath = '') => {
let current = data;
let pathSegments = [];
if (data === null || data === undefined || data?.length === 0) {
return {
value: data,
path: basePath,
};
}
while (Array.isArray(current) && current.length > 0) {
pathSegments.push('[0]');
current = current[0];
}
if (current && typeof current === 'object' && !Array.isArray(current)) {
// Inject path segments before the closing "}}"
const insertAt = basePath.lastIndexOf('}}');
const fullPath =
insertAt !== -1
? basePath.slice(0, insertAt) + pathSegments.join('') + basePath.slice(insertAt)
: basePath + pathSegments.join('');
return {
value: current,
path: fullPath,
};
}
return {
value: null,
path: null,
};
};
export const mergeArrays = (arr1, arr2) => {
const map = new Map();
// Add all from arr1
arr1.forEach((item) => map.set(item.isCustomField ? item.componentId : item.key, item));
// Overwrite/add from arr2
arr2.forEach((item) => map.set(item.isCustomField ? item.componentId : item.key, item));
return Array.from(map.values());
};

View file

@ -43,7 +43,9 @@ export const Modal = ({ componentMeta, darkMode, ...restProps }) => {
return accordionItems;
};
const properties = Object.keys(componentMeta.properties);
const properties = Object.keys(componentMeta.properties || {}).filter(
(key) => componentMeta.properties[key].section !== 'additionalActions'
);
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
@ -64,7 +66,8 @@ export const Modal = ({ componentMeta, darkMode, ...restProps }) => {
apps,
allComponents,
validations,
darkMode
darkMode,
undefined
);
accordionItems.splice(1, 0, ...conditionalAccordionItems(component));

View file

@ -0,0 +1,527 @@
import React, { useState, useEffect } from 'react';
import Accordion from '@/_ui/Accordion';
import { EventManager } from '../EventManager';
import { renderElement } from '../Utils';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import List from '@/ToolJetUI/List/List';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { resolveReferences } from '@/_helpers/utils';
import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton';
import ListGroup from 'react-bootstrap/ListGroup';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SortableList from '@/_components/SortableList';
import Trash from '@/_ui/Icon/solidIcons/Trash';
export function TabsLayout({ componentMeta, darkMode, ...restProps }) {
const {
layoutPropertyChanged,
component,
dataQueries,
paramUpdated,
currentState,
eventsChanged,
apps,
allComponents,
pages,
} = restProps;
const isDynamicEnabled = resolveReferences(
component?.component?.definition?.properties?.useDynamicOptions?.value,
currentState
);
const [tabItems, setTabItems] = useState([]);
const [activeColumnPopoverIndex, setActiveColumnPopoverIndex] = useState(null);
const [hoveredTabItemIndex, setHoveredTabItemIndex] = useState(null);
let properties = [];
let additionalActions = [];
for (const [key] of Object.entries(componentMeta?.properties)) {
if (componentMeta?.properties[key]?.section === 'additionalActions') {
additionalActions.push(key);
} else {
properties.push(key);
}
}
const constructTabItems = () => {
const tabItemsValue = component?.component?.definition?.properties?.tabItems?.value;
let tabItems = [];
if (typeof tabItemsValue === 'string') {
tabItems = resolveReferences(tabItemsValue, currentState);
} else {
tabItems = tabItemsValue?.map((tabItem) => tabItem);
}
return tabItems?.map((tabItem) => {
const newTabItem = { ...tabItem };
Object.keys(tabItem)?.forEach((key) => {
if (typeof tabItem[key]?.value === 'boolean') {
newTabItem[key]['value'] = `{{${tabItem[key]?.value}}}`;
}
});
return newTabItem;
});
};
const handleAddTabItem = () => {
const generateNewTabItem = () => {
let found = false;
let title = '';
let currentNumber = tabItems.length;
let id = `t${currentNumber}`;
while (!found) {
title = `Tab ${currentNumber}`;
if (tabItems.find((tabItem) => tabItem.title === title) === undefined) {
found = true;
}
currentNumber += 1;
}
return {
id: id,
title,
visible: { value: '{{true}}' },
disable: { value: '{{false}}' },
iconVisibility: { value: '{{false}}' },
icon: { value: 'IconHome2' },
};
};
let newTabItem = generateNewTabItem();
const updatedTabItems = [...tabItems, newTabItem];
setTabItems(updatedTabItems);
updateAllTabItemsParams(updatedTabItems);
};
const updateAllTabItemsParams = (tabItems) => {
paramUpdated({ name: 'tabItems' }, 'value', tabItems, 'properties', false);
};
const getItemStyle = (isDragging, draggableStyle) => ({
userSelect: 'none',
...draggableStyle,
});
const handleDeleteTabItem = (index) => {
const updatedTabItems = tabItems.filter((tabItem, i) => i !== index);
setTabItems(updatedTabItems);
updateAllTabItemsParams(updatedTabItems);
};
const reorderTabItems = (startIndex, endIndex) => {
const result = Array.from(tabItems);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
setTabItems(result);
updateAllTabItemsParams(result);
};
const onDragEnd = ({ source, destination }) => {
if (!destination || source.index === destination.index) {
return;
}
reorderTabItems(source.index, destination.index);
};
const handleValueChange = (item, value, property, index) => {
const updatedTabItems = tabItems.map((tabItem) => {
if (tabItem.id === item.id) {
return {
...tabItem,
[property]: value,
};
}
return tabItem;
});
setTabItems(updatedTabItems);
updateAllTabItemsParams(updatedTabItems);
};
const onChangeVisibility = (item, value, property, index) => {
const updatedTabItems = tabItems.map((tabItem) => {
if (tabItem.id === item.id) {
let newVisibilityValue = resolveReferences(tabItem[property]);
newVisibilityValue = typeof newVisibilityValue === 'boolean' ? newVisibilityValue : newVisibilityValue['value'];
return {
...tabItem,
[property]: !newVisibilityValue,
};
}
return tabItem;
});
setTabItems(updatedTabItems);
updateAllTabItemsParams(updatedTabItems);
};
const onChangeIcon = (item, value, property, index) => {
const updatedTabItems = tabItems.map((tabItem) => {
if (tabItem.id === item.id) {
return {
...tabItem,
[property]: value,
iconVisibility: { value: true },
};
}
return tabItem;
});
setTabItems(updatedTabItems);
updateAllTabItemsParams(updatedTabItems);
};
const _renderOverlay = (item, index) => {
return (
<Popover className={`${darkMode && 'dark-theme theme-dark'}`} style={{ minWidth: '248px' }}>
<Popover.Body>
<div className="field mb-3" data-cy={`input-and-label-tab-title`}>
<label data-cy={`label-tab-title`} className="font-weight-500 mb-1 font-size-12">
{'Tab Title'}
</label>
<CodeHinter
currentState={currentState}
type={'basic'}
initialValue={item?.title}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'Tab Title'}
onChange={(value) => handleValueChange(item, value, 'title', index)}
/>
</div>
<div className="field mb-3" data-cy={`input-and-label-tab-id`}>
<label data-cy={`label-tab-id`} className="font-weight-500 mb-1 font-size-12">
{'Id'}
</label>
<CodeHinter
currentState={currentState}
type={'basic'}
initialValue={item?.id}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'Tab ID'}
onChange={(value) => handleValueChange(item, value, 'id', index)}
/>
</div>
<div className="field mb-3" data-cy={`input-and-label-tab-id`}>
<CodeHinter
currentState={currentState}
initialValue={item?.icon?.value || ''}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
component={component}
type={'fxEditor'}
paramLabel={'Icon'}
paramName={'icon'}
onChange={(value) => {
onChangeIcon(item, { value }, 'icon', index);
}}
onVisibilityChange={(value) => onChangeVisibility(item, { value: true }, 'iconVisibility', index)}
fieldMeta={{ type: 'icon', displayName: 'Icon' }}
paramType={'icon'}
/>
</div>
<div className="field mb-2">
<CodeHinter
currentState={currentState}
initialValue={item?.fieldBackgroundColor?.value}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
component={component}
type={'fxEditor'}
paramLabel={'Background'}
paramName={'fieldBackgroundColor'}
onChange={(value) => {
handleValueChange(item, { value }, 'fieldBackgroundColor', index);
}}
fieldMeta={{ type: 'color', displayName: 'Background' }}
paramType={'color'}
/>
</div>
<div className="field mb-2">
<CodeHinter
currentState={currentState}
initialValue={item?.loading?.value}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
component={component}
type={'fxEditor'}
paramLabel={'Loading'}
paramName={'loading'}
onChange={(value) => {
handleValueChange(item, { value }, 'loading', index);
}}
fieldMeta={{ type: 'toggle', displayName: 'Loading' }}
paramType={'toggle'}
/>
</div>
<div className="field mb-2" data-cy={`input-and-label-tab-visible`}>
<CodeHinter
currentState={currentState}
initialValue={item?.visible?.value}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
component={component}
type={'fxEditor'}
paramLabel={'Visibility'}
onChange={(value) => handleValueChange(item, { value }, 'visible', index)}
paramName={'visible'}
onFxPress={(active) => handleOnFxPress(active, index, 'visible')}
fxActive={item?.visible?.fxActive}
fieldMeta={{
type: 'toggle',
displayName: 'Visible',
}}
paramType={'toggle'}
/>
</div>
<div className="field" data-cy={`input-and-label-tab-disable`}>
<CodeHinter
currentState={currentState}
initialValue={item?.disable?.value}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
component={component}
type={'fxEditor'}
paramLabel={'Disable'}
paramName={'disable'}
onChange={(value) => handleValueChange(item, { value }, 'disable', index)}
onFxPress={(active) => handleOnFxPress(active, index, 'disable')}
fxActive={item?.disable?.fxActive}
fieldMeta={{
type: 'toggle',
displayName: 'Disable',
}}
paramType={'toggle'}
/>
</div>
</Popover.Body>
</Popover>
);
};
useEffect(() => {
setTabItems(constructTabItems());
}, [isDynamicEnabled, component?.id]);
const handleToggleColumnPopover = (index) => {
setActiveColumnPopoverIndex(index);
};
const _renderTabOptions = () => {
return (
<List style={{ marginBottom: '20px' }}>
<DragDropContext
onDragEnd={(result) => {
onDragEnd(result);
}}
>
<Droppable droppableId="droppable">
{({ innerRef, droppableProps, placeholder }) => (
<div className="w-100" {...droppableProps} ref={innerRef}>
{tabItems?.map((item, index) => {
return (
<Draggable key={item.title + item.id} draggableId={item.title + item.id} index={index}>
{(provided, snapshot) => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
className="tabs-list"
>
<OverlayTrigger
trigger="click"
placement="left"
rootClose
overlay={_renderOverlay(item, index)}
onToggle={(show) => {
if (show) {
handleToggleColumnPopover(index);
} else {
handleToggleColumnPopover(null);
}
}}
>
<div
key={item.title}
style={{
marginBottom: '8px',
}}
>
<ListGroup.Item
isDraggable={true}
onMouseEnter={() => setHoveredTabItemIndex(index)}
onMouseLeave={() => setHoveredTabItemIndex(null)}
className={activeColumnPopoverIndex === index && 'active-column-list'}
{...restProps}
>
<div className="row">
<div className="col-auto d-flex align-items-center">
<SortableList.DragHandle show />
</div>
<div className="col text-truncate cursor-pointer" style={{ padding: '0px' }}>
{resolveReferences(item.title, currentState)}
</div>
<div className="col-auto">
{index === hoveredTabItemIndex && (
<ButtonSolid
variant="danger"
size="xs"
className={'delete-icon-btn'}
onClick={(e) => {
e.stopPropagation();
handleDeleteTabItem(index);
}}
>
<span className="d-flex">
<Trash fill={'var(--tomato9)'} width={12} />
</span>
</ButtonSolid>
)}
</div>
</div>
</ListGroup.Item>
</div>
</OverlayTrigger>
</div>
)}
</Draggable>
);
})}
{placeholder}
</div>
)}
</Droppable>
</DragDropContext>
<AddNewButton onClick={handleAddTabItem} className="mt-0">
Add new tab
</AddNewButton>
</List>
);
};
let items = [];
if (properties.length > 0) {
items.push({
title: 'Options',
isOpen: true,
children: isDynamicEnabled ? (
properties?.map((property) =>
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
property,
'properties',
currentState,
allComponents,
darkMode
)
)
) : (
<>
{renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'useDynamicOptions',
'properties',
currentState,
allComponents
)}
{_renderTabOptions()}
</>
),
});
}
items.push({
title: 'Events',
isOpen: true,
children: (
<EventManager
sourceId={component?.id}
eventSourceType="component"
eventMetaDefinition={componentMeta}
currentState={currentState}
dataQueries={dataQueries}
components={allComponents}
eventsChanged={eventsChanged}
apps={apps}
darkMode={darkMode}
pages={pages}
/>
),
});
items.push({
title: `Additional Actions`,
isOpen: true,
children: additionalActions.map((property) => {
return renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
property,
'properties',
currentState,
allComponents,
darkMode,
componentMeta.properties?.[property]?.placeholder
);
}),
});
items.push({
title: 'Devices',
isOpen: true,
children: (
<>
{renderElement(
component,
componentMeta,
layoutPropertyChanged,
dataQueries,
'showOnDesktop',
'others',
currentState,
allComponents
)}
{renderElement(
component,
componentMeta,
layoutPropertyChanged,
dataQueries,
'showOnMobile',
'others',
currentState,
allComponents
)}
</>
),
});
return <Accordion items={items} />;
}

View file

@ -76,7 +76,10 @@ export const ColumnPopoverContent = ({
</div>
</div>
</Popover.Header>
<Popover.Body className={`table-column-popover ${darkMode && 'theme-dark'}`}>
<Popover.Body
className={`table-column-popover ${darkMode && 'theme-dark'}`}
style={{ maxHeight: '80vh', overflowY: 'auto' }}
>
{activeTab === 'propertiesTab' ? (
<PropertiesTabElements
column={column}

View file

@ -7,11 +7,57 @@ import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandlePropert
import { OptionsList } from '../SelectOptionsList/OptionsList';
import { ValidationProperties } from './ValidationProperties';
import DatepickerProperties from './DatepickerProperties';
import { Option } from '@/AppBuilder/CodeBuilder/Elements/Select';
import DeprecatedColumnTypeMsg from './DeprecatedColumnTypeMsg';
import DeprecatedColumnTypeMsg, {
DeprecatedColumnTooltip,
checkIfTableColumnDeprecated,
} from './DeprecatedColumnTypeMsg';
import CustomSelect from '@/_ui/Select';
import defaultStyles from '@/_ui/Select/styles';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { getColumnIcon } from '../utils';
import { components } from 'react-select';
import Check from '@/_ui/Icon/solidIcons/Check';
import Icon from '@/_ui/Icon/solidIcons/index';
const CustomOption = (props) => {
const ColumnIcon = getColumnIcon(props.data.value);
const isDeprecated = checkIfTableColumnDeprecated(props.data.value);
return (
<components.Option {...props}>
<DeprecatedColumnTooltip columnType={props.data.value}>
<div className="d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center gap-2">
{ColumnIcon && <ColumnIcon width="16" height="16" />}
<span>{props.label}</span>
</div>
<div className="d-flex align-items-center gap-2">
{props.isSelected && (
<span>
<Check width={'20'} fill={'#3E63DD'} />
</span>
)}
{isDeprecated && (
<span>
<Icon name={'warning'} height={16} width={16} fill="#DB4324" />
</span>
)}
</div>
</div>
</DeprecatedColumnTooltip>
</components.Option>
);
};
const CustomValueContainer = ({ data, ...props }) => {
const Icon = getColumnIcon(data.value);
return (
<div className="d-flex align-items-center gap-2">
{Icon && <Icon width="16" height="16" />}
<span>{data.label}</span>
</div>
);
};
export const PropertiesTabElements = ({
column,
@ -54,6 +100,7 @@ export const PropertiesTabElements = ({
{ label: 'Link', value: 'link' },
{ label: 'JSON', value: 'json' },
{ label: 'Markdown', value: 'markdown' },
{ label: 'HTML', value: 'html' },
// Following column types are deprecated
{ label: 'Default', value: 'default' },
{ label: 'Dropdown', value: 'dropdown' },
@ -64,7 +111,11 @@ export const PropertiesTabElements = ({
{ label: 'Multiple badges', value: 'badges' },
{ label: 'Tags', value: 'tags' },
]}
components={{ DropdownIndicator, Option }}
components={{
DropdownIndicator,
Option: CustomOption,
SingleValue: CustomValueContainer,
}}
onChange={(value) => {
onColumnItemChange(index, 'columnType', value);
}}

View file

@ -129,6 +129,7 @@ export const StylesTabElements = ({
'number',
'json',
'markdown',
'html',
'boolean',
'select',
'text',
@ -147,7 +148,7 @@ export const StylesTabElements = ({
property="textColor"
props={column}
component={component}
paramMeta={{ type: 'color', displayName: 'Text color' }}
paramMeta={{ type: 'colorSwatches', displayName: 'Text color' }}
paramType="properties"
/>
</div>
@ -162,7 +163,7 @@ export const StylesTabElements = ({
property="cellBackgroundColor"
props={column}
component={component}
paramMeta={{ type: 'color', displayName: 'Cell color' }}
paramMeta={{ type: 'colorSwatches', displayName: 'Cell color' }}
paramType="properties"
/>
</div>

View file

@ -34,6 +34,8 @@ export const ProgramaticallyHandleProperties = ({
return props.linkColor;
case 'useDynamicOptions':
return props?.useDynamicOptions;
case 'autoAssignColors':
return props?.autoAssignColors;
case 'makeDefaultOption':
return props?.[index]?.makeDefaultOption;
case 'textColor':
@ -52,6 +54,10 @@ export const ProgramaticallyHandleProperties = ({
return props?.isDateSelectionEnabled;
case 'jsonIndentation':
return props?.jsonIndentation;
case 'labelColor':
return props?.labelColor;
case 'optionColor':
return props?.optionColor;
default:
return;
}
@ -74,6 +80,14 @@ export const ProgramaticallyHandleProperties = ({
if (property === 'textColor') {
return definitionObj?.value ?? '#11181C';
}
if (property === 'labelColor') {
// return definitionObj?.value ?? 'var(--cc-primary-text)';
return definitionObj?.value ?? '#1B1F24';
}
if (property === 'optionColor') {
// return definitionObj?.value ?? 'var(--cc-surface2-surface)';
return definitionObj?.value ?? '#E4E7EB';
}
if (property === 'underlineColor') {
return definitionObj?.value ?? '#4368E3';
}

View file

@ -9,6 +9,7 @@ import Popover from 'react-bootstrap/Popover';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties';
import { resolveReferences } from '@/_helpers/utils';
import { Button as ButtonComponent } from '@/components/ui/Button/Button';
import { unset } from 'lodash';
export const OptionsList = ({
column,
@ -141,12 +142,23 @@ export const OptionsList = ({
props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true);
};
const handleOptionColorChange = (index, property, value) => {
handleSelectOption(option, optionIndex, value, index, property);
};
return (
<Popover
id="popover-basic"
className={`${darkMode && 'dark-theme'}`}
onClick={(e) => e.stopPropagation()}
style={{ zIndex: 99999, minWidth: 200 }}
style={{
zIndex: 99999,
minWidth: 200,
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
borderRadius: '6px',
border: '1px solid var(--border-default)',
}}
>
<Popover.Body>
<div className="field mb-2 tj-app-input" onClick={(e) => e.stopPropagation()}>
@ -167,7 +179,7 @@ export const OptionsList = ({
}}
/>
</div>
<div className="field mb-2 tj-app-input" onClick={(e) => e.stopPropagation()}>
<div className="field tj-app-input" onClick={(e) => e.stopPropagation()}>
<label data-cy={`label-action-button-text`} className="form-label">
Option value
</label>
@ -185,8 +197,34 @@ export const OptionsList = ({
}}
/>
</div>
<div className="field table-select-colorpicker" style={{ marginBottom: '8px', marginTop: '8px' }}>
<ProgramaticallyHandleProperties
label="Label color"
index={index}
darkMode={darkMode}
callbackFunction={handleOptionColorChange}
property="labelColor"
props={option}
component={component}
paramMeta={{ type: 'colorSwatches', displayName: 'Label color' }}
paramType="properties"
/>
</div>
<div className="field table-select-colorpicker" style={{ marginBottom: '24px' }}>
<ProgramaticallyHandleProperties
label="Option color"
index={index}
darkMode={darkMode}
callbackFunction={handleOptionColorChange}
property="optionColor"
props={option}
component={component}
paramMeta={{ type: 'colorSwatches', displayName: 'Option color' }}
paramType="properties"
/>
</div>
<ProgramaticallyHandleProperties
label="Make this option as default"
label="Make this as default option"
currentState={currentState}
index={optionIndex}
darkMode={darkMode}
@ -220,6 +258,21 @@ export const OptionsList = ({
title: 'Options',
children: (
<div className="d-flex custom-gap-7 flex-column">
<ProgramaticallyHandleProperties
label="Auto assign colors"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="autoAssignColors"
props={column}
component={component}
paramMeta={{
type: 'toggle',
displayName: 'Auto assign colors',
}}
paramType="properties"
/>
<ProgramaticallyHandleProperties
label="Dynamic option"
currentState={currentState}
@ -330,10 +383,17 @@ export const OptionsList = ({
<div>
{column?.options?.length === 0 && <NoListItem text={'There are no columns'} dataCy={`-columns`} />}
<div>
<AddNewButton dataCy={`button-add-column`} onClick={() => createNewOption()}>
{/* {this.props.t('widget.Table.addNewColumn', ' Add new column')} */}
<ButtonComponent
leadingIcon="addrectangle"
onClick={() => {
createNewOption();
}}
variant="secondary"
className="tw-w-full mt-2"
width="100%"
>
Add new option
</AddNewButton>
</ButtonComponent>
</div>
</div>
</List>

View file

@ -19,6 +19,24 @@ import { ProgramaticallyHandleProperties } from './ProgramaticallyHandleProperti
import { ColumnPopoverContent } from './ColumnManager/ColumnPopover';
import { useAppDataStore } from '@/_stores/appDataStore';
import { checkIfTableColumnDeprecated } from './ColumnManager/DeprecatedColumnTypeMsg';
import {
TextTypeIcon,
DatepickerTypeIcon,
SelectTypeIcon,
MultiselectTypeIcon,
BooleanTypeIcon,
ImageTypeIcon,
LinkTypeIcon,
JSONTypeIcon,
MarkdownTypeIcon,
HTMLTypeIcon,
NumberTypeIcon,
StringTypeIcon,
BadgeTypeIcon,
TagsTypeIcon,
RadioTypeIcon,
} from './_assets';
import { getColumnIcon } from './utils';
const NON_EDITABLE_COLUMNS = ['link', 'image'];
class TableComponent extends React.Component {
@ -633,6 +651,8 @@ class TableComponent extends React.Component {
return 'JSON';
case 'markdown':
return 'Markdown';
case 'html':
return 'HTML';
default:
capitalize(text ?? '');
}
@ -677,6 +697,7 @@ class TableComponent extends React.Component {
}
}}
darkMode={darkMode}
showIconOnHover={true}
// menuActions={[
// {
// label: 'Delete',
@ -692,6 +713,7 @@ class TableComponent extends React.Component {
}`}
columnType={item?.columnType}
isDeprecated={checkIfTableColumnDeprecated(item?.columnType)}
Icon={getColumnIcon(item?.columnType)}
/>
</div>
</OverlayTrigger>
@ -777,6 +799,7 @@ class TableComponent extends React.Component {
'showBulkUpdateActions',
'visibility',
'disabledState',
'dynamicHeight',
];
items.push({

View file

@ -0,0 +1,20 @@
import React from 'react';
const BadgeTypeIcon = ({ fill = '#ACB2B9', width = '14', className = '', viewBox = '0 0 14 14', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
d="M7.83948 0.906557C7.46867 0.257384 6.53265 0.257385 6.16184 0.906558L5.28086 2.44888L3.6146 1.83367C2.91326 1.57473 2.19623 2.1764 2.32945 2.91204L2.64597 4.65982L0.974096 5.25959C0.270394 5.51204 0.107857 6.43384 0.682777 6.91174L2.04869 8.04717L1.15349 9.58128C0.776693 10.227 1.2447 11.0376 1.99231 11.0342L3.76849 11.0259L4.06883 12.7766C4.19526 13.5134 5.07483 13.8336 5.6453 13.3504L7.00066 12.2024L8.35602 13.3504C8.9265 13.8336 9.80606 13.5134 9.93246 12.7766L10.2328 11.0259L12.009 11.0342C12.7566 11.0376 13.2246 10.227 12.8478 9.58128L11.9526 8.04717L13.3185 6.91174C13.8934 6.43384 13.7309 5.51205 13.0272 5.25959L11.3554 4.65982L11.6719 2.91204C11.8051 2.1764 11.0881 1.57473 10.3867 1.83367L8.72046 2.44888L7.83948 0.906557Z"
fill={fill}
/>
</svg>
);
export default BadgeTypeIcon;

View file

@ -0,0 +1,22 @@
import React from 'react';
const BooleanTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.6673 8.00007C14.6673 5.63314 12.7486 3.71436 10.3816 3.71436H5.6197C3.25277 3.71436 1.33398 5.63314 1.33398 8.00007C1.33398 10.367 3.25277 12.2858 5.6197 12.2858H10.3816C12.7486 12.2858 14.6673 10.367 14.6673 8.00007ZM5.6197 9.90483C6.67167 9.90483 7.52446 9.05204 7.52446 8.00007C7.52446 6.9481 6.67167 6.09531 5.6197 6.09531C4.56773 6.09531 3.71494 6.9481 3.71494 8.00007C3.71494 9.05204 4.56773 9.90483 5.6197 9.90483Z"
fill={fill}
/>
</svg>
);
export default BooleanTypeIcon;

View file

@ -0,0 +1,29 @@
import React from 'react';
const DatepickerTypeIcon = ({
fill = '#ACB2B9',
width = '16',
className = '',
viewBox = '0 0 16 16',
style,
height,
}) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.43655 2.35889C5.43655 1.79245 4.97735 1.33325 4.41091 1.33325C3.84447 1.33325 3.38527 1.79245 3.38527 2.35889V3.38453H2.87245C2.02277 3.38453 1.33398 4.07332 1.33398 4.923V13.1281C1.33398 13.9778 2.02277 14.6666 2.87245 14.6666H13.1289C13.9785 14.6666 14.6673 13.9778 14.6673 13.1281V4.923C14.6673 4.07332 13.9785 3.38453 13.1289 3.38453H12.616V2.35889C12.616 1.79245 12.1569 1.33325 11.5904 1.33325C11.0239 1.33325 10.5648 1.79245 10.5648 2.35889V3.38453H5.43655V2.35889ZM4.41091 8.25633C4.97735 8.25633 5.43655 7.79713 5.43655 7.23069C5.43655 6.66425 4.97735 6.20505 4.41091 6.20505C3.84447 6.20505 3.38527 6.66425 3.38527 7.23069C3.38527 7.79713 3.84447 8.25633 4.41091 8.25633ZM8.00065 8.25633C8.56709 8.25633 9.02629 7.79713 9.02629 7.23069C9.02629 6.66425 8.56709 6.20505 8.00065 6.20505C7.43421 6.20505 6.97501 6.66425 6.97501 7.23069C6.97501 7.79713 7.43421 8.25633 8.00065 8.25633ZM5.43655 11.3333C5.43655 11.8997 4.97735 12.3589 4.41091 12.3589C3.84447 12.3589 3.38527 11.8997 3.38527 11.3333C3.38527 10.7668 3.84447 10.3076 4.41091 10.3076C4.97735 10.3076 5.43655 10.7668 5.43655 11.3333ZM8.00065 12.3589C8.56709 12.3589 9.02629 11.8997 9.02629 11.3333C9.02629 10.7668 8.56709 10.3076 8.00065 10.3076C7.43421 10.3076 6.97501 10.7668 6.97501 11.3333C6.97501 11.8997 7.43421 12.3589 8.00065 12.3589ZM12.616 7.23069C12.616 7.79713 12.1569 8.25633 11.5904 8.25633C11.0239 8.25633 10.5648 7.79713 10.5648 7.23069C10.5648 6.66425 11.0239 6.20505 11.5904 6.20505C12.1569 6.20505 12.616 6.66425 12.616 7.23069Z"
fill={fill}
/>
</svg>
);
export default DatepickerTypeIcon;

View file

@ -0,0 +1,22 @@
import React from 'react';
const HTMLTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.76256 3.23814V10.381H13.2387V3.23814H2.76256ZM1.33398 3.00005C1.33398 2.34256 1.86698 1.80957 2.52446 1.80957H13.4768C14.1344 1.80957 14.6673 2.34256 14.6673 3.00005V10.6191C14.6673 11.2766 14.1344 11.8096 13.4768 11.8096H9.61417L9.95502 12.762H10.8578C11.2523 12.762 11.5721 13.0818 11.5721 13.4762C11.5721 13.8707 11.2523 14.1905 10.8578 14.1905H5.14351C4.74902 14.1905 4.42922 13.8707 4.42922 13.4762C4.42922 13.0818 4.74902 12.762 5.14351 12.762H6.04619L6.38705 11.8096H2.52446C1.86698 11.8096 1.33398 11.2766 1.33398 10.6191V3.00005ZM8.94081 3.98908C9.31506 4.11384 9.51732 4.51835 9.39257 4.8926L7.96399 9.17831C7.83925 9.55255 7.43474 9.75481 7.06049 9.63007C6.68624 9.50531 6.48398 9.1008 6.60874 8.72655L8.03731 4.44084C8.16205 4.0666 8.56657 3.86434 8.94081 3.98908ZM11.0769 4.95131C10.7739 4.69877 10.3235 4.73971 10.071 5.04277C9.81842 5.34583 9.85937 5.79623 10.1624 6.04878L10.9417 6.69821L9.91676 7.57677C9.61723 7.83349 9.58255 8.28443 9.83928 8.58394C10.096 8.88347 10.5469 8.91815 10.8465 8.66142L12.5131 7.23285C12.6727 7.09602 12.764 6.89581 12.7626 6.68556C12.761 6.47531 12.667 6.27639 12.5055 6.14179L11.0769 4.95131ZM6.08455 6.04238C6.38407 5.78565 6.41876 5.33472 6.16202 5.0352C5.9053 4.73568 5.45437 4.701 5.15485 4.95772L3.48818 6.38629C3.32855 6.52312 3.23731 6.72333 3.23877 6.93358C3.24022 7.14383 3.33424 7.34275 3.49576 7.47735L4.92433 8.66783C5.22738 8.92037 5.67778 8.87943 5.93034 8.57637C6.18288 8.27332 6.14194 7.82291 5.83888 7.57037L5.05957 6.92094L6.08455 6.04238Z"
fill={fill}
/>
</svg>
);
export default HTMLTypeIcon;

View file

@ -0,0 +1,22 @@
import React from 'react';
const ImageTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.33398 3.99992C1.33398 2.52716 2.52789 1.33325 4.00065 1.33325H12.0007C13.4734 1.33325 14.6673 2.52716 14.6673 3.99992V8.29927L13.1696 7.31896C12.0133 6.5621 10.4997 6.64252 9.43012 7.51765L5.93798 10.3749C5.20615 10.9736 4.17053 11.0287 3.37937 10.5108L1.33398 9.172V3.99992ZM12.622 8.15566L14.6673 9.49444V11.9999C14.6673 13.4727 13.4734 14.6666 12.0007 14.6666H4.00065C2.52789 14.6666 1.33398 13.4727 1.33398 11.9999V10.3672L2.83171 11.3475C3.98803 12.1044 5.50162 12.0239 6.57122 11.1488L10.0634 8.29161C10.7952 7.69284 11.8308 7.63781 12.622 8.15566ZM5.66732 7.33325C6.58779 7.33325 7.33398 6.58706 7.33398 5.66659C7.33398 4.74611 6.58779 3.99992 5.66732 3.99992C4.74684 3.99992 4.00065 4.74611 4.00065 5.66659C4.00065 6.58706 4.74684 7.33325 5.66732 7.33325Z"
fill={fill}
/>
</svg>
);
export default ImageTypeIcon;

View file

@ -0,0 +1,22 @@
import React from 'react';
const JSONTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.40476 2.28564C3.48428 2.28564 2.73809 3.03183 2.73809 3.95231V5.87C2.73809 6.20229 2.5992 6.51946 2.35501 6.74483L1.56383 7.47503C1.41731 7.61025 1.33398 7.80055 1.33398 7.99993C1.33398 8.19931 1.41731 8.38961 1.56383 8.52483L2.35501 9.25503C2.5992 9.4804 2.73809 9.79757 2.73809 10.1299V12.0475C2.73809 12.968 3.48428 13.7142 4.40476 13.7142H4.77226C5.16675 13.7142 5.48655 13.3944 5.48655 12.9999C5.48655 12.6055 5.16675 12.2856 4.77226 12.2856H4.40476C4.27325 12.2856 4.16666 12.1791 4.16666 12.0475V10.1299C4.16666 9.39882 3.86111 8.70103 3.32389 8.20522L3.10145 7.99993L3.32389 7.79463C3.86111 7.29883 4.16666 6.60104 4.16666 5.87V3.95231C4.16666 3.82082 4.27325 3.71422 4.40476 3.71422H4.77226C5.16675 3.71422 5.48655 3.39442 5.48655 2.99993C5.48655 2.60544 5.16675 2.28564 4.77226 2.28564H4.40476ZM11.5966 2.28564C12.517 2.28564 13.2632 3.03183 13.2632 3.95231V5.87C13.2632 6.20229 13.4021 6.51946 13.6463 6.74483L14.4375 7.47503C14.584 7.61025 14.6673 7.80055 14.6673 7.99993C14.6673 8.19931 14.584 8.38961 14.4375 8.52483L13.6463 9.25503C13.4021 9.4804 13.2632 9.79757 13.2632 10.1299V12.0475C13.2632 12.968 12.517 13.7142 11.5966 13.7142H11.229C10.8346 13.7142 10.5148 13.3944 10.5148 12.9999C10.5148 12.6055 10.8346 12.2856 11.229 12.2856H11.5966C11.7281 12.2856 11.8347 12.1791 11.8347 12.0475V10.1299C11.8347 9.39882 12.1402 8.70103 12.6774 8.20522L12.8999 7.99993L12.6774 7.79463C12.1402 7.29883 11.8347 6.60104 11.8347 5.87V3.95231C11.8347 3.82082 11.7281 3.71422 11.5966 3.71422H11.229C10.8346 3.71422 10.5148 3.39442 10.5148 2.99993C10.5148 2.60544 10.8346 2.28564 11.229 2.28564H11.5966ZM7.16732 7.99993C7.16732 8.46017 7.54041 8.83326 8.00065 8.83326C8.46089 8.83326 8.83398 8.46017 8.83398 7.99993C8.83398 7.53969 8.46089 7.1666 8.00065 7.1666C7.54041 7.1666 7.16732 7.53969 7.16732 7.99993ZM9.78637 7.99993C9.78637 8.46017 10.1595 8.83326 10.6197 8.83326C11.0799 8.83326 11.453 8.46017 11.453 7.99993C11.453 7.53969 11.0799 7.1666 10.6197 7.1666C10.1595 7.1666 9.78637 7.53969 9.78637 7.99993ZM4.54827 7.99993C4.54827 8.46017 4.92137 8.83326 5.3816 8.83326C5.84184 8.83326 6.21494 8.46017 6.21494 7.99993C6.21494 7.53969 5.84184 7.1666 5.3816 7.1666C4.92137 7.1666 4.54827 7.53969 4.54827 7.99993Z"
fill={fill}
/>
</svg>
);
export default JSONTypeIcon;

View file

@ -0,0 +1,22 @@
import React from 'react';
const LinkTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.4022 2.59843C11.7152 0.911524 8.98021 0.911527 7.29331 2.59843L6.34092 3.55082C5.96899 3.92275 5.96899 4.52577 6.34092 4.8977C6.71285 5.26962 7.31587 5.26962 7.6878 4.8977L8.64019 3.94531C9.58323 3.00226 11.1122 3.00226 12.0552 3.94531C12.9983 4.88835 12.9983 6.41733 12.0552 7.36037L11.1028 8.31276C10.7309 8.68468 10.7309 9.28771 11.1028 9.65963C11.4748 10.0316 12.0778 10.0316 12.4498 9.65963L13.4022 8.70725C15.089 7.02034 15.089 4.28533 13.4022 2.59843ZM10.1027 5.89794C10.4747 6.26988 10.4747 6.87289 10.1027 7.24483L7.24556 10.102C6.87362 10.4739 6.27061 10.4739 5.89867 10.102C5.52675 9.73005 5.52675 9.12704 5.89867 8.7551L8.75584 5.89794C9.12777 5.52601 9.73079 5.52601 10.1027 5.89794ZM4.89843 6.34031C5.27036 6.71223 5.27036 7.31526 4.89843 7.68718L3.94604 8.63957C3.003 9.58261 3.003 11.1116 3.94604 12.0547C4.88909 12.9977 6.41806 12.9977 7.3611 12.0547L8.31349 11.1023C8.68542 10.7303 9.28844 10.7303 9.66037 11.1023C10.0323 11.4742 10.0323 12.0772 9.66037 12.4491L8.70798 13.4015C7.02108 15.0884 4.28606 15.0884 2.59916 13.4015C0.912257 11.7146 0.912258 8.9796 2.59916 7.29269L3.55155 6.34031C3.92348 5.96838 4.5265 5.96838 4.89843 6.34031Z"
fill={fill}
/>
</svg>
);
export default LinkTypeIcon;

View file

@ -0,0 +1,22 @@
import React from 'react';
const MarkdownTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.76256 3.23804C1.97358 3.23804 1.33398 3.87763 1.33398 4.66661V11.3333C1.33398 12.1222 1.97358 12.7618 2.76256 12.7618H13.2387C14.0277 12.7618 14.6673 12.1222 14.6673 11.3333V4.66661C14.6673 3.87763 14.0277 3.23804 13.2387 3.23804H2.76256ZM5.26256 9.9047V8.02215L5.28429 8.07046C5.42797 8.3898 5.74558 8.59518 6.09577 8.59518C6.4459 8.59518 6.76349 8.38984 6.90719 8.07055L6.9292 8.02166V9.9047C6.9292 10.2334 7.1957 10.4999 7.52444 10.4999C7.85317 10.4999 8.11968 10.2334 8.11968 9.9047V6.32654C8.11968 5.87003 7.74959 5.49994 7.29307 5.49994C6.96781 5.49994 6.67278 5.69069 6.5393 5.98729L6.0958 6.9727L5.65248 5.98737C5.519 5.69073 5.22397 5.49994 4.89867 5.49994C4.44216 5.49994 4.07208 5.87002 4.07208 6.32653V9.9047C4.07208 10.2334 4.33857 10.4999 4.66732 10.4999C4.99606 10.4999 5.26256 10.2334 5.26256 9.9047ZM10.9768 6.09524C10.9768 5.76649 10.7103 5.5 10.3816 5.5C10.0529 5.5 9.78637 5.76649 9.78637 6.09524V8.35706H9.42922C9.18847 8.35706 8.97142 8.50208 8.8793 8.7245C8.78717 8.94693 8.83809 9.20295 9.00833 9.37319L9.96071 10.3256C10.0723 10.4372 10.2237 10.4999 10.3816 10.4999C10.5395 10.4999 10.6909 10.4372 10.8025 10.3256L11.7548 9.37319C11.9251 9.20295 11.9761 8.94693 11.8839 8.7245C11.7918 8.50208 11.5747 8.35706 11.334 8.35706H10.9768V6.09524Z"
fill={fill}
/>
</svg>
);
export default MarkdownTypeIcon;

View file

@ -0,0 +1,29 @@
import React from 'react';
const MultiselectTypeIcon = ({
fill = '#ACB2B9',
width = '16',
className = '',
viewBox = '0 0 16 16',
style,
height,
}) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.32825 2.53798C7.59912 2.25118 7.58621 1.79911 7.29941 1.52824C7.01261 1.25738 6.56053 1.2703 6.28967 1.55709L4.10308 3.87231L3.51158 3.26391C3.23659 2.98106 2.78438 2.9747 2.50153 3.24969C2.21868 3.52467 2.21231 3.97689 2.4873 4.25973L3.59841 5.40259C3.73415 5.54221 3.921 5.62035 4.11573 5.61894C4.31044 5.61753 4.49614 5.53668 4.62985 5.39512L7.32825 2.53798ZM7.32825 7.06179C7.59912 6.77499 7.58621 6.32291 7.29941 6.05205C7.01261 5.78118 6.56053 5.7941 6.28967 6.08089L4.10308 8.39611L3.51158 7.78771C3.23659 7.50486 2.78438 7.4985 2.50153 7.77349C2.21868 8.04847 2.21231 8.50069 2.4873 8.78354L3.59841 9.92639C3.73415 10.066 3.921 10.1442 4.11573 10.1427C4.31044 10.1413 4.49614 10.0605 4.62985 9.91893L7.32825 7.06179ZM7.29941 10.5758C7.58621 10.8467 7.59912 11.2988 7.32825 11.5856L4.62985 14.4428C4.49614 14.5843 4.31044 14.6651 4.11573 14.6666C3.921 14.668 3.73415 14.5898 3.59841 14.4502L2.4873 13.3073C2.21231 13.0245 2.21868 12.5723 2.50153 12.2973C2.78438 12.0223 3.23659 12.0287 3.51158 12.3115L4.10308 12.9199L6.28967 10.6047C6.56053 10.3179 7.01261 10.305 7.29941 10.5758ZM8.95181 3.23801C8.55733 3.23801 8.23753 3.55781 8.23753 3.9523C8.23753 4.34679 8.55733 4.66658 8.95181 4.66658H12.9994C13.3939 4.66658 13.7137 4.34679 13.7137 3.9523C13.7137 3.55781 13.3939 3.23801 12.9994 3.23801H8.95181ZM8.23753 7.99991C8.23753 7.60542 8.55733 7.28562 8.95181 7.28562H12.9994C13.3939 7.28562 13.7137 7.60542 13.7137 7.99991C13.7137 8.3944 13.3939 8.71419 12.9994 8.71419H8.95181C8.55733 8.71419 8.23753 8.3944 8.23753 7.99991ZM8.95181 11.3332C8.55733 11.3332 8.23753 11.653 8.23753 12.0475C8.23753 12.442 8.55733 12.7618 8.95181 12.7618H12.9994C13.3939 12.7618 13.7137 12.442 13.7137 12.0475C13.7137 11.653 13.3939 11.3332 12.9994 11.3332H8.95181Z"
fill={fill}
/>
</svg>
);
export default MultiselectTypeIcon;

View file

@ -0,0 +1,22 @@
import React from 'react';
const NumberTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.7222 1.34475C12.1408 1.41755 12.4211 1.81586 12.3482 2.23441L11.8807 4.92312H13.8981C14.3229 4.92312 14.6673 5.26752 14.6673 5.69235C14.6673 6.11719 14.3229 6.46158 13.8981 6.46158H11.6131L11.078 9.53851H13.3853C13.8101 9.53851 14.1545 9.88291 14.1545 10.3077C14.1545 10.7326 13.8101 11.077 13.3853 11.077H10.8104L10.297 14.0293C10.2242 14.4478 9.82586 14.7281 9.40731 14.6553C8.98876 14.5825 8.70847 14.1842 8.78126 13.7657L9.24886 11.077H5.68221L5.16876 14.0293C5.09597 14.4478 4.69766 14.7281 4.2791 14.6553C3.86056 14.5825 3.58026 14.1842 3.65305 13.7657L4.12065 11.077H2.10322C1.67838 11.077 1.33398 10.7326 1.33398 10.3077C1.33398 9.88291 1.67838 9.53851 2.10322 9.53851H4.38821L4.92333 6.46158H2.61604C2.1912 6.46158 1.8468 6.11719 1.8468 5.69235C1.8468 5.26752 2.1912 4.92312 2.61604 4.92312H5.19089L5.70433 1.97081C5.77712 1.55226 6.17544 1.27196 6.594 1.34475C7.01254 1.41755 7.29284 1.81586 7.22005 2.23441L6.75245 4.92312H10.3191L10.8325 1.97081C10.9053 1.55226 11.3036 1.27196 11.7222 1.34475ZM6.48489 6.46158L5.94977 9.53851H9.51642L10.0515 6.46158H6.48489Z"
fill={fill}
/>
</svg>
);
export default NumberTypeIcon;

View file

@ -0,0 +1,26 @@
import React from 'react';
const RadioTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
d="M5.83398 7.99992C5.83398 6.8033 6.80403 5.83325 8.00065 5.83325C9.19727 5.83325 10.1673 6.8033 10.1673 7.99992C10.1673 9.19654 9.19727 10.1666 8.00065 10.1666C6.80403 10.1666 5.83398 9.19654 5.83398 7.99992Z"
fill={fill}
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.00065 14.6666C11.6825 14.6666 14.6673 11.6818 14.6673 7.99992C14.6673 4.31802 11.6825 1.33325 8.00065 1.33325C4.31875 1.33325 1.33398 4.31802 1.33398 7.99992C1.33398 11.6818 4.31875 14.6666 8.00065 14.6666ZM8.00065 4.83325C6.25175 4.83325 4.83398 6.25102 4.83398 7.99992C4.83398 9.74882 6.25175 11.1666 8.00065 11.1666C9.74955 11.1666 11.1673 9.74882 11.1673 7.99992C11.1673 6.25102 9.74955 4.83325 8.00065 4.83325Z"
fill={fill}
/>
</svg>
);
export default RadioTypeIcon;

View file

@ -0,0 +1,22 @@
import React from 'react';
const SelectTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
<svg
className={className}
width={width}
height={height}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.00065 4.66661C2.86916 4.66661 2.76256 4.77321 2.76256 4.9047V11.0952C2.76256 11.2267 2.86916 11.3333 3.00065 11.3333H8.47684V4.66661H3.00065ZM1.33398 4.9047C1.33398 3.98423 2.08018 3.23804 3.00065 3.23804H13.0007C13.9211 3.23804 14.6673 3.98423 14.6673 4.9047V11.0952C14.6673 12.0157 13.9211 12.7618 13.0007 12.7618H3.00065C2.08018 12.7618 1.33398 12.0157 1.33398 11.0952V4.9047ZM10.3816 7.04756C10.189 7.04756 10.0154 7.16358 9.94166 7.34152C9.86796 7.51947 9.9087 7.72428 10.0449 7.86047L10.9972 8.81285C11.1832 8.99882 11.4847 8.99882 11.6707 8.81285L12.6231 7.86047C12.7593 7.72428 12.8 7.51947 12.7263 7.34152C12.6526 7.16358 12.4789 7.04756 12.2864 7.04756H10.3816Z"
fill={fill}
/>
</svg>
);
export default SelectTypeIcon;

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