mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Desktop connection SaaS: config, billing, team support (#5768)
Co-authored-by: James Brunton <[email protected]> Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
co-authored by
James Brunton
James Brunton
parent
86072ec91a
commit
5c39acecd8
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Core stub for QuickAccessBar footer extensions
|
||||
* Desktop build overrides this with actual credit counter implementation
|
||||
*/
|
||||
|
||||
interface QuickAccessBarFooterExtensionsProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function QuickAccessBarFooterExtensions(_props: QuickAccessBarFooterExtensionsProps) {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
interface CloudBadgeProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub component for cloud badge (desktop override provides real implementation)
|
||||
* In web builds, this returns null since cloud routing is desktop-only
|
||||
*/
|
||||
export function CloudBadge(_props: CloudBadgeProps) {
|
||||
return null; // Stub - does nothing in web builds
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
getActiveNavButton,
|
||||
} from '@app/components/shared/quickAccessBar/QuickAccessBar';
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex';
|
||||
import { QuickAccessBarFooterExtensions } from '@app/components/quickAccessBar/QuickAccessBarFooterExtensions';
|
||||
|
||||
const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -265,6 +266,8 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
{/* Spacer to push bottom buttons to bottom */}
|
||||
<div className="spacer" />
|
||||
|
||||
<QuickAccessBarFooterExtensions className="quick-access-footer" />
|
||||
|
||||
{/* Bottom section */}
|
||||
<Stack gap="lg" align="stretch">
|
||||
{bottomButtons.map((buttonConfig, index) => {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Core stub for SaaS Teams Section
|
||||
* Desktop layer provides the real implementation
|
||||
* This component only appears in SaaS mode
|
||||
*/
|
||||
export function SaaSTeamsSection() {
|
||||
// Core stub - return null (no team management in web builds)
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Core stub for SaasPlanSection
|
||||
* This component returns null in non-desktop builds
|
||||
* The desktop layer shadows this with the real implementation
|
||||
*/
|
||||
export function SaasPlanSection() {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Core stub for Credit Exhausted Modal
|
||||
* Desktop build overrides this with actual modal implementation
|
||||
*/
|
||||
|
||||
interface CreditExhaustedModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function CreditExhaustedModal(_props: CreditExhaustedModalProps) {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Core stub for Insufficient Credits Modal
|
||||
* Desktop build overrides this with actual modal implementation
|
||||
*/
|
||||
|
||||
interface InsufficientCreditsModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
toolId?: string;
|
||||
requiredCredits?: number;
|
||||
}
|
||||
|
||||
export function InsufficientCreditsModal(_props: InsufficientCreditsModalProps) {
|
||||
return null;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { useFileSelection } from "@app/contexts/FileContext";
|
||||
import { useFileState } from "@app/contexts/FileContext";
|
||||
import { detectFileExtension } from "@app/utils/fileUtils";
|
||||
import { usePreferences } from "@app/contexts/PreferencesContext";
|
||||
import { useConversionCloudStatus } from "@app/hooks/useConversionCloudStatus";
|
||||
import GroupedFormatDropdown from "@app/components/tools/convert/GroupedFormatDropdown";
|
||||
import ConvertToImageSettings from "@app/components/tools/convert/ConvertToImageSettings";
|
||||
import ConvertFromImageSettings from "@app/components/tools/convert/ConvertFromImageSettings";
|
||||
@@ -63,7 +64,18 @@ const ConvertSettings = ({
|
||||
|
||||
const { endpointStatus } = useMultipleEndpointsEnabled(allEndpoints);
|
||||
|
||||
// Get comprehensive conversion status (availability + cloud routing)
|
||||
const conversionStatus = useConversionCloudStatus();
|
||||
|
||||
const isConversionAvailable = (fromExt: string, toExt: string): boolean => {
|
||||
const conversionKey = `${fromExt}-${toExt}`;
|
||||
|
||||
// In desktop SaaS mode, check combined availability (local OR SaaS)
|
||||
if (conversionStatus.availability[conversionKey] !== undefined) {
|
||||
return conversionStatus.availability[conversionKey];
|
||||
}
|
||||
|
||||
// Fallback to local-only check (web mode or desktop non-SaaS mode)
|
||||
const endpointKey = EXTENSION_TO_ENDPOINT[fromExt]?.[toExt];
|
||||
if (!endpointKey) return false;
|
||||
|
||||
@@ -71,6 +83,10 @@ const ConvertSettings = ({
|
||||
return isAvailable;
|
||||
};
|
||||
|
||||
const doesConversionUseCloud = (fromExt: string, toExt: string): boolean => {
|
||||
return conversionStatus.cloudStatus[`${fromExt}-${toExt}`] || false;
|
||||
};
|
||||
|
||||
// Enhanced FROM options with endpoint availability
|
||||
const enhancedFromOptions = useMemo(() => {
|
||||
const baseOptions = FROM_FORMAT_OPTIONS.map(option => {
|
||||
@@ -107,18 +123,20 @@ const ConvertSettings = ({
|
||||
}
|
||||
|
||||
return filteredOptions;
|
||||
}, [parameters.fromExtension, endpointStatus, preferences.hideUnavailableConversions]);
|
||||
}, [parameters.fromExtension, endpointStatus, preferences.hideUnavailableConversions, conversionStatus]);
|
||||
|
||||
// Enhanced TO options with endpoint availability
|
||||
// Enhanced TO options with endpoint availability and cloud status
|
||||
const enhancedToOptions = useMemo(() => {
|
||||
if (!parameters.fromExtension) return [];
|
||||
|
||||
const availableOptions = getAvailableToExtensions(parameters.fromExtension) || [];
|
||||
const enhanced = availableOptions.map(option => {
|
||||
const enabled = isConversionAvailable(parameters.fromExtension, option.value);
|
||||
const usesCloud = doesConversionUseCloud(parameters.fromExtension, option.value);
|
||||
return {
|
||||
...option,
|
||||
enabled
|
||||
enabled,
|
||||
usesCloud
|
||||
};
|
||||
});
|
||||
|
||||
@@ -128,7 +146,7 @@ const ConvertSettings = ({
|
||||
}
|
||||
|
||||
return enhanced;
|
||||
}, [parameters.fromExtension, endpointStatus, preferences.hideUnavailableConversions]);
|
||||
}, [parameters.fromExtension, endpointStatus, preferences.hideUnavailableConversions, conversionStatus]);
|
||||
|
||||
const resetParametersToDefaults = () => {
|
||||
onParameterChange('imageOptions', {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { Stack, Text, Group, Button, Box, Popover, UnstyledButton, useMantineTheme, useMantineColorScheme } from "@mantine/core";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import CloudOutlinedIcon from "@mui/icons-material/CloudOutlined";
|
||||
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
|
||||
|
||||
interface FormatOption {
|
||||
@@ -8,6 +9,7 @@ interface FormatOption {
|
||||
label: string;
|
||||
group: string;
|
||||
enabled?: boolean;
|
||||
usesCloud?: boolean;
|
||||
}
|
||||
|
||||
interface GroupedFormatDropdownProps {
|
||||
@@ -145,10 +147,20 @@ const GroupedFormatDropdown = ({
|
||||
fontSize: '0.75rem',
|
||||
height: '2rem',
|
||||
padding: '0 0.75rem',
|
||||
flexShrink: 0
|
||||
flexShrink: 0,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
{option.usesCloud && (
|
||||
<CloudOutlinedIcon
|
||||
style={{
|
||||
fontSize: '0.625rem',
|
||||
marginLeft: '0.25rem',
|
||||
opacity: 0.7
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Button } from '@mantine/core';
|
||||
import { Button, Box } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip } from '@app/components/shared/Tooltip';
|
||||
import { useBackendHealth } from '@app/hooks/useBackendHealth';
|
||||
import { CloudBadge } from '@app/components/shared/CloudBadge';
|
||||
|
||||
export interface OperationButtonProps {
|
||||
onClick?: () => void;
|
||||
@@ -14,6 +15,7 @@ export interface OperationButtonProps {
|
||||
fullWidth?: boolean;
|
||||
mt?: string;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
showCloudBadge?: boolean;
|
||||
'data-testid'?: string;
|
||||
'data-tour'?: string;
|
||||
}
|
||||
@@ -29,6 +31,7 @@ const OperationButton = ({
|
||||
fullWidth = false,
|
||||
mt = 'md',
|
||||
type = 'button',
|
||||
showCloudBadge = false,
|
||||
'data-testid': dataTestId,
|
||||
'data-tour': dataTour
|
||||
}: OperationButtonProps) => {
|
||||
@@ -54,12 +57,17 @@ const OperationButton = ({
|
||||
color={color}
|
||||
data-testid={dataTestId}
|
||||
data-tour={dataTour}
|
||||
style={{ minHeight: '2.5rem' }}
|
||||
style={{ minHeight: '2.5rem', position: 'relative' }}
|
||||
>
|
||||
{isLoading
|
||||
? (loadingText || t("loading", "Loading..."))
|
||||
: (submitText || t("submit", "Submit"))
|
||||
}
|
||||
{showCloudBadge && (
|
||||
<Box style={{ position: 'absolute', top: 4, right: 4 }}>
|
||||
<CloudBadge />
|
||||
</Box>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface ExecuteButtonConfig {
|
||||
isVisible?: boolean;
|
||||
disabled?: boolean;
|
||||
testId?: string;
|
||||
showCloudBadge?: boolean;
|
||||
}
|
||||
|
||||
export interface ReviewStepConfig<TParams = unknown> {
|
||||
@@ -105,6 +106,7 @@ export function createToolFlow<TParams = unknown>(config: ToolFlowConfig<TParams
|
||||
disabled={config.executeButton.disabled}
|
||||
loadingText={config.executeButton.loadingText}
|
||||
submitText={config.executeButton.text}
|
||||
showCloudBadge={config.executeButton.showCloudBadge ?? config.review.operation.willUseCloud ?? false}
|
||||
data-testid={config.executeButton.testId}
|
||||
data-tour="run-button"
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,8 @@ import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
|
||||
import { ToolId } from "@app/types/toolId";
|
||||
import { getToolDisabledReason, getDisabledLabel } from "@app/components/tools/fullscreen/shared";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import { CloudBadge } from "@app/components/shared/CloudBadge";
|
||||
import { useToolCloudStatus } from "@app/hooks/useToolCloudStatus";
|
||||
|
||||
interface ToolButtonProps {
|
||||
id: ToolId;
|
||||
@@ -38,6 +40,10 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
const { getToolNavigation } = useToolNavigation();
|
||||
const fav = isFavorite(id as ToolId);
|
||||
|
||||
// Check if this tool will route to SaaS backend (desktop only)
|
||||
const endpointName = tool.endpoints?.[0];
|
||||
const usesCloud = useToolCloudStatus(endpointName);
|
||||
|
||||
const handleClick = (id: ToolId) => {
|
||||
if (isUnavailable) return;
|
||||
if (tool.link) {
|
||||
@@ -98,6 +104,9 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
{t('toolPanel.alpha', 'Alpha')}
|
||||
</Badge>
|
||||
)}
|
||||
{usesCloud && !isUnavailable && (
|
||||
<CloudBadge />
|
||||
)}
|
||||
</div>
|
||||
{matchedSynonym && (
|
||||
<span style={{
|
||||
|
||||
Reference in New Issue
Block a user