robobyte-front-builder 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/jsconfig.json +28 -0
- package/next.config.js +75 -0
- package/package.json +163 -0
- package/public/fonts/NotoSansArabic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/font.js +2 -0
- package/public/fonts/logo.js +2 -0
- package/public/images/appicon.png +0 -0
- package/public/images/appicon1.png +0 -0
- package/public/images/asiacell.png +0 -0
- package/public/images/avatars/1.png +0 -0
- package/public/images/avatars/3.png +0 -0
- package/public/images/avatars/4.png +0 -0
- package/public/images/avatars/5.png +0 -0
- package/public/images/cards/trophy.png +0 -0
- package/public/images/favicon.png +0 -0
- package/public/images/favicon1.png +0 -0
- package/public/images/icons/bag.png +0 -0
- package/public/images/icons/briefcase.png +0 -0
- package/public/images/icons/calendar.png +0 -0
- package/public/images/icons/dashboard.png +0 -0
- package/public/images/icons/dollar.png +0 -0
- package/public/images/icons/person.png +0 -0
- package/public/images/icons/profit.png +0 -0
- package/public/images/icons/setting.png +0 -0
- package/public/images/misc/chart.png +0 -0
- package/public/images/misc/paypal.png +0 -0
- package/public/images/pages/401.png +0 -0
- package/public/images/pages/404.png +0 -0
- package/public/images/pages/500.png +0 -0
- package/public/images/pages/auth-v2-forgot-password-illustration-bordered-dark.png +0 -0
- package/public/images/pages/auth-v2-forgot-password-illustration-bordered-light.png +0 -0
- package/public/images/pages/auth-v2-forgot-password-illustration-dark.png +0 -0
- package/public/images/pages/auth-v2-forgot-password-illustration-light.png +0 -0
- package/public/images/pages/auth-v2-forgot-password-mask-dark.png +0 -0
- package/public/images/pages/auth-v2-forgot-password-mask-light.png +0 -0
- package/public/images/pages/auth-v2-login-illustration-bordered-dark.png +0 -0
- package/public/images/pages/auth-v2-login-illustration-bordered-light.png +0 -0
- package/public/images/pages/auth-v2-login-illustration-dark.png +0 -0
- package/public/images/pages/auth-v2-login-illustration-light.png +0 -0
- package/public/images/pages/auth-v2-login-mask-dark.png +0 -0
- package/public/images/pages/auth-v2-login-mask-light.png +0 -0
- package/public/images/pages/auth-v2-register-illustration-bordered-dark.png +0 -0
- package/public/images/pages/auth-v2-register-illustration-bordered-light.png +0 -0
- package/public/images/pages/auth-v2-register-illustration-dark.png +0 -0
- package/public/images/pages/auth-v2-register-illustration-light.png +0 -0
- package/public/images/pages/auth-v2-register-mask-dark.png +0 -0
- package/public/images/pages/auth-v2-register-mask-light.png +0 -0
- package/public/images/pages/misc-401-object.png +0 -0
- package/public/images/pages/misc-404-object.png +0 -0
- package/public/images/pages/misc-500-object.png +0 -0
- package/public/images/pages/misc-coming-soon-object.png +0 -0
- package/public/images/pages/misc-mask-dark.png +0 -0
- package/public/images/pages/misc-mask-light.png +0 -0
- package/public/images/white.jpg +0 -0
- package/public/images/zain.png +0 -0
- package/public/locales/ar.json +35 -0
- package/public/navigationTest/Navigation.json +13 -0
- package/public/vercel.svg +4 -0
- package/src/@core/components/auth/AclGuard.js +55 -0
- package/src/@core/components/auth/AuthGuard.js +40 -0
- package/src/@core/components/auth/GuestGuard.js +30 -0
- package/src/@core/components/custom-inputs/Horizontal.jsx +143 -0
- package/src/@core/components/custom-inputs/Image.jsx +78 -0
- package/src/@core/components/custom-inputs/Vertical.jsx +113 -0
- package/src/@core/components/customizer/index.jsx +470 -0
- package/src/@core/components/customizer/styles.module.css +169 -0
- package/src/@core/components/mui/Avatar.jsx +41 -0
- package/src/@core/components/mui/Badge.jsx +20 -0
- package/src/@core/components/mui/IconButton.jsx +74 -0
- package/src/@core/components/mui/TabList.jsx +60 -0
- package/src/@core/components/option-menu/index.jsx +137 -0
- package/src/@core/components/scroll-to-top/index.jsx +43 -0
- package/src/@core/components/spinner/index.js +26 -0
- package/src/@core/components/window-wrapper/index.js +27 -0
- package/src/@core/contexts/settingsContext.jsx +98 -0
- package/src/@core/hooks/useBgColor.js +63 -0
- package/src/@core/hooks/useImageVariant.js +27 -0
- package/src/@core/hooks/useLayoutInit.js +37 -0
- package/src/@core/hooks/useObjectCookie.js +18 -0
- package/src/@core/hooks/useSettings.jsx +15 -0
- package/src/@core/layouts/BlankLayout.js +37 -0
- package/src/@core/layouts/BlankLayoutWithAppBar.js +51 -0
- package/src/@core/layouts/HorizontalLayout.jsx +151 -0
- package/src/@core/layouts/Layout.js +39 -0
- package/src/@core/layouts/VerticalLayout.jsx +124 -0
- package/src/@core/layouts/components/blank-layout-with-appBar/index.js +115 -0
- package/src/@core/layouts/components/horizontal/app-bar-content/index.js +67 -0
- package/src/@core/layouts/components/horizontal/navigation/HorizontalNavGroup.js +352 -0
- package/src/@core/layouts/components/horizontal/navigation/HorizontalNavItems.js +21 -0
- package/src/@core/layouts/components/horizontal/navigation/HorizontalNavLink.js +195 -0
- package/src/@core/layouts/components/horizontal/navigation/index.js +31 -0
- package/src/@core/layouts/components/shared-components/LanguageDropdown.js +96 -0
- package/src/@core/layouts/components/shared-components/ModeToggler.js +32 -0
- package/src/@core/layouts/components/shared-components/NotificationDropdown.js +226 -0
- package/src/@core/layouts/components/shared-components/UserDropdown.js +177 -0
- package/src/@core/layouts/components/shared-components/footer/FooterContent.js +46 -0
- package/src/@core/layouts/components/shared-components/footer/index.js +61 -0
- package/src/@core/layouts/components/vertical/appBar/index.js +74 -0
- package/src/@core/layouts/components/vertical/navigation/Drawer.js +122 -0
- package/src/@core/layouts/components/vertical/navigation/VerticalNavGroup.js +435 -0
- package/src/@core/layouts/components/vertical/navigation/VerticalNavHeader.js +180 -0
- package/src/@core/layouts/components/vertical/navigation/VerticalNavItems.js +26 -0
- package/src/@core/layouts/components/vertical/navigation/VerticalNavLink.js +258 -0
- package/src/@core/layouts/components/vertical/navigation/VerticalNavSectionTitle.js +102 -0
- package/src/@core/layouts/components/vertical/navigation/index.js +169 -0
- package/src/@core/layouts/utils.js +69 -0
- package/src/@core/styles/Table.module.css +93 -0
- package/src/@core/styles/horizontal/menuItemStyles.js +100 -0
- package/src/@core/styles/horizontal/menuRootStyles.js +19 -0
- package/src/@core/styles/libs/fullcalendar/index.js +461 -0
- package/src/@core/styles/libs/keen-slider/index.js +111 -0
- package/src/@core/styles/libs/react-apexcharts/index.js +107 -0
- package/src/@core/styles/libs/react-cleave/index.js +33 -0
- package/src/@core/styles/libs/react-credit-cards/index.js +11 -0
- package/src/@core/styles/libs/react-datepicker/index.js +388 -0
- package/src/@core/styles/libs/react-draft-wysiwyg/index.js +144 -0
- package/src/@core/styles/libs/react-dropzone/index.js +76 -0
- package/src/@core/styles/libs/react-hot-toast/index.js +37 -0
- package/src/@core/styles/libs/recharts/index.js +47 -0
- package/src/@core/styles/stepper.js +103 -0
- package/src/@core/styles/vertical/menuItemStyles.js +138 -0
- package/src/@core/styles/vertical/menuSectionStyles.js +54 -0
- package/src/@core/styles/vertical/navigationCustomStyles.js +62 -0
- package/src/@core/svg/ContentCompact.jsx +17 -0
- package/src/@core/svg/ContentWide.jsx +17 -0
- package/src/@core/svg/DirectionLtr.jsx +93 -0
- package/src/@core/svg/DirectionRtl.jsx +93 -0
- package/src/@core/svg/LayoutCollapsed.jsx +59 -0
- package/src/@core/svg/LayoutHorizontal.jsx +42 -0
- package/src/@core/svg/LayoutVertical.jsx +59 -0
- package/src/@core/svg/Logo.jsx +76 -0
- package/src/@core/svg/SkinBordered.jsx +54 -0
- package/src/@core/svg/SkinDefault.jsx +59 -0
- package/src/@core/tailwind/plugin.js +78 -0
- package/src/@core/theme/ThemeComponent.js +63 -0
- package/src/@core/theme/ThemeOptions.js +71 -0
- package/src/@core/theme/breakpoints/index.js +11 -0
- package/src/@core/theme/colorSchemes.js +326 -0
- package/src/@core/theme/customShadows.js +11 -0
- package/src/@core/theme/globalStyles.js +81 -0
- package/src/@core/theme/index.js +42 -0
- package/src/@core/theme/overrides/accordion.js +51 -0
- package/src/@core/theme/overrides/accordion.jsx +85 -0
- package/src/@core/theme/overrides/alerts.js +110 -0
- package/src/@core/theme/overrides/alerts.jsx +180 -0
- package/src/@core/theme/overrides/autocomplete.js +14 -0
- package/src/@core/theme/overrides/autocomplete.jsx +68 -0
- package/src/@core/theme/overrides/avatar.js +38 -0
- package/src/@core/theme/overrides/avatars.js +27 -0
- package/src/@core/theme/overrides/backdrop.js +22 -0
- package/src/@core/theme/overrides/badges.js +16 -0
- package/src/@core/theme/overrides/breadcrumbs.js +11 -0
- package/src/@core/theme/overrides/button-group.js +84 -0
- package/src/@core/theme/overrides/button.js +93 -0
- package/src/@core/theme/overrides/buttonGroup.js +9 -0
- package/src/@core/theme/overrides/card.js +83 -0
- package/src/@core/theme/overrides/checkbox.jsx +95 -0
- package/src/@core/theme/overrides/chip.js +72 -0
- package/src/@core/theme/overrides/dataGrid.js +114 -0
- package/src/@core/theme/overrides/dateTimePicker.js +65 -0
- package/src/@core/theme/overrides/dialog.js +120 -0
- package/src/@core/theme/overrides/divider.js +13 -0
- package/src/@core/theme/overrides/drawer.js +20 -0
- package/src/@core/theme/overrides/fab.js +13 -0
- package/src/@core/theme/overrides/form-control-label.js +19 -0
- package/src/@core/theme/overrides/icon-button.js +145 -0
- package/src/@core/theme/overrides/index.js +103 -0
- package/src/@core/theme/overrides/input.js +72 -0
- package/src/@core/theme/overrides/link.js +9 -0
- package/src/@core/theme/overrides/list.js +44 -0
- package/src/@core/theme/overrides/menu.js +25 -0
- package/src/@core/theme/overrides/pagination.js +41 -0
- package/src/@core/theme/overrides/paper.js +9 -0
- package/src/@core/theme/overrides/popover.js +16 -0
- package/src/@core/theme/overrides/progress.js +38 -0
- package/src/@core/theme/overrides/radio.jsx +80 -0
- package/src/@core/theme/overrides/rating.js +16 -0
- package/src/@core/theme/overrides/rating.jsx +32 -0
- package/src/@core/theme/overrides/select.js +19 -0
- package/src/@core/theme/overrides/select.jsx +52 -0
- package/src/@core/theme/overrides/slider.js +97 -0
- package/src/@core/theme/overrides/snackbar.js +19 -0
- package/src/@core/theme/overrides/switch.js +73 -0
- package/src/@core/theme/overrides/switches.js +25 -0
- package/src/@core/theme/overrides/table-pagination.js +39 -0
- package/src/@core/theme/overrides/table.js +81 -0
- package/src/@core/theme/overrides/tabs.js +30 -0
- package/src/@core/theme/overrides/timeline.js +80 -0
- package/src/@core/theme/overrides/toggle-button.js +33 -0
- package/src/@core/theme/overrides/toggleButton.js +16 -0
- package/src/@core/theme/overrides/tooltip.js +21 -0
- package/src/@core/theme/overrides/typography.js +13 -0
- package/src/@core/theme/palette/index.js +107 -0
- package/src/@core/theme/shadows/index.js +61 -0
- package/src/@core/theme/shadows.js +12 -0
- package/src/@core/theme/spacing/index.js +3 -0
- package/src/@core/theme/spacing.js +5 -0
- package/src/@core/theme/typography/index.js +65 -0
- package/src/@core/theme/typography.js +84 -0
- package/src/@core/utils/create-emotion-cache.js +5 -0
- package/src/@core/utils/hex-to-rgba.js +11 -0
- package/src/@core/utils/serverHelpers.js +45 -0
- package/src/@menu/components/RouterLink.jsx +18 -0
- package/src/@menu/components/horizontal-menu/HorizontalNav.jsx +88 -0
- package/src/@menu/components/horizontal-menu/Menu.jsx +83 -0
- package/src/@menu/components/horizontal-menu/MenuButton.jsx +100 -0
- package/src/@menu/components/horizontal-menu/MenuItem.jsx +183 -0
- package/src/@menu/components/horizontal-menu/SubMenu.jsx +418 -0
- package/src/@menu/components/horizontal-menu/SubMenuContent.jsx +41 -0
- package/src/@menu/components/horizontal-menu/VerticalNavInHorizontal.jsx +20 -0
- package/src/@menu/components/vertical-menu/Menu.jsx +161 -0
- package/src/@menu/components/vertical-menu/MenuButton.jsx +95 -0
- package/src/@menu/components/vertical-menu/MenuItem.jsx +180 -0
- package/src/@menu/components/vertical-menu/MenuSection.jsx +124 -0
- package/src/@menu/components/vertical-menu/NavCollapseIcons.jsx +70 -0
- package/src/@menu/components/vertical-menu/NavHeader.jsx +39 -0
- package/src/@menu/components/vertical-menu/SubMenu.jsx +420 -0
- package/src/@menu/components/vertical-menu/SubMenuContent.jsx +101 -0
- package/src/@menu/components/vertical-menu/VerticalNav.jsx +216 -0
- package/src/@menu/contexts/horizontalNavContext.jsx +29 -0
- package/src/@menu/contexts/verticalNavContext.jsx +65 -0
- package/src/@menu/defaultConfigs.js +12 -0
- package/src/@menu/hooks/useHorizontalMenu.jsx +19 -0
- package/src/@menu/hooks/useHorizontalNav.jsx +19 -0
- package/src/@menu/hooks/useMediaQuery.jsx +29 -0
- package/src/@menu/hooks/useVerticalMenu.jsx +19 -0
- package/src/@menu/hooks/useVerticalNav.jsx +19 -0
- package/src/@menu/horizontal-menu/index.jsx +8 -0
- package/src/@menu/styles/StyledBackdrop.jsx +15 -0
- package/src/@menu/styles/StyledMenuIcon.jsx +12 -0
- package/src/@menu/styles/StyledMenuLabel.jsx +16 -0
- package/src/@menu/styles/StyledMenuPrefix.jsx +10 -0
- package/src/@menu/styles/StyledMenuSectionLabel.jsx +21 -0
- package/src/@menu/styles/StyledMenuSuffix.jsx +10 -0
- package/src/@menu/styles/StyledSubMenuContent.jsx +43 -0
- package/src/@menu/styles/horizontal/StyledHorizontalMenu.jsx +13 -0
- package/src/@menu/styles/horizontal/StyledHorizontalMenuItem.jsx +26 -0
- package/src/@menu/styles/horizontal/StyledHorizontalNav.jsx +11 -0
- package/src/@menu/styles/horizontal/StyledHorizontalNavExpandIcon.jsx +33 -0
- package/src/@menu/styles/horizontal/StyledHorizontalSubMenuContent.jsx +18 -0
- package/src/@menu/styles/horizontal/StyledHorizontalSubMenuContentWrapper.jsx +10 -0
- package/src/@menu/styles/horizontal/horizontalUl.module.css +15 -0
- package/src/@menu/styles/styles.module.css +5 -0
- package/src/@menu/styles/vertical/StyledVerticalMenu.jsx +16 -0
- package/src/@menu/styles/vertical/StyledVerticalMenuItem.jsx +28 -0
- package/src/@menu/styles/vertical/StyledVerticalMenuSection.jsx +23 -0
- package/src/@menu/styles/vertical/StyledVerticalNav.jsx +67 -0
- package/src/@menu/styles/vertical/StyledVerticalNavBgColorContainer.jsx +15 -0
- package/src/@menu/styles/vertical/StyledVerticalNavContainer.jsx +23 -0
- package/src/@menu/styles/vertical/StyledVerticalNavExpandIcon.jsx +25 -0
- package/src/@menu/styles/vertical/verticalNavBgImage.module.css +10 -0
- package/src/@menu/svg/ChevronRight.jsx +9 -0
- package/src/@menu/svg/Close.jsx +12 -0
- package/src/@menu/svg/RadioCircle.jsx +12 -0
- package/src/@menu/svg/RadioCircleMarked.jsx +13 -0
- package/src/@menu/utils/menuClasses.js +44 -0
- package/src/@menu/utils/menuUtils.jsx +145 -0
- package/src/@menu/vertical-menu/index.jsx +11 -0
- package/src/configs/Permissions/PermissionsActions.json +6 -0
- package/src/configs/Permissions/PermissionsSubjects.json +107 -0
- package/src/configs/acl.js +115 -0
- package/src/configs/auth.js +5 -0
- package/src/configs/aws-exports.js +30 -0
- package/src/configs/firebase.js +25 -0
- package/src/configs/i18n.js +34 -0
- package/src/configs/izColors.json +11 -0
- package/src/configs/primaryColorConfig.js +35 -0
- package/src/configs/themeConfig.js +44 -0
- package/src/context/AuthContext.js +179 -0
- package/src/context/BuilderContext.jsx +209 -0
- package/src/context/SystemContext.js +99 -0
- package/src/hooks/useAuth.js +4 -0
- package/src/layouts/UserLayout.js +94 -0
- package/src/layouts/UserThemeOptions.js +191 -0
- package/src/layouts/components/Direction.js +30 -0
- package/src/layouts/components/HtmlTooltip.js +15 -0
- package/src/layouts/components/Translations.js +11 -0
- package/src/layouts/components/UserDropdown.js +217 -0
- package/src/layouts/components/UserIcon.js +40 -0
- package/src/layouts/components/acl/Can.js +6 -0
- package/src/layouts/components/acl/CanViewNavGroup.js +36 -0
- package/src/layouts/components/acl/CanViewNavLink.js +17 -0
- package/src/layouts/components/acl/CanViewNavSectionTitle.js +17 -0
- package/src/layouts/components/horizontal/AppBarContent.js +39 -0
- package/src/layouts/components/horizontal/ServerSideNavItems.js +44 -0
- package/src/layouts/components/mui/StepperComps.js +55 -0
- package/src/layouts/components/vertical/AppBarContent.js +35 -0
- package/src/layouts/components/vertical/ServerSideNavItems.js +44 -0
- package/src/lib/index.js +75 -0
- package/src/lib/navigation/NavigationExtensionContext.jsx +81 -0
- package/src/lib/navigation/mergeNavExtensions.js +66 -0
- package/src/lib/navigation/useNavExtension.js +54 -0
- package/src/lib/providers/RoboByteFrontBuilderProvider.jsx +57 -0
- package/src/libs/ApexCharts.jsx +5 -0
- package/src/libs/ReactPlayer.jsx +5 -0
- package/src/libs/Recharts.jsx +4 -0
- package/src/libs/auth.js +124 -0
- package/src/libs/styles/AppFullCalendar.js +505 -0
- package/src/libs/styles/AppKeenSlider.js +116 -0
- package/src/libs/styles/AppReactApexCharts.jsx +110 -0
- package/src/libs/styles/AppReactDatepicker.jsx +470 -0
- package/src/libs/styles/AppReactDropzone.js +76 -0
- package/src/libs/styles/AppReactToastify.jsx +108 -0
- package/src/libs/styles/AppRecharts.js +55 -0
- package/src/libs/styles/inputOtp.module.css +39 -0
- package/src/libs/styles/tiptapEditor.css +72 -0
- package/src/navigation/horizontal/index.js +246 -0
- package/src/navigation/vertical/index.js +253 -0
- package/src/pages/401.js +70 -0
- package/src/pages/404.js +67 -0
- package/src/pages/500.js +68 -0
- package/src/pages/[slug].js +115 -0
- package/src/pages/_app.js +148 -0
- package/src/pages/_document.js +72 -0
- package/src/pages/api/navigation/regenerate-registry.js +116 -0
- package/src/pages/api/navigation/save.js +218 -0
- package/src/pages/authModule/acl/index.js +48 -0
- package/src/pages/authModule/forgot-password/index.js +228 -0
- package/src/pages/authModule/permissions/rolePermissions/[id]/rolePermissionsUser/index.js +392 -0
- package/src/pages/authModule/permissions/rolePermissions/index.js +343 -0
- package/src/pages/authModule/permissions/systemPermissions/index.js +354 -0
- package/src/pages/authModule/privacy/index.js +721 -0
- package/src/pages/authModule/users/index.js +210 -0
- package/src/pages/index.js +44 -0
- package/src/pages/login/index.js +328 -0
- package/src/pages/mainHome/index.js +181 -0
- package/src/pages/navigatorBuilder/index.jsx +2070 -0
- package/src/pages/reportModule/reportBuilder/index.js +1361 -0
- package/src/pages/reportModule/reportBuilder/reportViewer/index.js +187 -0
- package/src/pages/reportModule/reportBuilder/reports/index.js +198 -0
- package/src/pages/viewBuilder/index.jsx +94 -0
- package/src/pages/viewBuilder/viewPage/index.jsx +46 -0
- package/src/pages/viewBuilder/views/index.js +146 -0
- package/src/pages/viewer/[id]/index.js +88 -0
- package/src/services/ContentTypes.js +10 -0
- package/src/services/DeleteService.js +77 -0
- package/src/services/Endpoints/BunnyEndpoints.js +26 -0
- package/src/services/Endpoints/CaseEndpoints.js +29 -0
- package/src/services/Endpoints/CaseTypeEndpoints.js +29 -0
- package/src/services/Endpoints/ContactsEndpoints.js +81 -0
- package/src/services/Endpoints/CourtEndpoints.js +29 -0
- package/src/services/Endpoints/FilterEndpoints.js +31 -0
- package/src/services/Endpoints/NavigatorEndpoints.js +21 -0
- package/src/services/Endpoints/ReportBuilderEndpoints.js +45 -0
- package/src/services/Endpoints/UiBuilderEndpoints.js +37 -0
- package/src/services/Endpoints/UserTableTemplateEndpoints.js +26 -0
- package/src/services/Endpoints/UsersEndpoints.js +142 -0
- package/src/services/Endpoints/ViewEndpoints.js +28 -0
- package/src/services/Endpoints/ViewPermissionEndpoints.js +37 -0
- package/src/services/Endpoints/ViewPermissionRoleEndpoints.js +39 -0
- package/src/services/Endpoints/ViewPermissionRoleItemsEndpoints.js +32 -0
- package/src/services/Endpoints/ViewPermissionRoleUsersEndpoints.js +28 -0
- package/src/services/Endpoints/WidgetEndpoints.js +28 -0
- package/src/services/Endpoints.js +49 -0
- package/src/services/External/BunnyCdn/BunnyUploadFile.js +168 -0
- package/src/services/GetService.js +67 -0
- package/src/services/MetaDataTemplate/index.js +5 -0
- package/src/services/PatchService.js +83 -0
- package/src/services/PostService.js +81 -0
- package/src/services/StreamService.js +82 -0
- package/src/services/UpdateService.js +82 -0
- package/src/services/auth/AuthService.js +47 -0
- package/src/services/auth/GetUsersService.js +38 -0
- package/src/services/auth/PostRegisterUser.js +29 -0
- package/src/services/auth/PostResetPasswordService.js +28 -0
- package/src/services/auth/PutAccountService.js +29 -0
- package/src/services/auth/PutRefreshToken.js +69 -0
- package/src/services/auth/RefreshToken.js +0 -0
- package/src/services/builderHelper/actionExecutor.js +73 -0
- package/src/services/builderHelper/builderHelper.js +99 -0
- package/src/services/builderHelper/index.js +2 -0
- package/src/services/builderHelper/jsExecutor.js +115 -0
- package/src/services/builderHelper/layoutHelpers.js +231 -0
- package/src/services/builderHelper/nodeFactory.js +44 -0
- package/src/services/builderHelper/resolveProps.js +34 -0
- package/src/services/builderHelper/tree.js +131 -0
- package/src/services/components/UniversalNestedAutocomplete.js +331 -0
- package/src/services/components/agGridAutoComplete.js +172 -0
- package/src/services/components/universalAutoComplete.js +207 -0
- package/src/services/enums/ReportBuilderTypeEnum.js +18 -0
- package/src/services/helper/FilterFormat.js +70 -0
- package/src/services/helper/FormatCurrencyIQD.js +11 -0
- package/src/services/helper/datagridEditComponents.js +38 -0
- package/src/services/helper/dateFormat.js +30 -0
- package/src/services/helper/formData.js +21 -0
- package/src/services/helper/getFieldByType.js +0 -0
- package/src/services/helper/getPropByString.js +18 -0
- package/src/services/helper/handleChange.js +26 -0
- package/src/services/helper/helper.js +105 -0
- package/src/services/helper/multiSelectEditor.js +226 -0
- package/src/services/helper/translateOrderReturnStatus.js +12 -0
- package/src/services/helper/translateRole.js +24 -0
- package/src/services/helper/translateStatus.js +12 -0
- package/src/services/helper/translateTransferReceiptType.js +67 -0
- package/src/services/helper/translsateStoreProductTransfer.js +40 -0
- package/src/services/helper/translsateStoreType.js +25 -0
- package/src/services/helper/useInterval.js +21 -0
- package/src/services/helper/yupCutomization.js +15 -0
- package/src/services/reportData/fetchReportData.js +210 -0
- package/src/views/ConfirmDialog.js +178 -0
- package/src/views/builder/JSEditor.js +226 -0
- package/src/views/builder/inspector/Inspector.jsx +63 -0
- package/src/views/builder/inspector/Tabs/ComponentActionsTab.jsx +117 -0
- package/src/views/builder/inspector/Tabs/MainTab.jsx +95 -0
- package/src/views/builder/inspector/Tabs/RulesTab.jsx +79 -0
- package/src/views/builder/inspector/Tabs/StyleTab.jsx +79 -0
- package/src/views/builder/inspector/definitions/autocomplete/main.js +25 -0
- package/src/views/builder/inspector/definitions/breadcrumb/main.js +9 -0
- package/src/views/builder/inspector/definitions/button/actions.js +12 -0
- package/src/views/builder/inspector/definitions/button/main.js +14 -0
- package/src/views/builder/inspector/definitions/button/rules.js +5 -0
- package/src/views/builder/inspector/definitions/button/style.js +32 -0
- package/src/views/builder/inspector/definitions/button.actions.js +12 -0
- package/src/views/builder/inspector/definitions/card/main.js +11 -0
- package/src/views/builder/inspector/definitions/cell/main.js +4 -0
- package/src/views/builder/inspector/definitions/checkbox/actions.js +1 -0
- package/src/views/builder/inspector/definitions/checkbox/main.js +13 -0
- package/src/views/builder/inspector/definitions/checkbox/rules.js +8 -0
- package/src/views/builder/inspector/definitions/checkbox/style.js +38 -0
- package/src/views/builder/inspector/definitions/checkboxFields.js +0 -0
- package/src/views/builder/inspector/definitions/column/main.js +9 -0
- package/src/views/builder/inspector/definitions/column-group/main.js +18 -0
- package/src/views/builder/inspector/definitions/common/actions.js +1 -0
- package/src/views/builder/inspector/definitions/common/main.js +14 -0
- package/src/views/builder/inspector/definitions/common/rules.js +8 -0
- package/src/views/builder/inspector/definitions/common/style.js +39 -0
- package/src/views/builder/inspector/definitions/common.advanced.js +8 -0
- package/src/views/builder/inspector/definitions/common.main.js +14 -0
- package/src/views/builder/inspector/definitions/common.style.js +33 -0
- package/src/views/builder/inspector/definitions/commonMainFields.js +18 -0
- package/src/views/builder/inspector/definitions/container/actions.js +1 -0
- package/src/views/builder/inspector/definitions/container/main.js +4 -0
- package/src/views/builder/inspector/definitions/container/rules.js +4 -0
- package/src/views/builder/inspector/definitions/container/style.js +32 -0
- package/src/views/builder/inspector/definitions/datepicker/actions.js +1 -0
- package/src/views/builder/inspector/definitions/datepicker/main.js +28 -0
- package/src/views/builder/inspector/definitions/datepicker/rules.js +8 -0
- package/src/views/builder/inspector/definitions/datepicker/style.js +38 -0
- package/src/views/builder/inspector/definitions/datepicker.main.js +28 -0
- package/src/views/builder/inspector/definitions/divider/actions.js +1 -0
- package/src/views/builder/inspector/definitions/divider/main.js +23 -0
- package/src/views/builder/inspector/definitions/divider/rules.js +4 -0
- package/src/views/builder/inspector/definitions/divider/style.js +9 -0
- package/src/views/builder/inspector/definitions/dropdown/actions.js +10 -0
- package/src/views/builder/inspector/definitions/dropdown/main.js +19 -0
- package/src/views/builder/inspector/definitions/dropdown/rules.js +8 -0
- package/src/views/builder/inspector/definitions/dropdown/style.js +38 -0
- package/src/views/builder/inspector/definitions/header/actions.js +1 -0
- package/src/views/builder/inspector/definitions/header/main.js +29 -0
- package/src/views/builder/inspector/definitions/header/rules.js +4 -0
- package/src/views/builder/inspector/definitions/header/style.js +12 -0
- package/src/views/builder/inspector/definitions/header-cell/main.js +5 -0
- package/src/views/builder/inspector/definitions/image/actions.js +1 -0
- package/src/views/builder/inspector/definitions/image/main.js +13 -0
- package/src/views/builder/inspector/definitions/image/rules.js +4 -0
- package/src/views/builder/inspector/definitions/image/style.js +12 -0
- package/src/views/builder/inspector/definitions/index.js +407 -0
- package/src/views/builder/inspector/definitions/input/actions.js +1 -0
- package/src/views/builder/inspector/definitions/input/main.js +14 -0
- package/src/views/builder/inspector/definitions/input/rules.js +8 -0
- package/src/views/builder/inspector/definitions/input/style.js +32 -0
- package/src/views/builder/inspector/definitions/label/actions.js +1 -0
- package/src/views/builder/inspector/definitions/label/main.js +24 -0
- package/src/views/builder/inspector/definitions/label/rules.js +4 -0
- package/src/views/builder/inspector/definitions/label/style.js +12 -0
- package/src/views/builder/inspector/definitions/layout/actions.js +1 -0
- package/src/views/builder/inspector/definitions/layout/main.js +6 -0
- package/src/views/builder/inspector/definitions/layout/rules.js +3 -0
- package/src/views/builder/inspector/definitions/layout/style.js +7 -0
- package/src/views/builder/inspector/definitions/layout-cell/actions.js +1 -0
- package/src/views/builder/inspector/definitions/layout-cell/main.js +7 -0
- package/src/views/builder/inspector/definitions/layout-cell/rules.js +3 -0
- package/src/views/builder/inspector/definitions/layout-cell/style.js +44 -0
- package/src/views/builder/inspector/definitions/link/actions.js +1 -0
- package/src/views/builder/inspector/definitions/link/main.js +29 -0
- package/src/views/builder/inspector/definitions/link/rules.js +4 -0
- package/src/views/builder/inspector/definitions/link/style.js +11 -0
- package/src/views/builder/inspector/definitions/menu/actions.js +3 -0
- package/src/views/builder/inspector/definitions/menu/main.js +16 -0
- package/src/views/builder/inspector/definitions/menu/rules.js +4 -0
- package/src/views/builder/inspector/definitions/menu/style.js +33 -0
- package/src/views/builder/inspector/definitions/number/actions.js +10 -0
- package/src/views/builder/inspector/definitions/number/main.js +29 -0
- package/src/views/builder/inspector/definitions/number/rules.js +8 -0
- package/src/views/builder/inspector/definitions/number/style.js +32 -0
- package/src/views/builder/inspector/definitions/progress-circle/actions.js +1 -0
- package/src/views/builder/inspector/definitions/progress-circle/main.js +18 -0
- package/src/views/builder/inspector/definitions/progress-circle/rules.js +4 -0
- package/src/views/builder/inspector/definitions/progress-circle/style.js +5 -0
- package/src/views/builder/inspector/definitions/progress-line/actions.js +1 -0
- package/src/views/builder/inspector/definitions/progress-line/main.js +16 -0
- package/src/views/builder/inspector/definitions/progress-line/rules.js +4 -0
- package/src/views/builder/inspector/definitions/progress-line/style.js +6 -0
- package/src/views/builder/inspector/definitions/radio/actions.js +10 -0
- package/src/views/builder/inspector/definitions/radio/main.js +16 -0
- package/src/views/builder/inspector/definitions/radio/rules.js +8 -0
- package/src/views/builder/inspector/definitions/radio/style.js +32 -0
- package/src/views/builder/inspector/definitions/reportViewer/main.js +12 -0
- package/src/views/builder/inspector/definitions/richtext/actions.js +10 -0
- package/src/views/builder/inspector/definitions/richtext/main.js +21 -0
- package/src/views/builder/inspector/definitions/richtext/rules.js +8 -0
- package/src/views/builder/inspector/definitions/richtext/style.js +32 -0
- package/src/views/builder/inspector/definitions/signature/actions.js +1 -0
- package/src/views/builder/inspector/definitions/signature/main.js +13 -0
- package/src/views/builder/inspector/definitions/signature/rules.js +8 -0
- package/src/views/builder/inspector/definitions/signature/style.js +26 -0
- package/src/views/builder/inspector/definitions/table/main.js +9 -0
- package/src/views/builder/inspector/definitions/tag/actions.js +10 -0
- package/src/views/builder/inspector/definitions/tag/main.js +17 -0
- package/src/views/builder/inspector/definitions/tag/rules.js +8 -0
- package/src/views/builder/inspector/definitions/tag/style.js +32 -0
- package/src/views/builder/inspector/definitions/textarea/actions.js +1 -0
- package/src/views/builder/inspector/definitions/textarea/main.js +16 -0
- package/src/views/builder/inspector/definitions/textarea/rules.js +8 -0
- package/src/views/builder/inspector/definitions/textarea/style.js +32 -0
- package/src/views/builder/inspector/definitions/time/actions.js +10 -0
- package/src/views/builder/inspector/definitions/time/main.js +18 -0
- package/src/views/builder/inspector/definitions/time/rules.js +8 -0
- package/src/views/builder/inspector/definitions/time/style.js +32 -0
- package/src/views/builder/inspector/definitions/toggle/actions.js +10 -0
- package/src/views/builder/inspector/definitions/toggle/main.js +26 -0
- package/src/views/builder/inspector/definitions/toggle/rules.js +8 -0
- package/src/views/builder/inspector/definitions/toggle/style.js +32 -0
- package/src/views/builder/inspector/fields/BooleanEditor.jsx +30 -0
- package/src/views/builder/inspector/fields/ExpressionEditor.jsx +35 -0
- package/src/views/builder/inspector/fields/FieldWrapper.jsx +15 -0
- package/src/views/builder/inspector/fields/ItemsEditor.jsx +118 -0
- package/src/views/builder/inspector/fields/OptionsEditor.jsx +118 -0
- package/src/views/builder/inspector/fields/SelectEditor.jsx +32 -0
- package/src/views/builder/inspector/fields/TabsEditor.jsx +128 -0
- package/src/views/builder/inspector/fields/TextEditor.jsx +28 -0
- package/src/views/builder/inspector/fields/TextFieldEditor.jsx +0 -0
- package/src/views/builder/sidebar/Sidebar.jsx +20 -0
- package/src/views/builder/sidebar/SidebarTabs.jsx +50 -0
- package/src/views/builder/sidebar/tabs/ActionsTab.jsx +94 -0
- package/src/views/builder/sidebar/tabs/Components/ComponentItem.jsx +69 -0
- package/src/views/builder/sidebar/tabs/Components/ComponentsTab.jsx +89 -0
- package/src/views/builder/sidebar/tabs/Components/GroupLevelAutocomplete.jsx +269 -0
- package/src/views/builder/sidebar/tabs/Components/componentCatalog.js +69 -0
- package/src/views/builder/sidebar/tabs/TreeTab.jsx +205 -0
- package/src/views/builder/sidebar/tabs/ViewTab.jsx +121 -0
- package/src/views/builder/viewer/Canvas.jsx +7 -0
- package/src/views/builder/viewer/ComponentRenderer.jsx +166 -0
- package/src/views/builder/viewer/DropZone.jsx +60 -0
- package/src/views/builder/viewer/ProductionViewer.jsx +155 -0
- package/src/views/builder/viewer/SaveViewDialog.jsx +60 -0
- package/src/views/builder/viewer/SaveWidgetDialog.jsx +142 -0
- package/src/views/builder/viewer/Viewer.jsx +83 -0
- package/src/views/builder/viewer/ViewerComponentWrapper.jsx +238 -0
- package/src/views/builder/viewer/ViewerToolbar.jsx +89 -0
- package/src/views/builder/viewer/renderers/AutoCompleteRenderer.jsx +57 -0
- package/src/views/builder/viewer/renderers/BreadcrumbRenderer.jsx +32 -0
- package/src/views/builder/viewer/renderers/ButtonRenderer.jsx +55 -0
- package/src/views/builder/viewer/renderers/CardRenderer.jsx +76 -0
- package/src/views/builder/viewer/renderers/CellRenderer.jsx +71 -0
- package/src/views/builder/viewer/renderers/CheckboxRenderer.jsx +27 -0
- package/src/views/builder/viewer/renderers/ColumnGroupRenderer.jsx +96 -0
- package/src/views/builder/viewer/renderers/ColumnRenderer.jsx +71 -0
- package/src/views/builder/viewer/renderers/ContainerRenderer.jsx +111 -0
- package/src/views/builder/viewer/renderers/DatePickerRenderer.jsx +49 -0
- package/src/views/builder/viewer/renderers/DividerRenderer.jsx +22 -0
- package/src/views/builder/viewer/renderers/DropdownRenderer.jsx +63 -0
- package/src/views/builder/viewer/renderers/HeaderCellRenderer.jsx +78 -0
- package/src/views/builder/viewer/renderers/HeaderRenderer.jsx +23 -0
- package/src/views/builder/viewer/renderers/ImageRenderer.jsx +26 -0
- package/src/views/builder/viewer/renderers/InputRenderer.jsx +34 -0
- package/src/views/builder/viewer/renderers/LabelRenderer.jsx +23 -0
- package/src/views/builder/viewer/renderers/LayoutCellRenderer.jsx +162 -0
- package/src/views/builder/viewer/renderers/LayoutContextMenu.jsx +173 -0
- package/src/views/builder/viewer/renderers/LayoutRenderer.jsx +51 -0
- package/src/views/builder/viewer/renderers/LinkRenderer.jsx +24 -0
- package/src/views/builder/viewer/renderers/MenuRenderer.jsx +291 -0
- package/src/views/builder/viewer/renderers/NumberFormatRenderer.jsx +80 -0
- package/src/views/builder/viewer/renderers/ProgressCircleRenderer.jsx +47 -0
- package/src/views/builder/viewer/renderers/ProgressLineRenderer.jsx +36 -0
- package/src/views/builder/viewer/renderers/RadioGroupRenderer.jsx +57 -0
- package/src/views/builder/viewer/renderers/RepeaterRenderer.jsx +94 -0
- package/src/views/builder/viewer/renderers/ReportViewerRenderer.jsx +15 -0
- package/src/views/builder/viewer/renderers/RichTextRenderer.jsx +76 -0
- package/src/views/builder/viewer/renderers/SignatureRenderer.jsx +89 -0
- package/src/views/builder/viewer/renderers/TabRenderer.jsx +82 -0
- package/src/views/builder/viewer/renderers/TableRenderer.jsx +92 -0
- package/src/views/builder/viewer/renderers/TagPickerRenderer.jsx +67 -0
- package/src/views/builder/viewer/renderers/TextAreaRenderer.jsx +37 -0
- package/src/views/builder/viewer/renderers/TextRenderer.jsx +9 -0
- package/src/views/builder/viewer/renderers/TimePickerRenderer.jsx +49 -0
- package/src/views/builder/viewer/renderers/ToggleRenderer.jsx +46 -0
- package/src/views/builder/viewer/renderers/WizardRenderer.jsx +88 -0
- package/src/views/builder/viewer/renderers/WizardStepRenderer.jsx +72 -0
- package/src/views/customFilter/CustomFilterDialog.js +1142 -0
- package/src/views/genericTable/BuilderExpressionParams.js +193 -0
- package/src/views/genericTable/FixedFilterDialog.js +447 -0
- package/src/views/genericTable/GenericForm.js +301 -0
- package/src/views/genericTable/QueryEditor.js +99 -0
- package/src/views/genericTable/RegexTextEditor.js +182 -0
- package/src/views/genericTable/ReportBuilderSaveDialog.js +153 -0
- package/src/views/genericTable/RoutingSettingDialog.js +189 -0
- package/src/views/genericTable/SGrid.js +2168 -0
- package/src/views/genericTable/SearchFilterDialog.js +247 -0
- package/src/views/genericTable/TAGGrid.js +1046 -0
- package/src/views/genericTable/cellEditors/autocompleteEditor.js +229 -0
- package/src/views/genericTable/cellRenderers/imageRenderer.js +14 -0
- package/src/views/genericTable/statusBar/rowCountStatusBar.js +37 -0
- package/src/views/genericTable/template/addTemplate.js +187 -0
- package/src/views/genericTable/toolPanels/CustomColumnsToolPanel.js +43 -0
- package/src/views/pages/auth/FooterIllustrationsV2.js +40 -0
- package/src/views/pages/misc/FooterIllustrations.js +47 -0
- package/src/views/pages/misc/muiTable/CustomPagination.js +34 -0
- package/src/views/pages/users/UserManageDialog.js +283 -0
- package/src/views/pages/users/UserViewPage.js +199 -0
- package/src/views/users/AddUserNameDialog.js +162 -0
- package/src/views/users/ContactManage.js +449 -0
- package/src/views/users/ResetPasswordDialog.js +242 -0
- package/styles/globals.css +71 -0
|
@@ -0,0 +1,2070 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useReducer, useCallback, useMemo, useState, useEffect } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
DndContext,
|
|
6
|
+
closestCenter,
|
|
7
|
+
KeyboardSensor,
|
|
8
|
+
PointerSensor,
|
|
9
|
+
useSensor,
|
|
10
|
+
useSensors,
|
|
11
|
+
DragOverlay
|
|
12
|
+
} from '@dnd-kit/core'
|
|
13
|
+
import {
|
|
14
|
+
arrayMove,
|
|
15
|
+
SortableContext,
|
|
16
|
+
sortableKeyboardCoordinates,
|
|
17
|
+
useSortable,
|
|
18
|
+
verticalListSortingStrategy
|
|
19
|
+
} from '@dnd-kit/sortable'
|
|
20
|
+
import { CSS } from '@dnd-kit/utilities'
|
|
21
|
+
import {
|
|
22
|
+
Box,
|
|
23
|
+
Paper,
|
|
24
|
+
Typography,
|
|
25
|
+
Button,
|
|
26
|
+
TextField,
|
|
27
|
+
IconButton,
|
|
28
|
+
List,
|
|
29
|
+
ListItem,
|
|
30
|
+
ListItemText,
|
|
31
|
+
ListItemButton,
|
|
32
|
+
ListItemIcon,
|
|
33
|
+
Divider as MuiDivider,
|
|
34
|
+
Chip,
|
|
35
|
+
Alert,
|
|
36
|
+
Dialog,
|
|
37
|
+
DialogTitle,
|
|
38
|
+
DialogContent,
|
|
39
|
+
DialogActions,
|
|
40
|
+
Select,
|
|
41
|
+
MenuItem,
|
|
42
|
+
FormControl,
|
|
43
|
+
InputLabel,
|
|
44
|
+
Tooltip,
|
|
45
|
+
Stack,
|
|
46
|
+
Tabs,
|
|
47
|
+
Tab,
|
|
48
|
+
Collapse
|
|
49
|
+
} from '@mui/material'
|
|
50
|
+
import {
|
|
51
|
+
Add as AddIcon,
|
|
52
|
+
Delete as DeleteIcon,
|
|
53
|
+
Edit as EditIcon,
|
|
54
|
+
ContentCopy as CloneIcon,
|
|
55
|
+
ArrowUpward as MoveUpIcon,
|
|
56
|
+
ArrowDownward as MoveDownIcon,
|
|
57
|
+
ArrowBack as MoveOutIcon,
|
|
58
|
+
ArrowForward as MoveInIcon,
|
|
59
|
+
DragIndicator as DragIcon,
|
|
60
|
+
Save as SaveIcon,
|
|
61
|
+
FolderOpen as LoadIcon,
|
|
62
|
+
CheckCircle as ValidIcon,
|
|
63
|
+
Error as ErrorIcon,
|
|
64
|
+
Label as ItemIcon,
|
|
65
|
+
Folder as GroupIcon,
|
|
66
|
+
Remove as DividerIcon,
|
|
67
|
+
Link as ExternalIcon,
|
|
68
|
+
Visibility as PreviewIcon,
|
|
69
|
+
ChevronRight as ChevronRightIcon,
|
|
70
|
+
} from '@mui/icons-material'
|
|
71
|
+
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
72
|
+
import * as Icons from 'mdi-material-ui'
|
|
73
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
74
|
+
import PostService from 'src/services/PostService'
|
|
75
|
+
import GetService from 'src/services/GetService'
|
|
76
|
+
import { Endpoints } from 'src/services/Endpoints'
|
|
77
|
+
|
|
78
|
+
const NODE_TYPES = {
|
|
79
|
+
ITEM: 'item',
|
|
80
|
+
DIVIDER: 'divider',
|
|
81
|
+
EXTERNAL: 'external'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const NAVIGATION_TYPES = {
|
|
85
|
+
STATIC: 'static', // Static path (hardcoded)
|
|
86
|
+
DYNAMIC: 'dynamic', // Dynamic viewId (from registry)
|
|
87
|
+
EXTERNAL: 'external' // External URL
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create a navigation item (becomes group if it has children)
|
|
91
|
+
const createNavItem = () => ({
|
|
92
|
+
id: uuidv4(),
|
|
93
|
+
title: 'New Item',
|
|
94
|
+
icon: null,
|
|
95
|
+
type: NAVIGATION_TYPES.DYNAMIC, // Default to dynamic (viewId)
|
|
96
|
+
viewId: null, // For dynamic type
|
|
97
|
+
path: null, // For static type
|
|
98
|
+
externalUrl: null, // For external type
|
|
99
|
+
children: [] // ✅ ALWAYS array
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Create the default mainHome root group
|
|
103
|
+
const createMainHomeRoot = () => ({
|
|
104
|
+
id: 'mainHome-root',
|
|
105
|
+
title: 'mainHome',
|
|
106
|
+
icon: null,
|
|
107
|
+
type: null, // Root group has no type
|
|
108
|
+
viewId: null,
|
|
109
|
+
path: null,
|
|
110
|
+
externalUrl: null,
|
|
111
|
+
children: [] // ✅ ALWAYS array
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Navigation Config - the single output
|
|
116
|
+
* Always has a mainHome root group
|
|
117
|
+
*/
|
|
118
|
+
const createEmptyNavConfig = () => ({
|
|
119
|
+
id: uuidv4(),
|
|
120
|
+
name: 'New Navigation',
|
|
121
|
+
items: [createMainHomeRoot()], // Always start with mainHome root
|
|
122
|
+
createdAt: new Date().toISOString(),
|
|
123
|
+
updatedAt: new Date().toISOString()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// STEP 3: TREE OPERATIONS
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Find node in tree by ID
|
|
132
|
+
*/
|
|
133
|
+
const findNodeInTree = (tree, nodeId) => {
|
|
134
|
+
if (!tree || !Array.isArray(tree)) return null
|
|
135
|
+
|
|
136
|
+
for (const node of tree) {
|
|
137
|
+
if (node.id === nodeId) return node
|
|
138
|
+
if (node.children && Array.isArray(node.children)) {
|
|
139
|
+
const found = findNodeInTree(node.children, nodeId)
|
|
140
|
+
if (found) return found
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const findParent = (tree, nodeId, parent = null) => {
|
|
147
|
+
if (!tree || !Array.isArray(tree)) return null
|
|
148
|
+
|
|
149
|
+
for (const node of tree) {
|
|
150
|
+
if (node.id === nodeId) return parent
|
|
151
|
+
if (node.children && Array.isArray(node.children)) {
|
|
152
|
+
const found = findParent(node.children, nodeId, node)
|
|
153
|
+
if (found !== null) return found
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const removeNodeFromTree = (tree, nodeId) => {
|
|
160
|
+
if (!tree || !Array.isArray(tree)) return tree
|
|
161
|
+
|
|
162
|
+
return tree
|
|
163
|
+
.filter(node => node.id !== nodeId)
|
|
164
|
+
.map(node => ({
|
|
165
|
+
...node,
|
|
166
|
+
children: Array.isArray(node.children)
|
|
167
|
+
? removeNodeFromTree(node.children, nodeId)
|
|
168
|
+
: []
|
|
169
|
+
}))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const updateNodeInTree = (tree, nodeId, updates) => {
|
|
173
|
+
if (!tree || !Array.isArray(tree)) return tree
|
|
174
|
+
|
|
175
|
+
return tree.map(node => {
|
|
176
|
+
if (node.id === nodeId) {
|
|
177
|
+
// Ensure children is always an array - NEVER undefined or null
|
|
178
|
+
const updatedNode = { ...node, ...updates, updatedAt: new Date().toISOString() }
|
|
179
|
+
if (!Array.isArray(updatedNode.children)) {
|
|
180
|
+
updatedNode.children = []
|
|
181
|
+
}
|
|
182
|
+
return updatedNode
|
|
183
|
+
}
|
|
184
|
+
if (Array.isArray(node.children)) {
|
|
185
|
+
return {
|
|
186
|
+
...node,
|
|
187
|
+
children: updateNodeInTree(node.children, nodeId, updates)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Always return children as array
|
|
191
|
+
return { ...node, children: [] }
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const addNodeToTree = (tree, parentId, newNode, position = 'end') => {
|
|
196
|
+
if (!tree || !Array.isArray(tree)) return tree
|
|
197
|
+
|
|
198
|
+
if (parentId === null) {
|
|
199
|
+
const newTree = [...tree]
|
|
200
|
+
if (position === 'start') {
|
|
201
|
+
newTree.unshift(newNode)
|
|
202
|
+
} else {
|
|
203
|
+
newTree.push(newNode)
|
|
204
|
+
}
|
|
205
|
+
return newTree.map((node, index) => ({ ...node, order: index }))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return tree.map(node => {
|
|
209
|
+
if (node.id === parentId) {
|
|
210
|
+
// Create a new node object, don't mutate the original
|
|
211
|
+
// Ensure children is always an array
|
|
212
|
+
const existingChildren = Array.isArray(node.children) ? node.children : []
|
|
213
|
+
const newChildren = [...existingChildren]
|
|
214
|
+
if (position === 'start') {
|
|
215
|
+
newChildren.unshift(newNode)
|
|
216
|
+
} else {
|
|
217
|
+
newChildren.push(newNode)
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
...node,
|
|
221
|
+
children: newChildren.map((child, index) => ({ ...child, order: index }))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (Array.isArray(node.children)) {
|
|
225
|
+
return {
|
|
226
|
+
...node,
|
|
227
|
+
children: addNodeToTree(node.children, parentId, newNode, position)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Always return children as array
|
|
231
|
+
return { ...node, children: [] }
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
const cloneNode = (node) => {
|
|
237
|
+
const cloned = {
|
|
238
|
+
...node,
|
|
239
|
+
id: uuidv4(),
|
|
240
|
+
title: `${node.title || 'Untitled'} (Copy)`,
|
|
241
|
+
order: 0
|
|
242
|
+
}
|
|
243
|
+
if (Array.isArray(cloned.children) && cloned.children.length > 0) {
|
|
244
|
+
cloned.children = cloned.children.map(child => cloneNode(child))
|
|
245
|
+
} else {
|
|
246
|
+
cloned.children = []
|
|
247
|
+
}
|
|
248
|
+
return cloned
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Reorder nodes using drag and drop
|
|
253
|
+
*/
|
|
254
|
+
const reorderNodesInTree = (tree, source, destination) => {
|
|
255
|
+
if (!tree || !Array.isArray(tree)) return tree
|
|
256
|
+
if (!destination) return tree
|
|
257
|
+
|
|
258
|
+
// Helper to find node by ID
|
|
259
|
+
const findNodeById = (nodes, nodeId) => {
|
|
260
|
+
for (const node of nodes) {
|
|
261
|
+
if (node.id === nodeId) return node
|
|
262
|
+
if (node.children) {
|
|
263
|
+
const found = findNodeById(node.children, nodeId)
|
|
264
|
+
if (found) return found
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Helper to update children of a node
|
|
271
|
+
const updateNodeChildren = (nodes, nodeId, newChildren) => {
|
|
272
|
+
return nodes.map(node => {
|
|
273
|
+
if (node.id === nodeId) {
|
|
274
|
+
return { ...node, children: newChildren }
|
|
275
|
+
}
|
|
276
|
+
if (node.children) {
|
|
277
|
+
return { ...node, children: updateNodeChildren(node.children, nodeId, newChildren) }
|
|
278
|
+
}
|
|
279
|
+
return node
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Get the node being moved
|
|
284
|
+
const nodeToMove = source.droppableId === 'root'
|
|
285
|
+
? tree[source.index]
|
|
286
|
+
: (findNodeById(tree, source.droppableId)?.children?.[source.index])
|
|
287
|
+
|
|
288
|
+
if (!nodeToMove) return tree
|
|
289
|
+
|
|
290
|
+
// Remove from source
|
|
291
|
+
let result = tree
|
|
292
|
+
if (source.droppableId === 'root') {
|
|
293
|
+
result = tree.filter((_, idx) => idx !== source.index)
|
|
294
|
+
} else {
|
|
295
|
+
const parent = findNodeById(tree, source.droppableId)
|
|
296
|
+
if (parent && parent.children) {
|
|
297
|
+
const newChildren = parent.children.filter((_, idx) => idx !== source.index)
|
|
298
|
+
result = updateNodeChildren(tree, source.droppableId, newChildren)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Add to destination
|
|
303
|
+
if (destination.droppableId === 'root') {
|
|
304
|
+
const newRoot = [...result]
|
|
305
|
+
newRoot.splice(destination.index, 0, nodeToMove)
|
|
306
|
+
result = newRoot
|
|
307
|
+
} else {
|
|
308
|
+
const parent = findNodeById(result, destination.droppableId)
|
|
309
|
+
if (parent) {
|
|
310
|
+
const newChildren = [...(parent.children || [])]
|
|
311
|
+
newChildren.splice(destination.index, 0, nodeToMove)
|
|
312
|
+
result = updateNodeChildren(result, destination.droppableId, newChildren)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return result
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const moveNodeInTree = (tree, nodeId, direction) => {
|
|
320
|
+
if (!tree || !Array.isArray(tree)) return tree
|
|
321
|
+
|
|
322
|
+
const findSiblings = (nodes, targetId, parent = null) => {
|
|
323
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
324
|
+
if (nodes[i].id === targetId) {
|
|
325
|
+
return { siblings: nodes, index: i, parent }
|
|
326
|
+
}
|
|
327
|
+
if (nodes[i].children) {
|
|
328
|
+
const found = findSiblings(nodes[i].children, targetId, nodes[i])
|
|
329
|
+
if (found) return found
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const result = findSiblings(tree, nodeId)
|
|
336
|
+
if (!result) return tree
|
|
337
|
+
|
|
338
|
+
const { siblings, index, parent } = result
|
|
339
|
+
const newSiblings = [...siblings]
|
|
340
|
+
|
|
341
|
+
if (direction === 'up' && index > 0) {
|
|
342
|
+
[newSiblings[index - 1], newSiblings[index]] = [newSiblings[index], newSiblings[index - 1]]
|
|
343
|
+
} else if (direction === 'down' && index < newSiblings.length - 1) {
|
|
344
|
+
[newSiblings[index], newSiblings[index + 1]] = [newSiblings[index + 1], newSiblings[index]]
|
|
345
|
+
} else if (direction === 'in' && index > 0) {
|
|
346
|
+
// Move as child of previous sibling
|
|
347
|
+
const prevSibling = newSiblings[index - 1]
|
|
348
|
+
if (prevSibling.type === NODE_TYPES.GROUP) {
|
|
349
|
+
const nodeToMove = newSiblings[index]
|
|
350
|
+
newSiblings.splice(index, 1)
|
|
351
|
+
prevSibling.children = [...(prevSibling.children || []), nodeToMove]
|
|
352
|
+
}
|
|
353
|
+
} else if (direction === 'out' && parent) {
|
|
354
|
+
const nodeToMove = newSiblings[index]
|
|
355
|
+
newSiblings.splice(index, 1)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
newSiblings.forEach((node, idx) => {
|
|
359
|
+
node.order = idx
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
const reconstructTree = (nodes) => {
|
|
363
|
+
return nodes.map(node => {
|
|
364
|
+
if (parent && node.id === parent.id) {
|
|
365
|
+
return { ...node, children: newSiblings }
|
|
366
|
+
}
|
|
367
|
+
if (node.children) {
|
|
368
|
+
return { ...node, children: reconstructTree(node.children) }
|
|
369
|
+
}
|
|
370
|
+
return node
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return parent ? reconstructTree(tree) : newSiblings
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
const validateNavigation = (navConfig) => {
|
|
379
|
+
const errors = []
|
|
380
|
+
const warnings = []
|
|
381
|
+
const nodeIds = new Set()
|
|
382
|
+
|
|
383
|
+
const validateNode = (node, path = []) => {
|
|
384
|
+
const currentPath = [...path, node.id]
|
|
385
|
+
|
|
386
|
+
// Check for duplicate IDs
|
|
387
|
+
if (nodeIds.has(node.id)) {
|
|
388
|
+
errors.push({
|
|
389
|
+
nodeId: node.id,
|
|
390
|
+
message: `Duplicate ID: ${node.id}`,
|
|
391
|
+
path: currentPath
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
nodeIds.add(node.id)
|
|
395
|
+
|
|
396
|
+
// Validate navigation item
|
|
397
|
+
// Single source of truth: Items with children are groups, items without children are simple nav items
|
|
398
|
+
const hasChildren = Array.isArray(node.children) && node.children.length > 0
|
|
399
|
+
|
|
400
|
+
if (!node.title || (typeof node.title === 'string' && node.title.trim() === '')) {
|
|
401
|
+
errors.push({
|
|
402
|
+
nodeId: node.id,
|
|
403
|
+
message: 'Navigation item must have a title',
|
|
404
|
+
path: currentPath
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Leaf must have appropriate field based on type
|
|
409
|
+
if (!hasChildren) {
|
|
410
|
+
const navType = node.type || NAVIGATION_TYPES.DYNAMIC
|
|
411
|
+
|
|
412
|
+
if (navType === NAVIGATION_TYPES.DYNAMIC) {
|
|
413
|
+
if (node.viewId === null || node.viewId === undefined || node.viewId === '') {
|
|
414
|
+
errors.push({
|
|
415
|
+
nodeId: node.id,
|
|
416
|
+
message: 'Dynamic navigation item must have a viewId',
|
|
417
|
+
path: currentPath
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
} else if (navType === NAVIGATION_TYPES.STATIC) {
|
|
421
|
+
if (!node.path || (typeof node.path === 'string' && node.path.trim() === '')) {
|
|
422
|
+
errors.push({
|
|
423
|
+
nodeId: node.id,
|
|
424
|
+
message: 'Static navigation item must have a path',
|
|
425
|
+
path: currentPath
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
} else if (navType === NAVIGATION_TYPES.EXTERNAL) {
|
|
429
|
+
if (!node.externalUrl || (typeof node.externalUrl === 'string' && node.externalUrl.trim() === '')) {
|
|
430
|
+
errors.push({
|
|
431
|
+
nodeId: node.id,
|
|
432
|
+
message: 'External navigation item must have an externalUrl',
|
|
433
|
+
path: currentPath
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Validate children recursively
|
|
440
|
+
if (hasChildren) {
|
|
441
|
+
node.children.forEach(child => validateNode(child, currentPath))
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Validate root structure
|
|
446
|
+
if (!navConfig.items || !Array.isArray(navConfig.items)) {
|
|
447
|
+
errors.push({
|
|
448
|
+
nodeId: 'root',
|
|
449
|
+
message: 'Navigation must have items array',
|
|
450
|
+
path: []
|
|
451
|
+
})
|
|
452
|
+
} else {
|
|
453
|
+
navConfig.items.forEach(item => validateNode(item, []))
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
isValid: errors.length === 0,
|
|
458
|
+
errors,
|
|
459
|
+
warnings
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ============================================================================
|
|
464
|
+
// STEP 4: REDUCER & STATE MANAGEMENT
|
|
465
|
+
// ============================================================================
|
|
466
|
+
|
|
467
|
+
const initialState = {
|
|
468
|
+
navConfig: createEmptyNavConfig(),
|
|
469
|
+
selectedNodeId: null,
|
|
470
|
+
expandedNodes: [], // Track which nodes are expanded (array of IDs)
|
|
471
|
+
validation: { isValid: true, errors: [], warnings: [] },
|
|
472
|
+
isDirty: false,
|
|
473
|
+
registry: {} // Dynamic registry loaded from server
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const navigatorReducer = (state, action) => {
|
|
477
|
+
switch (action.type) {
|
|
478
|
+
case 'CREATE_NAVIGATION':
|
|
479
|
+
return {
|
|
480
|
+
...initialState,
|
|
481
|
+
navConfig: createEmptyNavConfig()
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
case 'LOAD_NAVIGATION':
|
|
485
|
+
// Migration: convert old format (path/action/subject) to new format (viewId/type)
|
|
486
|
+
const migrate = (items) =>
|
|
487
|
+
items.map(i => {
|
|
488
|
+
const migrated = {
|
|
489
|
+
...i,
|
|
490
|
+
// Ensure ID exists (generate if missing)
|
|
491
|
+
id: i.id || (i.title === 'mainHome' ? 'mainHome-root' : uuidv4()),
|
|
492
|
+
// Set default type if missing
|
|
493
|
+
type: i.type || (i.viewId ? NAVIGATION_TYPES.DYNAMIC : i.path ? NAVIGATION_TYPES.STATIC : null),
|
|
494
|
+
viewId: i.viewId ?? null,
|
|
495
|
+
path: i.path ?? null,
|
|
496
|
+
externalUrl: i.externalUrl ?? null,
|
|
497
|
+
children: Array.isArray(i.children) ? migrate(i.children) : []
|
|
498
|
+
}
|
|
499
|
+
// Remove old keys
|
|
500
|
+
delete migrated.action
|
|
501
|
+
delete migrated.subject
|
|
502
|
+
return migrated
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const loadedConfig = action.payload.config || action.payload
|
|
506
|
+
const loadedRegistry = action.payload.registry || {}
|
|
507
|
+
console.log('LOAD_NAVIGATION reducer - received config:', loadedConfig)
|
|
508
|
+
console.log('LOAD_NAVIGATION reducer - received registry:', loadedRegistry)
|
|
509
|
+
|
|
510
|
+
if (loadedConfig.items) {
|
|
511
|
+
console.log('Migrating items, count:', loadedConfig.items.length)
|
|
512
|
+
loadedConfig.items = migrate(loadedConfig.items)
|
|
513
|
+
console.log('After migration:', loadedConfig.items)
|
|
514
|
+
|
|
515
|
+
// Ensure mainHome root exists
|
|
516
|
+
// Check if first item is mainHome root
|
|
517
|
+
const hasMainHomeRoot = loadedConfig.items.length > 0 &&
|
|
518
|
+
(loadedConfig.items[0].id === 'mainHome-root' ||
|
|
519
|
+
loadedConfig.items[0].title === 'mainHome')
|
|
520
|
+
|
|
521
|
+
console.log('Has mainHome root?', hasMainHomeRoot)
|
|
522
|
+
|
|
523
|
+
if (!hasMainHomeRoot) {
|
|
524
|
+
// Wrap existing items in mainHome root
|
|
525
|
+
console.log('Wrapping items in mainHome root')
|
|
526
|
+
const mainHomeRoot = createMainHomeRoot()
|
|
527
|
+
mainHomeRoot.children = loadedConfig.items
|
|
528
|
+
loadedConfig.items = [mainHomeRoot]
|
|
529
|
+
} else {
|
|
530
|
+
// Ensure the mainHome root has the correct ID
|
|
531
|
+
if (loadedConfig.items[0].title === 'mainHome' && loadedConfig.items[0].id !== 'mainHome-root') {
|
|
532
|
+
loadedConfig.items[0].id = 'mainHome-root'
|
|
533
|
+
}
|
|
534
|
+
// If mainHome root exists but has no children, ensure it has children array
|
|
535
|
+
if (!Array.isArray(loadedConfig.items[0].children)) {
|
|
536
|
+
loadedConfig.items[0].children = []
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
// No items, create empty mainHome root
|
|
541
|
+
console.log('No items found, creating empty mainHome root')
|
|
542
|
+
loadedConfig.items = [createMainHomeRoot()]
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
console.log('Final loaded config items:', loadedConfig.items)
|
|
546
|
+
|
|
547
|
+
const validation = validateNavigation(loadedConfig)
|
|
548
|
+
return {
|
|
549
|
+
...state,
|
|
550
|
+
navConfig: loadedConfig,
|
|
551
|
+
registry: loadedRegistry,
|
|
552
|
+
selectedNodeId: null,
|
|
553
|
+
validation,
|
|
554
|
+
isDirty: false
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
case 'SELECT_NODE':
|
|
558
|
+
return {
|
|
559
|
+
...state,
|
|
560
|
+
selectedNodeId: action.payload
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
case 'TOGGLE_EXPAND':
|
|
564
|
+
const nodeId = action.payload
|
|
565
|
+
const isCurrentlyExpanded = state.expandedNodes.includes(nodeId)
|
|
566
|
+
return {
|
|
567
|
+
...state,
|
|
568
|
+
expandedNodes: isCurrentlyExpanded
|
|
569
|
+
? state.expandedNodes.filter(id => id !== nodeId)
|
|
570
|
+
: [...state.expandedNodes, nodeId]
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
case 'ADD_NODE': {
|
|
574
|
+
const { parentId, position } = action.payload
|
|
575
|
+
const newNode = createNavItem()
|
|
576
|
+
// newNode.viewId stays null by default (acts like a group until you assign it)
|
|
577
|
+
|
|
578
|
+
const updatedItems = addNodeToTree(
|
|
579
|
+
state.navConfig.items,
|
|
580
|
+
parentId,
|
|
581
|
+
newNode,
|
|
582
|
+
position
|
|
583
|
+
)
|
|
584
|
+
const updatedConfig = {
|
|
585
|
+
...state.navConfig,
|
|
586
|
+
items: updatedItems,
|
|
587
|
+
updatedAt: new Date().toISOString()
|
|
588
|
+
}
|
|
589
|
+
const newValidation = validateNavigation(updatedConfig)
|
|
590
|
+
|
|
591
|
+
// Auto-expand parent if adding a child - use Set to avoid duplicates
|
|
592
|
+
const expandedNodes = parentId
|
|
593
|
+
? Array.from(new Set([...state.expandedNodes, parentId]))
|
|
594
|
+
: state.expandedNodes
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
...state,
|
|
598
|
+
navConfig: updatedConfig,
|
|
599
|
+
selectedNodeId: newNode.id,
|
|
600
|
+
expandedNodes,
|
|
601
|
+
validation: newValidation,
|
|
602
|
+
isDirty: true
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
case 'UPDATE_NODE': {
|
|
607
|
+
const { nodeId, updates } = action.payload
|
|
608
|
+
const updatedItems = updateNodeInTree(
|
|
609
|
+
state.navConfig.items,
|
|
610
|
+
nodeId,
|
|
611
|
+
updates
|
|
612
|
+
)
|
|
613
|
+
const updatedConfig = {
|
|
614
|
+
...state.navConfig,
|
|
615
|
+
items: updatedItems,
|
|
616
|
+
updatedAt: new Date().toISOString()
|
|
617
|
+
}
|
|
618
|
+
const newValidation = validateNavigation(updatedConfig)
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
...state,
|
|
622
|
+
navConfig: updatedConfig,
|
|
623
|
+
validation: newValidation,
|
|
624
|
+
isDirty: true
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
case 'DELETE_NODE': {
|
|
629
|
+
// Prevent deleting the mainHome root
|
|
630
|
+
if (action.payload === 'mainHome-root') {
|
|
631
|
+
return state
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const updatedItems = removeNodeFromTree(
|
|
635
|
+
state.navConfig.items,
|
|
636
|
+
action.payload
|
|
637
|
+
)
|
|
638
|
+
const updatedConfig = {
|
|
639
|
+
...state.navConfig,
|
|
640
|
+
items: updatedItems,
|
|
641
|
+
updatedAt: new Date().toISOString()
|
|
642
|
+
}
|
|
643
|
+
const newValidation = validateNavigation(updatedConfig)
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
...state,
|
|
647
|
+
navConfig: updatedConfig,
|
|
648
|
+
selectedNodeId: state.selectedNodeId === action.payload ? null : state.selectedNodeId,
|
|
649
|
+
validation: newValidation,
|
|
650
|
+
isDirty: true
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
case 'CLONE_NODE': {
|
|
655
|
+
const node = findNodeInTree(state.navConfig.items, action.payload)
|
|
656
|
+
if (!node) return state
|
|
657
|
+
|
|
658
|
+
const cloned = {
|
|
659
|
+
...node,
|
|
660
|
+
id: uuidv4(),
|
|
661
|
+
title: `${node.title} (Copy)`
|
|
662
|
+
}
|
|
663
|
+
if (cloned.children && Array.isArray(cloned.children)) {
|
|
664
|
+
const cloneChild = (child) => ({
|
|
665
|
+
...child,
|
|
666
|
+
id: uuidv4()
|
|
667
|
+
})
|
|
668
|
+
cloned.children = cloned.children.map(cloneChild)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const parent = findParent(state.navConfig.items, action.payload)
|
|
672
|
+
const parentId = parent ? parent.id : null
|
|
673
|
+
|
|
674
|
+
const updatedItems = addNodeToTree(
|
|
675
|
+
state.navConfig.items,
|
|
676
|
+
parentId,
|
|
677
|
+
cloned,
|
|
678
|
+
'end'
|
|
679
|
+
)
|
|
680
|
+
const updatedConfig = {
|
|
681
|
+
...state.navConfig,
|
|
682
|
+
items: updatedItems,
|
|
683
|
+
updatedAt: new Date().toISOString()
|
|
684
|
+
}
|
|
685
|
+
const newValidation = validateNavigation(updatedConfig)
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
...state,
|
|
689
|
+
navConfig: updatedConfig,
|
|
690
|
+
selectedNodeId: cloned.id,
|
|
691
|
+
validation: newValidation,
|
|
692
|
+
isDirty: true
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
case 'MOVE_NODE': {
|
|
697
|
+
const { nodeId, direction } = action.payload
|
|
698
|
+
const updatedItems = moveNodeInTree(
|
|
699
|
+
state.navConfig.items,
|
|
700
|
+
nodeId,
|
|
701
|
+
direction
|
|
702
|
+
)
|
|
703
|
+
const updatedConfig = {
|
|
704
|
+
...state.navConfig,
|
|
705
|
+
items: updatedItems,
|
|
706
|
+
updatedAt: new Date().toISOString()
|
|
707
|
+
}
|
|
708
|
+
const newValidation = validateNavigation(updatedConfig)
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
...state,
|
|
712
|
+
navConfig: updatedConfig,
|
|
713
|
+
validation: newValidation,
|
|
714
|
+
isDirty: true
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
case 'REORDER_NODES': {
|
|
719
|
+
const { source, destination } = action.payload
|
|
720
|
+
const updatedItems = reorderNodesInTree(
|
|
721
|
+
state.navConfig.items,
|
|
722
|
+
source,
|
|
723
|
+
destination
|
|
724
|
+
)
|
|
725
|
+
const updatedConfig = {
|
|
726
|
+
...state.navConfig,
|
|
727
|
+
items: updatedItems,
|
|
728
|
+
updatedAt: new Date().toISOString()
|
|
729
|
+
}
|
|
730
|
+
const newValidation = validateNavigation(updatedConfig)
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
...state,
|
|
734
|
+
navConfig: updatedConfig,
|
|
735
|
+
validation: newValidation,
|
|
736
|
+
isDirty: true
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
case 'VALIDATE':
|
|
741
|
+
return {
|
|
742
|
+
...state,
|
|
743
|
+
validation: validateNavigation(state.navConfig)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
case 'SET_DIRTY':
|
|
747
|
+
return {
|
|
748
|
+
...state,
|
|
749
|
+
isDirty: action.payload
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
default:
|
|
753
|
+
return state
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ============================================================================
|
|
758
|
+
// CONTEXT
|
|
759
|
+
// ============================================================================
|
|
760
|
+
|
|
761
|
+
const NavigatorContext = createContext(null)
|
|
762
|
+
|
|
763
|
+
export const NavigatorProvider = ({ children }) => {
|
|
764
|
+
const [state, dispatch] = useReducer(navigatorReducer, initialState)
|
|
765
|
+
|
|
766
|
+
const actions = useMemo(() => ({
|
|
767
|
+
createNavigation: () => dispatch({ type: 'CREATE_NAVIGATION' }),
|
|
768
|
+
loadNavigation: (data) => dispatch({ type: 'LOAD_NAVIGATION', payload: data }),
|
|
769
|
+
selectNode: (nodeId) => dispatch({ type: 'SELECT_NODE', payload: nodeId }),
|
|
770
|
+
toggleExpand: (nodeId) => dispatch({ type: 'TOGGLE_EXPAND', payload: nodeId }),
|
|
771
|
+
addNode: (parentId, position = 'end') =>
|
|
772
|
+
dispatch({ type: 'ADD_NODE', payload: { parentId, position } }),
|
|
773
|
+
updateNode: (nodeId, updates) =>
|
|
774
|
+
dispatch({ type: 'UPDATE_NODE', payload: { nodeId, updates } }),
|
|
775
|
+
deleteNode: (nodeId) => dispatch({ type: 'DELETE_NODE', payload: nodeId }),
|
|
776
|
+
cloneNode: (nodeId) => dispatch({ type: 'CLONE_NODE', payload: nodeId }),
|
|
777
|
+
moveNode: (nodeId, direction) =>
|
|
778
|
+
dispatch({ type: 'MOVE_NODE', payload: { nodeId, direction } }),
|
|
779
|
+
reorderNodes: (source, destination) =>
|
|
780
|
+
dispatch({ type: 'REORDER_NODES', payload: { source, destination } }),
|
|
781
|
+
validate: () => dispatch({ type: 'VALIDATE' }),
|
|
782
|
+
getNavConfig: () => {
|
|
783
|
+
// Convert to navigation format: remove internal fields, keep only navigation structure
|
|
784
|
+
// Handle different navigation types (static, dynamic, external)
|
|
785
|
+
const convertToNavFormat = (items) => {
|
|
786
|
+
return items.map(item => {
|
|
787
|
+
const navItem = {
|
|
788
|
+
title: item.title,
|
|
789
|
+
icon: item.icon
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const hasChildren = Array.isArray(item.children) && item.children.length > 0
|
|
793
|
+
const navType = item.type || NAVIGATION_TYPES.DYNAMIC
|
|
794
|
+
|
|
795
|
+
// Handle based on type (for both items and groups)
|
|
796
|
+
if (navType === NAVIGATION_TYPES.DYNAMIC && item.viewId) {
|
|
797
|
+
// Dynamic items/groups: always include viewId
|
|
798
|
+
navItem.type = 'dynamic'
|
|
799
|
+
navItem.viewId = item.viewId
|
|
800
|
+
|
|
801
|
+
// Only include explicit path if provided
|
|
802
|
+
// DO NOT generate /viewer/${viewId} path - let runtime resolve from registry
|
|
803
|
+
if (item.path) {
|
|
804
|
+
navItem.path = item.path
|
|
805
|
+
}
|
|
806
|
+
// Note: Path will be resolved at runtime from registry using viewId
|
|
807
|
+
} else if (navType === NAVIGATION_TYPES.STATIC && item.path && !hasChildren) {
|
|
808
|
+
// Static: use the path directly (only for items, not groups)
|
|
809
|
+
navItem.path = item.path
|
|
810
|
+
navItem.type = 'static'
|
|
811
|
+
} else if (navType === NAVIGATION_TYPES.EXTERNAL && item.externalUrl && !hasChildren) {
|
|
812
|
+
// External: use externalUrl (only for items, not groups)
|
|
813
|
+
navItem.externalUrl = item.externalUrl
|
|
814
|
+
navItem.type = 'external'
|
|
815
|
+
navItem.openInNewTab = true // External links typically open in new tab
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Add children if they exist (makes it a group)
|
|
819
|
+
if (hasChildren) {
|
|
820
|
+
navItem.children = convertToNavFormat(item.children)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return navItem
|
|
824
|
+
})
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Ensure mainHome root exists and extract its children for output
|
|
828
|
+
let itemsToConvert = state.navConfig.items
|
|
829
|
+
|
|
830
|
+
// If mainHome root exists, use its children; otherwise use items as-is
|
|
831
|
+
const mainHomeRoot = itemsToConvert.find(item =>
|
|
832
|
+
item.id === 'mainHome-root' || item.title === 'mainHome'
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
if (mainHomeRoot && Array.isArray(mainHomeRoot.children)) {
|
|
836
|
+
// Return mainHome with its children
|
|
837
|
+
itemsToConvert = [{
|
|
838
|
+
title: 'mainHome',
|
|
839
|
+
children: convertToNavFormat(mainHomeRoot.children)
|
|
840
|
+
}]
|
|
841
|
+
} else {
|
|
842
|
+
// No mainHome root, wrap items in mainHome
|
|
843
|
+
itemsToConvert = [{
|
|
844
|
+
title: 'mainHome',
|
|
845
|
+
children: convertToNavFormat(state.navConfig.items)
|
|
846
|
+
}]
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
...state.navConfig,
|
|
851
|
+
items: itemsToConvert
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}), [state.navConfig])
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<NavigatorContext.Provider value={{ state, actions }}>
|
|
858
|
+
{children}
|
|
859
|
+
</NavigatorContext.Provider>
|
|
860
|
+
)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export const useNavigator = () => {
|
|
864
|
+
const context = useContext(NavigatorContext)
|
|
865
|
+
if (!context) {
|
|
866
|
+
throw new Error('useNavigator must be used within NavigatorProvider')
|
|
867
|
+
}
|
|
868
|
+
return context
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ============================================================================
|
|
872
|
+
// UI COMPONENTS
|
|
873
|
+
// ============================================================================
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Tree Node Component - Using @dnd-kit useSortable
|
|
878
|
+
*/
|
|
879
|
+
const TreeNode = ({ node, depth = 0 }) => {
|
|
880
|
+
const { state, actions } = useNavigator()
|
|
881
|
+
|
|
882
|
+
// Safety check
|
|
883
|
+
if (!node || !node.id) {
|
|
884
|
+
return null
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Debug logging (temporary)
|
|
888
|
+
// Debug logging (temporary) - remove after testing
|
|
889
|
+
console.log('NODE', node.title, 'children:', node.children?.length || 0)
|
|
890
|
+
|
|
891
|
+
const isSelected = state.selectedNodeId === node.id
|
|
892
|
+
// Single source of truth: group = has children array with items
|
|
893
|
+
const hasChildren = Array.isArray(node.children) && node.children.length > 0
|
|
894
|
+
const isExpanded = state.expandedNodes.includes(node.id)
|
|
895
|
+
// Always show chevron if children array exists (even if empty)
|
|
896
|
+
const canHaveChildren = Array.isArray(node.children)
|
|
897
|
+
|
|
898
|
+
// @dnd-kit useSortable hook
|
|
899
|
+
const {
|
|
900
|
+
attributes,
|
|
901
|
+
listeners,
|
|
902
|
+
setNodeRef,
|
|
903
|
+
transform,
|
|
904
|
+
transition,
|
|
905
|
+
isDragging
|
|
906
|
+
} = useSortable({
|
|
907
|
+
id: String(node.id),
|
|
908
|
+
disabled: false
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
const style = {
|
|
912
|
+
transform: CSS.Transform.toString(transform),
|
|
913
|
+
transition,
|
|
914
|
+
opacity: isDragging ? 0.5 : 1
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const handleToggleExpand = (e) => {
|
|
918
|
+
e.preventDefault()
|
|
919
|
+
e.stopPropagation()
|
|
920
|
+
actions.toggleExpand(node.id)
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const getNodeIcon = () => {
|
|
924
|
+
// Use hasChildren (not isGroup) - single source of truth
|
|
925
|
+
if (hasChildren) {
|
|
926
|
+
return GroupIcon ? <GroupIcon fontSize="small" /> : null
|
|
927
|
+
}
|
|
928
|
+
return ItemIcon ? <ItemIcon fontSize="small" /> : null
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const getPathDisplay = () => {
|
|
932
|
+
if (hasChildren) return `${node.children.length} items`
|
|
933
|
+
|
|
934
|
+
const navType = node.type || NAVIGATION_TYPES.DYNAMIC
|
|
935
|
+
if (navType === NAVIGATION_TYPES.DYNAMIC) {
|
|
936
|
+
return node.viewId ? `View #${node.viewId}` : ''
|
|
937
|
+
} else if (navType === NAVIGATION_TYPES.STATIC) {
|
|
938
|
+
return node.path || ''
|
|
939
|
+
} else if (navType === NAVIGATION_TYPES.EXTERNAL) {
|
|
940
|
+
return node.externalUrl || ''
|
|
941
|
+
}
|
|
942
|
+
return ''
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const handleAddChild = (e) => {
|
|
946
|
+
e.stopPropagation()
|
|
947
|
+
actions.addNode(node.id)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return (
|
|
951
|
+
<Box
|
|
952
|
+
ref={setNodeRef}
|
|
953
|
+
style={style}
|
|
954
|
+
sx={{
|
|
955
|
+
mb: 0.5,
|
|
956
|
+
...(isDragging && {
|
|
957
|
+
boxShadow: 3,
|
|
958
|
+
transform: 'rotate(2deg)'
|
|
959
|
+
})
|
|
960
|
+
}}
|
|
961
|
+
>
|
|
962
|
+
<Box
|
|
963
|
+
sx={{
|
|
964
|
+
display: 'flex',
|
|
965
|
+
alignItems: 'center',
|
|
966
|
+
position: 'relative',
|
|
967
|
+
'&::before': depth > 0 ? {
|
|
968
|
+
content: '""',
|
|
969
|
+
position: 'absolute',
|
|
970
|
+
left: depth * 24 - 12,
|
|
971
|
+
top: 0,
|
|
972
|
+
bottom: 0,
|
|
973
|
+
width: '1px',
|
|
974
|
+
bgcolor: 'divider',
|
|
975
|
+
opacity: 0.3
|
|
976
|
+
} : {}
|
|
977
|
+
}}
|
|
978
|
+
>
|
|
979
|
+
{/* Tree connector lines */}
|
|
980
|
+
{depth > 0 && (
|
|
981
|
+
<Box
|
|
982
|
+
sx={{
|
|
983
|
+
position: 'absolute',
|
|
984
|
+
left: depth * 24 - 12,
|
|
985
|
+
top: -8,
|
|
986
|
+
width: 12,
|
|
987
|
+
height: '50%',
|
|
988
|
+
borderLeft: '1px solid',
|
|
989
|
+
borderBottom: '1px solid',
|
|
990
|
+
borderColor: 'divider',
|
|
991
|
+
opacity: 0.3
|
|
992
|
+
}}
|
|
993
|
+
/>
|
|
994
|
+
)}
|
|
995
|
+
<ListItemButton
|
|
996
|
+
selected={isSelected}
|
|
997
|
+
onClick={() => actions.selectNode(node.id)}
|
|
998
|
+
{...attributes}
|
|
999
|
+
sx={{
|
|
1000
|
+
pl: depth * 3 + 2,
|
|
1001
|
+
flex: 1,
|
|
1002
|
+
borderRadius: 1,
|
|
1003
|
+
'&.Mui-selected': {
|
|
1004
|
+
backgroundColor: 'primary.main',
|
|
1005
|
+
color: 'primary.contrastText',
|
|
1006
|
+
'&:hover': {
|
|
1007
|
+
backgroundColor: 'primary.dark'
|
|
1008
|
+
},
|
|
1009
|
+
'& .MuiListItemIcon-root': {
|
|
1010
|
+
color: 'primary.contrastText'
|
|
1011
|
+
},
|
|
1012
|
+
'& .MuiListItemText-primary': {
|
|
1013
|
+
color: 'primary.contrastText'
|
|
1014
|
+
},
|
|
1015
|
+
'& .MuiListItemText-secondary': {
|
|
1016
|
+
color: 'primary.contrastText',
|
|
1017
|
+
opacity: 0.8
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
}}
|
|
1021
|
+
>
|
|
1022
|
+
<ListItemIcon sx={{ minWidth: 32, display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
1023
|
+
{/* Drag handle - only this element has drag listeners */}
|
|
1024
|
+
<Box
|
|
1025
|
+
{...(listeners || {})}
|
|
1026
|
+
sx={{
|
|
1027
|
+
display: 'inline-flex',
|
|
1028
|
+
alignItems: 'center',
|
|
1029
|
+
cursor: isDragging ? 'grabbing' : 'grab',
|
|
1030
|
+
p: 0.5,
|
|
1031
|
+
borderRadius: 1,
|
|
1032
|
+
'&:hover': {
|
|
1033
|
+
bgcolor: isSelected ? 'rgba(255,255,255,0.1)' : 'action.hover'
|
|
1034
|
+
}
|
|
1035
|
+
}}
|
|
1036
|
+
>
|
|
1037
|
+
{DragIcon && <DragIcon fontSize="small" sx={{ color: isSelected ? 'primary.contrastText' : 'text.secondary' }} />}
|
|
1038
|
+
</Box>
|
|
1039
|
+
{/* Always show chevron if children array exists (even if empty) */}
|
|
1040
|
+
{canHaveChildren && KeyboardArrowDownIcon && ChevronRightIcon && (
|
|
1041
|
+
<IconButton
|
|
1042
|
+
size="small"
|
|
1043
|
+
onClick={handleToggleExpand}
|
|
1044
|
+
onMouseDown={(e) => {
|
|
1045
|
+
e.preventDefault()
|
|
1046
|
+
e.stopPropagation()
|
|
1047
|
+
}}
|
|
1048
|
+
onPointerDown={(e) => {
|
|
1049
|
+
e.preventDefault()
|
|
1050
|
+
e.stopPropagation()
|
|
1051
|
+
}}
|
|
1052
|
+
onTouchStart={(e) => {
|
|
1053
|
+
e.stopPropagation()
|
|
1054
|
+
}}
|
|
1055
|
+
sx={{
|
|
1056
|
+
p: 0.5,
|
|
1057
|
+
color: isSelected ? 'primary.contrastText' : 'text.secondary',
|
|
1058
|
+
'&:hover': {
|
|
1059
|
+
bgcolor: isSelected ? 'rgba(255,255,255,0.1)' : 'action.hover'
|
|
1060
|
+
},
|
|
1061
|
+
pointerEvents: 'auto'
|
|
1062
|
+
}}
|
|
1063
|
+
>
|
|
1064
|
+
{isExpanded ? (
|
|
1065
|
+
<KeyboardArrowDownIcon fontSize="small" />
|
|
1066
|
+
) : (
|
|
1067
|
+
<ChevronRightIcon fontSize="small" />
|
|
1068
|
+
)}
|
|
1069
|
+
</IconButton>
|
|
1070
|
+
)}
|
|
1071
|
+
{getNodeIcon() || <Box sx={{ width: 24, height: 24 }} />}
|
|
1072
|
+
</ListItemIcon>
|
|
1073
|
+
<ListItemText
|
|
1074
|
+
primary={node.title || 'Untitled'}
|
|
1075
|
+
secondary={getPathDisplay()}
|
|
1076
|
+
/>
|
|
1077
|
+
<Box sx={{ display: 'flex', gap: 0.5 }} onClick={(e) => e.stopPropagation()}>
|
|
1078
|
+
<Tooltip title="Add Item Inside">
|
|
1079
|
+
<IconButton
|
|
1080
|
+
size="small"
|
|
1081
|
+
color="primary"
|
|
1082
|
+
onClick={handleAddChild}
|
|
1083
|
+
>
|
|
1084
|
+
<AddIcon fontSize="small" />
|
|
1085
|
+
</IconButton>
|
|
1086
|
+
</Tooltip>
|
|
1087
|
+
<Tooltip title="Clone">
|
|
1088
|
+
<IconButton
|
|
1089
|
+
size="small"
|
|
1090
|
+
onClick={(e) => {
|
|
1091
|
+
e.stopPropagation()
|
|
1092
|
+
actions.cloneNode(node.id)
|
|
1093
|
+
}}
|
|
1094
|
+
>
|
|
1095
|
+
<CloneIcon fontSize="small" />
|
|
1096
|
+
</IconButton>
|
|
1097
|
+
</Tooltip>
|
|
1098
|
+
{/* Don't show delete button for mainHome root */}
|
|
1099
|
+
{node.id !== 'mainHome-root' && (
|
|
1100
|
+
<Tooltip title="Delete">
|
|
1101
|
+
<IconButton
|
|
1102
|
+
size="small"
|
|
1103
|
+
color="error"
|
|
1104
|
+
onClick={(e) => {
|
|
1105
|
+
e.stopPropagation()
|
|
1106
|
+
if (confirm('Delete this item?')) {
|
|
1107
|
+
actions.deleteNode(node.id)
|
|
1108
|
+
}
|
|
1109
|
+
}}
|
|
1110
|
+
>
|
|
1111
|
+
<DeleteIcon fontSize="small" />
|
|
1112
|
+
</IconButton>
|
|
1113
|
+
</Tooltip>
|
|
1114
|
+
)}
|
|
1115
|
+
</Box>
|
|
1116
|
+
</ListItemButton>
|
|
1117
|
+
</Box>
|
|
1118
|
+
</Box>
|
|
1119
|
+
)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Tree Children Component - SortableContext wrapper for children
|
|
1124
|
+
* @dnd-kit handles nested structures natively
|
|
1125
|
+
*/
|
|
1126
|
+
const TreeChildren = ({ parent, depth = 0 }) => {
|
|
1127
|
+
const { state } = useNavigator()
|
|
1128
|
+
const isExpanded = state.expandedNodes.includes(parent.id)
|
|
1129
|
+
|
|
1130
|
+
// Ensure children is always an array
|
|
1131
|
+
const children = Array.isArray(parent.children) ? parent.children : []
|
|
1132
|
+
const hasChildren = children.length > 0
|
|
1133
|
+
|
|
1134
|
+
// Always render if expanded (even if no children yet) to allow adding items
|
|
1135
|
+
if (!isExpanded) {
|
|
1136
|
+
return null
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const childIds = hasChildren
|
|
1140
|
+
? children
|
|
1141
|
+
.filter(child => child && child.id)
|
|
1142
|
+
.map(child => String(child.id))
|
|
1143
|
+
: []
|
|
1144
|
+
|
|
1145
|
+
return (
|
|
1146
|
+
<SortableContext items={childIds} strategy={verticalListSortingStrategy}>
|
|
1147
|
+
<Box
|
|
1148
|
+
sx={{
|
|
1149
|
+
ml: 3
|
|
1150
|
+
}}
|
|
1151
|
+
>
|
|
1152
|
+
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
|
1153
|
+
<List component="div" disablePadding>
|
|
1154
|
+
{hasChildren ? (
|
|
1155
|
+
children
|
|
1156
|
+
.filter(child => child && child.id)
|
|
1157
|
+
.map((child) => (
|
|
1158
|
+
<React.Fragment key={String(child.id)}>
|
|
1159
|
+
<TreeNode node={child} depth={depth + 1} />
|
|
1160
|
+
{state.expandedNodes.includes(child.id) && (
|
|
1161
|
+
<TreeChildren parent={child} depth={depth + 1} />
|
|
1162
|
+
)}
|
|
1163
|
+
|
|
1164
|
+
</React.Fragment>
|
|
1165
|
+
))
|
|
1166
|
+
) : (
|
|
1167
|
+
<Box sx={{ p: 2, textAlign: 'center', color: 'text.secondary' }}>
|
|
1168
|
+
<Typography variant="body2">No items yet. Click "Add Item Inside" to add.</Typography>
|
|
1169
|
+
</Box>
|
|
1170
|
+
)}
|
|
1171
|
+
</List>
|
|
1172
|
+
</Collapse>
|
|
1173
|
+
</Box>
|
|
1174
|
+
</SortableContext>
|
|
1175
|
+
)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Center Panel - Tree View with Root and Drag & Drop
|
|
1180
|
+
*/
|
|
1181
|
+
const TreePanel = () => {
|
|
1182
|
+
const { state, actions } = useNavigator()
|
|
1183
|
+
const [activeId, setActiveId] = useState(null)
|
|
1184
|
+
|
|
1185
|
+
// @dnd-kit sensors - configure to prevent accidental drags when clicking buttons
|
|
1186
|
+
const sensors = useSensors(
|
|
1187
|
+
useSensor(PointerSensor, {
|
|
1188
|
+
activationConstraint: {
|
|
1189
|
+
distance: 8, // Require 8px movement before drag starts
|
|
1190
|
+
delay: 0,
|
|
1191
|
+
tolerance: 5
|
|
1192
|
+
}
|
|
1193
|
+
}),
|
|
1194
|
+
useSensor(KeyboardSensor, {
|
|
1195
|
+
coordinateGetter: sortableKeyboardCoordinates
|
|
1196
|
+
})
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
const handleAddToRoot = () => {
|
|
1200
|
+
// Find mainHome root and add to it
|
|
1201
|
+
const mainHomeRoot = state.navConfig.items.find(item =>
|
|
1202
|
+
item.id === 'mainHome-root' || item.title === 'mainHome'
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
if (mainHomeRoot) {
|
|
1206
|
+
// Add to mainHome root
|
|
1207
|
+
actions.addNode(mainHomeRoot.id)
|
|
1208
|
+
// Auto-expand mainHome if not already expanded
|
|
1209
|
+
if (!state.expandedNodes.includes('mainHome-root')) {
|
|
1210
|
+
actions.toggleExpand('mainHome-root')
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
// No mainHome root, add to root (will be wrapped in mainHome on save)
|
|
1214
|
+
actions.addNode(null)
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const handleDragStart = (event) => {
|
|
1219
|
+
setActiveId(event.active.id)
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const handleDragEnd = (event) => {
|
|
1223
|
+
const { active, over } = event
|
|
1224
|
+
setActiveId(null)
|
|
1225
|
+
|
|
1226
|
+
if (!over || active.id === over.id) {
|
|
1227
|
+
return
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Helper to find node and its parent info
|
|
1231
|
+
const findNodeInfo = (items, targetId, parentId = null) => {
|
|
1232
|
+
for (let i = 0; i < items.length; i++) {
|
|
1233
|
+
const node = items[i]
|
|
1234
|
+
if (String(node.id) === String(targetId)) {
|
|
1235
|
+
return {
|
|
1236
|
+
node,
|
|
1237
|
+
parentId,
|
|
1238
|
+
index: i
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
1242
|
+
const found = findNodeInfo(node.children, targetId, node.id)
|
|
1243
|
+
if (found) return found
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return null
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const activeInfo = findNodeInfo(state.navConfig.items, active.id)
|
|
1250
|
+
const overInfo = findNodeInfo(state.navConfig.items, over.id)
|
|
1251
|
+
|
|
1252
|
+
if (!activeInfo || !overInfo) return
|
|
1253
|
+
|
|
1254
|
+
// Convert to the format expected by reorderNodes
|
|
1255
|
+
const source = {
|
|
1256
|
+
droppableId: activeInfo.parentId || 'root',
|
|
1257
|
+
index: activeInfo.index
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const destination = {
|
|
1261
|
+
droppableId: overInfo.parentId || 'root',
|
|
1262
|
+
index: overInfo.index
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
actions.reorderNodes(source, destination)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
return (
|
|
1269
|
+
<Paper sx={{ p: 2, height: '100%', overflow: 'auto' }}>
|
|
1270
|
+
<Typography variant="h6" gutterBottom>
|
|
1271
|
+
Navigation Tree
|
|
1272
|
+
</Typography>
|
|
1273
|
+
|
|
1274
|
+
{/* Root Node */}
|
|
1275
|
+
<Paper
|
|
1276
|
+
variant="outlined"
|
|
1277
|
+
sx={{
|
|
1278
|
+
p: 2,
|
|
1279
|
+
mb: 2,
|
|
1280
|
+
bgcolor: 'background.default',
|
|
1281
|
+
border: '2px dashed',
|
|
1282
|
+
borderColor: 'divider'
|
|
1283
|
+
}}
|
|
1284
|
+
>
|
|
1285
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
|
1286
|
+
<Typography variant="subtitle1" fontWeight="bold">
|
|
1287
|
+
Root Navigation
|
|
1288
|
+
</Typography>
|
|
1289
|
+
<Button
|
|
1290
|
+
variant="contained"
|
|
1291
|
+
size="small"
|
|
1292
|
+
startIcon={<AddIcon />}
|
|
1293
|
+
onClick={handleAddToRoot}
|
|
1294
|
+
>
|
|
1295
|
+
Add Navigation Item
|
|
1296
|
+
</Button>
|
|
1297
|
+
</Box>
|
|
1298
|
+
<Typography variant="body2" color="text.secondary">
|
|
1299
|
+
Drag items to reorder. Items with children become groups automatically.
|
|
1300
|
+
</Typography>
|
|
1301
|
+
</Paper>
|
|
1302
|
+
|
|
1303
|
+
{/* Navigation Items with Drag and Drop */}
|
|
1304
|
+
<DndContext
|
|
1305
|
+
sensors={sensors}
|
|
1306
|
+
collisionDetection={closestCenter}
|
|
1307
|
+
onDragStart={handleDragStart}
|
|
1308
|
+
onDragEnd={handleDragEnd}
|
|
1309
|
+
>
|
|
1310
|
+
{(() => {
|
|
1311
|
+
// Find mainHome root
|
|
1312
|
+
const mainHomeRoot = state.navConfig.items.find(item =>
|
|
1313
|
+
item.id === 'mainHome-root' || item.title === 'mainHome'
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
// If mainHome exists, use its children; otherwise use items directly
|
|
1317
|
+
const itemsToShow = mainHomeRoot && Array.isArray(mainHomeRoot.children)
|
|
1318
|
+
? mainHomeRoot.children
|
|
1319
|
+
: state.navConfig.items
|
|
1320
|
+
|
|
1321
|
+
// Auto-expand mainHome root if it exists
|
|
1322
|
+
const shouldExpandMainHome = mainHomeRoot && !state.expandedNodes.includes('mainHome-root')
|
|
1323
|
+
if (shouldExpandMainHome) {
|
|
1324
|
+
actions.toggleExpand('mainHome-root')
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (itemsToShow.length === 0 && !mainHomeRoot) {
|
|
1328
|
+
return <Alert severity="info">No navigation items yet. Click "Add Navigation Item" above to get started.</Alert>
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
return (
|
|
1332
|
+
<SortableContext
|
|
1333
|
+
items={itemsToShow.map(node => String(node.id))}
|
|
1334
|
+
strategy={verticalListSortingStrategy}
|
|
1335
|
+
>
|
|
1336
|
+
<List>
|
|
1337
|
+
{/* Show mainHome root as header if it exists */}
|
|
1338
|
+
{mainHomeRoot && (
|
|
1339
|
+
<ListItem
|
|
1340
|
+
sx={{
|
|
1341
|
+
px: 2,
|
|
1342
|
+
py: 1.5,
|
|
1343
|
+
bgcolor: 'action.hover',
|
|
1344
|
+
borderBottom: '1px solid',
|
|
1345
|
+
borderColor: 'divider'
|
|
1346
|
+
}}
|
|
1347
|
+
>
|
|
1348
|
+
<ListItemIcon>
|
|
1349
|
+
<GroupIcon color="primary" />
|
|
1350
|
+
</ListItemIcon>
|
|
1351
|
+
<ListItemText
|
|
1352
|
+
primary={
|
|
1353
|
+
<Typography variant="subtitle1" fontWeight="bold">
|
|
1354
|
+
{mainHomeRoot.title || 'mainHome'}
|
|
1355
|
+
</Typography>
|
|
1356
|
+
}
|
|
1357
|
+
secondary="Root Navigation Group"
|
|
1358
|
+
/>
|
|
1359
|
+
<IconButton
|
|
1360
|
+
size="small"
|
|
1361
|
+
onClick={() => actions.toggleExpand('mainHome-root')}
|
|
1362
|
+
>
|
|
1363
|
+
{state.expandedNodes.includes('mainHome-root') ? (
|
|
1364
|
+
<KeyboardArrowDownIcon />
|
|
1365
|
+
) : (
|
|
1366
|
+
<ChevronRightIcon />
|
|
1367
|
+
)}
|
|
1368
|
+
</IconButton>
|
|
1369
|
+
</ListItem>
|
|
1370
|
+
)}
|
|
1371
|
+
|
|
1372
|
+
{/* Show children of mainHome */}
|
|
1373
|
+
{mainHomeRoot && state.expandedNodes.includes('mainHome-root') && (
|
|
1374
|
+
<TreeChildren parent={mainHomeRoot} depth={0} />
|
|
1375
|
+
)}
|
|
1376
|
+
|
|
1377
|
+
{/* If no mainHome root, show items directly */}
|
|
1378
|
+
{!mainHomeRoot && itemsToShow.map((node) => (
|
|
1379
|
+
<React.Fragment key={String(node.id)}>
|
|
1380
|
+
<TreeNode node={node} depth={0} />
|
|
1381
|
+
{state.expandedNodes.includes(node.id) && (
|
|
1382
|
+
<TreeChildren parent={node} depth={0} />
|
|
1383
|
+
)}
|
|
1384
|
+
</React.Fragment>
|
|
1385
|
+
))}
|
|
1386
|
+
</List>
|
|
1387
|
+
</SortableContext>
|
|
1388
|
+
)
|
|
1389
|
+
})()}
|
|
1390
|
+
<DragOverlay>
|
|
1391
|
+
{activeId ? (() => {
|
|
1392
|
+
// Find the node being dragged
|
|
1393
|
+
const findNodeById = (items, id) => {
|
|
1394
|
+
for (const node of items) {
|
|
1395
|
+
if (String(node.id) === String(id)) return node
|
|
1396
|
+
if (Array.isArray(node.children)) {
|
|
1397
|
+
const found = findNodeById(node.children, id)
|
|
1398
|
+
if (found) return found
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return null
|
|
1402
|
+
}
|
|
1403
|
+
const draggedNode = findNodeById(state.navConfig.items, activeId)
|
|
1404
|
+
if (!draggedNode) return null
|
|
1405
|
+
|
|
1406
|
+
return (
|
|
1407
|
+
<Box
|
|
1408
|
+
sx={{
|
|
1409
|
+
p: 1.5,
|
|
1410
|
+
bgcolor: 'background.paper',
|
|
1411
|
+
boxShadow: 6,
|
|
1412
|
+
borderRadius: 1,
|
|
1413
|
+
border: '2px solid',
|
|
1414
|
+
borderColor: 'primary.main',
|
|
1415
|
+
minWidth: 200,
|
|
1416
|
+
opacity: 0.9
|
|
1417
|
+
}}
|
|
1418
|
+
>
|
|
1419
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
1420
|
+
<DragIcon sx={{ color: 'primary.main' }} />
|
|
1421
|
+
<Typography variant="body2" fontWeight="medium">
|
|
1422
|
+
{draggedNode.title || 'Untitled'}
|
|
1423
|
+
</Typography>
|
|
1424
|
+
</Box>
|
|
1425
|
+
</Box>
|
|
1426
|
+
)
|
|
1427
|
+
})() : null}
|
|
1428
|
+
</DragOverlay>
|
|
1429
|
+
</DndContext>
|
|
1430
|
+
</Paper>
|
|
1431
|
+
)
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Left Panel - Properties
|
|
1436
|
+
*/
|
|
1437
|
+
const PropertiesPanel = () => {
|
|
1438
|
+
const { state, actions } = useNavigator()
|
|
1439
|
+
const selectedNode = state.selectedNodeId
|
|
1440
|
+
? findNodeInTree(state.navConfig.items, state.selectedNodeId)
|
|
1441
|
+
: null
|
|
1442
|
+
|
|
1443
|
+
if (!selectedNode) {
|
|
1444
|
+
return (
|
|
1445
|
+
<Paper sx={{ p: 2, height: '100%' }}>
|
|
1446
|
+
<Typography variant="h6" gutterBottom>
|
|
1447
|
+
Properties
|
|
1448
|
+
</Typography>
|
|
1449
|
+
<Alert severity="info">Select a node to edit its properties</Alert>
|
|
1450
|
+
</Paper>
|
|
1451
|
+
)
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const handleUpdate = (field, value) => {
|
|
1455
|
+
actions.updateNode(selectedNode.id, { [field]: value })
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
return (
|
|
1460
|
+
<Paper sx={{ p: 2, height: '100%', overflow: 'auto' }}>
|
|
1461
|
+
<Typography variant="h6" gutterBottom>
|
|
1462
|
+
Properties
|
|
1463
|
+
</Typography>
|
|
1464
|
+
|
|
1465
|
+
<Stack spacing={2}>
|
|
1466
|
+
<TextField
|
|
1467
|
+
label="Title"
|
|
1468
|
+
value={selectedNode.title || ''}
|
|
1469
|
+
onChange={(e) => handleUpdate('title', e.target.value)}
|
|
1470
|
+
fullWidth
|
|
1471
|
+
required
|
|
1472
|
+
/>
|
|
1473
|
+
|
|
1474
|
+
{/* Navigation Type - show for both items and groups */}
|
|
1475
|
+
<FormControl fullWidth>
|
|
1476
|
+
<InputLabel>Navigation Type</InputLabel>
|
|
1477
|
+
<Select
|
|
1478
|
+
value={selectedNode.type || NAVIGATION_TYPES.DYNAMIC}
|
|
1479
|
+
label="Navigation Type"
|
|
1480
|
+
onChange={(e) => {
|
|
1481
|
+
const newType = e.target.value
|
|
1482
|
+
// Clear fields when switching types
|
|
1483
|
+
const updates = { type: newType }
|
|
1484
|
+
if (newType === NAVIGATION_TYPES.DYNAMIC) {
|
|
1485
|
+
updates.externalUrl = null
|
|
1486
|
+
} else if (newType === NAVIGATION_TYPES.STATIC) {
|
|
1487
|
+
updates.viewId = null
|
|
1488
|
+
updates.externalUrl = null
|
|
1489
|
+
} else if (newType === NAVIGATION_TYPES.EXTERNAL) {
|
|
1490
|
+
updates.viewId = null
|
|
1491
|
+
updates.path = null
|
|
1492
|
+
}
|
|
1493
|
+
actions.updateNode(selectedNode.id, updates)
|
|
1494
|
+
}}
|
|
1495
|
+
>
|
|
1496
|
+
<MenuItem value={NAVIGATION_TYPES.DYNAMIC}>Dynamic (View ID)</MenuItem>
|
|
1497
|
+
<MenuItem value={NAVIGATION_TYPES.STATIC}>Static (Path)</MenuItem>
|
|
1498
|
+
<MenuItem value={NAVIGATION_TYPES.EXTERNAL}>External (URL)</MenuItem>
|
|
1499
|
+
</Select>
|
|
1500
|
+
</FormControl>
|
|
1501
|
+
|
|
1502
|
+
{/* Dynamic type fields - available for both items and groups */}
|
|
1503
|
+
{selectedNode.type === NAVIGATION_TYPES.DYNAMIC && (
|
|
1504
|
+
<>
|
|
1505
|
+
<TextField
|
|
1506
|
+
label="View ID"
|
|
1507
|
+
type="number"
|
|
1508
|
+
value={selectedNode.viewId ?? ''}
|
|
1509
|
+
onChange={(e) => {
|
|
1510
|
+
const v = e.target.value.trim()
|
|
1511
|
+
actions.updateNode(selectedNode.id, { viewId: v === '' ? null : Number(v) })
|
|
1512
|
+
}}
|
|
1513
|
+
fullWidth
|
|
1514
|
+
placeholder="e.g., 22"
|
|
1515
|
+
helperText={(() => {
|
|
1516
|
+
if (!selectedNode.viewId) return null
|
|
1517
|
+
const view = state.registry[selectedNode.viewId]
|
|
1518
|
+
return view ? `View: ${view.name} (/${view.slug})` : 'View details not found'
|
|
1519
|
+
})()}
|
|
1520
|
+
/>
|
|
1521
|
+
{/* Groups with viewId can also have a path */}
|
|
1522
|
+
{(selectedNode.children && selectedNode.children.length > 0) && selectedNode.viewId && (
|
|
1523
|
+
<TextField
|
|
1524
|
+
label="Path (optional for groups)"
|
|
1525
|
+
value={selectedNode.path || ''}
|
|
1526
|
+
onChange={(e) => {
|
|
1527
|
+
actions.updateNode(selectedNode.id, { path: e.target.value.trim() || null })
|
|
1528
|
+
}}
|
|
1529
|
+
fullWidth
|
|
1530
|
+
placeholder="e.g., /viewer/22"
|
|
1531
|
+
helperText="Groups with viewId can have a path"
|
|
1532
|
+
/>
|
|
1533
|
+
)}
|
|
1534
|
+
</>
|
|
1535
|
+
)}
|
|
1536
|
+
|
|
1537
|
+
{/* Static type fields - only for items without children */}
|
|
1538
|
+
{selectedNode.type === NAVIGATION_TYPES.STATIC && (!selectedNode.children || selectedNode.children.length === 0) && (
|
|
1539
|
+
<TextField
|
|
1540
|
+
label="Path"
|
|
1541
|
+
value={selectedNode.path || ''}
|
|
1542
|
+
onChange={(e) => {
|
|
1543
|
+
actions.updateNode(selectedNode.id, { path: e.target.value.trim() || null })
|
|
1544
|
+
}}
|
|
1545
|
+
fullWidth
|
|
1546
|
+
placeholder="e.g., /mainHome or /users"
|
|
1547
|
+
/>
|
|
1548
|
+
)}
|
|
1549
|
+
|
|
1550
|
+
{/* External type fields - only for items without children */}
|
|
1551
|
+
{selectedNode.type === NAVIGATION_TYPES.EXTERNAL && (!selectedNode.children || selectedNode.children.length === 0) && (
|
|
1552
|
+
<TextField
|
|
1553
|
+
label="External URL"
|
|
1554
|
+
value={selectedNode.externalUrl || ''}
|
|
1555
|
+
onChange={(e) => {
|
|
1556
|
+
actions.updateNode(selectedNode.id, { externalUrl: e.target.value.trim() || null })
|
|
1557
|
+
}}
|
|
1558
|
+
fullWidth
|
|
1559
|
+
placeholder="e.g., https://example.com"
|
|
1560
|
+
/>
|
|
1561
|
+
)}
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
<TextField
|
|
1565
|
+
label="Icon (MUI icon name)"
|
|
1566
|
+
value={selectedNode.icon || ''}
|
|
1567
|
+
onChange={(e) => handleUpdate('icon', e.target.value)}
|
|
1568
|
+
fullWidth
|
|
1569
|
+
placeholder="e.g., HomeOutline"
|
|
1570
|
+
/>
|
|
1571
|
+
|
|
1572
|
+
<Chip
|
|
1573
|
+
label={Array.isArray(selectedNode.children) && selectedNode.children.length > 0 ? 'Group' : 'Navigation Item'}
|
|
1574
|
+
color={Array.isArray(selectedNode.children) && selectedNode.children.length > 0 ? 'primary' : 'default'}
|
|
1575
|
+
variant="outlined"
|
|
1576
|
+
/>
|
|
1577
|
+
{Array.isArray(selectedNode.children) && selectedNode.children.length > 0 && (
|
|
1578
|
+
<Chip
|
|
1579
|
+
label={`${selectedNode.children.length} child items`}
|
|
1580
|
+
color="secondary"
|
|
1581
|
+
variant="outlined"
|
|
1582
|
+
/>
|
|
1583
|
+
)}
|
|
1584
|
+
</Stack>
|
|
1585
|
+
</Paper>
|
|
1586
|
+
)
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Preview Navigation Item Component
|
|
1591
|
+
*/
|
|
1592
|
+
const PreviewNavItem = ({ node, depth = 0, onItemClick }) => {
|
|
1593
|
+
const [expanded, setExpanded] = useState(false)
|
|
1594
|
+
|
|
1595
|
+
// Safety check: ensure node exists
|
|
1596
|
+
if (!node || !node.id) {
|
|
1597
|
+
return null
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// Single source of truth: use only children array
|
|
1601
|
+
const hasChildren = Array.isArray(node.children) && node.children.length > 0
|
|
1602
|
+
|
|
1603
|
+
const getIcon = () => {
|
|
1604
|
+
if (node.icon && Icons[node.icon]) {
|
|
1605
|
+
const IconComponent = Icons[node.icon]
|
|
1606
|
+
return <IconComponent />
|
|
1607
|
+
}
|
|
1608
|
+
if (hasChildren) {
|
|
1609
|
+
return <GroupIcon />
|
|
1610
|
+
}
|
|
1611
|
+
return <ItemIcon />
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
return (
|
|
1616
|
+
<Box>
|
|
1617
|
+
<ListItemButton
|
|
1618
|
+
onClick={() => {
|
|
1619
|
+
if (hasChildren) {
|
|
1620
|
+
setExpanded(!expanded)
|
|
1621
|
+
} else if (onItemClick) {
|
|
1622
|
+
onItemClick(node)
|
|
1623
|
+
}
|
|
1624
|
+
}}
|
|
1625
|
+
sx={{
|
|
1626
|
+
pl: depth * 2 + 2,
|
|
1627
|
+
py: 1.5,
|
|
1628
|
+
borderRadius: 1,
|
|
1629
|
+
mb: 0.5,
|
|
1630
|
+
'&:hover': {
|
|
1631
|
+
backgroundColor: 'action.hover'
|
|
1632
|
+
}
|
|
1633
|
+
}}
|
|
1634
|
+
>
|
|
1635
|
+
<ListItemIcon sx={{ minWidth: 40, color: 'text.secondary' }}>
|
|
1636
|
+
{getIcon()}
|
|
1637
|
+
</ListItemIcon>
|
|
1638
|
+
<ListItemText
|
|
1639
|
+
primary={node.title || 'Untitled'}
|
|
1640
|
+
secondary={
|
|
1641
|
+
hasChildren
|
|
1642
|
+
? `${node.children.length} items`
|
|
1643
|
+
: (node.viewId ? `View #${node.viewId}` : '')
|
|
1644
|
+
}
|
|
1645
|
+
/>
|
|
1646
|
+
{hasChildren && (
|
|
1647
|
+
<Box sx={{ ml: 1 }}>
|
|
1648
|
+
{expanded ? <KeyboardArrowDownIcon /> : <ChevronRightIcon />}
|
|
1649
|
+
</Box>
|
|
1650
|
+
)}
|
|
1651
|
+
</ListItemButton>
|
|
1652
|
+
{hasChildren && (
|
|
1653
|
+
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
|
1654
|
+
<List component="div" disablePadding>
|
|
1655
|
+
{node.children
|
|
1656
|
+
.filter(child => child && child.id) // Filter out invalid children
|
|
1657
|
+
.map(child => (
|
|
1658
|
+
<PreviewNavItem
|
|
1659
|
+
key={child.id}
|
|
1660
|
+
node={child}
|
|
1661
|
+
depth={depth + 1}
|
|
1662
|
+
onItemClick={onItemClick}
|
|
1663
|
+
/>
|
|
1664
|
+
))}
|
|
1665
|
+
</List>
|
|
1666
|
+
</Collapse>
|
|
1667
|
+
)}
|
|
1668
|
+
</Box>
|
|
1669
|
+
)
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Preview Panel - Shows how navigation will look
|
|
1674
|
+
*/
|
|
1675
|
+
const PreviewPanel = () => {
|
|
1676
|
+
const { state, actions } = useNavigator()
|
|
1677
|
+
|
|
1678
|
+
const handleItemClick = (node) => {
|
|
1679
|
+
actions.selectNode(node.id)
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return (
|
|
1683
|
+
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', bgcolor: 'background.default' }}>
|
|
1684
|
+
<Paper sx={{ p: 2, borderRadius: 0, borderBottom: 1, borderColor: 'divider' }}>
|
|
1685
|
+
<Typography variant="h6" gutterBottom>
|
|
1686
|
+
Navigation Preview
|
|
1687
|
+
</Typography>
|
|
1688
|
+
<Typography variant="body2" color="text.secondary">
|
|
1689
|
+
This is how your navigation will appear in the application
|
|
1690
|
+
</Typography>
|
|
1691
|
+
</Paper>
|
|
1692
|
+
|
|
1693
|
+
{state.navConfig.items.length === 0 ? (
|
|
1694
|
+
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
1695
|
+
<Alert severity="info" sx={{ maxWidth: 500 }}>
|
|
1696
|
+
No navigation items to preview. Add items from the builder.
|
|
1697
|
+
</Alert>
|
|
1698
|
+
</Box>
|
|
1699
|
+
) : (
|
|
1700
|
+
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
|
1701
|
+
<Paper
|
|
1702
|
+
variant="outlined"
|
|
1703
|
+
sx={{
|
|
1704
|
+
p: 2,
|
|
1705
|
+
bgcolor: 'background.paper',
|
|
1706
|
+
borderRadius: 2,
|
|
1707
|
+
border: 1,
|
|
1708
|
+
borderColor: 'divider',
|
|
1709
|
+
maxWidth: 400,
|
|
1710
|
+
mx: 'auto'
|
|
1711
|
+
}}
|
|
1712
|
+
>
|
|
1713
|
+
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 2, px: 2 }}>
|
|
1714
|
+
{state.navConfig.name || 'Navigation Menu'}
|
|
1715
|
+
</Typography>
|
|
1716
|
+
<List sx={{ py: 0 }}>
|
|
1717
|
+
{state.navConfig.items.map(node => (
|
|
1718
|
+
<PreviewNavItem
|
|
1719
|
+
key={node.id}
|
|
1720
|
+
node={node}
|
|
1721
|
+
onItemClick={handleItemClick}
|
|
1722
|
+
/>
|
|
1723
|
+
))}
|
|
1724
|
+
</List>
|
|
1725
|
+
</Paper>
|
|
1726
|
+
|
|
1727
|
+
{/* JSON Output Preview */}
|
|
1728
|
+
<Paper
|
|
1729
|
+
variant="outlined"
|
|
1730
|
+
sx={{
|
|
1731
|
+
mt: 3,
|
|
1732
|
+
p: 2,
|
|
1733
|
+
bgcolor: 'background.paper',
|
|
1734
|
+
borderRadius: 2,
|
|
1735
|
+
border: 1,
|
|
1736
|
+
borderColor: 'divider',
|
|
1737
|
+
maxWidth: 800,
|
|
1738
|
+
mx: 'auto'
|
|
1739
|
+
}}
|
|
1740
|
+
>
|
|
1741
|
+
<Typography variant="subtitle2" gutterBottom>
|
|
1742
|
+
JSON Output
|
|
1743
|
+
</Typography>
|
|
1744
|
+
<Box
|
|
1745
|
+
component="pre"
|
|
1746
|
+
sx={{
|
|
1747
|
+
p: 2,
|
|
1748
|
+
direction:'rtl',
|
|
1749
|
+
bgcolor: 'grey.100',
|
|
1750
|
+
borderRadius: 1,
|
|
1751
|
+
overflow: 'auto',
|
|
1752
|
+
fontSize: '0.75rem',
|
|
1753
|
+
maxHeight: 300
|
|
1754
|
+
}}
|
|
1755
|
+
>
|
|
1756
|
+
{JSON.stringify(state.navConfig, null, 2)}
|
|
1757
|
+
</Box>
|
|
1758
|
+
</Paper>
|
|
1759
|
+
</Box>
|
|
1760
|
+
)}
|
|
1761
|
+
</Box>
|
|
1762
|
+
)
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Main Navigator Builder Component
|
|
1767
|
+
*/
|
|
1768
|
+
const NavigatorBuilder = () => {
|
|
1769
|
+
const { state, actions } = useNavigator()
|
|
1770
|
+
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
|
1771
|
+
const [loadDialogOpen, setLoadDialogOpen] = useState(false)
|
|
1772
|
+
const [navName, setNavName] = useState(state.navConfig.name)
|
|
1773
|
+
const [viewMode, setViewMode] = useState('builder') // 'builder' or 'preview'
|
|
1774
|
+
|
|
1775
|
+
const fetchNavigationSettings = useCallback(async () => {
|
|
1776
|
+
try {
|
|
1777
|
+
const result = await GetService(Endpoints.Navigator.Get.NavigatorSetting, false)
|
|
1778
|
+
console.log('Raw fetched result:', result)
|
|
1779
|
+
|
|
1780
|
+
let data = result
|
|
1781
|
+
if (result && result.data) {
|
|
1782
|
+
try {
|
|
1783
|
+
data = JSON.parse(result.data)
|
|
1784
|
+
} catch (e) {
|
|
1785
|
+
console.error('Error parsing result.value:', e)
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (data && data.config) {
|
|
1790
|
+
console.log('Loading navigation config from DTO:', data.config)
|
|
1791
|
+
actions.loadNavigation(data)
|
|
1792
|
+
setNavName(data.config.name)
|
|
1793
|
+
} else if (data && data.items) {
|
|
1794
|
+
// Fallback for direct config object
|
|
1795
|
+
actions.loadNavigation({ config: data, registry: {} })
|
|
1796
|
+
setNavName(data.name || 'Loaded Navigation')
|
|
1797
|
+
}
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
console.error('Error fetching navigation settings:', error)
|
|
1800
|
+
}
|
|
1801
|
+
}, [actions])
|
|
1802
|
+
|
|
1803
|
+
useEffect(() => {
|
|
1804
|
+
fetchNavigationSettings()
|
|
1805
|
+
}, [])
|
|
1806
|
+
|
|
1807
|
+
const handleSave = async () => {
|
|
1808
|
+
try {
|
|
1809
|
+
const config = actions.getNavConfig()
|
|
1810
|
+
config.name = navName
|
|
1811
|
+
|
|
1812
|
+
// Extract all viewIds and their titles from the navigation config
|
|
1813
|
+
const viewIdToTitle = new Map()
|
|
1814
|
+
const collectViewIds = (items) => {
|
|
1815
|
+
items.forEach(item => {
|
|
1816
|
+
if (item.viewId) {
|
|
1817
|
+
viewIdToTitle.set(String(item.viewId), item.title || `View ${item.viewId}`)
|
|
1818
|
+
}
|
|
1819
|
+
if (item.children && Array.isArray(item.children)) {
|
|
1820
|
+
collectViewIds(item.children)
|
|
1821
|
+
}
|
|
1822
|
+
})
|
|
1823
|
+
}
|
|
1824
|
+
collectViewIds(config.items)
|
|
1825
|
+
|
|
1826
|
+
// Helper function to create slug from title
|
|
1827
|
+
const createSlug = (title) => {
|
|
1828
|
+
return title
|
|
1829
|
+
.toLowerCase()
|
|
1830
|
+
.trim()
|
|
1831
|
+
.replace(/[^\w\s-]/g, '') // Remove special characters
|
|
1832
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
1833
|
+
.replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// Build registry directly from viewIds with nav titles
|
|
1837
|
+
const latestRegistry = {}
|
|
1838
|
+
|
|
1839
|
+
viewIdToTitle.forEach((title, viewId) => {
|
|
1840
|
+
latestRegistry[viewId] = {
|
|
1841
|
+
id: Number(viewId),
|
|
1842
|
+
name: title,
|
|
1843
|
+
slug: createSlug(title)
|
|
1844
|
+
}
|
|
1845
|
+
})
|
|
1846
|
+
|
|
1847
|
+
const saveData = {
|
|
1848
|
+
config: config,
|
|
1849
|
+
registry: latestRegistry
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
const jsonString = JSON.stringify(saveData)
|
|
1853
|
+
|
|
1854
|
+
// Use 'navigatorSetting' key as requested in previous sessions for DataTypes.Params
|
|
1855
|
+
const result = await PostService(Endpoints.Navigator.Post.NavigatorSetting, true, {
|
|
1856
|
+
value: jsonString
|
|
1857
|
+
})
|
|
1858
|
+
|
|
1859
|
+
if (result) {
|
|
1860
|
+
setSaveDialogOpen(false)
|
|
1861
|
+
}
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
console.error('Error saving navigation:', error)
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const handleLoad = (event) => {
|
|
1868
|
+
const file = event.target.files[0]
|
|
1869
|
+
if (!file) {
|
|
1870
|
+
console.warn('No file selected')
|
|
1871
|
+
return
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
console.log('Loading file:', file.name, file.type, file.size)
|
|
1875
|
+
|
|
1876
|
+
const reader = new FileReader()
|
|
1877
|
+
reader.onload = (e) => {
|
|
1878
|
+
try {
|
|
1879
|
+
const rawData = e.target.result
|
|
1880
|
+
console.log('File content length:', rawData.length)
|
|
1881
|
+
|
|
1882
|
+
const data = JSON.parse(rawData)
|
|
1883
|
+
console.log('Parsed JSON data:', data)
|
|
1884
|
+
|
|
1885
|
+
// Handle different JSON formats
|
|
1886
|
+
let config
|
|
1887
|
+
if (Array.isArray(data)) {
|
|
1888
|
+
// If it's an array, wrap it in a config object
|
|
1889
|
+
console.log('Detected array format, wrapping in config object')
|
|
1890
|
+
config = {
|
|
1891
|
+
name: 'Loaded Navigation',
|
|
1892
|
+
items: data,
|
|
1893
|
+
createdAt: new Date().toISOString(),
|
|
1894
|
+
updatedAt: new Date().toISOString()
|
|
1895
|
+
}
|
|
1896
|
+
} else if (data.items && Array.isArray(data.items)) {
|
|
1897
|
+
// If it's already a config object with items
|
|
1898
|
+
console.log('Detected config object format')
|
|
1899
|
+
config = {
|
|
1900
|
+
name: data.name || 'Loaded Navigation',
|
|
1901
|
+
items: data.items,
|
|
1902
|
+
createdAt: data.createdAt || new Date().toISOString(),
|
|
1903
|
+
updatedAt: data.updatedAt || new Date().toISOString()
|
|
1904
|
+
}
|
|
1905
|
+
} else {
|
|
1906
|
+
throw new Error('Invalid navigation format. Expected array or object with items array.')
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
console.log('Final config to load:', config)
|
|
1910
|
+
actions.loadNavigation(config)
|
|
1911
|
+
setNavName(config.name)
|
|
1912
|
+
|
|
1913
|
+
// Show success message
|
|
1914
|
+
alert(`Navigation loaded successfully! Found ${config.items.length} root item(s).`)
|
|
1915
|
+
|
|
1916
|
+
// Reset file input so same file can be loaded again
|
|
1917
|
+
event.target.value = ''
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
console.error('Error loading navigation:', error)
|
|
1920
|
+
console.error('Error stack:', error.stack)
|
|
1921
|
+
alert(`Error loading navigation: ${error.message || 'Invalid JSON file'}\n\nCheck console for details.`)
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
reader.onerror = (error) => {
|
|
1925
|
+
console.error('FileReader error:', error)
|
|
1926
|
+
alert('Error reading file. Please try again.')
|
|
1927
|
+
}
|
|
1928
|
+
reader.readAsText(file)
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
return (
|
|
1932
|
+
<Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
1933
|
+
{/* Header */}
|
|
1934
|
+
<Paper sx={{ p: 2, borderRadius: 0 }}>
|
|
1935
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
1936
|
+
<Typography variant="h5">Navigator Builder</Typography>
|
|
1937
|
+
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
1938
|
+
<Button
|
|
1939
|
+
startIcon={<ValidIcon />}
|
|
1940
|
+
onClick={() => actions.validate()}
|
|
1941
|
+
color={state.validation.isValid ? 'success' : 'warning'}
|
|
1942
|
+
>
|
|
1943
|
+
Validate
|
|
1944
|
+
</Button>
|
|
1945
|
+
<Button
|
|
1946
|
+
startIcon={<LoadIcon />}
|
|
1947
|
+
onClick={fetchNavigationSettings}
|
|
1948
|
+
variant="outlined"
|
|
1949
|
+
>
|
|
1950
|
+
Reload
|
|
1951
|
+
</Button>
|
|
1952
|
+
<Button
|
|
1953
|
+
startIcon={<LoadIcon />}
|
|
1954
|
+
onClick={() => document.getElementById('load-input').click()}
|
|
1955
|
+
variant="outlined"
|
|
1956
|
+
>
|
|
1957
|
+
Load File
|
|
1958
|
+
</Button>
|
|
1959
|
+
<input
|
|
1960
|
+
id="load-input"
|
|
1961
|
+
type="file"
|
|
1962
|
+
accept=".json"
|
|
1963
|
+
style={{ display: 'none' }}
|
|
1964
|
+
onChange={handleLoad}
|
|
1965
|
+
/>
|
|
1966
|
+
<Button
|
|
1967
|
+
startIcon={<SaveIcon />}
|
|
1968
|
+
onClick={() => setSaveDialogOpen(true)}
|
|
1969
|
+
variant="contained"
|
|
1970
|
+
disabled={!state.validation.isValid}
|
|
1971
|
+
>
|
|
1972
|
+
Save
|
|
1973
|
+
</Button>
|
|
1974
|
+
</Box>
|
|
1975
|
+
</Box>
|
|
1976
|
+
|
|
1977
|
+
{/* Validation Status */}
|
|
1978
|
+
{state.validation.errors.length > 0 && (
|
|
1979
|
+
<Alert severity="error" sx={{ mt: 1 }}>
|
|
1980
|
+
{state.validation.errors.length} error(s) found
|
|
1981
|
+
</Alert>
|
|
1982
|
+
)}
|
|
1983
|
+
{state.validation.warnings.length > 0 && (
|
|
1984
|
+
<Alert severity="warning" sx={{ mt: 1 }}>
|
|
1985
|
+
{state.validation.warnings.length} warning(s) found
|
|
1986
|
+
</Alert>
|
|
1987
|
+
)}
|
|
1988
|
+
{state.validation.isValid && state.validation.errors.length === 0 && (
|
|
1989
|
+
<Alert severity="success" sx={{ mt: 1 }}>
|
|
1990
|
+
Navigation is valid
|
|
1991
|
+
</Alert>
|
|
1992
|
+
)}
|
|
1993
|
+
</Paper>
|
|
1994
|
+
|
|
1995
|
+
{/* View Mode Toggle */}
|
|
1996
|
+
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
|
1997
|
+
<Tabs
|
|
1998
|
+
value={viewMode}
|
|
1999
|
+
onChange={(e, newValue) => setViewMode(newValue)}
|
|
2000
|
+
sx={{ minHeight: 48 }}
|
|
2001
|
+
>
|
|
2002
|
+
<Tab
|
|
2003
|
+
label="Builder"
|
|
2004
|
+
value="builder"
|
|
2005
|
+
icon={<EditIcon />}
|
|
2006
|
+
iconPosition="start"
|
|
2007
|
+
/>
|
|
2008
|
+
<Tab
|
|
2009
|
+
label="Preview"
|
|
2010
|
+
value="preview"
|
|
2011
|
+
icon={<PreviewIcon />}
|
|
2012
|
+
iconPosition="start"
|
|
2013
|
+
/>
|
|
2014
|
+
</Tabs>
|
|
2015
|
+
</Box>
|
|
2016
|
+
|
|
2017
|
+
{/* Main Content */}
|
|
2018
|
+
{viewMode === 'builder' ? (
|
|
2019
|
+
<Box sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
|
2020
|
+
{/* Left Panel - Properties */}
|
|
2021
|
+
<Box sx={{ width: 300, borderRight: 1, borderColor: 'divider' }}>
|
|
2022
|
+
<PropertiesPanel />
|
|
2023
|
+
</Box>
|
|
2024
|
+
|
|
2025
|
+
{/* Center Panel - Tree */}
|
|
2026
|
+
<Box sx={{ flex: 1 }}>
|
|
2027
|
+
<TreePanel />
|
|
2028
|
+
</Box>
|
|
2029
|
+
</Box>
|
|
2030
|
+
) : (
|
|
2031
|
+
<Box sx={{ flex: 1, overflow: 'hidden' }}>
|
|
2032
|
+
<PreviewPanel />
|
|
2033
|
+
</Box>
|
|
2034
|
+
)}
|
|
2035
|
+
|
|
2036
|
+
{/* Save Dialog */}
|
|
2037
|
+
<Dialog open={saveDialogOpen} onClose={() => setSaveDialogOpen(false)}>
|
|
2038
|
+
<DialogTitle>Save Navigation</DialogTitle>
|
|
2039
|
+
<DialogContent>
|
|
2040
|
+
<TextField
|
|
2041
|
+
label="Navigation Name"
|
|
2042
|
+
value={navName}
|
|
2043
|
+
onChange={(e) => setNavName(e.target.value)}
|
|
2044
|
+
fullWidth
|
|
2045
|
+
sx={{ mt: 1 }}
|
|
2046
|
+
/>
|
|
2047
|
+
</DialogContent>
|
|
2048
|
+
<DialogActions>
|
|
2049
|
+
<Button onClick={() => setSaveDialogOpen(false)}>Cancel</Button>
|
|
2050
|
+
<Button onClick={handleSave} variant="contained">
|
|
2051
|
+
Save
|
|
2052
|
+
</Button>
|
|
2053
|
+
</DialogActions>
|
|
2054
|
+
</Dialog>
|
|
2055
|
+
</Box>
|
|
2056
|
+
)
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// ============================================================================
|
|
2060
|
+
// EXPORT
|
|
2061
|
+
// ============================================================================
|
|
2062
|
+
|
|
2063
|
+
export default function NavigatorBuilderPage() {
|
|
2064
|
+
return (
|
|
2065
|
+
<NavigatorProvider>
|
|
2066
|
+
<NavigatorBuilder />
|
|
2067
|
+
</NavigatorProvider>
|
|
2068
|
+
)
|
|
2069
|
+
}
|
|
2070
|
+
|