mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 08:58:26 +00:00
Merge branch 'appbuilder/sprint-14' into snapping-react-moveable
This commit is contained in:
commit
6257ec8a78
96 changed files with 1469 additions and 1002 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 387bb6e55bc6a7600b7125bb9e22ca2b17dfe65d
|
||||
Subproject commit 9458c8d66f29f8334765b5757dd096139a8d53d2
|
||||
|
|
@ -143,10 +143,11 @@ export const Container = React.memo(
|
|||
if (canvasWidth !== undefined) {
|
||||
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
|
||||
if (id === 'canvas') return canvasWidth;
|
||||
return getSubContainerWidthAfterPadding(canvasWidth, componentType, id);
|
||||
return getSubContainerWidthAfterPadding(canvasWidth, componentType, id, realCanvasRef);
|
||||
}
|
||||
return realCanvasRef?.current?.offsetWidth;
|
||||
}
|
||||
|
||||
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -39,3 +39,5 @@ export const DROPPABLE_PARENTS = new Set([
|
|||
export const TAB_CANVAS_PADDING = 7.5;
|
||||
|
||||
export const MODAL_CANVAS_PADDING = 5;
|
||||
|
||||
export const LISTVIEW_CANVAS_PADDING = 7;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
BOX_PADDING,
|
||||
TAB_CANVAS_PADDING,
|
||||
MODAL_CANVAS_PADDING,
|
||||
LISTVIEW_CANVAS_PADDING,
|
||||
} from './appCanvasConstants';
|
||||
|
||||
export function snapToGrid(canvasWidth, x, y) {
|
||||
|
|
@ -779,7 +780,7 @@ export const getSubContainerIdWithSlots = (parentId) => {
|
|||
return cleanParentId;
|
||||
};
|
||||
|
||||
export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, componentId) => {
|
||||
export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, componentId, realCanvasRef) => {
|
||||
let padding = 2; //Need to update this 2 to correct value for other subcontainers
|
||||
if (componentType === 'Container' || componentType === 'Form') {
|
||||
padding = 2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING;
|
||||
|
|
@ -797,5 +798,8 @@ export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, com
|
|||
padding = 2 * MODAL_CANVAS_PADDING;
|
||||
}
|
||||
}
|
||||
if (componentType === 'Listview') {
|
||||
padding = 2 * LISTVIEW_CANVAS_PADDING + 5; // 5 is accounting for scrollbar
|
||||
}
|
||||
return canvasWidth - padding;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
|
||||
const wrapperRef = useRef(null);
|
||||
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
|
||||
const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow);
|
||||
const getServerSideGlobalResolveSuggestions = useStore((state) => state.getServerSideGlobalResolveSuggestions, shallow);
|
||||
|
||||
const isInsideQueryPane = !!document.querySelector('.code-hinter-wrapper')?.closest('.query-details');
|
||||
const isInsideQueryManager = useMemo(
|
||||
|
|
@ -116,7 +116,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
|
||||
const hints = getSuggestions();
|
||||
|
||||
const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager);
|
||||
const serverHints = getServerSideGlobalResolveSuggestions(isInsideQueryManager);
|
||||
|
||||
const allHints = {
|
||||
...hints,
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export const PreviewBox = ({
|
|||
const [largeDataset, setLargeDataset] = useState(false);
|
||||
const globals = useStore((state) => state.getAllExposedValues(moduleId).constants || {}, shallow);
|
||||
const secrets = useStore((state) => state.getSecrets(), shallow);
|
||||
const globalServerConstantsRegex = /^\{\{.*globals\.server.*\}\}$/;
|
||||
const globalServerConstantsRegex = /\{\{.*globals\.server.*\}\}/;
|
||||
|
||||
const getPreviewContent = (content, type) => {
|
||||
if (content === undefined || content === null) return currentValue;
|
||||
|
|
@ -251,7 +251,10 @@ const RenderResolvedValue = ({
|
|||
isServerConstant = false,
|
||||
isLargeDataset,
|
||||
}) => {
|
||||
const isServerSideGlobalEnabled = useStore((state) => !!state?.license?.featureAccess?.serverSideGlobal, shallow);
|
||||
const isServerSideGlobalResolveEnabled = useStore(
|
||||
(state) => !!state?.license?.featureAccess?.serverSideGlobalResolve,
|
||||
shallow
|
||||
);
|
||||
|
||||
const computeCoersionPreview = (resolvedValue, coersionData) => {
|
||||
if (coersionData?.typeBeforeCoercion === coersionData?.typeAfterCoercion) return resolvedValue;
|
||||
|
|
@ -276,7 +279,7 @@ const RenderResolvedValue = ({
|
|||
: previewType;
|
||||
|
||||
const previewContent = isServerConstant
|
||||
? isServerSideGlobalEnabled
|
||||
? isServerSideGlobalResolveEnabled
|
||||
? 'Server variables would be resolved at runtime'
|
||||
: 'Server variables are only available in paid plans'
|
||||
: isSecretConstant
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ const EditorInput = ({
|
|||
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
|
||||
const [codeMirrorView, setCodeMirrorView] = useState(undefined);
|
||||
|
||||
const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow);
|
||||
const getServerSideGlobalResolveSuggestions = useStore((state) => state.getServerSideGlobalResolveSuggestions, shallow);
|
||||
|
||||
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline');
|
||||
|
||||
|
|
@ -226,7 +226,7 @@ const EditorInput = ({
|
|||
);
|
||||
function autoCompleteExtensionConfig(context) {
|
||||
const hintsWithoutParamHints = getSuggestions();
|
||||
const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager);
|
||||
const serverHints = getServerSideGlobalResolveSuggestions(isInsideQueryManager);
|
||||
|
||||
let word = context.matchBefore(/\w*/);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ import AppModeToggle from './AppModeToggle';
|
|||
import { ThemeSelect } from '@/modules/Appbuilder/components';
|
||||
import MaintenanceMode from './MaintenanceMode';
|
||||
import HideHeaderToggle from './HideHeaderToggle';
|
||||
import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const GlobalSettings = ({ darkMode }) => {
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModuleProvider moduleId={'canvas'}>
|
||||
<div>
|
||||
<div bsPrefix="global-settings-popover" className="global-settings-panel">
|
||||
<HeaderSection>
|
||||
|
|
@ -44,7 +45,7 @@ const GlobalSettings = ({ darkMode }) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</ModuleProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@ import './style.scss';
|
|||
import { SortableTree } from './Tree/SortableTree';
|
||||
import { PageGroupMenu } from './AddPageButton';
|
||||
import { PageHandlerMenu } from './PageHandlerMenu.jsx';
|
||||
import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal';
|
||||
import { EditModal } from './EditModal';
|
||||
import { SettingsModal } from './SettingsModal';
|
||||
import { DeletePageConfirmationModal } from './DeletePageConfirmationModal';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import PagePermission from './PagePermission';
|
||||
import { appPermissionService } from '@/_services';
|
||||
|
||||
export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
|
||||
const showAddNewPageInput = useStore((state) => state.showAddNewPageInput);
|
||||
|
|
@ -27,6 +28,12 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
|
|||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState);
|
||||
const closePageEditPopover = useStore((state) => state.closePageEditPopover);
|
||||
const editingPageId = useStore((state) => state.editingPage?.id);
|
||||
const editingPageName = useStore((state) => state.editingPage?.name);
|
||||
const showPagePermissionModal = useStore((state) => state.showPagePermissionModal);
|
||||
const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
|
||||
const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
closePageEditPopover();
|
||||
|
|
@ -95,7 +102,21 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
|
|||
>
|
||||
<div>
|
||||
<PageHandlerMenu darkMode={darkMode} />
|
||||
{isLicensed ? <PagePermission darkMode={darkMode} /> : <></>}
|
||||
{isLicensed && (
|
||||
<AppPermissionsModal
|
||||
modalType="page"
|
||||
resourceId={editingPageId}
|
||||
resourceName={editingPageName}
|
||||
showModal={showPagePermissionModal}
|
||||
toggleModal={togglePagePermissionModal}
|
||||
darkMode={darkMode}
|
||||
fetchPermission={(id, appId) => appPermissionService.getPagePermission(appId, id)}
|
||||
createPermission={(id, appId, body) => appPermissionService.createPagePermission(appId, id, body)}
|
||||
updatePermission={(id, appId, body) => appPermissionService.updatePagePermission(appId, id, body)}
|
||||
deletePermission={(id, appId) => appPermissionService.deletePagePermission(appId, id)}
|
||||
onSuccess={(data) => updatePageWithPermissions(editingPageId, data)}
|
||||
/>
|
||||
)}
|
||||
<EditModal darkMode={darkMode} />
|
||||
<SettingsModal darkMode={darkMode} />
|
||||
<DeletePageConfirmationModal darkMode={darkMode} />
|
||||
|
|
|
|||
|
|
@ -1,509 +0,0 @@
|
|||
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';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const PERMISSION_TYPES = {
|
||||
single: 'SINGLE',
|
||||
group: 'GROUP',
|
||||
all: 'ALL',
|
||||
};
|
||||
|
||||
export default function PagePermission({ darkMode }) {
|
||||
const { moduleId } = useModuleContext();
|
||||
const showPagePermissionModal = useStore((state) => state.showPagePermissionModal);
|
||||
const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
|
||||
const editingPage = useStore((state) => state.editingPage);
|
||||
const appId = useStore((state) => state.appStore.modules[moduleId].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 [initialSelectedGroups, setInitialSelectedGroups] = useState([]);
|
||||
const [initialSelectedUsers, setInitialSelectedUsers] = useState([]);
|
||||
const [initalPagePermissionType, setInitialPagePermissionType] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPagePermissionModal) return;
|
||||
const fetchPagePermission = () => {
|
||||
appPermissionService.getPagePermission(appId, editingPage?.id).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);
|
||||
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);
|
||||
setInitialSelectedUsers(users);
|
||||
data?.length && setSelectedUsers(users);
|
||||
}
|
||||
}
|
||||
setPermissionsLoading(false);
|
||||
});
|
||||
};
|
||||
fetchPagePermission();
|
||||
}, [showPagePermissionModal]);
|
||||
|
||||
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, editingPage?.id)
|
||||
.then((data) => {
|
||||
toast.success('Permission successfully deleted!', {
|
||||
className: 'text-nowrap w-auto mw-100',
|
||||
});
|
||||
updatePageWithPermissions(editingPage?.id, []);
|
||||
})
|
||||
.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
|
||||
? 'Save changes'
|
||||
: pagePermissionType === 'all'
|
||||
? 'Default permission'
|
||||
: 'Create permission',
|
||||
disabled: isPermissionsLoading || isSelectionUnchanged,
|
||||
tooltipMessage: '',
|
||||
leftIcon: pagePermission && 'save',
|
||||
className: 'action-btn-page-permission',
|
||||
}}
|
||||
darkMode={darkMode}
|
||||
className="page-permissions-modal"
|
||||
>
|
||||
<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 { moduleId } = useModuleContext();
|
||||
const appId = useStore((state) => state.appStore.modules[moduleId].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 { moduleId } = useModuleContext();
|
||||
const appId = useStore((state) => state.appStore.modules[moduleId].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>
|
||||
);
|
||||
};
|
||||
|
|
@ -289,115 +289,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-base .modal-footer .action-btn-page-permission svg path {
|
||||
fill: var(--indigo1) !important;
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import { updateQuerySuggestions } from '@/_helpers/appUtils';
|
||||
// import { Confirm } from '../Viewer/Confirm';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import Copy from '@/_ui/Icon/solidIcons/Copy';
|
||||
import DataSourceIcon from '../QueryManager/Components/DataSourceIcon';
|
||||
import { isQueryRunnable, decodeEntities } from '@/_helpers/utils';
|
||||
import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers';
|
||||
|
|
@ -12,13 +12,10 @@ import useStore from '@/AppBuilder/_stores/store';
|
|||
//TODO: Remove this
|
||||
import { Confirm } from '@/Editor/Viewer/Confirm';
|
||||
// TODO: enable delete query confirmation popup
|
||||
import { debounce } from 'lodash';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
|
||||
|
||||
const isQuerySelected = useStore((state) => state.queryPanel.isQuerySelected(dataQuery.id), shallow);
|
||||
const setSelectedQuery = useStore((state) => state.queryPanel.setSelectedQuery);
|
||||
const checkExistingQueryName = useStore((state) => state.dataQuery.checkExistingQueryName);
|
||||
|
|
@ -26,9 +23,16 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
|||
const isDeletingQueryInProcess = useStore((state) => state.dataQuery.isDeletingQueryInProcess);
|
||||
const renameQuery = useStore((state) => state.dataQuery.renameQuery);
|
||||
const deleteDataQueries = useStore((state) => state.dataQuery.deleteDataQueries);
|
||||
const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery);
|
||||
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
|
||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
|
||||
const renamingQueryId = useStore((state) => state.queryPanel.renamingQueryId);
|
||||
const deletingQueryId = useStore((state) => state.queryPanel.deletingQueryId);
|
||||
const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery);
|
||||
const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery);
|
||||
const isRenaming = renamingQueryId === dataQuery.id;
|
||||
const isDeleting = deletingQueryId === dataQuery.id;
|
||||
|
||||
const hasPermissions =
|
||||
selectedDataSourceScope === 'global'
|
||||
? canUpdateDataSource(dataQuery?.data_source_id) ||
|
||||
|
|
@ -36,57 +40,77 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
|||
canDeleteDataSource()
|
||||
: true;
|
||||
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
|
||||
const [renamingQuery, setRenamingQuery] = useState(false);
|
||||
|
||||
const deleteDataQuery = (e) => {
|
||||
e.stopPropagation();
|
||||
setShowDeleteConfirmation(true);
|
||||
};
|
||||
const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu);
|
||||
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
|
||||
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
|
||||
const isRestricted = dataQuery.permissions && dataQuery.permissions.length !== 0;
|
||||
|
||||
const updateQueryName = (dataQuery, newName) => {
|
||||
const { name } = dataQuery;
|
||||
if (name === newName) {
|
||||
return setRenamingQuery(false);
|
||||
return setRenamingQuery(null);
|
||||
}
|
||||
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName);
|
||||
if (newName && !isNewQueryNameAlreadyExists) {
|
||||
renameQuery(dataQuery?.id, newName);
|
||||
setRenamingQuery(false);
|
||||
setRenamingQuery(null);
|
||||
updateQuerySuggestions(name, newName);
|
||||
} else {
|
||||
if (isNewQueryNameAlreadyExists) {
|
||||
toast.error('Query name already exists');
|
||||
}
|
||||
setRenamingQuery(false);
|
||||
setRenamingQuery(null);
|
||||
}
|
||||
};
|
||||
|
||||
const executeDataQueryDeletion = () => {
|
||||
setShowDeleteConfirmation(false);
|
||||
deleteDataQuery(null);
|
||||
deleteDataQueries(dataQuery?.id);
|
||||
setPreviewData(null);
|
||||
};
|
||||
|
||||
// To prevent user clicking from continuous clicks
|
||||
const debouncedDuplicateQuery = useCallback(
|
||||
debounce((queryId, appId) => {
|
||||
duplicateQuery(queryId, appId);
|
||||
setPreviewData(null);
|
||||
}, 500),
|
||||
[duplicateQuery]
|
||||
);
|
||||
const getTooltip = () => {
|
||||
const permission = dataQuery.permissions?.[0];
|
||||
if (!permission) return null;
|
||||
|
||||
const users = permission.groups || permission.users || [];
|
||||
if (users.length === 0) return null;
|
||||
|
||||
const isSingle = permission.type === 'SINGLE';
|
||||
const isGroup = permission.type === 'GROUP';
|
||||
|
||||
if (isSingle) {
|
||||
return users.length === 1
|
||||
? `Access restricted to ${users[0].user.email}`
|
||||
: `Access restricted to ${users.length} users`;
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
return users.length === 1
|
||||
? `Access restricted to ${users[0].permission_group?.name || users[0].permissionGroup?.name} group`
|
||||
: `Access restricted to ${users.length} user groups`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`row query-row pe-2 ${darkMode && 'dark-theme'}` + (isQuerySelected ? ' query-row-selected' : '')}
|
||||
key={dataQuery.id}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (isQuerySelected) return;
|
||||
setSelectedQuery(dataQuery?.id);
|
||||
setPreviewData(null);
|
||||
const menuBtn = document.getElementById(`query-handler-menu-${dataQuery?.id}`);
|
||||
if (menuBtn.contains(e.target)) {
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
toggleQueryHandlerMenu(false);
|
||||
}
|
||||
setTimeout(() => {
|
||||
setSelectedQuery(dataQuery?.id);
|
||||
setPreviewData(null);
|
||||
}, 0);
|
||||
}}
|
||||
role="button"
|
||||
>
|
||||
|
|
@ -94,7 +118,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
|||
<DataSourceIcon source={dataQuery} height={16} />
|
||||
</div>
|
||||
<div className="col query-row-query-name">
|
||||
{renamingQuery ? (
|
||||
{isRenaming ? (
|
||||
<input
|
||||
data-cy={`query-edit-input-field`}
|
||||
className={`query-name query-name-input-field border-indigo-09 bg-transparent ${
|
||||
|
|
@ -121,7 +145,12 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
|||
data-tooltip-dynamic="true"
|
||||
>
|
||||
{decodeEntities(dataQuery.name)}
|
||||
</span>{' '}
|
||||
</span>
|
||||
<ToolTip message={getTooltip()} show={licenseValid && isRestricted}>
|
||||
<div className="d-flex align-items-center" style={{ marginLeft: '8px', marginRight: 'auto' }}>
|
||||
{licenseValid && isRestricted && <SolidIcon width="16" name="lock" fill="var(--icon-strong)" />}
|
||||
</div>
|
||||
</ToolTip>{' '}
|
||||
{!isQueryRunnable(dataQuery) && <small className="mx-2 text-secondary">Draft</small>}
|
||||
{localDs && (
|
||||
<>
|
||||
|
|
@ -143,80 +172,24 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!shouldFreeze && isQuerySelected && (
|
||||
<div className="col-auto query-rename-delete-btn">
|
||||
<div
|
||||
className={`col-auto ${(renamingQuery || !hasPermissions) && 'd-none'} rename-query`}
|
||||
onClick={() => setRenamingQuery(true)}
|
||||
>
|
||||
<span className="d-flex" data-tooltip-id="query-card-btn-tooltip" data-tooltip-content="Rename query">
|
||||
<svg
|
||||
data-cy={`edit-query-${dataQuery.name.toLowerCase()}`}
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 19 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.7087 1.40712C14.29 0.826221 15.0782 0.499893 15.9 0.499893C16.7222 0.499893 17.5107 0.82651 18.0921 1.40789C18.6735 1.98928 19.0001 2.7778 19.0001 3.6C19.0001 4.42197 18.6737 5.21028 18.0926 5.79162C18.0924 5.79178 18.0928 5.79145 18.0926 5.79162L16.8287 7.06006C16.7936 7.11191 16.753 7.16118 16.7071 7.20711C16.6621 7.25215 16.6138 7.292 16.563 7.32665L9.70837 14.2058C9.52073 14.3942 9.26584 14.5 9 14.5H6C5.44772 14.5 5 14.0523 5 13.5V10.5C5 10.2342 5.10585 9.97927 5.29416 9.79163L12.1733 2.93697C12.208 2.88621 12.2478 2.83794 12.2929 2.79289C12.3388 2.74697 12.3881 2.70645 12.4399 2.67132L13.7079 1.40789C13.7082 1.40763 13.7084 1.40738 13.7087 1.40712ZM13.0112 4.92545L7 10.9153V12.5H8.58474L14.5745 6.48876L13.0112 4.92545ZM15.9862 5.07202L14.428 3.51376L15.1221 2.82211C15.3284 2.6158 15.6082 2.49989 15.9 2.49989C16.1918 2.49989 16.4716 2.6158 16.6779 2.82211C16.8842 3.02842 17.0001 3.30823 17.0001 3.6C17.0001 3.89177 16.8842 4.17158 16.6779 4.37789L15.9862 5.07202ZM0.87868 5.37868C1.44129 4.81607 2.20435 4.5 3 4.5H4C4.55228 4.5 5 4.94772 5 5.5C5 6.05228 4.55228 6.5 4 6.5H3C2.73478 6.5 2.48043 6.60536 2.29289 6.79289C2.10536 6.98043 2 7.23478 2 7.5V16.5C2 16.7652 2.10536 17.0196 2.29289 17.2071C2.48043 17.3946 2.73478 17.5 3 17.5H12C12.2652 17.5 12.5196 17.3946 12.7071 17.2071C12.8946 17.0196 13 16.7652 13 16.5V15.5C13 14.9477 13.4477 14.5 14 14.5C14.5523 14.5 15 14.9477 15 15.5V16.5C15 17.2957 14.6839 18.0587 14.1213 18.6213C13.5587 19.1839 12.7957 19.5 12 19.5H3C2.20435 19.5 1.44129 19.1839 0.87868 18.6213C0.31607 18.0587 0 17.2957 0 16.5V7.5C0 6.70435 0.31607 5.94129 0.87868 5.37868Z"
|
||||
fill="#11181C"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`col-auto rename-query ${!hasPermissions && 'd-none'}`}
|
||||
onClick={() => debouncedDuplicateQuery(dataQuery?.id, appId)}
|
||||
>
|
||||
<span className="d-flex" data-tooltip-id="query-card-btn-tooltip" data-tooltip-content="Duplicate query">
|
||||
<Copy height={16} width={16} viewBox="0 5 20 20" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
{isDeletingQueryInProcess ? (
|
||||
<div className="px-2">
|
||||
<div className="text-center spinner-border spinner-border-sm" role="status"></div>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
className={`delete-query ${!hasPermissions && 'd-none'}`}
|
||||
onClick={deleteDataQuery}
|
||||
data-tooltip-id="query-card-btn-tooltip"
|
||||
data-tooltip-content="Delete query"
|
||||
>
|
||||
<span className="d-flex">
|
||||
<svg
|
||||
data-cy={`delete-query-${dataQuery.name.toLowerCase()}`}
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 18 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.58579 0.585786C5.96086 0.210714 6.46957 0 7 0H11C11.5304 0 12.0391 0.210714 12.4142 0.585786C12.7893 0.960859 13 1.46957 13 2V4H15.9883C15.9953 3.99993 16.0024 3.99993 16.0095 4H17C17.5523 4 18 4.44772 18 5C18 5.55228 17.5523 6 17 6H16.9201L15.9997 17.0458C15.9878 17.8249 15.6731 18.5695 15.1213 19.1213C14.5587 19.6839 13.7957 20 13 20H5C4.20435 20 3.44129 19.6839 2.87868 19.1213C2.32687 18.5695 2.01223 17.8249 2.00035 17.0458L1.07987 6H1C0.447715 6 0 5.55228 0 5C0 4.44772 0.447715 4 1 4H1.99054C1.9976 3.99993 2.00466 3.99993 2.0117 4H5V2C5 1.46957 5.21071 0.960859 5.58579 0.585786ZM3.0868 6L3.99655 16.917C3.99885 16.9446 4 16.9723 4 17C4 17.2652 4.10536 17.5196 4.29289 17.7071C4.48043 17.8946 4.73478 18 5 18H13C13.2652 18 13.5196 17.8946 13.7071 17.7071C13.8946 17.5196 14 17.2652 14 17C14 16.9723 14.0012 16.9446 14.0035 16.917L14.9132 6H3.0868ZM11 4H7V2H11V4ZM6.29289 10.7071C5.90237 10.3166 5.90237 9.68342 6.29289 9.29289C6.68342 8.90237 7.31658 8.90237 7.70711 9.29289L9 10.5858L10.2929 9.29289C10.6834 8.90237 11.3166 8.90237 11.7071 9.29289C12.0976 9.68342 12.0976 10.3166 11.7071 10.7071L10.4142 12L11.7071 13.2929C12.0976 13.6834 12.0976 14.3166 11.7071 14.7071C11.3166 15.0976 10.6834 15.0976 10.2929 14.7071L9 13.4142L7.70711 14.7071C7.31658 15.0976 6.68342 15.0976 6.29289 14.7071C5.90237 14.3166 5.90237 13.6834 6.29289 13.2929L7.58579 12L6.29289 10.7071Z"
|
||||
fill="#DB4324"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip id="query-card-btn-tooltip" className="tooltip" />
|
||||
</div>
|
||||
)}
|
||||
<div className={`col-auto query-rename-delete-btn ${!shouldFreeze && isQuerySelected ? 'd-flex' : 'd-none'}`}>
|
||||
<ButtonComponent
|
||||
iconOnly
|
||||
leadingIcon="morevertical01"
|
||||
onClick={(e) => toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)}
|
||||
size="small"
|
||||
variant="outline"
|
||||
className=""
|
||||
id={`query-handler-menu-${dataQuery?.id}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Confirm
|
||||
show={showDeleteConfirmation}
|
||||
show={isDeleting}
|
||||
message={'Do you really want to delete this query?'}
|
||||
confirmButtonLoading={isDeletingQueryInProcess}
|
||||
onConfirm={executeDataQueryDeletion}
|
||||
onCancel={() => setShowDeleteConfirmation(false)}
|
||||
onCancel={() => deleteDataQuery(null)}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
174
frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx
Normal file
174
frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { Overlay, Popover } from 'react-bootstrap';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import classNames from 'classnames';
|
||||
import Edit from '@/_ui/Icon/bulkIcons/Edit';
|
||||
import Trash from '@/_ui/Icon/solidIcons/Trash';
|
||||
import Copy from '@/_ui/Icon/solidIcons/Copy';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import { debounce } from 'lodash';
|
||||
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const QueryCardMenu = ({ darkMode }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
|
||||
const selectedQuery = useStore((state) => state.queryPanel.selectedQuery);
|
||||
const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal);
|
||||
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
|
||||
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
|
||||
const targetBtnForMenu = useStore((state) => state.queryPanel.targetBtnForMenu);
|
||||
const targetElement = document.getElementById(targetBtnForMenu);
|
||||
const showQueryHandlerMenu = useStore((state) => state.queryPanel.showQueryHandlerMenu);
|
||||
const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu);
|
||||
const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery);
|
||||
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
|
||||
const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery);
|
||||
const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery);
|
||||
|
||||
const QUERY_MENU_OPTIONS = [
|
||||
{
|
||||
label: 'Rename',
|
||||
value: 'rename',
|
||||
icon: <Edit width={16} />,
|
||||
showTooltip: false,
|
||||
},
|
||||
{
|
||||
label: 'Duplicate',
|
||||
value: 'duplicate',
|
||||
icon: <Copy width={16} />,
|
||||
showTooltip: false,
|
||||
},
|
||||
{
|
||||
label: 'Query permission',
|
||||
value: 'permission',
|
||||
icon: (
|
||||
<img
|
||||
alt="permission-icon"
|
||||
src="assets/images/icons/editor/left-sidebar/authorization.svg"
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
),
|
||||
trailingIcon: <SolidIcon width={16} name="enterprisecrown" className="mx-1" />,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
value: 'delete',
|
||||
icon: <Trash width={16} fill={'#E54D2E'} />,
|
||||
showTooltip: false,
|
||||
},
|
||||
];
|
||||
|
||||
// To prevent user clicking from continuous clicks
|
||||
const debouncedDuplicateQuery = useCallback(
|
||||
debounce((queryId, appId) => {
|
||||
duplicateQuery(queryId, appId);
|
||||
setPreviewData(null);
|
||||
}, 500),
|
||||
[duplicateQuery]
|
||||
);
|
||||
|
||||
const handleQueryMenuActions = (value) => {
|
||||
if (value === 'rename') {
|
||||
setRenamingQuery(selectedQuery?.id);
|
||||
}
|
||||
if (value === 'duplicate') {
|
||||
debouncedDuplicateQuery(selectedQuery?.id, appId);
|
||||
}
|
||||
if (value === 'permission') {
|
||||
if (!licenseValid) return;
|
||||
toggleQueryPermissionModal(true);
|
||||
}
|
||||
if (value === 'delete') {
|
||||
deleteDataQuery(selectedQuery?.id);
|
||||
}
|
||||
toggleQueryHandlerMenu(false);
|
||||
};
|
||||
|
||||
usePopoverObserver(
|
||||
document.getElementsByClassName('query-list')[0],
|
||||
targetElement,
|
||||
document.getElementById('query-list-menu'),
|
||||
showQueryHandlerMenu,
|
||||
() => (document.getElementById('query-list-menu').style.display = 'block'),
|
||||
() => (document.getElementById('query-list-menu').style.display = 'none')
|
||||
);
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
placement="bottom-start"
|
||||
target={targetElement}
|
||||
show={showQueryHandlerMenu}
|
||||
rootClose
|
||||
onHide={() => toggleQueryHandlerMenu(false)}
|
||||
popperConfig={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
fallbackPlacements: ['top-start'],
|
||||
flipVariations: true,
|
||||
allowedAutoPlacements: ['top', 'bottom'],
|
||||
boundary: 'viewport',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 3],
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{(props) => (
|
||||
<Popover {...props} id="query-list-menu" className={darkMode && 'dark-theme'}>
|
||||
<Popover.Body bsPrefix="list-item-popover-body">
|
||||
{QUERY_MENU_OPTIONS.map((option) => {
|
||||
const optionBody = (
|
||||
<div
|
||||
data-cy={`component-inspector-${String(option?.value).toLowerCase()}-button`}
|
||||
className="list-item-popover-option"
|
||||
key={option?.value}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQueryMenuActions(option.value);
|
||||
}}
|
||||
>
|
||||
<div className="list-item-popover-menu-option-icon">{option.icon}</div>
|
||||
<div
|
||||
className={classNames('list-item-option-menu-label', {
|
||||
'color-tomato9': option.value === 'delete',
|
||||
'color-disabled': option.value === 'permission' && !licenseValid,
|
||||
})}
|
||||
>
|
||||
{option?.label}
|
||||
</div>
|
||||
{option.value === 'permission' && !licenseValid && option.trailingIcon && option.trailingIcon}
|
||||
</div>
|
||||
);
|
||||
|
||||
return option.value === 'permission' ? (
|
||||
<ToolTip
|
||||
key={option.value}
|
||||
message={'Component permissions are available only in paid plans'}
|
||||
placement="left"
|
||||
show={!licenseValid}
|
||||
>
|
||||
{optionBody}
|
||||
</ToolTip>
|
||||
) : (
|
||||
optionBody
|
||||
);
|
||||
})}
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
)}
|
||||
</Overlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryCardMenu;
|
||||
|
|
@ -16,6 +16,10 @@ import DataSourceSelect from '../QueryManager/Components/DataSourceSelect';
|
|||
import { OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import FolderEmpty from '@/_ui/Icon/solidIcons/FolderEmpty';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { appPermissionService } from '@/_services';
|
||||
import QueryCardMenu from './QueryCardMenu';
|
||||
|
||||
export const QueryDataPane = ({ darkMode }) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -34,6 +38,12 @@ export const QueryDataPane = ({ darkMode }) => {
|
|||
function isDataSourceLocal(dataQuery) {
|
||||
return dataSources.some((dataSource) => dataSource.id === dataQuery.data_source_id);
|
||||
}
|
||||
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
|
||||
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
|
||||
const selectedQuery = useStore((state) => state.queryPanel.selectedQuery);
|
||||
const showQueryPermissionModal = useStore((state) => state.queryPanel.showQueryPermissionModal);
|
||||
const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal);
|
||||
const setQueries = useStore((state) => state.dataQuery.setQueries);
|
||||
|
||||
useEffect(() => {
|
||||
setQueryPanelSearchTerm(searchTermForFilters);
|
||||
|
|
@ -171,6 +181,33 @@ export const QueryDataPane = ({ darkMode }) => {
|
|||
{filteredQueries.map((query) => (
|
||||
<QueryCard key={query.id} dataQuery={query} darkMode={darkMode} localDs={!!isDataSourceLocal(query)} />
|
||||
))}
|
||||
<QueryCardMenu darkMode={darkMode} />
|
||||
{licenseValid && (
|
||||
<AppPermissionsModal
|
||||
modalType="query"
|
||||
resourceId={selectedQuery?.id}
|
||||
resourceName={selectedQuery?.name}
|
||||
showModal={showQueryPermissionModal}
|
||||
toggleModal={toggleQueryPermissionModal}
|
||||
darkMode={darkMode}
|
||||
fetchPermission={(id, appId) => appPermissionService.getQueryPermission(appId, id)}
|
||||
createPermission={(id, appId, body) => appPermissionService.createQueryPermission(appId, id, body)}
|
||||
updatePermission={(id, appId, body) => appPermissionService.updateQueryPermission(appId, id, body)}
|
||||
deletePermission={(id, appId) => appPermissionService.deleteQueryPermission(appId, id)}
|
||||
onSuccess={(data) => {
|
||||
const updatedDataQueries = dataQueries.map((query) => {
|
||||
if (query.id === selectedQuery.id) {
|
||||
return {
|
||||
...query,
|
||||
permissions: data.length === 0 || data.length === undefined ? [] : [data[0]],
|
||||
};
|
||||
}
|
||||
return query;
|
||||
});
|
||||
setQueries(updatedDataQueries);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
id="query-card-name-tooltip"
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ export const EventManager = ({
|
|||
if (data.label === 'run-action') return;
|
||||
return (
|
||||
<div
|
||||
className="tw-border-x-0 tw-border-t-0 tw-border-b-[0.5px] tw-border-solid tw-my-[4px]"
|
||||
className="tw-border-x-0 tw-border-t-0 tw-border-b-[1px] tw-border-solid tw-my-[4px]"
|
||||
style={{ borderColor: 'var(--border-weak)' }}
|
||||
></div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -47,11 +47,6 @@ export const containerConfig = {
|
|||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
},
|
||||
defaultChildren: [
|
||||
{
|
||||
|
|
@ -61,10 +56,10 @@ export const containerConfig = {
|
|||
top: 20,
|
||||
left: 1,
|
||||
height: 40,
|
||||
width: 20,
|
||||
},
|
||||
displayName: 'ContainerText',
|
||||
properties: ['text'],
|
||||
slotName: 'header',
|
||||
accessorKey: 'text',
|
||||
styles: ['fontWeight', 'textSize', 'textColor'],
|
||||
defaultValue: {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const listviewConfig = {
|
|||
top: 15,
|
||||
left: 3,
|
||||
height: 100,
|
||||
width: 7,
|
||||
width: 4,
|
||||
},
|
||||
properties: ['source'],
|
||||
accessorKey: 'imageURL',
|
||||
|
|
@ -24,6 +24,7 @@ export const listviewConfig = {
|
|||
top: 50,
|
||||
left: 11,
|
||||
height: 30,
|
||||
width: 4,
|
||||
},
|
||||
properties: ['text'],
|
||||
accessorKey: 'text',
|
||||
|
|
@ -49,12 +50,14 @@ export const listviewConfig = {
|
|||
data: {
|
||||
type: 'code',
|
||||
displayName: 'List data',
|
||||
schema: {
|
||||
type: 'union',
|
||||
schemas: [
|
||||
{ type: 'array', element: { type: 'object' } },
|
||||
{ type: 'array', element: { type: 'string' } },
|
||||
],
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'union',
|
||||
schemas: [
|
||||
{ type: 'array', element: { type: 'object' } },
|
||||
{ type: 'array', element: { type: 'string' } },
|
||||
],
|
||||
},
|
||||
defaultValue: "[{text: 'Sample text 1'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -92,18 +92,6 @@ export const modalV2Config = {
|
|||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
footerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Footer height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
|
||||
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
|
||||
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export const tableConfig = {
|
|||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
defaultSize: {
|
||||
width: 35,
|
||||
width: 25,
|
||||
height: 456,
|
||||
},
|
||||
events: {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {
|
|||
} from '@/AppBuilder/AppCanvas/appCanvasConstants';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import './container.scss';
|
||||
import { useActiveSlot } from '@/AppBuilder/_hooks/useActiveSlot';
|
||||
import { HorizontalSlot } from '@/AppBuilder/Widgets/Form/Components/HorizontalSlot';
|
||||
|
||||
export const Container = ({
|
||||
id,
|
||||
|
|
@ -33,8 +35,13 @@ export const Container = ({
|
|||
shallow
|
||||
);
|
||||
|
||||
const isEditing = useStore((state) => state.currentMode === 'edit');
|
||||
const setComponentProperty = useStore((state) => state.setComponentProperty, shallow);
|
||||
|
||||
const activeSlot = useActiveSlot(isEditing ? id : null); // Track the active slot for this widget
|
||||
const { borderRadius, borderColor, boxShadow } = styles;
|
||||
const { headerHeight = 80 } = properties;
|
||||
const headerMaxHeight = parseInt(height, 10) - 100 - 10;
|
||||
const contentBgColor = useMemo(() => {
|
||||
return {
|
||||
backgroundColor:
|
||||
|
|
@ -65,9 +72,9 @@ export const Container = ({
|
|||
const containerHeaderStyles = {
|
||||
flexShrink: 0,
|
||||
padding: `${CONTAINER_FORM_CANVAS_PADDING}px ${CONTAINER_FORM_CANVAS_PADDING}px 3px ${CONTAINER_FORM_CANVAS_PADDING}px`,
|
||||
maxHeight: `${headerMaxHeight}px`,
|
||||
...headerBgColor,
|
||||
};
|
||||
|
||||
const containerContentStyles = {
|
||||
overflow: 'hidden auto',
|
||||
display: 'flex',
|
||||
|
|
@ -75,6 +82,11 @@ export const Container = ({
|
|||
padding: `${CONTAINER_FORM_CANVAS_PADDING}px`,
|
||||
};
|
||||
|
||||
const updateHeaderSizeInStore = ({ newHeight }) => {
|
||||
const _height = parseInt(newHeight, 10);
|
||||
setComponentProperty(id, `headerHeight`, _height, 'properties', 'value', false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`jet-container ${isLoading ? 'jet-container-loading' : ''}`}
|
||||
|
|
@ -87,17 +99,19 @@ export const Container = ({
|
|||
) : (
|
||||
<>
|
||||
{properties.showHeader && (
|
||||
<div style={containerHeaderStyles} className="wj-container-header">
|
||||
<ContainerComponent
|
||||
id={`${id}-header`}
|
||||
styles={{ ...headerBgColor, height: `${headerHeight}px` }}
|
||||
canvasHeight={headerHeight / 10}
|
||||
canvasWidth={width}
|
||||
allowContainerSelect={true}
|
||||
darkMode={darkMode}
|
||||
componentType="Container"
|
||||
/>
|
||||
</div>
|
||||
<HorizontalSlot
|
||||
slotName={'header'}
|
||||
slotStyle={containerHeaderStyles}
|
||||
isEditing={isEditing}
|
||||
id={`${id}-header`}
|
||||
height={headerHeight}
|
||||
width={width}
|
||||
darkMode={darkMode}
|
||||
isDisabled={isDisabled}
|
||||
isActive={activeSlot === `${id}-header`}
|
||||
onResize={updateHeaderSizeInStore}
|
||||
componentType="Container"
|
||||
/>
|
||||
)}
|
||||
<div style={containerContentStyles}>
|
||||
<ContainerComponent
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
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';
|
||||
import { showGridLines, hideGridLines } from '@/AppBuilder/AppCanvas/Grid/gridUtils';
|
||||
import { useSubContainerResizable } from '@/AppBuilder/_hooks/useSubContainerResizable';
|
||||
|
||||
export const HorizontalSlot = React.memo(
|
||||
({
|
||||
|
|
@ -16,10 +16,10 @@ export const HorizontalSlot = React.memo(
|
|||
onResize,
|
||||
isEditing,
|
||||
maxHeight,
|
||||
componentType,
|
||||
}) => {
|
||||
const parsedHeight = parseInt(height, 10);
|
||||
|
||||
const { getRootProps, getHandleProps, getResizeState } = useResizable({
|
||||
const { getRootProps, getHandleProps, getResizeState } = useSubContainerResizable({
|
||||
initialHeight: parsedHeight,
|
||||
initialWidth: '100%', // Now respects parent's width
|
||||
minHeight: 10,
|
||||
|
|
@ -34,12 +34,11 @@ export const HorizontalSlot = React.memo(
|
|||
});
|
||||
|
||||
const { height: resizedHeight, isDragging } = getResizeState();
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
showGridLinesOnSlot(id);
|
||||
showGridLines();
|
||||
} else {
|
||||
hideGridLinesOnSlot(id);
|
||||
hideGridLines();
|
||||
}
|
||||
}, [isDragging, id]);
|
||||
|
||||
|
|
@ -50,7 +49,10 @@ export const HorizontalSlot = React.memo(
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={`jet-form-${slotName} wj-form-${slotName}`} style={slotStyle}>
|
||||
<div
|
||||
className={`jet-${componentType?.toLowerCase()}-${slotName} wj-${componentType?.toLowerCase()}-${slotName}`}
|
||||
style={slotStyle}
|
||||
>
|
||||
<div
|
||||
className={`resizable-slot only-${slotName} ${isActive ? 'active' : ''} ${isEditing && 'is-editing'} ${
|
||||
isDragging ? 'dragging' : ''
|
||||
|
|
@ -68,7 +70,7 @@ export const HorizontalSlot = React.memo(
|
|||
backgroundColor: 'transparent',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
componentType="Form"
|
||||
componentType={componentType}
|
||||
/>
|
||||
{isEditing && <div className="resize-handle" {...getHandleProps()} style={resizeStyle} />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -341,6 +341,7 @@ export const Form = function Form(props) {
|
|||
isDisabled={isDisabled}
|
||||
isActive={activeSlot === `${id}-header`}
|
||||
onResize={updateHeaderSizeInStore}
|
||||
componentType="Form"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -417,6 +418,7 @@ export const Form = function Form(props) {
|
|||
isDisabled={isDisabled}
|
||||
onResize={updateFooterSizeInStore}
|
||||
isActive={activeSlot === `${id}-footer`}
|
||||
componentType="Form"
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ export const Item = React.memo(
|
|||
>
|
||||
<div className="subcontainer-container" onMouseDown={(e) => e.stopPropagation()}>
|
||||
<SubContainer
|
||||
parentComponent={component}
|
||||
canvasWidth={Number(cardWidth) || 300}
|
||||
canvasHeight={Number(cardHeight) || 100}
|
||||
id={id}
|
||||
|
|
@ -90,11 +89,7 @@ export const Item = React.memo(
|
|||
backgroundColor: 'var(--base)',
|
||||
}}
|
||||
darkMode={darkMode}
|
||||
// parentName={component.name}
|
||||
// customResolvables={{ cardData: cardDataAsObj[value] }}
|
||||
// {...containerProps}
|
||||
// readOnly={isDragActive || !isFirstItem}
|
||||
// parentRef={parentRef}
|
||||
componentType="Kanban"
|
||||
/>
|
||||
</div>
|
||||
<span className="handle-container">
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ export const Listview = function Listview({
|
|||
display: visibility ? 'flex' : 'none',
|
||||
borderRadius: borderRadius ?? 0,
|
||||
boxShadow,
|
||||
padding: '7px 2px 7px 7px',
|
||||
scrollbarGutter: 'stable',
|
||||
};
|
||||
|
||||
const computeCanvasBackgroundColor = useMemo(() => {
|
||||
|
|
@ -235,7 +237,6 @@ export const Listview = function Listview({
|
|||
// Update the customResolvables with the new listItems
|
||||
if (listItems.length > 0) updateCustomResolvables(id, listItems, 'listItem', moduleId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-disabled={disabledState}
|
||||
|
|
@ -243,10 +244,9 @@ export const Listview = function Listview({
|
|||
id={id}
|
||||
ref={parentRef}
|
||||
style={computedStyles}
|
||||
// onClick={() => containerProps.onComponentClick(id, component)}
|
||||
data-cy={dataCy}
|
||||
>
|
||||
<div className={`row w-100 m-0 ${enablePagination && 'pagination-margin-bottom-last-child'}`}>
|
||||
<div className={`w-100 m-0 ${enablePagination && 'pagination-margin-bottom-last-child'}`}>
|
||||
{filteredData.map((listItem, index) => (
|
||||
<div
|
||||
className={`list-item ${mode == 'list' && 'w-100'} ${showBorder && mode == 'list' ? 'border-bottom' : ''}`}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,55 @@
|
|||
import React from 'react';
|
||||
import { default as BootstrapModal } from 'react-bootstrap/Modal';
|
||||
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
|
||||
import { getCanvasHeight } from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
|
||||
import { HorizontalSlot } from '@/AppBuilder/Widgets/Form/Components/HorizontalSlot';
|
||||
import { MODAL_CANVAS_PADDING } from '@/AppBuilder/AppCanvas/appCanvasConstants';
|
||||
|
||||
export const ModalFooter = React.memo(({ id, isDisabled, customStyles, darkMode, width, footerHeight, onClick }) => {
|
||||
const canvasFooterHeight = getCanvasHeight(footerHeight);
|
||||
return (
|
||||
<BootstrapModal.Footer style={{ ...customStyles.modalFooter }} data-cy={`modal-footer`} onClick={onClick}>
|
||||
<SubContainer
|
||||
id={`${id}-footer`}
|
||||
canvasHeight={canvasFooterHeight}
|
||||
canvasWidth={width}
|
||||
allowContainerSelect={false}
|
||||
darkMode={darkMode}
|
||||
styles={{
|
||||
margin: 0,
|
||||
backgroundColor: 'transparent',
|
||||
overflowX: 'hidden',
|
||||
overflowY: isDisabled ? 'hidden' : 'auto',
|
||||
}}
|
||||
componentType="ModalV2"
|
||||
/>
|
||||
{isDisabled && (
|
||||
<div
|
||||
id={`${id}-footer-disabled`}
|
||||
className="tj-modal-disabled-overlay"
|
||||
style={{ height: footerHeight || '100%' }}
|
||||
onClick={onClick}
|
||||
onDrop={(e) => e.stopPropagation()}
|
||||
export const ModalFooter = React.memo(
|
||||
({
|
||||
id,
|
||||
isDisabled,
|
||||
customStyles,
|
||||
darkMode,
|
||||
width,
|
||||
footerHeight,
|
||||
onClick,
|
||||
isEditing,
|
||||
updateFooterSizeInStore,
|
||||
activeSlot,
|
||||
footerMaxHeight,
|
||||
isFullScreen,
|
||||
}) => {
|
||||
const canvasFooterHeight = getCanvasHeight(footerHeight);
|
||||
return (
|
||||
<BootstrapModal.Footer style={{ ...customStyles.modalFooter }} data-cy={`modal-footer`} onClick={onClick}>
|
||||
<HorizontalSlot
|
||||
slotName={'footer'}
|
||||
slotStyle={{
|
||||
width: `100%`,
|
||||
padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`,
|
||||
margin: '0px',
|
||||
}}
|
||||
isEditing={isEditing}
|
||||
id={`${id}-footer`}
|
||||
height={canvasFooterHeight}
|
||||
width={width}
|
||||
darkMode={darkMode}
|
||||
isDisabled={isDisabled}
|
||||
isActive={activeSlot === `${id}-footer`}
|
||||
onResize={updateFooterSizeInStore}
|
||||
componentType="ModalV2"
|
||||
maxHeight={isFullScreen ? undefined : footerMaxHeight}
|
||||
/>
|
||||
)}
|
||||
</BootstrapModal.Footer>
|
||||
);
|
||||
});
|
||||
{isDisabled && (
|
||||
<div
|
||||
id={`${id}-footer-disabled`}
|
||||
className="tj-modal-disabled-overlay"
|
||||
style={{ height: footerHeight || '100%' }}
|
||||
onClick={onClick}
|
||||
onDrop={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
</BootstrapModal.Footer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,49 @@
|
|||
import React from 'react';
|
||||
import { default as BootstrapModal } from 'react-bootstrap/Modal';
|
||||
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
|
||||
import { getCanvasHeight } from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
|
||||
import { HorizontalSlot } from '@/AppBuilder/Widgets/Form/Components/HorizontalSlot';
|
||||
import { MODAL_CANVAS_PADDING } from '@/AppBuilder/AppCanvas/appCanvasConstants';
|
||||
|
||||
export const ModalHeader = React.memo(
|
||||
({ id, isDisabled, customStyles, hideCloseButton, darkMode, width, onHideModal, headerHeight, onClick }) => {
|
||||
({
|
||||
id,
|
||||
isDisabled,
|
||||
customStyles,
|
||||
hideCloseButton,
|
||||
darkMode,
|
||||
width,
|
||||
onHideModal,
|
||||
headerHeight,
|
||||
onClick,
|
||||
isEditing,
|
||||
updateHeaderSizeInStore,
|
||||
activeSlot,
|
||||
headerMaxHeight,
|
||||
isFullScreen,
|
||||
}) => {
|
||||
const canvasHeaderHeight = getCanvasHeight(headerHeight);
|
||||
|
||||
// console.log(headerMaxHeight, 'headerMaxHeight');
|
||||
return (
|
||||
<BootstrapModal.Header style={{ ...customStyles.modalHeader }} data-cy={`modal-header`} onClick={onClick}>
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<SubContainer
|
||||
id={`${id}-header`}
|
||||
canvasHeight={canvasHeaderHeight}
|
||||
canvasWidth={width}
|
||||
allowContainerSelect={false}
|
||||
darkMode={darkMode}
|
||||
styles={{
|
||||
backgroundColor: 'transparent',
|
||||
overflowX: 'hidden',
|
||||
overflowY: isDisabled ? 'hidden' : 'auto',
|
||||
<HorizontalSlot
|
||||
slotName={'header'}
|
||||
slotStyle={{
|
||||
height: `100%`,
|
||||
padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`,
|
||||
maxHeight: isFullScreen ? `${headerMaxHeight}` : `${headerMaxHeight}px`,
|
||||
minHeight: '10px',
|
||||
}}
|
||||
isEditing={isEditing}
|
||||
id={`${id}-header`}
|
||||
height={canvasHeaderHeight}
|
||||
width={width}
|
||||
darkMode={darkMode}
|
||||
isDisabled={isDisabled}
|
||||
isActive={activeSlot === `${id}-header`}
|
||||
onResize={updateHeaderSizeInStore}
|
||||
componentType="ModalV2"
|
||||
maxHeight={isFullScreen ? undefined : headerMaxHeight}
|
||||
/>
|
||||
</div>
|
||||
{isDisabled && (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
|
|||
import { ConfigHandle } from '@/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle';
|
||||
import { ModalHeader } from '@/AppBuilder/Widgets/ModalV2/Components/Header';
|
||||
import { ModalFooter } from '@/AppBuilder/Widgets/ModalV2/Components/Footer';
|
||||
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useActiveSlot } from '@/AppBuilder/_hooks/useActiveSlot';
|
||||
export const ModalWidget = ({ ...restProps }) => {
|
||||
const {
|
||||
customStyles,
|
||||
|
|
@ -24,8 +25,31 @@ export const ModalWidget = ({ ...restProps }) => {
|
|||
headerHeight,
|
||||
footerHeight,
|
||||
onSelectModal,
|
||||
modalHeight,
|
||||
isFullScreen,
|
||||
} = restProps['modalProps'];
|
||||
|
||||
const isEditing = useStore((state) => state.currentMode === 'edit');
|
||||
const setComponentProperty = useStore((state) => state.setComponentProperty);
|
||||
const activeSlot = useActiveSlot(isEditing ? id : null); // Track the active slot for this widget
|
||||
const _modalHeight = isFullScreen ? '100vh' : `${modalHeight}px`;
|
||||
const headerMaxHeight = isFullScreen
|
||||
? `calc(${_modalHeight} - ${footerHeight} - 100px - 10px)`
|
||||
: parseInt(_modalHeight, 10) - parseInt(footerHeight, 10) - 100 - 10;
|
||||
const footerMaxHeight = isFullScreen
|
||||
? `calc(${_modalHeight} - ${headerHeight} - 100px - 10px)`
|
||||
: parseInt(_modalHeight, 10) - parseInt(headerHeight, 10) - 100 - 10;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// When the modal body is clicked capture it and use the callback to set the selected component as modal
|
||||
const handleModalSlotClick = (event) => {
|
||||
const clickedComponentId = event.target.getAttribute('component-id');
|
||||
|
|
@ -53,10 +77,21 @@ export const ModalWidget = ({ ...restProps }) => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const modalContent = document.querySelector(`.tj-modal-content-${id}`);
|
||||
if (restProps.show && modalContent) {
|
||||
modalContent.style.setProperty('height', _modalHeight, 'important');
|
||||
modalContent.style.setProperty('max-height', isFullScreen ? '100%' : modalHeight, 'important');
|
||||
}
|
||||
}, 100);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [modalHeight, restProps.show, isFullScreen]);
|
||||
|
||||
return (
|
||||
<BootstrapModal
|
||||
{...restProps}
|
||||
contentClassName="modal-component tj-modal--container tj-modal-widget-content"
|
||||
contentClassName={`modal-component tj-modal--container tj-modal-widget-content tj-modal-content-${id}`}
|
||||
animation={true}
|
||||
onEscapeKeyDown={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -87,6 +122,11 @@ export const ModalWidget = ({ ...restProps }) => {
|
|||
onHideModal={onHideModal}
|
||||
headerHeight={headerHeight}
|
||||
onClick={handleModalSlotClick}
|
||||
isEditing={isEditing}
|
||||
updateHeaderSizeInStore={updateHeaderSizeInStore}
|
||||
activeSlot={activeSlot}
|
||||
headerMaxHeight={headerMaxHeight}
|
||||
isFullScreen={isFullScreen}
|
||||
/>
|
||||
)}
|
||||
<BootstrapModal.Body style={{ ...customStyles.modalBody }} ref={parentRef} id={id} data-cy={`modal-body`}>
|
||||
|
|
@ -128,6 +168,11 @@ export const ModalWidget = ({ ...restProps }) => {
|
|||
width={modalWidth}
|
||||
footerHeight={footerHeight}
|
||||
onClick={handleModalSlotClick}
|
||||
isEditing={isEditing}
|
||||
updateFooterSizeInStore={updateFooterSizeInStore}
|
||||
activeSlot={activeSlot}
|
||||
footerMaxHeight={footerMaxHeight}
|
||||
isFullScreen={isFullScreen}
|
||||
/>
|
||||
)}
|
||||
</BootstrapModal>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
} from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
|
||||
import { createModalStyles } from '@/AppBuilder/Widgets/ModalV2/helpers/stylesFactory';
|
||||
import { onShowSideEffects, onHideSideEffects } from '@/AppBuilder/Widgets/ModalV2/helpers/sideEffects';
|
||||
|
||||
import '@/AppBuilder/Widgets/ModalV2/style.scss';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
|
|
@ -238,6 +237,7 @@ export const ModalV2 = function Modal({
|
|||
modalBodyHeight: computedCanvasHeight,
|
||||
modalWidth,
|
||||
onSelectModal: setSelectedComponentAsModal,
|
||||
isFullScreen,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,16 +29,12 @@ export function createModalStyles({
|
|||
modalHeader: {
|
||||
backgroundColor:
|
||||
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
|
||||
height: headerHeightPx,
|
||||
overflowY: isDisabledModal ? 'hidden' : 'auto',
|
||||
padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`,
|
||||
},
|
||||
modalFooter: {
|
||||
backgroundColor:
|
||||
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
|
||||
height: footerHeightPx,
|
||||
overflowY: isDisabledModal ? 'hidden' : 'auto',
|
||||
padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`,
|
||||
},
|
||||
buttonStyles: {
|
||||
backgroundColor: triggerButtonBackgroundColor,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
.modal-header {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
|
|
@ -62,5 +63,11 @@
|
|||
border-top: 1px solid var(--border-weak);
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.jet-modalv2-footer .resize-handle {
|
||||
top: -4px;
|
||||
bottom: unset;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -632,7 +632,7 @@ const useAppData = (
|
|||
setSecrets(orgSecrets);
|
||||
}
|
||||
|
||||
const queryData = await dataqueryService.getAll(currentVersionId);
|
||||
const queryData = await dataqueryService.getAll(currentVersionId, mode);
|
||||
const dataQueries = queryData.data_queries;
|
||||
dataQueries.forEach((query) => normalizeQueryTransformationOptions(query));
|
||||
setQueries(dataQueries, moduleId);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const defaultProps = {
|
|||
isReverseVerticalDrag: false,
|
||||
};
|
||||
|
||||
export const useResizable = (options = {}) => {
|
||||
export const useSubContainerResizable = (options = {}) => {
|
||||
const props = { ...defaultProps, ...options };
|
||||
const parentRef = useRef(null);
|
||||
const [isDragging, setIsDragging] = useState(false); // ✅ Track dragging state
|
||||
|
|
@ -115,7 +115,6 @@ export const useResizable = (options = {}) => {
|
|||
// 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 });
|
||||
}
|
||||
};
|
||||
|
|
@ -132,4 +131,4 @@ export const useResizable = (options = {}) => {
|
|||
return { rootRef: parentRef, getRootProps, getHandleProps, getResizeState };
|
||||
};
|
||||
|
||||
export default useResizable;
|
||||
export default useSubContainerResizable;
|
||||
|
|
@ -16,6 +16,11 @@ const initialState = {
|
|||
pageSwitchInProgress: false,
|
||||
isTJDarkMode: localStorage.getItem('darkMode') === 'true',
|
||||
isViewer: false,
|
||||
isComponentLayoutReady: false,
|
||||
appPermission: {
|
||||
selectedUsers: [],
|
||||
selectedUserGroups: [],
|
||||
},
|
||||
appStore: {
|
||||
modules: {
|
||||
canvas: {
|
||||
|
|
@ -276,4 +281,12 @@ export const createAppSlice = (set, get) => ({
|
|||
return get().appStore.modules[moduleId].app.homePageId;
|
||||
},
|
||||
updateIsTJDarkMode: (newMode) => set({ isTJDarkMode: newMode }, false, 'updateIsTJDarkMode'),
|
||||
setSelectedUserGroups: (groups) =>
|
||||
set((state) => {
|
||||
state.appPermission.selectedUserGroups = groups;
|
||||
}),
|
||||
setSelectedUsers: (users) =>
|
||||
set((state) => {
|
||||
state.appPermission.selectedUsers = users;
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@ export const createCodeHinterSlice = (set, get) => ({
|
|||
setSuggestions({ appHints: suggestionList, jsHints: jsHints });
|
||||
},
|
||||
getSuggestions: () => get().suggestions,
|
||||
getServerSideGlobalSuggestions: (isInsideQueryManager) => {
|
||||
const isServerSideGlobalEnabled = !!get()?.license?.featureAccess?.serverSideGlobal;
|
||||
getServerSideGlobalResolveSuggestions: (isInsideQueryManager) => {
|
||||
const isServerSideGlobalResolveEnabled = !!get()?.license?.featureAccess?.serverSideGlobalResolve;
|
||||
const serverHints = [];
|
||||
const hints = get().getSuggestions();
|
||||
if (isInsideQueryManager && isServerSideGlobalEnabled) {
|
||||
if (isInsideQueryManager && isServerSideGlobalResolveEnabled) {
|
||||
serverHints.push({ hint: 'globals.server', type: 'Object' });
|
||||
hints?.appHints?.forEach((appHint) => {
|
||||
if (appHint?.hint?.startsWith('globals.currentUser')) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { dataqueryService } from '@/_services';
|
||||
import { dataqueryService, appPermissionService } from '@/_services';
|
||||
import { getDefaultOptions } from '@/_stores/storeHelper';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import _, { isEmpty, throttle } from 'lodash';
|
||||
|
|
@ -270,7 +270,6 @@ export const createDataQuerySlice = (set, get) => ({
|
|||
)
|
||||
.then((data) => {
|
||||
set((state) => {
|
||||
state.dataQuery.creatingQueryInProcessId = null;
|
||||
state.dataQuery.queries.modules[moduleId] = [
|
||||
{
|
||||
...data,
|
||||
|
|
@ -308,6 +307,42 @@ export const createDataQuerySlice = (set, get) => ({
|
|||
};
|
||||
createAppVersionEventHandlers(newEvent, moduleId);
|
||||
});
|
||||
|
||||
if (queryToClone.permissions && queryToClone.permissions.length !== 0) {
|
||||
const body = {
|
||||
type: queryToClone.permissions[0]?.type,
|
||||
...(queryToClone.permissions[0]?.type === 'GROUP'
|
||||
? {
|
||||
groups: (queryToClone.permissions[0]?.groups || queryToClone.permissions[0]?.users || []).map(
|
||||
(group) => group.permissionGroupsId || group.permission_groups_id
|
||||
),
|
||||
}
|
||||
: { users: queryToClone.permissions[0]?.users.map((user) => user.userId || user.user_id) }),
|
||||
};
|
||||
appPermissionService
|
||||
.createQueryPermission(appId, data.id, body)
|
||||
.then((newQuery) => {
|
||||
const dataQueries = get().dataQuery.queries.modules[moduleId];
|
||||
const updatedDataQueries = dataQueries.map((query) => {
|
||||
if (query.id === data.id) {
|
||||
return {
|
||||
...query,
|
||||
permissions: newQuery.length === 0 || newQuery.length === undefined ? [] : [newQuery[0]],
|
||||
};
|
||||
}
|
||||
return query;
|
||||
});
|
||||
get().dataQuery.setQueries(updatedDataQueries);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Permission could not be created. Please try again!', {
|
||||
className: 'text-nowrap w-auto mw-100',
|
||||
});
|
||||
});
|
||||
}
|
||||
set((state) => {
|
||||
state.dataQuery.creatingQueryInProcessId = null;
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('error', error);
|
||||
|
|
@ -438,7 +473,10 @@ export const createDataQuerySlice = (set, get) => ({
|
|||
const queries = get().dataQuery.queries.modules[moduleId];
|
||||
try {
|
||||
for (const query of queries) {
|
||||
if ((query.options.runOnPageLoad || query.options.run_on_page_load) && isQueryRunnable(query)) {
|
||||
if (
|
||||
(query.options?.runOnPageLoad || query.options?.run_on_page_load) &&
|
||||
(query.restricted || isQueryRunnable(query))
|
||||
) {
|
||||
await get().queryPanel.runQuery(
|
||||
query.id,
|
||||
query.name,
|
||||
|
|
|
|||
|
|
@ -100,10 +100,6 @@ export const createPageMenuSlice = (set, get) => {
|
|||
pageSettingSelected: false,
|
||||
pageSettings: {},
|
||||
showPagePermissionModal: false,
|
||||
permissionPage: null,
|
||||
selectedUserGroups: [],
|
||||
selectedUsers: [],
|
||||
pagePermission: null,
|
||||
|
||||
toggleSearch: (show) =>
|
||||
set((state) => {
|
||||
|
|
@ -432,26 +428,12 @@ export const createPageMenuSlice = (set, get) => {
|
|||
}
|
||||
},
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ const initialState = {
|
|||
loadingDataQueries: false,
|
||||
isPreviewQueryLoading: false,
|
||||
queryPanelSearchTem: '',
|
||||
showQueryPermissionModal: false,
|
||||
targetBtnForMenu: null,
|
||||
showQueryHandlerMenu: false,
|
||||
showDeleteConfirmation: false,
|
||||
renamingQueryId: null,
|
||||
deletingQueryId: null,
|
||||
};
|
||||
|
||||
export const createQueryPanelSlice = (set, get) => ({
|
||||
|
|
@ -223,6 +229,7 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
selectedEnvironment,
|
||||
isPublicAccess,
|
||||
currentVersionId,
|
||||
currentMode,
|
||||
} = get();
|
||||
const {
|
||||
queryPreviewData,
|
||||
|
|
@ -352,14 +359,15 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
|
||||
let queryExecutionPromise = null;
|
||||
if (query.kind === 'runjs') {
|
||||
queryExecutionPromise = executeMultilineJS(query.options.code, query?.id, false, mode, parameters, moduleId);
|
||||
queryExecutionPromise = executeMultilineJS(query.options?.code, query?.id, false, mode, parameters, moduleId);
|
||||
} else if (query.kind === 'runpy') {
|
||||
queryExecutionPromise = executeRunPycode(query.options.code, query, false, mode, queryState, moduleId);
|
||||
queryExecutionPromise = executeRunPycode(query.options?.code, query, false, mode, queryState, moduleId);
|
||||
} else if (query.kind === 'workflows') {
|
||||
queryExecutionPromise = executeWorkflow(
|
||||
moduleId,
|
||||
query.options.workflowId,
|
||||
query.options.blocking,
|
||||
query,
|
||||
query.options?.workflowId,
|
||||
query.options?.blocking,
|
||||
query.options?.params,
|
||||
(currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id //TODO: currentAppEnvironmentId may no longer required. Need to check
|
||||
);
|
||||
|
|
@ -374,7 +382,8 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
options,
|
||||
query?.options,
|
||||
versionId,
|
||||
!isPublicAccess ? (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id : undefined //TODO: currentAppEnvironmentId may no longer required. Need to check
|
||||
!isPublicAccess ? (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id : undefined, //TODO: currentAppEnvironmentId may no longer required. Need to check
|
||||
currentMode
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -447,7 +456,7 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
queryId,
|
||||
{
|
||||
isLoading: false,
|
||||
...(query.kind === 'restapi'
|
||||
...(query.kind === 'restapi' || data.data.type === 'tj-401'
|
||||
? {
|
||||
metadata: data.metadata,
|
||||
request: data.data.requestObject,
|
||||
|
|
@ -725,6 +734,28 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
const {
|
||||
queryPanel: { evaluatePythonCode },
|
||||
} = get();
|
||||
|
||||
if (query.restricted) {
|
||||
return {
|
||||
status: 'failed',
|
||||
message: 'Query could not be completed',
|
||||
description: 'Response code 401 (Unauthorized)',
|
||||
data: {
|
||||
type: 'tj-401',
|
||||
responseObject: {
|
||||
statusCode: 401,
|
||||
responseBody: 'Unauthorized Access',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
response: {
|
||||
statusCode: 401,
|
||||
responseBody: 'Unauthorized Access',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { data: await evaluatePythonCode({ code, query, isPreview, mode, currentState }) };
|
||||
},
|
||||
|
||||
|
|
@ -948,12 +979,33 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
// queries: updatedQueries,
|
||||
// });
|
||||
},
|
||||
executeWorkflow: async (moduleId = 'canvas', workflowId, _blocking = false, params = {}, appEnvId) => {
|
||||
executeWorkflow: async (moduleId = 'canvas', query, workflowId, _blocking = false, params = {}, appEnvId) => {
|
||||
const { getAppId, getAllExposedValues } = get();
|
||||
const appId = getAppId('canvas');
|
||||
const currentState = getAllExposedValues(moduleId);
|
||||
const resolvedParams = get().resolveReferences(moduleId, params, currentState, {}, {});
|
||||
|
||||
if (query.restricted) {
|
||||
return {
|
||||
status: 'failed',
|
||||
message: 'Query could not be completed',
|
||||
description: 'Response code 401 (Unauthorized)',
|
||||
data: {
|
||||
type: 'tj-401',
|
||||
responseObject: {
|
||||
statusCode: 401,
|
||||
responseBody: 'Unauthorized Access',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
response: {
|
||||
statusCode: 401,
|
||||
responseBody: 'Unauthorized Access',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await workflowExecutionsService.execute(workflowId, resolvedParams, appId, appEnvId);
|
||||
return { data: response.result, status: 'ok' };
|
||||
|
|
@ -1004,6 +1056,27 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
|
||||
const queryDetails = dataQuery.queries.modules?.[moduleId].find((q) => q.id === queryId);
|
||||
|
||||
if (queryDetails.restricted) {
|
||||
return {
|
||||
status: 'failed',
|
||||
message: 'Query could not be completed',
|
||||
description: 'Response code 401 (Unauthorized)',
|
||||
data: {
|
||||
type: 'tj-401',
|
||||
responseObject: {
|
||||
statusCode: 401,
|
||||
responseBody: 'Unauthorized Access',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
response: {
|
||||
statusCode: 401,
|
||||
responseBody: 'Unauthorized Access',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const defaultParams =
|
||||
queryDetails?.options?.parameters?.reduce(
|
||||
(paramObj, param) => ({
|
||||
|
|
@ -1154,5 +1227,24 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
};
|
||||
previewQuery(query, false, undefined, moduleId);
|
||||
},
|
||||
toggleQueryPermissionModal: (show) => {
|
||||
set((state) => {
|
||||
state.queryPanel.showQueryPermissionModal = show;
|
||||
});
|
||||
},
|
||||
toggleQueryHandlerMenu: (show, id) => {
|
||||
set((state) => {
|
||||
if (show) state.queryPanel.targetBtnForMenu = id;
|
||||
state.queryPanel.showQueryHandlerMenu = show;
|
||||
});
|
||||
},
|
||||
setRenamingQuery: (queryId) =>
|
||||
set((state) => {
|
||||
state.queryPanel.renamingQueryId = queryId;
|
||||
}),
|
||||
deleteDataQuery: (queryId) =>
|
||||
set((state) => {
|
||||
state.queryPanel.deletingQueryId = queryId;
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,11 +47,6 @@ export const containerConfig = {
|
|||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
},
|
||||
defaultChildren: [
|
||||
{
|
||||
|
|
@ -61,10 +56,10 @@ export const containerConfig = {
|
|||
top: 20,
|
||||
left: 1,
|
||||
height: 40,
|
||||
width: 20,
|
||||
},
|
||||
displayName: 'ContainerText',
|
||||
properties: ['text'],
|
||||
slotName: 'header',
|
||||
accessorKey: 'text',
|
||||
styles: ['fontWeight', 'textSize', 'textColor'],
|
||||
defaultValue: {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const listviewConfig = {
|
|||
top: 15,
|
||||
left: 3,
|
||||
height: 100,
|
||||
width: 7,
|
||||
width: 4,
|
||||
},
|
||||
properties: ['source'],
|
||||
accessorKey: 'imageURL',
|
||||
|
|
@ -24,6 +24,7 @@ export const listviewConfig = {
|
|||
top: 50,
|
||||
left: 11,
|
||||
height: 30,
|
||||
width: 4,
|
||||
},
|
||||
properties: ['text'],
|
||||
accessorKey: 'text',
|
||||
|
|
@ -127,7 +128,7 @@ export const listviewConfig = {
|
|||
},
|
||||
styles: {
|
||||
backgroundColor: {
|
||||
type: 'color',
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
|
|
@ -135,7 +136,7 @@ export const listviewConfig = {
|
|||
},
|
||||
},
|
||||
borderColor: {
|
||||
type: 'color',
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Border color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
|
|
|
|||
|
|
@ -92,18 +92,6 @@ export const modalV2Config = {
|
|||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
footerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Footer height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
|
||||
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
|
||||
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export const tableConfig = {
|
|||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
defaultSize: {
|
||||
width: 35,
|
||||
width: 25,
|
||||
height: 456,
|
||||
},
|
||||
events: {
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ class HomePageComponent extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
importFile = async (importJSON, appName, skipPagePermissionsGroupCheck = false) => {
|
||||
importFile = async (importJSON, appName, skipPermissionsGroupCheck = false) => {
|
||||
this.setState({ isImportingApp: true });
|
||||
// For backward compatibility with legacy app import
|
||||
const organization_id = this.state.currentUser?.organization_id;
|
||||
|
|
@ -397,7 +397,7 @@ class HomePageComponent extends React.Component {
|
|||
const requestBody = {
|
||||
organization_id,
|
||||
...importJSON,
|
||||
skip_page_permissions_group_check: skipPagePermissionsGroupCheck,
|
||||
skip_permissions_group_check: skipPermissionsGroupCheck,
|
||||
};
|
||||
let installedPluginsInfo = [];
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ button:focus:not(:focus-visible) {
|
|||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--slate3);
|
||||
|
|
@ -78,6 +79,10 @@ button:focus:not(:focus-visible) {
|
|||
.color-tomato9 {
|
||||
color: var(--tomato9)
|
||||
}
|
||||
|
||||
.color-disabled {
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const usePortal = ({ children, ...restProps }) => {
|
|||
isCopilotEnabled={isCopilotEnabled}
|
||||
>
|
||||
<div
|
||||
className={`editor-container ${optionalProps.cls ?? ''}`}
|
||||
className={`editor-container codehinter-popup ${optionalProps.cls ?? ''}`}
|
||||
key={key}
|
||||
data-cy={`codehinder-popup-input-field`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ export const appPermissionService = {
|
|||
createPagePermission,
|
||||
updatePagePermission,
|
||||
deletePagePermission,
|
||||
getQueryPermission,
|
||||
createQueryPermission,
|
||||
updateQueryPermission,
|
||||
deleteQueryPermission,
|
||||
};
|
||||
|
||||
function getPagePermission(appId, pageId) {
|
||||
|
|
@ -47,3 +51,41 @@ function deletePagePermission(appId, pageId) {
|
|||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getQueryPermission(appId, queryId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function createQueryPermission(appId, queryId, body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function updateQueryPermission(appId, queryId, body) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function deleteQueryPermission(appId, queryId) {
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ export const dataqueryService = {
|
|||
bulkUpdateQueryOptions,
|
||||
};
|
||||
|
||||
function getAll(appVersionId) {
|
||||
function getAll(appVersionId, mode) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/data-queries/${appVersionId}`, requestOptions).then(handleResponse);
|
||||
return fetch(`${config.apiUrl}/data-queries/${appVersionId}?mode=${mode}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function create(app_id, app_version_id, name, kind, options, data_source_id, plugin_id) {
|
||||
|
|
@ -72,7 +72,7 @@ function del(id, versionId) {
|
|||
return fetch(`${config.apiUrl}/data-queries/${id}/versions/${versionId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function run(queryId, resolvedOptions, options, versionId, environmentId) {
|
||||
function run(queryId, resolvedOptions, options, versionId, environmentId, mode) {
|
||||
const body = {
|
||||
resolvedOptions: resolvedOptions,
|
||||
options: options,
|
||||
|
|
@ -80,7 +80,7 @@ function run(queryId, resolvedOptions, options, versionId, environmentId) {
|
|||
|
||||
let url = `${config.apiUrl}/data-queries/${queryId}/versions/${versionId}/run${
|
||||
environmentId && environmentId !== 'undefined' ? `/${environmentId}` : ''
|
||||
}`;
|
||||
}?mode=${mode}`;
|
||||
|
||||
//For public/released apps
|
||||
if (!environmentId || !versionId) {
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ $border-radius: 4px;
|
|||
}
|
||||
|
||||
.query-row:hover .query-rename-delete-btn {
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.query-row {
|
||||
|
|
@ -189,12 +189,10 @@ $border-radius: 4px;
|
|||
}
|
||||
|
||||
.query-rename-delete-btn {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-left: 2px;
|
||||
width: 66px;
|
||||
width: 22px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4274,7 +4274,6 @@ input[type="text"] {
|
|||
|
||||
.jet-listview::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
|
||||
}
|
||||
|
||||
.jet-listview::-webkit-scrollbar-thumb {
|
||||
|
|
@ -16048,9 +16047,7 @@ textarea.tj-text-input-widget {
|
|||
}
|
||||
|
||||
.jet-listview {
|
||||
&:hover {
|
||||
scrollbar-color: #6a727c4d;
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #6a727c4d !important;
|
||||
}
|
||||
|
|
@ -16070,7 +16067,6 @@ textarea.tj-text-input-widget {
|
|||
|
||||
.jet-listview {
|
||||
&:hover {
|
||||
scrollbar-color: #6a727c4d;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #6a727c4d !important;
|
||||
|
|
@ -18951,6 +18947,14 @@ section.ai-message-prompt-input-wrapper {
|
|||
}
|
||||
}
|
||||
|
||||
.codehinter-popup {
|
||||
.single-line-codehinter-input {
|
||||
.cm-editor {
|
||||
max-height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.missing-groups-modal {
|
||||
.modal-body {
|
||||
padding: 16px;
|
||||
|
|
|
|||
21
frontend/src/_ui/Icon/solidIcons/MoreVertical01.jsx
Normal file
21
frontend/src/_ui/Icon/solidIcons/MoreVertical01.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
const MoreVertical01 = ({ fill = '#11181C', width = '12', height = '13', className = '', viewBox = '0 0 12 13' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={viewBox}
|
||||
className={className}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.00354 2.53516C5.00354 1.98287 5.45126 1.53516 6.00354 1.53516C6.55582 1.53516 7.00354 1.98287 7.00354 2.53516C7.00354 3.08744 6.55582 3.53516 6.00354 3.53516C5.45126 3.53516 5.00354 3.08744 5.00354 2.53516ZM5.00354 6.03516C5.00354 5.48287 5.45126 5.03516 6.00354 5.03516C6.55582 5.03516 7.00354 5.48287 7.00354 6.03516C7.00354 6.58744 6.55582 7.03516 6.00354 7.03516C5.45126 7.03516 5.00354 6.58744 5.00354 6.03516ZM5.00354 9.53516C5.00354 8.98287 5.45126 8.53516 6.00354 8.53516C6.55582 8.53516 7.00354 8.98287 7.00354 9.53516C7.00354 10.0874 6.55582 10.5352 6.00354 10.5352C5.45126 10.5352 5.00354 10.0874 5.00354 9.53516Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default MoreVertical01;
|
||||
|
|
@ -239,6 +239,7 @@ import EnterpriseCrown from './EnterrpiseCrown.jsx';
|
|||
import FileCode from './FileCode.jsx';
|
||||
import Corners from './Corners.jsx';
|
||||
import Moon from './Moon.jsx';
|
||||
import MoreVertical01 from './MoreVertical01.jsx';
|
||||
import RemoveFolder from './RemoveFolder.jsx';
|
||||
|
||||
const Icon = (props) => {
|
||||
|
|
@ -725,6 +726,8 @@ const Icon = (props) => {
|
|||
return <Play01 {...props} />;
|
||||
case 'moon':
|
||||
return <Moon {...props} />;
|
||||
case 'morevertical01':
|
||||
return <MoreVertical01 {...props} />;
|
||||
default:
|
||||
return <Apps {...props} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
|
||||
|
||||
const AppPermissionsModal = () => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default withEditionSpecificComponent(AppPermissionsModal, 'Appbuilder');
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AppPermissionsModal';
|
||||
|
|
@ -4,6 +4,7 @@ import LogoNavDropdown from './LogoNavDropdown';
|
|||
import AppEnvironments from './AppEnvironments';
|
||||
import ThemeSelect from './ThemeSelect';
|
||||
import ColorSwatches from './ColorSwatches';
|
||||
import AppPermissionsModal from './AppPermissionsModal';
|
||||
import ComponentModuleTab from './ComponentModuleTab';
|
||||
|
||||
export {
|
||||
|
|
@ -13,5 +14,6 @@ export {
|
|||
AppEnvironments,
|
||||
ThemeSelect,
|
||||
ColorSwatches,
|
||||
AppPermissionsModal,
|
||||
ComponentModuleTab,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 9e988341aeb7be4348bd7d893a4389507785281c
|
||||
Subproject commit e76477d30eb21df5d188ca204c520df75fddd529
|
||||
51
server/migrations/1747759439358-CreateQueryPermissions.ts
Normal file
51
server/migrations/1747759439358-CreateQueryPermissions.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
|
||||
export class CreateQueryPermissions1747759439358 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'query_permissions',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isGenerated: true,
|
||||
default: 'gen_random_uuid()',
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: 'query_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(
|
||||
'query_permissions',
|
||||
new TableForeignKey({
|
||||
columnNames: ['query_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'data_queries',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('query_permissions');
|
||||
}
|
||||
}
|
||||
76
server/migrations/1747763331564-CreateQueryUsers.ts
Normal file
76
server/migrations/1747763331564-CreateQueryUsers.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
|
||||
export class CreateQueryUsers1747763331564 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'query_users',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isGenerated: true,
|
||||
default: 'gen_random_uuid()',
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: 'query_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(
|
||||
'query_users',
|
||||
new TableForeignKey({
|
||||
columnNames: ['query_permissions_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'query_permissions',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'query_users',
|
||||
new TableForeignKey({
|
||||
columnNames: ['user_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'users',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'query_users',
|
||||
new TableForeignKey({
|
||||
columnNames: ['permission_groups_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'permission_groups',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('query_users');
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ export class ImportResourcesDto {
|
|||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
skip_page_permissions_group_check?: boolean;
|
||||
skip_permissions_group_check?: boolean;
|
||||
}
|
||||
|
||||
export class ImportAppDto {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ import {
|
|||
JoinTable,
|
||||
ManyToMany,
|
||||
AfterLoad,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { App } from './app.entity';
|
||||
import { AppVersion } from './app_version.entity';
|
||||
import { DataSource } from './data_source.entity';
|
||||
import { Plugin } from './plugin.entity';
|
||||
import { QueryPermission } from './query_permissions.entity';
|
||||
|
||||
@Entity({ name: 'data_queries' })
|
||||
export class DataQuery extends BaseEntity {
|
||||
|
|
@ -81,6 +83,9 @@ export class DataQuery extends BaseEntity {
|
|||
|
||||
app: App;
|
||||
|
||||
@OneToMany(() => QueryPermission, (permission) => permission.query)
|
||||
permissions: QueryPermission[];
|
||||
|
||||
@AfterLoad()
|
||||
updatePlugin() {
|
||||
if (this.plugins?.length) this.plugin = this.plugins[0];
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ 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';
|
||||
import { QueryUser } from './query_users.entity';
|
||||
|
||||
@Entity({ name: 'permission_groups' })
|
||||
export class GroupPermissions extends BaseEntity {
|
||||
|
|
@ -72,5 +73,8 @@ export class GroupPermissions extends BaseEntity {
|
|||
@OneToMany(() => PageUser, (pageUser) => pageUser.permissionGroup)
|
||||
pageUsers: PageUser[];
|
||||
|
||||
@OneToMany(() => QueryUser, (queryUser) => queryUser.permissionGroup)
|
||||
queryUsers: QueryUser[];
|
||||
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
|
|||
29
server/src/entities/query_permissions.entity.ts
Normal file
29
server/src/entities/query_permissions.entity.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, OneToMany } from 'typeorm';
|
||||
import { DataQuery } from './data_query.entity';
|
||||
import { QueryUser } from './query_users.entity';
|
||||
import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants';
|
||||
|
||||
@Entity('query_permissions')
|
||||
export class QueryPermission {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'query_id', type: 'uuid', nullable: false })
|
||||
queryId: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: PAGE_PERMISSION_TYPE,
|
||||
})
|
||||
type: PAGE_PERMISSION_TYPE;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => DataQuery, (query) => query.permissions, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'query_id' })
|
||||
query: DataQuery;
|
||||
|
||||
@OneToMany(() => QueryUser, (queryUser) => queryUser.queryPermission)
|
||||
users: QueryUser[];
|
||||
}
|
||||
34
server/src/entities/query_users.entity.ts
Normal file
34
server/src/entities/query_users.entity.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { QueryPermission } from './query_permissions.entity';
|
||||
import { GroupPermissions } from './group_permissions.entity';
|
||||
|
||||
@Entity('query_users')
|
||||
export class QueryUser {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'query_permissions_id', type: 'uuid' })
|
||||
queryPermissionsId: string;
|
||||
|
||||
@Column({ name: 'user_id', type: 'uuid', nullable: true })
|
||||
userId: string | null;
|
||||
|
||||
@Column({ name: 'permission_groups_id', type: 'uuid', nullable: true })
|
||||
permissionGroupsId: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => QueryPermission, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'query_permissions_id' })
|
||||
queryPermission: QueryPermission;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ 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';
|
||||
import { QueryUser } from './query_users.entity';
|
||||
|
||||
@Entity({ name: 'users' })
|
||||
export class User extends BaseEntity {
|
||||
|
|
@ -188,6 +189,9 @@ export class User extends BaseEntity {
|
|||
@OneToMany(() => PageUser, (pageUser) => pageUser.user)
|
||||
pageUsers: PageUser[];
|
||||
|
||||
@OneToMany(() => QueryUser, (queryUser) => queryUser.user)
|
||||
queryUsers: QueryUser[];
|
||||
|
||||
organizationId: string;
|
||||
invitedOrganizationId: string;
|
||||
organizationIds?: Array<string>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export const APP_ERROR_TYPE = {
|
||||
IMPORT_EXPORT_SERVICE: {
|
||||
UNSUPPORTED_VERSION_ERROR: 'Apps built on later versions of ToolJet cannot be imported',
|
||||
PAGE_PERMISSION_GROUP_ERROR: 'Following groups are missing from the workspace',
|
||||
PERMISSION_GROUP_ERROR: 'Following groups are missing from the workspace',
|
||||
PERMISSION_CHECK: 'permission-check',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
FEATURE_KEY.CREATE_PAGE_PERMISSIONS,
|
||||
FEATURE_KEY.UPDATE_PAGE_PERMISSIONS,
|
||||
FEATURE_KEY.DELETE_PAGE_PERMISSIONS,
|
||||
FEATURE_KEY.FETCH_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.CREATE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.UPDATE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.DELETE_QUERY_PERMISSIONS,
|
||||
],
|
||||
App
|
||||
);
|
||||
|
|
@ -56,6 +60,10 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
FEATURE_KEY.CREATE_PAGE_PERMISSIONS,
|
||||
FEATURE_KEY.UPDATE_PAGE_PERMISSIONS,
|
||||
FEATURE_KEY.DELETE_PAGE_PERMISSIONS,
|
||||
FEATURE_KEY.FETCH_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.CREATE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.UPDATE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.DELETE_QUERY_PERMISSIONS,
|
||||
],
|
||||
App
|
||||
);
|
||||
|
|
@ -66,7 +74,15 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
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);
|
||||
can(
|
||||
[
|
||||
FEATURE_KEY.FETCH_USERS,
|
||||
FEATURE_KEY.FETCH_USER_GROUPS,
|
||||
FEATURE_KEY.FETCH_PAGE_PERMISSIONS,
|
||||
FEATURE_KEY.FETCH_QUERY_PERMISSIONS,
|
||||
],
|
||||
App
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,5 +10,9 @@ export const FEATURES: FeaturesConfig = {
|
|||
[FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.FETCH_QUERY_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ export enum PAGE_PERMISSION_TYPE {
|
|||
ALL = 'ALL',
|
||||
}
|
||||
|
||||
export enum PERMISSION_ENTITY_TYPE {
|
||||
PAGE = 'PAGE',
|
||||
QUERY = 'QUERY',
|
||||
COMPONENT = 'COMPONENT',
|
||||
}
|
||||
|
||||
export enum FEATURE_KEY {
|
||||
FETCH_USERS = 'fetch_users',
|
||||
FETCH_USER_GROUPS = 'fetch_user_groups',
|
||||
|
|
@ -11,4 +17,8 @@ export enum FEATURE_KEY {
|
|||
CREATE_PAGE_PERMISSIONS = 'create_page_permissions',
|
||||
UPDATE_PAGE_PERMISSIONS = 'update_page_permissions',
|
||||
DELETE_PAGE_PERMISSIONS = 'delete_page_permissions',
|
||||
FETCH_QUERY_PERMISSIONS = 'fetch_query_permissions',
|
||||
CREATE_QUERY_PERMISSIONS = 'create_query_permissions',
|
||||
UPDATE_QUERY_PERMISSIONS = 'update_query_permissions',
|
||||
DELETE_QUERY_PERMISSIONS = 'delete_query_permissions',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ 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';
|
||||
import { CreatePermissionDto } from './dto';
|
||||
|
||||
@InitModule(MODULES.APP_PERMISSIONS)
|
||||
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
|
||||
|
|
@ -53,7 +53,7 @@ export class AppPermissionsController implements IAppPermissionsController {
|
|||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('pageId') pageId: string,
|
||||
@Body() body: CreatePagePermissionDto,
|
||||
@Body() body: CreatePermissionDto,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
|
|
@ -65,7 +65,7 @@ export class AppPermissionsController implements IAppPermissionsController {
|
|||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('pageId') pageId: string,
|
||||
@Body() body: CreatePagePermissionDto,
|
||||
@Body() body: CreatePermissionDto,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
|
|
@ -81,4 +81,50 @@ export class AppPermissionsController implements IAppPermissionsController {
|
|||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.FETCH_QUERY_PERMISSIONS)
|
||||
@Get(':appId/queries/:queryId')
|
||||
async fetchQueryPermissions(
|
||||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('queryId') queryId: string,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.CREATE_QUERY_PERMISSIONS)
|
||||
@Post(':appId/queries/:queryId')
|
||||
async createQueryPermissions(
|
||||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('queryId') queryId: string,
|
||||
@Body() body: CreatePermissionDto,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.UPDATE_QUERY_PERMISSIONS)
|
||||
@Put(':appId/queries/:queryId')
|
||||
async updateQueryPermissions(
|
||||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('queryId') queryId: string,
|
||||
@Body() body: CreatePermissionDto,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.DELETE_QUERY_PERMISSIONS)
|
||||
@Delete(':appId/queries/:queryId')
|
||||
async deleteQueryPermissions(
|
||||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('queryId') queryId: string,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { IsUUID, IsEnum, IsArray, IsString, IsOptional, ValidateIf } from 'class
|
|||
import { Type } from 'class-transformer';
|
||||
import { PAGE_PERMISSION_TYPE } from '../constants';
|
||||
|
||||
export class CreatePagePermissionDto {
|
||||
export class CreatePermissionDto {
|
||||
@IsUUID(4)
|
||||
@IsOptional()
|
||||
pageId: string;
|
||||
id: string;
|
||||
|
||||
@IsEnum(PAGE_PERMISSION_TYPE)
|
||||
type: PAGE_PERMISSION_TYPE;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { User } from '@entities/user.entity';
|
||||
import { Response } from 'express';
|
||||
import { CreatePagePermissionDto } from '../dto';
|
||||
import { CreatePermissionDto } from '../dto';
|
||||
|
||||
export interface IAppPermissionsController {
|
||||
fetchUsers(user: User, appId: string, response: Response): Promise<any>;
|
||||
|
|
@ -13,7 +13,7 @@ export interface IAppPermissionsController {
|
|||
user: User,
|
||||
appId: string,
|
||||
pageId: string,
|
||||
body: CreatePagePermissionDto,
|
||||
body: CreatePermissionDto,
|
||||
response: Response
|
||||
): Promise<any>;
|
||||
|
||||
|
|
@ -21,9 +21,29 @@ export interface IAppPermissionsController {
|
|||
user: User,
|
||||
appId: string,
|
||||
pageId: string,
|
||||
body: CreatePagePermissionDto,
|
||||
body: CreatePermissionDto,
|
||||
response: Response
|
||||
): Promise<any>;
|
||||
|
||||
deletePagePermissions(user: User, appId: string, pageId: string, response: Response): Promise<any>;
|
||||
|
||||
fetchQueryPermissions(user: User, appId: string, queryId: string, response: Response): Promise<any>;
|
||||
|
||||
createQueryPermissions(
|
||||
user: User,
|
||||
appId: string,
|
||||
queryId: string,
|
||||
body: CreatePermissionDto,
|
||||
response: Response
|
||||
): Promise<any>;
|
||||
|
||||
updateQueryPermissions(
|
||||
user: User,
|
||||
appId: string,
|
||||
queryId: string,
|
||||
body: CreatePermissionDto,
|
||||
response: Response
|
||||
): Promise<any>;
|
||||
|
||||
deleteQueryPermissions(user: User, appId: string, queryId: string, response: Response): Promise<any>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
import { User } from '@entities/user.entity';
|
||||
import { CreatePagePermissionDto } from '../dto';
|
||||
import { CreatePermissionDto } from '../dto';
|
||||
import { PERMISSION_ENTITY_TYPE } from '../constants';
|
||||
|
||||
export interface IAppPermissionsService {
|
||||
fetchUsers(appId: string, user: User): Promise<any>;
|
||||
|
||||
fetchUserGroups(appId: string, user: User): Promise<any>;
|
||||
|
||||
fetchPagePermissions(pageId: string): Promise<any>;
|
||||
fetchAppPermissions(type: PERMISSION_ENTITY_TYPE, id: string): Promise<any>;
|
||||
|
||||
createPagePermissions(pageId: string, body: CreatePagePermissionDto): Promise<any>;
|
||||
createAppPermissions(type: PERMISSION_ENTITY_TYPE, id: string, body: CreatePermissionDto): Promise<any>;
|
||||
|
||||
updatePagePermissions(appId: string, pageId: string, body: CreatePagePermissionDto, user: User): Promise<any>;
|
||||
updateAppPermissions(
|
||||
type: PERMISSION_ENTITY_TYPE,
|
||||
appId: string,
|
||||
id: string,
|
||||
body: CreatePermissionDto,
|
||||
user: User
|
||||
): Promise<any>;
|
||||
|
||||
deletePagePermissions(pageId: string): Promise<any>;
|
||||
deleteAppPermissions(type: PERMISSION_ENTITY_TYPE, id: string): Promise<any>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { User } from '@entities/user.entity';
|
||||
import { GroupPermissions } from '@entities/group_permissions.entity';
|
||||
import { CreatePagePermissionDto } from '../dto';
|
||||
import { CreatePermissionDto } 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>;
|
||||
createPagePermission(pageId: string, body: CreatePermissionDto): Promise<any>;
|
||||
|
||||
updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise<any>;
|
||||
updatePagePermission(pageId: string, body: CreatePermissionDto): Promise<any>;
|
||||
|
||||
createQueryPermission(queryId: string, body: CreatePermissionDto): Promise<any>;
|
||||
|
||||
updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise<any>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@ 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 { QueryUsersRepository } from './repositories/query-users.repository';
|
||||
import { QueryPermissionsRepository } from './repositories/query-permissions.repository';
|
||||
import { PageUser } from '@entities/page_users.entity';
|
||||
import { PagePermission } from '@entities/page_permissions.entity';
|
||||
import { QueryUser } from '@entities/query_users.entity';
|
||||
import { QueryPermission } from '@entities/query_permissions.entity';
|
||||
|
||||
export class AppPermissionsModule {
|
||||
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||
|
|
@ -19,7 +23,9 @@ export class AppPermissionsModule {
|
|||
|
||||
return {
|
||||
module: AppPermissionsModule,
|
||||
imports: [TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission])],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission, QueryUser, QueryPermission]),
|
||||
],
|
||||
controllers: [AppPermissionsController],
|
||||
providers: [
|
||||
AppPermissionsService,
|
||||
|
|
@ -27,6 +33,8 @@ export class AppPermissionsModule {
|
|||
RolesRepository,
|
||||
PageUsersRepository,
|
||||
PagePermissionsRepository,
|
||||
QueryUsersRepository,
|
||||
QueryPermissionsRepository,
|
||||
FeatureAbilityFactory,
|
||||
],
|
||||
exports: [AppPermissionsUtilService, AppPermissionsService],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import { QueryPermission } from '@entities/query_permissions.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, EntityManager, Repository } from 'typeorm';
|
||||
import { QueryUsersRepository } from './query-users.repository';
|
||||
import { dbTransactionWrap } from '@helpers/database.helper';
|
||||
import { PAGE_PERMISSION_TYPE } from '../constants';
|
||||
|
||||
@Injectable()
|
||||
export class QueryPermissionsRepository extends Repository<QueryPermission> {
|
||||
constructor(private dataSource: DataSource, private readonly queryUsersRepository: QueryUsersRepository) {
|
||||
super(QueryPermission, dataSource.createEntityManager());
|
||||
}
|
||||
|
||||
async getQueryPermissions(queryId: string, manager?: EntityManager): Promise<QueryPermission[]> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const queryPermissions = await manager.find(QueryPermission, {
|
||||
where: { queryId },
|
||||
relations: ['users', 'users.user', 'users.permissionGroup'],
|
||||
});
|
||||
|
||||
return queryPermissions.map((permission) => {
|
||||
if (permission.type === PAGE_PERMISSION_TYPE.GROUP) {
|
||||
return {
|
||||
...permission,
|
||||
groups: permission.users,
|
||||
users: undefined,
|
||||
};
|
||||
}
|
||||
return permission;
|
||||
});
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async createQueryPermissions(
|
||||
queryId: string,
|
||||
type: PAGE_PERMISSION_TYPE,
|
||||
manager?: EntityManager
|
||||
): Promise<QueryPermission> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const existingPermission = await manager.findOne(QueryPermission, { where: { queryId } });
|
||||
if (existingPermission) {
|
||||
throw new Error(`Query permission already exists for Query id: ${queryId}`);
|
||||
}
|
||||
|
||||
const queryPermission = manager.create(QueryPermission, {
|
||||
queryId,
|
||||
type,
|
||||
});
|
||||
return manager.save(queryPermission);
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async deleteQueryPermissions(queryId: string, manager?: EntityManager): Promise<void> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
await manager.delete(QueryPermission, { queryId });
|
||||
}, manager || this.manager);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { QueryUser } from '@entities/query_users.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, EntityManager, Repository } from 'typeorm';
|
||||
import { dbTransactionWrap } from '@helpers/database.helper';
|
||||
import { QueryPermission } from '@entities/query_permissions.entity';
|
||||
|
||||
@Injectable()
|
||||
export class QueryUsersRepository extends Repository<QueryUser> {
|
||||
constructor(private dataSource: DataSource) {
|
||||
super(QueryUser, dataSource.createEntityManager());
|
||||
}
|
||||
|
||||
async createQueryUsersWithSingle(
|
||||
queryPermissionsId: string,
|
||||
users: string[],
|
||||
manager?: EntityManager
|
||||
): Promise<QueryUser[]> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const queryUsers = users.map((userId) => {
|
||||
return manager.create(QueryUser, {
|
||||
queryPermissionsId,
|
||||
userId,
|
||||
permissionGroupsId: null,
|
||||
});
|
||||
});
|
||||
return manager.save(queryUsers);
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async createQueryUsersWithGroup(
|
||||
queryPermissionsId: string,
|
||||
groups: string[],
|
||||
manager?: EntityManager
|
||||
): Promise<QueryUser[]> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const queryUsers = groups.map((permissionGroupsId) => {
|
||||
return manager.create(QueryUser, {
|
||||
queryPermissionsId,
|
||||
permissionGroupsId,
|
||||
userId: null,
|
||||
});
|
||||
});
|
||||
return manager.save(queryUsers);
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async checkQueryUserWithGroup(
|
||||
queryPermission: QueryPermission,
|
||||
userId: string,
|
||||
manager?: EntityManager
|
||||
): Promise<boolean> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const result = await manager
|
||||
.createQueryBuilder(QueryUser, 'query_users')
|
||||
.innerJoin('query_users.permissionGroup', 'group')
|
||||
.innerJoin('group.groupUsers', 'groupUser')
|
||||
.where('query_users.queryPermission = :permissionId', {
|
||||
permissionId: queryPermission.id,
|
||||
})
|
||||
.andWhere('groupUser.userId = :userId', { userId })
|
||||
.getOne();
|
||||
|
||||
return !!result;
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async checkQueryUserWithSingle(
|
||||
queryPermission: QueryPermission,
|
||||
userId: string,
|
||||
manager?: EntityManager
|
||||
): Promise<boolean> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const queryUser = await manager.findOne(QueryUser, {
|
||||
where: {
|
||||
queryPermission: { id: queryPermission.id },
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
return !!queryUser;
|
||||
}, manager || this.manager);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,19 +13,19 @@ export class AppPermissionsService implements IAppPermissionsService {
|
|||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async fetchPagePermissions(pageId) {
|
||||
async fetchAppPermissions(type, id) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async createPagePermissions(pageId, body) {
|
||||
async createAppPermissions(type, id, body) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async updatePagePermissions(appId, pageId, body, user) {
|
||||
async updateAppPermissions(type, appId, id, body, user) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async deletePagePermissions(pageId) {
|
||||
async deleteAppPermissions(type, id) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ interface Features {
|
|||
[FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.FETCH_QUERY_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: FeatureConfig;
|
||||
}
|
||||
|
||||
export interface FeaturesConfig {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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';
|
||||
import { CreatePermissionDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class AppPermissionsUtilService implements IUtilService {
|
||||
|
|
@ -16,11 +16,19 @@ export class AppPermissionsUtilService implements IUtilService {
|
|||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async createPagePermission(pageId: string, body: CreatePagePermissionDto): Promise<any> {
|
||||
async createPagePermission(pageId: string, body: CreatePermissionDto): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise<any> {
|
||||
async updatePagePermission(pageId: string, body: CreatePermissionDto): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async createQueryPermission(queryId: string, body: CreatePermissionDto): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants';
|
|||
import { PagePermission } from '@entities/page_permissions.entity';
|
||||
import { PageUser } from '@entities/page_users.entity';
|
||||
import { UsersUtilService } from '@modules/users/util.service';
|
||||
import { QueryPermission } from '@entities/query_permissions.entity';
|
||||
import { QueryUser } from '@entities/query_users.entity';
|
||||
interface AppResourceMappings {
|
||||
defaultDataSourceIdMapping: Record<string, string>;
|
||||
dataQueryMapping: Record<string, string>;
|
||||
|
|
@ -161,6 +163,9 @@ export class AppImportExportService {
|
|||
if (dataSources?.length) {
|
||||
dataQueries = await manager
|
||||
.createQueryBuilder(DataQuery, 'data_queries')
|
||||
.leftJoinAndSelect('data_queries.permissions', 'permission')
|
||||
.leftJoinAndSelect('permission.users', 'queryUser')
|
||||
.leftJoinAndSelect('queryUser.permissionGroup', 'permissionGroup')
|
||||
.where('data_queries.dataSourceId IN(:...dataSourceId)', {
|
||||
dataSourceId: dataSources?.map((v) => v.id),
|
||||
})
|
||||
|
|
@ -213,6 +218,21 @@ export class AppImportExportService {
|
|||
};
|
||||
});
|
||||
|
||||
const queriesWithPermissionGroups = dataQueries.map((query) => {
|
||||
const groupPermission = query.permissions.find((perm) => perm.type === 'GROUP');
|
||||
|
||||
return {
|
||||
...query,
|
||||
permissions: groupPermission
|
||||
? {
|
||||
permissionGroup: groupPermission.users
|
||||
.map((user) => user.permissionGroup?.name)
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const components =
|
||||
pages.length > 0
|
||||
? await manager
|
||||
|
|
@ -236,7 +256,7 @@ export class AppImportExportService {
|
|||
appToExport['components'] = components;
|
||||
appToExport['pages'] = pagesWithPermissionGroups;
|
||||
appToExport['events'] = events;
|
||||
appToExport['dataQueries'] = dataQueries;
|
||||
appToExport['dataQueries'] = queriesWithPermissionGroups;
|
||||
appToExport['dataSources'] = dataSources;
|
||||
appToExport['appVersions'] = appVersions;
|
||||
appToExport['appEnvironments'] = appEnvironments;
|
||||
|
|
@ -1119,7 +1139,15 @@ export class AppImportExportService {
|
|||
});
|
||||
|
||||
await manager.save(newQuery);
|
||||
|
||||
if (importingQuery.permissions) {
|
||||
newQuery.permissions = importingQuery.permissions;
|
||||
}
|
||||
|
||||
appResourceMappings.dataQueryMapping[importingQuery.id] = newQuery.id;
|
||||
|
||||
//create query permissions of query if flag enabled in dto
|
||||
await this.createQueryPermissionsForGroups(newQuery, organizationId, manager);
|
||||
}
|
||||
|
||||
return appResourceMappings;
|
||||
|
|
@ -1355,7 +1383,7 @@ export class AppImportExportService {
|
|||
return pageSettings;
|
||||
}
|
||||
|
||||
async checkIfGroupPermissionsExist(pages, organizationId) {
|
||||
async checkIfGroupPermissionsExist(pages, queries, organizationId) {
|
||||
const allGroupNames = new Set<string>();
|
||||
|
||||
for (const page of pages) {
|
||||
|
|
@ -1365,6 +1393,15 @@ export class AppImportExportService {
|
|||
}
|
||||
}
|
||||
|
||||
for (const query of queries) {
|
||||
const groupNames = query.permissions?.permissionGroup || [];
|
||||
for (const name of groupNames) {
|
||||
if (!allGroupNames.has(name)) {
|
||||
allGroupNames.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allGroupNames.size) return;
|
||||
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
|
|
@ -1425,6 +1462,41 @@ export class AppImportExportService {
|
|||
await manager.save(pageUsers);
|
||||
}
|
||||
|
||||
async createQueryPermissionsForGroups(query, organizationId: string, manager: EntityManager) {
|
||||
const groupNames = query.permissions?.permissionGroup || [];
|
||||
if (!groupNames.length) return;
|
||||
|
||||
const existingGroups = await manager
|
||||
.createQueryBuilder(GroupPermissions, 'gp')
|
||||
.where('gp.name IN (:...names)', { names: groupNames })
|
||||
.andWhere('gp.organizationId = :organizationId', { organizationId })
|
||||
.getMany();
|
||||
|
||||
const groupMap = new Map(existingGroups.map((g) => [g.name, g]));
|
||||
|
||||
// Filter to only existing group names
|
||||
const validGroupNames = groupNames.filter((name) => groupMap.has(name));
|
||||
|
||||
// If no valid group names exist, do not create permissions
|
||||
if (!validGroupNames.length) return;
|
||||
|
||||
const permission = manager.create(QueryPermission, {
|
||||
queryId: query.id,
|
||||
type: PAGE_PERMISSION_TYPE.GROUP,
|
||||
});
|
||||
|
||||
const savedPermission = await manager.save(permission);
|
||||
|
||||
const queryUsers = validGroupNames.map((name) =>
|
||||
manager.create(QueryUser, {
|
||||
queryPermissionsId: savedPermission.id,
|
||||
permissionGroupsId: groupMap.get(name).id,
|
||||
})
|
||||
);
|
||||
|
||||
await manager.save(queryUsers);
|
||||
}
|
||||
|
||||
async createAppVersionsForImportedApp(
|
||||
manager: EntityManager,
|
||||
user: User,
|
||||
|
|
|
|||
|
|
@ -47,11 +47,6 @@ export const containerConfig = {
|
|||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
},
|
||||
defaultChildren: [
|
||||
{
|
||||
|
|
@ -61,10 +56,10 @@ export const containerConfig = {
|
|||
top: 20,
|
||||
left: 1,
|
||||
height: 40,
|
||||
width: 20,
|
||||
},
|
||||
displayName: 'ContainerText',
|
||||
properties: ['text'],
|
||||
slotName: 'header',
|
||||
accessorKey: 'text',
|
||||
styles: ['fontWeight', 'textSize', 'textColor'],
|
||||
defaultValue: {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const listviewConfig = {
|
|||
top: 15,
|
||||
left: 3,
|
||||
height: 100,
|
||||
width: 7,
|
||||
width: 4,
|
||||
},
|
||||
properties: ['source'],
|
||||
accessorKey: 'imageURL',
|
||||
|
|
@ -24,6 +24,7 @@ export const listviewConfig = {
|
|||
top: 50,
|
||||
left: 11,
|
||||
height: 30,
|
||||
width: 4,
|
||||
},
|
||||
properties: ['text'],
|
||||
accessorKey: 'text',
|
||||
|
|
@ -50,7 +51,13 @@ export const listviewConfig = {
|
|||
type: 'code',
|
||||
displayName: 'List data',
|
||||
validation: {
|
||||
schema: { type: 'union', schemas: [{ type: 'array', element: { type: 'object' } },{ type: 'array', element: { type: 'string' } }] },
|
||||
schema: {
|
||||
type: 'union',
|
||||
schemas: [
|
||||
{ type: 'array', element: { type: 'object' } },
|
||||
{ type: 'array', element: { type: 'string' } },
|
||||
],
|
||||
},
|
||||
defaultValue: "[{text: 'Sample text 1'}]",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -92,18 +92,6 @@ export const modalV2Config = {
|
|||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
footerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Footer height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
|
||||
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
|
||||
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller, Get, Param, Body, Post, Patch, Delete, UseGuards, Put, Res } from '@nestjs/common';
|
||||
import { Controller, Get, Param, Body, Post, Patch, Delete, UseGuards, Put, Res, Query } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard';
|
||||
import { DataQueriesService } from './service';
|
||||
import { User, UserEntity } from '@modules/app/decorators/user.decorator';
|
||||
|
|
@ -30,8 +30,8 @@ export class DataQueriesController implements IDataQueriesController {
|
|||
@InitFeature(FEATURE_KEY.GET)
|
||||
@UseGuards(JwtAuthGuard, ValidateAppVersionGuard, ValidateQueryAppGuard, AppFeatureAbilityGuard)
|
||||
@Get(':versionId')
|
||||
index(@Param('versionId') versionId: string) {
|
||||
return this.dataQueriesService.getAll(versionId);
|
||||
index(@User() user: UserEntity, @Param('versionId') versionId: string, @Query('mode') mode?: string) {
|
||||
return this.dataQueriesService.getAll(user, versionId, mode);
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.CREATE)
|
||||
|
|
@ -113,7 +113,8 @@ export class DataQueriesController implements IDataQueriesController {
|
|||
@Body() updateDataQueryDto: UpdateDataQueryDto,
|
||||
@Ability() ability: AppAbility,
|
||||
@DataSource() dataSource: DataSourceEntity,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@Query('mode') mode?: string
|
||||
) {
|
||||
return this.dataQueriesService.runQueryOnBuilder(
|
||||
user,
|
||||
|
|
@ -122,7 +123,8 @@ export class DataQueriesController implements IDataQueriesController {
|
|||
updateDataQueryDto,
|
||||
ability,
|
||||
dataSource,
|
||||
response
|
||||
response,
|
||||
mode
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { App } from '@entities/app.entity';
|
|||
import { UpdateSourceDto } from '../dto';
|
||||
import { Response } from 'express';
|
||||
export interface IDataQueriesController {
|
||||
index(versionId: string): Promise<object>;
|
||||
index(user: UserEntity, versionId: string, mode?: string): Promise<object>;
|
||||
|
||||
create(
|
||||
user: UserEntity,
|
||||
|
|
@ -36,7 +36,8 @@ export interface IDataQueriesController {
|
|||
updateDataQueryDto: UpdateDataQueryDto,
|
||||
ability: AppAbility,
|
||||
dataSource: DataSourceEntity,
|
||||
response: Response
|
||||
response: Response,
|
||||
mode?: string
|
||||
): Promise<object>;
|
||||
|
||||
runQuery(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { CreateDataQueryDto, IUpdatingReferencesOptions, UpdateDataQueryDto } fr
|
|||
import { DataQuery } from '@entities/data_query.entity';
|
||||
|
||||
export interface IDataQueriesService {
|
||||
getAll(versionId: string): Promise<{ data_queries: object[] }>;
|
||||
getAll(user: User, versionId: string, mode?: string): Promise<{ data_queries: object[] }>;
|
||||
|
||||
create(user: User, dataSource: DataSource, dataQueryDto: CreateDataQueryDto): Promise<object>;
|
||||
|
||||
|
|
@ -22,7 +22,8 @@ export interface IDataQueriesService {
|
|||
updateDataQueryDto: UpdateDataQueryDto,
|
||||
ability: object,
|
||||
dataSource: DataSource,
|
||||
response: Response
|
||||
response: Response,
|
||||
mode?: string
|
||||
): Promise<object>;
|
||||
|
||||
runQueryForApp(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export interface IDataQueriesUtilService {
|
|||
dataQuery: any,
|
||||
queryOptions: object,
|
||||
response: Response,
|
||||
environmentId?: string
|
||||
environmentId?: string,
|
||||
mode?: string
|
||||
): Promise<object>;
|
||||
|
||||
fetchServiceAndParsedParams(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { FeatureAbilityFactory as AppFeatureAbilityFactory } from './ability/app
|
|||
import { FeatureAbilityFactory as DataSourceFeatureAbilityFactory } from './ability/data-source';
|
||||
import { AppsRepository } from '@modules/apps/repository';
|
||||
import { OrganizationRepository } from '@modules/organizations/repository';
|
||||
import { LicenseModule } from '@modules/licensing/module';
|
||||
import { AppPermissionsModule } from '@modules/app-permissions/module';
|
||||
|
||||
export class DataQueriesModule {
|
||||
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||
|
|
@ -19,7 +21,12 @@ export class DataQueriesModule {
|
|||
|
||||
return {
|
||||
module: DataQueriesModule,
|
||||
imports: [await AppEnvironmentsModule.register(configs), await DataSourcesModule.register(configs)],
|
||||
imports: [
|
||||
await AppEnvironmentsModule.register(configs),
|
||||
await DataSourcesModule.register(configs),
|
||||
await LicenseModule.forRoot(configs),
|
||||
await AppPermissionsModule.register(configs),
|
||||
],
|
||||
providers: [
|
||||
DataQueryRepository,
|
||||
VersionRepository,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,25 @@ export class DataQueryRepository extends Repository<DataQuery> {
|
|||
});
|
||||
}
|
||||
|
||||
getAllWithPermissions(appVersionId: string): Promise<DataQuery[]> {
|
||||
return dbTransactionWrap((manager: EntityManager) => {
|
||||
return manager
|
||||
.createQueryBuilder(DataQuery, 'data_query')
|
||||
.innerJoinAndSelect('data_query.dataSource', 'data_source')
|
||||
.leftJoinAndSelect('data_query.plugins', 'plugins')
|
||||
.leftJoinAndSelect('plugins.iconFile', 'iconFile')
|
||||
.leftJoinAndSelect('plugins.manifestFile', 'manifestFile')
|
||||
.leftJoinAndSelect('data_query.permissions', 'permission')
|
||||
.leftJoinAndSelect('permission.users', 'queryUser')
|
||||
.leftJoinAndSelect('queryUser.user', 'user')
|
||||
.leftJoinAndSelect('queryUser.permissionGroup', 'group')
|
||||
.where('data_source.appVersionId = :appVersionId', { appVersionId })
|
||||
.where('data_query.app_version_id = :appVersionId', { appVersionId })
|
||||
.orderBy('data_query.updatedAt', 'DESC')
|
||||
.getMany();
|
||||
});
|
||||
}
|
||||
|
||||
async createOne(data: Partial<DataQuery>, manager?: EntityManager): Promise<DataQuery> {
|
||||
return dbTransactionWrap((manager: EntityManager) => {
|
||||
const newDataQuery = manager.create(DataQuery, {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class DataQueriesService implements IDataQueriesService {
|
|||
protected readonly dataSourceRepository: DataSourcesRepository
|
||||
) { }
|
||||
|
||||
async getAll(versionId: string) {
|
||||
async getAll(user: User, versionId: string, mode?: string) {
|
||||
const queries = await this.dataQueryRepository.getAll(versionId);
|
||||
const serializedQueries = [];
|
||||
|
||||
|
|
@ -127,7 +127,8 @@ export class DataQueriesService implements IDataQueriesService {
|
|||
updateDataQueryDto: UpdateDataQueryDto,
|
||||
ability: AppAbility,
|
||||
dataSource: DataSource,
|
||||
response: Response
|
||||
response: Response,
|
||||
mode?: string
|
||||
) {
|
||||
const { options, resolvedOptions } = updateDataQueryDto;
|
||||
|
||||
|
|
@ -138,7 +139,7 @@ export class DataQueriesService implements IDataQueriesService {
|
|||
dataQuery['options'] = options;
|
||||
}
|
||||
|
||||
return this.runAndGetResult(user, dataQuery, resolvedOptions, response, environmentId);
|
||||
return this.runAndGetResult(user, dataQuery, resolvedOptions, response, environmentId, mode);
|
||||
}
|
||||
|
||||
async runQueryForApp(user: User, dataQueryId: string, updateDataQueryDto: UpdateDataQueryDto, response: Response) {
|
||||
|
|
@ -153,17 +154,25 @@ export class DataQueriesService implements IDataQueriesService {
|
|||
return this.runAndGetResult(user, dataQuery, options, response, environmentId);
|
||||
}
|
||||
|
||||
private async runAndGetResult(
|
||||
protected async runAndGetResult(
|
||||
user: User,
|
||||
dataQuery: DataQuery,
|
||||
resolvedOptions: object,
|
||||
response: Response,
|
||||
environmentId?: string
|
||||
environmentId?: string,
|
||||
mode?: string
|
||||
): Promise<object> {
|
||||
let result = {};
|
||||
|
||||
try {
|
||||
result = await this.dataQueryUtilService.runQuery(user, dataQuery, resolvedOptions, response, environmentId);
|
||||
result = await this.dataQueryUtilService.runQuery(
|
||||
user,
|
||||
dataQuery,
|
||||
resolvedOptions,
|
||||
response,
|
||||
environmentId,
|
||||
mode
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.constructor.name === 'QueryError') {
|
||||
result = {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ export class DataQueriesUtilService implements IDataQueriesUtilService {
|
|||
dataQuery: any,
|
||||
queryOptions: object,
|
||||
response: Response,
|
||||
environmentId?: string
|
||||
environmentId?: string,
|
||||
mode?: string
|
||||
): Promise<object> {
|
||||
let result;
|
||||
const queryStatus = new DataQueryStatus();
|
||||
|
|
@ -320,7 +321,7 @@ export class DataQueriesUtilService implements IDataQueriesUtilService {
|
|||
return { service, sourceOptions, parsedQueryOptions };
|
||||
}
|
||||
|
||||
private getCurrentUserToken = (isMultiAuthEnabled: boolean, tokenData: any, userId: string, isAppPublic: boolean) => {
|
||||
protected getCurrentUserToken = (isMultiAuthEnabled: boolean, tokenData: any, userId: string, isAppPublic: boolean) => {
|
||||
if (isMultiAuthEnabled) {
|
||||
if (!tokenData || !Array.isArray(tokenData)) return null;
|
||||
return !isAppPublic
|
||||
|
|
|
|||
|
|
@ -70,15 +70,17 @@ export class ImportExportResourcesService {
|
|||
let tableNameMapping = {};
|
||||
const imports = { app: [], tooljet_database: [], tableNameMapping: {} };
|
||||
const importingVersion = importResourcesDto.tooljet_version;
|
||||
const skipPagePermissionsGroupCheck = importResourcesDto.skip_page_permissions_group_check;
|
||||
const skipPermissionsGroupCheck = importResourcesDto.skip_permissions_group_check;
|
||||
|
||||
if (!isEmpty(importResourcesDto.app) && !skipPagePermissionsGroupCheck) {
|
||||
if (!isEmpty(importResourcesDto.app) && !skipPermissionsGroupCheck) {
|
||||
for (const appImportDto of importResourcesDto.app) {
|
||||
let appParams = appImportDto.definition;
|
||||
if (appParams?.appV2) {
|
||||
appParams = { ...appParams.appV2 };
|
||||
const pages = appParams?.pages;
|
||||
pages?.length && (await this.appImportExportService.checkIfGroupPermissionsExist(pages, user.organizationId));
|
||||
const queries = appParams?.dataQueries;
|
||||
(pages?.length || queries?.length) &&
|
||||
(await this.appImportExportService.checkIfGroupPermissionsExist(pages, queries, user.organizationId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default class LicenseBase {
|
|||
private _isCustomStyling: boolean;
|
||||
private _isWhiteLabelling: boolean;
|
||||
private _isCustomThemes: boolean;
|
||||
private _isServerSideGlobal: boolean;
|
||||
private _isServerSideGlobalResolve: boolean;
|
||||
private _isMultiEnvironment: boolean;
|
||||
private _isMultiPlayerEdit: boolean;
|
||||
private _isComments: boolean;
|
||||
|
|
@ -65,7 +65,7 @@ export default class LicenseBase {
|
|||
this._isCustomStyling = true;
|
||||
this._isWhiteLabelling = true;
|
||||
this._isCustomThemes = true;
|
||||
this._isServerSideGlobal = true;
|
||||
this._isServerSideGlobalResolve = true;
|
||||
this._isLicenseValid = true;
|
||||
this._isMultiEnvironment = true;
|
||||
this._isAi = true;
|
||||
|
|
@ -108,7 +108,7 @@ export default class LicenseBase {
|
|||
this._isWhiteLabelling = this.getFeatureValue('whiteLabelling');
|
||||
this._isAppWhiteLabelling = this.getFeatureValue('appWhiteLabelling');
|
||||
this._isCustomThemes = this.getFeatureValue('customThemes');
|
||||
this._isServerSideGlobal = this.getFeatureValue('serverSideGlobal');
|
||||
this._isServerSideGlobalResolve = this.getFeatureValue('serverSideGlobalResolve');
|
||||
this._isMultiEnvironment = this.getFeatureValue('multiEnvironment');
|
||||
this._isMultiPlayerEdit = this.getFeatureValue('multiPlayerEdit');
|
||||
this._isComments = this.getFeatureValue('comments');
|
||||
|
|
@ -285,11 +285,11 @@ export default class LicenseBase {
|
|||
return this._isCustomThemes;
|
||||
}
|
||||
|
||||
public get serverSideGlobal(): boolean {
|
||||
public get serverSideGlobalResolve(): boolean {
|
||||
if (this.IsBasicPlan) {
|
||||
return !!this.BASIC_PLAN_TERMS.features?.serverSideGlobal;
|
||||
return !!this.BASIC_PLAN_TERMS.features?.serverSideGlobalResolve;
|
||||
}
|
||||
return this._isServerSideGlobal;
|
||||
return this._isServerSideGlobalResolve;
|
||||
}
|
||||
public get externalApis(): boolean {
|
||||
if (this.IsBasicPlan) {
|
||||
|
|
@ -340,7 +340,7 @@ export default class LicenseBase {
|
|||
customStyling: this.customStyling,
|
||||
whiteLabelling: this.whiteLabelling,
|
||||
customThemes: this.customThemes,
|
||||
serverSideGlobal: this.serverSideGlobal,
|
||||
serverSideGlobalResolve: this.serverSideGlobalResolve,
|
||||
multiEnvironment: this.multiEnvironment,
|
||||
multiPlayerEdit: this.multiPlayerEdit,
|
||||
gitSync: this.gitSync,
|
||||
|
|
@ -370,7 +370,7 @@ export default class LicenseBase {
|
|||
samlEnabled: this.saml,
|
||||
customStylingEnabled: this.customStyling,
|
||||
customThemesEnabled: this.customThemes,
|
||||
serverSideGlobalEnabled: this.serverSideGlobal,
|
||||
serverSideGlobalResolveEnabled: this.serverSideGlobalResolve,
|
||||
multiEnvironmentEnabled: this.multiEnvironment,
|
||||
multiPlayerEditEnabled: this.multiPlayerEdit,
|
||||
commentsEnabled: this.comments,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const BASIC_PLAN_TERMS: Partial<Terms> = {
|
|||
gitSync: false,
|
||||
comments: false,
|
||||
customThemes: false,
|
||||
serverSideGlobal: false,
|
||||
serverSideGlobalResolve: false,
|
||||
},
|
||||
domains: [],
|
||||
workflows: {
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export enum LICENSE_FIELD {
|
|||
CUSTOM_STYLE = 'customStylingEnabled',
|
||||
WHITE_LABEL = 'whitelabellingEnabled',
|
||||
CUSTOM_THEMES = 'customThemeEnabled',
|
||||
SERVER_SIDE_GLOBAL = 'serverSideGlobalEnabled',
|
||||
SERVER_SIDE_GLOBAL = 'serverSideGlobalResolveEnabled',
|
||||
AUDIT_LOGS = 'auditLogsEnabled',
|
||||
MAX_DURATION_FOR_AUDIT_LOGS = 'maxDaysForAuditLogs',
|
||||
MULTI_ENVIRONMENT = 'multiEnvironmentEnabled',
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export function getLicenseFieldValue(type: LICENSE_FIELD, licenseInstance: Licen
|
|||
return licenseInstance.customThemes;
|
||||
|
||||
case LICENSE_FIELD.SERVER_SIDE_GLOBAL:
|
||||
return licenseInstance.serverSideGlobal;
|
||||
return licenseInstance.serverSideGlobalResolve;
|
||||
case LICENSE_FIELD.EXTERNAL_API:
|
||||
return licenseInstance.externalApis;
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export interface Terms {
|
|||
gitSync?: boolean;
|
||||
comments?: boolean;
|
||||
customThemes?: boolean;
|
||||
serverSideGlobal?: boolean;
|
||||
serverSideGlobalResolve?: boolean;
|
||||
ai?: boolean;
|
||||
externalApi?: boolean;
|
||||
appWhiteLabelling?: boolean;
|
||||
|
|
|
|||
|
|
@ -107,6 +107,21 @@ export class VersionRepository extends Repository<AppVersion> {
|
|||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async findDataQueriesForVersionWithPermissions(appVersionId: string, manager?: EntityManager): Promise<DataQuery[]> {
|
||||
return dbTransactionWrap((manager: EntityManager) => {
|
||||
return manager
|
||||
.createQueryBuilder(DataQuery, 'query')
|
||||
.where('query.appVersionId = :appVersionId', { appVersionId })
|
||||
.leftJoinAndSelect('query.dataSource', 'dataSource')
|
||||
.leftJoinAndSelect('query.permissions', 'permission')
|
||||
.leftJoinAndSelect('permission.users', 'queryUser')
|
||||
.leftJoinAndSelect('queryUser.user', 'user')
|
||||
.leftJoinAndSelect('queryUser.permissionGroup', 'group')
|
||||
.select(['query', 'dataSource.kind', 'permission', 'queryUser', 'user', 'group'])
|
||||
.getMany();
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async findVersion(id: string, manager?: EntityManager): Promise<AppVersion> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const appVersion = await manager.findOneOrFail(AppVersion, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue