mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 00:48:25 +00:00
Merge branch 'modularisation/v3' into label-input-alignment
This commit is contained in:
commit
13c2e4485d
91 changed files with 1645 additions and 641 deletions
4
.github/workflows/render-preview-deploy.yml
vendored
4
.github/workflows/render-preview-deploy.yml
vendored
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 7e6cf7d2e9c2dfaa4ca3d1308406cfed3c95715b
|
||||
Subproject commit d93ee7e1318f044ef2327671b8b257648071453d
|
||||
|
|
@ -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
|
||||
|
|
@ -28,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';
|
||||
|
|
@ -49,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',
|
||||
}}
|
||||
|
|
@ -64,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"
|
||||
>
|
||||
|
|
@ -78,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"
|
||||
|
|
@ -100,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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,7 +86,6 @@ const WidgetWrapper = memo(
|
|||
{mode == 'edit' && (
|
||||
<ConfigHandle
|
||||
id={id}
|
||||
position={layoutData.top < 15 ? 'bottom' : 'top'}
|
||||
widgetTop={layoutData.top}
|
||||
widgetHeight={layoutData.height}
|
||||
showHandle={isWidgetActive}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,19 +5,13 @@ const MIN_TABLE_ROW_HEIGHT_DEFAULT = 45;
|
|||
|
||||
const TableRowHeightInput = ({ value, onChange, cyLabel, staticText, styleDefinition }) => {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const minValue =
|
||||
styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT;
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(value < minValue ? minValue : value);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value, styleDefinition.cellSize?.value]);
|
||||
useEffect(() => {
|
||||
onChange(
|
||||
styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const minValue =
|
||||
styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT;
|
||||
|
||||
const handleBlur = () => {
|
||||
const newValue = Math.max(inputValue, minValue);
|
||||
|
|
|
|||
|
|
@ -300,10 +300,10 @@ const MultiLineCodeEditor = (props) => {
|
|||
editable={editable} //for transformations in query manager
|
||||
onCreateEditor={(view) => setEditorView(view)}
|
||||
onUpdate={(view) => {
|
||||
const icon = document.querySelector('.codehinter-search-btn-wrapper');
|
||||
const icon = document.querySelector('.codehinter-search-btn');
|
||||
if (searchPanelOpen(view.state)) {
|
||||
icon.style.top = '44px';
|
||||
} else icon.style.top = '0px';
|
||||
icon.style.display = 'none';
|
||||
} else icon.style.display = 'block';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable import/no-unresolved */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
|
|
@ -9,12 +10,13 @@ import {
|
|||
replaceNext,
|
||||
replaceAll,
|
||||
openSearchPanel,
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
} from '@codemirror/search';
|
||||
import './SearchBox.scss';
|
||||
import InputComponent from '@/components/ui/Input/Index.jsx';
|
||||
import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import { SelectionRange } from '@codemirror/state';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
export const handleSearchPanel = (view) => {
|
||||
const dom = document.createElement('div');
|
||||
|
|
@ -35,6 +37,11 @@ function SearchPanel({ view }) {
|
|||
replace: replaceTerm,
|
||||
});
|
||||
view.dispatch({ effects: setSearchQuery.of(query) });
|
||||
|
||||
const currentPos = view.state.selection.main.head;
|
||||
view.dispatch({
|
||||
selection: SelectionRange.create(currentPos, currentPos),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -44,12 +51,28 @@ function SearchPanel({ view }) {
|
|||
return () => clearTimeout(handler);
|
||||
}, [searchText, replaceText]);
|
||||
|
||||
const [shortcutEnabled, setShortcutEnabled] = useState(false);
|
||||
|
||||
// Shortcuts for search input field
|
||||
useHotkeys(
|
||||
['shift+enter', 'enter'],
|
||||
(event, handler) => {
|
||||
if (handler.shift && handler.keys[0] === 'enter') findPrevious(view);
|
||||
else if (handler.keys[0] === 'enter') findNext(view);
|
||||
},
|
||||
{
|
||||
enabled: shortcutEnabled,
|
||||
enableOnFormTags: true,
|
||||
}
|
||||
);
|
||||
|
||||
const displaySearchField = () => (
|
||||
<div className="search-replace-inputs">
|
||||
<InputComponent
|
||||
leadingIcon="search01"
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && findNext(view)}
|
||||
onFocus={() => setShortcutEnabled(true)}
|
||||
onBlur={() => setShortcutEnabled(false)}
|
||||
placeholder="Find"
|
||||
size="small"
|
||||
value={searchText}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -644,13 +644,15 @@
|
|||
}
|
||||
|
||||
.cm-searchMatch {
|
||||
background-color: #F9E71A !important;
|
||||
|
||||
.cm-selectionMatch {
|
||||
background-color: #F9E71A !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-searchMatch.cm-searchMatch-selected {
|
||||
background-color: #F28F2D;
|
||||
background-color: #F28F2D !important;
|
||||
}
|
||||
|
||||
.tjdb-hinter-portal{
|
||||
|
|
|
|||
|
|
@ -88,9 +88,19 @@ const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]);
|
||||
|
||||
const handleNodeExpansion = (path) => {
|
||||
const handleNodeExpansion = (path, data, currentNode) => {
|
||||
if (pathToBeInspected && path?.length > 0) {
|
||||
return pathToBeInspected.includes(path[path.length - 1]);
|
||||
const shouldExpand = pathToBeInspected.includes(path[path.length - 1]);
|
||||
|
||||
// Scroll to the component in the inspector
|
||||
if (path?.length === 2 && path?.[0] === 'components' && shouldExpand) {
|
||||
const target = document.getElementById(`inspector-node-${String(currentNode).toLowerCase()}`);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
|
||||
return shouldExpand;
|
||||
} else return false;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const useCallbackActions = () => {
|
|||
const currentPageComponents = useStore((state) => state?.getCurrentPageComponents(), shallow);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const runQuery = useStore((state) => state.queryPanel.runQuery);
|
||||
const getComponentIdToAutoScroll = useStore((state) => state.getComponentIdToAutoScroll);
|
||||
|
||||
const handleRemoveComponent = (component) => {
|
||||
deleteComponents([component.id]);
|
||||
|
|
@ -30,30 +31,22 @@ const useCallbackActions = () => {
|
|||
return toast.success('Copied to the clipboard', { position: 'top-center' });
|
||||
};
|
||||
|
||||
const autoScrollTo = (id) => {
|
||||
setSelectedComponents([id]);
|
||||
const target = document.getElementById(id);
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
};
|
||||
|
||||
const handleAutoScrollToComponent = (data) => {
|
||||
const currentPageComponents = useStore.getState().getCurrentPageComponents();
|
||||
const component = currentPageComponents?.[data.id];
|
||||
|
||||
let parentId = component?.component?.parent;
|
||||
if (parentId) {
|
||||
const regex = /-\d+$/;
|
||||
if (regex.test(parentId)) {
|
||||
parentId = parentId.replace(regex, ''); // To get parentId without tab index if parent type is Tab
|
||||
}
|
||||
const parentType = currentPageComponents?.[parentId]?.component?.component;
|
||||
if (parentType && (parentType === 'Modal' || parentType === 'Tabs')) {
|
||||
autoScrollTo(parentId); // To scroll to parent component if parent type is Modal or Tabs
|
||||
return;
|
||||
}
|
||||
const { isAccessible, computedComponentId, isOnCanvas } = getComponentIdToAutoScroll(data.id);
|
||||
if (!isAccessible) {
|
||||
if (isOnCanvas) {
|
||||
toast.success(
|
||||
`This component can't be opened because it's on the main canvas. Close ${computedComponentId} and click "Go to component" to view it there`
|
||||
);
|
||||
} else
|
||||
toast.success(
|
||||
`This component can't be opened because it's inside ${computedComponentId}. Open ${computedComponentId} and click "Go to component"to view it.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
autoScrollTo(data.id);
|
||||
setSelectedComponents([computedComponentId]);
|
||||
const target = document.getElementById(computedComponentId);
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
};
|
||||
|
||||
const callbackActions = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')}`,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export const PropertiesTabElements = ({
|
|||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Image', value: 'image' },
|
||||
{ label: 'Link', value: 'link' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
// Following column types are deprecated
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Dropdown', value: 'dropdown' },
|
||||
|
|
@ -266,6 +267,24 @@ export const PropertiesTabElements = ({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{column.columnType === 'json' && (
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Indent"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={onColumnItemChange}
|
||||
property="jsonIndentation"
|
||||
props={column}
|
||||
component={component}
|
||||
paramMeta={{ type: 'toggle', displayName: 'Indent' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
|
|
|
|||
|
|
@ -122,9 +122,18 @@ export const StylesTabElements = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{['string', 'default', undefined, 'number', 'boolean', 'select', 'text', 'newMultiSelect', 'datepicker'].includes(
|
||||
column.columnType
|
||||
) && (
|
||||
{[
|
||||
'string',
|
||||
'default',
|
||||
undefined,
|
||||
'number',
|
||||
'json',
|
||||
'boolean',
|
||||
'select',
|
||||
'text',
|
||||
'newMultiSelect',
|
||||
'datepicker',
|
||||
].includes(column.columnType) && (
|
||||
<>
|
||||
{column.columnType !== 'boolean' && (
|
||||
<div data-cy={`input-and-label-text-color`} className="field px-3">
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ export const ProgramaticallyHandleProperties = ({
|
|||
return props?.parseInUnixTimestamp;
|
||||
case 'isDateSelectionEnabled':
|
||||
return props?.isDateSelectionEnabled;
|
||||
case 'jsonIndentation':
|
||||
return props?.jsonIndentation;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
|
@ -81,6 +83,9 @@ export const ProgramaticallyHandleProperties = ({
|
|||
if (property === 'linkColor') {
|
||||
return definitionObj?.value ?? '#1B1F24';
|
||||
}
|
||||
if (property === 'jsonIndentation') {
|
||||
return definitionObj?.value ?? `{{true}}`;
|
||||
}
|
||||
return definitionObj?.value ?? `{{false}}`;
|
||||
};
|
||||
|
||||
|
|
@ -111,7 +116,9 @@ export const ProgramaticallyHandleProperties = ({
|
|||
const fxActiveFieldsPropExists = props?.hasOwnProperty('fxActiveFields') ?? false;
|
||||
//to support backward compatibility, when fxActive is true for a particular column, we are passing all possible combinations which should render codehinter
|
||||
const fxActive =
|
||||
props?.fxActive && resolveReferences(props.fxActive) ? ['isEditable', 'columnVisibility', 'linkTarget'] : [];
|
||||
props?.fxActive && resolveReferences(props.fxActive)
|
||||
? ['isEditable', 'columnVisibility', 'jsonIndentation', 'linkTarget']
|
||||
: [];
|
||||
|
||||
const checkFxActiveFieldIsArrray = (fxActiveFieldsProperty) => {
|
||||
// adding error handling mechanism for fxActiveFieldsProperty , if props.fxActiveFields is array , then return props.fxActiveFields or else return [], this will make sure, fxActiveFields wil always be array
|
||||
|
|
|
|||
|
|
@ -624,6 +624,8 @@ class TableComponent extends React.Component {
|
|||
return 'Select';
|
||||
case 'newMultiSelect':
|
||||
return 'Multiselect';
|
||||
case 'json':
|
||||
return 'JSON';
|
||||
default:
|
||||
capitalize(text ?? '');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
119
frontend/src/AppBuilder/Widgets/Container/Container.jsx
Normal file
119
frontend/src/AppBuilder/Widgets/Container/Container.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
13
frontend/src/AppBuilder/Widgets/Container/container.scss
Normal file
13
frontend/src/AppBuilder/Widgets/Container/container.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
40
frontend/src/AppBuilder/Widgets/Form/form.scss
Normal file
40
frontend/src/AppBuilder/Widgets/Form/form.scss
Normal 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;
|
||||
}
|
||||
|
|
@ -56,6 +56,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) {
|
|||
const [containers, setContainers] = useState([]);
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas);
|
||||
const [activeId, setActiveId] = useState(null);
|
||||
const cardMovementRef = useRef(null);
|
||||
const shouldUpdateData = useRef(false);
|
||||
|
|
@ -117,6 +118,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) {
|
|||
}
|
||||
/**** End - Logic to reduce the zIndex of modal control box ****/
|
||||
}
|
||||
setModalOpenOnCanvas(`${id}-modal`, showModal);
|
||||
}, [showModal]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -410,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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export const Modal = function Modal({
|
|||
const size = properties.size ?? 'lg';
|
||||
const [modalWidth, setModalWidth] = useState();
|
||||
const mode = useStore((state) => state.currentMode, shallow);
|
||||
const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas);
|
||||
|
||||
/**** Start - Logic to reset the zIndex of modal control box ****/
|
||||
useEffect(() => {
|
||||
|
|
@ -63,6 +64,7 @@ export const Modal = function Modal({
|
|||
useGridStore.getState().actions.setOpenModalWidgetId(null);
|
||||
}
|
||||
}
|
||||
setModalOpenOnCanvas(id, showModal);
|
||||
}, [showModal, id, mode]);
|
||||
/**** End - Logic to reset the zIndex of modal control box ****/
|
||||
|
||||
|
|
|
|||
|
|
@ -263,6 +263,9 @@ export const Datepicker = function Datepicker({
|
|||
}
|
||||
setIsDateInputFocussed(false);
|
||||
}}
|
||||
closeOnScroll={(e) => {
|
||||
return e.target.className === 'table-responsive jet-data-table false false';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
191
frontend/src/AppBuilder/Widgets/Table/Json.jsx
Normal file
191
frontend/src/AppBuilder/Widgets/Table/Json.jsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
/* eslint-disable no-undef */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { determineJustifyContentValue } from '@/_helpers/utils';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
|
||||
const Json = ({
|
||||
isEditable,
|
||||
jsonIndentation,
|
||||
darkMode,
|
||||
handleCellValueChange,
|
||||
cellTextColor,
|
||||
cellValue,
|
||||
column,
|
||||
containerWidth,
|
||||
cell,
|
||||
horizontalAlignment,
|
||||
isMaxRowHeightAuto,
|
||||
cellSize,
|
||||
maxRowHeightValue,
|
||||
}) => {
|
||||
const ref = React.useRef(null);
|
||||
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const cellStyles = {
|
||||
color: cellTextColor ?? 'inherit',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditable && isEditing) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
}, [isEditable]);
|
||||
|
||||
function format(obj) {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return typeof obj === 'string' ? `"${obj}"` : obj;
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
return `[ ${obj.map(format).join(', ')} ]`;
|
||||
}
|
||||
return `{ ${Object.entries(obj)
|
||||
.map(([key, value]) => `"${key}": ${format(value)}`)
|
||||
.join(', ')} }`;
|
||||
}
|
||||
|
||||
const formatCellValue = (value, overlay = false) => {
|
||||
try {
|
||||
if (typeof value === 'object') {
|
||||
if (jsonIndentation === true && !overlay) {
|
||||
return JSON.stringify(value, null, 4).replace(/":/g, '": ');
|
||||
}
|
||||
const formattedJSON = format(value);
|
||||
return formattedJSON;
|
||||
} else {
|
||||
if (jsonIndentation === true && !overlay) {
|
||||
return JSON.stringify(JSON.parse(value), null, 4).replace(/":/g, '": ');
|
||||
}
|
||||
const formattedJSON = format(JSON.parse(value));
|
||||
return formattedJSON;
|
||||
}
|
||||
} catch (error) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const _renderString = () => (
|
||||
<div
|
||||
ref={ref}
|
||||
contentEditable={'true'}
|
||||
className={`h-100 text-container long-text-input d-flex align-items-center ${
|
||||
darkMode ? ' textarea-dark-theme' : ''
|
||||
} justify-content-${determineJustifyContentValue(horizontalAlignment)}`}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
color: cellTextColor ? cellTextColor : 'inherit',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
background: 'inherit',
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
}}
|
||||
readOnly={!isEditable}
|
||||
onBlur={(e) => {
|
||||
setIsEditing(false);
|
||||
if (cellValue !== e.target.textContent) {
|
||||
const value = JSON.stringify(JSON.parse(e.target.textContent.replace(/\n/g, '')));
|
||||
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
ref.current.blur();
|
||||
if (cellValue !== e.target.textContent) {
|
||||
const value = JSON.stringify(JSON.parse(e.target.textContent.replace(/\n/g, '')));
|
||||
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
setIsEditing(true);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{String(formatCellValue(cellValue))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const getOverlay = () => {
|
||||
return (
|
||||
<div
|
||||
className={`overlay-cell-table ${darkMode && 'dark-theme'}`}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: `${containerWidth}px`,
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{String(formatCellValue(cellValue, true))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const _showOverlay =
|
||||
ref?.current &&
|
||||
(ref?.current?.clientWidth < ref?.current?.children[0]?.offsetWidth ||
|
||||
ref?.current?.clientHeight < ref?.current?.children[0]?.offsetHeight);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
overlay={_showOverlay ? getOverlay() : <div></div>}
|
||||
trigger={_showOverlay && ['hover', 'focus']}
|
||||
rootClose={true}
|
||||
show={_showOverlay && hovered && !isEditing}
|
||||
>
|
||||
{!isEditable ? (
|
||||
<div
|
||||
className={`d-flex align-items-center h-100 w-100 justify-content-${determineJustifyContentValue(
|
||||
horizontalAlignment
|
||||
)}`}
|
||||
style={cellStyles}
|
||||
onMouseMove={() => {
|
||||
if (!hovered) setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHovered(false);
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
maxHeight: isMaxRowHeightAuto
|
||||
? 'fit-content'
|
||||
: maxRowHeightValue
|
||||
? `${maxRowHeightValue}px`
|
||||
: cellSize === 'condensed'
|
||||
? '39px'
|
||||
: '45px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{String(formatCellValue(cellValue))}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-100 d-flex flex-column justify-content-center position-relative">
|
||||
<div
|
||||
onMouseMove={() => {
|
||||
if (!hovered) setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
className={`${isEditing ? 'h-100 content-editing' : ''} h-100`}
|
||||
>
|
||||
{_renderString()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</OverlayTrigger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Json;
|
||||
|
|
@ -1114,10 +1114,9 @@ export const Table = React.memo(
|
|||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
top: `${items[0]?.start ?? 0}px`,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${items[0]?.start ?? 0}px)`,
|
||||
}}
|
||||
>
|
||||
{items.map((virtualRow) => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { CustomSelect } from '../CustomSelect';
|
|||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import Text from '../Text';
|
||||
import StringColumn from '../String';
|
||||
import Json from '../Json';
|
||||
|
||||
export default function generateColumnsData({
|
||||
columnProperties,
|
||||
|
|
@ -714,6 +715,27 @@ export default function generateColumnsData({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
case 'json': {
|
||||
const jsonIndentation = getResolvedValue(column?.jsonIndentation) ?? true;
|
||||
return (
|
||||
<Json
|
||||
isEditable={isEditable}
|
||||
jsonIndentation={jsonIndentation}
|
||||
darkMode={darkMode}
|
||||
handleCellValueChange={handleCellValueChange}
|
||||
cellTextColor={cellTextColor}
|
||||
horizontalAlignment={horizontalAlignment}
|
||||
cellValue={cellValue}
|
||||
column={column}
|
||||
currentState={currentState}
|
||||
containerWidth={width}
|
||||
cell={cell}
|
||||
isMaxRowHeightAuto={isMaxRowHeightAuto}
|
||||
cellSize={cellSize}
|
||||
maxRowHeightValue={maxRowHeightValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return cellValue || '';
|
||||
},
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export const Tabs = function Tabs({
|
|||
allowContainerSelect={true}
|
||||
styles={{ backgroundColor: bgColor }}
|
||||
darkMode={darkMode}
|
||||
componentType="Tabs"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { getWorkspaceId } from '@/_helpers/utils';
|
|||
import { navigate } from '@/AppBuilder/_utils/misc';
|
||||
import queryString from 'query-string';
|
||||
import { replaceEntityReferencesWithIds } from '../utils';
|
||||
import _ from 'lodash';
|
||||
|
||||
const initialState = {
|
||||
app: {},
|
||||
|
|
@ -124,10 +125,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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const initialState = {
|
|||
currentPageHandle: null,
|
||||
showWidgetDeleteConfirmation: false,
|
||||
focusedParentId: null,
|
||||
modalsOpenOnCanvas: [],
|
||||
};
|
||||
|
||||
export const createComponentsSlice = (set, get) => ({
|
||||
|
|
@ -1867,4 +1868,17 @@ export const createComponentsSlice = (set, get) => ({
|
|||
const currentPage = getCurrentPage(moduleId);
|
||||
return currentPage?.autoComputeLayout;
|
||||
},
|
||||
setModalOpenOnCanvas: (modalId, isOpen) => {
|
||||
const { modalsOpenOnCanvas } = get();
|
||||
let newModalOpenOnCanvas = [];
|
||||
|
||||
if (isOpen) {
|
||||
newModalOpenOnCanvas = [...modalsOpenOnCanvas, modalId];
|
||||
} else {
|
||||
newModalOpenOnCanvas = modalsOpenOnCanvas.filter((id) => id !== modalId);
|
||||
}
|
||||
set((state) => {
|
||||
state.modalsOpenOnCanvas = newModalOpenOnCanvas;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
|
|
|||
|
|
@ -37,4 +37,85 @@ export const createLeftSideBarSlice = (set, get) => ({
|
|||
toggleLeftSidebar(true);
|
||||
}
|
||||
},
|
||||
getComponentIdToAutoScroll: (componentId) => {
|
||||
const { getCurrentPageComponents, getAllExposedValues, modalsOpenOnCanvas } = get();
|
||||
const currentPageComponents = getCurrentPageComponents();
|
||||
|
||||
let targetComponentId = componentId;
|
||||
let current = componentId;
|
||||
const visited = new Set();
|
||||
let isInsideOpenModal = false;
|
||||
|
||||
// Bubble up to the outermost parent to find the target component
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
if (visited.has(current)) break;
|
||||
visited.add(current);
|
||||
|
||||
const parentId = currentPageComponents?.[current]?.component?.parent;
|
||||
if (!parentId) break;
|
||||
|
||||
let isComponentVisibleInParent = true;
|
||||
let nextPossibleCandidate = parentId;
|
||||
|
||||
// If the component exists inside a tab component
|
||||
const regForTabs = /-(?!\d{12}$)\d+$/; // Parent id for tabs follow the format 'id-index' and index is not UUIDv4 id segment
|
||||
if (regForTabs.test(parentId)) {
|
||||
const reg = /-(\d+)$/;
|
||||
const tabIndex = Number(parentId.match(reg)[1]); // Tab index inside which the component exists
|
||||
|
||||
const tabId = parentId.replace(regForTabs, ''); // Extract tab id from parent id
|
||||
|
||||
const { currentTab } = getAllExposedValues().components?.[tabId] || {};
|
||||
const activeTabIndex = Number(currentTab);
|
||||
|
||||
nextPossibleCandidate = tabId;
|
||||
if (tabIndex !== activeTabIndex) {
|
||||
isComponentVisibleInParent = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the component exists inside a modal component
|
||||
if (currentPageComponents?.[parentId]?.component?.component === 'Modal') {
|
||||
nextPossibleCandidate = parentId;
|
||||
if (!modalsOpenOnCanvas.includes(parentId)) {
|
||||
isComponentVisibleInParent = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the component exists inside the kanban component's modal
|
||||
if (parentId.endsWith('-modal')) {
|
||||
nextPossibleCandidate = parentId.replace(/-modal$/, ''); // Extract kanban id from parent id
|
||||
if (!modalsOpenOnCanvas.includes(parentId)) {
|
||||
isComponentVisibleInParent = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the open modal contains the component
|
||||
if (modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] === parentId) {
|
||||
isInsideOpenModal = true;
|
||||
}
|
||||
|
||||
if (!isComponentVisibleInParent) {
|
||||
targetComponentId = nextPossibleCandidate;
|
||||
}
|
||||
current = nextPossibleCandidate;
|
||||
}
|
||||
|
||||
if (modalsOpenOnCanvas.length > 0 && !isInsideOpenModal) {
|
||||
const targetId = visited.size === 1 ? modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] : current;
|
||||
const componentName = currentPageComponents?.[targetId]?.component?.name;
|
||||
|
||||
return {
|
||||
isAccessible: false,
|
||||
computedComponentId: componentName,
|
||||
isOnCanvas: visited.size === 1,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isAccessible: true,
|
||||
computedComponentId: targetComponentId,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const CustomComponent = (props) => {
|
|||
setCustomProps({ ...customPropRef.current, ...e.data.updatedObj });
|
||||
} else if (e.data.message === 'RUN_QUERY') {
|
||||
const options = {
|
||||
parameters: e.data.parameters,
|
||||
parameters: JSON.parse(e.data.parameters),
|
||||
queryName: e.data.queryName,
|
||||
};
|
||||
onEvent('onTrigger', [], options);
|
||||
|
|
|
|||
|
|
@ -436,6 +436,7 @@ export const DropdownV2 = ({
|
|||
onChange={(selectedOption, actionProps) => {
|
||||
if (actionProps.action === 'clear') {
|
||||
setInputValue(null);
|
||||
fireEvent('onSelect');
|
||||
}
|
||||
if (actionProps.action === 'select-option') {
|
||||
setInputValue(selectedOption.value);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import JSONTreeViewer from '@/_ui/JSONTreeViewer';
|
|||
import cx from 'classnames';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
function Logs({ logProps, idx }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
|
@ -52,10 +53,19 @@ function Logs({ logProps, idx }) {
|
|||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (data) => {
|
||||
const stringified = JSON.stringify(data, null, 2).replace(/\\/g, '');
|
||||
navigator.clipboard.writeText(stringified);
|
||||
return toast.success('Value copied to clipboard', { position: 'top-center' });
|
||||
};
|
||||
|
||||
const callbackActions = [
|
||||
{
|
||||
for: 'all',
|
||||
actions: [{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true }],
|
||||
actions: [
|
||||
{ name: 'Copy value', dispatchAction: copyToClipboard, icon: false },
|
||||
{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true },
|
||||
],
|
||||
enableForAllChildren: true,
|
||||
enableFor1stLevelChildren: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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} />{' '}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export const DATA_SOURCE_TYPE = {
|
|||
LOCAL: 'local',
|
||||
GLOBAL: 'global',
|
||||
STATIC: 'static',
|
||||
DEFAULT: 'default',
|
||||
};
|
||||
|
||||
export const SAMPLE_DB_KIND = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export const JSONNode = ({ data, ...restProps }) => {
|
|||
|
||||
React.useEffect(() => {
|
||||
if (typeof shouldExpandNode === 'function') {
|
||||
set(shouldExpandNode(path, data));
|
||||
set(shouldExpandNode(path, data, currentNode));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathToBeInspected]);
|
||||
|
|
@ -268,7 +268,15 @@ export const JSONNode = ({ data, ...restProps }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div style={{ fontSize: '9px', marginTop: '0px', right: '10px' }} className="d-flex position-absolute">
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: !enableCopyToClipboard ? '13px' : '0px', // Temporary fix for hover issue for copy value button. Need to remove this once inspector gets revamped.
|
||||
fontSize: '9px',
|
||||
marginTop: '0px',
|
||||
right: '10px',
|
||||
}}
|
||||
className="d-flex position-absolute"
|
||||
>
|
||||
{enableCopyToClipboard && (
|
||||
<ToolTip message={'Copy to clipboard'}>
|
||||
<span
|
||||
|
|
@ -337,6 +345,7 @@ export const JSONNode = ({ data, ...restProps }) => {
|
|||
'group-object-container': shouldDisplayIntendedBlock,
|
||||
'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array',
|
||||
})}
|
||||
id={`inspector-node-${String(currentNode).toLowerCase()}`}
|
||||
data-cy={`inspector-node-${String(currentNode).toLowerCase()}`}
|
||||
>
|
||||
{$NODEIcon && <div className="json-tree-icon-container">{$NODEIcon}</div>}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 17df343008fcedfd7b05540c9c21bebd152af086
|
||||
Subproject commit 26447ec13d91119d001319ac940264bbc39a3b54
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,4 @@ export class AppGitAbilityGuard extends AbilityGuard {
|
|||
protected getSubjectType() {
|
||||
return App;
|
||||
}
|
||||
|
||||
protected forwardAbility(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ export abstract class AbilityFactory<TActions extends string, TSubject> {
|
|||
}
|
||||
: {}),
|
||||
}));
|
||||
|
||||
if (request) {
|
||||
request.tj_user_permissions = userPermission;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export abstract class AbilityGuard implements CanActivate {
|
|||
user,
|
||||
{ moduleName: module, features },
|
||||
resourceArray,
|
||||
this.forwardAbility() ? request : undefined
|
||||
request
|
||||
);
|
||||
|
||||
if (this.forwardAbility()) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { FEATURE_KEY } from './constants';
|
|||
import { AbilityDecorator as Ability, AppAbility } from '@modules/app/decorators/ability.decorator';
|
||||
import { AppDecorator as App } from '@modules/app/decorators/app.decorator';
|
||||
import { App as AppEntity } from '@entities/app.entity';
|
||||
import { AppAuthGuard } from './guards/app-auth.guard';
|
||||
import { AppAuthGuard } from '@ee/apps/guards/app-auth.guard';
|
||||
import { ValidSlugGuard } from './guards/valid-slug.guard';
|
||||
import { ValidAppGuard } from './guards/valid-app.guard';
|
||||
import { IAppsController } from './interfaces/IController';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,20 @@ 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(
|
||||
{
|
||||
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.organizationUsersRepository.createOne(userDetails, defaultOrganization, true, manager);
|
||||
organizationDetails = defaultOrganization;
|
||||
} else if (userDetails) {
|
||||
// Finding organization to be loaded
|
||||
|
|
@ -213,7 +214,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 },
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -196,42 +194,28 @@ export class AuthUtilService implements IAuthUtilService {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -94,11 +94,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) {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@ export class FeatureAbilityGuard extends AbilityGuard {
|
|||
return App;
|
||||
}
|
||||
|
||||
protected forwardAbility(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected getResource(): ResourceDetails | ResourceDetails[] {
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,8 +12,4 @@ export class FeatureAbilityGuard extends AbilityGuard {
|
|||
protected getAbilityFactory() {
|
||||
return FeatureAbilityFactory;
|
||||
}
|
||||
|
||||
protected forwardAbility(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ 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';
|
||||
|
||||
import { GroupPermissions } from '@entities/group_permissions.entity';
|
||||
import { GroupUsers } from '@entities/group_users.entity';
|
||||
@Injectable()
|
||||
export class OrganizationUsersUtilService implements IOrganizationUsersUtilService {
|
||||
constructor(
|
||||
|
|
@ -147,8 +148,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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,8 +12,4 @@ export class FeatureAbilityGuard extends AbilityGuard {
|
|||
protected getSubjectType() {
|
||||
return InternalTable;
|
||||
}
|
||||
|
||||
protected forwardAbility(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@ export class FeatureAbilityGuard extends AbilityGuard {
|
|||
return App;
|
||||
}
|
||||
|
||||
protected forwardAbility(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected getResource(): ResourceDetails | ResourceDetails[] {
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export class WorkflowsModule {
|
|||
UserRepository,
|
||||
DataSourcesRepository,
|
||||
DataQueryRepository,
|
||||
DataSourcesRepository,
|
||||
OrganizationConstantRepository,
|
||||
VersionRepository,
|
||||
AppsService,
|
||||
|
|
|
|||
Loading…
Reference in a new issue