mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
admin onboarding (#4863)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
This commit is contained in:
parent
ebf4bab80b
commit
3cf89b6ede
@ -3539,7 +3539,13 @@
|
||||
"account": "Account",
|
||||
"config": "Config",
|
||||
"adminSettings": "Admin Settings",
|
||||
"allTools": "All Tools"
|
||||
"allTools": "All Tools",
|
||||
"helpMenu": {
|
||||
"toolsTour": "Tools Tour",
|
||||
"toolsTourDesc": "Learn what the tools can do",
|
||||
"adminTour": "Admin Tour",
|
||||
"adminTourDesc": "Explore admin settings & features"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"error": "Error",
|
||||
@ -4674,6 +4680,17 @@
|
||||
"startTour": "Start Tour",
|
||||
"startTourDescription": "Take a guided tour of Stirling PDF's key features"
|
||||
},
|
||||
"adminOnboarding": {
|
||||
"welcome": "Welcome to the <strong>Admin Tour</strong>! Let's explore the powerful enterprise features and settings available to system administrators.",
|
||||
"configButton": "Click the <strong>Config</strong> button to access all system settings and administrative controls.",
|
||||
"settingsOverview": "This is the <strong>Settings Panel</strong>. Admin settings are organised by category for easy navigation.",
|
||||
"teamsAndUsers": "Manage <strong>Teams</strong> and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself.",
|
||||
"systemCustomization": "We have extensive ways to customise the UI: <strong>System Settings</strong> let you change the app name and languages, <strong>Features</strong> allows server certificate management, and <strong>Endpoints</strong> lets you enable or disable specific tools for your users.",
|
||||
"databaseSection": "For advanced production environments, we have settings to allow <strong>external database hookups</strong> so you can integrate with your existing infrastructure.",
|
||||
"connectionsSection": "The <strong>Connections</strong> section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications.",
|
||||
"adminTools": "Finally, we have advanced administration tools like <strong>Auditing</strong> to track system activity and <strong>Usage Analytics</strong> to monitor how your users interact with the platform.",
|
||||
"wrapUp": "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the <strong>Help</strong> menu."
|
||||
},
|
||||
"workspace": {
|
||||
"title": "Workspace",
|
||||
"people": {
|
||||
|
||||
@ -14,6 +14,7 @@ import { ViewerProvider } from "@app/contexts/ViewerContext";
|
||||
import { SignatureProvider } from "@app/contexts/SignatureContext";
|
||||
import { OnboardingProvider } from "@app/contexts/OnboardingContext";
|
||||
import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext";
|
||||
import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext";
|
||||
import ErrorBoundary from "@app/components/shared/ErrorBoundary";
|
||||
import { useScarfTracking } from "@app/hooks/useScarfTracking";
|
||||
import { useAppInitialization } from "@app/hooks/useAppInitialization";
|
||||
@ -59,7 +60,9 @@ export function AppProviders({ children, appConfigRetryOptions }: AppProvidersPr
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<TourOrchestrationProvider>
|
||||
{children}
|
||||
<AdminTourOrchestrationProvider>
|
||||
{children}
|
||||
</AdminTourOrchestrationProvider>
|
||||
</TourOrchestrationProvider>
|
||||
</RightRailProvider>
|
||||
</SignatureProvider>
|
||||
|
||||
@ -6,3 +6,28 @@
|
||||
ry: 8px;
|
||||
filter: drop-shadow(0 0 10px var(--mantine-primary-color-filled));
|
||||
}
|
||||
|
||||
/* Add glowing border to navigation items during admin tour */
|
||||
.modal-nav-item.tour-nav-glow {
|
||||
position: relative;
|
||||
box-shadow:
|
||||
0 0 0 2px var(--mantine-primary-color-filled),
|
||||
0 0 15px var(--mantine-primary-color-filled),
|
||||
inset 0 0 15px rgba(59, 130, 246, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 0 3px var(--mantine-primary-color-filled),
|
||||
0 0 20px var(--mantine-primary-color-filled),
|
||||
inset 0 0 20px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 0 3px var(--mantine-primary-color-filled),
|
||||
0 0 30px var(--mantine-primary-color-filled),
|
||||
inset 0 0 30px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { CloseButton, ActionIcon } from '@mantine/core';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { useTourOrchestration } from '@app/contexts/TourOrchestrationContext';
|
||||
import { useAdminTourOrchestration } from '@app/contexts/AdminTourOrchestrationContext';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import TourWelcomeModal from '@app/components/onboarding/TourWelcomeModal';
|
||||
@ -32,6 +33,18 @@ enum TourStep {
|
||||
WRAP_UP,
|
||||
}
|
||||
|
||||
enum AdminTourStep {
|
||||
WELCOME,
|
||||
CONFIG_BUTTON,
|
||||
SETTINGS_OVERVIEW,
|
||||
TEAMS_AND_USERS,
|
||||
SYSTEM_CUSTOMIZATION,
|
||||
DATABASE_SECTION,
|
||||
CONNECTIONS_SECTION,
|
||||
ADMIN_TOOLS,
|
||||
WRAP_UP,
|
||||
}
|
||||
|
||||
function TourContent() {
|
||||
const { isOpen } = useOnboarding();
|
||||
const { setIsOpen, setCurrentStep } = useTour();
|
||||
@ -54,8 +67,36 @@ function TourContent() {
|
||||
|
||||
export default function OnboardingTour() {
|
||||
const { t } = useTranslation();
|
||||
const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour } = useOnboarding();
|
||||
const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour, tourType, isOpen } = useOnboarding();
|
||||
const { openFilesModal, closeFilesModal } = useFilesModalContext();
|
||||
|
||||
// Helper to add glow to multiple elements
|
||||
const addGlowToElements = (selectors: string[]) => {
|
||||
selectors.forEach(selector => {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
if (selector === '[data-tour="settings-content-area"]') {
|
||||
element.classList.add('tour-content-glow');
|
||||
} else {
|
||||
element.classList.add('tour-nav-glow');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Helper to remove all glows
|
||||
const removeAllGlows = () => {
|
||||
document.querySelectorAll('.tour-content-glow').forEach(el => el.classList.remove('tour-content-glow'));
|
||||
document.querySelectorAll('.tour-nav-glow').forEach(el => el.classList.remove('tour-nav-glow'));
|
||||
};
|
||||
|
||||
// Cleanup glows when tour closes
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) {
|
||||
removeAllGlows();
|
||||
}
|
||||
return () => removeAllGlows();
|
||||
}, [isOpen]);
|
||||
const {
|
||||
saveWorkbenchState,
|
||||
restoreWorkbenchState,
|
||||
@ -70,6 +111,13 @@ export default function OnboardingTour() {
|
||||
modifyCropSettings,
|
||||
executeTool,
|
||||
} = useTourOrchestration();
|
||||
const {
|
||||
saveAdminState,
|
||||
restoreAdminState,
|
||||
openConfigModal,
|
||||
navigateToSection,
|
||||
scrollNavToSection,
|
||||
} = useAdminTourOrchestration();
|
||||
|
||||
// Define steps as object keyed by enum - TypeScript ensures all keys are present
|
||||
const stepsConfig: Record<TourStep, StepType> = {
|
||||
@ -202,8 +250,125 @@ export default function OnboardingTour() {
|
||||
},
|
||||
};
|
||||
|
||||
// Convert to array using enum's numeric ordering
|
||||
const steps = Object.values(stepsConfig);
|
||||
// Define admin tour steps
|
||||
const adminStepsConfig: Record<AdminTourStep, StepType> = {
|
||||
[AdminTourStep.WELCOME]: {
|
||||
selector: '[data-tour="config-button"]',
|
||||
content: t('adminOnboarding.welcome', "Welcome to the <strong>Admin Tour</strong>! Let's explore the powerful enterprise features and settings available to system administrators."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
action: () => {
|
||||
saveAdminState();
|
||||
},
|
||||
},
|
||||
[AdminTourStep.CONFIG_BUTTON]: {
|
||||
selector: '[data-tour="config-button"]',
|
||||
content: t('adminOnboarding.configButton', "Click the <strong>Config</strong> button to access all system settings and administrative controls."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
actionAfter: () => {
|
||||
openConfigModal();
|
||||
},
|
||||
},
|
||||
[AdminTourStep.SETTINGS_OVERVIEW]: {
|
||||
selector: '.modal-nav',
|
||||
content: t('adminOnboarding.settingsOverview', "This is the <strong>Settings Panel</strong>. Admin settings are organised by category for easy navigation."),
|
||||
position: 'right',
|
||||
padding: 0,
|
||||
action: () => {
|
||||
removeAllGlows();
|
||||
},
|
||||
},
|
||||
[AdminTourStep.TEAMS_AND_USERS]: {
|
||||
selector: '[data-tour="admin-people-nav"]',
|
||||
highlightedSelectors: ['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]'],
|
||||
content: t('adminOnboarding.teamsAndUsers', "Manage <strong>Teams</strong> and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
action: () => {
|
||||
removeAllGlows();
|
||||
navigateToSection('people');
|
||||
setTimeout(() => {
|
||||
addGlowToElements(['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]']);
|
||||
}, 100);
|
||||
},
|
||||
},
|
||||
[AdminTourStep.SYSTEM_CUSTOMIZATION]: {
|
||||
selector: '[data-tour="admin-adminGeneral-nav"]',
|
||||
highlightedSelectors: ['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]'],
|
||||
content: t('adminOnboarding.systemCustomization', "We have extensive ways to customise the UI: <strong>System Settings</strong> let you change the app name and languages, <strong>Features</strong> allows server certificate management, and <strong>Endpoints</strong> lets you enable or disable specific tools for your users."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
action: () => {
|
||||
removeAllGlows();
|
||||
navigateToSection('adminGeneral');
|
||||
setTimeout(() => {
|
||||
addGlowToElements(['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]']);
|
||||
}, 100);
|
||||
},
|
||||
},
|
||||
[AdminTourStep.DATABASE_SECTION]: {
|
||||
selector: '[data-tour="admin-adminDatabase-nav"]',
|
||||
highlightedSelectors: ['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]'],
|
||||
content: t('adminOnboarding.databaseSection', "For advanced production environments, we have settings to allow <strong>external database hookups</strong> so you can integrate with your existing infrastructure."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
action: () => {
|
||||
removeAllGlows();
|
||||
navigateToSection('adminDatabase');
|
||||
setTimeout(() => {
|
||||
addGlowToElements(['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]']);
|
||||
}, 100);
|
||||
},
|
||||
},
|
||||
[AdminTourStep.CONNECTIONS_SECTION]: {
|
||||
selector: '[data-tour="admin-adminConnections-nav"]',
|
||||
highlightedSelectors: ['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]'],
|
||||
content: t('adminOnboarding.connectionsSection', "The <strong>Connections</strong> section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
action: () => {
|
||||
removeAllGlows();
|
||||
navigateToSection('adminConnections');
|
||||
setTimeout(() => {
|
||||
addGlowToElements(['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]']);
|
||||
}, 100);
|
||||
},
|
||||
actionAfter: async () => {
|
||||
// Scroll for the NEXT step before it shows
|
||||
await scrollNavToSection('adminAudit');
|
||||
},
|
||||
},
|
||||
[AdminTourStep.ADMIN_TOOLS]: {
|
||||
selector: '[data-tour="admin-adminAudit-nav"]',
|
||||
highlightedSelectors: ['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]'],
|
||||
content: t('adminOnboarding.adminTools', "Finally, we have advanced administration tools like <strong>Auditing</strong> to track system activity and <strong>Usage Analytics</strong> to monitor how your users interact with the platform."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
action: () => {
|
||||
// Just navigate, scroll already happened in previous step
|
||||
removeAllGlows();
|
||||
navigateToSection('adminAudit');
|
||||
setTimeout(() => {
|
||||
addGlowToElements(['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]']);
|
||||
}, 100);
|
||||
},
|
||||
},
|
||||
[AdminTourStep.WRAP_UP]: {
|
||||
selector: '[data-tour="help-button"]',
|
||||
content: t('adminOnboarding.wrapUp', "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the <strong>Help</strong> menu."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
action: () => {
|
||||
removeAllGlows();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Select steps based on tour type
|
||||
const steps = tourType === 'admin'
|
||||
? Object.values(adminStepsConfig)
|
||||
: Object.values(stepsConfig);
|
||||
|
||||
const advanceTour = ({ setCurrentStep, currentStep, steps, setIsOpen }: {
|
||||
setCurrentStep: (value: number | ((prev: number) => number)) => void;
|
||||
@ -213,7 +378,11 @@ export default function OnboardingTour() {
|
||||
}) => {
|
||||
if (steps && currentStep === steps.length - 1) {
|
||||
setIsOpen(false);
|
||||
restoreWorkbenchState();
|
||||
if (tourType === 'admin') {
|
||||
restoreAdminState();
|
||||
} else {
|
||||
restoreWorkbenchState();
|
||||
}
|
||||
completeTour();
|
||||
} else if (steps) {
|
||||
setCurrentStep((s) => (s === steps.length - 1 ? 0 : s + 1));
|
||||
@ -222,7 +391,11 @@ export default function OnboardingTour() {
|
||||
|
||||
const handleCloseTour = ({ setIsOpen }: { setIsOpen: (value: boolean) => void }) => {
|
||||
setIsOpen(false);
|
||||
restoreWorkbenchState();
|
||||
if (tourType === 'admin') {
|
||||
restoreAdminState();
|
||||
} else {
|
||||
restoreWorkbenchState();
|
||||
}
|
||||
completeTour();
|
||||
};
|
||||
|
||||
@ -243,7 +416,9 @@ export default function OnboardingTour() {
|
||||
}}
|
||||
/>
|
||||
<TourProvider
|
||||
key={tourType}
|
||||
steps={steps}
|
||||
maskClassName={tourType === 'admin' ? 'admin-tour-mask' : undefined}
|
||||
onClickClose={handleCloseTour}
|
||||
onClickMask={advanceTour}
|
||||
onClickHighlighted={(e, clickProps) => {
|
||||
|
||||
@ -154,6 +154,7 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
opacity: isDisabled ? 0.5 : 1,
|
||||
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
data-tour={`admin-${item.key}-nav`}
|
||||
>
|
||||
<LocalIcon icon={item.icon} width={iconSize} height={iconSize} style={{ color }} />
|
||||
{!isMobile && (
|
||||
@ -185,7 +186,7 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
</div>
|
||||
|
||||
{/* Right content */}
|
||||
<div className="modal-content">
|
||||
<div className="modal-content" data-tour="settings-content-area">
|
||||
<div className="modal-content-scroll">
|
||||
{/* Sticky header with section title and small close button */}
|
||||
<div
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, forwardRef, useEffect } from "react";
|
||||
import { ActionIcon, Stack, Divider } from "@mantine/core";
|
||||
import { ActionIcon, Stack, Divider, Menu } from "@mantine/core";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
@ -21,6 +21,7 @@ import {
|
||||
getNavButtonStyle,
|
||||
getActiveNavButton,
|
||||
} from '@app/components/shared/quickAccessBar/QuickAccessBar';
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex';
|
||||
|
||||
const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const { t } = useTranslation();
|
||||
@ -179,7 +180,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
size: 'lg',
|
||||
type: 'action',
|
||||
onClick: () => {
|
||||
startTour();
|
||||
// This will be overridden by the wrapper logic
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -258,11 +259,70 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
|
||||
{/* Bottom section */}
|
||||
<Stack gap="lg" align="center">
|
||||
{bottomButtons.map((config, index) => (
|
||||
<React.Fragment key={config.id}>
|
||||
{renderNavButton(config, index)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{bottomButtons.map((buttonConfig, index) => {
|
||||
// Handle help button with menu or direct action
|
||||
if (buttonConfig.id === 'help') {
|
||||
const isAdmin = config?.isAdmin === true;
|
||||
|
||||
// If not admin, just show button that starts tools tour directly
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div
|
||||
key={buttonConfig.id}
|
||||
data-tour="help-button"
|
||||
onClick={() => startTour('tools')}
|
||||
>
|
||||
{renderNavButton(buttonConfig, index)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If admin, show menu with both options
|
||||
return (
|
||||
<div key={buttonConfig.id} data-tour="help-button">
|
||||
<Menu position="right" offset={10} zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE}>
|
||||
<Menu.Target>
|
||||
<div>{renderNavButton(buttonConfig, index)}</div>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<LocalIcon icon="view-carousel-rounded" width="1.25rem" height="1.25rem" />}
|
||||
onClick={() => startTour('tools')}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>
|
||||
{t("quickAccess.helpMenu.toolsTour", "Tools Tour")}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', opacity: 0.7 }}>
|
||||
{t("quickAccess.helpMenu.toolsTourDesc", "Learn what the tools can do")}
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<LocalIcon icon="admin-panel-settings-rounded" width="1.25rem" height="1.25rem" />}
|
||||
onClick={() => startTour('admin')}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>
|
||||
{t("quickAccess.helpMenu.adminTour", "Admin Tour")}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', opacity: 0.7 }}>
|
||||
{t("quickAccess.helpMenu.adminTourDesc", "Explore admin settings & features")}
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={buttonConfig.id}>
|
||||
{renderNavButton(buttonConfig, index)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
124
frontend/src/core/contexts/AdminTourOrchestrationContext.tsx
Normal file
124
frontend/src/core/contexts/AdminTourOrchestrationContext.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { createContext, useContext, useCallback, useRef } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
interface AdminTourOrchestrationContextType {
|
||||
// State management
|
||||
saveAdminState: () => void;
|
||||
restoreAdminState: () => void;
|
||||
|
||||
// Modal & navigation
|
||||
openConfigModal: () => void;
|
||||
closeConfigModal: () => void;
|
||||
navigateToSection: (section: string) => void;
|
||||
scrollNavToSection: (section: string) => void;
|
||||
|
||||
// Section-specific actions
|
||||
scrollToSetting: (settingId: string) => void;
|
||||
}
|
||||
|
||||
const AdminTourOrchestrationContext = createContext<AdminTourOrchestrationContextType | undefined>(undefined);
|
||||
|
||||
export const AdminTourOrchestrationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Store the user's location before tour starts
|
||||
const savedLocationRef = useRef<string>('');
|
||||
|
||||
const saveAdminState = useCallback(() => {
|
||||
savedLocationRef.current = location.pathname;
|
||||
console.log('Saving admin state, location:', location.pathname);
|
||||
}, [location.pathname]);
|
||||
|
||||
const restoreAdminState = useCallback(() => {
|
||||
console.log('Restoring admin state, saved location:', savedLocationRef.current);
|
||||
|
||||
// Navigate back to saved location or home
|
||||
const targetPath = savedLocationRef.current || '/';
|
||||
navigate(targetPath, { replace: true });
|
||||
|
||||
savedLocationRef.current = '';
|
||||
}, [navigate]);
|
||||
|
||||
const openConfigModal = useCallback(() => {
|
||||
// Navigate to settings overview to open the modal
|
||||
navigate('/settings/overview');
|
||||
}, [navigate]);
|
||||
|
||||
const closeConfigModal = useCallback(() => {
|
||||
// Navigate back to home to close the modal
|
||||
navigate('/', { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
const navigateToSection = useCallback((section: string) => {
|
||||
navigate(`/settings/${section}`);
|
||||
}, [navigate]);
|
||||
|
||||
const scrollNavToSection = useCallback((section: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
const navElement = document.querySelector(`[data-tour="admin-${section}-nav"]`) as HTMLElement;
|
||||
const scrollContainer = document.querySelector('.modal-nav-scroll') as HTMLElement;
|
||||
|
||||
if (navElement && scrollContainer) {
|
||||
// Get the position of the nav element relative to the scroll container
|
||||
const navTop = navElement.offsetTop;
|
||||
const containerHeight = scrollContainer.clientHeight;
|
||||
const navHeight = navElement.offsetHeight;
|
||||
|
||||
// Calculate scroll position to center the element
|
||||
const scrollTo = navTop - (containerHeight / 2) + (navHeight / 2);
|
||||
|
||||
// Instant scroll to avoid timing issues
|
||||
scrollContainer.scrollTo({
|
||||
top: Math.max(0, scrollTo),
|
||||
behavior: 'auto'
|
||||
});
|
||||
|
||||
// Use multiple animation frames to ensure browser has fully updated
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const scrollToSetting = useCallback((settingId: string) => {
|
||||
// Wait for the DOM to update, then scroll to the setting
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`[data-tour="${settingId}"]`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
const value: AdminTourOrchestrationContextType = {
|
||||
saveAdminState,
|
||||
restoreAdminState,
|
||||
openConfigModal,
|
||||
closeConfigModal,
|
||||
navigateToSection,
|
||||
scrollNavToSection,
|
||||
scrollToSetting,
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminTourOrchestrationContext.Provider value={value}>
|
||||
{children}
|
||||
</AdminTourOrchestrationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAdminTourOrchestration = (): AdminTourOrchestrationContextType => {
|
||||
const context = useContext(AdminTourOrchestrationContext);
|
||||
if (!context) {
|
||||
throw new Error('useAdminTourOrchestration must be used within AdminTourOrchestrationProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@ -2,14 +2,17 @@ import React, { createContext, useContext, useState, useEffect, useCallback } fr
|
||||
import { usePreferences } from '@app/contexts/PreferencesContext';
|
||||
import { useShouldShowWelcomeModal } from '@app/hooks/useShouldShowWelcomeModal';
|
||||
|
||||
export type TourType = 'tools' | 'admin';
|
||||
|
||||
interface OnboardingContextValue {
|
||||
isOpen: boolean;
|
||||
currentStep: number;
|
||||
tourType: TourType;
|
||||
setCurrentStep: (step: number) => void;
|
||||
startTour: () => void;
|
||||
startTour: (type?: TourType) => void;
|
||||
closeTour: () => void;
|
||||
completeTour: () => void;
|
||||
resetTour: () => void;
|
||||
resetTour: (type?: TourType) => void;
|
||||
showWelcomeModal: boolean;
|
||||
setShowWelcomeModal: (show: boolean) => void;
|
||||
}
|
||||
@ -20,6 +23,7 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
||||
const { updatePreference } = usePreferences();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [tourType, setTourType] = useState<TourType>('tools');
|
||||
const [showWelcomeModal, setShowWelcomeModal] = useState(false);
|
||||
const shouldShow = useShouldShowWelcomeModal();
|
||||
|
||||
@ -30,7 +34,8 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
||||
}
|
||||
}, [shouldShow]);
|
||||
|
||||
const startTour = useCallback(() => {
|
||||
const startTour = useCallback((type: TourType = 'tools') => {
|
||||
setTourType(type);
|
||||
setCurrentStep(0);
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
@ -44,8 +49,9 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
||||
updatePreference('hasCompletedOnboarding', true);
|
||||
}, [updatePreference]);
|
||||
|
||||
const resetTour = useCallback(() => {
|
||||
const resetTour = useCallback((type: TourType = 'tools') => {
|
||||
updatePreference('hasCompletedOnboarding', false);
|
||||
setTourType(type);
|
||||
setCurrentStep(0);
|
||||
setIsOpen(true);
|
||||
}, [updatePreference]);
|
||||
@ -55,6 +61,7 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
||||
value={{
|
||||
isOpen,
|
||||
currentStep,
|
||||
tourType,
|
||||
setCurrentStep,
|
||||
startTour,
|
||||
closeTour,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user