mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 01:18:23 +00:00
Merge branch 'appbuilder/sprint-11' into feat/steps-v2-alignment-style-improvement
This commit is contained in:
commit
45a0882022
76 changed files with 2816 additions and 1030 deletions
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ export class DragContext {
|
|||
|
||||
const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot];
|
||||
return !restrictedWidgets.includes(dragged.widgetType);
|
||||
ß;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [
|
|||
'Divider',
|
||||
'VerticalDivider',
|
||||
'Link',
|
||||
'Form',
|
||||
];
|
||||
|
||||
const RenderWidget = ({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
516
frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx
Normal file
516
frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}}' },
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
84
frontend/src/AppBuilder/_hooks/useActiveSlot.js
Normal file
84
frontend/src/AppBuilder/_hooks/useActiveSlot.js
Normal 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;
|
||||
};
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
135
frontend/src/AppBuilder/_hooks/useMoveable.js
Normal file
135
frontend/src/AppBuilder/_hooks/useMoveable.js
Normal 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;
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}}' },
|
||||
|
|
|
|||
|
|
@ -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 don’t have access to any page in this app. Kindly contact admin to know more.',
|
||||
retry: false,
|
||||
queryParams: [],
|
||||
},
|
||||
'ws-login-restricted': {
|
||||
title: 'Restricted access',
|
||||
message:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
49
frontend/src/_services/appPermission.service.js
Normal file
49
frontend/src/_services/appPermission.service.js
Normal 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);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -237,6 +237,16 @@ $btn-dark-color: #FFFFFF;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-permission-btn {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-dot {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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> {}
|
||||
}
|
||||
57
server/migrations/1744610362161-CreatePagePermissions.ts
Normal file
57
server/migrations/1744610362161-CreatePagePermissions.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
82
server/migrations/1744611380594-CreatePageUsers.ts
Normal file
82
server/migrations/1744611380594-CreatePageUsers.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
29
server/src/entities/page_permissions.entity.ts
Normal file
29
server/src/entities/page_permissions.entity.ts
Normal 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[];
|
||||
}
|
||||
37
server/src/entities/page_users.entity.ts
Normal file
37
server/src/entities/page_users.entity.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
26
server/src/modules/app-permissions/ability/guard.ts
Normal file
26
server/src/modules/app-permissions/ability/guard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
72
server/src/modules/app-permissions/ability/index.ts
Normal file
72
server/src/modules/app-permissions/ability/index.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
server/src/modules/app-permissions/constants/features.ts
Normal file
14
server/src/modules/app-permissions/constants/features.ts
Normal 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]: {},
|
||||
},
|
||||
};
|
||||
14
server/src/modules/app-permissions/constants/index.ts
Normal file
14
server/src/modules/app-permissions/constants/index.ts
Normal 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',
|
||||
}
|
||||
84
server/src/modules/app-permissions/controller.ts
Normal file
84
server/src/modules/app-permissions/controller.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
26
server/src/modules/app-permissions/dto/index.ts
Normal file
26
server/src/modules/app-permissions/dto/index.ts
Normal 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[];
|
||||
}
|
||||
29
server/src/modules/app-permissions/interfaces/IController.ts
Normal file
29
server/src/modules/app-permissions/interfaces/IController.ts
Normal 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>;
|
||||
}
|
||||
16
server/src/modules/app-permissions/interfaces/IService.ts
Normal file
16
server/src/modules/app-permissions/interfaces/IService.ts
Normal 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>;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
35
server/src/modules/app-permissions/module.ts
Normal file
35
server/src/modules/app-permissions/module.ts
Normal 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],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
31
server/src/modules/app-permissions/service.ts
Normal file
31
server/src/modules/app-permissions/service.ts
Normal 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.');
|
||||
}
|
||||
}
|
||||
16
server/src/modules/app-permissions/types/index.ts
Normal file
16
server/src/modules/app-permissions/types/index.ts
Normal 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;
|
||||
}
|
||||
26
server/src/modules/app-permissions/util.service.ts
Normal file
26
server/src/modules/app-permissions/util.service.ts
Normal 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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,4 +36,5 @@ export enum MODULES {
|
|||
IMPORT_EXPORT_RESOURCES = 'ImportExportResources',
|
||||
TEMPLATES = 'Templates',
|
||||
AI = 'ai',
|
||||
APP_PERMISSIONS = 'AppPermissions',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -142,4 +142,5 @@ export const iconConfig = {
|
|||
padding: { value: 'default' },
|
||||
boxShadow: { value: '0px 0px 0px 0px #00000040' },
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -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}}' },
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue