Merge branch 'appbuilder/sprint-11' into feat/steps-v2-alignment-style-improvement

This commit is contained in:
johnsoncherian 2025-04-28 13:34:41 +05:30
commit 45a0882022
76 changed files with 2816 additions and 1030 deletions

View file

@ -0,0 +1,3 @@
<svg width="11" height="14" viewBox="0 0 11 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.33317 3.49967C3.33317 2.30306 4.30322 1.33301 5.49984 1.33301C6.69645 1.33301 7.6665 2.30306 7.6665 3.49967V4.33301H3.33317V3.49967ZM2.33317 4.37981V3.49967C2.33317 1.75077 3.75094 0.333008 5.49984 0.333008C7.24874 0.333008 8.6665 1.75077 8.6665 3.49967V4.37981C9.90027 4.61384 10.8332 5.69781 10.8332 6.99967V10.9997C10.8332 12.4724 9.63926 13.6663 8.1665 13.6663H2.83317C1.36041 13.6663 0.166504 12.4724 0.166504 10.9997V6.99967C0.166504 5.69781 1.09941 4.61384 2.33317 4.37981ZM6.83317 8.99967C6.83317 9.73605 6.23622 10.333 5.49984 10.333C4.76346 10.333 4.1665 9.73605 4.1665 8.99967C4.1665 8.2633 4.76346 7.66634 5.49984 7.66634C6.23622 7.66634 6.83317 8.2633 6.83317 8.99967Z" fill="#ACB2B9"/>
</svg>

After

Width:  |  Height:  |  Size: 855 B

@ -1 +1 @@
Subproject commit a1435b3b0e66a0c731812256d3d495d5bf48d5bb
Subproject commit dcd948d284b5f14a868480830e09b90496db8572

View file

@ -22,6 +22,8 @@ import {
handleActivateTargets,
handleDeactivateTargets,
handleActivateNonDraggingComponents,
computeScrollDelta,
computeScrollDeltaOnDrag,
} from './gridUtils';
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
import useStore from '@/AppBuilder/_stores/store';
@ -56,6 +58,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const canvasWidth = NO_OF_GRIDS * gridWidth;
const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow);
const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow);
const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow);
const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
const draggingComponentId = useStore((state) => state.draggingComponentId, shallow);
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
@ -345,6 +348,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const handleDragEnd = useCallback(
(boxPositions) => {
let newParent = null;
let oldParent = null;
const updatedLayouts = boxPositions.reduce((layouts, { id, x, y, parent }) => {
const currentWidget = boxList.find((box) => box.id === id);
const containerWidth = parent ? useGridStore.getState().subContainerWidths[parent] : gridWidth;
@ -389,7 +393,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
}
newParent = parent ? parent : null;
oldParent = currentWidget.component?.parent;
layouts[id] = {
width: _width,
height: _height,
@ -400,6 +404,11 @@ export default function Grid({ gridWidth, currentLayout }) {
return layouts;
}, {});
setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: true });
// const currentWidget = boxList.find((box) => box.id === id);
updateContainerAutoHeight(newParent);
updateContainerAutoHeight(oldParent);
toggleCanvasUpdater();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -870,20 +879,19 @@ export default function Grid({ gridWidth, currentLayout }) {
const targetSlotId = target?.slotId;
const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth;
// const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || [];
// const draggedWidgetType = dragged.widgetType;
const isParentChangeAllowed = dragContext.isDroppable;
// Compute new position
let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged);
const isModalToCanvas = source.isModal && target.slotId === 'real-canvas';
let scrollDelta = computeScrollDelta({ source });
if (isParentChangeAllowed && !isModalToCanvas) {
const parent = target.slotId === 'real-canvas' ? null : target.slotId;
// Special case for Modal; If source widget is modal, prevent drops to canvas
handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]);
const parent = target.slotId === 'real-canvas' ? null : target.slotId;
handleDragEnd([{ id: e.target.id, x: left, y: top + scrollDelta, parent }]);
} else {
const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth;
@ -892,9 +900,8 @@ export default function Grid({ gridWidth, currentLayout }) {
!isModalToCanvas ??
toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`);
}
// Apply transform for smooth transition
e.target.style.transform = `translate(${left}px, ${top}px)`;
e.target.style.transform = `translate(${left}px, ${top + scrollDelta}px)`;
// Force reordering of conatiner if the parent has not changed
const newParentId = target.slotId === 'real-canvas' ? 'canvas' : target.slotId;
@ -962,12 +969,6 @@ export default function Grid({ gridWidth, currentLayout }) {
setCanvasBounds({ ...relativePosition });
}
e.target.style.transform = `translate(${left}px, ${top}px)`;
e.target.setAttribute(
'widget-pos2',
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
);
// 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);
@ -995,6 +996,17 @@ export default function Grid({ gridWidth, currentLayout }) {
handleActivateTargets(newParentId);
}
}
// Build the drag context from the event
const source = { slotId: oldParentId };
let scrollDelta = computeScrollDeltaOnDrag({ source });
e.target.style.transform = `translate(${left}px, ${top - scrollDelta}px)`;
e.target.setAttribute(
'widget-pos2',
`translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}`
);
// Postion ghost element exactly as same at dragged element
if (document.getElementById(`moveable-drag-ghost`)) {
document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`;
@ -1084,6 +1096,7 @@ export default function Grid({ gridWidth, currentLayout }) {
}
}}
snapGridAll={true}
scrollable={true}
/>
</>
);

View file

@ -415,6 +415,20 @@ export function hideGridLines() {
document.getElementById('real-canvas')?.classList.add('hide-grid');
}
export function showGridLinesOnSlot(slotId) {
var canvasElm = document.getElementById(`canvas-${slotId}`);
canvasElm.classList.remove('hide-grid');
canvasElm.classList.add('show-grid');
}
export function hideGridLinesOnSlot(slotId) {
var canvasElm = document.getElementById(`canvas-${slotId}`);
canvasElm.classList.remove('show-grid');
canvasElm.classList.add('hide-grid');
}
// Track previously active elements for efficient cleanup
let previousActiveWidgets = null;
let previousActiveCanvas = null;
@ -488,3 +502,18 @@ export const handleDeactivateTargets = () => {
component.classList.remove('non-dragging-component');
});
};
export const computeScrollDelta = ({ source }) => {
// Only need to calculate scroll delta when moving from a sub-container
if (source.slotId !== 'real-canvas') {
const subContainerWrap = document
.querySelector(`#canvas-${source.slotId}`)
?.closest('.sub-container-overflow-wrap');
return subContainerWrap?.scrollTop || 0;
}
// Default case: No scroll adjustment needed
return 0;
};
export const computeScrollDeltaOnDrag = computeScrollDelta;

View file

@ -175,7 +175,6 @@ export class DragContext {
const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot];
return !restrictedWidgets.includes(dragged.widgetType);
ß;
}
}

View file

@ -33,6 +33,7 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [
'Divider',
'VerticalDivider',
'Link',
'Form',
];
const RenderWidget = ({

View file

@ -2,6 +2,9 @@ import React from 'react';
import { Overlay, Popover } from 'react-bootstrap';
import { Button } from '@/_ui/LeftSidebar';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { ToolTip } from '@/_components/ToolTip';
import SolidIcon from '@/_ui/Icon/SolidIcons';
export const PageHandlerMenu = ({ darkMode }) => {
const setShowEditingPopover = useStore((state) => state.setShowEditingPopover);
@ -20,23 +23,9 @@ export const PageHandlerMenu = ({ darkMode }) => {
const toggleDeleteConfirmationModal = useStore((state) => state.toggleDeleteConfirmationModal);
const clonePage = useStore((state) => state.clonePage);
const markAsHomePage = useStore((state) => state.markAsHomePage);
// const popoverTargetRef = null;
// console.log(
// {
// setShowEditingPopover,
// setShowRenameHandlerModal,
// setEditingPage,
// setShowPageEventsModal,
// popoverTargetRef,
// editingPage,
// showRenameHandlerModal,
// showPageEventsModal,
// setEditingPageName,
// showEditingPopover,
// closeEditingPopover,
// },
// 'editingPage'
// );
const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
const closeMenu = () => {
closePageEditPopover();
@ -119,7 +108,6 @@ export const PageHandlerMenu = ({ darkMode }) => {
callback={() => markAsHomePage(editingPage.id)}
/>
)}
{!isDisabled && (
<Field
id={isHidden ? 'unhide-page' : 'hide-page'}
@ -132,7 +120,6 @@ export const PageHandlerMenu = ({ darkMode }) => {
disabled={isHomePage}
/>
)}
<Field
id="clone-page"
text="Duplicate page"
@ -142,7 +129,6 @@ export const PageHandlerMenu = ({ darkMode }) => {
clonePage(editingPage.id);
}}
/>
<Field
id="settings"
text="Event Handlers"
@ -164,6 +150,32 @@ export const PageHandlerMenu = ({ darkMode }) => {
}}
disabled={isHomePage}
/>
<Field
id={isDisabled ? 'enable-page' : 'disable-page'}
disabled={!licenseValid}
classNames={'page-permission-btn'}
text={() => {
return (
<ToolTip
message={'Page permissions are available only in paid plans'}
placement="right"
show={!licenseValid}
>
<div className="d-flex align-items-center">
<div>Page permission</div>
{!licenseValid && <SolidIcon name="enterprisesmall" />}
</div>
</ToolTip>
);
}}
customClass={'delete-btn'}
iconSrc={`assets/images/icons/editor/left-sidebar/authorization.svg`}
closeMenu={closeMenu}
callback={(id) => {
togglePagePermissionModal(true);
}}
/>
<Field
id="delete-page"
text="Delete page"
@ -223,7 +235,16 @@ const PageHandleField = ({ page, updatePageHandle }) => {
);
};
const Field = ({ id, text, iconSrc, customClass = '', closeMenu, disabled = false, callback = () => null }) => {
const Field = ({
id,
text,
iconSrc,
customClass = '',
classNames,
closeMenu,
disabled = false,
callback = () => null,
}) => {
const handleOnClick = (e) => {
e.preventDefault();
e.stopPropagation();
@ -233,7 +254,12 @@ const Field = ({ id, text, iconSrc, customClass = '', closeMenu, disabled = fals
return (
<div className={`field ${customClass ? ` ${customClass}` : ''}`}>
<Button.UnstyledButton onClick={handleOnClick} styles={{ height: '28px' }} disabled={disabled}>
<Button.UnstyledButton
onClick={handleOnClick}
styles={{ height: '28px' }}
classNames={classNames}
disabled={disabled}
>
<Button.Content title={text} iconSrc={iconSrc} direction="left" />
</Button.UnstyledButton>
</div>

View file

@ -16,6 +16,7 @@ import { EditModal } from './EditModal';
import { SettingsModal } from './SettingsModal';
import { DeletePageConfirmationModal } from './DeletePageConfirmationModal';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import PagePermission from './PagePermission';
export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
const showAddNewPageInput = useStore((state) => state.showAddNewPageInput);
@ -94,6 +95,7 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
>
<div>
<PageHandlerMenu darkMode={darkMode} />
{isLicensed ? <PagePermission darkMode={darkMode} /> : <></>}
<EditModal darkMode={darkMode} />
<SettingsModal darkMode={darkMode} />
<DeletePageConfirmationModal darkMode={darkMode} />

View file

@ -16,6 +16,7 @@ import { RenameInput } from './RenameInput';
import IconSelector from './IconSelector';
import { withRouter } from '@/_hoc/withRouter';
import OverflowTooltip from '@/_components/OverflowTooltip';
import { shallow } from 'zustand/shallow';
export const PageMenuItem = withRouter(
memo(({ darkMode, page, navigate }) => {
@ -27,7 +28,10 @@ export const PageMenuItem = withRouter(
const isDisabled = page?.disabled ?? false;
const [isHovered, setIsHovered] = useState(false);
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
const showEditingPopover = useStore((state) => state.showEditingPopover);
const restricted = page?.permissions && page?.permissions?.length > 0;
const {
definition: { styles, properties },
} = useStore((state) => state.pageSettings);
@ -195,8 +199,11 @@ export const PageMenuItem = withRouter(
{isHidden && !isDisabled && 'Hidden'}
</span>
</div>
{!shouldFreeze && (
<div className={cx('right', { 'handler-menu-open': showEditingPopover })}>
<div style={{ marginLeft: '8px', marginRight: 'auto' }}>
{licenseValid && restricted && <SolidIcon width="16" name="lock" fill="var(--icon-strong)" />}
</div>
<div className={cx('right', { 'handler-menu-open': showEditingPopover })}>
{!shouldFreeze && (
<button
style={{
backgroundColor: 'transparent',
@ -215,8 +222,8 @@ export const PageMenuItem = withRouter(
>
<SolidIcon width="20" dataCy={`page-menu`} name="morevertical" />
</button>
</div>
)}
)}
</div>
</>
)}
</div>

View file

@ -0,0 +1,516 @@
import React, { useEffect, useMemo, useState } from 'react';
import { components } from 'react-select';
import ModalBase from '@/_ui/Modal';
import Select from '@/_ui/Select';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import useStore from '@/AppBuilder/_stores/store';
import { appPermissionService } from '@/_services';
import { ConfirmDialog } from '@/_components';
import toast from 'react-hot-toast';
import Spinner from '@/_ui/Spinner';
const PERMISSION_TYPES = {
single: 'SINGLE',
group: 'GROUP',
all: 'ALL',
};
export default function PagePermission({ darkMode }) {
const showPagePermissionModal = useStore((state) => state.showPagePermissionModal);
const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
const editingPage = useStore((state) => state.editingPage);
const appId = useStore((state) => state.app.appId);
const selectedUserGroups = useStore((state) => state.selectedUserGroups);
const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups);
const selectedUsers = useStore((state) => state.selectedUsers);
const setSelectedUsers = useStore((state) => state.setSelectedUsers);
const pagePermission = useStore((state) => state.pagePermission);
const setPagePermission = useStore((state) => state.setPagePermission);
const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions);
const [pagePermissionType, setPagePermissionType] = useState('all');
const [showUserGroupSelect, toggleUserGroupSelect] = useState(false);
const [showUsersSelect, toggleUsersSelect] = useState(false);
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isPermissionsLoading, setPermissionsLoading] = useState(true);
const [pageToDelete, setPageToDelete] = useState(null);
const [initialSelectedGroups, setInitialSelectedGroups] = useState([]);
const [initialSelectedUsers, setInitialSelectedUsers] = useState([]);
const [initalPagePermissionType, setInitialPagePermissionType] = useState('all');
useEffect(() => {
if (!showPagePermissionModal) return;
const fetchPagePermission = () => {
appPermissionService.getPagePermission(appId, editingPage?.id || pageToDelete).then((data) => {
if (data) {
if (data[0] && data[0]?.type === PERMISSION_TYPES.group) {
const groups =
data[0]?.groups?.map((user) => ({
label: user?.permissionGroup?.name,
value: user?.permissionGroup?.id,
count: user?.permissionGroup?.count,
})) ?? [];
setPagePermissionType(data[0]?.type?.toLowerCase());
setInitialPagePermissionType(data[0]?.type?.toLowerCase());
setPagePermission(data);
toggleUserGroupSelect(true);
setPageToDelete(null);
setInitialSelectedGroups(groups);
data?.length && setSelectedUserGroups(groups);
} else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) {
const users =
data[0]?.users?.map(({ user }) => {
const firstName = user.firstName || '';
const lastName = user.lastName || '';
return {
value: user.id,
label: `${firstName} ${lastName}`.trim(),
email: user.email,
initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(),
};
}) ?? [];
setPagePermissionType(data[0]?.type?.toLowerCase());
setInitialPagePermissionType(data[0]?.type?.toLowerCase());
setPagePermission(data);
toggleUsersSelect(true);
setPageToDelete(null);
setInitialSelectedUsers(users);
data?.length && setSelectedUsers(users);
}
}
setPermissionsLoading(false);
});
};
fetchPagePermission();
}, [showPagePermissionModal, pageToDelete]);
const isSelectionUnchanged = useMemo(() => {
if (pagePermissionType === 'group') {
if (!selectedUserGroups.length) return true;
const current = selectedUserGroups
.map((g) => g.value)
.sort()
.join(',');
const initial = initialSelectedGroups
.map((g) => g.value)
.sort()
.join(',');
return current === initial;
} else if (pagePermissionType === 'single') {
if (!selectedUsers.length) return true;
const current = selectedUsers
.map((u) => u.value)
.sort()
.join(',');
const initial = initialSelectedUsers
.map((u) => u.value)
.sort()
.join(',');
return current === initial;
} else {
if (!pagePermission?.length) {
return true;
} else {
return initalPagePermissionType == pagePermissionType;
}
}
}, [
pagePermissionType,
selectedUserGroups,
initialSelectedGroups,
selectedUsers,
initialSelectedUsers,
initalPagePermissionType,
]);
const permissionTypeOptions = useMemo(
() => [
{
label: 'All users with access to the app',
value: 'all',
icon: 'globe',
},
{
label: 'Users',
value: 'single',
icon: 'user',
},
{
label: 'User groups',
value: 'group',
icon: 'usergroup',
},
],
[]
);
const handlePermissionTypeChange = (value) => {
switch (value) {
case 'group': {
toggleUserGroupSelect(true);
toggleUsersSelect(false);
setPagePermissionType('group');
break;
}
case 'single': {
toggleUsersSelect(true);
toggleUserGroupSelect(false);
setPagePermissionType('single');
break;
}
case 'all': {
toggleUsersSelect(false);
toggleUserGroupSelect(false);
setPagePermissionType('all');
}
}
};
const handlePagePermissionModalClose = () => {
togglePagePermissionModal(false);
toggleUserGroupSelect(false);
toggleUsersSelect(false);
setPagePermissionType('all');
setPagePermission(null);
setSelectedUsers([]);
setSelectedUserGroups([]);
setInitialSelectedGroups([]);
setInitialSelectedUsers([]);
};
const createPagePermission = () => {
const body = {
pageId: editingPage?.id,
type: PERMISSION_TYPES[pagePermissionType],
...(pagePermissionType === 'group'
? { groups: selectedUserGroups.map((group) => group?.value) }
: { users: selectedUsers.map((user) => user?.value) }),
};
setIsLoading(true);
appPermissionService
.createPagePermission(appId, editingPage?.id, body)
.then((data) => {
toast.success('Permission successfully created!', {
className: 'text-nowrap w-auto mw-100',
});
updatePageWithPermissions(editingPage?.id, data);
})
.catch(() => {
toast.error('Permission could not be created. Please try again!', {
className: 'text-nowrap w-auto mw-100',
});
})
.finally(() => {
setIsLoading(false);
handlePagePermissionModalClose();
});
};
const updatePagePermission = () => {
const body = {
pageId: editingPage?.id,
type: PERMISSION_TYPES[pagePermissionType],
...(pagePermissionType === 'group'
? { groups: selectedUserGroups.map((group) => group?.value) }
: { users: selectedUsers.map((user) => user?.value) }),
};
setIsLoading(true);
appPermissionService
.updatePagePermission(appId, editingPage?.id, body)
.then((data) => {
toast.success('Permission successfully updated!', {
className: 'text-nowrap w-auto mw-100',
});
updatePageWithPermissions(editingPage?.id, data);
})
.catch(() => {
toast.error('Permission could not be updated. Please try again!', {
className: 'text-nowrap w-auto mw-100',
});
})
.finally(() => {
setIsLoading(false);
handlePagePermissionModalClose();
});
};
const deletePagePermission = () => {
setIsLoading(true);
appPermissionService
.deletePagePermission(appId, pageToDelete)
.then((data) => {
toast.success('Permission successfully deleted!', {
className: 'text-nowrap w-auto mw-100',
});
updatePageWithPermissions(pageToDelete, []);
setPageToDelete(null);
})
.catch(() => {
toast.error('Permission could not be deleted. Please try again!', {
className: 'text-nowrap w-auto mw-100',
});
setShowConfirmDelete(false);
togglePagePermissionModal(true);
})
.finally(() => {
setIsLoading(false);
setShowConfirmDelete(false);
});
};
const renderPermissionTypeOptions = ({ label, icon }) => {
return (
<div className="row permission-type-select" style={{ padding: '0px 8px' }}>
<div className="col-auto">
<SolidIcon width="20" name={icon} />
</div>
<div className="col">
<span>{label}</span>
</div>
</div>
);
};
return (
<>
<ModalBase
title={
<div className="my-3">
<span className="tj-text-md font-weight-500">Page permission</span>
</div>
}
handleConfirm={!pagePermission ? createPagePermission : updatePagePermission}
show={showPagePermissionModal}
isLoading={isLoading}
handleClose={handlePagePermissionModalClose}
confirmBtnProps={{
title: pagePermission ? 'Update' : pagePermissionType === 'all' ? 'Default permission' : 'Create permission',
disabled: isPermissionsLoading || isSelectionUnchanged,
tooltipMessage: '',
}}
darkMode={darkMode}
className="page-permissions-modal"
headerAction={() =>
pagePermission && (
<span
onClick={(e) => {
setPageToDelete(editingPage?.id);
togglePagePermissionModal(false);
setShowConfirmDelete(true);
}}
>
<SolidIcon fill="var(--tomato10)" width="20" name="trash" />
</span>
)
}
>
<div className="page-permission">
{isPermissionsLoading ? (
<div className="spinner-center">
<Spinner />
</div>
) : (
<>
<div className="info-container">
<div className="col-md-1 info-btn">
<SolidIcon name="informationcircle" fill="#3E63DD" />
</div>
<div className="col-md-11">
<div className="message">
<p style={{ lineHeight: '18px' }}>
Only selected users will be allowed to access this page. Read docs to know more.
</p>
</div>
</div>
</div>
<label className="form-label">Type</label>
<Select
options={permissionTypeOptions}
value={pagePermissionType}
width={'100%'}
customOption={renderPermissionTypeOptions}
useMenuPortal={false}
onChange={handlePermissionTypeChange}
/>
{showUserGroupSelect && <UserGroupSelect />}
{showUsersSelect && <UserSelect />}
</>
)}
</div>
</ModalBase>
{showConfirmDelete && (
<ConfirmDialog
title={'Delete page permission'}
show={showConfirmDelete}
message={
'Deleting the permission will allow all users with access to the app to view this page. Are you sure you want to continue?'
}
confirmButtonLoading={isLoading}
onConfirm={() => deletePagePermission()}
onCancel={() => setShowConfirmDelete(false)}
confirmButtonText={'Delete'}
darkMode={darkMode}
confirmButtonIcon={'trash'}
confirmButtonIconWidth="20"
confirmButtonIconFill={'var(--slate3)'}
/>
)}
</>
);
}
const UserGroupSelect = () => {
const appId = useStore((state) => state.app.appId);
const selectedUserGroups = useStore((state) => state.selectedUserGroups);
const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups);
const [userGroups, setUserGroups] = useState([]);
useEffect(() => {
const fetchUserGroups = () => {
appPermissionService.getUsers(appId, 'user-groups').then((data) => {
if (data?.length) {
const groups = [];
data.map((group) => {
groups.push({ value: group.id, label: group.name, count: group.count });
});
setUserGroups(groups);
}
});
};
fetchUserGroups();
}, []);
const CustomOption = (props) => {
const { data, isFocused, isSelected } = props;
return (
<components.Option {...props}>
<div className={`user-select-option ${isFocused ? 'focused' : ''}`}>
<input
style={{ width: '1.2rem', height: '1.2rem', borderRadius: '6px !important' }}
type={'checkbox'}
className="form-check-input"
checked={isSelected}
/>
<div className="group-info">
<div className="name">{data.label}</div>
<div className="count">{data.count} users</div>
</div>
</div>
</components.Option>
);
};
return (
<div>
<label className="form-label mt-3">User groups</label>
<Select
isMulti={true}
options={userGroups}
value={selectedUserGroups}
width={'100%'}
closeMenuOnSelect={false}
components={{ Option: CustomOption, MenuList: CustomMenuList }}
useMenuPortal={false}
hideSelectedOptions={false}
onChange={(groups) => setSelectedUserGroups(groups)}
info="Only user groups with access to this application can be selected"
/>
</div>
);
};
const UserSelect = () => {
const appId = useStore((state) => state.app.appId);
const editingPage = useStore((state) => state.editingPage);
const selectedUsers = useStore((state) => state.selectedUsers);
const setSelectedUsers = useStore((state) => state.setSelectedUsers);
const [users, setUsers] = useState([]);
useEffect(() => {
const fetchUsers = () => {
appPermissionService.getUsers(appId, 'users').then((data) => {
if (data?.length) {
const users = [];
data.map((user) => {
const firstName = user.firstName || '';
const lastName = user.lastName || '';
users.push({
value: user.id,
label: `${firstName} ${lastName}`.trim(),
email: user.email,
initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(),
});
});
setUsers(users);
}
});
};
fetchUsers();
}, []);
const CustomOption = (props) => {
const { data, isFocused, isSelected } = props;
return (
<components.Option {...props}>
<div className={`user-select-option ${isFocused ? 'focused' : ''}`}>
<input
style={{ width: '1.2rem', height: '1.2rem', borderRadius: '6px !important' }}
type={'checkbox'}
className="form-check-input"
checked={isSelected}
/>
<div className="avatar">{data.initials}</div>
<div className="user-info">
<div className="name">{data.label}</div>
<div className="email">{data.email}</div>
</div>
</div>
</components.Option>
);
};
const selectStyles = {
option: (base) => ({
...base,
padding: '8px 0px',
}),
};
return (
<div>
<label className="form-label mt-3">Users</label>
<Select
isMulti={true}
options={users}
value={selectedUsers}
width={'100%'}
useMenuPortal={false}
closeMenuOnSelect={false}
components={{ Option: CustomOption, MenuList: CustomMenuList }}
styles={selectStyles}
hideSelectedOptions={false}
info="Only user with access to this application can be selected"
onChange={(users) => {
setSelectedUsers(users);
}}
/>
</div>
);
};
const CustomMenuList = (props) => {
const { info } = props.selectProps;
return (
<components.MenuList {...props}>
<div className="info-container" style={{ marginLeft: '12px', marginRight: '12px', marginTop: '8px' }}>
<div className="col-md-1 info-btn">
<SolidIcon name="informationcircle" fill="#3E63DD" />
</div>
<div className="col-md-11">
<div className="message">
<p style={{ lineHeight: '18px' }}>{info}</p>
</div>
</div>
</div>
{props.children}
</components.MenuList>
);
};

View file

@ -266,4 +266,112 @@
}
}
}
}
.page-permission {
.info-container {
display: flex;
width: auto;
height: auto;
padding: 10px 12px 8px 12px;
border: 1px solid var(--slate5);
background: var(--slate2);
border-radius: 6px 6px 6px 6px;
margin-bottom: 13px;
margin-top: 0px;
}
.permission-type-select {
align-items: center;
.col-auto {
padding-right: 0px;
}
}
}
.page-permissions-modal {
#header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.react-select__option {
padding: 8px 0px;
input {
margin-right: 10px;
}
}
.react-select__menu-list {
overflow-y: unset !important;
}
.user-select-option {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
&.focused {
background-color: #f3f4f6; // Tailwind's gray-100 vibe
}
.avatar {
background-color: var(--slate5); // light gray
color: var(--slate12); // dark text
font-weight: 500;
font-size: 16px;
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
}
.user-info {
display: flex;
flex-direction: column;
.name {
font-weight: 500;
font-size: 14px;
color: var(--slate12);
}
.email {
font-size: 12px;
color: var(--slate10);
}
}
.group-info {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
.name {
font-weight: 400;
font-size: 14px;
color: var(--slate12);
}
.count {
font-size: 12px;
color: var(--slate9);
}
}
}
}
.page-permission {
.spinner-center {
min-height: 250px;
}
}

View file

@ -124,24 +124,6 @@ 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')}`,
@ -163,6 +145,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 (validations.length > 0) {
items.push({
title: `${i18next.t('widget.common.validation', 'Validation')}`,
@ -182,25 +182,6 @@ export const baseComponentProperties = (
});
}
items.push({
title: `${i18next.t('widget.common.general', 'General')}`,
isOpen: true,
children: (
<>
{renderElement(
component,
componentMeta,
layoutPropertyChanged,
dataQueries,
'tooltip',
'general',
currentState,
allComponents
)}
</>
),
});
items.push({
title: `${i18next.t('widget.common.devices', 'Devices')}`,
isOpen: true,

View file

@ -539,6 +539,17 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
currentState,
allComponents
)}
{isMultiSelect &&
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'showAllSelectedLabel',
'properties',
currentState,
allComponents
)}
{isSortingEnabled &&
renderElement(
component,

View file

@ -15,7 +15,7 @@ const RenderPage = ({ page, currentPageId, switchPageWrapper, labelStyle, comput
console.log({ isHomePage });
const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon;
const IconElement = Icons?.[iconName] ?? Icons?.['IconFileDescription'];
return page.hidden || page.disabled ? null : (
return (page.hidden || page.disabled) && page?.restricted ? null : (
<FolderList
key={page.handle}
onClick={() => switchPageWrapper(page?.id)}
@ -142,21 +142,26 @@ const RenderPageGroup = ({
export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkMode, switchPageWrapper }) => {
// Don't render empty folders if displaying only icons
const tree = buildTree(pages, !!labelStyle?.label?.hidden);
const filteredPages = tree.filter((page) => (!page?.isPageGroup || page.children?.length > 0) && !page?.restricted);
const currentPageId = useStore((state) => state.currentPageId);
const currentPage = pages.find((page) => page.id === currentPageId);
const homePageId = useStore((state) => state.app.homePageId);
return (
<div className={cx('page-handler-wrapper viewer', { 'dark-theme': darkMode })}>
{/* <Accordion alwaysOpen defaultActiveKey={tree.map((page) => page.id)}> */}
{tree.map((page, index) => {
if (page.isPageGroup && page.children.length === 0 && labelStyle?.label?.hidden) {
{filteredPages.map((page, index) => {
if (
page.isPageGroup &&
page.children.length === 0 &&
labelStyle?.label?.hidden &&
!page.children.some((child) => child?.restricted === true)
) {
return null;
}
if (page.children && page.isPageGroup) {
if (page.children && page.isPageGroup && !page.children.some((child) => child?.restricted === true)) {
// if we are only displaying icons, we don't display the groups instead display separator to separate a page groups
const renderSeparatorTop = index !== 0 && labelStyle?.label?.hidden;
const renderSeparatorBottom = !tree[index + 1]?.isPageGroup && labelStyle?.label?.hidden;
const renderSeparatorBottom = !filteredPages[index + 1]?.isPageGroup && labelStyle?.label?.hidden;
return (
<>
{renderSeparatorTop && (
@ -193,7 +198,7 @@ export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkM
)}
</>
);
} else {
} else if (!page.isPageGroup) {
return (
<RenderPage
key={page.handle}

View file

@ -149,7 +149,7 @@ export const ViewerSidebarNavigation = ({
const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon;
// eslint-disable-next-line import/namespace
const IconElement = Icons?.[iconName] ?? Icons?.['IconFileDescription'];
return page.hidden || page.disabled ? null : (
return page.hidden || page.disabled || page?.restricted ? null : (
<FolderList
key={page.handle}
onClick={() => switchPageWrapper(page?.id)}

View file

@ -3,7 +3,7 @@ export const containerConfig = {
displayName: 'Container',
description: 'Group components',
defaultSize: {
width: 5,
width: 10,
height: 200,
},
component: 'Container',
@ -44,13 +44,19 @@ export const containerConfig = {
displayName: 'Show header',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
defaultValue: true,
},
},
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 20,
left: 1,
@ -98,15 +104,6 @@ export const containerConfig = {
},
accordian: 'container',
},
headerHeight: {
type: 'numberInput',
displayName: 'Height',
validation: {
schema: { type: 'number' },
defaultValue: 80,
},
accordian: 'header',
},
borderRadius: {
type: 'numberInput',
displayName: 'Border',
@ -154,10 +151,11 @@ export const containerConfig = {
showOnMobile: { value: '{{false}}' },
},
properties: {
showHeader: { value: `{{false}}` },
showHeader: { value: `{{true}}` },
loadingState: { value: `{{false}}` },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: `{{80}}` },
},
events: [],
styles: {

View file

@ -4,7 +4,7 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 480,
height: 450,
},
defaultChildren: [
{
@ -19,8 +19,8 @@ export const formConfig = {
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
text: 'Form',
textSize: 16,
textColor: '#000',
},
},
@ -34,203 +34,68 @@ export const formConfig = {
},
properties: ['text'],
defaultValue: {
text: 'Button2',
text: 'Submit',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
top: 40,
left: 10,
height: 30,
width: 17,
},
properties: ['text'],
styles: [
'textSize',
'fontWeight',
'fontStyle',
'textColor',
'isScrollRequired',
'lineHeight',
'textIndent',
'textAlign',
'verticalAlignment',
'decoration',
'transformation',
'letterSpacing',
'wordSpacing',
'fontVariant',
'backgroundColor',
'borderColor',
'borderRadius',
'boxShadow',
'padding',
],
defaultValue: {
text: 'User Details',
fontWeight: 'bold',
textSize: 18,
textColor: '#000',
backgroundColor: '#fff00000',
textAlign: 'left',
decoration: 'none',
transformation: 'none',
fontStyle: 'normal',
lineHeight: 1.5,
textIndent: '0',
letterSpacing: '0',
wordSpacing: '0',
fontVariant: 'normal',
verticalAlignment: 'top',
padding: 'default',
boxShadow: '0px 0px 0px 0px #00000090',
borderRadius: '0',
isScrollRequired: 'enabled',
},
},
{
componentName: 'Text',
layout: {
top: 90,
left: 10,
height: 30,
},
properties: ['text'],
styles: [
'textSize',
'fontWeight',
'fontStyle',
'textColor',
'isScrollRequired',
'lineHeight',
'textIndent',
'textAlign',
'verticalAlignment',
'decoration',
'transformation',
'letterSpacing',
'wordSpacing',
'fontVariant',
'backgroundColor',
'borderColor',
'borderRadius',
'boxShadow',
'padding',
],
defaultValue: {
text: 'Name',
fontWeight: 'normal',
textSize: 14,
textColor: '#000',
backgroundColor: '#fff00000',
textAlign: 'left',
decoration: 'none',
transformation: 'none',
fontStyle: 'normal',
lineHeight: 1.5,
textIndent: '0',
letterSpacing: '0',
wordSpacing: '0',
fontVariant: 'normal',
verticalAlignment: 'top',
padding: 'default',
boxShadow: '0px 0px 0px 0px #00000090',
borderRadius: '0',
isScrollRequired: 'enabled',
},
},
{
componentName: 'Text',
layout: {
top: 160,
left: 10,
height: 30,
},
properties: ['text'],
styles: [
'textSize',
'fontWeight',
'fontStyle',
'textColor',
'isScrollRequired',
'lineHeight',
'textIndent',
'textAlign',
'verticalAlignment',
'decoration',
'transformation',
'letterSpacing',
'wordSpacing',
'fontVariant',
'backgroundColor',
'borderColor',
'borderRadius',
'boxShadow',
'padding',
],
defaultValue: {
text: 'Age',
fontWeight: 'normal',
textSize: 14,
textColor: '#000',
backgroundColor: '#fff00000',
textAlign: 'left',
decoration: 'none',
transformation: 'none',
fontStyle: 'normal',
lineHeight: 1.5,
textIndent: '0',
letterSpacing: '0',
wordSpacing: '0',
fontVariant: 'normal',
verticalAlignment: 'top',
padding: 'default',
boxShadow: '0px 0px 0px 0px #00000090',
borderRadius: '0',
isScrollRequired: 'enabled',
},
},
{
componentName: 'TextInput',
layout: {
top: 120,
left: 10,
height: 30,
width: 25,
top: 20,
left: 5,
height: 40,
width: 31,
},
properties: ['placeholder', 'label'],
styles: ['alignment', 'width', 'auto', 'padding', 'direction'],
defaultValue: {
placeholder: 'Enter your name',
label: '',
label: 'Name',
width: '{{60}}',
direction: 'left',
alignment: 'side',
auto: '{{false}}',
padding: 'default',
},
},
{
componentName: 'NumberInput',
layout: {
top: 190,
left: 10,
height: 30,
width: 25,
top: 80,
left: 5,
height: 40,
width: 31,
},
properties: ['value', 'label'],
properties: ['placeholder', 'label'],
styles: ['alignment', 'width', 'auto', 'padding', 'direction'],
defaultValue: {
value: 24,
label: '',
placeholder: 'Age',
label: 'Age',
width: '{{60}}',
direction: 'left',
alignment: 'side',
auto: '{{false}}',
padding: 'default',
},
},
{
componentName: 'Button',
componentName: 'TextInput',
layout: {
top: 240,
left: 10,
height: 30,
width: 10,
top: 140,
left: 5,
height: 40,
width: 31,
},
properties: ['text'],
properties: ['placeholder', 'label'],
styles: ['alignment', 'width', 'auto', 'padding', 'direction'],
defaultValue: {
text: 'Submit',
placeholder: 'Tomy',
label: 'Pet name',
width: '{{60}}',
alignment: 'side',
direction: 'left',
auto: '{{false}}',
padding: 'default',
},
},
],
@ -276,6 +141,24 @@ export const formConfig = {
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
isHidden: true,
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
canvasHeight: {
type: 'numberInput',
displayName: 'Canvas height',
isHidden: true,
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
footerHeight: {
type: 'numberInput',
displayName: 'Footer height',
isHidden: true,
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
@ -294,6 +177,13 @@ export const formConfig = {
defaultValue: false,
},
},
tooltip: {
type: 'code',
displayName: 'Tooltip',
validation: { schema: { type: 'string' } },
section: 'additionalActions',
placeholder: 'Enter tooltip text',
},
},
events: {
onSubmit: { displayName: 'On submit' },
@ -316,22 +206,6 @@ export const formConfig = {
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'colorSwatches',
displayName: 'Background color',
@ -403,18 +277,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}}' },
showHeader: { value: '{{true}}' },
showFooter: { value: '{{true}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: 60 },
footerHeight: { value: 60 },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -121,6 +121,12 @@ export const multiselectV2Config = {
},
accordian: 'Options',
},
showAllSelectedLabel: {
type: 'toggle',
displayName: 'Show "All items are selected"',
validation: { schema: { type: 'boolean' }, defaultValue: true },
accordian: 'Options',
},
optionsLoadingState: {
type: 'toggle',
displayName: 'Options loading state',
@ -339,6 +345,7 @@ export const multiselectV2Config = {
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select the options' },
showAllSelectedLabel: { value: '{{true}}' },
showClearBtn: { value: '{{true}}' },
showSearchInput: { value: '{{true}}' },
visibility: { value: '{{true}}' },

View file

@ -33,7 +33,8 @@ export const Container = ({
shallow
);
const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles;
const { borderRadius, borderColor, boxShadow } = styles;
const { headerHeight = 80 } = properties;
const contentBgColor = useMemo(() => {
return {
backgroundColor:

View file

@ -0,0 +1,88 @@
import React, { useEffect } from 'react';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
import { showGridLinesOnSlot, hideGridLinesOnSlot } from '@/AppBuilder/AppCanvas/Grid/gridUtils';
import { useResizable } from '@/AppBuilder/_hooks/useMoveable';
export const HorizontalSlot = React.memo(
({
id,
height = 0,
width,
darkMode,
isDisabled,
isActive,
slotName = 'header', // 'header' or 'footer'
slotStyle = {},
onResize,
isEditing,
maxHeight,
}) => {
const parsedHeight = parseInt(height, 10);
const { getRootProps, getHandleProps, getResizeState } = useResizable({
initialHeight: parsedHeight,
initialWidth: '100%', // Now respects parent's width
minHeight: 10,
maxHeight: maxHeight || 400,
maxWidth: '100%',
stepHeight: 10, // Height will change in steps of 10px
onResize: () => {},
onDragEnd: (values) => {
onResize(values);
},
isReverseVerticalDrag: slotName === 'footer', // Reverse dragging for Footer
});
const { height: resizedHeight, isDragging } = getResizeState();
useEffect(() => {
if (isDragging) {
showGridLinesOnSlot(id);
} else {
hideGridLinesOnSlot(id);
}
}, [isDragging, id]);
const canvasHeight = parseInt(resizedHeight, 10) / 10;
const resizeStyle = {
backgroundColor: darkMode ? '#1F2837' : '#fff',
};
return (
<div className={`jet-form-${slotName} wj-form-${slotName}`} style={slotStyle}>
<div
className={`resizable-slot only-${slotName} ${isActive ? 'active' : ''} ${isEditing && 'is-editing'} ${
isDragging ? 'dragging' : ''
}`}
{...getRootProps()}
>
<SubContainer
id={id}
canvasHeight={canvasHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
margin: 0,
backgroundColor: 'transparent',
overflow: 'hidden',
}}
componentType="Form"
/>
{isEditing && <div className="resize-handle" {...getHandleProps()} style={resizeStyle} />}
</div>
{isDisabled && (
<div
id={`${id}-disabled`}
className="tj-form-disabled-overlay"
style={{ height: resizedHeight || '100%' }}
onClick={() => {}}
onDrop={(e) => e.stopPropagation()}
/>
)}
</div>
);
}
);

View file

@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect } from 'react';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
// eslint-disable-next-line import/no-unresolved
import _, { debounce, omit } from 'lodash';
import { generateUIComponents } from './FormUtils';
import { generateUIComponents, getBodyHeight } from './FormUtils';
import { useMounted } from '@/_hooks/use-mount';
import { onComponentClick, removeFunctionObjects } from '@/_helpers/appUtils';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
@ -14,12 +14,10 @@ import {
CONTAINER_FORM_CANVAS_PADDING,
SUBCONTAINER_CANVAS_BORDER_WIDTH,
} from '@/AppBuilder/AppCanvas/appCanvasConstants';
import './form.scss';
import { HorizontalSlot } from './Components/HorizontalSlot';
import { useActiveSlot } from '@/AppBuilder/_hooks/useActiveSlot';
const getCanvasHeight = (height) => {
const parsedHeight = height.includes('px') ? parseInt(height, 10) : height;
return Math.ceil(parsedHeight);
};
import './form.scss';
export const Form = function Form(props) {
const {
@ -35,26 +33,19 @@ export const Form = function Form(props) {
properties,
resetComponent = () => {},
dataCy,
onComponentClick,
} = props;
const childComponents = useStore((state) => state.getChildComponents(id), shallow);
const {
borderRadius,
borderColor,
boxShadow,
headerHeight,
footerHeight,
footerBackgroundColor,
headerBackgroundColor,
} = styles;
const { borderRadius, borderColor, boxShadow, footerBackgroundColor, headerBackgroundColor } = styles;
const {
buttonToSubmit,
loadingState,
advanced,
JSONSchema,
showHeader = false,
showFooter = false,
visibility,
disabledState,
headerHeight = 80,
footerHeight = 80,
canvasHeight,
} = properties;
const { isDisabled, isVisible, isLoading } = useExposeState(
properties.loadingState,
@ -65,6 +56,10 @@ export const Form = function Form(props) {
);
const backgroundColor =
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor;
const computedFormBodyHeight = getBodyHeight(height, showHeader, showFooter, headerHeight, footerHeight);
const computedBorderRadius = `${borderRadius ? parseFloat(borderRadius) : 0}px`;
const computedStyles = {
backgroundColor,
borderRadius: borderRadius ? parseFloat(borderRadius) : 0,
@ -74,16 +69,7 @@ export const Form = function Form(props) {
position: 'relative',
boxShadow,
flexDirection: 'column',
};
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,
clipPath: `inset(0 round ${computedBorderRadius})`,
};
const formContent = {
@ -96,13 +82,6 @@ export const Form = function Form(props) {
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({});
@ -110,8 +89,6 @@ 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 = {
@ -287,9 +264,61 @@ export const Form = function Form(props) {
setChildrenData(childDataRef.current);
};
const mode = useStore((state) => state.currentMode, shallow);
const isEditing = mode === 'edit';
const activeSlot = useActiveSlot(isEditing ? id : null); // Track the active slot for this widget
const setComponentProperty = useStore((state) => state.setComponentProperty, shallow);
// const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight);
const updateHeaderSizeInStore = ({ newHeight }) => {
const _height = parseInt(newHeight, 10);
setComponentProperty(id, `headerHeight`, _height, 'properties', 'value', false);
};
const updateFooterSizeInStore = ({ newHeight }) => {
const _height = parseInt(newHeight, 10);
setComponentProperty(id, `footerHeight`, _height, 'properties', 'value', false);
};
const [canHeight, setCanHeight] = useState('100%');
useEffect(() => {
// const newHeight = parseInt(height, 10) - 14;
// const autoCanvasHeight = document.querySelector(`#canvas-${id}`)?.scrollHeight;
const wrapHeight = parseInt(computedFormBodyHeight, 10);
// Set height to the larger value between computed body height and canvas scroll height
const maxHeight = Math.max(wrapHeight, canvasHeight || 10);
const roundedHeight = Math.round(maxHeight / 10) * 10;
setCanHeight(`${roundedHeight}px`);
}, [computedFormBodyHeight, canvasHeight]);
const headerMaxHeight = parseInt(height, 10) - parseInt(footerHeight, 10) - 100 - 10;
const footerMaxHeight = parseInt(height, 10) - parseInt(headerHeight, 10) - 100 - 10;
const formFooter = {
flexShrink: 0,
paddingTop: '3px',
paddingBottom: '7px',
paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
maxHeight: `${footerMaxHeight}px`,
backgroundColor:
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
};
const formHeader = {
flexShrink: 0,
paddingBottom: '3px',
paddingTop: '7px',
paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`,
paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`,
maxHeight: `${headerMaxHeight}px`,
backgroundColor:
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
};
return (
<form
className={`jet-container ${advanced && 'jet-container-json-form'}`}
className={`jet-container jet-form-widget ${advanced && 'jet-container-json-form'}`}
id={id}
data-cy={dataCy}
ref={parentRef}
@ -300,31 +329,21 @@ export const Form = function Form(props) {
}} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked
>
{!advanced && 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()}
/>
)}
</div>
<HorizontalSlot
slotName="header"
slotStyle={formHeader}
isEditing={isEditing}
id={`${id}-header`}
height={headerHeight}
width={width}
darkMode={darkMode}
isDisabled={isDisabled}
isActive={activeSlot === `${id}-header`}
onResize={updateHeaderSizeInStore}
/>
)}
<div className="jet-form-body" style={formContent}>
<div className="jet-form-body sub-container-overflow-wrap" 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>
@ -332,14 +351,17 @@ export const Form = function Form(props) {
) : (
<fieldset disabled={isDisabled} style={{ width: '100%' }}>
{!advanced && (
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: '100%' }}>
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: canHeight || '100%' }}>
<SubContainer
id={id}
canvasHeight={computedStyles.height}
canvasHeight={parseInt(computedFormBodyHeight, 10)}
canvasWidth={width}
onOptionChange={onOptionChange}
onOptionsChange={onOptionsChange}
styles={{ backgroundColor: computedStyles.backgroundColor }}
styles={{
backgroundColor: computedStyles.backgroundColor,
height: canHeight,
}}
darkMode={darkMode}
componentType="Form"
/>
@ -382,30 +404,18 @@ export const Form = function Form(props) {
)}
</div>
{!advanced && 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>
<HorizontalSlot
slotName="footer"
slotStyle={formFooter}
isEditing={isEditing}
id={`${id}-footer`}
height={footerHeight}
width={width}
darkMode={darkMode}
isDisabled={isDisabled}
onResize={updateFooterSizeInStore}
isActive={activeSlot === `${id}-footer`}
/>
)}
</form>
);

View file

@ -533,3 +533,22 @@ const validBooleanChecker = (input) => {
if (/^(true|false)$/i.test(input) == true) return JSON.parse(input);
return true;
};
export const getBodyHeight = (height, showHeader, showFooter, headerHeight = 60, footerHeight = 60) => {
let modalHeight = height ? parseInt(height, 10) : 0;
let parsedHeaderHeight = showHeader ? parseInt(headerHeight, 10) : 0;
let parsedFooterHeight = showFooter ? parseInt(footerHeight, 10) : 0;
if (showHeader) {
// 10 is header padding
modalHeight = modalHeight - parsedHeaderHeight - 10;
}
if (showFooter) {
// 14 is footer padding
modalHeight = modalHeight - parsedFooterHeight - 14;
}
const rounded = Math.ceil(modalHeight / 10) * 10;
return `${Math.max(rounded - 20, 40)}px`;
};

View file

@ -1,11 +1,17 @@
.jet-form-widget {
display: flex;
flex-direction: column;
height: 100%;
}
.wj-form-header {
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0;
left: -7px;
right: -7px;
left: -2px;
right: -2px;
height: 1px;
background-color: var(--border-weak);
}
@ -17,8 +23,8 @@
content: "";
position: absolute;
top: 0;
left: -7px;
right: -7px;
left: -2px;
right: -2px;
height: 1px;
background-color: var(--border-weak);
}
@ -39,6 +45,67 @@
padding: 4px 0;
}
.resizable-slot {
position: relative;
height: auto;
box-shadow: 0 0 0 1px transparent; /* Acts as a border */
transition: box-shadow 0.15s ease-in-out;
max-height: 100%;
&.is-editing:hover {
box-shadow: 0 0 0 1px var(--border-weak);
}
&.is-editing.active {
box-shadow: 0 0 0 1px var(--border-accent-weak);
}
&.is-editing.dragging {
box-shadow: 0 0 0 1px var(--border-accent-strong);
}
.resize-handle {
position: absolute;
bottom: -4px;
left: 50%; /* Center horizontally */
transform: translateX(-50%); /* Ensure proper centering */
width: 24px;
height: 8px;
border-radius: 4px;
background-color: initial;
border: 1px solid var(--background-accent-strong);
cursor: ns-resize;
z-index: 1;
visibility: hidden;
transition: visibility 0.15s ease-in-out;
}
&.active .resize-handle {
visibility: visible;
}
}
.only-bottom {
}
.jet-form-header {
min-height: 10px;
}
.jet-form-body {
min-height: 100px;
background-color: inherit;
}
.jet-form-footer {
min-height: 10px;
}
.jet-form-footer .resize-handle {
top: -4px;
bottom: unset;
}
.jet-container.jet-container-json-form {
padding: 0px;

View file

@ -0,0 +1,84 @@
import { useState, useEffect } from 'react';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
const useIsWidgetSelected = (id) => {
// Get selected components from store using shallow comparison
const selectedComponents = useStore((state) => state.selectedComponents, shallow);
// Check if the only selected component is the provided `id`
return selectedComponents.length === 1 && selectedComponents[0] === id;
};
export const useActiveSlot = (widgetId) => {
const [activeSlot, setActiveSlot] = useState(''); // Default to widget ID
const isSelected = useIsWidgetSelected(widgetId); // Check if widget is selected
useEffect(() => {
if (!isSelected) {
setActiveSlot('');
}
}, [isSelected]);
useEffect(() => {
const handleDoubleClick = (event) => {
let target = event.target;
if (!widgetId) {
setActiveSlot(null);
return;
}
// Traverse up to find a slot with an id
while (target && target !== document.body) {
if (target.id && target.id.startsWith('canvas-')) {
const slotId = target.id.replace(/^canvas-/, ''); // ✅ Strip "canvas-"
setActiveSlot(slotId);
return;
}
target = target.parentElement;
}
// If no slot is found, reset to widget ID
setActiveSlot(widgetId);
};
const handleSingleClick = (event) => {
let target = event.target;
if (!widgetId) {
setActiveSlot(null);
return;
}
// Traverse up to find a valid main slot (not header/footer)
while (target && target !== document.body) {
if (
target.id &&
target.id.startsWith('canvas-') &&
!target.id.endsWith('-header') &&
!target.id.endsWith('-footer')
) {
const slotId = target.id.replace(/^canvas-/, ''); // Strip "canvas-"
setActiveSlot(slotId);
return;
}
target = target.parentElement;
}
// If no main slot is found, fallback to widget ID
setActiveSlot(widgetId);
};
// Attach single click if the widget is selected, otherwise listen for double-click
document.addEventListener('dblclick', handleDoubleClick);
document.addEventListener('click', handleSingleClick);
return () => {
document.removeEventListener('dblclick', handleDoubleClick);
document.removeEventListener('click', handleSingleClick);
};
}, [widgetId]); // Re-run when widgetId or selection state changes
return activeSlot;
};

View file

@ -28,6 +28,8 @@ import { baseTheme, convertAllKeysToSnakeCase } from '../_stores/utils';
import { getPreviewQueryParams } from '@/_helpers/routes';
import { useLocation, useMatch, useParams } from 'react-router-dom';
import useThemeAccess from './useThemeAccess';
import { handleError } from '@/_helpers/handleAppAccess';
import toast from 'react-hot-toast';
/**
* this is to normalize the query transformation options to match the expected schema. Takes care of corrupted data.
@ -214,224 +216,248 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
}
// const appDataPromise = appService.fetchApp(appId);
appDataPromise.then(async (result) => {
let appData = { ...result };
let editorEnvironment = result.editorEnvironment;
if (isPreviewForVersion) {
const rawDataQueries = appData?.data_queries;
const rawEditingVersionDataQueries = appData?.editing_version?.data_queries;
appData = convertAllKeysToSnakeCase(appData);
appDataPromise
.then(async (result) => {
let appData = { ...result };
let editorEnvironment = result.editorEnvironment;
if (isPreviewForVersion) {
const rawDataQueries = appData?.data_queries;
const rawEditingVersionDataQueries = appData?.editing_version?.data_queries;
appData = convertAllKeysToSnakeCase(appData);
appData.data_queries = rawDataQueries;
if (appData.editing_version && rawEditingVersionDataQueries) {
appData.editing_version.data_queries = rawEditingVersionDataQueries;
}
appData.data_queries = rawDataQueries;
if (appData.editing_version && rawEditingVersionDataQueries) {
appData.editing_version.data_queries = rawEditingVersionDataQueries;
}
editorEnvironment = {
id: environmentId,
name: queryParams.env,
};
}
let constantsResp;
if (mode !== 'edit') {
try {
const queryParams = { slug: slug };
const viewerEnvironment = await appEnvironmentService.getEnvironment(environmentId, queryParams);
editorEnvironment = {
id: viewerEnvironment?.environment?.id,
name: viewerEnvironment?.environment?.name,
id: environmentId,
name: queryParams.env,
};
constantsResp =
isPublicAccess && appData.is_public
? await orgEnvironmentConstantService.getConstantsFromPublicApp(slug, viewerEnvironment?.environment?.id)
: await orgEnvironmentConstantService.getConstantsFromApp(slug, viewerEnvironment?.environment?.id);
} catch (error) {
console.error('Error fetching viewer environment:', error);
}
}
if (mode === 'edit') {
constantsResp = await orgEnvironmentConstantService.getConstantsFromEnvironment(editorEnvironment?.id);
}
// get the constants for specific environment
constantsResp.constants = extractEnvironmentConstantsFromConstantsList(
constantsResp?.constants,
editorEnvironment?.name
);
setIsPublicAccess(isPublicAccess && mode !== 'edit' && appData.is_public);
fetchAndInjectCustomStyles(isPublicAccess && mode !== 'edit' && appData.is_public);
const pages = appData.pages.map((page) => {
return page;
});
const conversation = appData.ai_conversation;
const docsConversation = appData.ai_conversation_learn;
if (setConversation && setDocsConversation) {
setConversation(conversation);
setDocsConversation(docsConversation);
// important to control ai inputs
getCreditBalance();
}
let showWalkthrough = true;
// if app was created from propmt, and no earlier messages are present in the conversation, send the prompt message
// handles the getappdataby slug api call. Gets the homePageId from the appData.
const homePageId =
appData.editing_version?.homePageId || appData.editing_version?.home_page_id || appData.home_page_id;
setApp({
appName: appData.name,
appId: appData.id,
slug: appData.slug,
currentAppEnvironmentId: editorEnvironment.id,
isMaintenanceOn:
'is_maintenance_on' in result
? result.is_maintenance_on
: 'isMaintenanceOn' in result
? result.isMaintenanceOn
: false,
organizationId: appData.organizationId || appData.organization_id,
homePageId: homePageId,
isPublic: appData.is_public,
creationMode: appData.creation_mode,
});
setIsEditorFreezed(appData.should_freeze_editor);
const global_settings = mapKeys(
appData.editing_version?.global_settings || appData.global_settings,
(value, key) => camelCase(key)
);
if (!global_settings?.theme) {
global_settings.theme = baseTheme;
}
setGlobalSettings(global_settings);
setPages(pages, moduleId);
setPageSettings(
computePageSettings(deepCamelCase(appData?.editing_version?.page_settings ?? appData?.page_settings))
);
// set starting page as homepage initially
let startingPage = appData.pages.find((page) => page.id === homePageId);
if (initialLoadRef.current) {
// if initial load, check if the path has a page handle and set that as the starting page
const initialLoadPath = location.pathname.split('/').pop();
const page = appData.pages.find((page) => page.handle === initialLoadPath && !page.isPageGroup);
if (page) {
// if page is disabled, and not editing redirect to home page
if (mode !== 'edit' && page?.disabled) {
const currentUrl = window.location.href;
const replacedUrl = currentUrl.replace(initialLoadPath, startingPage.handle);
window.history.replaceState(null, null, replacedUrl);
} else {
startingPage = page;
let constantsResp;
if (mode !== 'edit') {
try {
const queryParams = { slug: slug };
const viewerEnvironment = await appEnvironmentService.getEnvironment(environmentId, queryParams);
editorEnvironment = {
id: viewerEnvironment?.environment?.id,
name: viewerEnvironment?.environment?.name,
};
constantsResp =
isPublicAccess && appData.is_public
? await orgEnvironmentConstantService.getConstantsFromPublicApp(
slug,
viewerEnvironment?.environment?.id
)
: await orgEnvironmentConstantService.getConstantsFromApp(slug, viewerEnvironment?.environment?.id);
} catch (error) {
console.error('Error fetching viewer environment:', error);
}
}
// navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`);
}
// Add page id and handle to the state on initial load
const currentState = window.history.state || {};
const pageInfo = {
id: startingPage.id,
handle: startingPage.handle,
};
const newState = { ...currentState, ...pageInfo };
window.history.replaceState(newState, '', window.location.href);
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);
setAppHomePageId(homePageId);
const queryData =
isPublicAccess || (mode !== 'edit' && appData.is_public)
? appData
: await dataqueryService.getAll(appData.editing_version?.id || appData.current_version_id);
const dataQueries = queryData.data_queries || queryData?.editing_version?.data_queries;
dataQueries.forEach((query) => normalizeQueryTransformationOptions(query));
setQueries(dataQueries);
if (dataQueries?.length > 0) {
setSelectedQuery(dataQueries[0]?.id);
initialiseResolvedQuery(dataQueries.map((query) => query.id));
}
const constants = constantsResp?.constants;
if (constants) {
const orgConstants = {};
const orgSecrets = {};
constants.map((constant) => {
if (constant.type !== 'Secret') {
orgConstants[constant.name] = constant.value;
} else {
orgSecrets[constant.name] = constant.value;
}
});
setResolvedConstants(orgConstants);
setSecrets(orgSecrets);
}
setQueryMapping(moduleId);
setResolvedGlobals('environment', editorEnvironment);
setResolvedGlobals('mode', { value: mode });
setResolvedGlobals('currentUser', {
...user,
groups: currentSession?.groups,
role: currentSession?.role?.name,
ssoUserInfo: currentSession?.ssoUserInfo,
...(currentSession?.currentUser?.metadata && !isEmpty(currentSession?.currentUser?.metadata)
? { metadata: currentSession?.currentUser?.metadata }
: {}),
});
setResolvedGlobals('urlparams', JSON.parse(JSON.stringify(queryString.parse(location?.search))));
initDependencyGraph(moduleId);
setCurrentMode(mode); // TODO: set mode based on the slug/appDef
if (
state.ai &&
state?.prompt &&
initialLoadRef.current &&
(conversation?.aiConversationMessages || []).length === 0
) {
setSelectedSidebarItem('tooljetai');
toggleLeftSidebar('true');
sendMessage(state.prompt);
setConversationZeroState(true);
showWalkthrough = false;
}
// fetchDataSources(appData.editing_version.id, editorEnvironment.id);
if (!isPublicAccess) {
const envFromQueryParams = mode === 'view' && new URLSearchParams(location?.search)?.get('env');
useStore.getState().init(appData.editing_version?.id || appData.current_version_id, envFromQueryParams);
fetchGlobalDataSources(
appData.organization_id,
appData.editing_version?.id || appData.current_version_id,
editorEnvironment.id
if (mode === 'edit') {
constantsResp = await orgEnvironmentConstantService.getConstantsFromEnvironment(editorEnvironment?.id);
}
// get the constants for specific environment
constantsResp.constants = extractEnvironmentConstantsFromConstantsList(
constantsResp?.constants,
editorEnvironment?.name
);
}
useStore.getState().updateEditingVersion(appData.editing_version?.id || appData.current_version_id); //check if this is needed
updateReleasedVersionId(appData.current_version_id);
setEditorLoading(false);
initialLoadRef.current = false;
// only show if app is not created from prompt
if (showWalkthrough) initEditorWalkThrough();
checkAndSetTrueBuildSuggestionsFlag();
return () => {
document.title = retrieveWhiteLabelText();
};
});
setIsPublicAccess(isPublicAccess && mode !== 'edit' && appData.is_public);
fetchAndInjectCustomStyles(isPublicAccess && mode !== 'edit' && appData.is_public);
const pages = appData.pages.map((page) => {
return page;
});
const conversation = appData.ai_conversation;
const docsConversation = appData.ai_conversation_learn;
if (setConversation && setDocsConversation) {
setConversation(conversation);
setDocsConversation(docsConversation);
// important to control ai inputs
getCreditBalance();
}
let showWalkthrough = true;
// if app was created from propmt, and no earlier messages are present in the conversation, send the prompt message
// handles the getappdataby slug api call. Gets the homePageId from the appData.
const homePageId =
appData.editing_version?.homePageId || appData.editing_version?.home_page_id || appData.home_page_id;
setApp({
appName: appData.name,
appId: appData.id,
slug: appData.slug,
currentAppEnvironmentId: editorEnvironment.id,
isMaintenanceOn:
'is_maintenance_on' in result
? result.is_maintenance_on
: 'isMaintenanceOn' in result
? result.isMaintenanceOn
: false,
organizationId: appData.organizationId || appData.organization_id,
homePageId: homePageId,
isPublic: appData.is_public,
creationMode: appData.creation_mode,
});
setIsEditorFreezed(appData.should_freeze_editor);
const global_settings = mapKeys(
appData.editing_version?.global_settings || appData.global_settings,
(value, key) => camelCase(key)
);
if (!global_settings?.theme) {
global_settings.theme = baseTheme;
}
setGlobalSettings(global_settings);
setPages(pages, moduleId);
setPageSettings(
computePageSettings(deepCamelCase(appData?.editing_version?.page_settings ?? appData?.page_settings))
);
// set starting page as homepage initially
let startingPage = appData.pages.find((page) => page.id === homePageId);
//no access to homepage, set to the next available page
if (startingPage?.restricted) {
startingPage = appData.pages.find((page) => !page?.restricted);
}
if (initialLoadRef.current) {
// if initial load, check if the path has a page handle and set that as the starting page
const initialLoadPath = location.pathname.split('/').pop();
const page = appData.pages.find((page) => page.handle === initialLoadPath && !page.isPageGroup);
if (page) {
// if page is disabled, and not editing redirect to home page
const shouldRedirect = page?.restricted || (mode !== 'edit' && page?.disabled);
if (shouldRedirect) {
const newUrl = window.location.href.replace(initialLoadPath, startingPage.handle);
window.history.replaceState(null, null, newUrl);
if (page?.restricted) {
toast.error('Access to this page is restricted. Contact admin to know more.', {
className: 'text-nowrap w-auto mw-100',
});
}
} else {
startingPage = page;
}
}
// navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`);
}
// Add page id and handle to the state on initial load
const currentState = window.history.state || {};
const pageInfo = {
id: startingPage.id,
handle: startingPage.handle,
};
const newState = { ...currentState, ...pageInfo };
window.history.replaceState(newState, '', window.location.href);
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);
setAppHomePageId(homePageId);
const queryData =
isPublicAccess || (mode !== 'edit' && appData.is_public)
? appData
: await dataqueryService.getAll(appData.editing_version?.id || appData.current_version_id);
const dataQueries = queryData.data_queries || queryData?.editing_version?.data_queries;
dataQueries.forEach((query) => normalizeQueryTransformationOptions(query));
setQueries(dataQueries);
if (dataQueries?.length > 0) {
setSelectedQuery(dataQueries[0]?.id);
initialiseResolvedQuery(dataQueries.map((query) => query.id));
}
const constants = constantsResp?.constants;
if (constants) {
const orgConstants = {};
const orgSecrets = {};
constants.map((constant) => {
if (constant.type !== 'Secret') {
orgConstants[constant.name] = constant.value;
} else {
orgSecrets[constant.name] = constant.value;
}
});
setResolvedConstants(orgConstants);
setSecrets(orgSecrets);
}
setQueryMapping(moduleId);
setResolvedGlobals('environment', editorEnvironment);
setResolvedGlobals('mode', { value: mode });
setResolvedGlobals('currentUser', {
...user,
groups: currentSession?.groups,
role: currentSession?.role?.name,
ssoUserInfo: currentSession?.ssoUserInfo,
...(currentSession?.currentUser?.metadata && !isEmpty(currentSession?.currentUser?.metadata)
? { metadata: currentSession?.currentUser?.metadata }
: {}),
});
setResolvedGlobals('urlparams', JSON.parse(JSON.stringify(queryString.parse(location?.search))));
initDependencyGraph(moduleId);
setCurrentMode(mode); // TODO: set mode based on the slug/appDef
if (
state.ai &&
state?.prompt &&
initialLoadRef.current &&
(conversation?.aiConversationMessages || []).length === 0
) {
setSelectedSidebarItem('tooljetai');
toggleLeftSidebar('true');
sendMessage(state.prompt);
setConversationZeroState(true);
showWalkthrough = false;
}
// fetchDataSources(appData.editing_version.id, editorEnvironment.id);
if (!isPublicAccess) {
const envFromQueryParams = mode === 'view' && new URLSearchParams(location?.search)?.get('env');
useStore.getState().init(appData.editing_version?.id || appData.current_version_id, envFromQueryParams);
fetchGlobalDataSources(
appData.organization_id,
appData.editing_version?.id || appData.current_version_id,
editorEnvironment.id
);
}
useStore.getState().updateEditingVersion(appData.editing_version?.id || appData.current_version_id); //check if this is needed
updateReleasedVersionId(appData.current_version_id);
setEditorLoading(false);
initialLoadRef.current = false;
// only show if app is not created from prompt
if (showWalkthrough) initEditorWalkThrough();
checkAndSetTrueBuildSuggestionsFlag();
return () => {
document.title = retrieveWhiteLabelText();
};
})
.catch((error) => {
if (isPublicAccess) {
if (mode !== 'edit') {
handleError('view', error);
}
}
});
}, [setApp, setEditorLoading, currentSession]);
useEffect(() => {

View file

@ -0,0 +1,135 @@
import { useRef, useState } from 'react';
const defaultProps = {
minHeight: 50,
maxHeight: 600,
minWidth: 50,
maxWidth: 600,
lockHorizontal: false,
lockVertical: false,
stepHeight: 10, // Default step size for height
stepWidth: 10, // Default step size for width
onResize: null,
onDragStart: null,
onDragEnd: null,
isReverseVerticalDrag: false,
};
export const useResizable = (options = {}) => {
const props = { ...defaultProps, ...options };
const parentRef = useRef(null);
const [isDragging, setIsDragging] = useState(false); // ✅ Track dragging state
const [height, setHeight] = useState(
typeof props.initialHeight === 'string' ? props.initialHeight : `${props.initialHeight || 200}px`
);
const [width, setWidth] = useState(
typeof props.initialWidth === 'string' ? props.initialWidth : `${props.initialWidth || 200}px`
);
const getRootProps = () => ({
ref: parentRef,
style: { height, width },
});
const getResizeState = () => ({
height,
width,
isDragging,
});
const getHandleProps = () => {
const handleMouseDown = (e) => {
// Prevent right-click drag activation (button === 2)
if (e.button === 2) return;
if (!parentRef.current) return;
e.stopPropagation();
e.preventDefault();
const startHeight = parseInt(parentRef.current.clientHeight);
const startWidth = parseInt(parentRef.current.clientWidth);
const parentWidth = parentRef.current.parentElement ? parentRef.current.parentElement.clientWidth : startWidth;
const startY = e.clientY;
const startX = e.clientX;
const isPercentage = typeof props.initialWidth === 'string' && props.initialWidth.includes('%');
setIsDragging(true); // ✅ Set dragging state to true
if (props.onDragStart) {
props.onDragStart({ newHeight: startHeight, newWidth: startWidth });
}
const handleMouseMove = (moveEvent) => {
moveEvent.stopPropagation();
moveEvent.preventDefault();
let newHeight = startHeight;
let newWidth = startWidth;
if (!props.lockVertical) {
const deltaY = props.isReverseVerticalDrag ? startY - moveEvent.clientY : moveEvent.clientY - startY;
newHeight = startHeight + deltaY;
newHeight = Math.max(props.minHeight, Math.min(props.maxHeight, newHeight));
newHeight = Math.round(newHeight / props.stepHeight) * props.stepHeight; // Snap to stepHeight
}
if (!props.lockHorizontal) {
newWidth = startWidth + (moveEvent.clientX - startX);
newWidth = Math.max(props.minWidth, Math.min(props.maxWidth, newWidth));
newWidth = Math.round(newWidth / props.stepWidth) * props.stepWidth; // Snap to stepWidth
if (isPercentage) {
newWidth = (newWidth / parentWidth) * 100; // Convert to percentage
newWidth = `${newWidth.toFixed(2)}%`;
} else {
newWidth = `${newWidth}px`;
}
}
setHeight(`${newHeight}px`);
setWidth(newWidth);
if (parentRef.current) {
parentRef.current.style.height = `${newHeight}px`;
parentRef.current.style.width = newWidth;
}
if (props.onResize) {
props.onResize({
newHeight,
newWidth,
heightDiff: newHeight - startHeight,
widthDiff: isPercentage
? parseInt(newWidth) - (startWidth / parentWidth) * 100
: parseInt(newWidth) - startWidth,
});
}
};
const handleMouseUp = () => {
setIsDragging(false); // ✅ Set dragging state to false
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
if (props.onDragEnd) {
// Get the updated height and width from the DOM instead of relying on state
const finalHeight = parentRef.current ? parseInt(parentRef.current.clientHeight) : parseInt(height);
const finalWidth = parentRef.current ? parseInt(parentRef.current.clientWidth) : parseInt(width);
props.onDragEnd({ newHeight: finalHeight, newWidth: finalWidth });
}
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
return {
onMouseDown: handleMouseDown,
};
};
return { rootRef: parentRef, getRootProps, getHandleProps, getResizeState };
};
export default useResizable;

View file

@ -1896,4 +1896,28 @@ export const createComponentsSlice = (set, get) => ({
state.modalsOpenOnCanvas = newModalOpenOnCanvas;
});
},
updateContainerAutoHeight: (componentId) => {
if (
!componentId ||
componentId === 'canvas' ||
componentId.includes('-header') ||
componentId.includes('-footer')
) {
return;
}
const { currentLayout, getCurrentPageComponents, setComponentProperty } = get();
const allComponents = getCurrentPageComponents();
const childComponents = getAllChildComponents(allComponents, componentId);
const maxHeight = Object.values(childComponents).reduce((max, component) => {
const layout = component?.layouts?.[currentLayout];
if (!layout) {
return max;
}
const sum = layout.top + layout.height;
return Math.max(max, sum);
}, 0);
setComponentProperty(componentId, `canvasHeight`, maxHeight, 'properties', 'value', false);
},
});

View file

@ -860,7 +860,9 @@ export const createEventsSlice = (set, get) => ({
const { switchPage } = get();
const page = get().modules.canvas.pages.find((page) => page.id === event.pageId);
const queryParams = event.queryParams || [];
if (!page.disabled) {
if (page.restricted && mode !== 'edit') {
toast.error('Access to this page is restricted. Contact admin to know more.');
} else if (!page.disabled) {
const resolvedQueryParams = [];
queryParams.forEach((param) => {
resolvedQueryParams.push([

View file

@ -41,7 +41,7 @@ export const savePageChanges = async (appId, versionId, pageId, diff, operation
};
const createPageUpdateCommand =
(updatePaths, afterUpdateFn = () => {}) =>
(updatePaths, afterUpdateFn = () => {}, enableSave = true) =>
(pageId, values) => {
return (set, get) => {
set((state) => {
@ -57,7 +57,7 @@ const createPageUpdateCommand =
const { app, currentVersionId } = get();
const diff = _.zipObject(updatePaths, values);
savePageChanges(app.appId, currentVersionId, pageId, diff);
if (enableSave) savePageChanges(app.appId, currentVersionId, pageId, diff);
};
};
@ -82,6 +82,8 @@ export const createPageMenuSlice = (set, get) => {
state.editingPage = null;
});
const updatePageWithPermissions = createPageUpdateCommand(['permissions'], (state) => {}, false);
return {
editingPage: null,
showEditingPopover: false,
@ -96,6 +98,11 @@ export const createPageMenuSlice = (set, get) => {
isPageGroup: false,
pageSettingSelected: false,
pageSettings: {},
showPagePermissionModal: false,
permissionPage: null,
selectedUserGroups: [],
selectedUsers: [],
pagePermission: null,
toggleSearch: (show) =>
set((state) => {
@ -117,7 +124,6 @@ export const createPageMenuSlice = (set, get) => {
closePageEditPopover: () =>
set((state) => {
state.editingPage = null;
state.showEditingPopover = false;
state.showEditPageEventsModal = false;
state.showRenamePageHandleModal = false;
@ -190,6 +196,7 @@ export const createPageMenuSlice = (set, get) => {
updatePageHandle(pageId, [value])(set, get);
},
updatePageGroupName: (pageId, value) => updatePageGroupName(pageId, [value])(set, get),
updatePageWithPermissions: (pageId, value) => updatePageWithPermissions(pageId, [value])(set, get),
// unsure about this one
clonePage: async (pageId) => {
const {
@ -419,5 +426,30 @@ export const createPageMenuSlice = (set, get) => {
console.error('Error updating page:', error);
}
},
setPagePermission: (pagePermission) =>
set((state) => {
state.pagePermission = pagePermission;
}),
togglePagePermissionModal: (show) => {
set((state) => {
state.showPagePermissionModal = show;
});
},
setSelectedUserGroups: (groups) =>
set((state) => {
state.selectedUserGroups = groups;
}),
setSelectedUsers: (users) =>
set((state) => {
state.selectedUsers = users;
}),
setEditingPage: (page) =>
set((state) => {
state.editingPage = page;
}),
};
};

View file

@ -36,7 +36,7 @@ const CustomValueContainer = ({ children, ...props }) => {
</Placeholder>
) : (
<span className="text-truncate" {...props} id="options" style={{ maxWidth: valueContainerWidth }}>
{isAllOptionsSelected ? 'All items are selected.' : values.join(', ')}
{selectProps?.showAllSelectedLabel && isAllOptionsSelected ? 'All items are selected.' : values.join(', ')}
</span>
)}
{/* Rendering children except Placeholder component to preserve the default behavior of react-select like focus

View file

@ -37,6 +37,7 @@ export const MultiselectV2 = ({
loadingState: multiSelectLoadingState,
optionsLoadingState,
sort,
showAllSelectedLabel,
showClearBtn,
showSearchInput,
} = properties;
@ -544,6 +545,7 @@ export const MultiselectV2 = ({
icon={icon}
doShowIcon={iconVisibility}
containerRef={valueContainerRef}
showAllSelectedLabel={showAllSelectedLabel}
iconColor={iconColor}
optionsLoadingState={optionsLoadingState && advanced}
darkMode={darkMode}

View file

@ -3,7 +3,7 @@ export const containerConfig = {
displayName: 'Container',
description: 'Group components',
defaultSize: {
width: 5,
width: 10,
height: 200,
},
component: 'Container',
@ -44,13 +44,19 @@ export const containerConfig = {
displayName: 'Show header',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
defaultValue: true,
},
},
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 20,
left: 1,
@ -98,15 +104,6 @@ export const containerConfig = {
},
accordian: 'container',
},
headerHeight: {
type: 'numberInput',
displayName: 'Height',
validation: {
schema: { type: 'number' },
defaultValue: 80,
},
accordian: 'header',
},
borderRadius: {
type: 'numberInput',
displayName: 'Border',
@ -154,10 +151,11 @@ export const containerConfig = {
showOnMobile: { value: '{{false}}' },
},
properties: {
showHeader: { value: `{{false}}` },
showHeader: { value: `{{true}}` },
loadingState: { value: `{{false}}` },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: `{{80}}` },
},
events: [],
styles: {

View file

@ -4,7 +4,7 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 480,
height: 450,
},
defaultChildren: [
{
@ -19,8 +19,8 @@ export const formConfig = {
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
text: 'Form',
textSize: 16,
textColor: '#000',
},
},
@ -34,203 +34,68 @@ export const formConfig = {
},
properties: ['text'],
defaultValue: {
text: 'Button2',
text: 'Submit',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
top: 40,
left: 10,
height: 30,
width: 17,
},
properties: ['text'],
styles: [
'textSize',
'fontWeight',
'fontStyle',
'textColor',
'isScrollRequired',
'lineHeight',
'textIndent',
'textAlign',
'verticalAlignment',
'decoration',
'transformation',
'letterSpacing',
'wordSpacing',
'fontVariant',
'backgroundColor',
'borderColor',
'borderRadius',
'boxShadow',
'padding',
],
defaultValue: {
text: 'User Details',
fontWeight: 'bold',
textSize: 18,
textColor: '#000',
backgroundColor: '#fff00000',
textAlign: 'left',
decoration: 'none',
transformation: 'none',
fontStyle: 'normal',
lineHeight: 1.5,
textIndent: '0',
letterSpacing: '0',
wordSpacing: '0',
fontVariant: 'normal',
verticalAlignment: 'top',
padding: 'default',
boxShadow: '0px 0px 0px 0px #00000090',
borderRadius: '0',
isScrollRequired: 'enabled',
},
},
{
componentName: 'Text',
layout: {
top: 90,
left: 10,
height: 30,
},
properties: ['text'],
styles: [
'textSize',
'fontWeight',
'fontStyle',
'textColor',
'isScrollRequired',
'lineHeight',
'textIndent',
'textAlign',
'verticalAlignment',
'decoration',
'transformation',
'letterSpacing',
'wordSpacing',
'fontVariant',
'backgroundColor',
'borderColor',
'borderRadius',
'boxShadow',
'padding',
],
defaultValue: {
text: 'Name',
fontWeight: 'normal',
textSize: 14,
textColor: '#000',
backgroundColor: '#fff00000',
textAlign: 'left',
decoration: 'none',
transformation: 'none',
fontStyle: 'normal',
lineHeight: 1.5,
textIndent: '0',
letterSpacing: '0',
wordSpacing: '0',
fontVariant: 'normal',
verticalAlignment: 'top',
padding: 'default',
boxShadow: '0px 0px 0px 0px #00000090',
borderRadius: '0',
isScrollRequired: 'enabled',
},
},
{
componentName: 'Text',
layout: {
top: 160,
left: 10,
height: 30,
},
properties: ['text'],
styles: [
'textSize',
'fontWeight',
'fontStyle',
'textColor',
'isScrollRequired',
'lineHeight',
'textIndent',
'textAlign',
'verticalAlignment',
'decoration',
'transformation',
'letterSpacing',
'wordSpacing',
'fontVariant',
'backgroundColor',
'borderColor',
'borderRadius',
'boxShadow',
'padding',
],
defaultValue: {
text: 'Age',
fontWeight: 'normal',
textSize: 14,
textColor: '#000',
backgroundColor: '#fff00000',
textAlign: 'left',
decoration: 'none',
transformation: 'none',
fontStyle: 'normal',
lineHeight: 1.5,
textIndent: '0',
letterSpacing: '0',
wordSpacing: '0',
fontVariant: 'normal',
verticalAlignment: 'top',
padding: 'default',
boxShadow: '0px 0px 0px 0px #00000090',
borderRadius: '0',
isScrollRequired: 'enabled',
},
},
{
componentName: 'TextInput',
layout: {
top: 120,
left: 10,
height: 30,
width: 25,
top: 20,
left: 5,
height: 40,
width: 31,
},
properties: ['placeholder', 'label'],
styles: ['alignment', 'width', 'auto', 'padding', 'direction'],
defaultValue: {
placeholder: 'Enter your name',
label: '',
label: 'Name',
width: '{{60}}',
direction: 'left',
alignment: 'side',
auto: '{{false}}',
padding: 'default',
},
},
{
componentName: 'NumberInput',
layout: {
top: 190,
left: 10,
height: 30,
width: 25,
top: 80,
left: 5,
height: 40,
width: 31,
},
properties: ['value', 'label'],
properties: ['placeholder', 'label'],
styles: ['alignment', 'width', 'auto', 'padding', 'direction'],
defaultValue: {
value: 24,
label: '',
placeholder: 'Age',
label: 'Age',
width: '{{60}}',
direction: 'left',
alignment: 'side',
auto: '{{false}}',
padding: 'default',
},
},
{
componentName: 'Button',
componentName: 'TextInput',
layout: {
top: 240,
left: 10,
height: 30,
width: 10,
top: 140,
left: 5,
height: 40,
width: 31,
},
properties: ['text'],
properties: ['placeholder', 'label'],
styles: ['alignment', 'width', 'auto', 'padding', 'direction'],
defaultValue: {
text: 'Submit',
placeholder: 'Tomy',
label: 'Pet name',
width: '{{60}}',
alignment: 'side',
direction: 'left',
auto: '{{false}}',
padding: 'default',
},
},
],
@ -276,6 +141,24 @@ export const formConfig = {
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
isHidden: true,
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
canvasHeight: {
type: 'numberInput',
displayName: 'Canvas height',
isHidden: true,
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
footerHeight: {
type: 'numberInput',
displayName: 'Footer height',
isHidden: true,
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
@ -294,6 +177,13 @@ export const formConfig = {
defaultValue: false,
},
},
tooltip: {
type: 'code',
displayName: 'Tooltip',
validation: { schema: { type: 'string' } },
section: 'additionalActions',
placeholder: 'Enter tooltip text',
},
},
events: {
onSubmit: { displayName: 'On submit' },
@ -316,24 +206,8 @@ export const formConfig = {
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',
type: 'colorSwatches',
displayName: 'Background color',
validation: {
schema: { type: 'string' },
@ -351,7 +225,7 @@ export const formConfig = {
},
},
borderColor: {
type: 'color',
type: 'colorSwatches',
displayName: 'Border color',
validation: {
schema: { type: 'string' },
@ -403,18 +277,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}}' },
showHeader: { value: '{{true}}' },
showFooter: { value: '{{true}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: 60 },
footerHeight: { value: 60 },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -121,6 +121,12 @@ export const multiselectV2Config = {
},
accordian: 'Options',
},
showAllSelectedLabel: {
type: 'toggle',
displayName: 'Show "All items are selected"',
validation: { schema: { type: 'boolean' }, defaultValue: true },
accordian: 'Options',
},
optionsLoadingState: {
type: 'toggle',
displayName: 'Options loading state',
@ -339,6 +345,7 @@ export const multiselectV2Config = {
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select the options' },
showAllSelectedLabel: { value: '{{true}}' },
showClearBtn: { value: '{{true}}' },
showSearchInput: { value: '{{true}}' },
visibility: { value: '{{true}}' },

View file

@ -31,6 +31,7 @@ export const ON_BOARDING_ROLES = [
export const ERROR_TYPES = {
URL_UNAVAILABLE: 'url-unavailable',
RESTRICTED: 'restricted',
NO_ACCESSIBLE_PAGES: 'no-accessible-pages',
INVALID: 'invalid-link',
UNKNOWN: 'unknown',
WORKSPACE_ARCHIVED: 'Organization is Archived',
@ -53,6 +54,12 @@ export const ERROR_MESSAGES = {
retry: false,
queryParams: [],
},
'no-accessible-pages': {
title: 'Restricted access',
message: 'You dont have access to any page in this app. Kindly contact admin to know more.',
retry: false,
queryParams: [],
},
'ws-login-restricted': {
title: 'Restricted access',
message:

View file

@ -47,7 +47,7 @@ const switchOrganization = (componentType, orgId, redirectPath) => {
);
};
const handleError = (componentType, error, redirectPath, editPermission, appSlug = null) => {
export const handleError = (componentType, error, redirectPath, editPermission, appSlug = null) => {
try {
if (error?.data) {
const statusCode = error.data?.statusCode;
@ -63,6 +63,10 @@ const handleError = (componentType, error, redirectPath, editPermission, appSlug
switchOrganization(componentType, errorObj?.organizationId, redirectPath);
return;
}
if (errorObj?.type === ERROR_TYPES.NO_ACCESSIBLE_PAGES) {
redirectToErrorPage(ERROR_TYPES.NO_ACCESSIBLE_PAGES);
return;
}
redirectToErrorPage(ERROR_TYPES.RESTRICTED);
return;
}

View file

@ -0,0 +1,49 @@
import config from 'config';
import { authHeader, handleResponse } from '@/_helpers';
export const appPermissionService = {
getPagePermission,
getUsers,
createPagePermission,
updatePagePermission,
deletePagePermission,
};
function getPagePermission(appId, pageId) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
}
function getUsers(appId, type) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${type}`, requestOptions).then(handleResponse);
}
function createPagePermission(appId, pageId, body) {
const requestOptions = {
method: 'POST',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
}
function updatePagePermission(appId, pageId, body) {
const requestOptions = {
method: 'PUT',
headers: authHeader(),
credentials: 'include',
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
}
function deletePagePermission(appId, pageId) {
const requestOptions = {
method: 'DELETE',
headers: authHeader(),
credentials: 'include',
};
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
}

View file

@ -33,3 +33,4 @@ export * from './workflow_schedules.service';
export * from './session.service';
export * from './login_configs.service';
export * from './ai.service';
export * from './appPermission.service';

View file

@ -237,6 +237,16 @@ $btn-dark-color: #FFFFFF;
}
}
}
.page-permission-btn {
display: flex;
align-items: baseline;
gap: 5px;
&.disabled {
opacity: 1 !important;
}
}
}
.notification-dot {

View file

@ -17,6 +17,7 @@ export default function ModalBase({
cancelDisabled,
className = '',
size = 'sm',
headerAction,
}) {
return (
<Modal
@ -30,7 +31,8 @@ export default function ModalBase({
<Modal.Title className="font-weight-500" data-cy="modal-title">
{title}
</Modal.Title>
<div onClick={handleClose} className="cursor-pointer" data-cy="modal-close-button">
<div onClick={handleClose} id="header-actions" className="cursor-pointer" data-cy="modal-close-button">
{headerAction && headerAction()}
<SolidIcon name="remove" width="20" />
</div>
</Modal.Header>

@ -1 +1 @@
Subproject commit 8a6b2e586cb92ecc1cd14859f665206324152586
Subproject commit 12599a28b17d84e30b0ea4897a239ed89c011425

View file

@ -0,0 +1,50 @@
import { Component } from 'src/entities/component.entity';
import { processDataInBatches } from '@helpers/migration.helper';
import { EntityManager, MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateContainerHeaderProperty1744097765065 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const componentTypes = ['Container'];
const batchSize = 100;
const entityManager = queryRunner.manager;
for (const componentType of componentTypes) {
await processDataInBatches(
entityManager,
async (entityManager: EntityManager) => {
return await entityManager.find(Component, {
where: { type: componentType },
order: { createdAt: 'ASC' },
});
},
async (entityManager: EntityManager, components: Component[]) => {
await this.processUpdates(entityManager, components);
},
batchSize
);
}
}
private async processUpdates(entityManager: EntityManager, components: Component[]) {
for (const component of components) {
const properties = component.properties;
const styles = component.styles;
const general = component.general;
// Update showHeader property to false for old instances
if (!properties.showHeader) {
properties.showHeader = { value: '{{false}}' };
}
// Update the modal component with the modified properties
await entityManager.update(Component, component.id, {
properties,
styles,
general,
});
}
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -0,0 +1,57 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
import { TOOLJET_EDITIONS } from '@modules/app/constants';
import { getTooljetEdition } from '@helpers/utils.helper';
export class CreatePagePermissions1744610362161 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
return;
}
await queryRunner.createTable(
new Table({
name: 'page_permissions',
columns: [
{
name: 'id',
type: 'uuid',
isGenerated: true,
default: 'gen_random_uuid()',
isPrimary: true,
},
{
name: 'page_id',
type: 'uuid',
isNullable: false,
},
{
name: 'type',
type: 'enum',
enum: ['SINGLE', 'GROUP'],
},
{
name: 'created_at',
type: 'timestamp',
isNullable: false,
default: 'now()',
},
],
}),
true
);
await queryRunner.createForeignKey(
'page_permissions',
new TableForeignKey({
columnNames: ['page_id'],
referencedColumnNames: ['id'],
referencedTableName: 'pages',
onDelete: 'CASCADE',
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('page_permissions');
}
}

View file

@ -0,0 +1,82 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
import { TOOLJET_EDITIONS } from '@modules/app/constants';
import { getTooljetEdition } from '@helpers/utils.helper';
export class CreatePageUsers1744611380594 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
return;
}
await queryRunner.createTable(
new Table({
name: 'page_users',
columns: [
{
name: 'id',
type: 'uuid',
isGenerated: true,
default: 'gen_random_uuid()',
isPrimary: true,
},
{
name: 'page_permissions_id',
type: 'uuid',
isNullable: false,
},
{
name: 'user_id',
type: 'uuid',
isNullable: true,
},
{
name: 'permission_groups_id',
type: 'uuid',
isNullable: true,
},
{
name: 'created_at',
type: 'timestamp',
isNullable: false,
default: 'now()',
},
],
}),
true
);
await queryRunner.createForeignKey(
'page_users',
new TableForeignKey({
columnNames: ['page_permissions_id'],
referencedColumnNames: ['id'],
referencedTableName: 'page_permissions',
onDelete: 'CASCADE',
})
);
await queryRunner.createForeignKey(
'page_users',
new TableForeignKey({
columnNames: ['user_id'],
referencedColumnNames: ['id'],
referencedTableName: 'users',
onDelete: 'CASCADE',
})
);
await queryRunner.createForeignKey(
'page_users',
new TableForeignKey({
columnNames: ['permission_groups_id'],
referencedColumnNames: ['id'],
referencedTableName: 'permission_groups',
onDelete: 'CASCADE',
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('page_users');
}
}

View file

@ -3,6 +3,7 @@ import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
@ -13,12 +14,14 @@ import { Organization } from './organization.entity';
import { GroupUsers } from './group_users.entity';
import { GranularPermissions } from './granular_permissions.entity';
import { GROUP_PERMISSIONS_TYPE } from '@modules/group-permissions/constants';
import { PageUser } from './page_users.entity';
@Entity({ name: 'permission_groups' })
export class GroupPermissions extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'organization_id', nullable: false })
organizationId: string;
@ -62,5 +65,8 @@ export class GroupPermissions extends BaseEntity {
@OneToMany(() => GranularPermissions, (granularPermissions) => granularPermissions.group, { onDelete: 'CASCADE' })
groupGranularPermissions: GranularPermissions[];
@OneToMany(() => PageUser, (pageUser) => pageUser.permissionGroup)
pageUsers: PageUser[];
disabled?: boolean;
}

View file

@ -3,6 +3,7 @@ import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
@ -16,9 +17,11 @@ export class GroupUsers extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'user_id', nullable: false })
userId: string;
@Index()
@Column({ name: 'group_id', nullable: false })
groupId: string;

View file

@ -10,6 +10,7 @@ import {
} from 'typeorm';
import { AppVersion } from './app_version.entity';
import { Component } from './component.entity';
import { PagePermission } from './page_permissions.entity';
@Entity({ name: 'pages' })
export class Page {
@ -61,4 +62,7 @@ export class Page {
@OneToMany(() => Component, (component) => component.page)
components: Component[];
@OneToMany(() => PagePermission, (permission) => permission.page)
permissions: PagePermission[];
}

View file

@ -0,0 +1,29 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, OneToMany } from 'typeorm';
import { Page } from './page.entity';
import { PageUser } from './page_users.entity';
import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants';
@Entity('page_permissions')
export class PagePermission {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'page_id', type: 'uuid', nullable: false })
pageId: string;
@Column({
type: 'enum',
enum: PAGE_PERMISSION_TYPE,
})
type: PAGE_PERMISSION_TYPE;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@ManyToOne(() => Page, (page) => page.permissions, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'page_id' })
page: Page;
@OneToMany(() => PageUser, (pageUser) => pageUser.pagePermission)
users: PageUser[];
}

View file

@ -0,0 +1,37 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, Index } from 'typeorm';
import { User } from './user.entity';
import { PagePermission } from './page_permissions.entity';
import { GroupPermissions } from './group_permissions.entity';
@Entity('page_users')
export class PageUser {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'page_permissions_id', type: 'uuid' })
pagePermissionsId: string;
@Index()
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string | null;
@Index()
@Column({ name: 'permission_groups_id', type: 'uuid', nullable: true })
permissionGroupsId: string | null;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@ManyToOne(() => PagePermission, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'page_permissions_id' })
pagePermission: PagePermission;
@ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'user_id' })
user: User;
@ManyToOne(() => GroupPermissions, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'permission_groups_id' })
permissionGroup: GroupPermissions;
}

View file

@ -29,6 +29,7 @@ import { OnboardingStatus } from '@modules/onboarding/constants';
import { AiConversation } from './ai_conversation.entity';
import { AiResponseVote } from './ai_response_vote.entity';
import { USER_ROLE } from '@modules/group-permissions/constants';
import { PageUser } from './page_users.entity';
@Entity({ name: 'users' })
export class User extends BaseEntity {
@ -184,6 +185,9 @@ export class User extends BaseEntity {
@OneToMany(() => AiResponseVote, (aiResponseVote) => aiResponseVote.user, { onDelete: 'CASCADE' })
aiResponseVotes: AiResponseVote[];
@OneToMany(() => PageUser, (pageUser) => pageUser.user)
pageUsers: PageUser[];
organizationId: string;
invitedOrganizationId: string;
organizationIds?: Array<string>;

View file

@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { FeatureAbilityFactory } from '.';
import { AbilityGuard } from '@modules/app/guards/ability.guard';
import { App } from '@entities/app.entity';
import { ResourceDetails } from '@modules/app/types';
import { MODULES } from '@modules/app/constants/modules';
@Injectable()
export class FeatureAbilityGuard extends AbilityGuard {
protected getResource(): ResourceDetails {
return {
resourceType: MODULES.APP_PERMISSIONS,
};
}
protected getAbilityFactory() {
return FeatureAbilityFactory;
}
protected getSubjectType() {
return App;
}
protected forwardAbility(): boolean {
return true;
}
}

View file

@ -0,0 +1,72 @@
import { Injectable } from '@nestjs/common';
import { Ability, AbilityBuilder, InferSubjects } from '@casl/ability';
import { AbilityFactory } from '@modules/app/ability-factory';
import { UserAllPermissions } from '@modules/app/types';
import { FEATURE_KEY } from '../constants';
import { App } from '@entities/app.entity';
import { MODULES } from '@modules/app/constants/modules';
type Subjects = InferSubjects<typeof App> | 'all';
export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>;
@Injectable()
export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects> {
protected getSubjectType() {
return App;
}
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];
const isAllAppsEditable = !!userAppPermissions?.isAllEditable;
const isAllAppsViewable = !!userAppPermissions?.isAllViewable;
if (isAdmin || superAdmin) {
// Admin or super admin and do all operations
can(
[
FEATURE_KEY.FETCH_USERS,
FEATURE_KEY.FETCH_USER_GROUPS,
FEATURE_KEY.FETCH_PAGE_PERMISSIONS,
FEATURE_KEY.CREATE_PAGE_PERMISSIONS,
FEATURE_KEY.UPDATE_PAGE_PERMISSIONS,
FEATURE_KEY.DELETE_PAGE_PERMISSIONS,
],
App
);
return;
}
if (
isAllAppsEditable ||
(userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId))
) {
can(
[
FEATURE_KEY.FETCH_USERS,
FEATURE_KEY.FETCH_USER_GROUPS,
FEATURE_KEY.FETCH_PAGE_PERMISSIONS,
FEATURE_KEY.CREATE_PAGE_PERMISSIONS,
FEATURE_KEY.UPDATE_PAGE_PERMISSIONS,
FEATURE_KEY.DELETE_PAGE_PERMISSIONS,
],
App
);
return;
}
if (
isAllAppsViewable ||
(userAppPermissions?.viewableAppsId?.length && appId && userAppPermissions.viewableAppsId.includes(appId))
) {
can([FEATURE_KEY.FETCH_USERS, FEATURE_KEY.FETCH_USER_GROUPS, FEATURE_KEY.FETCH_PAGE_PERMISSIONS], App);
}
}
}

View file

@ -0,0 +1,14 @@
import { FEATURE_KEY } from './index';
import { MODULES } from '@modules/app/constants/modules';
import { FeaturesConfig } from '../types';
export const FEATURES: FeaturesConfig = {
[MODULES.APP_PERMISSIONS]: {
[FEATURE_KEY.FETCH_USERS]: {},
[FEATURE_KEY.FETCH_USER_GROUPS]: {},
[FEATURE_KEY.FETCH_PAGE_PERMISSIONS]: {},
[FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: {},
[FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: {},
[FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: {},
},
};

View file

@ -0,0 +1,14 @@
export enum PAGE_PERMISSION_TYPE {
SINGLE = 'SINGLE',
GROUP = 'GROUP',
ALL = 'ALL',
}
export enum FEATURE_KEY {
FETCH_USERS = 'fetch_users',
FETCH_USER_GROUPS = 'fetch_user_groups',
FETCH_PAGE_PERMISSIONS = 'fetch_page_permissions',
CREATE_PAGE_PERMISSIONS = 'create_page_permissions',
UPDATE_PAGE_PERMISSIONS = 'update_page_permissions',
DELETE_PAGE_PERMISSIONS = 'delete_page_permissions',
}

View file

@ -0,0 +1,84 @@
import { Body, Controller, Delete, Get, NotFoundException, Param, Post, Put, Res, UseGuards } from '@nestjs/common';
import { Response } from 'express';
import { User } from '@modules/app/decorators/user.decorator';
import { IAppPermissionsController } from './interfaces/IController';
import { FeatureAbilityGuard } from './ability/guard';
import { InitModule } from '@modules/app/decorators/init-module';
import { MODULES } from '@modules/app/constants/modules';
import { InitFeature } from '@modules/app/decorators/init-feature.decorator';
import { FEATURE_KEY } from './constants';
import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard';
import { CreatePagePermissionDto } from './dto';
@InitModule(MODULES.APP_PERMISSIONS)
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
@Controller('app-permissions')
export class AppPermissionsController implements IAppPermissionsController {
constructor() {}
@InitFeature(FEATURE_KEY.FETCH_USERS)
@Get(':appId/pages/users')
async fetchUsers(
@User() user,
@Param('appId') appId: string,
@Res({ passthrough: true }) response: Response
): Promise<any> {
throw new NotFoundException();
}
@InitFeature(FEATURE_KEY.FETCH_USER_GROUPS)
@Get(':appId/pages/user-groups')
async fetchUserGroups(
@User() user,
@Param('appId') appId: string,
@Res({ passthrough: true }) response: Response
): Promise<any> {
throw new NotFoundException();
}
@InitFeature(FEATURE_KEY.FETCH_PAGE_PERMISSIONS)
@Get(':appId/pages/:pageId')
async fetchPagePermissions(
@User() user,
@Param('appId') appId: string,
@Param('pageId') pageId: string,
@Res({ passthrough: true }) response: Response
): Promise<any> {
throw new NotFoundException();
}
@InitFeature(FEATURE_KEY.CREATE_PAGE_PERMISSIONS)
@Post(':appId/pages/:pageId')
async createPagePermissions(
@User() user,
@Param('appId') appId: string,
@Param('pageId') pageId: string,
@Body() body: CreatePagePermissionDto,
@Res({ passthrough: true }) response: Response
): Promise<any> {
throw new NotFoundException();
}
@InitFeature(FEATURE_KEY.UPDATE_PAGE_PERMISSIONS)
@Put(':appId/pages/:pageId')
async updatePagePermissions(
@User() user,
@Param('appId') appId: string,
@Param('pageId') pageId: string,
@Body() body: CreatePagePermissionDto,
@Res({ passthrough: true }) response: Response
): Promise<any> {
throw new NotFoundException();
}
@InitFeature(FEATURE_KEY.DELETE_PAGE_PERMISSIONS)
@Delete(':appId/pages/:pageId')
async deletePagePermissions(
@User() user,
@Param('appId') appId: string,
@Param('pageId') pageId: string,
@Res({ passthrough: true }) response: Response
): Promise<any> {
throw new NotFoundException();
}
}

View file

@ -0,0 +1,26 @@
import { IsUUID, IsEnum, IsArray, IsString, IsOptional, ValidateIf } from 'class-validator';
import { Type } from 'class-transformer';
import { PAGE_PERMISSION_TYPE } from '../constants';
export class CreatePagePermissionDto {
@IsUUID(4)
@IsOptional()
pageId: string;
@IsEnum(PAGE_PERMISSION_TYPE)
type: PAGE_PERMISSION_TYPE;
@ValidateIf((o) => o.type === PAGE_PERMISSION_TYPE.SINGLE)
@IsArray()
@IsString({ each: true })
@IsOptional()
@Type(() => String)
users?: string[];
@ValidateIf((o) => o.type === PAGE_PERMISSION_TYPE.GROUP)
@IsArray()
@IsString({ each: true })
@IsOptional()
@Type(() => String)
groups?: string[];
}

View file

@ -0,0 +1,29 @@
import { User } from '@entities/user.entity';
import { Response } from 'express';
import { CreatePagePermissionDto } from '../dto';
export interface IAppPermissionsController {
fetchUsers(user: User, appId: string, response: Response): Promise<any>;
fetchUserGroups(user: User, appId: string, response: Response): Promise<any>;
fetchPagePermissions(user: User, appId: string, pageId: string, response: Response): Promise<any>;
createPagePermissions(
user: User,
appId: string,
pageId: string,
body: CreatePagePermissionDto,
response: Response
): Promise<any>;
updatePagePermissions(
user: User,
appId: string,
pageId: string,
body: CreatePagePermissionDto,
response: Response
): Promise<any>;
deletePagePermissions(user: User, appId: string, pageId: string, response: Response): Promise<any>;
}

View file

@ -0,0 +1,16 @@
import { User } from '@entities/user.entity';
import { CreatePagePermissionDto } from '../dto';
export interface IAppPermissionsService {
fetchUsers(appId: string, user: User): Promise<any>;
fetchUserGroups(appId: string, user: User): Promise<any>;
fetchPagePermissions(pageId: string): Promise<any>;
createPagePermissions(pageId: string, body: CreatePagePermissionDto): Promise<any>;
updatePagePermissions(appId: string, pageId: string, body: CreatePagePermissionDto, user: User): Promise<any>;
deletePagePermissions(pageId: string): Promise<any>;
}

View file

@ -0,0 +1,13 @@
import { User } from '@entities/user.entity';
import { GroupPermissions } from '@entities/group_permissions.entity';
import { CreatePagePermissionDto } from '../dto';
export interface IUtilService {
getUsersWithViewAccess(appId: string, organizationId: string): Promise<User[]>;
getUserGroupsWithViewAccess(appId: string, organizationId: string): Promise<GroupPermissions[]>;
createPagePermission(pageId: string, body: CreatePagePermissionDto): Promise<any>;
updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise<any>;
}

View file

@ -0,0 +1,35 @@
import { getImportPath } from '@modules/app/constants';
import { DynamicModule } from '@nestjs/common';
import { FeatureAbilityFactory } from './ability';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GroupPermissions } from '@entities/group_permissions.entity';
import { User } from '@entities/user.entity';
import { RolesRepository } from '@modules/roles/repository';
import { PageUsersRepository } from './repositories/page-users.repository';
import { PagePermissionsRepository } from './repositories/page-permissions.repository';
import { PageUser } from '@entities/page_users.entity';
import { PagePermission } from '@entities/page_permissions.entity';
export class AppPermissionsModule {
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
const importPath = await getImportPath(configs.IS_GET_CONTEXT);
const { AppPermissionsController } = await import(`${importPath}/app-permissions/controller`);
const { AppPermissionsService } = await import(`${importPath}/app-permissions/service`);
const { AppPermissionsUtilService } = await import(`${importPath}/app-permissions/util.service`);
return {
module: AppPermissionsModule,
imports: [TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission])],
controllers: [AppPermissionsController],
providers: [
AppPermissionsService,
AppPermissionsUtilService,
RolesRepository,
PageUsersRepository,
PagePermissionsRepository,
FeatureAbilityFactory,
],
exports: [AppPermissionsUtilService, AppPermissionsService],
};
}
}

View file

@ -0,0 +1,58 @@
import { PagePermission } from '@entities/page_permissions.entity';
import { Injectable } from '@nestjs/common';
import { DataSource, EntityManager, Repository } from 'typeorm';
import { PageUsersRepository } from './page-users.repository';
import { dbTransactionWrap } from '@helpers/database.helper';
import { PAGE_PERMISSION_TYPE } from '../constants';
@Injectable()
export class PagePermissionsRepository extends Repository<PagePermission> {
constructor(private dataSource: DataSource, private readonly pageUsersRepository: PageUsersRepository) {
super(PagePermission, dataSource.createEntityManager());
}
async getPagePermissions(pageId: string, manager?: EntityManager): Promise<PagePermission[]> {
return dbTransactionWrap(async (manager: EntityManager) => {
const pagePermissions = await manager.find(PagePermission, {
where: { pageId },
relations: ['users', 'users.user', 'users.permissionGroup'],
});
return pagePermissions.map((permission) => {
if (permission.type === PAGE_PERMISSION_TYPE.GROUP) {
return {
...permission,
groups: permission.users,
users: undefined,
};
}
return permission;
});
}, manager || this.manager);
}
async createPagePermissions(
pageId: string,
type: PAGE_PERMISSION_TYPE,
manager?: EntityManager
): Promise<PagePermission> {
return dbTransactionWrap(async (manager: EntityManager) => {
const existingPermission = await manager.findOne(PagePermission, { where: { pageId } });
if (existingPermission) {
throw new Error(`Page permission already exists for Page id: ${pageId}`);
}
const pagePermission = manager.create(PagePermission, {
pageId,
type,
});
return manager.save(pagePermission);
}, manager || this.manager);
}
async deletePagePermissions(pageId: string, manager?: EntityManager): Promise<void> {
return dbTransactionWrap(async (manager: EntityManager) => {
await manager.delete(PagePermission, { pageId });
}, manager || this.manager);
}
}

View file

@ -0,0 +1,91 @@
import { PageUser } from '@entities/page_users.entity';
import { Injectable } from '@nestjs/common';
import { DataSource, EntityManager, Repository } from 'typeorm';
import { dbTransactionWrap } from '@helpers/database.helper';
import { PagePermission } from '@entities/page_permissions.entity';
@Injectable()
export class PageUsersRepository extends Repository<PageUser> {
constructor(private dataSource: DataSource) {
super(PageUser, dataSource.createEntityManager());
}
async createPageUsersWithSingle(
pagePermissionsId: string,
users: string[],
manager?: EntityManager
): Promise<PageUser[]> {
return dbTransactionWrap(async (manager: EntityManager) => {
const pageUsers = users.map((userId) => {
return manager.create(PageUser, {
pagePermissionsId,
userId,
permissionGroupsId: null,
});
});
return manager.save(pageUsers);
}, manager || this.manager);
}
async createPageUsersWithGroup(
pagePermissionsId: string,
groups: string[],
manager?: EntityManager
): Promise<PageUser[]> {
return dbTransactionWrap(async (manager: EntityManager) => {
const pageUsers = groups.map((permissionGroupsId) => {
return manager.create(PageUser, {
pagePermissionsId,
permissionGroupsId,
userId: null,
});
});
return manager.save(pageUsers);
}, manager || this.manager);
}
async checkIfUserExistsInPermissionGroup(
pagePermission: PagePermission,
userId: string,
manager?: EntityManager
): Promise<PageUser> {
return dbTransactionWrap(async (manager: EntityManager) => {
const result = await manager
.createQueryBuilder(PageUser, 'page_users')
.innerJoin('page_users.permissionGroup', 'group')
.innerJoin('group.groupUsers', 'groupUser')
.where('page_users.pagePermission = :permissionId', {
permissionId: pagePermission.id,
})
.andWhere('groupUser.userId = :userId', { userId })
.getOne();
if (!result) {
return false;
}
return pagePermission;
}, manager || this.manager);
}
async checkIfUserExistsInSingleConfig(
pagePermission: PagePermission,
userId: string,
manager?: EntityManager
): Promise<PageUser> {
return dbTransactionWrap(async (manager: EntityManager) => {
const pageUser = await manager.findOne(PageUser, {
where: {
pagePermission: { id: pagePermission.id },
userId,
},
});
if (!pageUser) {
return false;
}
return pagePermission;
}, manager || this.manager);
}
}

View file

@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { IAppPermissionsService } from './interfaces/IService';
@Injectable()
export class AppPermissionsService implements IAppPermissionsService {
constructor() {}
async fetchUsers(appId, user) {
throw new Error('Method not implemented.');
}
async fetchUserGroups(appId, user) {
throw new Error('Method not implemented.');
}
async fetchPagePermissions(pageId) {
throw new Error('Method not implemented.');
}
async createPagePermissions(pageId, body) {
throw new Error('Method not implemented.');
}
async updatePagePermissions(appId, pageId, body, user) {
throw new Error('Method not implemented.');
}
async deletePagePermissions(pageId) {
throw new Error('Method not implemented.');
}
}

View file

@ -0,0 +1,16 @@
import { FEATURE_KEY } from '../constants';
import { FeatureConfig } from '@modules/app/types';
import { MODULES } from '@modules/app/constants/modules';
interface Features {
[FEATURE_KEY.FETCH_USERS]: FeatureConfig;
[FEATURE_KEY.FETCH_USER_GROUPS]: FeatureConfig;
[FEATURE_KEY.FETCH_PAGE_PERMISSIONS]: FeatureConfig;
[FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: FeatureConfig;
[FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: FeatureConfig;
[FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: FeatureConfig;
}
export interface FeaturesConfig {
[MODULES.APP_PERMISSIONS]: Features;
}

View file

@ -0,0 +1,26 @@
import { User } from '@entities/user.entity';
import { IUtilService } from './interfaces/IUtilService';
import { Injectable } from '@nestjs/common';
import { GroupPermissions } from '@entities/group_permissions.entity';
import { CreatePagePermissionDto } from './dto';
@Injectable()
export class AppPermissionsUtilService implements IUtilService {
constructor() {}
async getUserGroupsWithViewAccess(appId: string, organizationId: string): Promise<GroupPermissions[]> {
throw new Error('Method not implemented.');
}
async getUsersWithViewAccess(appId: string, organizationId: string): Promise<User[]> {
throw new Error('Method not implemented.');
}
async createPagePermission(pageId: string, body: CreatePagePermissionDto): Promise<any> {
throw new Error('Method not implemented.');
}
async updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise<any> {
throw new Error('Method not implemented.');
}
}

View file

@ -34,6 +34,7 @@ import { FEATURES as AI_FEATURES } from '@modules/ai/constants/feature';
import { getTooljetEdition } from '@helpers/utils.helper';
import { TOOLJET_EDITIONS } from '.';
import { FEATURES as WHITE_LABELLING_FEATURES } from '@modules/white-labelling/constant/feature';
import { FEATURES as APP_PERMISSIONS_FEATURES } from '@modules/app-permissions/constants/features';
const GROUP_PERMISSIONS_FEATURES =
getTooljetEdition() === TOOLJET_EDITIONS.EE ? GROUP_PERMISSIONS_FEATURES_EE : GROUP_PERMISSIONS_FEATURES_CE;
@ -73,4 +74,5 @@ export const MODULE_INFO: { [key: string]: any } = {
...ORGANIZATION_CONSTANT,
...AI_FEATURES,
...WHITE_LABELLING_FEATURES,
...APP_PERMISSIONS_FEATURES,
};

View file

@ -36,4 +36,5 @@ export enum MODULES {
IMPORT_EXPORT_RESOURCES = 'ImportExportResources',
TEMPLATES = 'Templates',
AI = 'ai',
APP_PERMISSIONS = 'AppPermissions',
}

View file

@ -41,6 +41,7 @@ import { TooljetDbModule } from '@modules/tooljet-db/module';
import { WorkflowsModule } from '@modules/workflows/module';
import { AiModule } from '@modules/ai/module';
import { CustomStylesModule } from '@modules/custom-styles/module';
import { AppPermissionsModule } from '@modules/app-permissions/module';
export class AppModule implements OnModuleInit {
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
@ -94,6 +95,7 @@ export class AppModule implements OnModuleInit {
await WorkflowsModule.register(configs),
await AiModule.register(configs),
await CustomStylesModule.register(configs),
await AppPermissionsModule.register(configs),
];
return {

View file

@ -19,6 +19,8 @@ import { FeatureAbilityFactory } from './ability';
import { DataSourcesModule } from '@modules/data-sources/module';
import { AppsSubscriber } from './subscribers/apps.subscriber';
import { AiModule } from '@modules/ai/module';
import { AppPermissionsModule } from '@modules/app-permissions/module';
import { RolesRepository } from '@modules/roles/repository';
@Module({})
export class AppsModule {
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
@ -36,7 +38,15 @@ export class AppsModule {
return {
module: AppsModule,
imports: [
TypeOrmModule.forFeature([App, Page, EventHandler, Organization, Component, VersionRepository]),
TypeOrmModule.forFeature([
App,
Page,
EventHandler,
Organization,
Component,
VersionRepository,
RolesRepository,
]),
await FolderAppsModule.register(configs),
await ThemesModule.register(configs),
await FoldersModule.register(configs),
@ -44,6 +54,7 @@ export class AppsModule {
await AppEnvironmentsModule.register(configs),
await DataSourcesModule.register(configs),
await AiModule.register(configs),
await AppPermissionsModule.register(configs),
],
controllers: [AppsController],
providers: [
@ -61,6 +72,7 @@ export class AppsModule {
AppsSubscriber,
DataSourcesRepository,
AppImportExportService,
RolesRepository,
],
exports: [AppsUtilService],
};

View file

@ -18,7 +18,7 @@ import {
VersionReleaseDto,
} from './dto';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { APP_TYPES, FEATURE_KEY } from './constants';
import { FEATURE_KEY } from './constants';
import { camelizeKeys, decamelizeKeys } from 'humps';
import { App } from '@entities/app.entity';
import { AppsUtilService } from './util.service';

View file

@ -3,7 +3,7 @@ export const containerConfig = {
displayName: 'Container',
description: 'Group components',
defaultSize: {
width: 5,
width: 10,
height: 200,
},
component: 'Container',
@ -44,13 +44,19 @@ export const containerConfig = {
displayName: 'Show header',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
defaultValue: true,
},
},
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 20,
left: 1,
@ -98,15 +104,6 @@ export const containerConfig = {
},
accordian: 'container',
},
headerHeight: {
type: 'numberInput',
displayName: 'Height',
validation: {
schema: { type: 'number' },
defaultValue: 80,
},
accordian: 'header',
},
borderRadius: {
type: 'numberInput',
displayName: 'Border',
@ -154,10 +151,11 @@ export const containerConfig = {
showOnMobile: { value: '{{false}}' },
},
properties: {
showHeader: { value: `{{false}}` },
showHeader: { value: `{{true}}` },
loadingState: { value: `{{false}}` },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: `{{80}}` },
},
events: [],
styles: {

View file

@ -4,7 +4,7 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 480,
height: 450,
},
defaultChildren: [
{
@ -19,8 +19,8 @@ export const formConfig = {
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
text: 'Form',
textSize: 16,
textColor: '#000',
},
},
@ -34,203 +34,68 @@ export const formConfig = {
},
properties: ['text'],
defaultValue: {
text: 'Button2',
text: 'Submit',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
top: 40,
left: 10,
height: 30,
width: 17,
},
properties: ['text'],
styles: [
'textSize',
'fontWeight',
'fontStyle',
'textColor',
'isScrollRequired',
'lineHeight',
'textIndent',
'textAlign',
'verticalAlignment',
'decoration',
'transformation',
'letterSpacing',
'wordSpacing',
'fontVariant',
'backgroundColor',
'borderColor',
'borderRadius',
'boxShadow',
'padding',
],
defaultValue: {
text: 'User Details',
fontWeight: 'bold',
textSize: 18,
textColor: '#000',
backgroundColor: '#fff00000',
textAlign: 'left',
decoration: 'none',
transformation: 'none',
fontStyle: 'normal',
lineHeight: 1.5,
textIndent: '0',
letterSpacing: '0',
wordSpacing: '0',
fontVariant: 'normal',
verticalAlignment: 'top',
padding: 'default',
boxShadow: '0px 0px 0px 0px #00000090',
borderRadius: '0',
isScrollRequired: 'enabled',
},
},
{
componentName: 'Text',
layout: {
top: 90,
left: 10,
height: 30,
},
properties: ['text'],
styles: [
'textSize',
'fontWeight',
'fontStyle',
'textColor',
'isScrollRequired',
'lineHeight',
'textIndent',
'textAlign',
'verticalAlignment',
'decoration',
'transformation',
'letterSpacing',
'wordSpacing',
'fontVariant',
'backgroundColor',
'borderColor',
'borderRadius',
'boxShadow',
'padding',
],
defaultValue: {
text: 'Name',
fontWeight: 'normal',
textSize: 14,
textColor: '#000',
backgroundColor: '#fff00000',
textAlign: 'left',
decoration: 'none',
transformation: 'none',
fontStyle: 'normal',
lineHeight: 1.5,
textIndent: '0',
letterSpacing: '0',
wordSpacing: '0',
fontVariant: 'normal',
verticalAlignment: 'top',
padding: 'default',
boxShadow: '0px 0px 0px 0px #00000090',
borderRadius: '0',
isScrollRequired: 'enabled',
},
},
{
componentName: 'Text',
layout: {
top: 160,
left: 10,
height: 30,
},
properties: ['text'],
styles: [
'textSize',
'fontWeight',
'fontStyle',
'textColor',
'isScrollRequired',
'lineHeight',
'textIndent',
'textAlign',
'verticalAlignment',
'decoration',
'transformation',
'letterSpacing',
'wordSpacing',
'fontVariant',
'backgroundColor',
'borderColor',
'borderRadius',
'boxShadow',
'padding',
],
defaultValue: {
text: 'Age',
fontWeight: 'normal',
textSize: 14,
textColor: '#000',
backgroundColor: '#fff00000',
textAlign: 'left',
decoration: 'none',
transformation: 'none',
fontStyle: 'normal',
lineHeight: 1.5,
textIndent: '0',
letterSpacing: '0',
wordSpacing: '0',
fontVariant: 'normal',
verticalAlignment: 'top',
padding: 'default',
boxShadow: '0px 0px 0px 0px #00000090',
borderRadius: '0',
isScrollRequired: 'enabled',
},
},
{
componentName: 'TextInput',
layout: {
top: 120,
left: 10,
height: 30,
width: 25,
top: 20,
left: 5,
height: 40,
width: 31,
},
properties: ['placeholder', 'label'],
styles: ['alignment', 'width', 'auto', 'padding', 'direction'],
defaultValue: {
placeholder: 'Enter your name',
label: '',
label: 'Name',
width: '{{60}}',
direction: 'left',
alignment: 'side',
auto: '{{false}}',
padding: 'default',
},
},
{
componentName: 'NumberInput',
layout: {
top: 190,
left: 10,
height: 30,
width: 25,
top: 80,
left: 5,
height: 40,
width: 31,
},
properties: ['value', 'label'],
properties: ['placeholder', 'label'],
styles: ['alignment', 'width', 'auto', 'padding', 'direction'],
defaultValue: {
value: 24,
label: '',
placeholder: 'Age',
label: 'Age',
width: '{{60}}',
direction: 'left',
alignment: 'side',
auto: '{{false}}',
padding: 'default',
},
},
{
componentName: 'Button',
componentName: 'TextInput',
layout: {
top: 240,
left: 10,
height: 30,
width: 10,
top: 140,
left: 5,
height: 40,
width: 31,
},
properties: ['text'],
properties: ['placeholder', 'label'],
styles: ['alignment', 'width', 'auto', 'padding', 'direction'],
defaultValue: {
text: 'Submit',
placeholder: 'Tomy',
label: 'Pet name',
width: '{{60}}',
alignment: 'side',
direction: 'left',
auto: '{{false}}',
padding: 'default',
},
},
],
@ -276,6 +141,24 @@ export const formConfig = {
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
headerHeight: {
type: 'numberInput',
displayName: 'Header height',
isHidden: true,
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
canvasHeight: {
type: 'numberInput',
displayName: 'Canvas height',
isHidden: true,
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
footerHeight: {
type: 'numberInput',
displayName: 'Footer height',
isHidden: true,
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
@ -294,6 +177,13 @@ export const formConfig = {
defaultValue: false,
},
},
tooltip: {
type: 'code',
displayName: 'Tooltip',
validation: { schema: { type: 'string' } },
section: 'additionalActions',
placeholder: 'Enter tooltip text',
},
},
events: {
onSubmit: { displayName: 'On submit' },
@ -316,22 +206,6 @@ export const formConfig = {
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'colorSwatches',
displayName: 'Background color',
@ -403,19 +277,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}}' },
showHeader: { value: '{{true}}' },
showFooter: { value: '{{true}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: 60 },
footerHeight: { value: 60 },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -142,4 +142,5 @@ export const iconConfig = {
padding: { value: 'default' },
boxShadow: { value: '0px 0px 0px 0px #00000040' },
},
};
}
};

View file

@ -121,6 +121,12 @@ export const multiselectV2Config = {
},
accordian: 'Options',
},
showAllSelectedLabel: {
type: 'toggle',
displayName: 'Show "All items are selected"',
validation: { schema: { type: 'boolean' }, defaultValue: true },
accordian: 'Options',
},
optionsLoadingState: {
type: 'toggle',
displayName: 'Options loading state',
@ -339,6 +345,7 @@ export const multiselectV2Config = {
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select the options' },
showAllSelectedLabel: { value: '{{true}}' },
showClearBtn: { value: '{{true}}' },
showSearchInput: { value: '{{true}}' },
visibility: { value: '{{true}}' },

View file

@ -6,7 +6,7 @@ import { DataSource } from '@entities/data_source.entity';
import { DataSourceOptions } from '@entities/data_source_options.entity';
import { EventHandler, Target } from '@entities/event_handler.entity';
import { dbTransactionWrap } from '@helpers/database.helper';
import { EntityManager } from 'typeorm';
import { EntityManager, In } from 'typeorm';
import { Credential } from 'src/entities/credential.entity';
import * as uuid from 'uuid';
import { Page } from '@entities/page.entity';
@ -22,6 +22,8 @@ import { DataSourcesRepository } from '@modules/data-sources/repository';
import { DataQueryRepository } from '@modules/data-queries/repository';
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
import { IVersionsCreateService } from '../interfaces/services/ICreateService';
import { PagePermission } from '@entities/page_permissions.entity';
import { PageUser } from '@entities/page_users.entity';
@Injectable()
export class VersionsCreateService implements IVersionsCreateService {
@ -401,6 +403,44 @@ export class VersionsCreateService implements IVersionsCreateService {
homePageId = savedPage.id;
}
const oldPermissions = await manager.find(PagePermission, {
where: { pageId: page.id },
});
const newPermissions = oldPermissions.map((permission) => {
return manager.create(PagePermission, {
...permission,
id: undefined,
pageId: oldPageToNewPageMapping[permission.pageId],
});
});
await manager.save(PagePermission, newPermissions);
const permissionIdMap = new Map<string, string>();
oldPermissions.forEach((oldPerm, index) => {
const newPerm = newPermissions[index];
permissionIdMap.set(oldPerm.id, newPerm.id);
});
const oldPermissionIds = oldPermissions.map((p) => p.id);
const oldPageUsers = await manager.find(PageUser, {
where: {
pagePermissionsId: In(oldPermissionIds),
},
});
const newPageUsers = oldPageUsers.map((pu) =>
manager.create(PageUser, {
...pu,
id: undefined,
pagePermissionsId: permissionIdMap.get(pu.pagePermissionsId),
})
);
await manager.save(PageUser, newPageUsers);
const pageEvents = allEvents.filter((event) => event.sourceId === page.id);
pageEvents.forEach(async (event, index) => {

View file

@ -29,6 +29,8 @@ 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';
import { AppPermissionsModule } from '@modules/app-permissions/module';
import { RolesRepository } from '@modules/roles/repository';
export class WorkflowsModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
@ -69,6 +71,7 @@ export class WorkflowsModule {
WorkflowExecutionNode,
WorkflowExecutionNode,
WorkflowExecutionEdge,
RolesRepository,
]),
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
@ -91,6 +94,7 @@ export class WorkflowsModule {
await FolderAppsModule.register(configs),
await ThemesModule.register(configs),
await AiModule.register(configs),
await AppPermissionsModule.register(configs),
],
providers: [
AppsAbilityFactory,
@ -113,6 +117,7 @@ export class WorkflowsModule {
WorkflowSchedulesService,
TemporalService,
FeatureAbilityFactory,
RolesRepository,
],
controllers: [
WorkflowsController,