diff --git a/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 83e67b49d..4a4d9cf8d 100644 --- a/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -391,13 +391,24 @@ public class EndpointConfiguration { addEndpointToGroup("Advance", "extract-image-scans"); addEndpointToGroup("Advance", "repair"); addEndpointToGroup("Advance", "auto-rename"); - addEndpointToGroup("Advance", "handleData"); addEndpointToGroup("Advance", "scanner-effect"); - addEndpointToGroup("Advance", "show-javascript"); addEndpointToGroup("Advance", "overlay-pdf"); // Backend-only endpoints addEndpointToGroup("Advance", "adjust-contrast"); - addEndpointToGroup("Advance", "pipeline"); + + // Adding endpoints to "Automation" group + addEndpointToGroup("Automation", "handleData"); + addEndpointToGroup("Automation", "automate"); // Alias for handleData (user-friendly name) + addEndpointToGroup("Automation", "pipeline"); + + // Adding endpoints to "DeveloperTools" group + addEndpointToGroup("DeveloperTools", "show-javascript"); + + // Adding endpoints to "DeveloperDocs" group (fake endpoints for link-only tools) + addEndpointToGroup("DeveloperDocs", "dev-api-docs"); + addEndpointToGroup("DeveloperDocs", "dev-folder-scanning-docs"); + addEndpointToGroup("DeveloperDocs", "dev-sso-guide-docs"); + addEndpointToGroup("DeveloperDocs", "dev-airgapped-docs"); // CLI addEndpointToGroup("CLI", "compress-pdf"); diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index fcf72d6ae..c6988098c 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -616,6 +616,8 @@ public class ApplicationProperties { private String appNameNavbar; private List languages; private String logoStyle = "classic"; // Options: "classic" (default) or "modern" + private boolean defaultHideUnavailableTools = false; + private boolean defaultHideUnavailableConversions = false; public String getAppNameNavbar() { return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java index b83848d67..247e8e18b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java @@ -153,6 +153,14 @@ public class ConfigController { configData.put("logoStyle", applicationProperties.getUi().getLogoStyle()); configData.put("defaultLocale", applicationProperties.getSystem().getDefaultLocale()); + // User preference defaults + configData.put( + "defaultHideUnavailableTools", + applicationProperties.getUi().isDefaultHideUnavailableTools()); + configData.put( + "defaultHideUnavailableConversions", + applicationProperties.getUi().isDefaultHideUnavailableConversions()); + // Security settings // enableLogin requires both the config flag AND proprietary features to be loaded // If userService is null, proprietary module isn't loaded diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index 8b1cbdeb8..a15da437d 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -230,10 +230,12 @@ ui: appNameNavbar: "" # name displayed on the navigation bar logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo) languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled. + defaultHideUnavailableTools: false # Default user preference: hide disabled tools instead of greying them out + defaultHideUnavailableConversions: false # Default user preference: hide disabled conversion options instead of greying them out endpoints: toRemove: [] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) - groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice']) + groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice', 'DeveloperTools', 'DeveloperDocs', 'Automation']) metrics: enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index a72515fa0..b4cb435dc 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -829,8 +829,9 @@ label = "Username" [admin.settings.endpoints] description = "Control which API endpoints and endpoint groups are available." management = "Endpoint Management" -note = "Note: Disabling endpoints restricts API access but does not remove UI components. Restart required for changes to take effect." title = "API Endpoints" +userDefaults = "User Preference Defaults" +userDefaultsDescription = "Set default values for user preferences. Users can override these in their personal settings." [admin.settings.endpoints.groupsToRemove] description = "Select endpoint groups to disable" @@ -840,6 +841,14 @@ label = "Disabled Endpoint Groups" description = "Select individual endpoints to disable" label = "Disabled Endpoints" +[admin.settings.endpoints.defaultHideUnavailableTools] +description = "Remove disabled tools instead of showing them greyed out" +label = "Hide unavailable tools by default" + +[admin.settings.endpoints.defaultHideUnavailableConversions] +description = "Remove disabled conversion options instead of showing them greyed out" +label = "Hide unavailable conversions by default" + [admin.settings.enterpriseRequired] message = "An Enterprise license is required to access {{featureName}}. You are viewing demo data for reference." title = "Enterprise License Required" diff --git a/frontend/src/core/components/AppProviders.tsx b/frontend/src/core/components/AppProviders.tsx index cbf8e4923..75c7d281c 100644 --- a/frontend/src/core/components/AppProviders.tsx +++ b/frontend/src/core/components/AppProviders.tsx @@ -7,8 +7,8 @@ import { FilesModalProvider } from "@app/contexts/FilesModalContext"; import { ToolWorkflowProvider } from "@app/contexts/ToolWorkflowContext"; import { HotkeyProvider } from "@app/contexts/HotkeyContext"; import { SidebarProvider } from "@app/contexts/SidebarContext"; -import { PreferencesProvider } from "@app/contexts/PreferencesContext"; -import { AppConfigProvider, AppConfigProviderProps, AppConfigRetryOptions } from "@app/contexts/AppConfigContext"; +import { PreferencesProvider, usePreferences } from "@app/contexts/PreferencesContext"; +import { AppConfigProvider, AppConfigProviderProps, AppConfigRetryOptions, useAppConfig } from "@app/contexts/AppConfigContext"; import { RightRailProvider } from "@app/contexts/RightRailContext"; import { ViewerProvider } from "@app/contexts/ViewerContext"; import { SignatureProvider } from "@app/contexts/SignatureContext"; @@ -70,6 +70,24 @@ export interface AppProvidersProps { appConfigProviderProps?: Partial; } +// Component to sync server defaults to preferences when AppConfig loads +function ServerDefaultsSync() { + const { config } = useAppConfig(); + const { updateServerDefaults } = usePreferences(); + + useEffect(() => { + if (config) { + const serverDefaults = { + hideUnavailableTools: config.defaultHideUnavailableTools ?? false, + hideUnavailableConversions: config.defaultHideUnavailableConversions ?? false, + }; + updateServerDefaults(serverDefaults); + } + }, [config, updateServerDefaults]); + + return null; +} + /** * Core application providers * Contains all providers needed for the core @@ -86,6 +104,7 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide > + diff --git a/frontend/src/core/components/shared/QuickAccessBar.tsx b/frontend/src/core/components/shared/QuickAccessBar.tsx index e13808c0f..cac8704f7 100644 --- a/frontend/src/core/components/shared/QuickAccessBar.tsx +++ b/frontend/src/core/components/shared/QuickAccessBar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, forwardRef, useEffect } from "react"; +import React, { useState, useRef, forwardRef, useEffect, useMemo } from "react"; import { Stack, Divider, Menu, Indicator } from "@mantine/core"; import { useTranslation } from 'react-i18next'; import { useNavigate, useLocation } from 'react-router-dom'; @@ -34,7 +34,7 @@ const QuickAccessBar = forwardRef((_, ref) => { const location = useLocation(); const { isRainbowMode } = useRainbowThemeContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); - const { handleReaderToggle, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow(); + const { handleReaderToggle, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool, toolAvailability } = useToolWorkflow(); const { hasUnsavedChanges } = useNavigationState(); const { actions: navigationActions } = useNavigationActions(); const { getToolNavigation } = useSidebarNavigation(); @@ -119,14 +119,14 @@ const QuickAccessBar = forwardRef((_, ref) => { ); }; - const mainButtons: ButtonConfig[] = [ + const mainButtons: ButtonConfig[] = useMemo(() => [ { id: 'read', name: t("quickAccess.reader", "Reader"), icon: , - size: 'md', + size: 'md' as const, isRound: false, - type: 'navigation', + type: 'navigation' as const, onClick: () => { setActiveButton('read'); handleReaderToggle(); @@ -136,9 +136,9 @@ const QuickAccessBar = forwardRef((_, ref) => { id: 'automate', name: t("quickAccess.automate", "Automate"), icon: , - size: 'md', + size: 'md' as const, isRound: false, - type: 'navigation', + type: 'navigation' as const, onClick: () => { setActiveButton('automate'); // If already on automate tool, reset it directly @@ -149,7 +149,14 @@ const QuickAccessBar = forwardRef((_, ref) => { } } }, - ]; + ].filter(button => { + // Filter out buttons for disabled tools + // 'read' is always available (viewer mode) + if (button.id === 'read') return true; + // Check if tool is actually available (not just present in registry) + const availability = toolAvailability[button.id as keyof typeof toolAvailability]; + return availability?.available !== false; + }), [t, setActiveButton, handleReaderToggle, selectedToolKey, resetTool, handleToolSelect, toolAvailability]); const middleButtons: ButtonConfig[] = [ { diff --git a/frontend/src/core/contexts/AppConfigContext.tsx b/frontend/src/core/contexts/AppConfigContext.tsx index cf2010e89..4a8b8095f 100644 --- a/frontend/src/core/contexts/AppConfigContext.tsx +++ b/frontend/src/core/contexts/AppConfigContext.tsx @@ -58,6 +58,8 @@ export interface AppConfig { error?: string; isNewServer?: boolean; isNewUser?: boolean; + defaultHideUnavailableTools?: boolean; + defaultHideUnavailableConversions?: boolean; } export type AppConfigBootstrapMode = 'blocking' | 'non-blocking'; diff --git a/frontend/src/core/contexts/PreferencesContext.tsx b/frontend/src/core/contexts/PreferencesContext.tsx index b28d1b35b..e8dc32eb4 100644 --- a/frontend/src/core/contexts/PreferencesContext.tsx +++ b/frontend/src/core/contexts/PreferencesContext.tsx @@ -8,13 +8,16 @@ interface PreferencesContextValue { value: UserPreferences[K] ) => void; resetPreferences: () => void; + updateServerDefaults: (defaults: Partial) => void; } const PreferencesContext = createContext(undefined); -export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const PreferencesProvider: React.FC<{ + children: React.ReactNode; +}> = ({ children }) => { const [preferences, setPreferences] = useState(() => { - // Load preferences synchronously on mount + // Load preferences synchronously on mount with hardcoded defaults return preferencesService.getAllPreferences(); }); @@ -34,12 +37,19 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c setPreferences(preferencesService.getAllPreferences()); }, []); + const updateServerDefaults = useCallback((defaults: Partial) => { + preferencesService.setServerDefaults(defaults); + // Reload preferences to apply server defaults + setPreferences(preferencesService.getAllPreferences()); + }, []); + return ( {children} diff --git a/frontend/src/core/data/useTranslatedToolRegistry.tsx b/frontend/src/core/data/useTranslatedToolRegistry.tsx index a79459cb0..0bfb6c9f1 100644 --- a/frontend/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/core/data/useTranslatedToolRegistry.tsx @@ -689,7 +689,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.AUTOMATION, maxFiles: -1, supportedFormats: CONVERT_SUPPORTED_FORMATS, - endpoints: ["handleData"], + endpoints: ["automate"], synonyms: getSynonyms(t, "automate"), automationSettings: null, }, @@ -808,6 +808,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: devApiLink, + endpoints: ["dev-api-docs"], synonyms: getSynonyms(t, "devApi"), supportsAutomate: false, automationSettings: null @@ -820,6 +821,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Configuration/Folder%20Scanning/", + endpoints: ["dev-folder-scanning-docs"], synonyms: getSynonyms(t, "devFolderScanning"), supportsAutomate: false, automationSettings: null @@ -832,6 +834,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Configuration/Single%20Sign-On%20Configuration/", + endpoints: ["dev-sso-guide-docs"], synonyms: getSynonyms(t, "devSsoGuide"), supportsAutomate: false, automationSettings: null @@ -844,6 +847,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Paid-Offerings/#activating-your-license", + endpoints: ["dev-airgapped-docs"], synonyms: getSynonyms(t, "devAirgapped"), supportsAutomate: false, automationSettings: null diff --git a/frontend/src/core/pages/HomePage.tsx b/frontend/src/core/pages/HomePage.tsx index 3d387af30..f99cbfc8b 100644 --- a/frontend/src/core/pages/HomePage.tsx +++ b/frontend/src/core/pages/HomePage.tsx @@ -42,6 +42,7 @@ export default function HomePage() { handleBackToTools, readerMode, setLeftPanelView, + toolAvailability, } = useToolWorkflow(); const { openFilesModal } = useFilesModalContext(); @@ -249,19 +250,21 @@ export default function HomePage() { {t('quickAccess.allTools', 'Tools')} - + {toolAvailability['automate']?.available !== false && ( + + )} + + + + +
+ {t('admin.settings.endpoints.userDefaults', 'User Preference Defaults')} + + {t('admin.settings.endpoints.userDefaultsDescription', 'Set default values for user preferences. Users can override these in their personal settings.')} + +
+ + + {t('admin.settings.endpoints.defaultHideUnavailableTools.label', 'Hide unavailable tools by default')} + + + } + description={t('admin.settings.endpoints.defaultHideUnavailableTools.description', 'Remove disabled tools instead of showing them greyed out')} + checked={uiSettings.defaultHideUnavailableTools || false} + onChange={(e) => { + if (!loginEnabled) return; + setUiSettings({ ...uiSettings, defaultHideUnavailableTools: e.currentTarget.checked }); + }} + disabled={!loginEnabled} + /> + + + {t('admin.settings.endpoints.defaultHideUnavailableConversions.label', 'Hide unavailable conversions by default')} + + + } + description={t('admin.settings.endpoints.defaultHideUnavailableConversions.description', 'Remove disabled conversion options instead of showing them greyed out')} + checked={uiSettings.defaultHideUnavailableConversions || false} + onChange={(e) => { + if (!loginEnabled) return; + setUiSettings({ ...uiSettings, defaultHideUnavailableConversions: e.currentTarget.checked }); + }} + disabled={!loginEnabled} + /> +
+
+ + +