From de1bd53f530c936fe28fbdf578bf5f4ab7b6bb0c Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Thu, 20 Aug 2026 15:04:50 +0100 Subject: [PATCH] Add User Groups and Permission Settings Management Components - Introduced UserGroups and PermissionSettings components for managing user groups and permission settings. - Implemented new icons for user groups and permission settings, enhancing visual representation in the dashboard. - Updated routing and sidebar configuration to include new management sections for user groups and permission settings. - Enhanced existing components with loading states and improved styling for better user experience. - Refactored related components to ensure consistency in naming and functionality across the application. --- assets/icons/persongroupicon.svg | 8 + assets/stylesheets/App.css | 10 +- .../Management/PermissionSettings.jsx | 14 +- ...nSettings.jsx => NewPermissionSetting.jsx} | 12 +- ...ingsInfo.jsx => PermissionSettingInfo.jsx} | 42 +-- .../Dashboard/Management/UserGroups.jsx | 123 ++++++++ .../Management/UserGroups/NewUserGroup.jsx | 77 +++++ .../Management/UserGroups/UserGroupInfo.jsx | 265 ++++++++++++++++++ .../Dashboard/Management/Users/UserInfo.jsx | 1 + .../Dashboard/common/DashboardBreadcrumb.jsx | 2 - .../Dashboard/common/NewObjectForm.jsx | 45 ++- .../Dashboard/common/ObjectForm.jsx | 19 +- .../Dashboard/common/PermissionCheckbox.jsx | 18 +- ...ingsIcon.jsx => PermissionSettingIcon.jsx} | 4 +- src/components/Icons/PersonGroupIcon.jsx | 6 + src/components/Icons/sidebarIconMap.jsx | 6 +- src/database/ObjectModels.js | 9 +- ...issionSettings.js => PermissionSetting.js} | 20 +- src/database/models/User.js | 16 +- src/database/models/UserGroup.js | 142 ++++++++++ src/database/permissions.js | 38 +++ src/database/sidebars/management.js | 8 +- src/routes/ManagementRoutes.jsx | 8 + 23 files changed, 822 insertions(+), 71 deletions(-) create mode 100644 assets/icons/persongroupicon.svg rename src/components/Dashboard/Management/PermissionSettings/{NewPermissionSettings.jsx => NewPermissionSetting.jsx} (88%) rename src/components/Dashboard/Management/PermissionSettings/{PermissionSettingsInfo.jsx => PermissionSettingInfo.jsx} (89%) create mode 100644 src/components/Dashboard/Management/UserGroups.jsx create mode 100644 src/components/Dashboard/Management/UserGroups/NewUserGroup.jsx create mode 100644 src/components/Dashboard/Management/UserGroups/UserGroupInfo.jsx rename src/components/Icons/{PermissionSettingsIcon.jsx => PermissionSettingIcon.jsx} (67%) create mode 100644 src/components/Icons/PersonGroupIcon.jsx rename src/database/models/{PermissionSettings.js => PermissionSetting.js} (86%) create mode 100644 src/database/models/UserGroup.js create mode 100644 src/database/permissions.js diff --git a/assets/icons/persongroupicon.svg b/assets/icons/persongroupicon.svg new file mode 100644 index 00000000..f9f288c1 --- /dev/null +++ b/assets/icons/persongroupicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 8805b3cd..fb0a5b03 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -1405,10 +1405,18 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { .permission-checkbox[data-state='inherit'] .permission-checkbox-inherit, .permission-checkbox[data-state='allow'] .permission-checkbox-allow, -.permission-checkbox[data-state='deny'] .permission-checkbox-deny { +.permission-checkbox[data-state='deny'] .permission-checkbox-deny, +.permission-checkbox[data-state='mixed'] .permission-checkbox-mixed { opacity: 1; } +.permission-checkbox-square { + width: 7px; + height: 7px; + background-color: #ffffff; + border-radius: 1px; +} + .permission-checkbox-face .anticon { display: flex; line-height: 0; diff --git a/src/components/Dashboard/Management/PermissionSettings.jsx b/src/components/Dashboard/Management/PermissionSettings.jsx index 71e70e7f..02ad21b9 100644 --- a/src/components/Dashboard/Management/PermissionSettings.jsx +++ b/src/components/Dashboard/Management/PermissionSettings.jsx @@ -1,6 +1,6 @@ import { useState, useRef } from 'react' import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewPermissionSettings from './PermissionSettings/NewPermissionSettings' +import NewPermissionSetting from './PermissionSettings/NewPermissionSetting' import ObjectTable from '../common/ObjectTable' import PlusIcon from '../../Icons/PlusIcon' import ReloadIcon from '../../Icons/ReloadIcon' @@ -19,10 +19,10 @@ const PermissionSettingsList = () => { useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('permissionSettings') + const [viewMode, setViewMode] = useViewMode('permissionSetting') const [columnVisibility, setColumnVisibility] = - useColumnVisibility('permissionSettings') + useColumnVisibility('permissionSetting') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('PermissionSettings') @@ -62,12 +62,12 @@ const PermissionSettingsList = () => { - + { { destroyOnHidden={true} width={700} > - { setNewPermissionSettingsOpen(false) tableRef.current?.reload() diff --git a/src/components/Dashboard/Management/PermissionSettings/NewPermissionSettings.jsx b/src/components/Dashboard/Management/PermissionSettings/NewPermissionSetting.jsx similarity index 88% rename from src/components/Dashboard/Management/PermissionSettings/NewPermissionSettings.jsx rename to src/components/Dashboard/Management/PermissionSettings/NewPermissionSetting.jsx index 3dc57254..3823d35d 100644 --- a/src/components/Dashboard/Management/PermissionSettings/NewPermissionSettings.jsx +++ b/src/components/Dashboard/Management/PermissionSettings/NewPermissionSetting.jsx @@ -3,10 +3,10 @@ import ObjectInfo from '../../common/ObjectInfo' import NewObjectForm from '../../common/NewObjectForm' import WizardView from '../../common/WizardView' -const NewPermissionSettings = ({ onOk, defaultValues }) => { +const NewPermissionSetting = ({ onOk, defaultValues }) => { return ( {({ handleSubmit, submitLoading, objectData, formValid }) => { @@ -16,7 +16,7 @@ const NewPermissionSettings = ({ onOk, defaultValues }) => { key: 'required', content: ( { key: 'summary', content: ( { ) } -NewPermissionSettings.propTypes = { +NewPermissionSetting.propTypes = { onOk: PropTypes.func.isRequired, reset: PropTypes.bool, defaultValues: PropTypes.object } -export default NewPermissionSettings +export default NewPermissionSetting diff --git a/src/components/Dashboard/Management/PermissionSettings/PermissionSettingsInfo.jsx b/src/components/Dashboard/Management/PermissionSettings/PermissionSettingInfo.jsx similarity index 89% rename from src/components/Dashboard/Management/PermissionSettings/PermissionSettingsInfo.jsx rename to src/components/Dashboard/Management/PermissionSettings/PermissionSettingInfo.jsx index 4d2201ee..7d22b591 100644 --- a/src/components/Dashboard/Management/PermissionSettings/PermissionSettingsInfo.jsx +++ b/src/components/Dashboard/Management/PermissionSettings/PermissionSettingInfo.jsx @@ -26,17 +26,17 @@ import { } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' import ObjectProperty from '../../common/ObjectProperty.jsx' -import PermissionSettingsIcon from '../../../Icons/PermissionSettingsIcon.jsx' +import PermissionSettingIcon from '../../../Icons/PermissionSettingIcon.jsx' -const PermissionSettingsInfo = () => { +const PermissionSettingInfo = () => { const location = useLocation() const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const permissionSettingsId = new URLSearchParams(location.search).get( - 'permissionSettingsId' + const permissionSettingId = new URLSearchParams(location.search).get( + 'permissionSettingId' ) const [collapseState, updateCollapseState] = useCollapseState( - 'PermissionSettingsInfo', + 'PermissionSettingInfo', { info: true, notes: true, @@ -84,8 +84,8 @@ const PermissionSettingsInfo = () => { @@ -100,12 +100,12 @@ const PermissionSettingsInfo = () => { updateVisibleState={updateCollapseState} /> @@ -138,8 +138,8 @@ const PermissionSettingsInfo = () => { /> @@ -152,8 +152,8 @@ const PermissionSettingsInfo = () => { ref={actionHandlerRef} > { loading={loading} indicator={} isEditing={isEditing} - type='permissionSettings' + type='permissionSetting' objectData={objectData} visibleProperties={{ permissions: false @@ -185,7 +185,7 @@ const PermissionSettingsInfo = () => { } + icon={} active={collapseState.matrix} onToggle={(expanded) => updateCollapseState('matrix', expanded) @@ -196,7 +196,7 @@ const PermissionSettingsInfo = () => { { > @@ -240,9 +240,9 @@ const PermissionSettingsInfo = () => { type='auditLog' masterFilter={{ parent: - getModelByName('permissionSettings').prefix + + getModelByName('permissionSetting').prefix + ':' + - permissionSettingsId + permissionSettingId }} visibleColumns={{ _id: false, parent: false }} /> @@ -255,4 +255,4 @@ const PermissionSettingsInfo = () => { ) } -export default PermissionSettingsInfo +export default PermissionSettingInfo diff --git a/src/components/Dashboard/Management/UserGroups.jsx b/src/components/Dashboard/Management/UserGroups.jsx new file mode 100644 index 00000000..ad655497 --- /dev/null +++ b/src/components/Dashboard/Management/UserGroups.jsx @@ -0,0 +1,123 @@ +import { useState, useRef } from 'react' +import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import NewUserGroup from './UserGroups/NewUserGroup' +import ObjectTable from '../common/ObjectTable' +import PlusIcon from '../../Icons/PlusIcon' +import ReloadIcon from '../../Icons/ReloadIcon' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useViewMode from '../hooks/useViewMode' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' + +const UserGroups = () => { + const [newUserGroupOpen, setNewUserGroupOpen] = useState(false) + const tableRef = useRef() + + const [viewMode, setViewMode] = useViewMode('userGroup') + + const [columnVisibility, setColumnVisibility] = useColumnVisibility('userGroup') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('UserGroups') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('UserGroups') + + const actionItems = { + items: [ + { + label: 'New User Group', + key: 'newUserGroup', + icon: + }, + { type: 'divider' }, + { + label: 'Reload List', + key: 'reloadList', + icon: + } + ], + onClick: ({ key }) => { + if (key === 'reloadList') { + tableRef.current?.reload() + } else if (key === 'newUserGroup') { + setNewUserGroupOpen(true) + } + } + } + + return ( + <> + + + + + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + setNewUserGroupOpen(false)} + footer={null} + destroyOnHidden={true} + width={700} + > + { + setNewUserGroupOpen(false) + tableRef.current?.reload() + }} + reset={!newUserGroupOpen} + /> + + + ) +} + +export default UserGroups diff --git a/src/components/Dashboard/Management/UserGroups/NewUserGroup.jsx b/src/components/Dashboard/Management/UserGroups/NewUserGroup.jsx new file mode 100644 index 00000000..257bd750 --- /dev/null +++ b/src/components/Dashboard/Management/UserGroups/NewUserGroup.jsx @@ -0,0 +1,77 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewUserGroup = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewUserGroup.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewUserGroup diff --git a/src/components/Dashboard/Management/UserGroups/UserGroupInfo.jsx b/src/components/Dashboard/Management/UserGroups/UserGroupInfo.jsx new file mode 100644 index 00000000..7a8a5c40 --- /dev/null +++ b/src/components/Dashboard/Management/UserGroups/UserGroupInfo.jsx @@ -0,0 +1,265 @@ +import { useRef, useState } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import ScrollBox from '../../common/ScrollBox.jsx' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' +import PersonGroupIcon from '../../../Icons/PersonGroupIcon.jsx' +import PersonIcon from '../../../Icons/PersonIcon.jsx' + +const UserGroupInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const userGroupId = new URLSearchParams(location.search).get('userGroupId') + const [collapseState, updateCollapseState] = useCollapseState( + 'UserGroupInfo', + { + info: true, + permissions: true, + users: true, + notes: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + editDisabled: false, + objectData: {} + }) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + }, + delete: () => { + objectFormRef?.current?.handleDelete?.() + return true + } + } + + return ( + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + } + isEditing={isEditing} + type='userGroup' + labelWidth='190px' + objectData={objectData} + visibleProperties={{ + permissions: false + }} + /> + )} + + + + } + active={collapseState.permissions} + onToggle={(expanded) => + updateCollapseState('permissions', expanded) + } + collapseKey='permissions' + > + } + > + + + + + + } + active={collapseState.users} + onToggle={(expanded) => updateCollapseState('users', expanded)} + collapseKey='users' + > + {!userGroupId ? ( + + ) : ( + + )} + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + ) +} + +export default UserGroupInfo diff --git a/src/components/Dashboard/Management/Users/UserInfo.jsx b/src/components/Dashboard/Management/Users/UserInfo.jsx index ab627945..2f38cc68 100644 --- a/src/components/Dashboard/Management/Users/UserInfo.jsx +++ b/src/components/Dashboard/Management/Users/UserInfo.jsx @@ -187,6 +187,7 @@ const UserInfo = () => { isEditing={isEditing} type='user' objectData={objectData} + labelWidth='190px' visibleProperties={{ profileImage: false, permissions: false diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx index a464cadd..d4e4a2dd 100644 --- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx +++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx @@ -14,8 +14,6 @@ const breadcrumbNameMap = { developer: 'Developer', finance: 'Finance', sales: 'Sales', - productcategories: 'Product Categories', - permissionSettings: 'Permission Settings', overview: 'Overview', info: 'Info', design: 'Design', diff --git a/src/components/Dashboard/common/NewObjectForm.jsx b/src/components/Dashboard/common/NewObjectForm.jsx index 4004433a..aba99983 100644 --- a/src/components/Dashboard/common/NewObjectForm.jsx +++ b/src/components/Dashboard/common/NewObjectForm.jsx @@ -3,7 +3,7 @@ import { Form } from 'antd' import { ApiServerContext } from '../context/ApiServerContext' import { useMessageContext } from '../context/MessageContext' import PropTypes from 'prop-types' -import merge from 'lodash/merge' +import mergeWith from 'lodash/mergeWith' import set from 'lodash/set' import { getModelByName } from '../../../database/ObjectModels' @@ -18,6 +18,15 @@ const buildObjectFromEntries = (entries = []) => { }, {}) } +const arrayReplaceCustomizer = (objValue, srcValue, key) => { + if (Array.isArray(srcValue)) { + return srcValue + } + if (key === 'permissions' && srcValue !== undefined) { + return srcValue + } +} + /** * NewObjectForm is a reusable form component for creating new objects. * It handles form validation, submission, and error handling logic. @@ -164,9 +173,16 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { // Calculate computed values for initial data const computedEntries = calculateComputedValues(defaultValues, model) const computedValuesObject = buildObjectFromEntries(computedEntries) - const initialFormData = merge({}, defaultValues, computedValuesObject) + const initialFormData = mergeWith( + {}, + defaultValues, + computedValuesObject, + arrayReplaceCustomizer + ) form.setFieldsValue(initialFormData) - setObjectData((prev) => merge({}, prev, initialFormData)) + setObjectData((prev) => + mergeWith({}, prev, initialFormData, arrayReplaceCustomizer) + ) return validateForm() } }, [form, defaultValues, calculateComputedValues, model, validateForm]) @@ -181,7 +197,12 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { setSubmitLoading(true) const computedEntries = calculateComputedValues(objectData, model) const computedValuesObject = buildObjectFromEntries(computedEntries) - const computedObjectData = merge({}, objectData, computedValuesObject) + const computedObjectData = mergeWith( + {}, + objectData, + computedValuesObject, + arrayReplaceCustomizer + ) const newObject = await createObject(type, computedObjectData) showSuccess('Object created successfully') return newObject @@ -207,7 +228,12 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { style={style} onValuesChange={(_changedValues, allFormValues) => { // Calculate computed values based on current form data - const currentFormData = merge({}, objectData || {}, allFormValues) + const currentFormData = mergeWith( + {}, + objectData || {}, + allFormValues, + arrayReplaceCustomizer + ) const computedEntries = calculateComputedValues(currentFormData, model) if (Array.isArray(computedEntries) && computedEntries.length > 0) { @@ -229,9 +255,14 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => { // Merge all values (user input + computed values) const computedValuesObject = buildObjectFromEntries(computedEntries) - const allValues = merge({}, allFormValues, computedValuesObject) + const allValues = mergeWith( + {}, + allFormValues, + computedValuesObject, + arrayReplaceCustomizer + ) setObjectData((prev) => { - return merge({}, prev, allValues) + return mergeWith({}, prev, allValues, arrayReplaceCustomizer) }) }} > diff --git a/src/components/Dashboard/common/ObjectForm.jsx b/src/components/Dashboard/common/ObjectForm.jsx index bc708207..9e6c9e89 100644 --- a/src/components/Dashboard/common/ObjectForm.jsx +++ b/src/components/Dashboard/common/ObjectForm.jsx @@ -20,10 +20,13 @@ import { useLocation, useNavigate } from 'react-router-dom' import PropTypes from 'prop-types' import { useActions } from '../context/ActionsContext' -const arrayReplaceCustomizer = (objValue, srcValue) => { +const arrayReplaceCustomizer = (objValue, srcValue, key) => { if (Array.isArray(srcValue)) { return srcValue } + if (key === 'permissions' && srcValue !== undefined) { + return srcValue + } } const getUserId = (user) => user?._id || user @@ -339,10 +342,11 @@ const ObjectForm = forwardRef( model ) const computedValuesObject = buildObjectFromEntries(computedEntries) - const resetFormData = merge( + const resetFormData = mergeWith( {}, serverObjectData.current, - computedValuesObject + computedValuesObject, + arrayReplaceCustomizer ) setIsEditing(false) isEditingRef.current = false @@ -470,7 +474,12 @@ const ObjectForm = forwardRef( // Calculate and set computed values on initial load const computedEntries = calculateComputedValues(data, model) const computedValuesObject = buildObjectFromEntries(computedEntries) - const initialFormData = merge({}, data, computedValuesObject) + const initialFormData = mergeWith( + {}, + data, + computedValuesObject, + arrayReplaceCustomizer + ) setObjectData({ ...initialFormData, _isEditing: isEditingRef.current }) form.setFieldsValue(initialFormData) setFetchLoading(false) @@ -828,9 +837,9 @@ const ObjectForm = forwardRef( const mergedFormValues = mergeWith( {}, allFormValues, + computedValuesObject, arrayReplaceCustomizer ) - merge(mergedFormValues, computedValuesObject) mergedFormValues._isEditing = isEditingRef.current diff --git a/src/components/Dashboard/common/PermissionCheckbox.jsx b/src/components/Dashboard/common/PermissionCheckbox.jsx index 756def49..d831c72a 100644 --- a/src/components/Dashboard/common/PermissionCheckbox.jsx +++ b/src/components/Dashboard/common/PermissionCheckbox.jsx @@ -11,11 +11,18 @@ const ICON_STYLE = { const PermissionCheckbox = ({ checked = false, indeterminate = false, + mixed = false, disabled = false, onChange }) => { const { token } = theme.useToken() - const state = indeterminate ? 'inherit' : checked ? 'allow' : 'deny' + const state = mixed + ? 'mixed' + : indeterminate + ? 'inherit' + : checked + ? 'allow' + : 'deny' return ( ) } @@ -53,6 +66,7 @@ const PermissionCheckbox = ({ PermissionCheckbox.propTypes = { checked: PropTypes.bool, indeterminate: PropTypes.bool, + mixed: PropTypes.bool, disabled: PropTypes.bool, onChange: PropTypes.func } diff --git a/src/components/Icons/PermissionSettingsIcon.jsx b/src/components/Icons/PermissionSettingIcon.jsx similarity index 67% rename from src/components/Icons/PermissionSettingsIcon.jsx rename to src/components/Icons/PermissionSettingIcon.jsx index d23ceb15..4c245c89 100644 --- a/src/components/Icons/PermissionSettingsIcon.jsx +++ b/src/components/Icons/PermissionSettingIcon.jsx @@ -1,8 +1,8 @@ import Icon from '@ant-design/icons' import CustomIconSvg from '../../../assets/icons/permissionsettingsicon.svg?react' -const PermissionSettingsIcon = (props) => ( +const PermissionSettingIcon = (props) => ( ) -export default PermissionSettingsIcon +export default PermissionSettingIcon diff --git a/src/components/Icons/PersonGroupIcon.jsx b/src/components/Icons/PersonGroupIcon.jsx new file mode 100644 index 00000000..5394521c --- /dev/null +++ b/src/components/Icons/PersonGroupIcon.jsx @@ -0,0 +1,6 @@ +import Icon from '@ant-design/icons' +import CustomIconSvg from '../../../assets/icons/persongroupicon.svg?react' + +const PersonGroupIcon = (props) => + +export default PersonGroupIcon diff --git a/src/components/Icons/sidebarIconMap.jsx b/src/components/Icons/sidebarIconMap.jsx index 875c858a..c31016f0 100644 --- a/src/components/Icons/sidebarIconMap.jsx +++ b/src/components/Icons/sidebarIconMap.jsx @@ -38,7 +38,8 @@ import SettingsIcon from './SettingsIcon' import AuditLogIcon from './AuditLogIcon' import DeveloperIcon from './DeveloperIcon' import PersonIcon from './PersonIcon' -import PermissionSettingsIcon from './PermissionSettingsIcon' +import PersonGroupIcon from './PersonGroupIcon' +import PermissionSettingIcon from './PermissionSettingIcon' import HostIcon from './HostIcon' import DocumentPrinterIcon from './DocumentPrinterIcon' import DocumentTemplateIcon from './DocumentTemplateIcon' @@ -96,7 +97,8 @@ const sidebarIconMap = { auditLog: , developer: , person: , - permissionSettings: , + userGroup: , + permissionSetting: , host: , documentPrinter: , documentTemplate: , diff --git a/src/database/ObjectModels.js b/src/database/ObjectModels.js index 42041d84..b4a74897 100644 --- a/src/database/ObjectModels.js +++ b/src/database/ObjectModels.js @@ -31,7 +31,8 @@ import { OrderItem } from './models/OrderItem' import { Shipment } from './models/Shipment' import { AuditLog } from './models/AuditLog' import { User } from './models/User' -import { PermissionSettings } from './models/PermissionSettings.js' +import { UserGroup } from './models/UserGroup.js' +import { PermissionSetting } from './models/PermissionSetting.js' import { AppPassword } from './models/AppPassword.js' import { NoteType } from './models/NoteType' import { Note } from './models/Note' @@ -84,7 +85,8 @@ export const objectModels = [ Shipment, AuditLog, User, - PermissionSettings, + UserGroup, + PermissionSetting, AppPassword, NoteType, Note, @@ -138,7 +140,8 @@ export { Shipment, AuditLog, User, - PermissionSettings, + UserGroup, + PermissionSetting, AppPassword, NoteType, Note, diff --git a/src/database/models/PermissionSettings.js b/src/database/models/PermissionSetting.js similarity index 86% rename from src/database/models/PermissionSettings.js rename to src/database/models/PermissionSetting.js index 5b13274e..b38510da 100644 --- a/src/database/models/PermissionSettings.js +++ b/src/database/models/PermissionSetting.js @@ -1,24 +1,24 @@ import { createElement, lazy } from 'react' -const PermissionSettingsInfo = lazy( +const PermissionSettingInfo = lazy( () => - import('../../components/Dashboard/Management/PermissionSettings/PermissionSettingsInfo') + import('../../components/Dashboard/Management/PermissionSettings/PermissionSettingInfo') ) -import PermissionSettingsIcon from '../../components/Icons/PermissionSettingsIcon' +import PermissionSettingIcon from '../../components/Icons/PermissionSettingIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import EditIcon from '../../components/Icons/EditIcon' import CheckIcon from '../../components/Icons/CheckIcon' import XMarkIcon from '../../components/Icons/XMarkIcon' import BinIcon from '../../components/Icons/BinIcon' -export const PermissionSettings = { - name: 'permissionSettings', - label: 'Permission Settings', +export const PermissionSetting = { + name: 'permissionSetting', + label: 'Permission Setting', labelPlural: 'Permission Settings', url: '/dashboard/management/permissionsettings', prefix: 'PMS', endpoint: 'permissionsettings', - icon: PermissionSettingsIcon, + icon: PermissionSettingIcon, actions: [ { name: 'info', @@ -73,7 +73,7 @@ export const PermissionSettings = { pages: [ { name: 'info', - content: () => createElement(PermissionSettingsInfo) + content: () => createElement(PermissionSettingInfo) } ], columns: ['_reference', 'name', 'createdAt', 'updatedAt'], @@ -85,7 +85,7 @@ export const PermissionSettings = { label: 'ID', columnFixed: 'left', type: 'id', - objectType: 'permissionSettings', + objectType: 'permissionSetting', showCopy: true, columnWidth: 140 }, @@ -101,7 +101,7 @@ export const PermissionSettings = { label: 'Reference', type: 'reference', columnFixed: 'left', - objectType: 'permissionSettings', + objectType: 'permissionSetting', showCopy: true, readOnly: true, columnWidth: 180 diff --git a/src/database/models/User.js b/src/database/models/User.js index 92d4985c..37b39322 100644 --- a/src/database/models/User.js +++ b/src/database/models/User.js @@ -13,6 +13,7 @@ import EditIcon from '../../components/Icons/EditIcon' import CheckIcon from '../../components/Icons/CheckIcon' import XMarkIcon from '../../components/Icons/XMarkIcon' import PlusIcon from '../../components/Icons/PlusIcon' +import { calculateUserPermissions } from '../permissions.js' export const User = { name: 'user', @@ -92,6 +93,7 @@ export const User = { 'name', 'username', 'email', + 'groups', 'permissionSettings', 'createdAt', 'updatedAt' @@ -102,6 +104,7 @@ export const User = { 'firstName', 'lastName', 'email', + 'groups', 'permissionSettings', 'createdAt', 'updatedAt', @@ -178,11 +181,19 @@ export const User = { columnWidth: 300, type: 'email' }, + { + name: 'groups', + label: 'Groups', + type: 'objectList', + objectType: 'userGroup', + span: 2, + columnWidth: 250 + }, { name: 'permissionSettings', label: 'Permission Settings', type: 'objectList', - objectType: 'permissionSettings', + objectType: 'permissionSetting', span: 2, columnWidth: 250 }, @@ -193,7 +204,8 @@ export const User = { inherit: false, readOnly: true, span: 2, - columnWidth: 200 + columnWidth: 200, + value: calculateUserPermissions }, { name: 'profileImage', diff --git a/src/database/models/UserGroup.js b/src/database/models/UserGroup.js new file mode 100644 index 00000000..2fe9a53a --- /dev/null +++ b/src/database/models/UserGroup.js @@ -0,0 +1,142 @@ +import { createElement, lazy } from 'react' + +const UserGroupInfo = lazy( + () => import('../../components/Dashboard/Management/UserGroups/UserGroupInfo') +) +import PersonGroupIcon from '../../components/Icons/PersonGroupIcon' +import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import EditIcon from '../../components/Icons/EditIcon' +import CheckIcon from '../../components/Icons/CheckIcon' +import XMarkIcon from '../../components/Icons/XMarkIcon' +import BinIcon from '../../components/Icons/BinIcon' +import { calculateUserGroupPermissions } from '../permissions.js' + +export const UserGroup = { + name: 'userGroup', + label: 'User Group', + labelPlural: 'User Groups', + url: '/dashboard/management/usergroups', + prefix: 'UGP', + icon: PersonGroupIcon, + actions: [ + { + name: 'info', + type: 'page', + pageName: 'info', + label: 'Info', + default: true, + row: true, + icon: InfoCircleIcon + }, + { + name: 'edit', + type: 'page', + pageName: 'info', + label: 'Edit', + row: true, + icon: EditIcon, + visible: (objectData) => { + return !(objectData?._isEditing && objectData?._isEditing == true) + } + }, + { + name: 'cancelEdit', + label: 'Cancel Edits', + type: 'page', + pageName: 'info', + icon: XMarkIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { + name: 'finishEdit', + label: 'Save Edits', + type: 'page', + pageName: 'info', + icon: CheckIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { type: 'divider' }, + { + name: 'delete', + type: 'page', + pageName: 'info', + label: 'Delete', + icon: BinIcon, + danger: true + } + ], + pages: [ + { + name: 'info', + content: () => createElement(UserGroupInfo) + } + ], + columns: ['_reference', 'name', 'permissionSettings', 'createdAt', 'updatedAt'], + filters: ['name', 'permissionSettings', 'createdAt', 'updatedAt', '_reference'], + sorters: ['name', 'createdAt', '_id', 'updatedAt'], + properties: [ + { + name: '_id', + label: 'ID', + columnFixed: 'left', + type: 'id', + objectType: 'userGroup', + showCopy: true, + columnWidth: 140 + }, + { + name: 'createdAt', + label: 'Created At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: '_reference', + label: 'Reference', + columnFixed: 'left', + type: 'reference', + objectType: 'userGroup', + showCopy: true, + readOnly: true, + columnWidth: 180 + }, + { + name: 'updatedAt', + label: 'Updated At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: 'name', + label: 'Name', + columnFixed: 'left', + required: true, + type: 'text', + columnWidth: 200 + }, + { + name: 'permissionSettings', + label: 'Permission Settings', + type: 'objectList', + objectType: 'permissionSetting', + span: 2, + columnWidth: 250 + }, + { + name: 'permissions', + label: 'Permissions', + type: 'permissionsMatrix', + inherit: false, + readOnly: true, + span: 2, + columnWidth: 200, + value: calculateUserGroupPermissions + } + ] +} diff --git a/src/database/permissions.js b/src/database/permissions.js new file mode 100644 index 00000000..52cbf461 --- /dev/null +++ b/src/database/permissions.js @@ -0,0 +1,38 @@ +export const applyPermissionSettingsList = (settingsList = []) => { + const permissions = {} + + for (const setting of settingsList) { + const matrix = setting?.permissions + if (!matrix || typeof matrix !== 'object' || Array.isArray(matrix)) { + continue + } + + for (const [modelName, actions] of Object.entries(matrix)) { + if (!actions || typeof actions !== 'object' || Array.isArray(actions)) { + continue + } + if (!permissions[modelName]) { + permissions[modelName] = {} + } + for (const [actionName, value] of Object.entries(actions)) { + if (value === true || value === false) { + permissions[modelName][actionName] = value + } + } + if (Object.keys(permissions[modelName]).length === 0) { + delete permissions[modelName] + } + } + } + + return permissions +} + +export const calculateUserPermissions = (objectData = {}) => + applyPermissionSettingsList([ + ...(objectData.groups || []), + ...(objectData.permissionSettings || []) + ]) + +export const calculateUserGroupPermissions = (objectData = {}) => + applyPermissionSettingsList(objectData.permissionSettings) diff --git a/src/database/sidebars/management.js b/src/database/sidebars/management.js index a482e579..d81bf317 100644 --- a/src/database/sidebars/management.js +++ b/src/database/sidebars/management.js @@ -125,9 +125,15 @@ const managementSidebarItems = [ label: 'Users', path: '/dashboard/management/users' }, + { + key: 'userGroups', + iconKey: 'userGroup', + label: 'User Groups', + path: '/dashboard/management/usergroups' + }, { key: 'permissionSettings', - iconKey: 'permissionSettings', + iconKey: 'permissionSetting', label: 'Permission Settings', path: '/dashboard/management/permissionsettings' }, diff --git a/src/routes/ManagementRoutes.jsx b/src/routes/ManagementRoutes.jsx index 5206275f..b7c2a5a2 100644 --- a/src/routes/ManagementRoutes.jsx +++ b/src/routes/ManagementRoutes.jsx @@ -43,6 +43,9 @@ const NoteInfo = lazy( () => import('../components/Dashboard/Management/Notes/NoteInfo.jsx') ) const Users = lazy(() => import('../components/Dashboard/Management/Users.jsx')) +const UserGroups = lazy( + () => import('../components/Dashboard/Management/UserGroups.jsx') +) const PermissionSettings = lazy( () => import('../components/Dashboard/Management/PermissionSettings.jsx') ) @@ -95,6 +98,11 @@ const ManagementRoutes = [ } />, } />, } />, + } + />,