Fix/usr fdbck ch (#1105)
* fix(privacy): mask voucher amounts in session replays * fix: persist transaction source filter * fix: clarify invoice filenames and booking previews * fix: truncate long uploaded filenames * feat: add invoice delivery history * fix: harden invoice delivery history * fix: include invoice deliveries in full archive
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
||||
import { useState, useRef, useEffect, useMemo, useCallback, useId } from 'react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { getAccountClassName } from '@/lib/bookkeeping/account-descriptions'
|
||||
@@ -41,15 +41,19 @@ interface AccountComboboxProps {
|
||||
// to the next konteringsrad's account on Enter: see JournalEntryForm.focusAccount).
|
||||
inputRef?: React.RefCallback<HTMLInputElement>
|
||||
disabled?: boolean
|
||||
// Optional always-visible label for compact editors where the selected
|
||||
// account name must remain readable after the dropdown closes.
|
||||
selectedName?: string
|
||||
}
|
||||
|
||||
export default function AccountCombobox({ value, accounts, onChange, onCommit, onCreateAccount, catalog, notActivatedLabel = 'Aktiveras vid bokföring', className, inputRef, disabled = false }: AccountComboboxProps) {
|
||||
export default function AccountCombobox({ value, accounts, onChange, onCommit, onCreateAccount, catalog, notActivatedLabel = 'Aktiveras vid bokföring', className, inputRef, disabled = false, selectedName }: AccountComboboxProps) {
|
||||
const [search, setSearch] = useState(value)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const internalInputRef = useRef<HTMLInputElement>(null)
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
const selectedNameId = useId()
|
||||
// Whether the user has typed or arrow-navigated since the field was focused.
|
||||
// Enter only selects the highlighted item after an actual interaction: a
|
||||
// bare Enter on a freshly-focused field must not grab the first account in
|
||||
@@ -236,6 +240,8 @@ export default function AccountCombobox({ value, accounts, onChange, onCommit, o
|
||||
}, 150)
|
||||
}
|
||||
|
||||
const showSelectedName = Boolean(selectedName && value && search === value)
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<Input
|
||||
@@ -249,8 +255,14 @@ export default function AccountCombobox({ value, accounts, onChange, onCommit, o
|
||||
className={`font-mono ${className ?? ''}`.trim()}
|
||||
autoComplete="off"
|
||||
disabled={disabled}
|
||||
aria-describedby={showSelectedName ? selectedNameId : undefined}
|
||||
/>
|
||||
|
||||
{showSelectedName ? (
|
||||
<p id={selectedNameId} className="mt-1 break-words px-1 text-sm leading-snug text-foreground">
|
||||
{selectedName}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{/* Dropdown */}
|
||||
{isOpen && !disabled && flatList.length > 0 && (
|
||||
|
||||
@@ -30,6 +30,41 @@ let uploadCounter = 0
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
|
||||
const ACCEPTED_TYPES = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp']
|
||||
const ACCEPTED_EXTENSIONS = '.pdf,.jpg,.jpeg,.png,.webp'
|
||||
const FILE_NAME_TAIL_LENGTH = 16
|
||||
|
||||
function TruncatedFileName({ fileName }: { fileName: string }) {
|
||||
const characters = Array.from(fileName)
|
||||
|
||||
if (characters.length <= FILE_NAME_TAIL_LENGTH * 2) {
|
||||
return (
|
||||
<span className="min-w-0 flex-1 truncate" title={fileName}>
|
||||
{fileName}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const splitAt = characters.length - FILE_NAME_TAIL_LENGTH
|
||||
const start = characters.slice(0, splitAt).join('')
|
||||
const end = characters.slice(splitAt).join('')
|
||||
|
||||
return (
|
||||
<span
|
||||
className="flex min-w-0 flex-1"
|
||||
title={fileName}
|
||||
>
|
||||
<span className="sr-only">{fileName}</span>
|
||||
<span aria-hidden="true" className="min-w-0 flex-1 truncate">
|
||||
{start}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="max-w-1/2 shrink overflow-hidden whitespace-nowrap text-right"
|
||||
>
|
||||
{end}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
@@ -208,7 +243,7 @@ export default function DocumentUploadZone({
|
||||
const isUploading = files.some((f) => f.status === 'uploading')
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="min-w-0 space-y-2">
|
||||
{/* Drop zone */}
|
||||
<div
|
||||
className={`
|
||||
@@ -253,14 +288,14 @@ export default function DocumentUploadZone({
|
||||
{files.map((file, index) => (
|
||||
<div
|
||||
key={file.uploadKey}
|
||||
className="flex items-center gap-2 text-sm py-1.5 px-2 rounded bg-muted/50"
|
||||
className="flex min-w-0 items-center gap-2 text-sm py-1.5 px-2 rounded bg-muted/50"
|
||||
>
|
||||
{isImageType(file.file.type) ? (
|
||||
<ImageIcon className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
) : (
|
||||
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
<span className="truncate flex-1">{file.fileName}</span>
|
||||
<TruncatedFileName fileName={file.fileName} />
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{formatFileSize(file.fileSize)}
|
||||
</span>
|
||||
|
||||
@@ -1233,7 +1233,7 @@ export default function JournalEntryList() {
|
||||
<JournalEntryStatusBadge entry={entry} showStatus={entry.status === 'reversed' || entry.status === 'draft'} />
|
||||
)}
|
||||
<span className="flex-1 truncate">{entry.description}</span>
|
||||
<span className="shrink-0 w-28 text-right tabular-nums text-sm font-medium sensitive-field">
|
||||
<span className="shrink-0 w-28 text-right tabular-nums text-sm font-medium rr-mask">
|
||||
{formatCurrency(voucherTotal, 'SEK', { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
|
||||
Reference in New Issue
Block a user