mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
Merge pull request #12658 from ToolJet/feat/page-permissoins-fe
Feature: Page permissions
This commit is contained in:
commit
fa16fddccf
20 changed files with 850 additions and 215 deletions
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="11" height="14" viewBox="0 0 11 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.33317 3.49967C3.33317 2.30306 4.30322 1.33301 5.49984 1.33301C6.69645 1.33301 7.6665 2.30306 7.6665 3.49967V4.33301H3.33317V3.49967ZM2.33317 4.37981V3.49967C2.33317 1.75077 3.75094 0.333008 5.49984 0.333008C7.24874 0.333008 8.6665 1.75077 8.6665 3.49967V4.37981C9.90027 4.61384 10.8332 5.69781 10.8332 6.99967V10.9997C10.8332 12.4724 9.63926 13.6663 8.1665 13.6663H2.83317C1.36041 13.6663 0.166504 12.4724 0.166504 10.9997V6.99967C0.166504 5.69781 1.09941 4.61384 2.33317 4.37981ZM6.83317 8.99967C6.83317 9.73605 6.23622 10.333 5.49984 10.333C4.76346 10.333 4.1665 9.73605 4.1665 8.99967C4.1665 8.2633 4.76346 7.66634 5.49984 7.66634C6.23622 7.66634 6.83317 8.2633 6.83317 8.99967Z" fill="#ACB2B9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 855 B |
|
|
@ -20,6 +20,8 @@ export const PageHandlerMenu = ({ darkMode }) => {
|
||||||
const toggleDeleteConfirmationModal = useStore((state) => state.toggleDeleteConfirmationModal);
|
const toggleDeleteConfirmationModal = useStore((state) => state.toggleDeleteConfirmationModal);
|
||||||
const clonePage = useStore((state) => state.clonePage);
|
const clonePage = useStore((state) => state.clonePage);
|
||||||
const markAsHomePage = useStore((state) => state.markAsHomePage);
|
const markAsHomePage = useStore((state) => state.markAsHomePage);
|
||||||
|
const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
|
||||||
|
|
||||||
// const popoverTargetRef = null;
|
// const popoverTargetRef = null;
|
||||||
// console.log(
|
// console.log(
|
||||||
// {
|
// {
|
||||||
|
|
@ -164,6 +166,16 @@ export const PageHandlerMenu = ({ darkMode }) => {
|
||||||
}}
|
}}
|
||||||
disabled={isHomePage}
|
disabled={isHomePage}
|
||||||
/>
|
/>
|
||||||
|
<Field
|
||||||
|
id={isDisabled ? 'enable-page' : 'disable-page'}
|
||||||
|
text={isDisabled ? 'Page permission' : 'Page permission'}
|
||||||
|
customClass={'delete-btn'}
|
||||||
|
iconSrc={`assets/images/icons/editor/left-sidebar/authorization.svg`}
|
||||||
|
closeMenu={closeMenu}
|
||||||
|
callback={(id) => {
|
||||||
|
togglePagePermissionModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Field
|
<Field
|
||||||
id="delete-page"
|
id="delete-page"
|
||||||
text="Delete page"
|
text="Delete page"
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { EditModal } from './EditModal';
|
||||||
import { SettingsModal } from './SettingsModal';
|
import { SettingsModal } from './SettingsModal';
|
||||||
import { DeletePageConfirmationModal } from './DeletePageConfirmationModal';
|
import { DeletePageConfirmationModal } from './DeletePageConfirmationModal';
|
||||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||||
|
import PagePermission from './PagePermission';
|
||||||
|
|
||||||
export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
|
export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
|
||||||
const showAddNewPageInput = useStore((state) => state.showAddNewPageInput);
|
const showAddNewPageInput = useStore((state) => state.showAddNewPageInput);
|
||||||
|
|
@ -94,6 +95,7 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<PageHandlerMenu darkMode={darkMode} />
|
<PageHandlerMenu darkMode={darkMode} />
|
||||||
|
<PagePermission darkMode={darkMode} />
|
||||||
<EditModal darkMode={darkMode} />
|
<EditModal darkMode={darkMode} />
|
||||||
<SettingsModal darkMode={darkMode} />
|
<SettingsModal darkMode={darkMode} />
|
||||||
<DeletePageConfirmationModal darkMode={darkMode} />
|
<DeletePageConfirmationModal darkMode={darkMode} />
|
||||||
|
|
|
||||||
377
frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx
Normal file
377
frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx
Normal file
|
|
@ -0,0 +1,377 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
const PERMISSION_TYPES = {
|
||||||
|
single: 'SINGLE',
|
||||||
|
group: 'GROUP',
|
||||||
|
all: 'ALL',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PagePermission({ darkMode }) {
|
||||||
|
const showPagePermissionModal = useStore((state) => state.showPagePermissionModal);
|
||||||
|
const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
|
||||||
|
const editingPage = useStore((state) => state.editingPage);
|
||||||
|
const appId = useStore((state) => state.app.appId);
|
||||||
|
const selectedUserGroups = useStore((state) => state.selectedUserGroups);
|
||||||
|
const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups);
|
||||||
|
const selectedUsers = useStore((state) => state.selectedUsers);
|
||||||
|
const setSelectedUsers = useStore((state) => state.setSelectedUsers);
|
||||||
|
const pagePermission = useStore((state) => state.pagePermission);
|
||||||
|
const setPagePermission = useStore((state) => state.setPagePermission);
|
||||||
|
|
||||||
|
const [pagePermissionType, setPagePermissionType] = useState('all');
|
||||||
|
const [showUserGroupSelect, toggleUserGroupSelect] = useState(false);
|
||||||
|
const [showUsersSelect, toggleUsersSelect] = useState(false);
|
||||||
|
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
console.log({ editingPage, showUserGroupSelect });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editingPage?.id && !showPagePermissionModal) return;
|
||||||
|
const fetchPagePermission = () => {
|
||||||
|
appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => {
|
||||||
|
if (data) {
|
||||||
|
if (data[0]) {
|
||||||
|
setPagePermissionType(data[0]?.type?.toLowerCase());
|
||||||
|
setPagePermission(data);
|
||||||
|
toggleUserGroupSelect(true);
|
||||||
|
data?.length &&
|
||||||
|
setSelectedUserGroups(
|
||||||
|
data[0]?.users?.map((user) => ({
|
||||||
|
label: user?.permissionGroup?.name,
|
||||||
|
value: user?.permissionGroup?.id,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fetchPagePermission();
|
||||||
|
}, [appId, editingPage, setPagePermission, setSelectedUserGroups, showPagePermissionModal]);
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
console.log({ pagePermission });
|
||||||
|
const handlePermissionTypeChange = (value) => {
|
||||||
|
console.log({ 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
console.log({ data });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error('Permission could not be created. Please try again!');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
handlePagePermissionModalClose();
|
||||||
|
toast.success('Permission successfully created!');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
console.log({ data });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error('Permission could not be updated. Please try again!');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
handlePagePermissionModalClose();
|
||||||
|
toast.success('Permission successfully updated!');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePagePermission = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
appPermissionService
|
||||||
|
.deletePagePermission(appId, editingPage?.id)
|
||||||
|
.then((data) => {
|
||||||
|
console.log({ data });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error('Permission could not be deleted. Please try again!');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setShowConfirmDelete(false);
|
||||||
|
handlePagePermissionModalClose();
|
||||||
|
toast.success('Permission successfully deleted!');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPermissionTypeOptions = ({ label, icon }) => {
|
||||||
|
return (
|
||||||
|
<div className="row permission-type-select">
|
||||||
|
<div className="col-auto">
|
||||||
|
<SolidIcon width="20" name={icon} />
|
||||||
|
</div>
|
||||||
|
<div className="col">
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ModalBase
|
||||||
|
title={
|
||||||
|
<div className="my-3">
|
||||||
|
<span className="tj-text-md font-weight-500">Page permission</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
handleConfirm={!pagePermission ? createPagePermission : updatePagePermission}
|
||||||
|
show={showPagePermissionModal}
|
||||||
|
isLoading={isLoading}
|
||||||
|
handleClose={handlePagePermissionModalClose}
|
||||||
|
confirmBtnProps={{
|
||||||
|
title: pagePermission ? 'Update' : pagePermissionType === 'all' ? 'Default permission' : 'Create permission',
|
||||||
|
disabled: pagePermissionType == 'all' ? true : false,
|
||||||
|
tooltipMessage: '',
|
||||||
|
}}
|
||||||
|
darkMode={darkMode}
|
||||||
|
className="page-permissions-modal"
|
||||||
|
headerAction={() =>
|
||||||
|
pagePermission && (
|
||||||
|
<span
|
||||||
|
onClick={(e) => {
|
||||||
|
togglePagePermissionModal(false);
|
||||||
|
setShowConfirmDelete(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SolidIcon fill="var(--tomato10)" width="20" name="trash" />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="page-permission">
|
||||||
|
<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 = () => {
|
||||||
|
console.log('rendering');
|
||||||
|
const appId = useStore((state) => state.app.appId);
|
||||||
|
const editingPage = useStore((state) => state.editingPage);
|
||||||
|
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) => {
|
||||||
|
console.log({ data });
|
||||||
|
if (data?.length) {
|
||||||
|
const groups = [];
|
||||||
|
data.map((group) => {
|
||||||
|
groups.push({ value: group.id, label: group.name });
|
||||||
|
});
|
||||||
|
setUserGroups(groups);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fetchUserGroups();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
console.log({ selectedUserGroups, userGroups });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="form-label mt-3">User groups</label>
|
||||||
|
<Select
|
||||||
|
isMulti={true}
|
||||||
|
options={userGroups}
|
||||||
|
value={selectedUserGroups}
|
||||||
|
width={'100%'}
|
||||||
|
// customOption={renderPermissionTypeOptions}
|
||||||
|
useMenuPortal={false}
|
||||||
|
// menuIsOpen={true}
|
||||||
|
onChange={(groups) => setSelectedUserGroups(groups)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserSelect = () => {
|
||||||
|
const appId = useStore((state) => state.app.appId);
|
||||||
|
const editingPage = useStore((state) => state.editingPage);
|
||||||
|
const selectedUsers = useStore((state) => state.selectedUsers);
|
||||||
|
const setSelectedUsers = useStore((state) => state.setSelectedUsers);
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUsers = () => {
|
||||||
|
appPermissionService.getUsers(appId, 'users').then((data) => {
|
||||||
|
console.log({ 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' : ''}`}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log({ users });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="form-label mt-3">Users</label>
|
||||||
|
<Select
|
||||||
|
isMulti={true}
|
||||||
|
options={users}
|
||||||
|
value={selectedUsers}
|
||||||
|
width={'100%'}
|
||||||
|
// customOption={renderUserSelectOptions}
|
||||||
|
useMenuPortal={false}
|
||||||
|
components={{ Option: CustomOption }}
|
||||||
|
// menuIsOpen={true}
|
||||||
|
onChange={(users) => {
|
||||||
|
console.log({ userstemp: users });
|
||||||
|
setSelectedUsers(users);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -266,4 +266,76 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--slate12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.email {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--slate10); // gray-500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,6 +142,7 @@ const RenderPageGroup = ({
|
||||||
export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkMode, switchPageWrapper }) => {
|
export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkMode, switchPageWrapper }) => {
|
||||||
// Don't render empty folders if displaying only icons
|
// Don't render empty folders if displaying only icons
|
||||||
const tree = buildTree(pages, !!labelStyle?.label?.hidden);
|
const tree = buildTree(pages, !!labelStyle?.label?.hidden);
|
||||||
|
const filteredPages = tree.filter((page) => !page?.isPageGroup || page.children?.length > 0);
|
||||||
|
|
||||||
const currentPageId = useStore((state) => state.currentPageId);
|
const currentPageId = useStore((state) => state.currentPageId);
|
||||||
const currentPage = pages.find((page) => page.id === currentPageId);
|
const currentPage = pages.find((page) => page.id === currentPageId);
|
||||||
|
|
@ -149,14 +150,14 @@ export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkM
|
||||||
return (
|
return (
|
||||||
<div className={cx('page-handler-wrapper viewer', { 'dark-theme': darkMode })}>
|
<div className={cx('page-handler-wrapper viewer', { 'dark-theme': darkMode })}>
|
||||||
{/* <Accordion alwaysOpen defaultActiveKey={tree.map((page) => page.id)}> */}
|
{/* <Accordion alwaysOpen defaultActiveKey={tree.map((page) => page.id)}> */}
|
||||||
{tree.map((page, index) => {
|
{filteredPages.map((page, index) => {
|
||||||
if (page.isPageGroup && page.children.length === 0 && labelStyle?.label?.hidden) {
|
if (page.isPageGroup && page.children.length === 0 && labelStyle?.label?.hidden) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (page.children && page.isPageGroup) {
|
if (page.children && page.isPageGroup) {
|
||||||
// if we are only displaying icons, we don't display the groups instead display separator to separate a page groups
|
// if we are only displaying icons, we don't display the groups instead display separator to separate a page groups
|
||||||
const renderSeparatorTop = index !== 0 && labelStyle?.label?.hidden;
|
const renderSeparatorTop = index !== 0 && labelStyle?.label?.hidden;
|
||||||
const renderSeparatorBottom = !tree[index + 1]?.isPageGroup && labelStyle?.label?.hidden;
|
const renderSeparatorBottom = !filteredPages[index + 1]?.isPageGroup && labelStyle?.label?.hidden;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderSeparatorTop && (
|
{renderSeparatorTop && (
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import { baseTheme, convertAllKeysToSnakeCase } from '../_stores/utils';
|
||||||
import { getPreviewQueryParams } from '@/_helpers/routes';
|
import { getPreviewQueryParams } from '@/_helpers/routes';
|
||||||
import { useLocation, useMatch, useParams } from 'react-router-dom';
|
import { useLocation, useMatch, useParams } from 'react-router-dom';
|
||||||
import useThemeAccess from './useThemeAccess';
|
import useThemeAccess from './useThemeAccess';
|
||||||
|
import { handleError } from '@/_helpers/handleAppAccess';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this is to normalize the query transformation options to match the expected schema. Takes care of corrupted data.
|
* this is to normalize the query transformation options to match the expected schema. Takes care of corrupted data.
|
||||||
|
|
@ -214,224 +216,248 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
|
||||||
}
|
}
|
||||||
|
|
||||||
// const appDataPromise = appService.fetchApp(appId);
|
// const appDataPromise = appService.fetchApp(appId);
|
||||||
appDataPromise.then(async (result) => {
|
appDataPromise
|
||||||
let appData = { ...result };
|
.then(async (result) => {
|
||||||
let editorEnvironment = result.editorEnvironment;
|
let appData = { ...result };
|
||||||
if (isPreviewForVersion) {
|
let editorEnvironment = result.editorEnvironment;
|
||||||
const rawDataQueries = appData?.data_queries;
|
if (isPreviewForVersion) {
|
||||||
const rawEditingVersionDataQueries = appData?.editing_version?.data_queries;
|
const rawDataQueries = appData?.data_queries;
|
||||||
appData = convertAllKeysToSnakeCase(appData);
|
const rawEditingVersionDataQueries = appData?.editing_version?.data_queries;
|
||||||
|
appData = convertAllKeysToSnakeCase(appData);
|
||||||
|
|
||||||
appData.data_queries = rawDataQueries;
|
appData.data_queries = rawDataQueries;
|
||||||
if (appData.editing_version && rawEditingVersionDataQueries) {
|
if (appData.editing_version && rawEditingVersionDataQueries) {
|
||||||
appData.editing_version.data_queries = rawEditingVersionDataQueries;
|
appData.editing_version.data_queries = rawEditingVersionDataQueries;
|
||||||
}
|
}
|
||||||
|
|
||||||
editorEnvironment = {
|
|
||||||
id: environmentId,
|
|
||||||
name: queryParams.env,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let constantsResp;
|
|
||||||
if (mode !== 'edit') {
|
|
||||||
try {
|
|
||||||
const queryParams = { slug: slug };
|
|
||||||
const viewerEnvironment = await appEnvironmentService.getEnvironment(environmentId, queryParams);
|
|
||||||
editorEnvironment = {
|
editorEnvironment = {
|
||||||
id: viewerEnvironment?.environment?.id,
|
id: environmentId,
|
||||||
name: viewerEnvironment?.environment?.name,
|
name: queryParams.env,
|
||||||
};
|
};
|
||||||
constantsResp =
|
|
||||||
isPublicAccess && appData.is_public
|
|
||||||
? await orgEnvironmentConstantService.getConstantsFromPublicApp(slug, viewerEnvironment?.environment?.id)
|
|
||||||
: await orgEnvironmentConstantService.getConstantsFromApp(slug, viewerEnvironment?.environment?.id);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching viewer environment:', error);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'edit') {
|
let constantsResp;
|
||||||
constantsResp = await orgEnvironmentConstantService.getConstantsFromEnvironment(editorEnvironment?.id);
|
if (mode !== 'edit') {
|
||||||
}
|
try {
|
||||||
// get the constants for specific environment
|
const queryParams = { slug: slug };
|
||||||
constantsResp.constants = extractEnvironmentConstantsFromConstantsList(
|
const viewerEnvironment = await appEnvironmentService.getEnvironment(environmentId, queryParams);
|
||||||
constantsResp?.constants,
|
editorEnvironment = {
|
||||||
editorEnvironment?.name
|
id: viewerEnvironment?.environment?.id,
|
||||||
);
|
name: viewerEnvironment?.environment?.name,
|
||||||
|
};
|
||||||
setIsPublicAccess(isPublicAccess && mode !== 'edit' && appData.is_public);
|
constantsResp =
|
||||||
|
isPublicAccess && appData.is_public
|
||||||
fetchAndInjectCustomStyles(isPublicAccess && mode !== 'edit' && appData.is_public);
|
? await orgEnvironmentConstantService.getConstantsFromPublicApp(
|
||||||
|
slug,
|
||||||
const pages = appData.pages.map((page) => {
|
viewerEnvironment?.environment?.id
|
||||||
return page;
|
)
|
||||||
});
|
: await orgEnvironmentConstantService.getConstantsFromApp(slug, viewerEnvironment?.environment?.id);
|
||||||
const conversation = appData.ai_conversation;
|
} catch (error) {
|
||||||
const docsConversation = appData.ai_conversation_learn;
|
console.error('Error fetching viewer environment:', error);
|
||||||
if (setConversation && setDocsConversation) {
|
|
||||||
setConversation(conversation);
|
|
||||||
setDocsConversation(docsConversation);
|
|
||||||
// important to control ai inputs
|
|
||||||
getCreditBalance();
|
|
||||||
}
|
|
||||||
|
|
||||||
let showWalkthrough = true;
|
|
||||||
// if app was created from propmt, and no earlier messages are present in the conversation, send the prompt message
|
|
||||||
|
|
||||||
// handles the getappdataby slug api call. Gets the homePageId from the appData.
|
|
||||||
const homePageId =
|
|
||||||
appData.editing_version?.homePageId || appData.editing_version?.home_page_id || appData.home_page_id;
|
|
||||||
|
|
||||||
setApp({
|
|
||||||
appName: appData.name,
|
|
||||||
appId: appData.id,
|
|
||||||
slug: appData.slug,
|
|
||||||
currentAppEnvironmentId: editorEnvironment.id,
|
|
||||||
isMaintenanceOn:
|
|
||||||
'is_maintenance_on' in result
|
|
||||||
? result.is_maintenance_on
|
|
||||||
: 'isMaintenanceOn' in result
|
|
||||||
? result.isMaintenanceOn
|
|
||||||
: false,
|
|
||||||
organizationId: appData.organizationId || appData.organization_id,
|
|
||||||
homePageId: homePageId,
|
|
||||||
isPublic: appData.is_public,
|
|
||||||
creationMode: appData.creation_mode,
|
|
||||||
});
|
|
||||||
setIsEditorFreezed(appData.should_freeze_editor);
|
|
||||||
const global_settings = mapKeys(
|
|
||||||
appData.editing_version?.global_settings || appData.global_settings,
|
|
||||||
(value, key) => camelCase(key)
|
|
||||||
);
|
|
||||||
if (!global_settings?.theme) {
|
|
||||||
global_settings.theme = baseTheme;
|
|
||||||
}
|
|
||||||
setGlobalSettings(global_settings);
|
|
||||||
setPages(pages, moduleId);
|
|
||||||
setPageSettings(
|
|
||||||
computePageSettings(deepCamelCase(appData?.editing_version?.page_settings ?? appData?.page_settings))
|
|
||||||
);
|
|
||||||
|
|
||||||
// set starting page as homepage initially
|
|
||||||
let startingPage = appData.pages.find((page) => page.id === homePageId);
|
|
||||||
|
|
||||||
if (initialLoadRef.current) {
|
|
||||||
// if initial load, check if the path has a page handle and set that as the starting page
|
|
||||||
const initialLoadPath = location.pathname.split('/').pop();
|
|
||||||
const page = appData.pages.find((page) => page.handle === initialLoadPath && !page.isPageGroup);
|
|
||||||
if (page) {
|
|
||||||
// if page is disabled, and not editing redirect to home page
|
|
||||||
if (mode !== 'edit' && page?.disabled) {
|
|
||||||
const currentUrl = window.location.href;
|
|
||||||
const replacedUrl = currentUrl.replace(initialLoadPath, startingPage.handle);
|
|
||||||
window.history.replaceState(null, null, replacedUrl);
|
|
||||||
} else {
|
|
||||||
startingPage = page;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`);
|
if (mode === 'edit') {
|
||||||
}
|
constantsResp = await orgEnvironmentConstantService.getConstantsFromEnvironment(editorEnvironment?.id);
|
||||||
|
}
|
||||||
// Add page id and handle to the state on initial load
|
// get the constants for specific environment
|
||||||
const currentState = window.history.state || {};
|
constantsResp.constants = extractEnvironmentConstantsFromConstantsList(
|
||||||
const pageInfo = {
|
constantsResp?.constants,
|
||||||
id: startingPage.id,
|
editorEnvironment?.name
|
||||||
handle: startingPage.handle,
|
|
||||||
};
|
|
||||||
const newState = { ...currentState, ...pageInfo };
|
|
||||||
window.history.replaceState(newState, '', window.location.href);
|
|
||||||
|
|
||||||
setCurrentPageHandle(startingPage.handle);
|
|
||||||
updateFeatureAccess();
|
|
||||||
setCurrentPageId(startingPage.id, moduleId);
|
|
||||||
setResolvedPageConstants({
|
|
||||||
id: startingPage?.id,
|
|
||||||
handle: startingPage?.handle,
|
|
||||||
name: startingPage?.name,
|
|
||||||
});
|
|
||||||
setComponentNameIdMapping(moduleId);
|
|
||||||
updateEventsField('events', appData.events);
|
|
||||||
setCurrentVersionId(appData.editing_version?.id || appData.current_version_id);
|
|
||||||
setAppHomePageId(homePageId);
|
|
||||||
|
|
||||||
const queryData =
|
|
||||||
isPublicAccess || (mode !== 'edit' && appData.is_public)
|
|
||||||
? appData
|
|
||||||
: await dataqueryService.getAll(appData.editing_version?.id || appData.current_version_id);
|
|
||||||
const dataQueries = queryData.data_queries || queryData?.editing_version?.data_queries;
|
|
||||||
dataQueries.forEach((query) => normalizeQueryTransformationOptions(query));
|
|
||||||
setQueries(dataQueries);
|
|
||||||
if (dataQueries?.length > 0) {
|
|
||||||
setSelectedQuery(dataQueries[0]?.id);
|
|
||||||
initialiseResolvedQuery(dataQueries.map((query) => query.id));
|
|
||||||
}
|
|
||||||
const constants = constantsResp?.constants;
|
|
||||||
|
|
||||||
if (constants) {
|
|
||||||
const orgConstants = {};
|
|
||||||
const orgSecrets = {};
|
|
||||||
constants.map((constant) => {
|
|
||||||
if (constant.type !== 'Secret') {
|
|
||||||
orgConstants[constant.name] = constant.value;
|
|
||||||
} else {
|
|
||||||
orgSecrets[constant.name] = constant.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setResolvedConstants(orgConstants);
|
|
||||||
setSecrets(orgSecrets);
|
|
||||||
}
|
|
||||||
setQueryMapping(moduleId);
|
|
||||||
|
|
||||||
setResolvedGlobals('environment', editorEnvironment);
|
|
||||||
setResolvedGlobals('mode', { value: mode });
|
|
||||||
setResolvedGlobals('currentUser', {
|
|
||||||
...user,
|
|
||||||
groups: currentSession?.groups,
|
|
||||||
role: currentSession?.role?.name,
|
|
||||||
ssoUserInfo: currentSession?.ssoUserInfo,
|
|
||||||
...(currentSession?.currentUser?.metadata && !isEmpty(currentSession?.currentUser?.metadata)
|
|
||||||
? { metadata: currentSession?.currentUser?.metadata }
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
setResolvedGlobals('urlparams', JSON.parse(JSON.stringify(queryString.parse(location?.search))));
|
|
||||||
initDependencyGraph(moduleId);
|
|
||||||
setCurrentMode(mode); // TODO: set mode based on the slug/appDef
|
|
||||||
if (
|
|
||||||
state.ai &&
|
|
||||||
state?.prompt &&
|
|
||||||
initialLoadRef.current &&
|
|
||||||
(conversation?.aiConversationMessages || []).length === 0
|
|
||||||
) {
|
|
||||||
setSelectedSidebarItem('tooljetai');
|
|
||||||
toggleLeftSidebar('true');
|
|
||||||
sendMessage(state.prompt);
|
|
||||||
setConversationZeroState(true);
|
|
||||||
showWalkthrough = false;
|
|
||||||
}
|
|
||||||
// fetchDataSources(appData.editing_version.id, editorEnvironment.id);
|
|
||||||
if (!isPublicAccess) {
|
|
||||||
const envFromQueryParams = mode === 'view' && new URLSearchParams(location?.search)?.get('env');
|
|
||||||
useStore.getState().init(appData.editing_version?.id || appData.current_version_id, envFromQueryParams);
|
|
||||||
fetchGlobalDataSources(
|
|
||||||
appData.organization_id,
|
|
||||||
appData.editing_version?.id || appData.current_version_id,
|
|
||||||
editorEnvironment.id
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
useStore.getState().updateEditingVersion(appData.editing_version?.id || appData.current_version_id); //check if this is needed
|
|
||||||
updateReleasedVersionId(appData.current_version_id);
|
|
||||||
|
|
||||||
setEditorLoading(false);
|
setIsPublicAccess(isPublicAccess && mode !== 'edit' && appData.is_public);
|
||||||
initialLoadRef.current = false;
|
|
||||||
// only show if app is not created from prompt
|
fetchAndInjectCustomStyles(isPublicAccess && mode !== 'edit' && appData.is_public);
|
||||||
if (showWalkthrough) initEditorWalkThrough();
|
|
||||||
checkAndSetTrueBuildSuggestionsFlag();
|
const pages = appData.pages.map((page) => {
|
||||||
return () => {
|
return page;
|
||||||
document.title = retrieveWhiteLabelText();
|
});
|
||||||
};
|
const conversation = appData.ai_conversation;
|
||||||
});
|
const docsConversation = appData.ai_conversation_learn;
|
||||||
|
if (setConversation && setDocsConversation) {
|
||||||
|
setConversation(conversation);
|
||||||
|
setDocsConversation(docsConversation);
|
||||||
|
// important to control ai inputs
|
||||||
|
getCreditBalance();
|
||||||
|
}
|
||||||
|
|
||||||
|
let showWalkthrough = true;
|
||||||
|
// if app was created from propmt, and no earlier messages are present in the conversation, send the prompt message
|
||||||
|
|
||||||
|
// handles the getappdataby slug api call. Gets the homePageId from the appData.
|
||||||
|
const homePageId =
|
||||||
|
appData.editing_version?.homePageId || appData.editing_version?.home_page_id || appData.home_page_id;
|
||||||
|
|
||||||
|
setApp({
|
||||||
|
appName: appData.name,
|
||||||
|
appId: appData.id,
|
||||||
|
slug: appData.slug,
|
||||||
|
currentAppEnvironmentId: editorEnvironment.id,
|
||||||
|
isMaintenanceOn:
|
||||||
|
'is_maintenance_on' in result
|
||||||
|
? result.is_maintenance_on
|
||||||
|
: 'isMaintenanceOn' in result
|
||||||
|
? result.isMaintenanceOn
|
||||||
|
: false,
|
||||||
|
organizationId: appData.organizationId || appData.organization_id,
|
||||||
|
homePageId: homePageId,
|
||||||
|
isPublic: appData.is_public,
|
||||||
|
creationMode: appData.creation_mode,
|
||||||
|
});
|
||||||
|
setIsEditorFreezed(appData.should_freeze_editor);
|
||||||
|
const global_settings = mapKeys(
|
||||||
|
appData.editing_version?.global_settings || appData.global_settings,
|
||||||
|
(value, key) => camelCase(key)
|
||||||
|
);
|
||||||
|
if (!global_settings?.theme) {
|
||||||
|
global_settings.theme = baseTheme;
|
||||||
|
}
|
||||||
|
setGlobalSettings(global_settings);
|
||||||
|
setPages(pages, moduleId);
|
||||||
|
setPageSettings(
|
||||||
|
computePageSettings(deepCamelCase(appData?.editing_version?.page_settings ?? appData?.page_settings))
|
||||||
|
);
|
||||||
|
|
||||||
|
// set starting page as homepage initially
|
||||||
|
let startingPage = appData.pages.find((page) => page.id === homePageId);
|
||||||
|
|
||||||
|
//no access to homepage, set to the next available page
|
||||||
|
if (!homePageId) {
|
||||||
|
startingPage = appData.pages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialLoadRef.current) {
|
||||||
|
// if initial load, check if the path has a page handle and set that as the starting page
|
||||||
|
const initialLoadPath = location.pathname.split('/')[3];
|
||||||
|
|
||||||
|
const page = appData.pages.find((page) => page.handle === initialLoadPath && !page.isPageGroup);
|
||||||
|
if (page) {
|
||||||
|
// if page is disabled, and not editing redirect to home page
|
||||||
|
if (mode !== 'edit' && page?.disabled) {
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const replacedUrl = currentUrl.replace(initialLoadPath, startingPage.handle);
|
||||||
|
window.history.replaceState(null, null, replacedUrl);
|
||||||
|
} else {
|
||||||
|
startingPage = page;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mode !== 'edit' && initialLoadPath) {
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const replacedUrl = currentUrl.replace(initialLoadPath, startingPage.handle);
|
||||||
|
window.history.replaceState(null, null, replacedUrl);
|
||||||
|
toast.error('Access to this page is restricted. Contact admin to know more.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add page id and handle to the state on initial load
|
||||||
|
const currentState = window.history.state || {};
|
||||||
|
const pageInfo = {
|
||||||
|
id: startingPage.id,
|
||||||
|
handle: startingPage.handle,
|
||||||
|
};
|
||||||
|
const newState = { ...currentState, ...pageInfo };
|
||||||
|
window.history.replaceState(newState, '', window.location.href);
|
||||||
|
|
||||||
|
setCurrentPageHandle(startingPage.handle);
|
||||||
|
updateFeatureAccess();
|
||||||
|
setCurrentPageId(startingPage.id, moduleId);
|
||||||
|
setResolvedPageConstants({
|
||||||
|
id: startingPage?.id,
|
||||||
|
handle: startingPage?.handle,
|
||||||
|
name: startingPage?.name,
|
||||||
|
});
|
||||||
|
setComponentNameIdMapping(moduleId);
|
||||||
|
updateEventsField('events', appData.events);
|
||||||
|
setCurrentVersionId(appData.editing_version?.id || appData.current_version_id);
|
||||||
|
setAppHomePageId(homePageId);
|
||||||
|
|
||||||
|
const queryData =
|
||||||
|
isPublicAccess || (mode !== 'edit' && appData.is_public)
|
||||||
|
? appData
|
||||||
|
: await dataqueryService.getAll(appData.editing_version?.id || appData.current_version_id);
|
||||||
|
const dataQueries = queryData.data_queries || queryData?.editing_version?.data_queries;
|
||||||
|
dataQueries.forEach((query) => normalizeQueryTransformationOptions(query));
|
||||||
|
setQueries(dataQueries);
|
||||||
|
if (dataQueries?.length > 0) {
|
||||||
|
setSelectedQuery(dataQueries[0]?.id);
|
||||||
|
initialiseResolvedQuery(dataQueries.map((query) => query.id));
|
||||||
|
}
|
||||||
|
const constants = constantsResp?.constants;
|
||||||
|
|
||||||
|
if (constants) {
|
||||||
|
const orgConstants = {};
|
||||||
|
const orgSecrets = {};
|
||||||
|
constants.map((constant) => {
|
||||||
|
if (constant.type !== 'Secret') {
|
||||||
|
orgConstants[constant.name] = constant.value;
|
||||||
|
} else {
|
||||||
|
orgSecrets[constant.name] = constant.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setResolvedConstants(orgConstants);
|
||||||
|
setSecrets(orgSecrets);
|
||||||
|
}
|
||||||
|
setQueryMapping(moduleId);
|
||||||
|
|
||||||
|
setResolvedGlobals('environment', editorEnvironment);
|
||||||
|
setResolvedGlobals('mode', { value: mode });
|
||||||
|
setResolvedGlobals('currentUser', {
|
||||||
|
...user,
|
||||||
|
groups: currentSession?.groups,
|
||||||
|
role: currentSession?.role?.name,
|
||||||
|
ssoUserInfo: currentSession?.ssoUserInfo,
|
||||||
|
...(currentSession?.currentUser?.metadata && !isEmpty(currentSession?.currentUser?.metadata)
|
||||||
|
? { metadata: currentSession?.currentUser?.metadata }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
setResolvedGlobals('urlparams', JSON.parse(JSON.stringify(queryString.parse(location?.search))));
|
||||||
|
initDependencyGraph(moduleId);
|
||||||
|
setCurrentMode(mode); // TODO: set mode based on the slug/appDef
|
||||||
|
if (
|
||||||
|
state.ai &&
|
||||||
|
state?.prompt &&
|
||||||
|
initialLoadRef.current &&
|
||||||
|
(conversation?.aiConversationMessages || []).length === 0
|
||||||
|
) {
|
||||||
|
setSelectedSidebarItem('tooljetai');
|
||||||
|
toggleLeftSidebar('true');
|
||||||
|
sendMessage(state.prompt);
|
||||||
|
setConversationZeroState(true);
|
||||||
|
showWalkthrough = false;
|
||||||
|
}
|
||||||
|
// fetchDataSources(appData.editing_version.id, editorEnvironment.id);
|
||||||
|
if (!isPublicAccess) {
|
||||||
|
const envFromQueryParams = mode === 'view' && new URLSearchParams(location?.search)?.get('env');
|
||||||
|
useStore.getState().init(appData.editing_version?.id || appData.current_version_id, envFromQueryParams);
|
||||||
|
fetchGlobalDataSources(
|
||||||
|
appData.organization_id,
|
||||||
|
appData.editing_version?.id || appData.current_version_id,
|
||||||
|
editorEnvironment.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
useStore.getState().updateEditingVersion(appData.editing_version?.id || appData.current_version_id); //check if this is needed
|
||||||
|
updateReleasedVersionId(appData.current_version_id);
|
||||||
|
|
||||||
|
setEditorLoading(false);
|
||||||
|
initialLoadRef.current = false;
|
||||||
|
// only show if app is not created from prompt
|
||||||
|
if (showWalkthrough) initEditorWalkThrough();
|
||||||
|
checkAndSetTrueBuildSuggestionsFlag();
|
||||||
|
return () => {
|
||||||
|
document.title = retrieveWhiteLabelText();
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (isPublicAccess) {
|
||||||
|
if (mode !== 'edit') {
|
||||||
|
handleError('view', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}, [setApp, setEditorLoading, currentSession]);
|
}, [setApp, setEditorLoading, currentSession]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1118,6 +1118,10 @@ export const createEventsSlice = (set, get) => ({
|
||||||
toast('Valid page handle is required', {
|
toast('Valid page handle is required', {
|
||||||
icon: '⚠️',
|
icon: '⚠️',
|
||||||
});
|
});
|
||||||
|
mode === 'view' &&
|
||||||
|
toast.error('Access to this page is restricted. Contact admin to know more.', {
|
||||||
|
icon: '⚠️',
|
||||||
|
});
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,11 @@ export const createPageMenuSlice = (set, get) => {
|
||||||
isPageGroup: false,
|
isPageGroup: false,
|
||||||
pageSettingSelected: false,
|
pageSettingSelected: false,
|
||||||
pageSettings: {},
|
pageSettings: {},
|
||||||
|
showPagePermissionModal: false,
|
||||||
|
permissionPage: null,
|
||||||
|
selectedUserGroups: [],
|
||||||
|
selectedUsers: [],
|
||||||
|
pagePermission: null,
|
||||||
|
|
||||||
toggleSearch: (show) =>
|
toggleSearch: (show) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
|
@ -117,7 +122,6 @@ export const createPageMenuSlice = (set, get) => {
|
||||||
|
|
||||||
closePageEditPopover: () =>
|
closePageEditPopover: () =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.editingPage = null;
|
|
||||||
state.showEditingPopover = false;
|
state.showEditingPopover = false;
|
||||||
state.showEditPageEventsModal = false;
|
state.showEditPageEventsModal = false;
|
||||||
state.showRenamePageHandleModal = false;
|
state.showRenamePageHandleModal = false;
|
||||||
|
|
@ -419,5 +423,26 @@ export const createPageMenuSlice = (set, get) => {
|
||||||
console.error('Error updating page:', error);
|
console.error('Error updating page:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setPagePermission: (pagePermission) =>
|
||||||
|
set((state) => {
|
||||||
|
state.pagePermission = pagePermission;
|
||||||
|
}),
|
||||||
|
|
||||||
|
togglePagePermissionModal: (show) => {
|
||||||
|
set((state) => {
|
||||||
|
state.showPagePermissionModal = show;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelectedUserGroups: (groups) =>
|
||||||
|
set((state) => {
|
||||||
|
state.selectedUserGroups = groups;
|
||||||
|
}),
|
||||||
|
|
||||||
|
setSelectedUsers: (users) =>
|
||||||
|
set((state) => {
|
||||||
|
state.selectedUsers = users;
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export const ON_BOARDING_ROLES = [
|
||||||
export const ERROR_TYPES = {
|
export const ERROR_TYPES = {
|
||||||
URL_UNAVAILABLE: 'url-unavailable',
|
URL_UNAVAILABLE: 'url-unavailable',
|
||||||
RESTRICTED: 'restricted',
|
RESTRICTED: 'restricted',
|
||||||
|
NO_ACCESSIBLE_PAGES: 'no-accessible-pages',
|
||||||
INVALID: 'invalid-link',
|
INVALID: 'invalid-link',
|
||||||
UNKNOWN: 'unknown',
|
UNKNOWN: 'unknown',
|
||||||
WORKSPACE_ARCHIVED: 'Organization is Archived',
|
WORKSPACE_ARCHIVED: 'Organization is Archived',
|
||||||
|
|
@ -53,6 +54,12 @@ export const ERROR_MESSAGES = {
|
||||||
retry: false,
|
retry: false,
|
||||||
queryParams: [],
|
queryParams: [],
|
||||||
},
|
},
|
||||||
|
'no-accessible-pages': {
|
||||||
|
title: 'Restricted access',
|
||||||
|
message: 'You don’t have access to any page in this app. Kindly contact admin to know more.',
|
||||||
|
retry: false,
|
||||||
|
queryParams: [],
|
||||||
|
},
|
||||||
'ws-login-restricted': {
|
'ws-login-restricted': {
|
||||||
title: 'Restricted access',
|
title: 'Restricted access',
|
||||||
message:
|
message:
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ const switchOrganization = (componentType, orgId, redirectPath) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (componentType, error, redirectPath, editPermission, appSlug = null) => {
|
export const handleError = (componentType, error, redirectPath, editPermission, appSlug = null) => {
|
||||||
try {
|
try {
|
||||||
if (error?.data) {
|
if (error?.data) {
|
||||||
const statusCode = error.data?.statusCode;
|
const statusCode = error.data?.statusCode;
|
||||||
|
|
@ -63,6 +63,10 @@ const handleError = (componentType, error, redirectPath, editPermission, appSlug
|
||||||
switchOrganization(componentType, errorObj?.organizationId, redirectPath);
|
switchOrganization(componentType, errorObj?.organizationId, redirectPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (errorObj?.type === ERROR_TYPES.NO_ACCESSIBLE_PAGES) {
|
||||||
|
redirectToErrorPage(ERROR_TYPES.NO_ACCESSIBLE_PAGES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
redirectToErrorPage(ERROR_TYPES.RESTRICTED);
|
redirectToErrorPage(ERROR_TYPES.RESTRICTED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
49
frontend/src/_services/appPermission.service.js
Normal file
49
frontend/src/_services/appPermission.service.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import config from 'config';
|
||||||
|
import { authHeader, handleResponse } from '@/_helpers';
|
||||||
|
|
||||||
|
export const appPermissionService = {
|
||||||
|
getPagePermission,
|
||||||
|
getUsers,
|
||||||
|
createPagePermission,
|
||||||
|
updatePagePermission,
|
||||||
|
deletePagePermission,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPagePermission(appId, pageId) {
|
||||||
|
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||||
|
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUsers(appId, type) {
|
||||||
|
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||||
|
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${type}`, requestOptions).then(handleResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPagePermission(appId, pageId, body) {
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeader(),
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePagePermission(appId, pageId, body) {
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: authHeader(),
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deletePagePermission(appId, pageId) {
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: authHeader(),
|
||||||
|
credentials: 'include',
|
||||||
|
};
|
||||||
|
return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse);
|
||||||
|
}
|
||||||
|
|
@ -33,3 +33,4 @@ export * from './workflow_schedules.service';
|
||||||
export * from './session.service';
|
export * from './session.service';
|
||||||
export * from './login_configs.service';
|
export * from './login_configs.service';
|
||||||
export * from './ai.service';
|
export * from './ai.service';
|
||||||
|
export * from './appPermission.service';
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export default function ModalBase({
|
||||||
cancelDisabled,
|
cancelDisabled,
|
||||||
className = '',
|
className = '',
|
||||||
size = 'sm',
|
size = 'sm',
|
||||||
|
headerAction,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -30,7 +31,8 @@ export default function ModalBase({
|
||||||
<Modal.Title className="font-weight-500" data-cy="modal-title">
|
<Modal.Title className="font-weight-500" data-cy="modal-title">
|
||||||
{title}
|
{title}
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
<div onClick={handleClose} className="cursor-pointer" data-cy="modal-close-button">
|
<div onClick={handleClose} id="header-actions" className="cursor-pointer" data-cy="modal-close-button">
|
||||||
|
{headerAction && headerAction()}
|
||||||
<SolidIcon name="remove" width="20" />
|
<SolidIcon name="remove" width="20" />
|
||||||
</div>
|
</div>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 683647f83d3efeeadbe69c40b8e8dd5ba4e8ea06
|
Subproject commit 0f5a0bab924ee8d2b8f9a8a6071e6a8e2cb4e215
|
||||||
|
|
@ -29,7 +29,7 @@ export class AppPermissionsModule {
|
||||||
PagePermissionsRepository,
|
PagePermissionsRepository,
|
||||||
FeatureAbilityFactory,
|
FeatureAbilityFactory,
|
||||||
],
|
],
|
||||||
exports: [AppPermissionsUtilService],
|
exports: [AppPermissionsUtilService, AppPermissionsService],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export class PagePermissionsRepository extends Repository<PagePermission> {
|
||||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||||
return manager.find(PagePermission, {
|
return manager.find(PagePermission, {
|
||||||
where: { pageId },
|
where: { pageId },
|
||||||
relations: ['users', 'users.user'],
|
relations: ['users', 'users.user', 'users.permissionGroup'],
|
||||||
});
|
});
|
||||||
}, manager || this.manager);
|
}, manager || this.manager);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { PageUser } from '@entities/page_users.entity';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, EntityManager, Repository } from 'typeorm';
|
import { DataSource, EntityManager, Repository } from 'typeorm';
|
||||||
import { dbTransactionWrap } from '@helpers/database.helper';
|
import { dbTransactionWrap } from '@helpers/database.helper';
|
||||||
|
import { PagePermission } from '@entities/page_permissions.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PageUsersRepository extends Repository<PageUser> {
|
export class PageUsersRepository extends Repository<PageUser> {
|
||||||
|
|
@ -42,4 +43,49 @@ export class PageUsersRepository extends Repository<PageUser> {
|
||||||
return manager.save(pageUsers);
|
return manager.save(pageUsers);
|
||||||
}, manager || this.manager);
|
}, manager || this.manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkIfUserExistsInPermissionGroup(
|
||||||
|
pagePermission: PagePermission,
|
||||||
|
userId: string,
|
||||||
|
manager?: EntityManager
|
||||||
|
): Promise<PageUser> {
|
||||||
|
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||||
|
const result = await manager
|
||||||
|
.createQueryBuilder(PageUser, 'page_users')
|
||||||
|
.innerJoin('page_users.permissionGroup', 'group')
|
||||||
|
.innerJoin('group.groupUsers', 'groupUser')
|
||||||
|
.where('page_users.pagePermission = :permissionId', {
|
||||||
|
permissionId: pagePermission.id,
|
||||||
|
})
|
||||||
|
.andWhere('groupUser.userId = :userId', { userId })
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pagePermission;
|
||||||
|
}, manager || this.manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIfUserExistsInSingleConfig(
|
||||||
|
pagePermission: PagePermission,
|
||||||
|
userId: string,
|
||||||
|
manager?: EntityManager
|
||||||
|
): Promise<PageUser> {
|
||||||
|
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||||
|
const pageUser = await manager.findOne(PageUser, {
|
||||||
|
where: {
|
||||||
|
pagePermission: { id: pagePermission.id },
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pageUser) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pagePermission;
|
||||||
|
}, manager || this.manager);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { FeatureAbilityFactory } from './ability';
|
||||||
import { DataSourcesModule } from '@modules/data-sources/module';
|
import { DataSourcesModule } from '@modules/data-sources/module';
|
||||||
import { AppsSubscriber } from './subscribers/apps.subscriber';
|
import { AppsSubscriber } from './subscribers/apps.subscriber';
|
||||||
import { AiModule } from '@modules/ai/module';
|
import { AiModule } from '@modules/ai/module';
|
||||||
|
import { AppPermissionsModule } from '@modules/app-permissions/module';
|
||||||
@Module({})
|
@Module({})
|
||||||
export class AppsModule {
|
export class AppsModule {
|
||||||
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||||
|
|
@ -44,6 +45,7 @@ export class AppsModule {
|
||||||
await AppEnvironmentsModule.register(configs),
|
await AppEnvironmentsModule.register(configs),
|
||||||
await DataSourcesModule.register(configs),
|
await DataSourcesModule.register(configs),
|
||||||
await AiModule.register(configs),
|
await AiModule.register(configs),
|
||||||
|
await AppPermissionsModule.register(configs),
|
||||||
],
|
],
|
||||||
controllers: [AppsController],
|
controllers: [AppsController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { WorkflowSchedule } from '@entities/workflow_schedule.entity';
|
||||||
import { App } from '@entities/app.entity';
|
import { App } from '@entities/app.entity';
|
||||||
import { AiModule } from '@modules/ai/module';
|
import { AiModule } from '@modules/ai/module';
|
||||||
import { DataSourcesRepository } from '@modules/data-sources/repository';
|
import { DataSourcesRepository } from '@modules/data-sources/repository';
|
||||||
|
import { AppPermissionsModule } from '@modules/app-permissions/module';
|
||||||
export class WorkflowsModule {
|
export class WorkflowsModule {
|
||||||
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||||
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
|
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
|
||||||
|
|
@ -91,6 +92,7 @@ export class WorkflowsModule {
|
||||||
await FolderAppsModule.register(configs),
|
await FolderAppsModule.register(configs),
|
||||||
await ThemesModule.register(configs),
|
await ThemesModule.register(configs),
|
||||||
await AiModule.register(configs),
|
await AiModule.register(configs),
|
||||||
|
await AppPermissionsModule.register(configs),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AppsAbilityFactory,
|
AppsAbilityFactory,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue