Files
accounted/components/bookkeeping/DocumentViewButton.tsx
T
Jakob Wennberg 2d97fbf1bc fix: false popup-blocked toast on Visa dokument + scope SKV reconnect line to skattekonto source (#1613)
* fix(documents): stop false popup-blocked toast on Visa dokument

window.open() returns null BY SPEC when 'noopener' is in the features
string, even when the tab opens, so the destructive 'Tillåt popupfönster'
toast fired on every successful open. Open without the features string and
sever the reverse channel manually (tab.opener = null), the same pattern
lib/browser/deferred-tab.ts already uses; the toast now fires only on a
genuine popup block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(transactions): scope the SKV reconnect line to the skattekonto source

The reconnect attn line rendered on /transactions whenever the SKV
connection needed renewal, regardless of what the user was looking at, so
it read as permanent noise. It now shows only when the source picker is on
Skatteverket (the rows it actually explains); the skattekonto page keeps
its own reconnect line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(transactions): keep Skatteverket source pickable while reconnect is needed

In the reconnect-needed state the transaktioner fetch 401s, skvRows goes
empty, the Skatteverket option left the source picker, and the stale-filter
effect reset the filter to 'all': the source-gated reconnect line became
unreachable exactly when it applied. Show the source whenever rows exist OR
reconnect is needed (skvNeedsReconnect already requires connected=true, so
never-connected companies get no phantom source).

No component test: repo test scope is lib/ + app/api/ (no component tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 10:04:36 +02:00

72 lines
2.4 KiB
TypeScript

'use client'
import { Eye } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/use-toast'
interface DocumentViewButtonProps {
documentId: string
label?: string
className?: string
}
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
/**
* Opens a document in the browser through the same-origin inline proxy
* (/api/documents/:id/inline), which serves it with
* `Content-Disposition: inline`: PDFs land in the browser's viewer and images
* render, instead of the file dropping into the Downloads folder (#1190).
*
* The proxy authorizes with the caller's own client before the service-role
* client reads the bucket, and no URL has to be minted first: navigation
* happens straight from the click, so there is nothing for a popup blocker to
* catch (the previous signed-URL fetch needed openDeferredTab for exactly that
* reason). The browser's own viewer still offers saving the file.
*/
export function DocumentViewButton({ documentId, label = 'Visa dokument', className }: DocumentViewButtonProps) {
const { toast } = useToast()
const handleClick = () => {
// documentId originates from staged preview_data (Record<string, unknown>);
// validate the shape before interpolating into the URL so a malformed
// payload can't point the tab at another internal endpoint.
if (!UUID_RE.test(documentId)) {
toast({
title: 'Ogiltigt dokument-ID',
description: 'Försök ladda om sidan eller kontakta support.',
variant: 'destructive',
})
return
}
// window.open() returns null BY SPEC when 'noopener' is in the features
// string, even on success, so passing it here made this toast fire on
// every successful open. Open with a real return value and sever the
// reverse channel manually (same pattern as lib/browser/deferred-tab.ts).
const tab = window.open(`/api/documents/${documentId}/inline`, '_blank')
if (tab) {
tab.opener = null
} else {
toast({
title: 'Kunde inte öppna dokumentet',
description: 'Tillåt popupfönster för Accounted i webbläsaren och försök igen.',
variant: 'destructive',
})
}
}
return (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleClick}
className={className}
>
<Eye className="mr-1.5 h-3.5 w-3.5" />
{label}
</Button>
)
}