Extracted page permission modal as a reusable component

This commit is contained in:
devanshu052000 2025-05-27 16:03:52 +05:30
parent dc0d46cd7b
commit 3bd6bc5beb
10 changed files with 55 additions and 641 deletions

@ -1 +1 @@
Subproject commit 777446d71e78e5941d34353606a12d982820438f
Subproject commit 5c2787b498202f4356b4598fb961ddbab04acbbf

View file

@ -12,11 +12,12 @@ import './style.scss';
import { SortableTree } from './Tree/SortableTree';
import { PageGroupMenu } from './AddPageButton';
import { PageHandlerMenu } from './PageHandlerMenu.jsx';
import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal';
import { EditModal } from './EditModal';
import { SettingsModal } from './SettingsModal';
import { DeletePageConfirmationModal } from './DeletePageConfirmationModal';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import PagePermission from './PagePermission';
import { appPermissionService } from '@/_services';
export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
const showAddNewPageInput = useStore((state) => state.showAddNewPageInput);
@ -27,6 +28,11 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState);
const closePageEditPopover = useStore((state) => state.closePageEditPopover);
const editingPageId = useStore((state) => state.editingPage?.id);
const showPagePermissionModal = useStore((state) => state.showPagePermissionModal);
const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions);
useEffect(() => {
return () => {
closePageEditPopover();
@ -95,7 +101,20 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => {
>
<div>
<PageHandlerMenu darkMode={darkMode} />
{isLicensed ? <PagePermission darkMode={darkMode} /> : <></>}
{isLicensed && (
<AppPermissionsModal
modalType="page"
resourceId={editingPageId}
showModal={showPagePermissionModal}
toggleModal={togglePagePermissionModal}
darkMode={darkMode}
fetchPermission={(id, appId) => appPermissionService.getPagePermission(appId, id)}
createPermission={(id, appId, body) => appPermissionService.createPagePermission(appId, id, body)}
updatePermission={(id, appId, body) => appPermissionService.updatePagePermission(appId, id, body)}
deletePermission={(id, appId) => appPermissionService.deletePagePermission(appId, id)}
onSuccess={(data) => updatePageWithPermissions(editingPageId, data)}
/>
)}
<EditModal darkMode={darkMode} />
<SettingsModal darkMode={darkMode} />
<DeletePageConfirmationModal darkMode={darkMode} />

View file

@ -1,505 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import { components } from 'react-select';
import ModalBase from '@/_ui/Modal';
import Select from '@/_ui/Select';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import useStore from '@/AppBuilder/_stores/store';
import { appPermissionService } from '@/_services';
import { ConfirmDialog } from '@/_components';
import toast from 'react-hot-toast';
import Spinner from '@/_ui/Spinner';
const PERMISSION_TYPES = {
single: 'SINGLE',
group: 'GROUP',
all: 'ALL',
};
export default function PagePermission({ darkMode }) {
const showPagePermissionModal = useStore((state) => state.showPagePermissionModal);
const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal);
const editingPage = useStore((state) => state.editingPage);
const appId = useStore((state) => state.app.appId);
const selectedUserGroups = useStore((state) => state.selectedUserGroups);
const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups);
const selectedUsers = useStore((state) => state.selectedUsers);
const setSelectedUsers = useStore((state) => state.setSelectedUsers);
const pagePermission = useStore((state) => state.pagePermission);
const setPagePermission = useStore((state) => state.setPagePermission);
const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions);
const [pagePermissionType, setPagePermissionType] = useState('all');
const [showUserGroupSelect, toggleUserGroupSelect] = useState(false);
const [showUsersSelect, toggleUsersSelect] = useState(false);
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isPermissionsLoading, setPermissionsLoading] = useState(true);
const [initialSelectedGroups, setInitialSelectedGroups] = useState([]);
const [initialSelectedUsers, setInitialSelectedUsers] = useState([]);
const [initalPagePermissionType, setInitialPagePermissionType] = useState('all');
useEffect(() => {
if (!showPagePermissionModal) return;
const fetchPagePermission = () => {
appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => {
if (data) {
if (data[0] && data[0]?.type === PERMISSION_TYPES.group) {
const groups =
data[0]?.groups?.map((user) => ({
label: user?.permissionGroup?.name,
value: user?.permissionGroup?.id,
count: user?.permissionGroup?.count,
})) ?? [];
setPagePermissionType(data[0]?.type?.toLowerCase());
setInitialPagePermissionType(data[0]?.type?.toLowerCase());
setPagePermission(data);
toggleUserGroupSelect(true);
setInitialSelectedGroups(groups);
data?.length && setSelectedUserGroups(groups);
} else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) {
const users =
data[0]?.users?.map(({ user }) => {
const firstName = user.firstName || '';
const lastName = user.lastName || '';
return {
value: user.id,
label: `${firstName} ${lastName}`.trim(),
email: user.email,
initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(),
};
}) ?? [];
setPagePermissionType(data[0]?.type?.toLowerCase());
setInitialPagePermissionType(data[0]?.type?.toLowerCase());
setPagePermission(data);
toggleUsersSelect(true);
setInitialSelectedUsers(users);
data?.length && setSelectedUsers(users);
}
}
setPermissionsLoading(false);
});
};
fetchPagePermission();
}, [showPagePermissionModal]);
const isSelectionUnchanged = useMemo(() => {
if (pagePermissionType === 'group') {
if (!selectedUserGroups.length) return true;
const current = selectedUserGroups
.map((g) => g.value)
.sort()
.join(',');
const initial = initialSelectedGroups
.map((g) => g.value)
.sort()
.join(',');
return current === initial;
} else if (pagePermissionType === 'single') {
if (!selectedUsers.length) return true;
const current = selectedUsers
.map((u) => u.value)
.sort()
.join(',');
const initial = initialSelectedUsers
.map((u) => u.value)
.sort()
.join(',');
return current === initial;
} else {
if (!pagePermission?.length) {
return true;
} else {
return initalPagePermissionType == pagePermissionType;
}
}
}, [
pagePermissionType,
selectedUserGroups,
initialSelectedGroups,
selectedUsers,
initialSelectedUsers,
initalPagePermissionType,
]);
const permissionTypeOptions = useMemo(
() => [
{
label: 'All users with access to the app',
value: 'all',
icon: 'globe',
},
{
label: 'Users',
value: 'single',
icon: 'user',
},
{
label: 'User groups',
value: 'group',
icon: 'usergroup',
},
],
[]
);
const handlePermissionTypeChange = (value) => {
switch (value) {
case 'group': {
toggleUserGroupSelect(true);
toggleUsersSelect(false);
setPagePermissionType('group');
break;
}
case 'single': {
toggleUsersSelect(true);
toggleUserGroupSelect(false);
setPagePermissionType('single');
break;
}
case 'all': {
toggleUsersSelect(false);
toggleUserGroupSelect(false);
setPagePermissionType('all');
}
}
};
const handlePagePermissionModalClose = () => {
togglePagePermissionModal(false);
toggleUserGroupSelect(false);
toggleUsersSelect(false);
setPagePermissionType('all');
setPagePermission(null);
setSelectedUsers([]);
setSelectedUserGroups([]);
setInitialSelectedGroups([]);
setInitialSelectedUsers([]);
};
const createPagePermission = () => {
const body = {
id: editingPage?.id,
type: PERMISSION_TYPES[pagePermissionType],
...(pagePermissionType === 'group'
? { groups: selectedUserGroups.map((group) => group?.value) }
: { users: selectedUsers.map((user) => user?.value) }),
};
setIsLoading(true);
appPermissionService
.createPagePermission(appId, editingPage?.id, body)
.then((data) => {
toast.success('Permission successfully created!', {
className: 'text-nowrap w-auto mw-100',
});
updatePageWithPermissions(editingPage?.id, data);
})
.catch(() => {
toast.error('Permission could not be created. Please try again!', {
className: 'text-nowrap w-auto mw-100',
});
})
.finally(() => {
setIsLoading(false);
handlePagePermissionModalClose();
});
};
const updatePagePermission = () => {
const body = {
id: editingPage?.id,
type: PERMISSION_TYPES[pagePermissionType],
...(pagePermissionType === 'group'
? { groups: selectedUserGroups.map((group) => group?.value) }
: { users: selectedUsers.map((user) => user?.value) }),
};
setIsLoading(true);
appPermissionService
.updatePagePermission(appId, editingPage?.id, body)
.then((data) => {
toast.success('Permission successfully updated!', {
className: 'text-nowrap w-auto mw-100',
});
updatePageWithPermissions(editingPage?.id, data);
})
.catch(() => {
toast.error('Permission could not be updated. Please try again!', {
className: 'text-nowrap w-auto mw-100',
});
})
.finally(() => {
setIsLoading(false);
handlePagePermissionModalClose();
});
};
const deletePagePermission = () => {
setIsLoading(true);
appPermissionService
.deletePagePermission(appId, editingPage?.id)
.then((data) => {
toast.success('Permission successfully deleted!', {
className: 'text-nowrap w-auto mw-100',
});
updatePageWithPermissions(editingPage?.id, []);
})
.catch(() => {
toast.error('Permission could not be deleted. Please try again!', {
className: 'text-nowrap w-auto mw-100',
});
setShowConfirmDelete(false);
togglePagePermissionModal(true);
})
.finally(() => {
setIsLoading(false);
setShowConfirmDelete(false);
});
};
const renderPermissionTypeOptions = ({ label, icon }) => {
return (
<div className="row permission-type-select" style={{ padding: '0px 8px' }}>
<div className="col-auto">
<SolidIcon width="20" name={icon} />
</div>
<div className="col">
<span>{label}</span>
</div>
</div>
);
};
return (
<>
<ModalBase
title={
<div className="my-3">
<span className="tj-text-md font-weight-500">Page permission</span>
</div>
}
handleConfirm={!pagePermission ? createPagePermission : updatePagePermission}
show={showPagePermissionModal}
isLoading={isLoading}
handleClose={handlePagePermissionModalClose}
confirmBtnProps={{
title: pagePermission
? 'Save changes'
: pagePermissionType === 'all'
? 'Default permission'
: 'Create permission',
disabled: isPermissionsLoading || isSelectionUnchanged,
tooltipMessage: '',
leftIcon: pagePermission && 'save',
className: 'action-btn-page-permission',
}}
darkMode={darkMode}
className="page-permissions-modal"
>
<div className="page-permission">
{isPermissionsLoading ? (
<div className="spinner-center">
<Spinner />
</div>
) : (
<>
<div className="info-container">
<div className="col-md-1 info-btn">
<SolidIcon name="informationcircle" fill="#3E63DD" />
</div>
<div className="col-md-11">
<div className="message">
<p style={{ lineHeight: '18px' }}>
Only selected users will be allowed to access this page. Read docs to know more.
</p>
</div>
</div>
</div>
<label className="form-label">Type</label>
<Select
options={permissionTypeOptions}
value={pagePermissionType}
width={'100%'}
customOption={renderPermissionTypeOptions}
useMenuPortal={false}
onChange={handlePermissionTypeChange}
/>
{showUserGroupSelect && <UserGroupSelect />}
{showUsersSelect && <UserSelect />}
</>
)}
</div>
</ModalBase>
{showConfirmDelete && (
<ConfirmDialog
title={'Delete page permission'}
show={showConfirmDelete}
message={
'Deleting the permission will allow all users with access to the app to view this page. Are you sure you want to continue?'
}
confirmButtonLoading={isLoading}
onConfirm={() => deletePagePermission()}
onCancel={() => setShowConfirmDelete(false)}
confirmButtonText={'Delete'}
darkMode={darkMode}
confirmButtonIcon={'trash'}
confirmButtonIconWidth="20"
confirmButtonIconFill={'var(--slate3)'}
/>
)}
</>
);
}
const UserGroupSelect = () => {
const appId = useStore((state) => state.app.appId);
const selectedUserGroups = useStore((state) => state.selectedUserGroups);
const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups);
const [userGroups, setUserGroups] = useState([]);
useEffect(() => {
const fetchUserGroups = () => {
appPermissionService.getUsers(appId, 'user-groups').then((data) => {
if (data?.length) {
const groups = [];
data.map((group) => {
groups.push({ value: group.id, label: group.name, count: group.count });
});
setUserGroups(groups);
}
});
};
fetchUserGroups();
}, []);
const CustomOption = (props) => {
const { data, isFocused, isSelected } = props;
return (
<components.Option {...props}>
<div className={`user-select-option ${isFocused ? 'focused' : ''}`}>
<input
style={{ width: '1.2rem', height: '1.2rem', borderRadius: '6px !important' }}
type={'checkbox'}
className="form-check-input"
checked={isSelected}
/>
<div className="group-info">
<div className="name">{data.label}</div>
<div className="count">{data.count} users</div>
</div>
</div>
</components.Option>
);
};
return (
<div>
<label className="form-label mt-3">User groups</label>
<Select
isMulti={true}
options={userGroups}
value={selectedUserGroups}
width={'100%'}
closeMenuOnSelect={false}
components={{ Option: CustomOption, MenuList: CustomMenuList }}
useMenuPortal={false}
hideSelectedOptions={false}
onChange={(groups) => setSelectedUserGroups(groups)}
info="Only user groups with access to this application can be selected"
/>
</div>
);
};
const UserSelect = () => {
const appId = useStore((state) => state.app.appId);
const editingPage = useStore((state) => state.editingPage);
const selectedUsers = useStore((state) => state.selectedUsers);
const setSelectedUsers = useStore((state) => state.setSelectedUsers);
const [users, setUsers] = useState([]);
useEffect(() => {
const fetchUsers = () => {
appPermissionService.getUsers(appId, 'users').then((data) => {
if (data?.length) {
const users = [];
data.map((user) => {
const firstName = user.firstName || '';
const lastName = user.lastName || '';
users.push({
value: user.id,
label: `${firstName} ${lastName}`.trim(),
email: user.email,
initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(),
});
});
setUsers(users);
}
});
};
fetchUsers();
}, []);
const CustomOption = (props) => {
const { data, isFocused, isSelected } = props;
return (
<components.Option {...props}>
<div className={`user-select-option ${isFocused ? 'focused' : ''}`}>
<input
style={{ width: '1.2rem', height: '1.2rem', borderRadius: '6px !important' }}
type={'checkbox'}
className="form-check-input"
checked={isSelected}
/>
<div className="avatar">{data.initials}</div>
<div className="user-info">
<div className="name">{data.label}</div>
<div className="email">{data.email}</div>
</div>
</div>
</components.Option>
);
};
const selectStyles = {
option: (base) => ({
...base,
padding: '8px 0px',
}),
};
return (
<div>
<label className="form-label mt-3">Users</label>
<Select
isMulti={true}
options={users}
value={selectedUsers}
width={'100%'}
useMenuPortal={false}
closeMenuOnSelect={false}
components={{ Option: CustomOption, MenuList: CustomMenuList }}
styles={selectStyles}
hideSelectedOptions={false}
info="Only user with access to this application can be selected"
onChange={(users) => {
setSelectedUsers(users);
}}
/>
</div>
);
};
const CustomMenuList = (props) => {
const { info } = props.selectProps;
return (
<components.MenuList {...props}>
<div className="info-container" style={{ marginLeft: '12px', marginRight: '12px', marginTop: '8px' }}>
<div className="col-md-1 info-btn">
<SolidIcon name="informationcircle" fill="#3E63DD" />
</div>
<div className="col-md-11">
<div className="message">
<p style={{ lineHeight: '18px' }}>{info}</p>
</div>
</div>
</div>
{props.children}
</components.MenuList>
);
};

View file

@ -267,115 +267,3 @@
}
}
}
.page-permission {
.info-container {
display: flex;
width: auto;
height: auto;
padding: 10px 12px 8px 12px;
border: 1px solid var(--slate5);
background: var(--slate2);
border-radius: 6px 6px 6px 6px;
margin-bottom: 13px;
margin-top: 0px;
}
.permission-type-select {
align-items: center;
.col-auto {
padding-right: 0px;
}
}
}
.page-permissions-modal {
#header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.react-select__option {
padding: 8px 0px;
input {
margin-right: 10px;
}
}
.react-select__menu-list {
overflow-y: unset !important;
}
.user-select-option {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
&.focused {
background-color: #f3f4f6; // Tailwind's gray-100 vibe
}
.avatar {
background-color: var(--slate5); // light gray
color: var(--slate12); // dark text
font-weight: 500;
font-size: 16px;
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
}
.user-info {
display: flex;
flex-direction: column;
.name {
font-weight: 500;
font-size: 14px;
color: var(--slate12);
}
.email {
font-size: 12px;
color: var(--slate10);
}
}
.group-info {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
.name {
font-weight: 400;
font-size: 14px;
color: var(--slate12);
}
.count {
font-size: 12px;
color: var(--slate9);
}
}
}
}
.page-permission {
.spinner-center {
min-height: 250px;
}
}
.modal-base .modal-footer .action-btn-page-permission svg path {
fill: var(--indigo1) !important;
}

View file

@ -20,6 +20,10 @@ const initialState = {
isTJDarkMode: localStorage.getItem('darkMode') === 'true',
isViewer: false,
isComponentLayoutReady: false,
appPermission: {
selectedUsers: [],
selectedUserGroups: [],
},
};
export const createAppSlice = (set, get) => ({
@ -206,4 +210,12 @@ export const createAppSlice = (set, get) => ({
);
},
updateIsTJDarkMode: (newMode) => set({ isTJDarkMode: newMode }, false, 'updateIsTJDarkMode'),
setSelectedUserGroups: (groups) =>
set((state) => {
state.appPermission.selectedUserGroups = groups;
}),
setSelectedUsers: (users) =>
set((state) => {
state.appPermission.selectedUsers = users;
}),
});

View file

@ -99,10 +99,6 @@ export const createPageMenuSlice = (set, get) => {
pageSettingSelected: false,
pageSettings: {},
showPagePermissionModal: false,
permissionPage: null,
selectedUserGroups: [],
selectedUsers: [],
pagePermission: null,
toggleSearch: (show) =>
set((state) => {
@ -427,26 +423,12 @@ export const createPageMenuSlice = (set, get) => {
}
},
setPagePermission: (pagePermission) =>
set((state) => {
state.pagePermission = pagePermission;
}),
togglePagePermissionModal: (show) => {
set((state) => {
state.showPagePermissionModal = show;
});
},
setSelectedUserGroups: (groups) =>
set((state) => {
state.selectedUserGroups = groups;
}),
setSelectedUsers: (users) =>
set((state) => {
state.selectedUsers = users;
}),
setEditingPage: (page) =>
set((state) => {
state.editingPage = page;

View file

@ -360,7 +360,7 @@ class HomePageComponent extends React.Component {
}
};
importFile = async (importJSON, appName, skipPagePermissionsGroupCheck = false) => {
importFile = async (importJSON, appName, skipPermissionsGroupCheck = false) => {
this.setState({ isImportingApp: true });
// For backward compatibility with legacy app import
const organization_id = this.state.currentUser?.organization_id;
@ -376,7 +376,7 @@ class HomePageComponent extends React.Component {
const requestBody = {
organization_id,
...importJSON,
skip_page_permissions_group_check: skipPagePermissionsGroupCheck,
skip_permissions_group_check: skipPermissionsGroupCheck,
};
let installedPluginsInfo = [];
try {

View file

@ -0,0 +1,8 @@
import React from 'react';
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
const AppPermissionsModal = () => {
return <></>;
};
export default withEditionSpecificComponent(AppPermissionsModal, 'Appbuilder');

View file

@ -0,0 +1 @@
export { default } from './AppPermissionsModal';

View file

@ -4,5 +4,14 @@ import LogoNavDropdown from './LogoNavDropdown';
import AppEnvironments from './AppEnvironments';
import ThemeSelect from './ThemeSelect';
import ColorSwatches from './ColorSwatches';
import AppPermissionsModal from './AppPermissionsModal';
export { CreateVersionModal, PromoteReleaseButton, LogoNavDropdown, AppEnvironments, ThemeSelect, ColorSwatches };
export {
CreateVersionModal,
PromoteReleaseButton,
LogoNavDropdown,
AppEnvironments,
ThemeSelect,
ColorSwatches,
AppPermissionsModal,
};