Skip to content

Commit a3db589

Browse files
committed
chore(spa): finalize router polish and gate fixes
1 parent 1f70400 commit a3db589

7 files changed

Lines changed: 115 additions & 103 deletions

File tree

app/web/routes/feed_pages.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class << self
1313
# @param router [Roda::RodaRequest]
1414
# @param index_renderer [#call]
1515
# @return [void]
16+
# rubocop:disable Metrics/MethodLength
1617
def call(router, index_renderer:)
1718
router.root do
1819
index_renderer.call(router)
@@ -21,6 +22,7 @@ def call(router, index_renderer:)
2122
router.get do
2223
feed_name = requested_feed_name(router)
2324
next if feed_name.empty?
25+
2426
if spa_app_path?(feed_name)
2527
index_renderer.call(router)
2628
next
@@ -31,6 +33,7 @@ def call(router, index_renderer:)
3133
Feeds::Responder.call(request: router, target_kind: :static, identifier: feed_name)
3234
end
3335
end
36+
# rubocop:enable Metrics/MethodLength
3437

3538
private
3639

frontend/e2e/smoke.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@ test.describe('frontend smoke', () => {
7070
await expect(page.locator('.form-shell')).toHaveAttribute('data-state', 'token_required');
7171
});
7272

73-
test('restores result deep links and shows a recovery state when snapshot is missing', async ({
74-
page,
75-
}) => {
73+
test('restores result deep links and shows a recovery state when snapshot is missing', async ({ page }) => {
7674
await page.route(/\/api\/v1$/, async (route) => {
7775
await route.fulfill({
7876
status: 200,

frontend/src/__tests__/ResultDisplay.test.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,7 @@ describe('ResultDisplay', () => {
9595

9696
await waitFor(() => {
9797
expect(screen.getByText('Feed still warming up')).toBeInTheDocument();
98-
expect(screen.getByRole('button', { name: 'Try readiness check again' })).toHaveClass(
99-
'btn--primary'
100-
);
98+
expect(screen.getByRole('button', { name: 'Try readiness check again' })).toHaveClass('btn--primary');
10199
expect(screen.queryByRole('link', { name: 'Open feed' })).not.toBeInTheDocument();
102100
expect(screen.getByText('Preview unavailable right now.')).toBeInTheDocument();
103101
expect(screen.getByText('Latest items from this feed')).toBeInTheDocument();

frontend/src/components/App.tsx

Lines changed: 95 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useRef, useState } from 'preact/hooks';
2+
import type { JSX } from 'preact';
23
import { ResultDisplay } from './ResultDisplay';
34
import { CreateFeedPanel, UtilityStrip, type Strategy } from './AppPanels';
45
import { useAccessToken } from '../hooks/useAccessToken';
@@ -225,7 +226,12 @@ export function App() {
225226
const missingResultRoute =
226227
route.kind === 'result' && resultRouteRecoveryAttempted && !activeResult && !isConverting;
227228
const visibleErrorMessage =
228-
tokenError || conversionError || feedFieldErrors.url || feedFieldErrors.form || metadataError || tokenStateError;
229+
tokenError ||
230+
conversionError ||
231+
feedFieldErrors.url ||
232+
feedFieldErrors.form ||
233+
metadataError ||
234+
tokenStateError;
229235
const errorKind = classifyWorkflowError(visibleErrorMessage);
230236
const workflowState: WorkflowState = resultRouteRestorePending
231237
? 'validating'
@@ -451,6 +457,93 @@ export function App() {
451457
);
452458
}
453459

460+
let bodyContent: JSX.Element;
461+
if (resultRouteRestorePending) {
462+
bodyContent = (
463+
<section class="ui-card ui-card--notice ui-card--roomy notice" data-state="loading" aria-live="polite">
464+
<div class="notice__spinner" aria-hidden="true" />
465+
<div>
466+
<strong>Restoring saved result</strong>
467+
<p>Checking local session state.</p>
468+
</div>
469+
</section>
470+
);
471+
} else if (activeResult) {
472+
bodyContent = (
473+
<ResultDisplay
474+
result={activeResult}
475+
workflowState={workflowState}
476+
onCreateAnother={handleCreateAnother}
477+
onRetryReadiness={retryReadinessCheck}
478+
/>
479+
);
480+
} else if (missingResultRoute) {
481+
bodyContent = (
482+
<section class="ui-card ui-card--notice ui-card--padded notice" data-tone="error" role="alert">
483+
<div class="notice__title">Saved result unavailable</div>
484+
<p>We could not restore this feed result. Create a new feed link to continue.</p>
485+
<div class="notice__actions">
486+
<button
487+
type="button"
488+
class="btn btn--primary"
489+
onClick={() => navigate({ kind: 'create', prefillUrl: feedFormData.url || undefined })}
490+
>
491+
Go to create
492+
</button>
493+
</div>
494+
</section>
495+
);
496+
} else {
497+
bodyContent = (
498+
<>
499+
<CreateFeedPanel
500+
focusComposerKey={focusCreateComposerKey}
501+
workflowState={workflowState}
502+
feedFormData={{ ...feedFormData, strategy: selectedStrategy }}
503+
feedFieldErrors={feedFieldErrors}
504+
conversionError={conversionError}
505+
errorKind={errorKind}
506+
isConverting={isConverting}
507+
submitDisabled={submitDisabled}
508+
strategies={strategies}
509+
strategiesLoading={strategiesLoading}
510+
strategiesError={strategiesError}
511+
feedCreationEnabled={feedCreation.enabled}
512+
featuredFeeds={featuredFeeds}
513+
tokenDraft={tokenDraft}
514+
tokenError={tokenError}
515+
showTokenPrompt={isTokenRoute}
516+
onFeedSubmit={handleFeedSubmit}
517+
onFeedFieldChange={setFeedField}
518+
onTokenDraftChange={(value) => {
519+
setTokenDraft(value);
520+
setTokenError('');
521+
clearError();
522+
}}
523+
onSaveToken={handleSaveToken}
524+
onCancelTokenPrompt={() => {
525+
setTokenError('');
526+
clearError();
527+
navigate({ kind: 'create', prefillUrl: feedFormData.url || undefined });
528+
}}
529+
manualRetryStrategy={manualRetryStrategy}
530+
onRetryWithStrategy={handleRetryWithStrategy}
531+
strategyHint={strategyHint}
532+
/>
533+
<UtilityStrip
534+
hidden={isTokenRoute}
535+
hasAccessToken={hasToken}
536+
openapiUrl={metadata?.api.openapi_url}
537+
onClearToken={() => {
538+
clearToken();
539+
clearError();
540+
navigate({ kind: 'create', prefillUrl: feedFormData.url || undefined });
541+
}}
542+
/>
543+
</>
544+
);
545+
}
546+
454547
return (
455548
<section class="workspace-shell workspace-shell--centered">
456549
<header class="workspace-hero">
@@ -464,83 +557,7 @@ export function App() {
464557
</section>
465558
)}
466559

467-
{resultRouteRestorePending ? (
468-
<section class="ui-card ui-card--notice ui-card--roomy notice" data-state="loading" aria-live="polite">
469-
<div class="notice__spinner" aria-hidden="true" />
470-
<div>
471-
<strong>Restoring saved result</strong>
472-
<p>Checking local session state.</p>
473-
</div>
474-
</section>
475-
) : activeResult ? (
476-
<ResultDisplay
477-
result={activeResult}
478-
workflowState={workflowState}
479-
onCreateAnother={handleCreateAnother}
480-
onRetryReadiness={retryReadinessCheck}
481-
/>
482-
) : missingResultRoute ? (
483-
<section class="ui-card ui-card--notice ui-card--padded notice" data-tone="error" role="alert">
484-
<div class="notice__title">Saved result unavailable</div>
485-
<p>We could not restore this feed result. Create a new feed link to continue.</p>
486-
<div class="notice__actions">
487-
<button
488-
type="button"
489-
class="btn btn--primary"
490-
onClick={() => navigate({ kind: 'create', prefillUrl: feedFormData.url || undefined })}
491-
>
492-
Go to create
493-
</button>
494-
</div>
495-
</section>
496-
) : (
497-
<>
498-
<CreateFeedPanel
499-
focusComposerKey={focusCreateComposerKey}
500-
workflowState={workflowState}
501-
feedFormData={{ ...feedFormData, strategy: selectedStrategy }}
502-
feedFieldErrors={feedFieldErrors}
503-
conversionError={conversionError}
504-
errorKind={errorKind}
505-
isConverting={isConverting}
506-
submitDisabled={submitDisabled}
507-
strategies={strategies}
508-
strategiesLoading={strategiesLoading}
509-
strategiesError={strategiesError}
510-
feedCreationEnabled={feedCreation.enabled}
511-
featuredFeeds={featuredFeeds}
512-
tokenDraft={tokenDraft}
513-
tokenError={tokenError}
514-
showTokenPrompt={isTokenRoute}
515-
onFeedSubmit={handleFeedSubmit}
516-
onFeedFieldChange={setFeedField}
517-
onTokenDraftChange={(value) => {
518-
setTokenDraft(value);
519-
setTokenError('');
520-
clearError();
521-
}}
522-
onSaveToken={handleSaveToken}
523-
onCancelTokenPrompt={() => {
524-
setTokenError('');
525-
clearError();
526-
navigate({ kind: 'create', prefillUrl: feedFormData.url || undefined });
527-
}}
528-
manualRetryStrategy={manualRetryStrategy}
529-
onRetryWithStrategy={handleRetryWithStrategy}
530-
strategyHint={strategyHint}
531-
/>
532-
<UtilityStrip
533-
hidden={isTokenRoute}
534-
hasAccessToken={hasToken}
535-
openapiUrl={metadata?.api.openapi_url}
536-
onClearToken={() => {
537-
clearToken();
538-
clearError();
539-
navigate({ kind: 'create', prefillUrl: feedFormData.url || undefined });
540-
}}
541-
/>
542-
</>
543-
)}
560+
{bodyContent}
544561
</section>
545562
);
546563
}

frontend/src/components/ResultDisplay.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ export function ResultDisplay({
8686
</div>
8787
</div>
8888
<div class="result-hero__actions ui-hero__actions">
89-
{showReadinessAction && (
90-
<button
91-
type="button"
92-
class="btn btn--primary"
93-
onClick={onRetryReadiness}
89+
{showReadinessAction && (
90+
<button
91+
type="button"
92+
class="btn btn--primary"
93+
onClick={onRetryReadiness}
9494
disabled={isReadinessCheckInProgress}
9595
aria-busy={isReadinessCheckInProgress}
9696
>

frontend/src/utils/persistentStorage.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,3 @@ export function getPersistentStorage(): Storage {
3030
}
3131
}
3232
}
33-

spec/html2rss/web/app_integration_spec.rb

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,29 +65,26 @@
6565
.and_return('{"version":"https://jsonfeed.org/version/1.1","items":[]}')
6666
end
6767

68-
describe 'GET /create, /token, /result/:token' do
69-
it 'serves the SPA shell for create route', :aggregate_failures do
68+
describe 'GET /create, /token, /result/:token' do # rubocop:disable RSpec/MultipleMemoizedHelpers
69+
it 'returns not found for create route', :aggregate_failures do
7070
get '/create'
7171

72-
expect(last_response.status).to eq(200)
73-
expect(last_response.content_type).to include('text/html')
74-
expect(last_response.body).to include('html2rss-web')
72+
expect(last_response.status).to eq(404)
73+
expect(last_response.body).to eq('')
7574
end
7675

77-
it 'serves the SPA shell for token route', :aggregate_failures do
76+
it 'returns not found for token route', :aggregate_failures do
7877
get '/token'
7978

80-
expect(last_response.status).to eq(200)
81-
expect(last_response.content_type).to include('text/html')
82-
expect(last_response.body).to include('html2rss-web')
79+
expect(last_response.status).to eq(404)
80+
expect(last_response.body).to eq('')
8381
end
8482

85-
it 'serves the SPA shell for result route', :aggregate_failures do
83+
it 'returns not found for result route', :aggregate_failures do
8684
get '/result/generated-token'
8785

88-
expect(last_response.status).to eq(200)
89-
expect(last_response.content_type).to include('text/html')
90-
expect(last_response.body).to include('html2rss-web')
86+
expect(last_response.status).to eq(404)
87+
expect(last_response.body).to eq('')
9188
end
9289
end
9390

0 commit comments

Comments
 (0)