mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
Merge page-permission-fixes, keeping current submodule versions
This commit is contained in:
commit
daa85fe48a
23 changed files with 427 additions and 97 deletions
|
|
@ -164,7 +164,7 @@ export const PageHandlerMenu = ({ darkMode }) => {
|
|||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<div>Page permission</div>
|
||||
{!licenseValid && <SolidIcon name="enterprisesmall" />}
|
||||
{!licenseValid && <SolidIcon name="enterprisecrown" />}
|
||||
</div>
|
||||
</ToolTip>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import IconSelector from './IconSelector';
|
|||
import { withRouter } from '@/_hoc/withRouter';
|
||||
import OverflowTooltip from '@/_components/OverflowTooltip';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
|
||||
export const PageMenuItem = withRouter(
|
||||
memo(({ darkMode, page, navigate }) => {
|
||||
|
|
@ -151,6 +152,36 @@ export const PageMenuItem = withRouter(
|
|||
[popoverRef.current, page]
|
||||
);
|
||||
|
||||
function getTooltip() {
|
||||
const permission = page?.permissions?.length ? page?.permissions[0] : null;
|
||||
if (!permission) return '';
|
||||
const users = permission.users || [];
|
||||
const isSingle = permission.type === 'SINGLE';
|
||||
const isGroup = permission.type === 'GROUP';
|
||||
|
||||
if (users.length === 0) return null;
|
||||
|
||||
if (isSingle) {
|
||||
if (users.length === 1) {
|
||||
const email = users[0].user.email;
|
||||
return `Access restricted to ${email}`;
|
||||
} else {
|
||||
return `Access restricted to ${users.length} users`;
|
||||
}
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
if (users.length === 1) {
|
||||
const groupName = users[0].permissionGroup?.name ?? 'Group';
|
||||
return `Access restricted to ${groupName} group`;
|
||||
} else {
|
||||
return `Access restricted to ${users.length} groups`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
|
|
@ -200,7 +231,13 @@ export const PageMenuItem = withRouter(
|
|||
</span>
|
||||
</div>
|
||||
<div style={{ marginLeft: '8px', marginRight: 'auto' }}>
|
||||
{licenseValid && restricted && <SolidIcon width="16" name="lock" fill="var(--icon-strong)" />}
|
||||
{licenseValid && restricted && (
|
||||
<ToolTip message={getTooltip()}>
|
||||
<div>
|
||||
<SolidIcon width="16" name="lock" fill="var(--icon-strong)" />
|
||||
</div>
|
||||
</ToolTip>
|
||||
)}
|
||||
</div>
|
||||
<div className={cx('right', { 'handler-menu-open': showEditingPopover })}>
|
||||
{!shouldFreeze && (
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ export default function PagePermission({ darkMode }) {
|
|||
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isPermissionsLoading, setPermissionsLoading] = useState(true);
|
||||
const [pageToDelete, setPageToDelete] = useState(null);
|
||||
const [initialSelectedGroups, setInitialSelectedGroups] = useState([]);
|
||||
const [initialSelectedUsers, setInitialSelectedUsers] = useState([]);
|
||||
const [initalPagePermissionType, setInitialPagePermissionType] = useState('all');
|
||||
|
|
@ -42,7 +41,7 @@ export default function PagePermission({ darkMode }) {
|
|||
useEffect(() => {
|
||||
if (!showPagePermissionModal) return;
|
||||
const fetchPagePermission = () => {
|
||||
appPermissionService.getPagePermission(appId, editingPage?.id || pageToDelete).then((data) => {
|
||||
appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => {
|
||||
if (data) {
|
||||
if (data[0] && data[0]?.type === PERMISSION_TYPES.group) {
|
||||
const groups =
|
||||
|
|
@ -55,7 +54,6 @@ export default function PagePermission({ darkMode }) {
|
|||
setInitialPagePermissionType(data[0]?.type?.toLowerCase());
|
||||
setPagePermission(data);
|
||||
toggleUserGroupSelect(true);
|
||||
setPageToDelete(null);
|
||||
setInitialSelectedGroups(groups);
|
||||
data?.length && setSelectedUserGroups(groups);
|
||||
} else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) {
|
||||
|
|
@ -74,7 +72,6 @@ export default function PagePermission({ darkMode }) {
|
|||
setInitialPagePermissionType(data[0]?.type?.toLowerCase());
|
||||
setPagePermission(data);
|
||||
toggleUsersSelect(true);
|
||||
setPageToDelete(null);
|
||||
setInitialSelectedUsers(users);
|
||||
data?.length && setSelectedUsers(users);
|
||||
}
|
||||
|
|
@ -83,7 +80,7 @@ export default function PagePermission({ darkMode }) {
|
|||
});
|
||||
};
|
||||
fetchPagePermission();
|
||||
}, [showPagePermissionModal, pageToDelete]);
|
||||
}, [showPagePermissionModal]);
|
||||
|
||||
const isSelectionUnchanged = useMemo(() => {
|
||||
if (pagePermissionType === 'group') {
|
||||
|
|
@ -237,13 +234,12 @@ export default function PagePermission({ darkMode }) {
|
|||
const deletePagePermission = () => {
|
||||
setIsLoading(true);
|
||||
appPermissionService
|
||||
.deletePagePermission(appId, pageToDelete)
|
||||
.deletePagePermission(appId, editingPage?.id)
|
||||
.then((data) => {
|
||||
toast.success('Permission successfully deleted!', {
|
||||
className: 'text-nowrap w-auto mw-100',
|
||||
});
|
||||
updatePageWithPermissions(pageToDelete, []);
|
||||
setPageToDelete(null);
|
||||
updatePageWithPermissions(editingPage?.id, []);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Permission could not be deleted. Please try again!', {
|
||||
|
|
@ -284,25 +280,18 @@ export default function PagePermission({ darkMode }) {
|
|||
isLoading={isLoading}
|
||||
handleClose={handlePagePermissionModalClose}
|
||||
confirmBtnProps={{
|
||||
title: pagePermission ? 'Update' : pagePermissionType === 'all' ? 'Default permission' : 'Create permission',
|
||||
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"
|
||||
headerAction={() =>
|
||||
pagePermission && (
|
||||
<span
|
||||
onClick={(e) => {
|
||||
setPageToDelete(editingPage?.id);
|
||||
togglePagePermissionModal(false);
|
||||
setShowConfirmDelete(true);
|
||||
}}
|
||||
>
|
||||
<SolidIcon fill="var(--tomato10)" width="20" name="trash" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="page-permission">
|
||||
{isPermissionsLoading ? (
|
||||
|
|
|
|||
|
|
@ -374,4 +374,8 @@
|
|||
.spinner-center {
|
||||
min-height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-base .modal-footer .action-btn-page-permission svg path {
|
||||
fill: var(--indigo1) !important;
|
||||
}
|
||||
|
|
@ -11,8 +11,6 @@ import cx from 'classnames';
|
|||
|
||||
const RenderPage = ({ page, currentPageId, switchPageWrapper, labelStyle, computeStyles, darkMode, homePageId }) => {
|
||||
const isHomePage = page.id === homePageId;
|
||||
console.log({ page, homePageId });
|
||||
console.log({ isHomePage });
|
||||
const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon;
|
||||
const IconElement = Icons?.[iconName] ?? Icons?.['IconFileDescription'];
|
||||
return (page.hidden || page.disabled) && page?.restricted ? null : (
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import {
|
|||
ConsultationBanner,
|
||||
} from '@/modules/dashboard/components';
|
||||
import CreateAppWithPrompt from '@/modules/AiBuilder/components/CreateAppWithPrompt';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
const { iconList, defaultIcon } = configs;
|
||||
|
||||
|
|
@ -116,6 +117,9 @@ class HomePageComponent extends React.Component {
|
|||
shouldAutoImportPlugin: false,
|
||||
dependentPlugins: [],
|
||||
dependentPluginsDetail: {},
|
||||
showMissingGroupsModal: false,
|
||||
missingGroups: [],
|
||||
missingGroupsExpanded: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -356,17 +360,24 @@ class HomePageComponent extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
importFile = async (importJSON, appName) => {
|
||||
importFile = async (importJSON, appName, skipPagePermissionsGroupCheck = false) => {
|
||||
this.setState({ isImportingApp: true });
|
||||
// For backward compatibility with legacy app import
|
||||
const organization_id = this.state.currentUser?.organization_id;
|
||||
const isLegacyImport = isEmpty(importJSON.tooljet_version);
|
||||
if (isLegacyImport) {
|
||||
importJSON = { app: [{ definition: importJSON, appName: appName }], tooljet_version: importJSON.tooljetVersion };
|
||||
importJSON = {
|
||||
app: [{ definition: importJSON, appName: appName }],
|
||||
tooljet_version: importJSON.tooljetVersion,
|
||||
};
|
||||
} else {
|
||||
importJSON.app[0].appName = appName;
|
||||
}
|
||||
const requestBody = { organization_id, ...importJSON };
|
||||
const requestBody = {
|
||||
organization_id,
|
||||
...importJSON,
|
||||
skip_page_permissions_group_check: skipPagePermissionsGroupCheck,
|
||||
};
|
||||
let installedPluginsInfo = [];
|
||||
try {
|
||||
if (this.state.dependentPlugins.length) {
|
||||
|
|
@ -388,6 +399,10 @@ class HomePageComponent extends React.Component {
|
|||
this.props.navigate(`/${getWorkspaceId()}/database`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.error?.type === 'permission-check') {
|
||||
this.setState({ showMissingGroupsModal: true, missingGroups: error?.error?.data });
|
||||
return;
|
||||
}
|
||||
if (installedPluginsInfo.length) {
|
||||
const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id);
|
||||
await pluginsService.uninstallPlugins(pluginsId);
|
||||
|
|
@ -888,6 +903,9 @@ class HomePageComponent extends React.Component {
|
|||
showGroupMigrationBanner,
|
||||
dependentPlugins,
|
||||
dependentPluginsDetail,
|
||||
showMissingGroupsModal,
|
||||
missingGroups,
|
||||
missingGroupsExpanded,
|
||||
} = this.state;
|
||||
const modalConfigs = {
|
||||
create: {
|
||||
|
|
@ -939,6 +957,12 @@ class HomePageComponent extends React.Component {
|
|||
};
|
||||
const isAdmin = authenticationService?.currentSessionValue?.admin;
|
||||
const isBuilder = authenticationService?.currentSessionValue?.is_builder;
|
||||
|
||||
//import app missing groups modal config
|
||||
const threshold = 3;
|
||||
const isLong = missingGroups.length > threshold;
|
||||
const displayedGroups = missingGroupsExpanded ? missingGroups : missingGroups.slice(0, threshold);
|
||||
|
||||
return (
|
||||
<Layout switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode}>
|
||||
<div className="wrapper home-page">
|
||||
|
|
@ -953,6 +977,93 @@ class HomePageComponent extends React.Component {
|
|||
configs={modalConfigs}
|
||||
onCommitChange={this.handleCommitChange}
|
||||
/>
|
||||
<ModalBase
|
||||
showHeader={false}
|
||||
showFooter={false}
|
||||
handleConfirm={() => this.importFile(fileContent, fileName, true)}
|
||||
show={showMissingGroupsModal}
|
||||
isLoading={importingApp}
|
||||
handleClose={() => this.setState({ showMissingGroupsModal: false })}
|
||||
confirmBtnProps={{
|
||||
title: 'Import',
|
||||
tooltipMessage: '',
|
||||
}}
|
||||
className="missing-groups-modal"
|
||||
darkMode={this.props.darkMode}
|
||||
>
|
||||
<div className="missing-groups-modal-body">
|
||||
<div className="flex items-start">
|
||||
<SolidIcon name="warning" width="40px" fill="var(--icon-warning)" />
|
||||
<div>
|
||||
<div className="header">Warning: Missing user groups for permissions</div>
|
||||
<p className="sub-header">
|
||||
Permissions for the following user group(s) won’t be applied since they do not exist in this
|
||||
workspace.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="groups-list">
|
||||
<div
|
||||
className={`border rounded text-sm container ${
|
||||
missingGroupsExpanded ? 'max-h-48 overflow-y-auto' : ''
|
||||
}`}
|
||||
>
|
||||
<div style={{ color: 'var(--text-placeholder)' }} className="tj-text-xsm font-weight-500">
|
||||
User groups
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
{displayedGroups.map((group, idx) => (
|
||||
<span className="tj-text-xsm font-weight-500" key={idx}>
|
||||
{group}
|
||||
{idx < displayedGroups.length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
))}
|
||||
{!missingGroupsExpanded && isLong && '...'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLong && (
|
||||
<button
|
||||
class="toggle-button"
|
||||
onClick={() => this.setState({ missingGroupsExpanded: !missingGroupsExpanded })}
|
||||
>
|
||||
<span className="chevron">
|
||||
<SolidIcon
|
||||
fill="var(--icon-brand)"
|
||||
name={missingGroupsExpanded ? 'cheveronup' : 'cheverondown'}
|
||||
/>
|
||||
</span>
|
||||
<span class="label">{missingGroupsExpanded ? 'See less' : 'See more'}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="info">
|
||||
Restricted pages, queries, or components will become accessible to all users or to existing groups with
|
||||
permissions. To avoid this, create the missing groups before importing, or reconfigure permissions after
|
||||
import.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 d-flex justify-between action-btns">
|
||||
<ButtonSolid
|
||||
className="secondary-action"
|
||||
variant={'tertiary'}
|
||||
onClick={() => this.setState({ showMissingGroupsModal: false, isImportingApp: false })}
|
||||
>
|
||||
Cancel import
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
isLoading={importingApp}
|
||||
variant={'primary'}
|
||||
onClick={() => this.importFile(fileContent, fileName, true)}
|
||||
className="primary-action"
|
||||
>
|
||||
Import with limited permissions
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBase>
|
||||
{showRenameAppModal && (
|
||||
<AppModal
|
||||
show={() => this.setState({ showRenameAppModal: true })}
|
||||
|
|
|
|||
|
|
@ -242,10 +242,16 @@ $btn-dark-color: #FFFFFF;
|
|||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
cursor: pointer !important;
|
||||
pointer-events: unset !important;
|
||||
|
||||
&.disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18891,4 +18891,69 @@ section.ai-message-prompt-input-wrapper {
|
|||
.cm-editor {
|
||||
max-height: 100px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.missing-groups-modal {
|
||||
.modal-body {
|
||||
padding: 16px;
|
||||
|
||||
.header {
|
||||
padding-top: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sub-header {
|
||||
margin-bottom: 0px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.groups-list {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.container {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-bottom: 0px;
|
||||
font-size: 12px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.primary-action, .secondary-action {
|
||||
padding: 8px !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: var(--icon-brand);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.toggle-button .chevron {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-button.expanded .chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx
Normal file
19
frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
const EnterpriseCrown = ({ fill = '#FCA23F', width = '12', className = '', viewBox = '0 0 16 16' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox="0 0 16 16"
|
||||
fill={fill}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.6474 6.49583L13.2967 12.6705C13.2195 13.1143 12.8143 13.4423 12.3705 13.4423H3.62954C3.16644 13.4423 2.78053 13.1143 2.70334 12.6705L1.35264 6.51512C1.25616 5.99414 1.60349 5.51175 2.12447 5.41527C2.4525 5.35738 2.78053 5.47315 3.01207 5.724L5.01883 7.88512L7.14136 3.08049C7.35361 2.59809 7.93248 2.38584 8.39558 2.61739C8.60783 2.71387 8.7622 2.86823 8.85867 3.08049L10.9812 7.86582L12.988 5.7047C13.3353 5.31879 13.9334 5.2802 14.3387 5.62752C14.5895 5.83977 14.7053 6.1871 14.6474 6.49583Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default EnterpriseCrown;
|
||||
|
|
@ -234,6 +234,7 @@ import NewTabSmall from './NewTabSmall.jsx';
|
|||
import Code from './Code.jsx';
|
||||
import WorkflowV3 from './WorkflowV3.jsx';
|
||||
import WorkspaceV3 from './WorkspaceV3.jsx';
|
||||
import EnterpriseCrown from './EnterrpiseCrown.jsx';
|
||||
import Moon from './Moon.jsx';
|
||||
|
||||
const Icon = (props) => {
|
||||
|
|
@ -356,6 +357,8 @@ const Icon = (props) => {
|
|||
return <EnterpriseNew {...props} />;
|
||||
case 'enterprisev3':
|
||||
return <EnterpriseV3 {...props} />;
|
||||
case 'enterprisecrown':
|
||||
return <EnterpriseCrown {...props} />;
|
||||
case 'lockGradient':
|
||||
return <LockGradient {...props} />;
|
||||
case 'datasourceGradient':
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export default function ModalBase({
|
|||
className = '',
|
||||
size = 'sm',
|
||||
headerAction,
|
||||
showHeader = true,
|
||||
showFooter = true,
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -27,15 +29,17 @@ export default function ModalBase({
|
|||
centered={true}
|
||||
contentClassName={`${className} ${darkMode ? 'theme-dark dark-theme modal-base' : 'modal-base'}`}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title className="font-weight-500" data-cy="modal-title">
|
||||
{title}
|
||||
</Modal.Title>
|
||||
<div onClick={handleClose} id="header-actions" className="cursor-pointer" data-cy="modal-close-button">
|
||||
{headerAction && headerAction()}
|
||||
<SolidIcon name="remove" width="20" />
|
||||
</div>
|
||||
</Modal.Header>
|
||||
{showHeader && (
|
||||
<Modal.Header>
|
||||
<Modal.Title className="font-weight-500" data-cy="modal-title">
|
||||
{title}
|
||||
</Modal.Title>
|
||||
<div onClick={handleClose} id="header-actions" className="cursor-pointer" data-cy="modal-close-button">
|
||||
{headerAction && headerAction()}
|
||||
<SolidIcon name="remove" width="20" />
|
||||
</div>
|
||||
</Modal.Header>
|
||||
)}
|
||||
<Modal.Body data-cy="modal-body">
|
||||
{children ? (
|
||||
children
|
||||
|
|
@ -45,28 +49,30 @@ export default function ModalBase({
|
|||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<ButtonSolid disabled={cancelDisabled} variant={'tertiary'} onClick={handleClose} data-cy="cancel-button">
|
||||
Cancel
|
||||
</ButtonSolid>
|
||||
<ToolTip
|
||||
show={confirmBtnProps?.tooltipMessage && confirmBtnProps?.disabled}
|
||||
message={confirmBtnProps?.tooltipMessage}
|
||||
>
|
||||
<div>
|
||||
<ButtonSolid
|
||||
disabled={isLoading || confirmBtnProps?.disabled}
|
||||
isLoading={isLoading}
|
||||
variant={confirmBtnProps?.variant || 'primary'}
|
||||
onClick={handleConfirm}
|
||||
{...confirmBtnProps}
|
||||
data-cy="confim-button"
|
||||
>
|
||||
{confirmBtnProps?.title || 'Continue'}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</ToolTip>
|
||||
</Modal.Footer>
|
||||
{showFooter && (
|
||||
<Modal.Footer>
|
||||
<ButtonSolid disabled={cancelDisabled} variant={'tertiary'} onClick={handleClose} data-cy="cancel-button">
|
||||
Cancel
|
||||
</ButtonSolid>
|
||||
<ToolTip
|
||||
show={confirmBtnProps?.tooltipMessage && confirmBtnProps?.disabled}
|
||||
message={confirmBtnProps?.tooltipMessage}
|
||||
>
|
||||
<div>
|
||||
<ButtonSolid
|
||||
disabled={isLoading || confirmBtnProps?.disabled}
|
||||
isLoading={isLoading}
|
||||
variant={confirmBtnProps?.variant || 'primary'}
|
||||
onClick={handleConfirm}
|
||||
{...confirmBtnProps}
|
||||
data-cy="confim-button"
|
||||
>
|
||||
{confirmBtnProps?.title || 'Continue'}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</ToolTip>
|
||||
</Modal.Footer>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
import { TOOLJET_EDITIONS } from '@modules/app/constants';
|
||||
import { getTooljetEdition } from '@helpers/utils.helper';
|
||||
|
||||
export class CreatePagePermissions1744610362161 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'page_permissions',
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
import { TOOLJET_EDITIONS } from '@modules/app/constants';
|
||||
import { getTooljetEdition } from '@helpers/utils.helper';
|
||||
|
||||
export class CreatePageUsers1744611380594 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'page_users',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested } from 'class-validator';
|
||||
import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested, IsBoolean } from 'class-validator';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { ValidateTooljetDatabaseSchema } from './validators/tooljet-database.validator';
|
||||
import { TjdbSchemaToLatestVersion } from './transformers/resource-transformer';
|
||||
|
|
@ -28,6 +28,10 @@ export class ImportResourcesDto {
|
|||
// and instantiated data
|
||||
@ValidateTooljetDatabaseSchema({ each: true })
|
||||
tooljet_database: ImportTooljetDatabaseDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
skip_page_permissions_group_check?: boolean;
|
||||
}
|
||||
|
||||
export class ImportAppDto {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
|
|
@ -21,7 +20,6 @@ export class GroupPermissions extends BaseEntity {
|
|||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'organization_id', nullable: false })
|
||||
organizationId: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
|
|
@ -17,11 +16,9 @@ export class GroupUsers extends BaseEntity {
|
|||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'user_id', nullable: false })
|
||||
userId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'group_id', nullable: false })
|
||||
groupId: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, Index } from 'typeorm';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { PagePermission } from './page_permissions.entity';
|
||||
import { GroupPermissions } from './group_permissions.entity';
|
||||
|
|
@ -8,15 +8,12 @@ export class PageUser {
|
|||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'page_permissions_id', type: 'uuid' })
|
||||
pagePermissionsId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'user_id', type: 'uuid', nullable: true })
|
||||
userId: string | null;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'permission_groups_id', type: 'uuid', nullable: true })
|
||||
permissionGroupsId: string | null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +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_CHECK: 'permission-check',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,15 +39,7 @@ export class AppsModule {
|
|||
return {
|
||||
module: AppsModule,
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
App,
|
||||
Page,
|
||||
EventHandler,
|
||||
Organization,
|
||||
Component,
|
||||
VersionRepository,
|
||||
RolesRepository,
|
||||
]),
|
||||
TypeOrmModule.forFeature([App, Page, EventHandler, Organization, Component, VersionRepository]),
|
||||
await FolderAppsModule.register(configs),
|
||||
await ThemesModule.register(configs),
|
||||
await FoldersModule.register(configs),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { isEmpty, set } from 'lodash';
|
||||
import { App } from 'src/entities/app.entity';
|
||||
import { AppEnvironment } from 'src/entities/app_environments.entity';
|
||||
|
|
@ -33,6 +33,11 @@ import { DataSourcesUtilService } from '@modules/data-sources/util.service';
|
|||
import { DataSourcesRepository } from '@modules/data-sources/repository';
|
||||
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
|
||||
import { ComponentsService } from './component.service';
|
||||
import { GroupPermissions } from '@entities/group_permissions.entity';
|
||||
import { APP_ERROR_TYPE } from '@helpers/error_type.constant';
|
||||
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';
|
||||
interface AppResourceMappings {
|
||||
defaultDataSourceIdMapping: Record<string, string>;
|
||||
|
|
@ -186,13 +191,31 @@ export class AppImportExportService {
|
|||
}
|
||||
|
||||
const pages = await manager
|
||||
.createQueryBuilder(Page, 'pages')
|
||||
.where('pages.appVersionId IN(:...versionId)', {
|
||||
.createQueryBuilder(Page, 'page')
|
||||
.leftJoinAndSelect('page.permissions', 'permission')
|
||||
.leftJoinAndSelect('permission.users', 'pageUser')
|
||||
.leftJoinAndSelect('pageUser.permissionGroup', 'permissionGroup')
|
||||
.where('page.appVersionId IN(:...versionId)', {
|
||||
versionId: appVersions.map((v) => v.id),
|
||||
})
|
||||
.orderBy('pages.created_at', 'ASC')
|
||||
.orderBy('page.created_at', 'ASC')
|
||||
.getMany();
|
||||
|
||||
const pagesWithPermissionGroups = pages.map((page) => {
|
||||
const groupPermission = page.permissions.find((perm) => perm.type === 'GROUP');
|
||||
|
||||
return {
|
||||
...page,
|
||||
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
|
||||
|
|
@ -214,7 +237,7 @@ export class AppImportExportService {
|
|||
.getMany();
|
||||
|
||||
appToExport['components'] = components;
|
||||
appToExport['pages'] = pages;
|
||||
appToExport['pages'] = pagesWithPermissionGroups;
|
||||
appToExport['events'] = events;
|
||||
appToExport['dataQueries'] = dataQueries;
|
||||
appToExport['dataSources'] = dataSources;
|
||||
|
|
@ -812,6 +835,10 @@ export class AppImportExportService {
|
|||
});
|
||||
}
|
||||
|
||||
if (page.permissions) {
|
||||
pageCreated.permissions = page.permissions;
|
||||
}
|
||||
|
||||
appResourceMappings.pagesMapping[page.id] = pageCreated.id;
|
||||
|
||||
isHomePage = importingAppVersion.homePageId === page.id;
|
||||
|
|
@ -820,6 +847,9 @@ export class AppImportExportService {
|
|||
updateHomepageId = pageCreated.id;
|
||||
}
|
||||
|
||||
//create page permissions of page if flag enabled in dto
|
||||
await this.createPagePermissionsForGroups(pageCreated, user.organizationId, manager);
|
||||
|
||||
const pageComponents = importingComponents.filter((component) => component.pageId === page.id);
|
||||
|
||||
const newComponentIdsMap = {};
|
||||
|
|
@ -936,6 +966,7 @@ export class AppImportExportService {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
// relink page groups
|
||||
const updateArr = [];
|
||||
for (const { pageId, groupId } of pageGroupIdArr) {
|
||||
|
|
@ -1327,6 +1358,76 @@ export class AppImportExportService {
|
|||
return pageSettings;
|
||||
}
|
||||
|
||||
async checkIfGroupPermissionsExist(pages, organizationId) {
|
||||
const allGroupNames = new Set<string>();
|
||||
|
||||
for (const page of pages) {
|
||||
const groupNames = page.permissions?.permissionGroup || [];
|
||||
for (const name of groupNames) {
|
||||
allGroupNames.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allGroupNames.size) return;
|
||||
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const existingGroups = await manager
|
||||
.createQueryBuilder(GroupPermissions, 'gp')
|
||||
.where('gp.name IN (:...names)', { names: Array.from(allGroupNames) })
|
||||
.andWhere('gp.organizationId = :organizationId', { organizationId })
|
||||
.select(['gp.name'])
|
||||
.getMany();
|
||||
|
||||
const existingGroupNames = new Set(existingGroups.map((g) => g.name));
|
||||
|
||||
const missingGroups = Array.from(allGroupNames).filter((name) => !existingGroupNames.has(name));
|
||||
|
||||
if (missingGroups.length > 0) {
|
||||
throw new HttpException(
|
||||
{
|
||||
message: { type: APP_ERROR_TYPE.IMPORT_EXPORT_SERVICE.PERMISSION_CHECK, data: missingGroups },
|
||||
},
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createPagePermissionsForGroups(page, organizationId: string, manager: EntityManager) {
|
||||
const groupNames = page.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(PagePermission, {
|
||||
pageId: page.id,
|
||||
type: PAGE_PERMISSION_TYPE.GROUP,
|
||||
});
|
||||
|
||||
const savedPermission = await manager.save(permission);
|
||||
|
||||
const pageUsers = validGroupNames.map((name) =>
|
||||
manager.create(PageUser, {
|
||||
pagePermissionsId: savedPermission.id,
|
||||
permissionGroupsId: groupMap.get(name).id,
|
||||
})
|
||||
);
|
||||
|
||||
await manager.save(pageUsers);
|
||||
}
|
||||
|
||||
async createAppVersionsForImportedApp(
|
||||
manager: EntityManager,
|
||||
user: User,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,18 @@ export class ImportExportResourcesService {
|
|||
let tableNameMapping = {};
|
||||
const imports = { app: [], tooljet_database: [], tableNameMapping: {} };
|
||||
const importingVersion = importResourcesDto.tooljet_version;
|
||||
const skipPagePermissionsGroupCheck = importResourcesDto.skip_page_permissions_group_check;
|
||||
|
||||
if (!isEmpty(importResourcesDto.app) && !skipPagePermissionsGroupCheck) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(importResourcesDto.tooljet_database)) {
|
||||
const res = await this.tooljetDbImportExportService.bulkImport(importResourcesDto, importingVersion, cloning);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { DataSourcesModule } from '@modules/data-sources/module';
|
|||
import { AppsRepository } from '@modules/apps/repository';
|
||||
import { FeatureAbilityFactory } from './ability';
|
||||
import { getImportPath } from '@modules/app/constants';
|
||||
import { AppPermissionsModule } from '@modules/app-permissions/module';
|
||||
|
||||
export class VersionModule {
|
||||
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||
|
|
@ -33,6 +34,7 @@ export class VersionModule {
|
|||
await DataSourcesModule.register(configs),
|
||||
await AppEnvironmentsModule.register(configs),
|
||||
await ThemesModule.register(configs),
|
||||
await AppPermissionsModule.register(configs),
|
||||
],
|
||||
controllers: [ComponentsController, EventsController, PagesController, VersionController, VersionControllerV2],
|
||||
providers: [
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ export class WorkflowsModule {
|
|||
WorkflowExecutionNode,
|
||||
WorkflowExecutionNode,
|
||||
WorkflowExecutionEdge,
|
||||
RolesRepository,
|
||||
]),
|
||||
ThrottlerModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
|
|
|
|||
Loading…
Reference in a new issue