feat: add crud operatios for page permissions

This commit is contained in:
Vijaykant Yadav 2025-04-22 09:27:02 +05:30
parent 54ffa99c3f
commit 8c53ac4bab
20 changed files with 850 additions and 215 deletions

View file

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

After

Width:  |  Height:  |  Size: 855 B

View file

@ -20,6 +20,8 @@ export const PageHandlerMenu = ({ darkMode }) => {
const toggleDeleteConfirmationModal = useStore((state) => state.toggleDeleteConfirmationModal);
const clonePage = useStore((state) => state.clonePage);
const markAsHomePage = useStore((state) => state.markAsHomePage);
const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
// const popoverTargetRef = null;
// console.log(
// {
@ -164,6 +166,16 @@ export const PageHandlerMenu = ({ darkMode }) => {
}}
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
id="delete-page"
text="Delete page"

View file

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

View 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>
);
};

View file

@ -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
}
}
}
}

View file

@ -142,6 +142,7 @@ const RenderPageGroup = ({
export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkMode, switchPageWrapper }) => {
// Don't render empty folders if displaying only icons
const tree = buildTree(pages, !!labelStyle?.label?.hidden);
const filteredPages = tree.filter((page) => !page?.isPageGroup || page.children?.length > 0);
const currentPageId = useStore((state) => state.currentPageId);
const currentPage = pages.find((page) => page.id === currentPageId);
@ -149,14 +150,14 @@ export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkM
return (
<div className={cx('page-handler-wrapper viewer', { 'dark-theme': darkMode })}>
{/* <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) {
return null;
}
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
const renderSeparatorTop = index !== 0 && labelStyle?.label?.hidden;
const renderSeparatorBottom = !tree[index + 1]?.isPageGroup && labelStyle?.label?.hidden;
const renderSeparatorBottom = !filteredPages[index + 1]?.isPageGroup && labelStyle?.label?.hidden;
return (
<>
{renderSeparatorTop && (

View file

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

View file

@ -1118,6 +1118,10 @@ export const createEventsSlice = (set, get) => ({
toast('Valid page handle is required', {
icon: '⚠️',
});
mode === 'view' &&
toast.error('Access to this page is restricted. Contact admin to know more.', {
icon: '⚠️',
});
return Promise.resolve();
}

View file

@ -96,6 +96,11 @@ export const createPageMenuSlice = (set, get) => {
isPageGroup: false,
pageSettingSelected: false,
pageSettings: {},
showPagePermissionModal: false,
permissionPage: null,
selectedUserGroups: [],
selectedUsers: [],
pagePermission: null,
toggleSearch: (show) =>
set((state) => {
@ -117,7 +122,6 @@ export const createPageMenuSlice = (set, get) => {
closePageEditPopover: () =>
set((state) => {
state.editingPage = null;
state.showEditingPopover = false;
state.showEditPageEventsModal = false;
state.showRenamePageHandleModal = false;
@ -419,5 +423,26 @@ export const createPageMenuSlice = (set, get) => {
console.error('Error updating page:', error);
}
},
setPagePermission: (pagePermission) =>
set((state) => {
state.pagePermission = pagePermission;
}),
togglePagePermissionModal: (show) => {
set((state) => {
state.showPagePermissionModal = show;
});
},
setSelectedUserGroups: (groups) =>
set((state) => {
state.selectedUserGroups = groups;
}),
setSelectedUsers: (users) =>
set((state) => {
state.selectedUsers = users;
}),
};
};

View file

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

View file

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

View file

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit 683647f83d3efeeadbe69c40b8e8dd5ba4e8ea06
Subproject commit 6b633ee14e024674a47dffbd731243cd2f002026

View file

@ -29,7 +29,7 @@ export class AppPermissionsModule {
PagePermissionsRepository,
FeatureAbilityFactory,
],
exports: [AppPermissionsUtilService],
exports: [AppPermissionsUtilService, AppPermissionsService],
};
}
}

View file

@ -15,7 +15,7 @@ export class PagePermissionsRepository extends Repository<PagePermission> {
return dbTransactionWrap(async (manager: EntityManager) => {
return manager.find(PagePermission, {
where: { pageId },
relations: ['users', 'users.user'],
relations: ['users', 'users.user', 'users.permissionGroup'],
});
}, manager || this.manager);
}

View file

@ -2,6 +2,7 @@ import { PageUser } from '@entities/page_users.entity';
import { Injectable } from '@nestjs/common';
import { DataSource, EntityManager, Repository } from 'typeorm';
import { dbTransactionWrap } from '@helpers/database.helper';
import { PagePermission } from '@entities/page_permissions.entity';
@Injectable()
export class PageUsersRepository extends Repository<PageUser> {
@ -42,4 +43,49 @@ export class PageUsersRepository extends Repository<PageUser> {
return manager.save(pageUsers);
}, manager || this.manager);
}
async checkIfUserExistsInPermissionGroup(
pagePermission: PagePermission,
userId: string,
manager?: EntityManager
): Promise<PageUser> {
return dbTransactionWrap(async (manager: EntityManager) => {
const result = await manager
.createQueryBuilder(PageUser, 'page_users')
.innerJoin('page_users.permissionGroup', 'group')
.innerJoin('group.groupUsers', 'groupUser')
.where('page_users.pagePermission = :permissionId', {
permissionId: pagePermission.id,
})
.andWhere('groupUser.userId = :userId', { userId })
.getOne();
if (!result) {
return false;
}
return pagePermission;
}, manager || this.manager);
}
async checkIfUserExistsInSingleConfig(
pagePermission: PagePermission,
userId: string,
manager?: EntityManager
): Promise<PageUser> {
return dbTransactionWrap(async (manager: EntityManager) => {
const pageUser = await manager.findOne(PageUser, {
where: {
pagePermission: { id: pagePermission.id },
userId,
},
});
if (!pageUser) {
return false;
}
return pagePermission;
}, manager || this.manager);
}
}

View file

@ -19,6 +19,7 @@ import { FeatureAbilityFactory } from './ability';
import { DataSourcesModule } from '@modules/data-sources/module';
import { AppsSubscriber } from './subscribers/apps.subscriber';
import { AiModule } from '@modules/ai/module';
import { AppPermissionsModule } from '@modules/app-permissions/module';
@Module({})
export class AppsModule {
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
@ -44,6 +45,7 @@ export class AppsModule {
await AppEnvironmentsModule.register(configs),
await DataSourcesModule.register(configs),
await AiModule.register(configs),
await AppPermissionsModule.register(configs),
],
controllers: [AppsController],
providers: [

View file

@ -29,6 +29,7 @@ import { WorkflowSchedule } from '@entities/workflow_schedule.entity';
import { App } from '@entities/app.entity';
import { AiModule } from '@modules/ai/module';
import { DataSourcesRepository } from '@modules/data-sources/repository';
import { AppPermissionsModule } from '@modules/app-permissions/module';
export class WorkflowsModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
@ -91,6 +92,7 @@ export class WorkflowsModule {
await FolderAppsModule.register(configs),
await ThemesModule.register(configs),
await AiModule.register(configs),
await AppPermissionsModule.register(configs),
],
providers: [
AppsAbilityFactory,