mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
Desktop/remove hard requirement auth wall on desktop (#5956)
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
081b1ec49e
commit
2e2b55e87d
@@ -78,7 +78,8 @@ export const ONBOARDING_STEPS: OnboardingStep[] = [
|
||||
id: 'welcome',
|
||||
type: 'modal-slide',
|
||||
slideId: 'welcome',
|
||||
condition: () => true,
|
||||
// Desktop has its own onboarding modal (DesktopOnboardingModal)
|
||||
condition: (ctx) => !ctx.isDesktopApp,
|
||||
},
|
||||
{
|
||||
id: 'admin-overview',
|
||||
@@ -107,7 +108,7 @@ export const ONBOARDING_STEPS: OnboardingStep[] = [
|
||||
id: 'tour-overview',
|
||||
type: 'modal-slide',
|
||||
slideId: 'tour-overview',
|
||||
condition: (ctx) => !ctx.effectiveIsAdmin && ctx.tourType !== 'admin',
|
||||
condition: (ctx) => !ctx.effectiveIsAdmin && ctx.tourType !== 'admin' && !ctx.isDesktopApp,
|
||||
},
|
||||
{
|
||||
id: 'server-license',
|
||||
|
||||
@@ -12,6 +12,7 @@ import ToolButton from "@app/components/tools/toolPicker/ToolButton";
|
||||
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
|
||||
import { ToolId } from "@app/types/toolId";
|
||||
import { getSubcategoryLabel } from "@app/data/toolsTaxonomy";
|
||||
import { ToolPickerFooterExtensions } from "@app/components/tools/toolPicker/ToolPickerFooterExtensions";
|
||||
|
||||
interface ToolPickerProps {
|
||||
selectedToolKey: string | null;
|
||||
@@ -150,6 +151,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<ToolPickerFooterExtensions />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,11 +3,14 @@ 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';
|
||||
import type { ExecuteDisabledReason } from '@app/hooks/tools/shared/toolOperationTypes';
|
||||
import { useToolActions } from '@app/contexts/ToolActionsContext';
|
||||
|
||||
export interface OperationButtonProps {
|
||||
onClick?: () => void;
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
disabledReason?: ExecuteDisabledReason;
|
||||
loadingText?: string;
|
||||
submitText?: string;
|
||||
variant?: 'filled' | 'outline' | 'subtle';
|
||||
@@ -24,6 +27,7 @@ const OperationButton = ({
|
||||
onClick,
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
disabledReason,
|
||||
loadingText,
|
||||
submitText,
|
||||
variant = 'filled',
|
||||
@@ -37,20 +41,32 @@ const OperationButton = ({
|
||||
}: OperationButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOnline, message: backendMessage } = useBackendHealth();
|
||||
const { onEndpointUnavailableClick } = useToolActions();
|
||||
const blockedByBackend = !isOnline;
|
||||
const combinedDisabled = disabled || blockedByBackend;
|
||||
|
||||
const effectiveDisabled = disabled || disabledReason !== null && disabledReason !== undefined;
|
||||
const combinedDisabled = effectiveDisabled || blockedByBackend;
|
||||
|
||||
const reasonTooltip: Record<NonNullable<ExecuteDisabledReason>, string> = {
|
||||
endpointUnavailable: onEndpointUnavailableClick
|
||||
? t('tool.endpointUnavailableClickable', "Not available in this mode. Click to sign in.")
|
||||
: t('tool.endpointUnavailable', 'This tool is unavailable on your server.'),
|
||||
noFiles: t('tool.noFiles', 'Add a file to get started.'),
|
||||
invalidParams: t('tool.invalidParams', 'Fill in the required settings.'),
|
||||
};
|
||||
|
||||
const tooltipLabel = blockedByBackend
|
||||
? (backendMessage ?? t('backendHealth.checking', 'Checking backend status...'))
|
||||
: null;
|
||||
: (disabledReason ? (reasonTooltip[disabledReason] ?? null) : null);
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
fullWidth={fullWidth}
|
||||
mr='md'
|
||||
ml='md'
|
||||
mt={mt}
|
||||
fullWidth={fullWidth || !!tooltipLabel}
|
||||
mr={tooltipLabel ? 0 : 'md'}
|
||||
ml={tooltipLabel ? 0 : 'md'}
|
||||
mt={tooltipLabel ? 0 : mt}
|
||||
loading={isLoading}
|
||||
disabled={combinedDisabled}
|
||||
variant={variant}
|
||||
@@ -72,9 +88,22 @@ const OperationButton = ({
|
||||
);
|
||||
|
||||
if (tooltipLabel) {
|
||||
// Disabled buttons suppress pointer events at the browser level, so the Tooltip's
|
||||
// cloneElement handlers would never fire. Wrap in a Box to capture them instead.
|
||||
// When endpointUnavailable and a click handler is provided (desktop), the Box
|
||||
// also acts as the click target to open the sign-in / connect modal.
|
||||
const boxClickHandler = disabledReason === 'endpointUnavailable' ? onEndpointUnavailableClick : undefined;
|
||||
return (
|
||||
<Tooltip content={tooltipLabel} position="top" arrow>
|
||||
{button}
|
||||
<Box
|
||||
mr="md"
|
||||
ml="md"
|
||||
mt={mt}
|
||||
style={{ display: 'block', cursor: boxClickHandler ? 'pointer' : undefined }}
|
||||
onClick={boxClickHandler}
|
||||
>
|
||||
{button}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ToolOperationHook } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from '@app/components/tools/shared/ToolWorkflowTitle';
|
||||
import { StirlingFile } from '@app/types/fileContext';
|
||||
import type { TooltipTip } from '@app/types/tips';
|
||||
import type { ExecuteDisabledReason } from '@app/hooks/tools/shared/toolOperationTypes';
|
||||
|
||||
export interface FilesStepConfig {
|
||||
selectedFiles: StirlingFile[];
|
||||
@@ -36,6 +37,23 @@ export interface ExecuteButtonConfig {
|
||||
loadingText: string;
|
||||
onClick: () => Promise<void>;
|
||||
isVisible?: boolean;
|
||||
/**
|
||||
* Pass the raw endpoint-enabled flag from useEndpointEnabled / useBaseTool.
|
||||
* createToolFlow derives the correct disabled reason automatically.
|
||||
* Priority: endpointUnavailable > noFiles > invalidParams
|
||||
*/
|
||||
endpointEnabled?: boolean | null;
|
||||
/**
|
||||
* Pass the result of params.validateParameters().
|
||||
* createToolFlow uses this to show the 'invalidParams' disabled reason.
|
||||
*/
|
||||
paramsValid?: boolean;
|
||||
/**
|
||||
* Explicit disabled reason override — use when the automatic computation
|
||||
* from endpointEnabled + paramsValid is insufficient.
|
||||
*/
|
||||
disabledReason?: ExecuteDisabledReason;
|
||||
/** Raw override for tools with fully custom disable logic (e.g. Compare, ShowJS). */
|
||||
disabled?: boolean;
|
||||
testId?: string;
|
||||
showCloudBadge?: boolean;
|
||||
@@ -99,18 +117,30 @@ export function createToolFlow<TParams = unknown>(config: ToolFlowConfig<TParams
|
||||
{!config.review.isVisible && (config.files.selectedFiles?.length ?? 0) > 0 && config.preview}
|
||||
|
||||
{/* Execute Button */}
|
||||
{config.executeButton && config.executeButton.isVisible !== false && (
|
||||
<OperationButton
|
||||
onClick={config.executeButton.onClick}
|
||||
isLoading={config.review.operation.isLoading}
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
{config.executeButton && config.executeButton.isVisible !== false && (() => {
|
||||
const eb = config.executeButton;
|
||||
const hasFiles = (config.files.selectedFiles?.length ?? 0) > 0;
|
||||
// Compute the disabled reason from structured fields; explicit disabledReason wins if set.
|
||||
const effectiveDisabledReason: ExecuteDisabledReason =
|
||||
eb.disabledReason !== undefined ? eb.disabledReason
|
||||
: eb.endpointEnabled === false ? 'endpointUnavailable'
|
||||
: !hasFiles ? 'noFiles'
|
||||
: eb.paramsValid === false ? 'invalidParams'
|
||||
: null;
|
||||
return (
|
||||
<OperationButton
|
||||
onClick={eb.onClick}
|
||||
isLoading={config.review.operation.isLoading}
|
||||
disabled={eb.disabled}
|
||||
disabledReason={effectiveDisabledReason}
|
||||
loadingText={eb.loadingText}
|
||||
submitText={eb.text}
|
||||
showCloudBadge={eb.showCloudBadge ?? config.review.operation.willUseCloud ?? false}
|
||||
data-testid={eb.testId}
|
||||
data-tour="run-button"
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Review Step */}
|
||||
{steps.createReviewStep({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useHotkeys } from "@app/contexts/HotkeyContext";
|
||||
import HotkeyDisplay from "@app/components/hotkeys/HotkeyDisplay";
|
||||
import FavoriteStar from "@app/components/tools/toolPicker/FavoriteStar";
|
||||
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
|
||||
import { ToolId } from "@app/types/toolId";
|
||||
import type { 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";
|
||||
@@ -26,15 +26,21 @@ interface ToolButtonProps {
|
||||
disableNavigation?: boolean;
|
||||
matchedSynonym?: string;
|
||||
hasStars?: boolean;
|
||||
/** Called when an unavailable tool is clicked; if provided, overrides the default no-op */
|
||||
onUnavailableClick?: () => void;
|
||||
}
|
||||
|
||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym, hasStars = false }) => {
|
||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym, hasStars = false, onUnavailableClick }) => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useAppConfig();
|
||||
const premiumEnabled = config?.premiumEnabled;
|
||||
const { isFavorite, toggleFavorite, toolAvailability } = useToolWorkflow();
|
||||
const disabledReason = getToolDisabledReason(id, tool, toolAvailability, premiumEnabled);
|
||||
const isUnavailable = disabledReason !== null;
|
||||
// If onUnavailableClick is provided for a non-comingSoon tool, render as "cloud-available":
|
||||
// full opacity, cloud badge, normal tooltip — clicking still fires onUnavailableClick (e.g. sign-in).
|
||||
const showAsCloudAvailable = isUnavailable && !!onUnavailableClick && disabledReason !== 'comingSoon' && disabledReason !== 'selfHostedOffline';
|
||||
const visuallyUnavailable = isUnavailable && !showAsCloudAvailable;
|
||||
const { hotkeys } = useHotkeys();
|
||||
const binding = hotkeys[id];
|
||||
const { getToolNavigation } = useToolNavigation();
|
||||
@@ -46,7 +52,10 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
const usesCloud = useWillUseCloud(endpointString);
|
||||
|
||||
const handleClick = (id: ToolId) => {
|
||||
if (isUnavailable) return;
|
||||
if (isUnavailable) {
|
||||
onUnavailableClick?.();
|
||||
return;
|
||||
}
|
||||
if (tool.link) {
|
||||
// Open external link in new tab
|
||||
window.open(tool.link, '_blank', 'noopener,noreferrer');
|
||||
@@ -62,7 +71,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
const { key: disabledKey, fallback: disabledFallback } = getDisabledLabel(disabledReason);
|
||||
const disabledMessage = t(disabledKey, disabledFallback);
|
||||
|
||||
const tooltipContent = isUnavailable
|
||||
const tooltipContent = visuallyUnavailable
|
||||
? (<span><strong>{disabledMessage}</strong> {tool.description}</span>)
|
||||
: (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
@@ -84,7 +93,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
<>
|
||||
<ToolIcon
|
||||
icon={tool.icon}
|
||||
opacity={isUnavailable ? 0.25 : 1}
|
||||
opacity={visuallyUnavailable ? 0.25 : 1}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', flex: 1, overflow: 'visible' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', width: '100%' }}>
|
||||
@@ -93,19 +102,19 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
lines={1}
|
||||
minimumFontScale={0.8}
|
||||
as="span"
|
||||
style={{ display: 'inline-block', maxWidth: '100%', opacity: isUnavailable ? 0.25 : 1 }}
|
||||
style={{ display: 'inline-block', maxWidth: '100%', opacity: visuallyUnavailable ? 0.25 : 1 }}
|
||||
/>
|
||||
{tool.versionStatus === 'alpha' && (
|
||||
<Badge
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
style={{ flexShrink: 0, opacity: isUnavailable ? 0.25 : 1 }}
|
||||
style={{ flexShrink: 0, opacity: visuallyUnavailable ? 0.25 : 1 }}
|
||||
>
|
||||
{t('toolPanel.alpha', 'Alpha')}
|
||||
</Badge>
|
||||
)}
|
||||
{usesCloud && !isUnavailable && (
|
||||
{(usesCloud && !visuallyUnavailable) && (
|
||||
<CloudBadge />
|
||||
)}
|
||||
</div>
|
||||
@@ -113,7 +122,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
<span style={{
|
||||
fontSize: '0.75rem',
|
||||
color: 'var(--mantine-color-dimmed)',
|
||||
opacity: isUnavailable ? 0.25 : 1,
|
||||
opacity: visuallyUnavailable ? 0.25 : 1,
|
||||
marginTop: '1px',
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'nowrap'
|
||||
@@ -195,7 +204,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
color: "var(--tools-text-and-icon-color)",
|
||||
cursor: isUnavailable ? 'not-allowed' : undefined,
|
||||
cursor: visuallyUnavailable ? 'not-allowed' : undefined,
|
||||
overflow: 'visible'
|
||||
},
|
||||
label: { overflow: 'visible' }
|
||||
@@ -205,7 +214,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
</Button>
|
||||
);
|
||||
|
||||
const star = hasStars && !isUnavailable ? (
|
||||
const star = hasStars && !visuallyUnavailable ? (
|
||||
<FavoriteStar
|
||||
isFavorite={fav}
|
||||
onToggle={() => toggleFavorite(id as ToolId)}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Stub — returns null in core/web builds.
|
||||
* Desktop build shadows this with a sign-in prompt for local mode.
|
||||
*/
|
||||
export function ToolPickerFooterExtensions() {
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user