Merge conflict resolved

This commit is contained in:
Shaurya Sharma 2025-03-20 12:39:16 +05:30
commit bb2664b47b
83 changed files with 1257 additions and 616 deletions

View file

@ -52,7 +52,7 @@ jobs:
run: |
git submodule update --init --recursive
git submodule foreach --recursive '
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout modularisation/v3'
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main'
- name: Set up Docker

View file

@ -50,7 +50,7 @@ jobs:
run: |
git submodule update --init --recursive
git submodule foreach --recursive '
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout modularisation/v3'
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main'
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master

View file

@ -215,7 +215,7 @@ jobs:
- name: Delete service
run: |
export SERVICE_ID=$(curl --request GET \
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \
--url 'https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
jq -r '.[0].service.id')
@ -583,7 +583,7 @@ jobs:
- name: Delete service
run: |
export SERVICE_ID=$(curl --request GET \
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \
--url 'https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${{ env.PR_NUMBER }}&limit=1' \
--header 'accept: application/json' \
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
jq -r '.[0].service.id')

4
.gitmodules vendored
View file

@ -1,8 +1,8 @@
[submodule "frontend/ee"]
path = frontend/ee
url = https://github.com/ToolJet/ee-frontend.git
branch = modularisation/v3
branch = main
[submodule "server/ee"]
path = server/ee
url = https://github.com/ToolJet/ee-server.git
branch = modularisation/v3
branch = main

View file

@ -10,7 +10,7 @@ RUN mkdir -p /app
WORKDIR /app
ARG CUSTOM_GITHUB_TOKEN
ARG BRANCH_NAME=modularisation/v3
ARG BRANCH_NAME=main
# Clone and checkout the frontend repository
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"

View file

@ -11,7 +11,7 @@ WORKDIR /app
# Set GitHub token and branch as build arguments
ARG CUSTOM_GITHUB_TOKEN
ARG BRANCH_NAME=modularisation/v3
ARG BRANCH_NAME=main
# Clone and checkout the frontend repository
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
@ -21,7 +21,7 @@ RUN git config --global http.postBuffer 524288000
RUN git clone https://github.com/ToolJet/ToolJet.git .
# The branch name needs to be changed the branch with modularisation in CE repo
RUN git checkout modularisation/v3
RUN git checkout main
RUN git submodule update --init --recursive

View file

@ -3,9 +3,13 @@ import { shallow } from 'zustand/shallow';
import './configHandle.scss';
import useStore from '@/AppBuilder/_stores/store';
import { findHighestLevelofSelection } from '../Grid/gridUtils';
import SolidIcon from '@/_ui/Icon/solidIcons/index';
const CONFIG_HANDLE_HEIGHT = 20;
const BUFFER_HEIGHT = 1;
export const ConfigHandle = ({
id,
position,
widgetTop,
widgetHeight,
setSelectedComponentAsModal = () => null, //! Only Modal widget passes this uses props down. All other widgets use selecto lib
@ -14,6 +18,7 @@ export const ConfigHandle = ({
showHandle,
componentType,
visibility,
subContainerIndex,
}) => {
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow);
@ -27,6 +32,7 @@ export const ConfigHandle = ({
(state) => componentType === 'Tabs' && state.getExposedValueOfComponent(id)?.currentTab,
shallow
);
const position = widgetTop < 15 ? 'bottom' : 'top';
const setComponentToInspect = useStore((state) => state.setComponentToInspect);
const isModal = componentType === 'Modal' || componentType === 'ModalV2';
@ -35,12 +41,12 @@ export const ConfigHandle = ({
const anyComponentHovered = state.getHoveredComponentForGrid() !== '' || state.hoveredComponentBoundaryId !== '';
// If one component is hovered and one is selected, show the handle for the hovered component
return (
isWidgetHovered ||
(showHandle &&
(!isMultipleComponentsSelected || (isModal && isModalOpen)) &&
!anyComponentHovered)
(subContainerIndex === 0 || subContainerIndex === null) &&
(isWidgetHovered ||
(showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) && !anyComponentHovered))
);
}, shallow);
let height = visibility === false ? 10 : widgetHeight;
return (
@ -48,7 +54,12 @@ export const ConfigHandle = ({
className={`config-handle ${customClassName}`}
widget-id={id}
style={{
top: position === 'top' ? '-20px' : widgetTop + height - (widgetTop < 10 ? 15 : 10),
top:
componentType === 'Modal' && isModalOpen
? '0px'
: position === 'top'
? '-20px'
: `${height - (CONFIG_HANDLE_HEIGHT + BUFFER_HEIGHT)}px`,
visibility: _showHandle ? 'visible' : 'hidden',
left: '-1px',
}}
@ -63,7 +74,10 @@ export const ConfigHandle = ({
>
<span
style={{
background: isModal && isModalOpen ? '#c6cad0' : '#4D72FA',
background:
visibility === false ? '#c6cad0' : componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
border: position === 'bottom' ? '1px solid white' : 'none',
color: visibility === false && 'var(--text-placeholder)',
}}
className="badge handle-content"
>
@ -77,17 +91,30 @@ export const ConfigHandle = ({
data-cy={`${componentName?.toLowerCase()}-config-handle`}
className="text-truncate"
>
<img
style={{ cursor: 'pointer', marginRight: '5px', verticalAlign: 'middle' }}
src="assets/images/icons/settings.svg"
width="12"
height="12"
draggable="false"
/>
{/* Settings Icon */}
<span style={{ cursor: 'pointer', marginRight: '5px' }}>
<SolidIcon
name="settings"
width="12"
height="12"
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
/>
</span>
<span>{componentName}</span>
{/* Divider */}
<hr
style={{
marginLeft: '10px',
height: '12px',
width: '2px',
backgroundColor: visibility === false ? 'var(--text-placeholder)' : '#fff',
opacity: 0.5,
}}
/>
</div>
{/* Delete Button */}
{!isMultipleComponentsSelected && !shouldFreeze && (
<div className="delete-part">
<div>
<img
style={{ cursor: 'pointer', marginLeft: '5px' }}
src="assets/images/icons/inspect.svg"
@ -99,19 +126,20 @@ export const ConfigHandle = ({
data-cy={`${componentName.toLowerCase()}-inspect-button`}
className="config-handle-inspect"
/>
<img
<span
style={{ cursor: 'pointer', marginLeft: '5px' }}
src="assets/images/icons/trash-light.svg"
width="12"
role="button"
height="12"
draggable="false"
onClick={() => {
deleteComponents([id]);
}}
data-cy={`${componentName.toLowerCase()}-delete-button`}
className="delete-icon"
/>
>
<SolidIcon
name="trash"
width="12"
height="12"
fill={visibility === false ? 'var(--text-placeholder)' : '#fff'}
/>
</span>
</div>
)}
</span>

View file

@ -31,22 +31,7 @@
.badge {
font-size: 9px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
.delete-part {
margin-left: 10px;
float: right;
}
.delete-part::before {
height: 12px;
display: inline-block;
width: 2px;
background-color: rgba(255, 255, 255, 0.8);
opacity: 0.5;
content: "";
vertical-align: middle;
}
border-bottom-right-radius: 0
}
}

View file

@ -6,7 +6,15 @@ import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { useDrop } from 'react-dnd';
import { addChildrenWidgetsToParent, addNewWidgetToTheEditor, computeViewerBackgroundColor } from './appCanvasUtils';
import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants';
import {
CANVAS_WIDTHS,
NO_OF_GRIDS,
WIDGETS_WITH_DEFAULT_CHILDREN,
GRID_HEIGHT,
CONTAINER_FORM_CANVAS_PADDING,
SUBCONTAINER_CANVAS_BORDER_WIDTH,
BOX_PADDING,
} from './appCanvasConstants';
import { useGridStore } from '@/_stores/gridStore';
import NoComponentCanvasContainer from './NoComponentCanvasContainer';
import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants';
@ -35,10 +43,10 @@ export const Container = React.memo(
canvasMaxWidth,
isViewerSidebarPinned,
pageSidebarStyle,
componentType,
}) => {
const realCanvasRef = useRef(null);
const components = useStore((state) => state.getContainerChildrenMapping(id), shallow);
const componentType = useStore((state) => state.getComponentTypeFromId(id), shallow);
const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow);
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow);
const setLastCanvasClickPosition = useStore((state) => state.setLastCanvasClickPosition, shallow);
@ -95,12 +103,16 @@ export const Container = React.memo(
if (canvasWidth !== undefined) {
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
if (id === 'canvas') return canvasWidth;
return canvasWidth - 2;
if (componentType === 'Container' || componentType === 'Form') {
return (
canvasWidth - (2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING)
);
}
return canvasWidth - 2; // Need to update this 2 to correct value for other subcontainers
}
return realCanvasRef?.current?.offsetWidth;
}
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
useEffect(() => {
useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS);
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -143,7 +155,7 @@ export const Container = React.memo(
}}
style={{
height: id === 'canvas' ? `${canvasHeight}` : '100%',
backgroundSize: `${gridWidth}px ${10}px`,
backgroundSize: `${gridWidth}px ${GRID_HEIGHT}px`,
backgroundColor:
currentMode === 'view'
? computeViewerBackgroundColor(darkMode, canvasBgColor)

View file

@ -1,17 +1,6 @@
.target, .nested-target {
position: absolute;
/* width: 100px;
height: 100px; */
/* top: 150px;
left: 100px; */
/* line-height: 100px; */
/* text-align: center; */
/* background: #ee8; */
/* color: #333; */
/* font-weight: bold; */
box-sizing: border-box;
/* transition: transform 0.1s; */
/* z-index: 3001; */
}
.target.hovered{
@ -76,43 +65,6 @@
background: #8DA4EF !important;
}
/* Hides all the control lines*/
/* .moveable-line {
color: transparent !important;
--moveable-color: transparent !important;
}
.moveable-control {
visibility: hidden;
}
.target {
outline: 1px solid #4af;
} */
.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover {
outline: 1px solid #4af;
z-index: 4 !important;
}
.main-editor-canvas .widget-target:has(.nested-target:hover):hover {
outline: 0px solid #4af;
}
.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
outline: 1px solid #4af;
z-index: 4 !important;
}
.active-target, .resizing-target {
outline: 1px solid #4af !important;
/* z-index: 1000000 !important; */
}
.moveable-control-box:not([data-able-groupable]) .moveable-control-box:not(:hover) {
opacity: 0;
}
@ -141,10 +93,6 @@
height: 0px !important;
}
.resizing-target * {
opacity: 0;
}
.moveable-control {
width: 8px !important;
@ -210,4 +158,19 @@
.moveable-guideline-group {
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;
}
.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

@ -19,6 +19,9 @@ import {
adjustWidth,
hideGridLines,
showGridLines,
handleActivateTargets,
handleDeactivateTargets,
handleActivateNonDraggingComponents,
} from './gridUtils';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
@ -56,7 +59,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow);
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
const draggingComponentId = useGridStore((state) => state.draggingComponentId, shallow);
const draggingComponentId = useStore((state) => state.draggingComponentId, shallow);
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
const [dragParentId, setDragParentId] = useState(null);
const [elementGuidelines, setElementGuidelines] = useState([]);
@ -580,7 +583,7 @@ export default function Grid({ gridWidth, currentLayout }) {
} else {
document.getElementById('real-canvas').classList.add('show-grid');
}
handleActivateTargets(currentWidget.component?.parent);
const currentWidth = currentWidget.width * _gridWidth;
const diffWidth = e.width - currentWidth;
const diffHeight = e.height - currentWidget.height;
@ -632,6 +635,7 @@ export default function Grid({ gridWidth, currentLayout }) {
if (!isComponentVisible(e.target.id)) {
return false;
}
handleActivateNonDraggingComponents();
useGridStore.getState().actions.setResizingComponentId(e.target.id);
e.setMin([gridWidth, GRID_HEIGHT]);
}}
@ -641,8 +645,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const currentWidget = boxList.find(({ id }) => {
return id === e.target.id;
});
document.getElementById('real-canvas')?.classList.remove('show-grid');
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid');
hideGridLines();
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth;
const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT;
@ -696,17 +699,19 @@ export default function Grid({ gridWidth, currentLayout }) {
} catch (error) {
console.error('ResizeEnd error ->', error);
}
handleDeactivateTargets();
setDragParentId(null);
toggleCanvasUpdater();
}}
onResizeGroupStart={({ events }) => {
showGridLines();
handleActivateNonDraggingComponents();
}}
onResizeGroup={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
const parentWidth = parentElm?.clientWidth;
const parentHeight = parentElm?.clientHeight;
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight);
events.forEach((ev) => {
ev.target.style.width = `${ev.width}px`;
@ -775,6 +780,7 @@ export default function Grid({ gridWidth, currentLayout }) {
} catch (error) {
console.error('Error resizing group', error);
}
handleDeactivateTargets();
toggleCanvasUpdater();
}}
checkInput
@ -785,6 +791,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
const box = boxList.find((box) => box.id === e.target.id);
// Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS
if (SUBCONTAINER_WIDGETS.includes(box?.component?.component) && e.inputEvent.shiftKey) {
@ -818,7 +825,6 @@ export default function Grid({ gridWidth, currentLayout }) {
container.contains(e.inputEvent.target)
);
}
if (['RangeSlider', 'BoundedBox'].includes(box?.component?.component) || isDragOnInnerElement) {
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
const isHandle = targetElems.find((ele) => ele.classList.contains('handle-content'));
@ -826,11 +832,13 @@ export default function Grid({ gridWidth, currentLayout }) {
return false;
}
}
handleActivateNonDraggingComponents();
}}
onDragEnd={(e) => {
handleDeactivateTargets();
try {
if (isDraggingRef.current) {
useGridStore.getState().actions.setDraggingComponentId(null);
useStore.getState().setDraggingComponentId(null);
isDraggingRef.current = false;
}
prevDragParentId.current = null;
@ -884,7 +892,7 @@ export default function Grid({ gridWidth, currentLayout }) {
onDrag={(e) => {
// Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again
if (!isDraggingRef.current) {
useGridStore.getState().actions.setDraggingComponentId(e.target.id);
useStore.getState().setDraggingComponentId(e.target.id);
showGridLines();
isDraggingRef.current = true;
}
@ -962,6 +970,7 @@ export default function Grid({ gridWidth, currentLayout }) {
setDragParentId(newParentId === 'canvas' ? null : newParentId);
newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
prevDragParentId.current = newParentId;
handleActivateTargets(newParentId);
}
}
// Postion ghost element exactly as same at dragged element
@ -988,14 +997,17 @@ export default function Grid({ gridWidth, currentLayout }) {
ev.target.style.transform = `translate(${left}px, ${top}px)`;
});
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
updateNewPosition(events);
}}
onDragGroupStart={({ events }) => {
showGridLines();
setIsGroupDragging(true);
handleActivateNonDraggingComponents();
}}
onDragGroupEnd={(e) => {
handleDragGroupEnd(e);
handleDeactivateTargets();
toggleCanvasUpdater();
}}
onClickGroup={(e) => {

View file

@ -1,7 +1,7 @@
import { useGridStore } from '@/_stores/gridStore';
import { isEmpty } from 'lodash';
import useStore from '@/AppBuilder/_stores/store';
import { getTabId, getSubContainerIdWithSlots } from '../appCanvasUtils';
export function correctBounds(layout, bounds) {
layout = scaleLayouts(layout);
const collidesWith = [];
@ -414,3 +414,77 @@ export function hideGridLines() {
document.getElementById('real-canvas')?.classList.remove('show-grid');
document.getElementById('real-canvas')?.classList.add('hide-grid');
}
// Track previously active elements for efficient cleanup
let previousActiveWidgets = null;
let previousActiveCanvas = null;
export const handleActivateNonDraggingComponents = () => {
// Only add non-dragging class to visible components in viewport
document.querySelectorAll('.moveable-box:not(.active-target)').forEach((component) => {
// Check if element is visible in viewport
const rect = component.getBoundingClientRect();
const isVisible =
rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
if (isVisible) {
component.classList.add('non-dragging-component');
}
});
};
export const handleActivateTargets = (parentId) => {
const WIDGETS_WITH_CANVAS_OUTLINE = ['Container', 'Modal', 'Form', 'Listview', 'Kanban'];
const newParentType = document.getElementById('canvas-' + parentId)?.getAttribute('component-type');
let _parentId = parentId;
if (newParentType === 'Tabs') {
_parentId = getTabId(parentId);
} else if (WIDGETS_WITH_CANVAS_OUTLINE.includes(newParentType)) {
_parentId = getSubContainerIdWithSlots(parentId);
}
// Clean up previous active elements
if (previousActiveWidgets) {
previousActiveWidgets.classList.remove('dragging-component-canvas');
previousActiveWidgets = null;
}
if (previousActiveCanvas) {
previousActiveCanvas.classList.remove('dragging-component-canvas');
previousActiveCanvas = null;
}
const parentComponent = document.getElementById(_parentId);
if (!parentComponent) return;
if (WIDGETS_WITH_CANVAS_OUTLINE?.includes(newParentType)) {
// If it's multiple canvas in single widget, highlight the specific canvas
const canvasElm = document.getElementById('canvas-' + parentId);
if (canvasElm) {
canvasElm.classList.add('dragging-component-canvas');
previousActiveCanvas = canvasElm;
}
} else {
// Otherwise highlight the component box
parentComponent.classList.remove('non-dragging-component');
parentComponent.classList.add('dragging-component-canvas');
previousActiveWidgets = parentComponent;
}
};
export const handleDeactivateTargets = () => {
if (previousActiveWidgets) {
previousActiveWidgets.classList.remove('dragging-component-canvas');
previousActiveWidgets = null;
}
if (previousActiveCanvas) {
previousActiveCanvas.classList.remove('dragging-component-canvas');
previousActiveCanvas = null;
}
document.querySelectorAll('.non-dragging-component').forEach((component) => {
component.classList.remove('non-dragging-component');
});
};

View file

@ -6,7 +6,7 @@ import { OverlayTrigger } from 'react-bootstrap';
import { renderTooltip } from '@/_helpers/appUtils';
import { useTranslation } from 'react-i18next';
import ErrorBoundary from '@/_ui/ErrorBoundary';
import { BOX_PADDING } from './appCanvasConstants';
const shouldAddBoxShadowAndVisibility = [
'Table',
'TextInput',
@ -164,7 +164,7 @@ const RenderWidget = ({
<div
style={{
height: '100%',
padding: resolvedStyles?.padding == 'none' ? '0px' : '2px', //chart and image has a padding property other than container padding
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

View file

@ -27,7 +27,7 @@ const WidgetWrapper = memo(
);
const layoutData = useStore((state) => state.getComponentDefinition(id)?.layouts?.[currentLayout], shallow);
const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow);
const isDragging = useGridStore((state) => state.draggingComponentId === id);
const isDragging = useStore((state) => state.draggingComponentId === id);
const isResizing = useGridStore((state) => state.resizingComponentId === id);
const componentType = useStore((state) => state.getComponentDefinition(id)?.component?.component, shallow);
const setHoveredComponentForGrid = useStore((state) => state.setHoveredComponentForGrid, shallow);
@ -52,7 +52,9 @@ const WidgetWrapper = memo(
height: visibility === false ? '10px' : `${height}px`,
transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`,
WebkitFontSmoothing: 'antialiased',
border: visibility === false ? `1px solid var(--border-default)` : 'none',
};
if (!componentType) return null;
return (
<>
@ -67,8 +69,8 @@ const WidgetWrapper = memo(
data-id={`${id}`}
id={id}
widgetid={id}
component-type={componentType}
style={{
// transform: `translate(332px, -134px)`,
// zIndex: mode === 'view' && widget.component.component == 'Datepicker' ? 2 : null,
...styles,
}}
@ -84,12 +86,12 @@ const WidgetWrapper = memo(
{mode == 'edit' && (
<ConfigHandle
id={id}
position={layoutData.top < 15 ? 'bottom' : 'top'}
widgetTop={layoutData.top}
widgetHeight={layoutData.height}
showHandle={isWidgetActive}
componentType={componentType}
visibility={visibility}
subContainerIndex={subContainerIndex}
/>
)}
<RenderWidget

View file

@ -1,5 +1,7 @@
export const NO_OF_GRIDS = 43;
export const GRID_HEIGHT = 10;
export const CANVAS_WIDTHS = Object.freeze({
deviceWindowWidth: 450,
leftSideBarWidth: 48,
@ -15,3 +17,9 @@ export const APP_HEADER_HEIGHT = 47;
export const LEFT_SIDEBAR_WIDTH = 348; // exclusive of border
export const SUBCONTAINER_WIDGETS = ['Container', 'Tabs', 'Listview', 'Kanban', 'Form'];
export const CONTAINER_FORM_CANVAS_PADDING = 7;
export const SUBCONTAINER_CANVAS_BORDER_WIDTH = 1;
export const BOX_PADDING = 2;

View file

@ -206,6 +206,7 @@ export const getAllChildComponents = (allComponents, parentId) => {
allComponents[parentId]?.component?.component === 'Calendar' ||
allComponents[parentId]?.component?.component === 'Kanban' ||
allComponents[parentId]?.component?.component === 'Container' ||
allComponents[parentId]?.component?.component === 'Form' ||
allComponents[parentId]?.component?.component === 'ModalV2';
if (componentParentId && isParentTabORCalendar) {
@ -327,6 +328,7 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI
parentComponent.component.component === 'Tabs' ||
parentComponent.component.component === 'Calendar' ||
parentComponent.component.component === 'Container' ||
parentComponent.component.component === 'Form' ||
parentComponent.component.component === 'ModalV2'
);
}
@ -665,11 +667,14 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
return canvasBgColor;
};
export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => {
export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName }) => {
const { tab } = child;
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
else if (parentComponent === 'Container' || parentComponent === 'ModalV2') {
else if (
slotName &&
(parentComponent === 'Form' || parentComponent === 'Container' || parentComponent === 'ModalV2')
) {
return `${parentId}-${slotName}`;
}
return parentId;
@ -685,3 +690,19 @@ export const getParentWidgetFromId = (parentType, parentId) => {
}
return parentType;
};
export const getTabId = (parentId) => {
return parentId.split('-').slice(0, -1).join('-');
};
export const getSubContainerIdWithSlots = (parentId) => {
let cleanParentId = parentId;
if (parentId) {
if (parentId.includes('header')) {
cleanParentId = parentId.replace('-header', '');
} else if (parentId.includes('footer')) {
cleanParentId = parentId.replace('-footer', '');
}
}
return cleanParentId;
};

View file

@ -3,15 +3,18 @@
}
.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover {
z-index: 4 !important;
}
.main-editor-canvas .widget-target:has(.nested-target:hover):hover {
outline: 0px solid #4af;
}
.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
outline: 1px solid #4af;
z-index: 4 !important;
}
.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover {
// outline: 1px solid #4af;
z-index: 4 !important;
}
// .main-editor-canvas .widget-target:hover {
// outline: 1px solid #4af;
// }

View file

@ -56,8 +56,8 @@ const TJDBCodeEditor = (props) => {
const handleOnChange = (value) => {
if (value === '') {
setErrorState(true);
setError('JSON cannot be empty');
setErrorState(false);
setError(null);
setCurrentValue(value);
return;
}
@ -150,7 +150,7 @@ const TJDBCodeEditor = (props) => {
className="cm-codehinter position-relative"
style={{
width: '100%',
height: isOpen ? '350px' : 'auto',
height: isOpen ? '350p' : 'auto',
}}
>
<div className={`cm-codehinter ${darkMode && 'cm-codehinter-dark-themed'}`}>
@ -178,7 +178,7 @@ const TJDBCodeEditor = (props) => {
<CodeMirror
value={currentValue}
placeholder={placeholder}
height={isOpen ? '350px' : '32px'}
height={isOpen ? '32px' : '32px'}
maxHeight={'350px'}
width="100%"
theme={theme}

View file

@ -7,7 +7,7 @@ import { getWorkspaceId, decodeEntities } from '@/_helpers/utils';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { staticDataSources as staticDatasources } from '../constants';
import { defaultSources, staticDataSources as staticDatasources } from '../constants';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import Search from '@/_ui/Icon/solidIcons/Search';
import { Tooltip } from 'react-tooltip';
@ -135,7 +135,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
<div>
<DataSourceIcon source={source} height={16} />{' '}
<span data-cy={`ds-${source.name.toLowerCase()}`} className="ms-1 small" style={{ fontSize: '13px' }}>
{source.name}
{defaultSources[cleanWord(source.name)].name}
</span>
</div>
),
@ -178,6 +178,10 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
}
};
function cleanWord(word) {
return word.replace(/default/g, '');
}
return (
<div>
<Select

View file

@ -19,16 +19,34 @@ export const Form = ({
allComponents,
pages,
}) => {
const properties = Object.keys(componentMeta.properties);
const tempComponentMeta = deepClone(componentMeta);
let properties = [];
let additionalActions = [];
let dataProperties = [];
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
const tempComponentMeta = deepClone(componentMeta);
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]) => {
if (component.component.parent === id && component?.component?.component === 'Button') {
newOptions.push({ name: component.component.name, value: componentId });
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 });
}
});
@ -48,7 +66,8 @@ export const Form = ({
allComponents,
validations,
darkMode,
pages
pages,
additionalActions
);
return <Accordion items={accordionItems} />;
@ -68,7 +87,8 @@ export const baseComponentProperties = (
allComponents,
validations,
darkMode,
pages
pages,
additionalActions
) => {
let items = [];
if (properties.length > 0) {
@ -90,6 +110,24 @@ export const baseComponentProperties = (
});
}
items.push({
title: 'Additional actions',
isOpen: true,
children: additionalActions?.map((property) =>
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
property,
'properties',
currentState,
allComponents,
darkMode
)
),
});
if (events.length > 0) {
items.push({
title: `${i18next.t('widget.common.events', 'Events')}`,

View file

@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
height: 480,
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 10,
left: 1,
height: 40,
},
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 12,
left: 32,
height: 36,
},
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'colorSwatches',
displayName: 'Background color',
@ -274,26 +358,13 @@ export const formConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
exposedVariables: {
data: {},
isValid: true,
isVisible: true,
isDisabled: false,
isLoading: false,
},
actions: [
{
@ -304,6 +375,21 @@ export const formConfig = {
handle: 'resetForm',
displayName: 'Reset Form',
},
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }],
},
{
handle: 'setDisable',
displayName: 'Set Disable',
params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set Loading',
params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
@ -317,15 +403,18 @@ export const formConfig = {
value:
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
buttonToSubmit: { value: '{{"none"}}' },
showHeader: { value: '{{false}}' },
showFooter: { value: '{{false}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -13,6 +13,7 @@ export const listviewConfig = {
top: 15,
left: 3,
height: 100,
width: 7,
},
properties: ['source'],
accessorKey: 'imageURL',

View file

@ -13,6 +13,7 @@ export const tabsConfig = {
top: 60,
left: 17,
height: 100,
width: 7,
},
tab: 0,
properties: ['source'],

View file

@ -1,99 +0,0 @@
import React, { useMemo } from 'react';
import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container';
import Spinner from '@/_ui/Spinner';
import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
export const Container = ({
id,
properties,
styles,
darkMode,
height,
width,
setExposedVariables,
setExposedVariable,
}) => {
const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles;
const { isDisabled, isVisible, isLoading } = useExposeState(
properties.loadingState,
properties.visibility,
properties.disabledState,
setExposedVariables,
setExposedVariable
);
const contentBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor,
};
}, [styles.backgroundColor, darkMode]);
const headerBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode
? '#232E3C'
: styles.headerBackgroundColor,
};
}, [styles.headerBackgroundColor, darkMode]);
const computedStyles = {
backgroundColor: contentBgColor.backgroundColor,
borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
border: `1px solid ${borderColor}`,
height,
display: isVisible ? 'flex' : 'none',
flexDirection: 'column',
position: 'relative',
boxShadow,
};
const computedHeaderStyles = {
...headerBgColor,
height: `${headerHeight}px`,
flexShrink: 0,
flexGrow: 0,
borderBottom: `1px solid var(--border-weak)`,
};
const computedContentStyles = {
...contentBgColor,
flex: 1,
overflow: 'auto',
};
return (
<div
className={`jet-container widget-type-container ${properties.loadingState && 'jet-container-loading'}`}
id={id}
data-disabled={isDisabled}
style={computedStyles}
>
{properties.showHeader && (
<ContainerComponent
id={`${id}-header`}
styles={computedHeaderStyles}
canvasHeight={headerHeight / 10}
canvasWidth={width}
allowContainerSelect={true}
darkMode={darkMode}
/>
)}
{isLoading ? (
<div className="h-100 d-flex align-items-center">
<Spinner />
</div>
) : (
<ContainerComponent
id={id}
styles={computedContentStyles}
canvasHeight={height}
canvasWidth={width}
darkMode={darkMode}
/>
)}
</div>
);
};

View file

@ -0,0 +1,119 @@
import React, { useMemo } from 'react';
import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container';
import Spinner from '@/_ui/Spinner';
import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
import { shallow } from 'zustand/shallow';
import {
CONTAINER_FORM_CANVAS_PADDING,
SUBCONTAINER_CANVAS_BORDER_WIDTH,
} from '@/AppBuilder/AppCanvas/appCanvasConstants';
import useStore from '@/AppBuilder/_stores/store';
import './container.scss';
export const Container = ({
id,
properties,
styles,
darkMode,
height,
width,
setExposedVariables,
setExposedVariable,
}) => {
const { isDisabled, isVisible, isLoading } = useExposeState(
properties.loadingState,
properties.visibility,
properties.disabledState,
setExposedVariables,
setExposedVariable
);
const isWidgetInContainerDragging = useStore(
(state) => state.containerChildrenMapping?.[id]?.includes(state?.draggingComponentId),
shallow
);
const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles;
const contentBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor,
};
}, [styles.backgroundColor, darkMode]);
const headerBgColor = useMemo(() => {
return {
backgroundColor:
['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode
? '#232E3C'
: styles.headerBackgroundColor,
};
}, [styles.headerBackgroundColor, darkMode]);
const computedStyles = {
backgroundColor: contentBgColor.backgroundColor,
borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`,
height,
display: isVisible ? 'flex' : 'none',
flexDirection: 'column',
position: 'relative',
boxShadow,
};
const containerHeaderStyles = {
flexShrink: 0,
padding: `${CONTAINER_FORM_CANVAS_PADDING}px ${CONTAINER_FORM_CANVAS_PADDING}px 3px ${CONTAINER_FORM_CANVAS_PADDING}px`,
...headerBgColor,
};
const containerContentStyles = {
overflow: 'hidden auto',
display: 'flex',
height: '100%',
padding: `${CONTAINER_FORM_CANVAS_PADDING}px`,
};
return (
<div
className={`jet-container ${isLoading ? 'jet-container-loading' : ''}`}
id={id}
data-disabled={isDisabled}
style={computedStyles}
>
{isLoading ? (
<Spinner />
) : (
<>
{properties.showHeader && (
<div style={containerHeaderStyles} className="wj-container-header">
<ContainerComponent
id={`${id}-header`}
styles={{ ...headerBgColor, height: `${headerHeight}px` }}
canvasHeight={headerHeight / 10}
canvasWidth={width}
allowContainerSelect={true}
darkMode={darkMode}
componentType="Container"
/>
</div>
)}
<div style={containerContentStyles}>
<ContainerComponent
id={id}
styles={{
...contentBgColor,
// Prevent the scroll when dragging a widget inside the container or moving out of the container
overflow: isWidgetInContainerDragging ? 'hidden' : 'hidden auto',
}}
canvasHeight={height}
canvasWidth={width}
darkMode={darkMode}
componentType="Container"
/>
</div>
</>
)}
</div>
);
};

View file

@ -0,0 +1,13 @@
.wj-container-header {
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: -7px;
right: -7px;
height: 1px;
background-color: var(--border-weak);
}
}

View file

@ -1,17 +1,25 @@
import React, { useRef, useState, useEffect, useMemo } from 'react';
import React, { useRef, useState, useEffect } from 'react';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import _, { debounce, omit } from 'lodash';
import { Box } from '@/Editor/Box';
import { generateUIComponents } from './FormUtils';
import { useMounted } from '@/_hooks/use-mount';
import { onComponentClick, removeFunctionObjects } from '@/_helpers/appUtils';
import { useAppInfo } from '@/_stores/appDataStore';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import RenderSchema from './RenderSchema';
import useStore from '@/AppBuilder/_stores/store';
import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables';
import { shallow } from 'zustand/shallow';
import {
CONTAINER_FORM_CANVAS_PADDING,
SUBCONTAINER_CANVAS_BORDER_WIDTH,
} from '@/AppBuilder/AppCanvas/appCanvasConstants';
import './form.scss';
const getCanvasHeight = (height) => {
const parsedHeight = height.includes('px') ? parseInt(height, 10) : height;
return Math.ceil(parsedHeight);
};
export const Form = function Form(props) {
const {
@ -29,27 +37,71 @@ export const Form = function Form(props) {
dataCy,
} = props;
const childComponents = useStore((state) => state.getChildComponents(id), shallow);
const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles;
const { buttonToSubmit, loadingState, advanced, JSONSchema } = properties;
const {
borderRadius,
borderColor,
boxShadow,
headerHeight,
footerHeight,
footerBackgroundColor,
headerBackgroundColor,
} = styles;
const {
buttonToSubmit,
loadingState,
advanced,
JSONSchema,
showHeader = false,
showFooter = false,
visibility,
disabledState,
} = properties;
const { isDisabled, isVisible, isLoading } = useExposeState(
properties.loadingState,
properties.visibility,
properties.disabledState,
setExposedVariables,
setExposedVariable
);
const backgroundColor =
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor;
const computedStyles = {
backgroundColor,
borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
border: `1px solid ${borderColor}`,
border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`,
height,
display: visibility ? 'flex' : 'none',
display: isVisible ? 'flex' : 'none',
position: 'relative',
overflow: 'hidden auto',
boxShadow,
flexDirection: 'column',
};
const childIdNameMap = useMemo(() => {
return Object.keys(childComponents).reduce((acc, id) => {
const component = childComponents[id]?.component?.component;
return { ...acc, [id]: component?.name };
}, {});
}, [childComponents]);
const formHeader = {
flexShrink: 0,
paddingBottom: '3px',
paddingTop: '7px',
paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
backgroundColor:
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
};
const formContent = {
overflow: 'hidden auto',
display: 'flex',
height: '100%',
paddingTop: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingBottom: showFooter ? '3px' : '7px',
paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
};
const formFooter = {
flexShrink: 0,
padding: `${CONTAINER_FORM_CANVAS_PADDING}px`,
backgroundColor:
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
};
const parentRef = useRef(null);
const childDataRef = useRef({});
@ -58,6 +110,8 @@ export const Form = function Form(props) {
const [isValid, setValidation] = useState(true);
const [uiComponents, setUIComponents] = useState([]);
const mounted = useMounted();
const canvasHeaderHeight = getCanvasHeight(headerHeight) / 10;
const canvasFooterHeight = getCanvasHeight(footerHeight) / 10;
useEffect(() => {
const exposedVariables = {
@ -155,7 +209,7 @@ export const Form = function Form(props) {
};
setExposedVariables(exposedVariables);
setValidation(childValidation);
}, [childrenData, advanced, JSON.stringify(childIdNameMap)]);
}, [childrenData, advanced]);
useEffect(() => {
document.addEventListener('submitForm', handleFormSubmission);
@ -245,105 +299,113 @@ export const Form = function Form(props) {
if (e.target.className === 'real-canvas') onComponentClick(id, component);
}} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked
>
{loadingState ? (
<div className="p-2" style={{ margin: '0px auto' }}>
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>
</div>
) : (
<fieldset disabled={disabledState} style={{ width: '100%' }}>
{!advanced && (
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: '100%' }}>
<SubContainer
id={id}
canvasHeight={computedStyles.height}
canvasWidth={width}
onOptionChange={onOptionChange}
onOptionsChange={onOptionsChange}
styles={{ backgroundColor: computedStyles.backgroundColor }}
darkMode={darkMode}
/>
{/* <SubContainer
parentComponent={component}
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
removeComponent={removeComponent}
onOptionChange={function ({ component, optionName, value, componentId }) {
if (componentId) {
onOptionChange({ component, optionName, value, componentId });
}
}}
currentPageId={props.currentPageId}
{...props}
{...containerProps}
height={'100%'} // This height is required since Subcontainer has a issue if height is provided, it stores it in the ref and never updates that ref
/>
<SubCustomDragLayer
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
currentLayout={currentLayout}
/> */}
</div>
{showHeader && (
<div style={formHeader} className="wj-form-header">
<SubContainer
id={`${id}-header`}
canvasHeight={canvasHeaderHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
backgroundColor: 'transparent',
height: headerHeight,
}}
componentType="Form"
/>
{isDisabled && (
<div
id={`${id}-header-disabled`}
className="tj-form-disabled-overlay"
style={{ height: headerHeight || '100%' }}
onClick={() => {}}
onDrop={(e) => e.stopPropagation()}
/>
)}
{advanced &&
uiComponents?.map((item, index) => {
return (
<div
//check to avoid labels for these widgets as label is already present for them
className={
![
'Checkbox',
'StarRating',
'Multiselect',
'DropDown',
'RadioButton',
'ToggleSwitch',
'ToggleSwitchV2',
].includes(uiComponents?.[index + 1]?.component)
? `json-form-wrapper json-form-wrapper-disabled`
: `json-form-wrapper json-form-wrapper-disabled form-label-restricted`
}
key={index}
>
<div style={{ position: 'relative' }} className={`form-ele form-${id}-${index}`}>
<RenderSchema
component={item}
parent={id}
id={index}
darkMode={darkMode}
onOptionChange={onComponentOptionChangedForSubcontainer}
onOptionsChange={onComponentOptionsChangedForSubcontainer}
/>
</div>
)}
<div className="jet-form-body" style={formContent}>
{isLoading ? (
<div className="p-2 tw-flex tw-items-center tw-justify-center" style={{ margin: '0px auto' }}>
<div className="spinner-border" role="status"></div>
</div>
) : (
<fieldset disabled={isDisabled} style={{ width: '100%' }}>
{!advanced && (
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: '100%' }}>
<SubContainer
id={id}
canvasHeight={computedStyles.height}
canvasWidth={width}
onOptionChange={onOptionChange}
onOptionsChange={onOptionsChange}
styles={{ backgroundColor: computedStyles.backgroundColor }}
darkMode={darkMode}
componentType="Form"
/>
</div>
)}
{advanced &&
uiComponents?.map((item, index) => {
return (
<div
//check to avoid labels for these widgets as label is already present for them
className={
![
'Checkbox',
'StarRating',
'Multiselect',
'DropDown',
'RadioButton',
'ToggleSwitch',
'ToggleSwitchV2',
].includes(uiComponents?.[index + 1]?.component)
? `json-form-wrapper json-form-wrapper-disabled`
: `json-form-wrapper json-form-wrapper-disabled form-label-restricted`
}
key={index}
>
<div style={{ position: 'relative' }} className={`form-ele form-${id}-${index}`}>
<RenderSchema
component={item}
parent={id}
id={index}
darkMode={darkMode}
onOptionChange={onComponentOptionChangedForSubcontainer}
onOptionsChange={onComponentOptionsChangedForSubcontainer}
/>
</div>
</div>
{/* <Box
{...props}
component={item}
id={index}
width={width}
height={item.defaultSize.height}
mode={mode}
inCanvas={true}
paramUpdated={paramUpdated}
onEvent={onEvent}
onComponentClick={onComponentClick}
darkMode={darkMode}
removeComponent={removeComponent}
// canvasWidth={width}
// readOnly={readOnly}
// customResolvables={customResolvables}
parentId={id}
getContainerProps={getContainerProps}
onOptionChanged={onComponentOptionChangedForSubcontainer}
onOptionsChanged={onComponentOptionsChanged}
isFromSubContainer={true}
/> */}
</div>
);
})}
</fieldset>
);
})}
</fieldset>
)}
</div>
{showFooter && (
<div className="jet-form-footer wj-form-footer" style={formFooter}>
<SubContainer
id={`${id}-footer`}
canvasHeight={canvasFooterHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
margin: 0,
backgroundColor: 'transparent',
height: footerHeight,
}}
componentType="Form"
/>
{isDisabled && (
<div
id={`${id}-footer-disabled`}
className="tj-form-disabled-overlay"
style={{ height: footerHeight || '100%' }}
onClick={() => {}}
onDrop={(e) => e.stopPropagation()}
/>
)}
</div>
)}
</form>
);

View file

@ -0,0 +1,40 @@
.wj-form-header {
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0;
left: -7px;
right: -7px;
height: 1px;
background-color: var(--border-weak);
}
}
.wj-form-footer {
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
left: -7px;
right: -7px;
height: 1px;
background-color: var(--border-weak);
}
}
.tj-form-disabled-overlay {
/* TODO: Make slot overlays common */
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 1;
margin: 0;
box-sizing: content-box;
padding: 4px 0;
}

View file

@ -412,6 +412,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) {
width: `${(Number(cardWidth) || 300) + 48}px`,
}}
kanbanProps={kanbanProps}
componentType="Kanban"
>
{items[columnId] && (
<SortableContext items={items[columnId]} strategy={verticalListSortingStrategy}>

View file

@ -12,11 +12,8 @@ import { shallow } from 'zustand/shallow';
export const Listview = function Listview({
id,
component,
width,
height,
containerProps,
removeComponent,
properties,
styles,
fireEvent,
@ -270,38 +267,8 @@ export const Listview = function Listview({
columns={positiveColumns}
listViewMode={mode}
darkMode={darkMode}
componentType="Listview"
/>
{/* <SubContainer
columns={positiveColumns}
listmode={mode}
parentComponent={component}
containerCanvasWidth={width}
parent={`${id}`}
parentName={component.name}
{...containerProps}
readOnly={index !== 0}
customResolvables={{ listItem }}
parentRef={parentRef}
removeComponent={removeComponent}
listViewItemOptions={{ index }}
exposedVariables={childrenData[index]}
onOptionChange={function ({ component, optionName, value, componentId }) {
setChildrenData((prevData) => {
const changedData = { [component.name]: { [optionName]: value } };
const existingDataAtIndex = prevData[index] ?? {};
const newDataAtIndex = {
...prevData[index],
[component.name]: {
...existingDataAtIndex[component.name],
...changedData[component.name],
id: componentId,
},
};
const newChildrenData = { ...prevData, [index]: newDataAtIndex };
return { ...prevData, ...newChildrenData };
});
}}
/> */}
</div>
))}
</div>

View file

@ -126,6 +126,7 @@ export const Tabs = function Tabs({
allowContainerSelect={true}
styles={{ backgroundColor: bgColor }}
darkMode={darkMode}
componentType="Tabs"
/>
</div>
);

View file

@ -59,7 +59,7 @@ import { BoundedBox } from '@/Editor/Components/BoundedBox/BoundedBox';
import { isPDFSupported } from '@/_helpers/appUtils';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { useEditorStore } from '@/_stores/editorStore';
import { Container } from '@/AppBuilder/Widgets/Container';
import { Container } from '@/AppBuilder/Widgets/Container/Container';
import { Listview } from '@/AppBuilder/Widgets/Listview';
import { Tabs } from '@/AppBuilder/Widgets/Tabs';
import { Kanban } from '@/AppBuilder/Widgets/Kanban/Kanban';

View file

@ -7,6 +7,7 @@ import { getWorkspaceId } from '@/_helpers/utils';
import { navigate } from '@/AppBuilder/_utils/misc';
import queryString from 'query-string';
import { replaceEntityReferencesWithIds, baseTheme } from '../utils';
import _ from 'lodash';
const initialState = {
app: {},
@ -126,10 +127,14 @@ export const createAppSlice = (set, get) => ({
setComponentNameIdMapping('canvas');
setQueryMapping('canvas');
const isLicenseValid =
!_.get(license, 'featureAccess.licenseStatus.isExpired', true) &&
_.get(license, 'featureAccess.licenseStatus.isLicenseValid', false);
const appId = get().app.appId;
const filteredQueryParams = queryParams.filter(([key, value]) => {
if (!value) return false;
if (key === 'env' && !license.isLicenseValid()) return false;
if (key === 'env' && isLicenseValid) return false;
return true;
});

View file

@ -136,7 +136,6 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
updateVersionNameAction: async (appId, versionId, versionName, onSuccess, onFailure) => {
try {
await appVersionService.save(appId, versionId, { name: versionName });
console.log('happening');
set((state) => {
if (state.selectedVersion && state.selectedVersion.id === versionId) {
@ -177,7 +176,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
appVersionsLazyLoaded: false,
selectedEnvironment: response.editorEnvironment,
appVersionEnvironment: response.appVersionEnvironment,
environments: response.environments,
environments: response?.environments?.length ? response.environments : get().environments,
};
if (state.selectedVersion?.id === versionId) {
@ -241,7 +240,6 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
useStore.getState()?.license?.featureAccess
),
};
console.log({ environment, get: get().appVersionEnvironment });
const versionIsAvailableInEnvironment = environment?.priority <= get().currentAppVersionEnvironment?.priority;
if (!versionIsAvailableInEnvironment) {
@ -252,7 +250,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
});
selectedVersion = response.editorVersion;
const appVersionEnvironment = get().environments.find(
(environment) => environment.id === selectedVersion.current_environment_id
(environment) => environment.id === selectedVersion.currentEnvironmentId
);
//TODO: need to check if this is needed

View file

@ -7,6 +7,7 @@ const initialState = {
triggerCanvasUpdater: false,
lastCanvasIdClick: '',
lastCanvasClickPosition: null,
draggingComponentId: null,
};
export const createGridSlice = (set, get) => ({
@ -21,6 +22,7 @@ export const createGridSlice = (set, get) => ({
debouncedToggleCanvasUpdater: debounce(() => {
get().toggleCanvasUpdater();
}, 200),
setDraggingComponentId: (id) => set(() => ({ draggingComponentId: id })),
moveComponentPosition: (direction) => {
const { setComponentLayout, currentLayout, getSelectedComponentsDefinition, debouncedToggleCanvasUpdater } = get();
let layouts = {};

View file

@ -236,8 +236,6 @@ export const TextInput = function TextInput({
value: properties.value,
isMandatory: isMandatory,
isLoading: loading,
isVisible: visibility,
isDisabled: disable,
};
setExposedVariables(exposedVariables);
@ -245,6 +243,17 @@ export const TextInput = function TextInput({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
// Fix for "visibility is not defined" in production because there's a naming conflict in the code.
// The issue is in the exposedVariables object where we had both a function named visibility and a property isVisible that depends on the state variable with the same name.
const exposedVariables = {
isVisible: visibility,
isDisabled: disable,
};
setExposedVariables(exposedVariables);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const setInputValue = (value) => {
setValue(value);
setExposedVariable('value', value);

View file

@ -10,7 +10,7 @@ import { resolveWidgetFieldValue } from '@/_helpers/utils';
import ErrorBoundary from './ErrorBoundary';
import { useEditorStore } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
import { useNoOfGrid, useGridStore } from '@/_stores/gridStore';
import { useGridStore } from '@/_stores/gridStore';
import WidgetBox from './WidgetBox';
import * as Sentry from '@sentry/react';
import { findHighestLevelofSelection } from './DragContainer';
@ -61,7 +61,7 @@ const DraggableBox = React.memo(
}) => {
const isResizing = useGridStore((state) => state.resizingComponentId === id);
const [canDrag, setCanDrag] = useState(true);
const noOfGrid = useNoOfGrid();
const noOfGrid = 43;
const {
currentLayout,
setHoveredComponent,

View file

@ -7,7 +7,6 @@ import { getWorkspaceId, decodeEntities } from '@/_helpers/utils';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useDataSources, useGlobalDataSources, useSampleDataSource } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { staticDataSources as staticDatasources } from '../constants';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import Search from '@/_ui/Icon/solidIcons/Search';
import { Tooltip } from 'react-tooltip';
@ -16,7 +15,7 @@ import { canCreateDataSource } from '@/_helpers';
import './../queryManager.theme.scss';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, defaultDataSources }) {
function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, staticDataSources }) {
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const sampleDataSource = useSampleDataSource();
@ -33,11 +32,6 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
closePopup();
};
const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true';
const staticDataSources = workflowsEnabled
? staticDatasources
: staticDatasources.filter((ds) => ds?.kind !== 'workflows');
useEffect(() => {
const shouldAddSampleDataSource = !!sampleDataSource;
const allDataSources = [...dataSources, ...globalDataSources, shouldAddSampleDataSource && sampleDataSource].filter(
@ -148,7 +142,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
</div>
),
isDisabled: true,
options: defaultDataSources?.map((source) => ({
options: staticDataSources?.map((source) => ({
label: (
<div>
<DataSourceIcon source={source} height={16} />{' '}

View file

@ -23,7 +23,7 @@ import { useEditorStore } from '@/_stores/editorStore';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import { useGridStore, useResizingComponentId } from '@/_stores/gridStore';
import { useGridStore } from '@/_stores/gridStore';
import GhostWidget from './GhostWidget';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
@ -68,7 +68,7 @@ export const SubContainer = ({
shallow
);
const resizingComponentId = useResizingComponentId();
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
const noOfGrids = 43;
const { isGridActive } = useGridStore((state) => ({ isGridActive: state.activeGrid === parent }), shallow);

View file

@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
height: 480,
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 10,
left: 1,
height: 40,
},
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 12,
left: 32,
height: 36,
},
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'color',
displayName: 'Background color',
@ -274,26 +358,13 @@ export const formConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
exposedVariables: {
data: {},
isValid: true,
isVisible: true,
isDisabled: false,
isLoading: false,
},
actions: [
{
@ -304,6 +375,21 @@ export const formConfig = {
handle: 'resetForm',
displayName: 'Reset Form',
},
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }],
},
{
handle: 'setDisable',
displayName: 'Set Disable',
params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set Loading',
params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
@ -317,14 +403,18 @@ export const formConfig = {
value:
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
showHeader: { value: '{{false}}' },
showFooter: { value: '{{false}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -13,6 +13,7 @@ export const listviewConfig = {
top: 15,
left: 3,
height: 100,
width: 7,
},
properties: ['source'],
accessorKey: 'imageURL',

View file

@ -13,6 +13,7 @@ export const tabsConfig = {
top: 60,
left: 17,
height: 100,
width: 7,
},
tab: 0,
properties: ['source'],

View file

@ -129,6 +129,7 @@ export const DATA_SOURCE_TYPE = {
LOCAL: 'local',
GLOBAL: 'global',
STATIC: 'static',
DEFAULT: 'default',
};
export const SAMPLE_DB_KIND = {

View file

@ -7,7 +7,6 @@ const initialState = {
noOfGrid: 43,
draggedSubContainer: false,
resizingComponentId: null,
draggingComponentId: null,
dragTarget: null,
isGroupHandleHoverd: false,
idGroupDragged: false,
@ -20,11 +19,7 @@ export const useGridStore = create(
(set) => ({
...initialState,
actions: {
setActiveGrid: (gridId) => set({ activeGrid: gridId }),
setNoOfGrid: (noOfGrid) => set({ noOfGrid }),
setDraggedSubContainer: (draggedSubContainer) => set({ draggedSubContainer }),
setResizingComponentId: (id) => set({ resizingComponentId: id }),
setDraggingComponentId: (id) => set({ draggingComponentId: id }),
setDragTarget: (dragTarget) => set({ dragTarget }),
setIsGroupHandleHoverd: (isGroupHandleHoverd) => set({ isGroupHandleHoverd }),
setIdGroupDragged: (idGroupDragged) => set({ idGroupDragged }),
@ -46,21 +41,5 @@ useGridStore.subscribe(({ draggingComponentId }) => {
}
});
// useEditorStore.subscribe(({ hoveredComponent }) => {
// console.log('hoveredComponent--', hoveredComponent);
// if (hoveredComponent) {
// document.querySelector(`[data-hovered-control]`)?.removeAttribute('data-hovered-control');
// document.querySelector(`[target-id='${hoveredComponent}']`)?.setAttribute('data-hovered-control', true);
// } else if (document.querySelector(`[data-hovered-control]`)) {
// document.querySelector(`[data-hovered-control]`)?.removeAttribute('data-hovered-control');
// }
// });
export const useActiveGrid = () => useGridStore((state) => state.activeGrid, shallow);
export const useNoOfGrid = () => useGridStore((state) => state.noOfGrid, shallow);
export const useDraggedSubContainer = () => useGridStore((state) => state.draggedSubContainer, shallow);
export const useGridStoreActions = () => useGridStore((state) => state.actions, shallow);
export const useDragTarget = () => useGridStore((state) => state.dragTarget, shallow);
export const useResizingComponentId = () => useGridStore((state) => state.resizingComponentId, shallow);
export const useIsGroupHandleHoverd = () => useGridStore((state) => state.isGroupHandleHoverd, shallow);
export const useOpenModalWidgetId = () => useGridStore((state) => state.openModalWidgetId, shallow);

View file

@ -628,7 +628,6 @@ button {
.inspector {
padding: 0px !important;
.form-control-plaintext {
padding: 0;
color: var(--slate12);

View file

@ -13,7 +13,7 @@ function Label({ label, width, labelRef, color, defaultAlignment, direction, aut
justifyContent: direction == 'right' ? 'flex-end' : 'flex-start',
fontSize: '12px',
height: defaultAlignment === 'top' && '20px',
overflow: 'hidden',
overflow: auto ? 'visible' : 'hidden',
}}
>
<p

View file

@ -69,7 +69,7 @@ const useCEOnboardingStore = create(
createNewOnboardingApp: async () => {
const session = authenticationService.currentSessionValue;
const app = await appsService.createApp({ name: 'My App' });
const app = await appsService.createApp({ name: 'My App', type: 'front-end' });
const appId = app?.id;
utils.clearPageHistory();
const path = getSubpath()
@ -80,7 +80,7 @@ const useCEOnboardingStore = create(
setAccountCreated: (value) => set({ accountCreated: value }),
resumeSignupOnboarding: async (callBack = (resumeOnboardingSession = false) => { }) => {
resumeSignupOnboarding: async (callBack = (resumeOnboardingSession = false) => {}) => {
return callBack(false);
},
}))

View file

@ -26,4 +26,17 @@ export class AppVersionUpdateDto {
@IsOptional()
pageSettings: any;
// Workflow related fields
@IsOptional()
@IsString()
@IsUUID()
currentEnvironmentId: string;
@IsOptional()
definition: any;
@IsOptional()
@IsBoolean()
is_user_switched_version: boolean;
}

View file

@ -20,7 +20,7 @@ export class AppEnvironmentService implements IAppEnvironmentService {
async init(editingVersionId: string, organizationId: string): Promise<IAppEnvironmentResponse> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const editorVersion = await manager.findOne(AppVersion, {
select: ['id', 'name', 'appId'],
select: ['id', 'name', 'currentEnvironmentId', 'appId'],
where: { id: editingVersionId },
});
return await this.appEnvironmentUtilService.init(editorVersion, organizationId, false, manager);

View file

@ -119,7 +119,7 @@ export class AppEnvironmentUtilService implements IAppEnvironmentUtilService {
return {
name: result.appVersion_name,
id: result.appVersion_id,
currentEnvironmentId: result.appVersion_currentEnvironmentId,
currentEnvironmentId: result.appVersion_current_environment_id,
};
}

View file

@ -20,8 +20,4 @@ export class AppGitAbilityGuard extends AbilityGuard {
protected getSubjectType() {
return App;
}
protected forwardAbility(): boolean {
return true;
}
}

View file

@ -14,7 +14,13 @@ export class AppGitAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
return App;
}
protected defineAbilityFor(can: AbilityBuilder<AppGitAbility>['can'], UserAllPermissions: UserAllPermissions): void {
protected defineAbilityFor(
can: AbilityBuilder<AppGitAbility>['can'],
UserAllPermissions: UserAllPermissions,
extractedMetadata: { moduleName: string; features: string[] },
request?: any
): void {
const appId = request?.tj_resource_id;
const { superAdmin, isAdmin, userPermission } = UserAllPermissions;
const userAppGitPermissions = userPermission?.APP;
@ -35,11 +41,12 @@ export class AppGitAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
}
// READ-based features
if (isAllAppsViewable || userAppGitPermissions?.viewableAppsId?.length) {
if (
isAllAppsViewable ||
(userAppGitPermissions?.viewableAppsId?.length && appId && userAppGitPermissions?.viewableAppsId?.includes(appId))
) {
can(FEATURE_KEY.GIT_GET_APPS, App);
can(FEATURE_KEY.GIT_GET_APP, App, {
id: { $in: isAllAppsViewable ? undefined : userAppGitPermissions?.viewableAppsId },
});
can(FEATURE_KEY.GIT_GET_APP, App);
}
// CREATE-based features
@ -48,20 +55,21 @@ export class AppGitAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
}
// UPDATE-based features
if (isAllAppsEditable || userAppGitPermissions?.editableAppsId?.length) {
can(FEATURE_KEY.GIT_UPDATE_APP, App, {
id: { $in: isAllAppsEditable ? undefined : userAppGitPermissions?.editableAppsId },
});
can(FEATURE_KEY.GIT_SYNC_APP, App, {
id: { $in: isAllAppsEditable ? undefined : userAppGitPermissions?.editableAppsId },
});
if (
isAllAppsEditable ||
(userAppGitPermissions?.editableAppsId?.length && appId && userAppGitPermissions.editableAppsId.includes(appId))
) {
can(FEATURE_KEY.GIT_UPDATE_APP, App);
can(FEATURE_KEY.GIT_SYNC_APP, App);
}
// Additional checks based on specific actions
if (userAppGitPermissions?.editableAppsId?.length) {
can(FEATURE_KEY.GIT_GET_APP_CONFIG, App, {
id: { $in: userAppGitPermissions.editableAppsId },
});
if (
userAppGitPermissions?.editableAppsId?.length &&
appId &&
userAppGitPermissions.editableAppsId.includes(appId)
) {
can(FEATURE_KEY.GIT_GET_APP_CONFIG, App);
}
}
}

View file

@ -35,7 +35,6 @@ export abstract class AbilityFactory<TActions extends string, TSubject> {
}
: {}),
}));
if (request) {
request.tj_user_permissions = userPermission;
}

View file

@ -83,7 +83,7 @@ export abstract class AbilityGuard implements CanActivate {
user,
{ moduleName: module, features },
resourceArray,
this.forwardAbility() ? request : undefined
request
);
if (this.forwardAbility()) {

View file

@ -1,6 +1,7 @@
import { sanitizeInput } from '@helpers/utils.helper';
import { IsString, IsOptional, IsNotEmpty, MaxLength, IsBoolean, IsUUID } from 'class-validator';
import { IsString, IsOptional, IsNotEmpty, MaxLength, IsBoolean, IsUUID, IsEnum } from 'class-validator';
import { Exclude, Expose, Transform } from 'class-transformer';
import { APP_TYPES } from '../constants';
export class AppCreateDto {
@IsNotEmpty()
@ -12,6 +13,11 @@ export class AppCreateDto {
@IsString()
@MaxLength(200, { message: 'Maximum length has been reached.' })
icon?: string;
@IsNotEmpty()
@IsString()
@IsEnum(APP_TYPES, { message: 'Invalid app type.' })
type: string;
}
export class AppUpdateDto {

View file

@ -60,9 +60,9 @@ export class AppsService implements IAppsService {
protected readonly aiUtilService: AiUtilService
) {}
async create(user: User, appCreateDto: AppCreateDto) {
const { name, icon } = appCreateDto;
const { name, icon, type } = appCreateDto;
return await dbTransactionWrap(async (manager: EntityManager) => {
const app = await this.appsUtilService.create(name, user, APP_TYPES.FRONT_END, manager);
const app = await this.appsUtilService.create(name, user, type, manager);
const appUpdateDto = new AppUpdateDto();
appUpdateDto.name = name;

View file

@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
height: 480,
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 10,
left: 1,
height: 40,
},
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 12,
left: 32,
height: 36,
},
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'colorSwatches',
displayName: 'Background color',
@ -274,26 +358,13 @@ export const formConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
exposedVariables: {
data: {},
isValid: true,
isVisible: true,
isDisabled: false,
isLoading: false,
},
actions: [
{
@ -304,6 +375,21 @@ export const formConfig = {
handle: 'resetForm',
displayName: 'Reset Form',
},
{
handle: 'setVisibility',
displayName: 'Set visibility',
params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }],
},
{
handle: 'setDisable',
displayName: 'Set Disable',
params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }],
},
{
handle: 'setLoading',
displayName: 'Set Loading',
params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }],
},
],
definition: {
others: {
@ -318,14 +404,18 @@ export const formConfig = {
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
buttonToSubmit: { value: '{{"none"}}' },
showHeader: { value: '{{false}}' },
showFooter: { value: '{{false}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -13,6 +13,7 @@ export const listviewConfig = {
top: 15,
left: 3,
height: 100,
width: 7,
},
properties: ['source'],
accessorKey: 'imageURL',

View file

@ -13,6 +13,7 @@ export const tabsConfig = {
top: 60,
left: 17,
height: 100,
width: 7,
},
tab: 0,
properties: ['source'],

View file

@ -11,7 +11,7 @@ import {
NotAcceptableException,
NotFoundException,
} from '@nestjs/common';
import { EntityManager, SelectQueryBuilder } from 'typeorm';
import { EntityManager, MoreThan, SelectQueryBuilder } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { AppsRepository } from './repository';
import { AppVersion } from '@entities/app_version.entity';
@ -36,6 +36,7 @@ import { AbilityService } from '@modules/ability/interfaces/IService';
import { DataSourcesRepository } from '@modules/data-sources/repository';
import { IAppsUtilService } from './interfaces/IUtilService';
import { DataSourcesUtilService } from '@modules/data-sources/util.service';
import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
@Injectable()
export class AppsUtilService implements IAppsUtilService {
@ -281,6 +282,70 @@ export class AppsUtilService implements IAppsUtilService {
}, manager);
}
async updateWorflowVersion(version: AppVersion, body: AppVersionUpdateDto, app: App) {
const { name, currentEnvironmentId, definition } = body;
const { currentVersionId, organizationId } = app;
let currentEnvironment: AppEnvironment;
if (version.id === currentVersionId && !body?.is_user_switched_version)
throw new BadRequestException('You cannot update a released version');
if (currentEnvironmentId || definition) {
currentEnvironment = await AppEnvironment.findOne({
where: { id: version.currentEnvironmentId },
});
}
const editableParams = {};
if (name) {
//means user is trying to update the name
const versionNameExists = await this.versionRepository.findOne({
where: { name, appId: version.appId },
});
if (versionNameExists) {
throw new BadRequestException('Version name already exists.');
}
editableParams['name'] = name;
}
//check if the user is trying to promote the environment & raise an error if the currentEnvironmentId is not correct
if (currentEnvironmentId) {
if (!(await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT))) {
throw new BadRequestException('You do not have permissions to perform this action');
}
if (version.currentEnvironmentId !== currentEnvironmentId) {
throw new NotAcceptableException();
}
const nextEnvironment = await AppEnvironment.findOne({
select: ['id'],
where: {
priority: MoreThan(currentEnvironment.priority),
organizationId,
},
order: { priority: 'ASC' },
});
editableParams['currentEnvironmentId'] = nextEnvironment.id;
}
if (definition) {
const environments = await AppEnvironment.count({
where: {
organizationId,
},
});
if (environments > 1 && currentEnvironment.priority !== 1 && !body?.is_user_switched_version) {
throw new BadRequestException('You cannot update a promoted version');
}
editableParams['definition'] = definition;
}
editableParams['updatedAt'] = new Date();
return await this.versionRepository.update(version.id, editableParams);
}
protected async getEnvironmentOfVersion(versionId: string, manager: EntityManager): Promise<AppEnvironment> {
return manager
.createQueryBuilder(AppEnvironment, 'app_environments')

View file

@ -11,7 +11,7 @@ export interface IAuthUtilService {
[key: string]: any;
}>;
verifyToken(token: string): any;
getSSOConfigs(ssoType: SSOType.GOOGLE | SSOType.GIT | SSOType.OPENID): Promise<Partial<SSOConfigs>>;
getSSOConfigs(ssoType: SSOType.GOOGLE | SSOType.GIT): Promise<Partial<SSOConfigs>>;
getInstanceSSOConfigsOfType(ssoType: SSOType.GOOGLE | SSOType.GIT | SSOType.OPENID): Promise<DeepPartial<SSOConfigs>>;
syncUserAndGroups(
userResponse: UserResponse,

View file

@ -16,6 +16,9 @@ import { SSOResponseRepository } from '@modules/auth/oauth/repository/sso-respon
import { FeatureAbilityFactory } from './ability';
import { AbilityService } from '@modules/ability/service';
import { AbilityUtilService } from '@modules/ability/util.service';
import { GroupPermissionsRepository } from '@modules/group-permissions/repository';
import { SetupOrganizationsModule } from '@modules/setup-organization/module';
import { SSOConfigsRepository } from '@modules/login-configs/repository';
@Module({})
export class AuthModule {
@ -31,6 +34,7 @@ export class AuthModule {
const { GoogleOAuthService } = await import(`${importPath}/auth/oauth/util-services/google-oauth.service`);
const { OidcOAuthService } = await import(`${importPath}/auth/oauth/util-services/oidc-auth.service`);
const { LdapService } = await import(`${importPath}/auth/oauth/util-services/ldap.service`);
const { AppEnvironmentUtilService } = await import(`${importPath}/app-environments/util.service`);
return {
module: AuthModule,
@ -45,6 +49,7 @@ export class AuthModule {
await SessionModule.register(configs),
await OrganizationUsersModule.register(configs),
await LoginConfigsModule.register(configs),
await SetupOrganizationsModule.register(configs),
],
controllers: [AuthController, OauthController],
providers: [
@ -64,6 +69,9 @@ export class AuthModule {
FeatureAbilityFactory,
AbilityService,
AbilityUtilService,
AppEnvironmentUtilService,
GroupPermissionsRepository,
SSOConfigsRepository,
],
exports: [AuthUtilService],
};

View file

@ -36,6 +36,7 @@ import { OrganizationUsersRepository } from '@modules/organization-users/reposit
import { LicenseUserService } from '@modules/licensing/services/user.service';
import { OnboardingUtilService } from '@modules/onboarding/util.service';
import { SessionUtilService } from '@modules/session/util.service';
import { SetupOrganizationsUtilService } from '@modules/setup-organization/util.service';
const uuid = require('uuid');
@Injectable()
@ -56,7 +57,8 @@ export class OauthService implements IOAuthService {
protected readonly organizationUsersRepository: OrganizationUsersRepository,
protected readonly licenseUserService: LicenseUserService,
protected readonly onboardingUtilService: OnboardingUtilService,
protected readonly sessionUtilService: SessionUtilService
protected readonly sessionUtilService: SessionUtilService,
protected readonly setupOrganizationsUtilService: SetupOrganizationsUtilService
) {}
async signIn(
@ -172,21 +174,26 @@ export class OauthService implements IOAuthService {
// Not logging in to specific organization, creating new
const { name, slug } = generateNextNameAndSlug('My workspace');
defaultOrganization = await this.organizationRepository.createOne(name, slug, manager);
defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
userDetails = await this.userRepository.createOne(
userDetails = await this.userRepository.createOrUpdate(
{
firstName: userResponse.firstName,
lastName: userResponse.lastName,
email: userResponse.email,
userType: USER_ROLE.ADMIN,
defaultOrganizationId: defaultOrganization.id,
...getUserStatusAndSource(lifecycleEvents.USER_SSO_VERIFY, sso),
...getUserStatusAndSource(lifecycleEvents.USER_SSO_ACTIVATE, sso),
},
manager
);
await this.organizationUsersRepository.createOne(userDetails, defaultOrganization, false, manager);
await this.organizationUsersUtilService.attachUserGroup(
[USER_ROLE.ADMIN],
defaultOrganization.id,
userDetails.id,
manager
);
await this.organizationUsersRepository.createOne(userDetails, defaultOrganization, true, manager);
organizationDetails = defaultOrganization;
} else if (userDetails) {
// Finding organization to be loaded
@ -213,7 +220,7 @@ export class OauthService implements IOAuthService {
if (!isInviteRedirect) {
// no SSO login enabled organization available for user - creating new one
const { name, slug } = generateNextNameAndSlug('My workspace');
organizationDetails = await this.organizationRepository.createOne(name, slug, manager);
organizationDetails = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
await this.userRepository.updateOne(
userDetails.id,
{ defaultOrganizationId: organizationDetails.id },

View file

@ -25,7 +25,6 @@ import { dbTransactionWrap } from 'src/helpers/database.helper';
import { DeepPartial } from 'typeorm';
import { SSOType } from '../../entities/sso_config.entity';
import { LicenseTermsService } from '../licensing/interfaces/IService';
import { LICENSE_FIELD } from '../licensing/constants';
import { GroupPermissionsUtilService } from '../group-permissions/util.service';
import { App } from '../../entities/app.entity';
import { In } from 'typeorm';
@ -38,7 +37,6 @@ import { RolesRepository } from '@modules/roles/repository';
import { GroupPermissions } from '@entities/group_permissions.entity';
import { ProfileUtilService } from '@modules/profile/util.service';
import { OrganizationUsersRepository } from '@modules/organization-users/repository';
import { InstanceSSOConfigMap } from '@modules/login-configs/types';
import { SessionUtilService } from '@modules/session/util.service';
import { OnboardingStatus } from '@modules/onboarding/constants';
import { IAuthUtilService } from './interfaces/IUtilService';
@ -152,11 +150,11 @@ export class AuthUtilService implements IAuthUtilService {
defaultOrganization = await this.organizationRepository.createOne(name, slug, manager);
}
const { source, status } = getUserStatusAndSource(lifecycleEvents.USER_SSO_VERIFY, sso);
const { source, status } = getUserStatusAndSource(lifecycleEvents.USER_SSO_ACTIVATE, sso);
/* Default password for sso-signed workspace user */
const password = uuid.v4();
user = await this.userRepository.createOne(
user = await this.userRepository.createOrUpdate(
{
firstName,
lastName,
@ -164,9 +162,8 @@ export class AuthUtilService implements IAuthUtilService {
source,
status,
password,
organizationId: organization.id,
role: USER_ROLE.END_USER,
defaultOrganizationId: defaultOrganization?.id,
defaultOrganizationId: defaultOrganization?.id || organization.id,
},
manager
);
@ -193,45 +190,32 @@ export class AuthUtilService implements IAuthUtilService {
// Setting up default organization
await this.organizationUsersRepository.createOne(user, defaultOrganization, true, manager);
}
await this.organizationUsersUtilService.attachUserGroup([USER_ROLE.END_USER], organization.id, user.id, manager); //localhost:8082/login/tooljets-workspace?redirectTo=/
return user;
}
async getSSOConfigs(ssoType: SSOType.GOOGLE | SSOType.GIT | SSOType.OPENID): Promise<Partial<SSOConfigs>> {
const ssoConfigs = await this.getInstanceSSOConfigsOfType(ssoType);
const oidcEnabled = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.OIDC);
// Create a map from the ssoConfig object
const ssoConfigMap: InstanceSSOConfigMap = {
[ssoConfigs.sso]: {
enabled: ssoConfigs.enabled,
configs: ssoConfigs.configs,
},
};
async getSSOConfigs(ssoType: SSOType.GOOGLE | SSOType.GIT): Promise<Partial<SSOConfigs>> {
switch (ssoType) {
case SSOType.GOOGLE:
return {
enabled: ssoConfigMap.google.enabled || false,
configs: ssoConfigMap.google.configs || {},
enabled: !!this.configService.get<string>('SSO_GOOGLE_OAUTH2_CLIENT_ID'),
configs: { clientId: this.configService.get<string>('SSO_GOOGLE_OAUTH2_CLIENT_ID') },
};
case SSOType.GIT:
return {
enabled: ssoConfigMap.git.enabled || false,
configs: ssoConfigMap.git.configs || {},
};
case SSOType.OPENID:
return {
enabled: ssoConfigMap.openid.enabled && oidcEnabled,
configs: ssoConfigMap.openid.configs || {},
enabled: !!this.configService.get<string>('SSO_GIT_OAUTH2_CLIENT_ID'),
configs: {
clientId: this.configService.get<string>('SSO_GIT_OAUTH2_CLIENT_ID'),
clientSecret: this.configService.get<string>('SSO_GIT_OAUTH2_CLIENT_SECRET'),
hostName: this.configService.get<string>('SSO_GIT_OAUTH2_HOST'),
},
};
default:
return;
}
}
async getInstanceSSOConfigsOfType(
ssoType: SSOType.GOOGLE | SSOType.GIT | SSOType.OPENID
): Promise<DeepPartial<SSOConfigs>> {
async getInstanceSSOConfigsOfType(ssoType: SSOType.GOOGLE | SSOType.GIT): Promise<DeepPartial<SSOConfigs>> {
const instanceSettings = await this.instanceSettingsUtilService.getSettings([
INSTANCE_SYSTEM_SETTINGS.ALLOWED_DOMAINS,
INSTANCE_SYSTEM_SETTINGS.ENABLE_SIGNUP,

View file

@ -191,11 +191,12 @@ export class DataQueriesService implements IDataQueriesService {
async changeQueryDataSource(user: User, queryId: string, dataSource: DataSource, newDataSourceId: string) {
return dbTransactionWrap(async (manager: EntityManager) => {
const newDataSource = await this.dataSourceRepository.findOne({ where: { id: newDataSourceId } });
if (dataSource.kind !== newDataSource.kind) {
throw new BadRequestException();
}
return this.dataQueryRepository.updateOne(queryId, { dataSourceId: newDataSourceId }, manager);
const newDataSource = await this.dataSourceRepository.findOneOrFail({ where: { id: newDataSourceId } });
// FIXME: Disabling this check as workflows can change data source of a query with different kind
// if (dataSource.kind !== newDataSource.kind && dataSource) {
// throw new BadRequestException();
// }
return this.dataQueryRepository.updateOne(queryId, { dataSourceId: newDataSource.id }, manager);
// TODO: Audit logs
});

View file

@ -15,7 +15,12 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
return DataSource;
}
protected defineAbilityFor(can: AbilityBuilder<FeatureAbility>['can'], UserAllPermissions: UserAllPermissions): void {
protected defineAbilityFor(
can: AbilityBuilder<FeatureAbility>['can'],
UserAllPermissions: UserAllPermissions,
extractedMetadata: { moduleName: string; features: string[] },
request?: any
): void {
// Data source permissions
// EE - data source create/delete -> full access
// CE - Admin - full access. builder -> use access
@ -27,6 +32,8 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
const isCanDelete = userPermission.dataSourceDelete;
const isAllViewable = !!resourcePermissions?.isAllUsable;
const dataSourceId = request?.tj_resource_id;
// Oauth end points available to all
can(FEATURE_KEY.GET_OAUTH2_BASE_URL, DataSource);
can(FEATURE_KEY.AUTHORIZE, DataSource);
@ -81,11 +88,14 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
return;
}
if (resourcePermissions?.configurableDataSourceId?.length) {
if (
resourcePermissions?.configurableDataSourceId?.length &&
dataSourceId &&
resourcePermissions?.configurableDataSourceId?.includes(dataSourceId)
) {
can(
[FEATURE_KEY.GET, FEATURE_KEY.UPDATE, FEATURE_KEY.GET_BY_ENVIRONMENT, FEATURE_KEY.TEST_CONNECTION],
DataSource,
{ id: { $in: resourcePermissions.configurableDataSourceId } }
DataSource
);
}
@ -93,10 +103,12 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
can([FEATURE_KEY.GET, FEATURE_KEY.GET_BY_ENVIRONMENT], DataSource);
return;
}
if (resourcePermissions.usableDataSourcesId?.length) {
can([FEATURE_KEY.GET, FEATURE_KEY.GET_BY_ENVIRONMENT], DataSource, {
id: { $in: resourcePermissions.usableDataSourcesId },
});
if (
resourcePermissions.usableDataSourcesId?.length &&
dataSourceId &&
resourcePermissions?.usableDataSourcesId?.includes(dataSourceId)
) {
can([FEATURE_KEY.GET, FEATURE_KEY.GET_BY_ENVIRONMENT], DataSource);
}
}
}

View file

@ -20,7 +20,6 @@ import { GroupUsers } from '@entities/group_users.entity';
import { USER_STATUS, WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle';
import { User } from '@entities/user.entity';
import { DATA_BASE_CONSTRAINTS } from './constants/error';
@Injectable()
export class GroupPermissionsRepository extends Repository<GroupPermissions> {
constructor(private dataSource: DataSource) {

View file

@ -52,8 +52,9 @@ export class GroupPermissionsUtilService implements IGroupPermissionsUtilService
validateAddGroupUserOperation(group: GroupPermissions) {
if (!group || Object.keys(group)?.length === 0) throw new BadRequestException(ERROR_HANDLER.GROUP_NOT_EXIST);
if (group.type == GROUP_PERMISSIONS_TYPE.DEFAULT)
throw new MethodNotAllowedException(ERROR_HANDLER.ADD_GROUP_USER_DEFAULT_GROUP);
//commented out the default group check because for enable signup cases, user is added to default admin group
// if (group.type == GROUP_PERMISSIONS_TYPE.DEFAULT)
// throw new MethodNotAllowedException(ERROR_HANDLER.ADD_GROUP_USER_DEFAULT_GROUP);
}
validateDeleteGroupUserOperation(group: GroupPermissions, organizationId: string) {
@ -94,11 +95,14 @@ export class GroupPermissionsUtilService implements IGroupPermissionsUtilService
return await dbTransactionWrap(async (manager: EntityManager) => {
// Get Group details
const group = await this.groupPermissionsRepository.getGroup({
id,
organizationId,
...(!isLicenseValid ? noLicenseFilter : {}),
});
const group = await this.groupPermissionsRepository.getGroup(
{
id,
organizationId,
...(!isLicenseValid ? noLicenseFilter : {}),
},
manager
);
if (!isLicenseValid) {
if (group.name !== USER_ROLE.END_USER) {

View file

@ -15,10 +15,6 @@ export class FeatureAbilityGuard extends AbilityGuard {
return App;
}
protected forwardAbility(): boolean {
return true;
}
protected getResource(): ResourceDetails | ResourceDetails[] {
return [
{

View file

@ -12,8 +12,4 @@ export class FeatureAbilityGuard extends AbilityGuard {
protected getAbilityFactory() {
return FeatureAbilityFactory;
}
protected forwardAbility(): boolean {
return true;
}
}

View file

@ -45,11 +45,12 @@ export const INSTANCE_SETTINGS_ENCRYPTION_KEY = 'instance_settings';
export function getDefaultInstanceSettings() {
return {
[INSTANCE_SYSTEM_SETTINGS.ENABLE_SIGNUP]: process.env.SSO_DISABLE_SIGNUPS,
[INSTANCE_SYSTEM_SETTINGS.ENABLE_SIGNUP]: process.env.DISABLE_SIGNUPS === 'false' ? 'true' : 'false',
[INSTANCE_SYSTEM_SETTINGS.ENABLE_WORKSPACE_LOGIN_CONFIGURATION]: 'true',
[INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE]: 'true',
[INSTANCE_USER_SETTINGS.ENABLE_MULTIPLAYER_EDITING]: process.env.ENABLE_MULTIPLAYER_EDITING,
[INSTANCE_USER_SETTINGS.ENABLE_COMMENTS]: process.env.COMMENT_FEATURE_ENABLE,
[INSTANCE_USER_SETTINGS.ENABLE_MULTIPLAYER_EDITING]:
process.env.ENABLE_MULTIPLAYER_EDITING === 'true' ? 'true' : 'false',
[INSTANCE_USER_SETTINGS.ENABLE_COMMENTS]: process.env.COMMENT_FEATURE_ENABLE === 'true' ? 'true' : 'false',
[INSTANCE_SYSTEM_SETTINGS.SMTP_PORT]: process.env.SMTP_PORT,
[INSTANCE_SYSTEM_SETTINGS.SMTP_DOMAIN]: process.env.SMTP_DOMAIN,
[INSTANCE_SYSTEM_SETTINGS.SMTP_USERNAME]: process.env.SMTP_USERNAME,

View file

@ -48,7 +48,7 @@ export const BASIC_PLAN_TERMS: Partial<Terms> = {
export const BASIC_PLAN_SETTINGS = {
ALLOW_PERSONAL_WORKSPACE: {
value: 'true',
value: 'false',
},
WHITE_LABEL_LOGO: {
value: '',

View file

@ -575,7 +575,7 @@ export class OnboardingUtilService implements IOnboardingUtilService {
): Promise<User> {
return dbTransactionWrap(async (manager: EntityManager) => {
// Create the user
const user = await this.userRepository.createOne(userParams, manager);
const user = await this.userRepository.createOrUpdate(userParams, manager);
// Add the role for the user in the specified organization
await this.rolesUtilService.addUserRole(organizationId, { role, userId: user.id }, manager);

View file

@ -40,7 +40,6 @@ import { SessionUtilService } from '@modules/session/util.service';
import { SetupOrganizationsUtilService } from '@modules/setup-organization/util.service';
import { IOrganizationUsersUtilService } from './interfaces/IUtilService';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class OrganizationUsersUtilService implements IOrganizationUsersUtilService {
constructor(
@ -147,8 +146,18 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
try {
for (const addGroup of groups) {
const orgGroupPermission = await this.groupPermissionsRepository.getGroup(
{
organizationId: organizationId,
name: addGroup,
},
manager
);
if (!orgGroupPermission) {
throw new BadRequestException(`${addGroup} group does not exist for current organization`);
}
await this.groupPermissionsUtilService.addUsersToGroup(
{ allowRoleChange: false, userIds: [userId], groupId: addGroup },
{ allowRoleChange: false, userIds: [userId], groupId: orgGroupPermission.id },
organizationId,
manager
);
@ -223,7 +232,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
return await dbTransactionWrap(async (manager: EntityManager) => {
const userType = (await manager.count(User)) === 0 ? USER_TYPE.INSTANCE : USER_TYPE.WORKSPACE;
return await this.userRepository.createOne(
return await this.userRepository.createOrUpdate(
{
email,
firstName,

View file

@ -9,12 +9,13 @@ export class OrganizationsModule {
const { OrganizationsService } = await import(`${importPath}/organizations/service`);
const { OrganizationsController } = await import(`${importPath}/organizations/controller`);
const { FeatureAbilityFactory } = await import(`${importPath}/organizations/ability`);
const { AppEnvironmentUtilService } = await import(`${importPath}/app-environments/util.service`);
return {
module: OrganizationsModule,
imports: [await InstanceSettingsModule.register(configs)],
controllers: [OrganizationsController],
providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory],
providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory, AppEnvironmentUtilService],
};
}
}

View file

@ -9,7 +9,7 @@ import { RolesModule } from '@modules/roles/module';
import { ThemesModule } from '@modules/organization-themes/module';
import { SessionModule } from '@modules/session/module';
import { InstanceSettingsModule } from '@modules/instance-settings/module';
import { TooljetDbTableOperationsService } from '@modules/tooljet-db/services/tooljet-db-table-operations.service';
import { TooljetDbModule } from '@modules/tooljet-db/module';
export class SetupOrganizationsModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
@ -29,6 +29,7 @@ export class SetupOrganizationsModule {
await ThemesModule.register(configs),
await SessionModule.register(configs),
await InstanceSettingsModule.register(configs),
await TooljetDbModule.register(configs),
],
controllers: [SetupOrganizationsController],
providers: [
@ -37,7 +38,7 @@ export class SetupOrganizationsModule {
OrganizationRepository,
OrganizationUsersRepository,
FeatureAbilityFactory,
TooljetDbTableOperationsService,
TooljetDbModule,
],
exports: [SetupOrganizationsUtilService],
};

View file

@ -12,8 +12,4 @@ export class FeatureAbilityGuard extends AbilityGuard {
protected getSubjectType() {
return InternalTable;
}
protected forwardAbility(): boolean {
return true;
}
}

View file

@ -86,9 +86,18 @@ export class UserRepository extends Repository<User> {
};
}
createOne(user: Partial<User>, manager?: EntityManager): Promise<User> {
return dbTransactionWrap((manager: EntityManager) => {
return manager.save(manager.create(User, user));
async createOrUpdate(user: Partial<User>, manager?: EntityManager): Promise<User> {
//not using upsert because hook is not supported for password digest
return dbTransactionWrap(async (manager: EntityManager) => {
const existingUser = await manager.findOne(User, { where: { email: user.email } });
if (existingUser) {
Object.assign(existingUser, user);
return manager.save(User, existingUser);
} else {
const newUser = manager.create(User, user);
return manager.save(User, newUser);
}
}, manager || this.manager);
}

View file

@ -15,7 +15,13 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
return App;
}
protected defineAbilityFor(can: AbilityBuilder<FeatureAbility>['can'], UserAllPermissions: UserAllPermissions): void {
protected defineAbilityFor(
can: AbilityBuilder<FeatureAbility>['can'],
UserAllPermissions: UserAllPermissions,
extractedMetadata: { moduleName: string; features: string[] },
request?: any
): void {
const appId = request?.tj_resource_id;
const { superAdmin, isAdmin, userPermission } = UserAllPermissions;
const userAppPermissions = userPermission?.[MODULES.APP];
@ -52,7 +58,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
return;
}
if (userAppPermissions?.editableAppsId?.length) {
if (userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId)) {
can(
[
FEATURE_KEY.GET,
@ -76,16 +82,19 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
FEATURE_KEY.UPDATE_EVENT,
FEATURE_KEY.DELETE_EVENT,
],
App,
{ id: { $in: userAppPermissions.editableAppsId } }
App
);
}
if (isAllAppsViewable) {
// add view permissions for all apps
can([FEATURE_KEY.GET_EVENTS], App);
} else if (userAppPermissions?.viewableAppsId?.length) {
can([FEATURE_KEY.GET_EVENTS], App, { id: { $in: userAppPermissions.viewableAppsId } });
} else if (
userAppPermissions?.viewableAppsId?.length &&
appId &&
userAppPermissions.viewableAppsId.includes(appId)
) {
can([FEATURE_KEY.GET_EVENTS], App);
}
}
}

View file

@ -175,6 +175,10 @@ export class VersionService implements IVersionService {
await this.versionsUtilService.updateVersion(appVersion, appVersionUpdateDto);
if (app.type === 'workflow') {
await this.appUtilService.updateWorflowVersion(appVersion, appVersionUpdateDto, app);
}
this.eventEmitter.emit('auditLogEntry', {
userId: user.id,
organizationId: user.organizationId,

View file

@ -42,7 +42,7 @@ export class VersionUtilService implements IVersionUtilService {
async updateVersion(appVersion: AppVersion, appVersionUpdateDto: AppVersionUpdateDto) {
const editableParams = {};
const { globalSettings, homePageId, pageSettings } = appVersion;
const { globalSettings, homePageId, pageSettings, name } = appVersion;
if (appVersionUpdateDto?.homePageId && homePageId !== appVersionUpdateDto.homePageId) {
editableParams['homePageId'] = appVersionUpdateDto.homePageId;
@ -65,6 +65,10 @@ export class VersionUtilService implements IVersionUtilService {
editableParams['showViewerNavigation'] = appVersionUpdateDto.showViewerNavigation;
}
if (appVersionUpdateDto?.name && name !== appVersionUpdateDto.name) {
editableParams['name'] = appVersionUpdateDto.name;
}
await this.versionRepository.update(appVersion.id, editableParams);
return;
}

View file

@ -15,10 +15,6 @@ export class FeatureAbilityGuard extends AbilityGuard {
return App;
}
protected forwardAbility(): boolean {
return true;
}
protected getResource(): ResourceDetails | ResourceDetails[] {
return [
{

View file

@ -98,6 +98,7 @@ export class WorkflowsModule {
UserRepository,
DataSourcesRepository,
DataQueryRepository,
DataSourcesRepository,
OrganizationConstantRepository,
VersionRepository,
AppsService,