mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
Merge branch 'modularisation/v3' into modularisation/modalV2
This commit is contained in:
commit
c16fcc645c
47 changed files with 674 additions and 367 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit bef751a47e40ad8ea052f278edcf0993363c1b93
|
||||
Subproject commit 7e6cf7d2e9c2dfaa4ca3d1308406cfed3c95715b
|
||||
|
|
@ -131,9 +131,13 @@ class AppComponent extends React.Component {
|
|||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
render() {
|
||||
const { updateAvailable, darkMode, isEditorOrViewer } = this.state;
|
||||
const mergedProps = {
|
||||
...this.props,
|
||||
switchDarkMode: this.switchDarkMode,
|
||||
darkMode: darkMode,
|
||||
};
|
||||
let toastOptions = {
|
||||
style: {
|
||||
wordBreak: 'break-all',
|
||||
|
|
@ -256,7 +260,7 @@ class AppComponent extends React.Component {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...this.props} />}></Route>
|
||||
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...mergedProps} />}></Route>
|
||||
<Route path="settings/*" element={<InstanceSettings {...this.props} />}></Route>
|
||||
<Route path="/:workspaceId/settings/*" element={<Settings {...this.props} />}></Route>
|
||||
|
||||
|
|
@ -270,7 +274,7 @@ class AppComponent extends React.Component {
|
|||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
{getDataSourcesRoutes(this.props)}
|
||||
{getDataSourcesRoutes(mergedProps)}
|
||||
<Route
|
||||
exact
|
||||
path="/applications/:id/versions/:versionId/:pageHandle?"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const ConfigHandle = ({
|
|||
customClassName = '',
|
||||
showHandle,
|
||||
componentType,
|
||||
visibility,
|
||||
}) => {
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const componentName = useStore((state) => state.getComponentDefinition(id)?.component?.name || '', shallow);
|
||||
|
|
@ -29,14 +30,26 @@ export const ConfigHandle = ({
|
|||
|
||||
const setComponentToInspect = useStore((state) => state.setComponentToInspect);
|
||||
const isModal = componentType === 'Modal' || componentType === 'ModalV2';
|
||||
const _showHandle = useStore((state) => {
|
||||
const isWidgetHovered = state.getHoveredComponentForGrid() === id || state.hoveredComponentBoundaryId === id;
|
||||
const anyComponentHovered = state.getHoveredComponentForGrid() !== '' || state.hoveredComponentBoundaryId !== '';
|
||||
// If one component is hovered and one is selected, show the handle for the hovered component
|
||||
return (
|
||||
isWidgetHovered ||
|
||||
(showHandle &&
|
||||
(!isMultipleComponentsSelected || (isModal && isModalOpen)) &&
|
||||
!anyComponentHovered)
|
||||
);
|
||||
}, shallow);
|
||||
let height = visibility === false ? 10 : widgetHeight;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`config-handle ${customClassName}`}
|
||||
widget-id={id}
|
||||
style={{
|
||||
top: position === 'top' ? '-20px' : widgetTop + widgetHeight - (widgetTop < 10 ? 15 : 10),
|
||||
visibility: showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) ? 'visible' : 'hidden',
|
||||
top: position === 'top' ? '-20px' : widgetTop + height - (widgetTop < 10 ? 15 : 10),
|
||||
visibility: _showHandle ? 'visible' : 'hidden',
|
||||
left: '-1px',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
|
|
|
|||
|
|
@ -65,9 +65,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-editor-canvas .widget-target:hover > .config-handle {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ export const Container = React.memo(
|
|||
|
||||
const [{ isOverCurrent }, drop] = useDrop({
|
||||
accept: 'box',
|
||||
hover: (item) => {
|
||||
item.canvasRef = realCanvasRef?.current;
|
||||
item.canvasId = id;
|
||||
item.canvasWidth = getContainerCanvasWidth();
|
||||
},
|
||||
drop: async ({ componentType }, monitor) => {
|
||||
const didDrop = monitor.didDrop();
|
||||
if (didDrop) return;
|
||||
|
|
@ -89,14 +94,15 @@ export const Container = React.memo(
|
|||
function getContainerCanvasWidth() {
|
||||
if (canvasWidth !== undefined) {
|
||||
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
|
||||
return canvasWidth;
|
||||
if (id === 'canvas') return canvasWidth;
|
||||
return canvasWidth - 2;
|
||||
}
|
||||
return realCanvasRef?.current?.offsetWidth;
|
||||
}
|
||||
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
|
||||
|
||||
useEffect(() => {
|
||||
useGridStore.getState().actions.setSubContainerWidths(id, (getContainerCanvasWidth() - 2) / NO_OF_GRIDS);
|
||||
useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [canvasWidth, listViewMode, columns]);
|
||||
|
||||
|
|
@ -137,8 +143,7 @@ export const Container = React.memo(
|
|||
}}
|
||||
style={{
|
||||
height: id === 'canvas' ? `${canvasHeight}` : '100%',
|
||||
// backgroundSize: '25.3953px 10px',
|
||||
backgroundSize: `${gridWidth}px 10px`,
|
||||
backgroundSize: `${gridWidth}px ${10}px`,
|
||||
backgroundColor:
|
||||
currentMode === 'view'
|
||||
? computeViewerBackgroundColor(darkMode, canvasBgColor)
|
||||
|
|
@ -169,6 +174,7 @@ export const Container = React.memo(
|
|||
data-parentId={id}
|
||||
canvas-height={canvasHeight}
|
||||
onClick={handleCanvasClick}
|
||||
component-type={componentType}
|
||||
>
|
||||
<div
|
||||
className={cx('container-fluid rm-container p-0', {
|
||||
|
|
|
|||
|
|
@ -186,5 +186,28 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.moveable-guideline {
|
||||
background: #97AEFC !important;
|
||||
opacity: 0.8;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* */
|
||||
.moveable-guideline.moveable-horizontal {
|
||||
height: 1px !important;
|
||||
width: 100% !important;
|
||||
background: #97AEFC !important;
|
||||
left: 0 !important;
|
||||
|
||||
}
|
||||
|
||||
.moveable-guideline.moveable-vertical {
|
||||
width: 1px !important;
|
||||
height: 100% !important;
|
||||
background: #97AEFC !important;
|
||||
top: 0 !important;
|
||||
|
||||
}
|
||||
|
||||
.moveable-guideline-group {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ import {
|
|||
hasParentWithClass,
|
||||
getPositionForGroupDrag,
|
||||
adjustWidth,
|
||||
hideGridLines,
|
||||
showGridLines,
|
||||
} from './gridUtils';
|
||||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
import { resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
|
@ -30,6 +32,7 @@ const RESIZABLE_CONFIG = {
|
|||
edge: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
|
||||
renderDirections: ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'],
|
||||
};
|
||||
export const GRID_HEIGHT = 10;
|
||||
|
||||
export default function Grid({ gridWidth, currentLayout }) {
|
||||
const lastDraggedEventsRef = useRef(null);
|
||||
|
|
@ -53,6 +56,48 @@ 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 resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
|
||||
const [dragParentId, setDragParentId] = useState(null);
|
||||
const [elementGuidelines, setElementGuidelines] = useState([]);
|
||||
const componentsSnappedTo = useRef(null);
|
||||
const prevDragParentId = useRef(null);
|
||||
const newDragParentId = useRef(null);
|
||||
const [isGroupDragging, setIsGroupDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedSet = new Set(selectedComponents);
|
||||
const draggingOrResizingId = draggingComponentId || resizingComponentId;
|
||||
const isGrouped = findHighestLevelofSelection().length > 1;
|
||||
const firstSelectedParent =
|
||||
selectedComponents.length > 0 ? boxList.find((b) => b.id === selectedComponents[0])?.parent : null;
|
||||
const selectedParent = dragParentId || firstSelectedParent;
|
||||
|
||||
const guidelines = boxList
|
||||
.filter((box) => {
|
||||
const isVisible =
|
||||
getResolvedValue(box?.component?.definition?.properties?.visibility?.value) ||
|
||||
getResolvedValue(box?.component?.definition?.styles?.visibility?.value);
|
||||
|
||||
// Early return for non-visible elements
|
||||
if (!isVisible) return false;
|
||||
|
||||
if (isGrouped) {
|
||||
// If component is selected, don't show its guidelines
|
||||
if (selectedSet.has(box.id)) return false;
|
||||
return selectedParent ? box.parent === selectedParent : !box.parent;
|
||||
}
|
||||
|
||||
if (draggingOrResizingId) {
|
||||
if (box.id === draggingOrResizingId) return false;
|
||||
return dragParentId ? box.parent === dragParentId : !box.parent;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((box) => `.ele-${box.id}`);
|
||||
setElementGuidelines(guidelines);
|
||||
}, [boxList, dragParentId, draggingComponentId, resizingComponentId, selectedComponents, getResolvedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setBoxList(
|
||||
|
|
@ -96,7 +141,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
boxList.forEach(({ id, height, width, x, y, gw }) => {
|
||||
const _canvasWidth = gw ? gw * NO_OF_GRIDS : canvasWidth;
|
||||
let newWidth = Math.round((width * NO_OF_GRIDS) / _canvasWidth);
|
||||
y = Math.round(y / 10) * 10;
|
||||
y = Math.round(y / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
gw = gw ? gw : gridWidth;
|
||||
|
||||
const parent = transformedBoxes[id]?.component?.parent;
|
||||
|
|
@ -119,7 +164,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
setComponentLayout({
|
||||
[id]: {
|
||||
height: height ? height : 10,
|
||||
height: height ? height : GRID_HEIGHT,
|
||||
width: newWidth ? newWidth : 1,
|
||||
top: y,
|
||||
left: Math.round(x / gw),
|
||||
|
|
@ -321,7 +366,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
|
||||
// Round y position
|
||||
y = Math.max(0, Math.round(y / 10) * 10);
|
||||
y = Math.max(0, Math.round(y / GRID_HEIGHT) * GRID_HEIGHT);
|
||||
// Adjust height for certain parent components
|
||||
if (parent) {
|
||||
const parentElem = document.getElementById(`canvas-${parent}`);
|
||||
|
|
@ -356,17 +401,16 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
);
|
||||
|
||||
// Add event listeners for config handle visibility when hovering over widget boundary
|
||||
// This is needed even though we have hovered widget state because when hovered on boundary,
|
||||
// the hovered widget state is empty, hence created a separate state for boundary
|
||||
React.useEffect(() => {
|
||||
const moveableBox = document.querySelector(`.moveable-control-box`);
|
||||
const showConfigHandle = (e) => {
|
||||
const targetId = e.target.offsetParent.getAttribute('target-id');
|
||||
const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
|
||||
configHandle.classList.add('config-handle-visible');
|
||||
useStore.getState().setHoveredComponentBoundaryId(targetId);
|
||||
};
|
||||
const hideConfigHandle = (e) => {
|
||||
const targetId = e.target.offsetParent.getAttribute('target-id');
|
||||
const configHandle = document.querySelector(`.config-handle[widget-id="${targetId}"]`);
|
||||
configHandle.classList.remove('config-handle-visible');
|
||||
const hideConfigHandle = () => {
|
||||
useStore.getState().setHoveredComponentBoundaryId('');
|
||||
};
|
||||
if (moveableBox) {
|
||||
moveableBox.addEventListener('mouseover', showConfigHandle);
|
||||
|
|
@ -378,49 +422,10 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const handleDragGridLinesVisibility = (e, events = []) => {
|
||||
const { clientX, clientY } = e;
|
||||
if (!document.elementFromPoint(clientX, clientY)) return;
|
||||
|
||||
const targetElems = document.elementsFromPoint(clientX, clientY);
|
||||
const draggedOverElements = targetElems.filter(
|
||||
(ele) =>
|
||||
!events.some((event) => event.target.id === ele.id) &&
|
||||
(ele.classList.contains('target') || ele.classList.contains('real-canvas'))
|
||||
);
|
||||
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
|
||||
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
|
||||
const appCanvas = document.getElementById('real-canvas');
|
||||
|
||||
// Show grid line for main canvas
|
||||
draggedOverContainer?.classList.remove('hide-grid');
|
||||
draggedOverContainer?.classList.add('show-grid');
|
||||
|
||||
// Remove 'show-grid' class from all sub-canvases
|
||||
const canvasElms = document.getElementsByClassName('sub-canvas');
|
||||
Array.from(canvasElms).forEach((element) => {
|
||||
element.classList.remove('show-grid');
|
||||
element.classList.add('hide-grid');
|
||||
});
|
||||
|
||||
// Determine the potential new parent
|
||||
const parentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
|
||||
|
||||
// Show grid for the appropriate canvas
|
||||
if (parentId) {
|
||||
const newParentCanvas = document.getElementById('canvas-' + parentId);
|
||||
if (newParentCanvas) {
|
||||
appCanvas?.classList?.remove('show-grid');
|
||||
newParentCanvas?.classList.remove('hide-grid');
|
||||
newParentCanvas?.classList.add('show-grid');
|
||||
}
|
||||
}
|
||||
|
||||
useGridStore.getState().actions.setDragTarget(parentId);
|
||||
};
|
||||
|
||||
const handleDragGroupEnd = (e) => {
|
||||
try {
|
||||
hideGridLines();
|
||||
setIsGroupDragging(false);
|
||||
const { events, clientX, clientY } = e;
|
||||
const initialParent = events[0].target.closest('.real-canvas');
|
||||
// Get potential new parent using same logic as onDragEnd
|
||||
|
|
@ -479,7 +484,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
// Apply transform to return to original position
|
||||
ev.target.style.transform = `translate(${Math.round(_left / _gridWidth) * _gridWidth}px, ${
|
||||
Math.round(_top / 10) * 10
|
||||
Math.round(_top / GRID_HEIGHT) * GRID_HEIGHT
|
||||
}px)`;
|
||||
}
|
||||
});
|
||||
|
|
@ -516,7 +521,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
|
||||
// Apply grid snapping and bounds
|
||||
const snappedX = Math.round(posX / _gridWidth) * _gridWidth;
|
||||
const snappedY = Math.round(posY / 10) * 10;
|
||||
const snappedY = Math.round(posY / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
ev.target.style.transform = `translate(${snappedX}px, ${snappedY}px)`;
|
||||
return {
|
||||
|
|
@ -533,6 +538,18 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const components = Array.from(document.querySelectorAll('.active-target')).filter(
|
||||
(component) => !selectedComponents.includes(component.getAttribute('widgetid'))
|
||||
);
|
||||
const draggingOrResizing = draggingComponentId || resizingComponentId;
|
||||
if (!draggingOrResizing && components.length > 0) {
|
||||
for (const component of components) {
|
||||
component?.classList?.remove('active-target');
|
||||
}
|
||||
}
|
||||
}, [draggingComponentId, resizingComponentId, isGroupDragging, selectedComponents]);
|
||||
|
||||
if (mode !== 'edit') return null;
|
||||
|
||||
return (
|
||||
|
|
@ -559,7 +576,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth;
|
||||
if (currentWidget.component?.parent) {
|
||||
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.add('show-grid');
|
||||
useGridStore.getState().actions.setDragTarget(currentWidget.component?.parent);
|
||||
setDragParentId(currentWidget.component?.parent);
|
||||
} else {
|
||||
document.getElementById('real-canvas').classList.add('show-grid');
|
||||
}
|
||||
|
|
@ -586,9 +603,6 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const maxLeft = containerWidth - e.target.clientWidth;
|
||||
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
|
||||
const maxHeightHit = transformY < 0 || transformY >= maxY;
|
||||
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
|
||||
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
|
||||
|
||||
if (!maxWidthHit || e.width < e.target.clientWidth) {
|
||||
e.target.style.width = `${e.width}px`;
|
||||
}
|
||||
|
|
@ -614,12 +628,12 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
// When clicked on widget boundary/resizer, select the component
|
||||
setSelectedComponents([e.target.id]);
|
||||
}
|
||||
|
||||
showGridLines();
|
||||
if (!isComponentVisible(e.target.id)) {
|
||||
return false;
|
||||
}
|
||||
useGridStore.getState().actions.setResizingComponentId(e.target.id);
|
||||
e.setMin([gridWidth, 10]);
|
||||
e.setMin([gridWidth, GRID_HEIGHT]);
|
||||
}}
|
||||
onResizeEnd={(e) => {
|
||||
try {
|
||||
|
|
@ -631,7 +645,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid');
|
||||
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 / 10) * 10;
|
||||
const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
const currentWidth = currentWidget.width * _gridWidth;
|
||||
const diffWidth = e.lastEvent?.width - currentWidth;
|
||||
|
|
@ -656,19 +670,17 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const maxLeft = containerWidth - e.target.clientWidth;
|
||||
const maxWidthHit = transformX < 0 || transformX >= maxLeft;
|
||||
const maxHeightHit = transformY < 0 || transformY >= maxY;
|
||||
transformY = transformY < 0 ? 0 : transformY > maxY ? maxY : transformY;
|
||||
transformX = transformX < 0 ? 0 : transformX > maxLeft ? maxLeft : transformX;
|
||||
|
||||
const roundedTransformY = Math.round(transformY / 10) * 10;
|
||||
transformY = transformY % 10 === 5 ? roundedTransformY - 10 : roundedTransformY;
|
||||
const roundedTransformY = Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
transformY = transformY % GRID_HEIGHT === 5 ? roundedTransformY - GRID_HEIGHT : roundedTransformY;
|
||||
e.target.style.transform = `translate(${Math.round(transformX / _gridWidth) * _gridWidth}px, ${
|
||||
Math.round(transformY / 10) * 10
|
||||
Math.round(transformY / GRID_HEIGHT) * GRID_HEIGHT
|
||||
}px)`;
|
||||
if (!maxWidthHit || e.width < e.target.clientWidth) {
|
||||
e.target.style.width = `${Math.round(e.lastEvent.width / _gridWidth) * _gridWidth}px`;
|
||||
}
|
||||
if (!maxHeightHit || e.height < e.target.clientHeight) {
|
||||
e.target.style.height = `${Math.round(e.lastEvent.height / 10) * 10}px`;
|
||||
e.target.style.height = `${Math.round(e.lastEvent.height / GRID_HEIGHT) * GRID_HEIGHT}px`;
|
||||
}
|
||||
const resizeData = {
|
||||
id: e.target.id,
|
||||
|
|
@ -684,12 +696,11 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
} catch (error) {
|
||||
console.error('ResizeEnd error ->', error);
|
||||
}
|
||||
useGridStore.getState().actions.setDragTarget();
|
||||
setDragParentId(null);
|
||||
toggleCanvasUpdater();
|
||||
}}
|
||||
onResizeGroupStart={({ events }) => {
|
||||
const parentElm = events[0].target.closest('.real-canvas');
|
||||
parentElm.classList.add('show-grid');
|
||||
showGridLines();
|
||||
}}
|
||||
onResizeGroup={({ events }) => {
|
||||
const parentElm = events[0].target.closest('.real-canvas');
|
||||
|
|
@ -712,8 +723,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const { events } = e;
|
||||
const newBoxs = [];
|
||||
|
||||
const parentElm = events[0].target.closest('.real-canvas');
|
||||
parentElm.classList.remove('show-grid');
|
||||
hideGridLines();
|
||||
|
||||
// TODO: Logic needs to be relooked post go live P2
|
||||
groupResizeDataRef.current.forEach((ev) => {
|
||||
|
|
@ -724,9 +734,9 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
let width = Math.round(ev.width / _gridWidth) * _gridWidth;
|
||||
width = width < _gridWidth ? _gridWidth : width;
|
||||
let posX = Math.round(ev.drag.translate[0] / _gridWidth) * _gridWidth;
|
||||
let posY = Math.round(ev.drag.translate[1] / 10) * 10;
|
||||
let height = Math.round(ev.height / 10) * 10;
|
||||
height = height < 10 ? 10 : height;
|
||||
let posY = Math.round(ev.drag.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
let height = Math.round(ev.height / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
|
||||
|
||||
ev.target.style.width = `${width}px`;
|
||||
ev.target.style.height = `${height}px`;
|
||||
|
|
@ -754,7 +764,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
let posX = currentWidget?.layouts[currentLayout].left * _gridWidth;
|
||||
let posY = currentWidget?.layouts[currentLayout].top;
|
||||
let height = currentWidget?.layouts[currentLayout].height;
|
||||
height = height < 10 ? 10 : height;
|
||||
height = height < GRID_HEIGHT ? GRID_HEIGHT : height;
|
||||
ev.target.style.width = `${width}px`;
|
||||
ev.target.style.height = `${height}px`;
|
||||
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
|
||||
|
|
@ -769,6 +779,11 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}}
|
||||
checkInput
|
||||
onDragStart={(e) => {
|
||||
// This is to prevent parent component from being dragged and the stop the propagation of the event
|
||||
if (getHoveredComponentForGrid() !== e.target.id) {
|
||||
return false;
|
||||
}
|
||||
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
|
||||
|
|
@ -811,10 +826,6 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
// This is to prevent parent component from being dragged and the stop the propagation of the event
|
||||
if (getHoveredComponentForGrid() !== e.target.id) {
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
try {
|
||||
|
|
@ -822,6 +833,9 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
useGridStore.getState().actions.setDraggingComponentId(null);
|
||||
isDraggingRef.current = false;
|
||||
}
|
||||
prevDragParentId.current = null;
|
||||
newDragParentId.current = null;
|
||||
setDragParentId(null);
|
||||
|
||||
if (!e.lastEvent) return;
|
||||
|
||||
|
|
@ -881,18 +895,32 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
element.classList.add('hide-grid');
|
||||
});
|
||||
document.getElementById('real-canvas')?.classList.remove('show-grid');
|
||||
hideGridLines();
|
||||
toggleCanvasUpdater();
|
||||
}}
|
||||
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);
|
||||
showGridLines();
|
||||
isDraggingRef.current = true;
|
||||
}
|
||||
const currentWidget = boxList.find((box) => box.id === e.target.id);
|
||||
const currentParentId =
|
||||
currentWidget?.component?.parent === null ? 'canvas' : currentWidget?.component?.parent;
|
||||
const _gridWidth = useGridStore.getState().subContainerWidths[dragParentId] || gridWidth;
|
||||
const _dragParentId = newDragParentId.current === null ? 'canvas' : newDragParentId.current;
|
||||
|
||||
let top = e.translate[1];
|
||||
let left = e.translate[0];
|
||||
// Snap to grid
|
||||
let left = Math.round(e.translate[0] / _gridWidth) * _gridWidth;
|
||||
let top = Math.round(e.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
// This logic is to handle the case when the dragged element is over a new canvas
|
||||
if (_dragParentId !== currentParentId) {
|
||||
left = e.translate[0];
|
||||
top = e.translate[1];
|
||||
}
|
||||
|
||||
// Special case for Modal
|
||||
const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent;
|
||||
const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId;
|
||||
|
|
@ -918,7 +946,6 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
bottom: modalRect.height + (modalRect.top - mainRect.top),
|
||||
left: modalRect.left - mainRect.left,
|
||||
};
|
||||
|
||||
setCanvasBounds({ ...relativePosition });
|
||||
}
|
||||
|
||||
|
|
@ -928,8 +955,32 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
|
||||
);
|
||||
|
||||
handleDragGridLinesVisibility(e, [{ target: e.target }]);
|
||||
// This block is to show grid lines on the canvas when the dragged element is over a new canvas
|
||||
if (document.elementFromPoint(e.clientX, e.clientY)) {
|
||||
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
|
||||
const draggedOverElements = targetElems.filter(
|
||||
(ele) =>
|
||||
(ele.id !== e.target.id && ele.classList.contains('target')) || ele.classList.contains('real-canvas')
|
||||
);
|
||||
const draggedOverElem = draggedOverElements.find((ele) => ele.classList.contains('target'));
|
||||
const draggedOverContainer = draggedOverElements.find((ele) => ele.classList.contains('real-canvas'));
|
||||
|
||||
// Determine potential new parent
|
||||
let newParentId = draggedOverContainer?.getAttribute('data-parentId') || draggedOverElem?.id;
|
||||
|
||||
if (newParentId === e.target.id) {
|
||||
newParentId = boxList.find((box) => box.id === e.target.id)?.component?.parent;
|
||||
} else if (parentComponent?.component?.component === 'Modal') {
|
||||
// Never update parentId for Modal
|
||||
newParentId = parentComponent?.id;
|
||||
}
|
||||
|
||||
if (newParentId !== prevDragParentId.current) {
|
||||
setDragParentId(newParentId === 'canvas' ? null : newParentId);
|
||||
newDragParentId.current = newParentId === 'canvas' ? null : newParentId;
|
||||
prevDragParentId.current = newParentId;
|
||||
}
|
||||
}
|
||||
// Postion ghost element exactly as same at dragged element
|
||||
if (document.getElementById(`moveable-drag-ghost`)) {
|
||||
document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`;
|
||||
|
|
@ -944,31 +995,26 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
parentElm?.classList?.add('show-grid');
|
||||
}
|
||||
|
||||
handleDragGridLinesVisibility(ev, events);
|
||||
|
||||
events.forEach((ev) => {
|
||||
let left = ev.translate[0];
|
||||
let top = ev.translate[1];
|
||||
const currentWidget = boxList.find(({ id }) => id === ev.target.id);
|
||||
const _gridWidth =
|
||||
useGridStore.getState().subContainerWidths?.[currentWidget?.component?.parent] || gridWidth;
|
||||
|
||||
let left = Math.round(ev.translate[0] / _gridWidth) * _gridWidth;
|
||||
let top = Math.round(ev.translate[1] / GRID_HEIGHT) * GRID_HEIGHT;
|
||||
|
||||
ev.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
});
|
||||
updateNewPosition(events);
|
||||
}}
|
||||
onDragGroupStart={({ events }) => {
|
||||
const parentElm = events[0]?.target?.closest('.real-canvas');
|
||||
parentElm?.classList?.add('show-grid');
|
||||
showGridLines();
|
||||
setIsGroupDragging(true);
|
||||
}}
|
||||
onDragGroupEnd={(e) => {
|
||||
handleDragGroupEnd(e);
|
||||
toggleCanvasUpdater();
|
||||
}}
|
||||
//snap settgins
|
||||
snappable={true}
|
||||
snapThreshold={10}
|
||||
isDisplaySnapDigit={false}
|
||||
bounds={canvasBounds}
|
||||
displayAroundControls={true}
|
||||
controlPadding={20}
|
||||
onClickGroup={(e) => {
|
||||
const targetId =
|
||||
e.inputEvent.target.id || e.inputEvent.target.closest('.moveable-box')?.getAttribute('widgetid');
|
||||
|
|
@ -984,6 +1030,42 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
}
|
||||
}}
|
||||
//snap settgins
|
||||
snappable={true}
|
||||
snapGap={false}
|
||||
isDisplaySnapDigit={false}
|
||||
snapThreshold={GRID_HEIGHT}
|
||||
// Guidelines configuration
|
||||
elementGuidelines={elementGuidelines}
|
||||
snapDirections={{
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
center: false,
|
||||
middle: false,
|
||||
}}
|
||||
elementSnapDirections={{
|
||||
top: true,
|
||||
left: true,
|
||||
bottom: true,
|
||||
right: true,
|
||||
center: false,
|
||||
middle: false,
|
||||
}}
|
||||
onSnap={(e) => {
|
||||
const components = e.elements;
|
||||
if (isArray(componentsSnappedTo.current)) {
|
||||
for (const component of componentsSnappedTo.current) {
|
||||
component?.element?.classList?.remove('active-target');
|
||||
}
|
||||
}
|
||||
componentsSnappedTo.current = components;
|
||||
for (const component of components) {
|
||||
component.element.classList.add('active-target');
|
||||
}
|
||||
}}
|
||||
snapGridAll={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -392,3 +392,25 @@ export function hasParentWithClass(child, className) {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function showGridLines() {
|
||||
var canvasElms = document.getElementsByClassName('sub-canvas');
|
||||
var elementsArray = Array.from(canvasElms);
|
||||
elementsArray.forEach(function (element) {
|
||||
element.classList.remove('hide-grid');
|
||||
element.classList.add('show-grid');
|
||||
});
|
||||
document.getElementById('real-canvas')?.classList.remove('hide-grid');
|
||||
document.getElementById('real-canvas')?.classList.add('show-grid');
|
||||
}
|
||||
|
||||
export function hideGridLines() {
|
||||
var canvasElms = document.getElementsByClassName('sub-canvas');
|
||||
var elementsArray = Array.from(canvasElms);
|
||||
elementsArray.forEach(function (element) {
|
||||
element.classList.remove('show-grid');
|
||||
element.classList.add('hide-grid');
|
||||
});
|
||||
document.getElementById('real-canvas')?.classList.remove('show-grid');
|
||||
document.getElementById('real-canvas')?.classList.add('hide-grid');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const WidgetWrapper = memo(
|
|||
widgetHeight={layoutData.height}
|
||||
showHandle={isWidgetActive}
|
||||
componentType={componentType}
|
||||
visibility={visibility}
|
||||
/>
|
||||
)}
|
||||
<RenderWidget
|
||||
|
|
|
|||
|
|
@ -256,7 +256,6 @@ export const copyComponents = ({ isCut = false, isCloning = false }) => {
|
|||
const parentComponentId = isChildOfTabsOrCalendar(selectedComponent, allComponents)
|
||||
? selectedComponent.component.parent.split('-').slice(0, -1).join('-')
|
||||
: selectedComponent?.component?.parent;
|
||||
|
||||
if (parentComponentId) {
|
||||
// Check if the parent component is also selected
|
||||
const isParentSelected = selectedComponents.some((comp) => comp.id === parentComponentId);
|
||||
|
|
@ -491,11 +490,14 @@ export function pasteComponents(targetParentId, copiedComponentObj) {
|
|||
// Prevent pasting if the parent subcontainer was deleted during a cut operation
|
||||
if (
|
||||
targetParentId &&
|
||||
// Check if targetParentId is deleted from the components
|
||||
!Object.keys(components).find(
|
||||
(key) =>
|
||||
targetParentId === key ||
|
||||
(components?.[key]?.component.component === 'Tabs' &&
|
||||
targetParentId?.split('-')?.slice(0, -1)?.join('-') === key)
|
||||
targetParentId?.split('-')?.slice(0, -1)?.join('-') === key) ||
|
||||
(['Container', 'Form', 'Modal'].includes(components?.[key]?.component.component) &&
|
||||
['header', 'footer'].some((section) => targetParentId.includes(section)))
|
||||
)
|
||||
) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -45,15 +45,15 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
|
|||
const queryName = selectedQuery?.name ?? '';
|
||||
const sourcecomponentName = selectedDataSource?.kind?.charAt(0).toUpperCase() + selectedDataSource?.kind?.slice(1);
|
||||
|
||||
const ElementToRender = selectedDataSource?.pluginId ? source : allSources[sourcecomponentName];
|
||||
const ElementToRender = selectedDataSource?.plugin_id ? source : allSources[sourcecomponentName];
|
||||
const defaultOptions = useRef({});
|
||||
|
||||
const isFreezed = useStore((state) => state.getShouldFreeze());
|
||||
|
||||
useEffect(() => {
|
||||
setDataSourceMeta(
|
||||
selectedQuery?.pluginId
|
||||
? selectedQuery?.manifestFile?.data?.source
|
||||
selectedQuery?.plugin_id
|
||||
? selectedQuery?.manifest_file?.data?.source
|
||||
: DataSourceTypes.find((source) => source.kind === selectedQuery?.kind)
|
||||
);
|
||||
setSelectedQueryId(selectedQuery?.id);
|
||||
|
|
@ -188,7 +188,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
|
|||
<ElementToRender
|
||||
renderCopilot={renderCopilot}
|
||||
key={selectedQuery?.id}
|
||||
pluginSchema={selectedDataSource?.plugin?.operationsFile?.data}
|
||||
pluginSchema={selectedDataSource?.plugin?.operations_file?.data}
|
||||
selectedDataSource={selectedDataSource}
|
||||
options={selectedQuery?.options}
|
||||
optionsChanged={optionsChanged}
|
||||
|
|
@ -281,7 +281,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
|
|||
const isSampleDb = selectedDataSource?.type === DATA_SOURCE_TYPE.SAMPLE;
|
||||
const docLink = isSampleDb
|
||||
? 'https://docs.tooljet.com/docs/data-sources/sample-data-sources'
|
||||
: selectedDataSource?.pluginId && selectedDataSource.pluginId.trim() !== ''
|
||||
: selectedDataSource?.plugin_id && selectedDataSource.plugin_id.trim() !== ''
|
||||
? `https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/`
|
||||
: `https://docs.tooljet.com/docs/data-sources/${selectedDataSource?.kind}`;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ import React, { useEffect } from 'react';
|
|||
import { WidgetBox } from '../WidgetBox';
|
||||
import { useDrag, useDragLayer } from 'react-dnd';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { snapToGrid } from '@/AppBuilder/AppCanvas/appCanvasUtils';
|
||||
import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants';
|
||||
|
||||
export const DragLayer = ({ index, component }) => {
|
||||
const [{ isDragging }, drag, preview] = useDrag(
|
||||
() => ({
|
||||
type: 'box',
|
||||
item: { componentType: component.component },
|
||||
item: { componentType: component.component, component },
|
||||
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
|
||||
}),
|
||||
[component.component]
|
||||
|
|
@ -18,7 +20,6 @@ export const DragLayer = ({ index, component }) => {
|
|||
}, []);
|
||||
|
||||
const size = component.defaultSize || { width: 30, height: 40 };
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDragging && <CustomDragLayer size={size} />}
|
||||
|
|
@ -30,32 +31,39 @@ export const DragLayer = ({ index, component }) => {
|
|||
};
|
||||
|
||||
const CustomDragLayer = ({ size }) => {
|
||||
const { currentOffset } = useDragLayer((monitor) => ({
|
||||
const { currentOffset, item } = useDragLayer((monitor) => ({
|
||||
currentOffset: monitor.getSourceClientOffset(),
|
||||
item: monitor.getItem(),
|
||||
}));
|
||||
|
||||
if (!currentOffset) return null;
|
||||
|
||||
const canvasWidth = document.getElementsByClassName('real-canvas')[0]?.getBoundingClientRect()?.width;
|
||||
|
||||
const canvasWidth = item?.canvasWidth;
|
||||
const canvasBounds = item?.canvasRef?.getBoundingClientRect();
|
||||
const height = size.height;
|
||||
const width = (canvasWidth * size.width) / 43;
|
||||
|
||||
const width = (canvasWidth * size.width) / NO_OF_GRIDS;
|
||||
|
||||
// Calculate position relative to the current canvas (parent or child)
|
||||
const left = currentOffset.x - (canvasBounds?.left || 0);
|
||||
const top = currentOffset.y - (canvasBounds?.top || 0);
|
||||
|
||||
const [x, y] = snapToGrid(canvasWidth, left, top);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
zIndex: -1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 1000,
|
||||
left: canvasBounds?.left || 0,
|
||||
top: canvasBounds?.top || 0,
|
||||
height: `${height}px`,
|
||||
width: `${width}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
|
||||
transform: `translate(${x}px, ${y}px)`,
|
||||
background: '#D9E2FC',
|
||||
opacity: '0.7',
|
||||
height: '100%',
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
|
|||
if (!Array.isArray(optionsValue)) {
|
||||
optionsValue = Object.values(optionsValue);
|
||||
}
|
||||
const valuesToResolve = ['label', 'value'];
|
||||
let options = [];
|
||||
|
||||
if (isDynamicOptionsEnabled || typeof optionsValue === 'string') {
|
||||
|
|
@ -202,9 +201,8 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
|
|||
}
|
||||
});
|
||||
setOptions(_options);
|
||||
updateAllOptionsParams(_options);
|
||||
setMarkedAsDefault(_value);
|
||||
paramUpdated({ name: 'value' }, 'value', _value, 'properties');
|
||||
updateAllOptionsParams(_options);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -299,7 +299,6 @@ export const dropdownV2Config = {
|
|||
],
|
||||
},
|
||||
label: { value: 'Select' },
|
||||
value: { value: '{{"2"}}' },
|
||||
optionsLoadingState: { value: '{{false}}' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
visibility: { value: '{{true}}' },
|
||||
|
|
|
|||
|
|
@ -48,8 +48,12 @@ export const listviewConfig = {
|
|||
data: {
|
||||
type: 'code',
|
||||
displayName: 'List data',
|
||||
validation: {
|
||||
schema: { type: 'array', element: { type: 'object' } },
|
||||
schema: {
|
||||
type: 'union',
|
||||
schemas: [
|
||||
{ type: 'array', element: { type: 'object' } },
|
||||
{ type: 'array', element: { type: 'string' } },
|
||||
],
|
||||
defaultValue: "[{text: 'Sample text 1'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ export const Container = ({
|
|||
border: `1px solid ${borderColor}`,
|
||||
height,
|
||||
display: isVisible ? 'flex' : 'none',
|
||||
overflow: 'hidden auto',
|
||||
position: 'relative',
|
||||
boxShadow,
|
||||
};
|
||||
|
|
@ -66,9 +65,7 @@ export const Container = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`jet-container tw-flex tw-flex-col ${isLoading && 'jet-container-loading'} ${
|
||||
properties.showHeader && 'jet-container--with-header'
|
||||
}`}
|
||||
className={`jet-container widget-type-container ${properties.loadingState && 'jet-container-loading'}`}
|
||||
id={id}
|
||||
data-disabled={isDisabled}
|
||||
style={computedStyles}
|
||||
|
|
|
|||
|
|
@ -329,6 +329,11 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
|
|||
setCurrentPageHandle(startingPage.handle);
|
||||
updateFeatureAccess();
|
||||
setCurrentPageId(startingPage.id, moduleId);
|
||||
setResolvedPageConstants({
|
||||
id: startingPage?.id,
|
||||
handle: startingPage?.handle,
|
||||
name: startingPage?.name,
|
||||
});
|
||||
setComponentNameIdMapping(moduleId);
|
||||
updateEventsField('events', appData.events);
|
||||
setCurrentVersionId(appData.editing_version?.id || appData.current_version_id);
|
||||
|
|
|
|||
|
|
@ -1746,7 +1746,10 @@ export const createComponentsSlice = (set, get) => ({
|
|||
getCustomResolvableReference: (value, parentId, moduleId) => {
|
||||
const { getParentComponentType } = get();
|
||||
const parentComponentType = getParentComponentType(parentId, moduleId);
|
||||
if (parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) {
|
||||
if (
|
||||
(parentComponentType === 'Listview' && value.includes('listItem') && checkSubstringRegex(value, 'listItem')) ||
|
||||
value === '{{listItem}}'
|
||||
) {
|
||||
return { entityType: 'components', entityNameOrId: parentId, entityKey: 'listItem' };
|
||||
} else if (
|
||||
parentComponentType === 'Kanban' &&
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { debounce } from 'lodash';
|
|||
|
||||
const initialState = {
|
||||
hoveredComponentForGrid: '',
|
||||
hoveredComponentBoundaryId: '',
|
||||
triggerCanvasUpdater: false,
|
||||
lastCanvasIdClick: '',
|
||||
lastCanvasClickPosition: null,
|
||||
|
|
@ -13,6 +14,8 @@ export const createGridSlice = (set, get) => ({
|
|||
setHoveredComponentForGrid: (id) =>
|
||||
set(() => ({ hoveredComponentForGrid: id }), false, { type: 'setHoveredComponentForGrid', id }),
|
||||
getHoveredComponentForGrid: () => get().hoveredComponentForGrid,
|
||||
setHoveredComponentBoundaryId: (id) =>
|
||||
set(() => ({ hoveredComponentBoundaryId: id }), false, { type: 'setHoveredComponentBoundaryId', id }),
|
||||
toggleCanvasUpdater: () =>
|
||||
set((state) => ({ triggerCanvasUpdater: !state.triggerCanvasUpdater }), false, { type: 'toggleCanvasUpdater' }),
|
||||
debouncedToggleCanvasUpdater: debounce(() => {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ export const DropdownV2 = ({
|
|||
}) => {
|
||||
const {
|
||||
label,
|
||||
value,
|
||||
advanced,
|
||||
schema,
|
||||
placeholder,
|
||||
|
|
@ -89,7 +88,7 @@ export const DropdownV2 = ({
|
|||
padding,
|
||||
} = styles;
|
||||
const isInitialRender = useRef(true);
|
||||
const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value));
|
||||
const [currentValue, setCurrentValue] = useState(() => findDefaultItem(schema));
|
||||
const isMandatory = validation?.mandatory ?? false;
|
||||
const options = properties?.options;
|
||||
const [validationStatus, setValidationStatus] = useState(validate(currentValue));
|
||||
|
|
@ -168,18 +167,9 @@ export const DropdownV2 = ({
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (advanced) {
|
||||
setInputValue(findDefaultItem(schema));
|
||||
}
|
||||
setInputValue(findDefaultItem(advanced ? schema : options));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [advanced, JSON.stringify(schema)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!advanced) {
|
||||
setInputValue(value);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [advanced, value]);
|
||||
}, [advanced, JSON.stringify(schema), JSON.stringify(options)]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visibility !== properties.visibility) setVisibility(properties.visibility);
|
||||
|
|
|
|||
|
|
@ -299,7 +299,6 @@ export const dropdownV2Config = {
|
|||
],
|
||||
},
|
||||
label: { value: 'Select' },
|
||||
value: { value: '{{"2"}}' },
|
||||
optionsLoadingState: { value: '{{false}}' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
visibility: { value: '{{true}}' },
|
||||
|
|
|
|||
|
|
@ -49,7 +49,13 @@ export const listviewConfig = {
|
|||
type: 'code',
|
||||
displayName: 'List data',
|
||||
validation: {
|
||||
schema: { type: 'array', element: { type: 'object' } },
|
||||
schema: {
|
||||
type: 'union',
|
||||
schemas: [
|
||||
{ type: 'array', element: { type: 'object' } },
|
||||
{ type: 'array', element: { type: 'string' } },
|
||||
],
|
||||
},
|
||||
defaultValue: "[{text: 'Sample text 1'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export const workspaceSettingsLinks = [
|
|||
{ id: 'groups', name: 'Groups', route: 'groups', conditions: ['admin'] },
|
||||
{ id: 'workspacelogin', name: 'Workspace login', route: 'workspace-login', conditions: ['admin', 'wsLoginEnabled'] },
|
||||
{ id: 'workspace-variables', name: 'Workspace variables', route: 'workspace-variables', conditions: ['admin'] },
|
||||
{ id: 'copilot', name: 'Copilot', route: 'copilot', conditions: ['admin'] },
|
||||
{ id: 'custom-styles', name: 'Custom styles', route: 'custom-styles', conditions: ['admin'] },
|
||||
{ id: 'configure-git', name: 'Configure Git', route: 'configure-git', conditions: ['admin'] },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ export const authorizeUserAndHandleErrors = (workspace_id, workspace_slug, callb
|
|||
const unauthorized_organization_slug = workspace_slug;
|
||||
|
||||
/* get current session's workspace id */
|
||||
authenticationService
|
||||
sessionService
|
||||
.validateSession()
|
||||
.then(({ current_organization_id, ...restSessionData }) => {
|
||||
/* change current organization id to valid one [current logged in organization] */
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const orgEnvironmentConstantService = {
|
|||
function getAll(type = null) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
const queryParams = type ? `?type=${type}` : '';
|
||||
return fetch(`${config.apiUrl}/organization-constants${queryParams}`, requestOptions).then(handleResponse);
|
||||
return fetch(`${config.apiUrl}/organization-constants/decrypted${queryParams}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function create(name, value, type, environments) {
|
||||
|
|
|
|||
|
|
@ -493,4 +493,13 @@ $btn-dark-color: #FFFFFF;
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//[Container-widget]Show scrollbar only on hover
|
||||
.widget-type-container {
|
||||
overflow: hidden auto;
|
||||
scrollbar-width: none;
|
||||
&:hover {
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -1467,5 +1467,59 @@
|
|||
}
|
||||
|
||||
.tj-table-tag-col-readonly {
|
||||
margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
|
||||
}
|
||||
margin-left: -2px !important; //this -ve margin offset for the margin given to each tags in overall column width
|
||||
}
|
||||
|
||||
.jet-data-table {
|
||||
.table-bordered {
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid var(--interactive-overlay-border-pressed) !important;
|
||||
border-right: 1px solid var(--interactive-overlay-border-pressed) !important;
|
||||
|
||||
&:first-child {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
thead th {
|
||||
border-top: none !important;
|
||||
|
||||
&:first-child {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-striped {
|
||||
tbody {
|
||||
div[data-index]:nth-child(odd) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
div[data-index]:nth-child(even) {
|
||||
background-color: var(--slate2) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.jet-data-table {
|
||||
overflow: auto;
|
||||
}
|
||||
// hide scrollbar on touch devices
|
||||
.jet-data-table::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function Label({ label, width, labelRef, color, defaultAlignment, direction, aut
|
|||
justifyContent: direction == 'right' ? 'flex-end' : 'flex-start',
|
||||
fontSize: '12px',
|
||||
height: defaultAlignment === 'top' && '20px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { redirectToErrorPage } from '@/_helpers/routes';
|
|||
import { ERROR_TYPES } from '@/_helpers/constants';
|
||||
import { BreadCrumbContext } from '@/App/App';
|
||||
import { checkConditionsForRoute } from '@/_helpers/utils';
|
||||
import { OrganizationList } from '@/modules/dashboard/components';
|
||||
export default function WorkspaceSettingsPage({ extraLinks, ...props }) {
|
||||
const workspaceSettingsLinks = constructWorkspaceSettingsLinks(extraLinks);
|
||||
const admin = authenticationService.currentSessionValue?.admin;
|
||||
|
|
@ -107,6 +108,7 @@ export default function WorkspaceSettingsPage({ extraLinks, ...props }) {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
<OrganizationList />
|
||||
</div>
|
||||
|
||||
<div className={cx('col workspace-content-wrapper')} style={{ paddingTop: '40px' }}>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e1f5fbcdc4ede5f818d1bacb2a51d29f8e2dc4bd
|
||||
Subproject commit 17df343008fcedfd7b05540c9c21bebd152af086
|
||||
|
|
@ -299,7 +299,6 @@ export const dropdownV2Config = {
|
|||
],
|
||||
},
|
||||
label: { value: 'Select' },
|
||||
value: { value: '{{"2"}}' },
|
||||
optionsLoadingState: { value: '{{false}}' },
|
||||
placeholder: { value: 'Select an option' },
|
||||
visibility: { value: '{{true}}' },
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const listviewConfig = {
|
|||
type: 'code',
|
||||
displayName: 'List data',
|
||||
validation: {
|
||||
schema: { type: 'array', element: { type: 'object' } },
|
||||
schema: { type: 'union', schemas: [{ type: 'array', element: { type: 'object' } },{ type: 'array', element: { type: 'string' } }] },
|
||||
defaultValue: "[{text: 'Sample text 1'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { MODULES } from '@modules/app/constants/modules';
|
|||
import { dbTransactionWrap } from '@helpers/database.helper';
|
||||
import { DataSourceScopes, DataSourceTypes } from './constants';
|
||||
import { GetQueryVariables } from './types';
|
||||
import { decode } from 'js-base64';
|
||||
|
||||
@Injectable()
|
||||
export class DataSourcesRepository extends Repository<DataSource> {
|
||||
|
|
@ -70,6 +71,23 @@ export class DataSourcesRepository extends Repository<DataSource> {
|
|||
query.andWhere('data_source_options.environmentId = :environmentId', { environmentId });
|
||||
}
|
||||
const result = await query.getMany();
|
||||
result.forEach((dataSource) => {
|
||||
if (dataSource.plugin) {
|
||||
if (dataSource.plugin.iconFile) {
|
||||
dataSource.plugin.iconFile.data = dataSource.plugin.iconFile.data.toString('utf8');
|
||||
}
|
||||
if (dataSource.plugin.manifestFile) {
|
||||
dataSource.plugin.manifestFile.data = JSON.parse(
|
||||
decode(dataSource.plugin.manifestFile.data.toString('utf8'))
|
||||
);
|
||||
}
|
||||
if (dataSource.plugin.operationsFile) {
|
||||
dataSource.plugin.operationsFile.data = JSON.parse(
|
||||
decode(dataSource.plugin.operationsFile.data.toString('utf8'))
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sampleDataSourceQuery = await manager
|
||||
.createQueryBuilder(DataSource, 'data_source')
|
||||
|
|
|
|||
|
|
@ -61,11 +61,24 @@ export class DataSourcesService implements IDataSourcesService {
|
|||
environmentId: selectedEnvironmentId,
|
||||
});
|
||||
for (const dataSource of dataSources) {
|
||||
if (dataSource.pluginId) {
|
||||
dataSource.plugin.iconFile.data = dataSource.plugin.iconFile.data.toString('utf8');
|
||||
dataSource.plugin.manifestFile.data = JSON.parse(decode(dataSource.plugin.manifestFile.data.toString('utf8')));
|
||||
dataSource.plugin.operationsFile.data = JSON.parse(
|
||||
decode(dataSource.plugin.operationsFile.data.toString('utf8'))
|
||||
const parseIfNeeded = (data: any) => {
|
||||
if (typeof data === 'object' && data !== null) return data;
|
||||
if (Buffer.isBuffer(data) || typeof data === 'string') {
|
||||
return JSON.parse(decode(data.toString('utf8')));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
try {
|
||||
if (dataSource.pluginId) {
|
||||
if (Buffer.isBuffer(dataSource.plugin.iconFile.data)) {
|
||||
dataSource.plugin.iconFile.data = dataSource.plugin.iconFile.data.toString('utf8');
|
||||
}
|
||||
dataSource.plugin.manifestFile.data = parseIfNeeded(dataSource.plugin.manifestFile.data);
|
||||
dataSource.plugin.operationsFile.data = parseIfNeeded(dataSource.plugin.operationsFile.data);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new BadRequestException(
|
||||
`Error parsing plugin data for dataSourceId: ${dataSource.id}. Details: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ export const DEFAULT_GROUP_PERMISSIONS = {
|
|||
appDelete: true,
|
||||
folderCRUD: true,
|
||||
orgConstantCRUD: true,
|
||||
dataSourceCreate: true,
|
||||
dataSourceDelete: true,
|
||||
dataSourceCreate: false,
|
||||
dataSourceDelete: false,
|
||||
isBuilderLevel: true,
|
||||
},
|
||||
END_USER: {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ export class LoginConfigsUtilService implements ILoginConfigsUtilService {
|
|||
return result;
|
||||
}
|
||||
|
||||
return this.hideSSOSensitiveData(result?.ssoConfigs, result?.name, result.id);
|
||||
const filteredConfigs = this.hideSSOSensitiveData(result?.ssoConfigs, result?.name, result.id);
|
||||
return { ...filteredConfigs, enableSignUp: result.enableSignUp, automaticSsoLogin: result.automaticSsoLogin };
|
||||
}
|
||||
|
||||
async addInstanceLevelSSOConfigs(result: DeepPartial<Organization>) {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ export class AllowPersonalWorkspaceGuard implements CanActivate {
|
|||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
const organizationId = request.body.organizationId;
|
||||
|
||||
const isPersonalWorkspaceEnabled =
|
||||
(await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) === 'true';
|
||||
|
||||
return isSuperAdmin(user) || isPersonalWorkspaceEnabled;
|
||||
return isSuperAdmin(user) || isPersonalWorkspaceEnabled || !!organizationId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { INSTANCE_SYSTEM_SETTINGS } from '@modules/instance-settings/constants';
|
||||
import { InstanceSettingsUtilService } from '@modules/instance-settings/util.service';
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class SignupDisableGuard implements CanActivate {
|
||||
constructor(protected readonly instanceSettingsUtilService: InstanceSettingsUtilService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const enableSignUp = await this.instanceSettingsUtilService.getSettings(INSTANCE_SYSTEM_SETTINGS.ENABLE_SIGNUP);
|
||||
return request.body.organizationId || enableSignUp;
|
||||
constructor(private configService: ConfigService) {}
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
return this.configService.get<string>('DISABLE_SIGNUPS') !== 'true';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,30 +22,51 @@ export class OrganizationConstantController implements IOrganizationConstantCont
|
|||
@InitFeature(FEATURE_KEY.GET)
|
||||
@Get()
|
||||
async get(@User() user, @Query('type') type: OrganizationConstantType) {
|
||||
const result = await this.organizationConstantsService.allEnvironmentConstants(user.organizationId);
|
||||
const result = await this.organizationConstantsService.allEnvironmentConstants(user.organizationId, false, type);
|
||||
return { constants: result };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
|
||||
@InitFeature(FEATURE_KEY.GET_DECRYPTED_CONSTANTS)
|
||||
@Get('decrypted')
|
||||
async getDecryptedConstants(@User() user, @Query('type') type: OrganizationConstantType) {
|
||||
const result = await this.organizationConstantsService.allEnvironmentConstants(user.organizationId, true, type);
|
||||
return { constants: result };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
|
||||
@InitFeature(FEATURE_KEY.GET_SECRETS)
|
||||
@Get('secrets')
|
||||
async getAllSecrets(@User() user) {
|
||||
const result = await this.organizationConstantsService.allEnvironmentConstants(
|
||||
user.organizationId,
|
||||
false,
|
||||
OrganizationConstantType.SECRET
|
||||
);
|
||||
return { constants: result };
|
||||
}
|
||||
|
||||
//by default, this api fetches only global constants (for public apps, need to fetch app to get orgId in the public guard)
|
||||
@UseGuards(AppAuthGuard)
|
||||
@Get('public/:app_slug')
|
||||
@InitFeature(FEATURE_KEY.GET_PUBLIC)
|
||||
async getConstantsFromPublicApp(@App() app, @Query('environmentId') environmentId) {
|
||||
const result = await this.organizationConstantsService.getConstantsForEnvironment(
|
||||
@Get('public/:slug')
|
||||
async getConstantsFromPublicApp(@App() app) {
|
||||
const result = await this.organizationConstantsService.allEnvironmentConstants(
|
||||
app.organizationId,
|
||||
environmentId,
|
||||
false,
|
||||
OrganizationConstantType.GLOBAL
|
||||
);
|
||||
return { constants: result };
|
||||
}
|
||||
|
||||
//by default, this api fetches only global constants
|
||||
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
|
||||
@Get(':app_slug')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@InitFeature(FEATURE_KEY.GET_FROM_APP)
|
||||
async getConstantsFromApp(@User() user, @Query('environmentId') environmentId) {
|
||||
const result = await this.organizationConstantsService.getConstantsForEnvironment(
|
||||
@Get(':app_slug')
|
||||
async getConstantsFromApp(@User() user) {
|
||||
const result = await this.organizationConstantsService.allEnvironmentConstants(
|
||||
user.organizationId,
|
||||
environmentId,
|
||||
false,
|
||||
OrganizationConstantType.GLOBAL
|
||||
);
|
||||
return { constants: result };
|
||||
|
|
@ -92,7 +113,7 @@ export class OrganizationConstantController implements IOrganizationConstantCont
|
|||
async delete(@User() user, @Param('id') constantId, @Query('environmentId') environmentId) {
|
||||
const { organizationId } = user;
|
||||
|
||||
await this.organizationConstantsService.delete(constantId, organizationId, environmentId);
|
||||
await this.organizationConstantsService.delete(constantId, organizationId);
|
||||
|
||||
return { statusCode: 204 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,16 @@ export class OrganizationConstantRepository extends Repository<OrganizationConst
|
|||
}
|
||||
|
||||
// Updated function to find all organization constants by organizationId
|
||||
async findAllByOrganizationId(organizationId: string) {
|
||||
async findAllByOrganizationId(organizationId: string, type?: OrganizationConstantType) {
|
||||
const whereCondition: any = {
|
||||
organizationId,
|
||||
};
|
||||
// Add type filter if provided
|
||||
if (type) {
|
||||
whereCondition.type = type;
|
||||
}
|
||||
return this.find({
|
||||
where: { organizationId },
|
||||
where: whereCondition,
|
||||
relations: ['orgEnvironmentConstantValues'],
|
||||
});
|
||||
}
|
||||
|
|
@ -31,14 +38,21 @@ export class OrganizationConstantRepository extends Repository<OrganizationConst
|
|||
});
|
||||
}
|
||||
|
||||
async findByEnvironment(organizationId: string, environmentId: string) {
|
||||
return this.find({
|
||||
where: {
|
||||
organizationId,
|
||||
orgEnvironmentConstantValues: {
|
||||
environmentId,
|
||||
},
|
||||
async findByEnvironment(organizationId: string, environmentId: string, type?: OrganizationConstantType) {
|
||||
const whereCondition: any = {
|
||||
organizationId,
|
||||
orgEnvironmentConstantValues: {
|
||||
environmentId,
|
||||
},
|
||||
};
|
||||
|
||||
// Add type filter if provided
|
||||
if (type) {
|
||||
whereCondition.type = type;
|
||||
}
|
||||
|
||||
return this.find({
|
||||
where: whereCondition,
|
||||
relations: ['orgEnvironmentConstantValues'],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { IOrganizationConstantsService } from './interfaces/IService';
|
|||
import { OrganizationConstantsUtilService } from './util.service';
|
||||
import { OrganizationConstantType } from './constants';
|
||||
import { OrganizationConstantRepository } from './repository';
|
||||
|
||||
const secretValue = '**********';
|
||||
@Injectable()
|
||||
export class OrganizationConstantsService implements IOrganizationConstantsService {
|
||||
constructor(
|
||||
|
|
@ -23,22 +23,37 @@ export class OrganizationConstantsService implements IOrganizationConstantsServi
|
|||
type?: OrganizationConstantType
|
||||
): Promise<OrganizationConstant[]> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const result = await this.organizationConstantRepository.findAllByOrganizationId(organizationId);
|
||||
const result = await this.organizationConstantRepository.findAllByOrganizationId(organizationId, type);
|
||||
const appEnvironments = await this.appEnvironmentUtilService.getAll(organizationId);
|
||||
|
||||
const constantsWithValues = await Promise.all(
|
||||
result.map(async (constant) => {
|
||||
// Skip processing values if type is SECRET and decryptSecretValue is false
|
||||
if (constant.type === OrganizationConstantType.SECRET && !decryptSecretValue) {
|
||||
return {
|
||||
name: constant.constantName,
|
||||
};
|
||||
}
|
||||
const values = await Promise.all(
|
||||
appEnvironments.map(async (env) => {
|
||||
const value = constant.orgEnvironmentConstantValues.find((value) => value.environmentId === env.id);
|
||||
let resolvedValue = '';
|
||||
if (value) {
|
||||
if (constant.type === OrganizationConstantType.SECRET) {
|
||||
resolvedValue = decryptSecretValue
|
||||
? await this.organizationConstantsUtilService.decryptSecret(organizationId, value.value)
|
||||
: secretValue;
|
||||
} else {
|
||||
resolvedValue = await this.organizationConstantsUtilService.decryptSecret(
|
||||
organizationId,
|
||||
value.value
|
||||
); // Constant type values are always decrypted
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
environmentName: env.name,
|
||||
value:
|
||||
value && value.value.length > 0
|
||||
? await this.organizationConstantsUtilService.decryptSecret(organizationId, value.value)
|
||||
: '',
|
||||
id: value.environmentId,
|
||||
value: resolvedValue,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
@ -62,22 +77,26 @@ export class OrganizationConstantsService implements IOrganizationConstantsServi
|
|||
environmentId: string,
|
||||
type?: OrganizationConstantType
|
||||
): Promise<any[]> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const result = await this.organizationConstantRepository.findByEnvironment(organizationId, environmentId);
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const result = await this.organizationConstantRepository.findByEnvironment(organizationId, environmentId, type);
|
||||
|
||||
const constantsWithValues = result.map(async (constant) => {
|
||||
const decryptedValue = await this.organizationConstantsUtilService.decryptSecret(
|
||||
organizationId,
|
||||
constant.orgEnvironmentConstantValues[0].value
|
||||
);
|
||||
return {
|
||||
id: constant.id,
|
||||
name: constant.constantName,
|
||||
value: decryptedValue,
|
||||
};
|
||||
});
|
||||
return await Promise.all(
|
||||
result.map(async (constant) => {
|
||||
const resolvedValue = !(constant.type === OrganizationConstantType.SECRET)
|
||||
? await this.organizationConstantsUtilService.decryptSecret(
|
||||
organizationId,
|
||||
constant.orgEnvironmentConstantValues[0].value
|
||||
)
|
||||
: secretValue;
|
||||
|
||||
return Promise.all(constantsWithValues);
|
||||
return {
|
||||
id: constant.id,
|
||||
name: constant.constantName,
|
||||
type: constant.type,
|
||||
value: resolvedValue,
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export class OrganizationConstantsUtilService implements IOrganizationConstantsU
|
|||
async deleteOrgEnvironmentConstant(
|
||||
constantId: string,
|
||||
organizationId: string,
|
||||
environmentId: string
|
||||
environmentId?: string
|
||||
): Promise<DeleteResult> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const constantToDelete = await this.organizationConstantRepository.findOneByIdAndOrganizationId(
|
||||
|
|
@ -95,6 +95,11 @@ export class OrganizationConstantsUtilService implements IOrganizationConstantsU
|
|||
throw new Error('Constant not found');
|
||||
}
|
||||
|
||||
// If no environmentId is provided, delete the constant from all environments
|
||||
if (!environmentId) {
|
||||
return this.organizationConstantRepository.deleteOneById(constantId);
|
||||
}
|
||||
|
||||
if (constantToDelete.orgEnvironmentConstantValues.length === 1) {
|
||||
return this.organizationConstantRepository.deleteOneById(constantId);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -193,138 +193,136 @@ export class OrganizationUsersService implements IOrganizationUsersService {
|
|||
let invalidGroups = [];
|
||||
const emailPattern = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
|
||||
const invalidRoles = [];
|
||||
await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const groupPermissions = (
|
||||
await this.groupPermissionsUtilService.getAllGroupByOrganization(currentUser.organizationId)
|
||||
).groupPermissions?.filter((gp) => !gp.disabled);
|
||||
const existingGroups = groupPermissions.map((groupPermission) => groupPermission.name);
|
||||
csv
|
||||
.parseString(fileStream.toString(), {
|
||||
headers: ['first_name', 'last_name', 'email', 'user_role', 'groups', 'metadata'],
|
||||
renameHeaders: true,
|
||||
ignoreEmpty: true,
|
||||
})
|
||||
.transform((row: UserCsvRow, next) => {
|
||||
const groupNames = this.organizationUsersUtilService.createGroupsList(row?.groups);
|
||||
invalidGroups = [...invalidGroups, ...groupNames.filter((group) => !existingGroups.includes(group))];
|
||||
const groups = groupPermissions.filter((group) => groupNames.includes(group.name)).map((group) => group.id);
|
||||
return next(null, {
|
||||
...row,
|
||||
groups: groups,
|
||||
user_role: this.organizationUsersUtilService.convertUserRolesCasing(row?.user_role),
|
||||
userMetadata: row?.metadata ? JSON.parse(row.metadata) : null,
|
||||
email: row?.email?.toLowerCase(),
|
||||
});
|
||||
})
|
||||
.validate(async (data: UserCsvRow, next) => {
|
||||
await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
//Check for existing users
|
||||
let isInvalidRole = false;
|
||||
|
||||
const user = await this.userRepository.findByEmail(data?.email, undefined, undefined, manager);
|
||||
|
||||
if (user?.status === USER_STATUS.ARCHIVED) {
|
||||
archivedUsers.push(data?.email);
|
||||
} else if (user?.organizationUsers?.some((ou) => ou.organizationId === currentUser.organizationId)) {
|
||||
existingUsers.push(data?.email);
|
||||
} else {
|
||||
const user = {
|
||||
firstName: data?.first_name,
|
||||
lastName: data?.last_name,
|
||||
email: data?.email,
|
||||
role: data?.user_role,
|
||||
groups: data?.groups,
|
||||
userMetadata: data?.metadata,
|
||||
};
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
//Check for invalid groups
|
||||
|
||||
if (!Object.values(USER_ROLE).includes(data?.user_role as USER_ROLE)) {
|
||||
invalidRoles.push(data?.user_role);
|
||||
isInvalidRole = true;
|
||||
}
|
||||
|
||||
data.first_name = data.first_name?.trim();
|
||||
data.last_name = data.last_name?.trim();
|
||||
|
||||
const isValidName = data.first_name !== '' || data.last_name !== '';
|
||||
return next(null, isValidName && emailPattern.test(data.email) && !isInvalidRole);
|
||||
}, manager);
|
||||
})
|
||||
.on('data', function () {})
|
||||
.on('data-invalid', (row, rowNumber) => {
|
||||
const invalidField = Object.keys(row).filter((key) => {
|
||||
if (Array.isArray(row[key])) {
|
||||
return row[key].length === 0;
|
||||
}
|
||||
return !row[key] || row[key] === '';
|
||||
});
|
||||
invalidRows.push(rowNumber);
|
||||
invalidFields.add(invalidField);
|
||||
})
|
||||
.on('end', async (rowCount: number) => {
|
||||
try {
|
||||
if (rowCount > MAX_ROW_COUNT) {
|
||||
throw new BadRequestException('Row count cannot be greater than 500');
|
||||
}
|
||||
|
||||
if (invalidRows.length) {
|
||||
const invalidFieldsArray = invalidFields.entries().next().value[1];
|
||||
const errorMsg = `Missing ${[invalidFieldsArray.join(',')]} information in ${
|
||||
invalidRows.length
|
||||
} row(s);. No users were uploaded, please update and try again.`;
|
||||
throw new BadRequestException(errorMsg);
|
||||
}
|
||||
|
||||
if (invalidGroups.length) {
|
||||
throw new BadRequestException(
|
||||
`${invalidGroups.length} group${isPlural(invalidGroups)} doesn't exist. No users were uploaded`
|
||||
);
|
||||
}
|
||||
|
||||
if (invalidRoles.length > 0) {
|
||||
throw new BadRequestException('Invalid role present for the users');
|
||||
}
|
||||
|
||||
if (archivedUsers.length) {
|
||||
throw new BadRequestException(
|
||||
`User${isPlural(archivedUsers)} with email ${archivedUsers.join(
|
||||
', '
|
||||
)} is archived. No users were uploaded`
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUsers.length) {
|
||||
throw new BadRequestException(
|
||||
`${existingUsers.length} users with same email already exist. No users were uploaded `
|
||||
);
|
||||
}
|
||||
|
||||
if (users.length === 0) {
|
||||
throw new BadRequestException('No users were uploaded');
|
||||
}
|
||||
|
||||
if (users.length > 250) {
|
||||
throw new BadRequestException(`You can only invite 250 users at a time`);
|
||||
}
|
||||
|
||||
await this.organizationUsersUtilService.inviteUserswrapper(users, currentUser);
|
||||
res.status(201).send({ message: `${rowCount} user${isPlural(users)} are being added` });
|
||||
} catch (error) {
|
||||
const { status, response } = error;
|
||||
if (status === 451) {
|
||||
res.status(status).send({ message: response, statusCode: status });
|
||||
return;
|
||||
}
|
||||
res.status(status).send(JSON.stringify(response));
|
||||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
throw error.message;
|
||||
const groupPermissions = (
|
||||
await this.groupPermissionsUtilService.getAllGroupByOrganization(currentUser.organizationId)
|
||||
).groupPermissions?.filter((gp) => !gp.disabled);
|
||||
const existingGroups = groupPermissions.map((groupPermission) => groupPermission.name);
|
||||
csv
|
||||
.parseString(fileStream.toString(), {
|
||||
headers: ['first_name', 'last_name', 'email', 'user_role', 'groups', 'metadata'],
|
||||
renameHeaders: true,
|
||||
ignoreEmpty: true,
|
||||
})
|
||||
.transform((row: UserCsvRow, next) => {
|
||||
const groupNames = this.organizationUsersUtilService.createGroupsList(row?.groups);
|
||||
invalidGroups = [...invalidGroups, ...groupNames.filter((group) => !existingGroups.includes(group))];
|
||||
const groups = groupPermissions.filter((group) => groupNames.includes(group.name)).map((group) => group.id);
|
||||
return next(null, {
|
||||
...row,
|
||||
groups: groups,
|
||||
user_role: this.organizationUsersUtilService.convertUserRolesCasing(row?.user_role),
|
||||
userMetadata: row?.metadata ? JSON.parse(row.metadata) : null,
|
||||
email: row?.email?.toLowerCase(),
|
||||
});
|
||||
});
|
||||
})
|
||||
.validate(async (data: UserCsvRow, next) => {
|
||||
await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
//Check for existing users
|
||||
let isInvalidRole = false;
|
||||
|
||||
const user = await this.userRepository.findByEmail(data?.email, undefined, undefined, manager);
|
||||
|
||||
if (user?.status === USER_STATUS.ARCHIVED) {
|
||||
archivedUsers.push(data?.email);
|
||||
} else if (user?.organizationUsers?.some((ou) => ou.organizationId === currentUser.organizationId)) {
|
||||
existingUsers.push(data?.email);
|
||||
} else {
|
||||
const user = {
|
||||
firstName: data?.first_name,
|
||||
lastName: data?.last_name,
|
||||
email: data?.email,
|
||||
role: data?.user_role,
|
||||
groups: data?.groups,
|
||||
userMetadata: data?.metadata,
|
||||
};
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
//Check for invalid groups
|
||||
|
||||
if (!Object.values(USER_ROLE).includes(data?.user_role as USER_ROLE)) {
|
||||
invalidRoles.push(data?.user_role);
|
||||
isInvalidRole = true;
|
||||
}
|
||||
|
||||
data.first_name = data.first_name?.trim();
|
||||
data.last_name = data.last_name?.trim();
|
||||
|
||||
const isValidName = data.first_name !== '' || data.last_name !== '';
|
||||
return next(null, isValidName && emailPattern.test(data.email) && !isInvalidRole);
|
||||
});
|
||||
})
|
||||
.on('data', function () {})
|
||||
.on('data-invalid', (row, rowNumber) => {
|
||||
const invalidField = Object.keys(row).filter((key) => {
|
||||
if (Array.isArray(row[key])) {
|
||||
return row[key].length === 0;
|
||||
}
|
||||
return !row[key] || row[key] === '';
|
||||
});
|
||||
invalidRows.push(rowNumber);
|
||||
invalidFields.add(invalidField);
|
||||
})
|
||||
.on('end', async (rowCount: number) => {
|
||||
try {
|
||||
if (rowCount > MAX_ROW_COUNT) {
|
||||
throw new BadRequestException('Row count cannot be greater than 500');
|
||||
}
|
||||
|
||||
if (invalidRows.length) {
|
||||
const invalidFieldsArray = invalidFields.entries().next().value[1];
|
||||
const errorMsg = `Missing ${[invalidFieldsArray.join(',')]} information in ${
|
||||
invalidRows.length
|
||||
} row(s);. No users were uploaded, please update and try again.`;
|
||||
throw new BadRequestException(errorMsg);
|
||||
}
|
||||
|
||||
if (invalidGroups.length) {
|
||||
throw new BadRequestException(
|
||||
`${invalidGroups.length} group${isPlural(invalidGroups)} doesn't exist. No users were uploaded`
|
||||
);
|
||||
}
|
||||
|
||||
if (invalidRoles.length > 0) {
|
||||
throw new BadRequestException('Invalid role present for the users');
|
||||
}
|
||||
|
||||
if (archivedUsers.length) {
|
||||
throw new BadRequestException(
|
||||
`User${isPlural(archivedUsers)} with email ${archivedUsers.join(
|
||||
', '
|
||||
)} is archived. No users were uploaded`
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUsers.length) {
|
||||
throw new BadRequestException(
|
||||
`${existingUsers.length} users with same email already exist. No users were uploaded `
|
||||
);
|
||||
}
|
||||
|
||||
if (users.length === 0) {
|
||||
throw new BadRequestException('No users were uploaded');
|
||||
}
|
||||
|
||||
if (users.length > 250) {
|
||||
throw new BadRequestException(`You can only invite 250 users at a time`);
|
||||
}
|
||||
|
||||
await this.organizationUsersUtilService.inviteUserswrapper(users, currentUser);
|
||||
res.status(201).send({ message: `${rowCount} user${isPlural(users)} are being added` });
|
||||
} catch (error) {
|
||||
const { status, response } = error;
|
||||
if (status === 451) {
|
||||
res.status(status).send({ message: response, statusCode: status });
|
||||
return;
|
||||
}
|
||||
res.status(status).send(JSON.stringify(response));
|
||||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
throw error.message;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchUsersByValue(organizationId: string, searchInput: string) {
|
||||
|
|
|
|||
|
|
@ -252,7 +252,6 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
|
|||
|
||||
const role = orgUser.user.userPermissions.filter((group) => group.type === GROUP_PERMISSIONS_TYPE.DEFAULT);
|
||||
const groups = orgUser.user.userPermissions.filter((group) => group.type === GROUP_PERMISSIONS_TYPE.CUSTOM_GROUP);
|
||||
|
||||
return {
|
||||
email: orgUser.user.email,
|
||||
firstName: orgUser.user.firstName ?? '',
|
||||
|
|
@ -263,9 +262,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
|
|||
role: orgUser.role,
|
||||
status: orgUser.status,
|
||||
avatarId: orgUser.user.avatarId,
|
||||
groups: isBasicPlan
|
||||
? []
|
||||
: groups.map((groupPermission) => ({ name: groupPermission.name, id: groupPermission.id })),
|
||||
groups: groups.map((groupPermission) => ({ name: groupPermission.name, id: groupPermission.id })),
|
||||
roleGroup: role.map((groupPermission) => ({ name: groupPermission.name, id: groupPermission.id })),
|
||||
...(orgUser.invitationToken ? { invitationToken: orgUser.invitationToken } : {}),
|
||||
...(this.configService.get<string>('HIDE_ACCOUNT_SETUP_LINK') !== 'true' && orgUser.user.invitationToken
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export class InvitedUserSessionAuthGuard extends AuthGuard('jwt') {
|
|||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException) {
|
||||
/* No valid session / Expired token */
|
||||
return this.onInvalidSession(invitedUser);
|
||||
return this.onInvalidSession(invitedUser, request.body.accountToken);
|
||||
}
|
||||
throw new BadRequestException(
|
||||
'Invalid or expired invitation session. Please check your invitation and try again.'
|
||||
|
|
@ -139,20 +139,18 @@ export class InvitedUserSessionAuthGuard extends AuthGuard('jwt') {
|
|||
return isValidUser;
|
||||
}
|
||||
|
||||
async onInvalidSession(invitedUser: any) {
|
||||
const { invitationToken, status, organizationUserSource } = invitedUser;
|
||||
if (invitationToken && [USER_STATUS.INVITED, USER_STATUS.VERIFIED].includes(status as USER_STATUS)) {
|
||||
async onInvalidSession(invitedUser: any, accountToken: string) {
|
||||
const { status, source: organizationUserSource } = invitedUser;
|
||||
if (accountToken && [USER_STATUS.INVITED, USER_STATUS.VERIFIED].includes(status as USER_STATUS)) {
|
||||
/* User doesn't have a valid session & User didn't activate account yet */
|
||||
return invitedUser;
|
||||
} else {
|
||||
//by-pass 401 check for organization invite url / organization and account url + source from workspace signup
|
||||
if (
|
||||
organizationUserSource === WORKSPACE_USER_SOURCE.SIGNUP ||
|
||||
(status === SOURCE.WORKSPACE_SIGNUP && organizationUserSource === WORKSPACE_USER_SOURCE.SIGNUP)
|
||||
)
|
||||
if (organizationUserSource === WORKSPACE_USER_SOURCE.SIGNUP) {
|
||||
return invitedUser;
|
||||
}
|
||||
/* User doesn't have a session. Next?: login again and accept invite */
|
||||
const organization = await this.organizationRepository.fetchOrganization(invitedUser.invitedOrganizationId);
|
||||
const organization = await this.organizationRepository.fetchOrganization(invitedUser.organizationId);
|
||||
|
||||
if (!organization || organization.status !== WORKSPACE_STATUS.ACTIVE) {
|
||||
throw new BadRequestException('Organization is Archived');
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { FeatureAbilityGuard } from '@modules/organizations/ability/guard';
|
|||
|
||||
@InitModule(MODULES.ORGANIZATIONS)
|
||||
@Controller('organizations')
|
||||
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
|
||||
export class SetupOrganizationsController implements ISetupOrganizationsController {
|
||||
constructor(
|
||||
protected setupOrganizationsService: SetupOrganizationsService,
|
||||
|
|
@ -22,6 +21,7 @@ export class SetupOrganizationsController implements ISetupOrganizationsControll
|
|||
) {}
|
||||
|
||||
@InitFeature(FEATURE_KEY.CREATE)
|
||||
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
|
||||
@Post()
|
||||
async create(
|
||||
@User() user: UserEntity,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { AppsAbilityFactory } from '@modules/casl/abilities/apps-ability.factory
|
|||
import { WorkflowSchedule } from '@entities/workflow_schedule.entity';
|
||||
import { App } from '@entities/app.entity';
|
||||
import { AiModule } from '@modules/ai/module';
|
||||
import { DataSourcesRepository } from '@modules/data-sources/repository';
|
||||
export class WorkflowsModule {
|
||||
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
|
||||
|
|
@ -95,6 +96,7 @@ export class WorkflowsModule {
|
|||
AppsAbilityFactory,
|
||||
AppsRepository,
|
||||
UserRepository,
|
||||
DataSourcesRepository,
|
||||
DataQueryRepository,
|
||||
OrganizationConstantRepository,
|
||||
VersionRepository,
|
||||
|
|
|
|||
Loading…
Reference in a new issue