Conversation
The review screen now submits explicit wallet/party/category IDs and pre-fills them from the user's existing records by exact name match. Resolution is reactive: when the wallets/parties/categories caches load after the analyzer response (common on a fresh page open), any suggestion that was sitting unresolved gets re-matched automatically. The confirm dialog only surfaces auto-create toggles when the analyzer produced a name that doesn't match an existing record, so a fully resolved batch shows a clean confirmation. Signed-off-by: nfebe <fenn25.fn@gmail.com>
Transaction attachments were being sent as base64 strings inside the JSON payload, so the backend file validator rejected every upload (failing the "must be a file" check and tripping the size rule on the encoded length). Selected files now travel as binary multipart parts to the dedicated upload endpoint, so valid PNG, JPG, and PDF attachments are accepted again.
…clients The transactions list relied on an in-memory cache that was only rebuilt when the user logged out and back in. Records created on the mobile app and synced to the server stayed invisible on the web until that reset, even though they were saved on the backend. The list now refetches when the page is opened and when the tab regains focus, so newly synced transactions show up without forcing a relogin.
Creating a transaction routed picked files through the dedicated multipart upload endpoint. Editing a transaction accepted files in the form but never uploaded them, so attachments added during an edit silently disappeared. The update flow now performs the same upload step, so files added on edit actually land on the transaction.
Attachments only appeared as filename chips: picked images had no thumbnail, saved attachments were invisible during edit, and reopening the picker replaced the previous selection instead of extending it. Picked attachments now render as cards with image thumbnails or a label tile for documents, successive picks append to the selection, and saved attachments load through the auth-gated file endpoint with a per-file remove control that deletes them from the transaction.
The "Max 5 files" hint was advisory only -- the picker happily added more than five attachments, leaving the count to be rejected later by the server. The form now silently drops anything past five total attachments (counting both saved and newly picked files), matching the hint.
Bumps UI to 1.1.1. CHANGELOG covers the patch: transaction attachments finally upload as binary multipart files (and on edit, not just create), the form previews picked images and renders saved attachments as cards with per-file delete, the picker appends instead of replacing and caps at five files, the transactions list refetches on tab focus so records synced from other clients show up without re-login, and the imports confirm step resolves targets by id.
Code Review SummaryThis PR prepares version 1.1.1, introducing significant improvements to attachment handling and the data import workflow. It transitions file uploads to multipart/form-data and adds reactive ID resolution for imported records. 🚀 Key Improvements
💡 Minor Suggestions
|
| reader.readAsDataURL(file); | ||
| }) | ||
| ); | ||
| const remaining = |
There was a problem hiding this comment.
The current implementation silently drops files if the limit is exceeded. It is better to provide feedback to the user when files are ignored to prevent confusion.
| const remaining = | |
| const remaining = MAX_ATTACHMENTS - (newAttachments.value.length + existingAttachments.value.length); | |
| const pickedFiles = Array.from(files); | |
| if (pickedFiles.length > remaining) { | |
| emit('error', t('Only {count} more files can be added', { count: remaining })); | |
| } | |
| for (const file of pickedFiles.slice(0, Math.max(0, remaining))) { |
| if (updated) { | ||
| let updatedOrWithFiles = updated; | ||
| const filesToUpload = Array.isArray(updates.filesToUpload) ? updates.filesToUpload : []; | ||
| if (filesToUpload.length > 0) { |
There was a problem hiding this comment.
Files are uploaded after the transaction update. If the file upload fails, the transaction is already updated but the files are missing. Consider wrapping this in a more robust error handler or alerting the user specifically about the upload failure.
| if (filesToUpload.length > 0) { | |
| if (filesToUpload.length > 0) { | |
| try { | |
| const updatedWithFiles = await api.transactions.addFilesBulk(numericId, filesToUpload); | |
| if (updatedWithFiles) updatedOrWithFiles = updatedWithFiles; | |
| } catch (e) { | |
| console.error('[updateTransaction] Error uploading files:', e); | |
| emit('error', 'Transaction updated, but some files failed to upload.'); | |
| } | |
| } |
Deploying trakli-dev with
|
| Latest commit: |
7f5c0ea
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://85484ddf.trakli-dev.pages.dev |
Commit message validation only ran on pull requests targeting main, so non-conforming subjects could land on dev unnoticed and only surface when the dev to main release PR was opened. The check now also runs on pushes and pull requests against dev so violations are caught at the original review.
| } | ||
| }; | ||
|
|
||
| onMounted(() => { |
There was a problem hiding this comment.
The check for typeof document !== 'undefined' is unnecessary inside an onMounted hook in Nuxt/Vue 3, as onMounted only executes on the client-side where document is guaranteed to exist.
| onMounted(() => { | |
| onMounted(() => { | |
| document.addEventListener('visibilitychange', handleVisibilityChange); | |
| }); |
| if (loadingPreviewIds.value.has(file.id)) return; | ||
| loadingPreviewIds.value.add(file.id); | ||
| try { | ||
| const blob = await api.transactions.fetchFileBlob(file.id); |
There was a problem hiding this comment.
Using URL.createObjectURL is efficient but can lead to memory leaks if not properly managed. While you handle it in onUnmounted, long-lived forms that allow repeatedly adding/removing many files will accumulate revoked or orphan blobs. Consider revoking the old URL immediately when a user replaces a file or if the component logic allows.
| const blob = await api.transactions.fetchFileBlob(file.id); | |
| const blob = await api.transactions.fetchFileBlob(file.id); | |
| const url = URL.createObjectURL(blob); | |
| existingPreviews.value[file.id] = url; |
| field: 'wallet_id' | 'party_id' | 'category_id', | ||
| value: string | ||
| ) => { | ||
| const id = value === '' ? null : Number(value); |
There was a problem hiding this comment.
When coercing the select value to a number, Number(value) will return 0 if the value is an empty string (though you handled empty string separately). It is safer to use parseInt(value, 10) to ensure consistent base-10 parsing of IDs.
| const id = value === '' ? null : Number(value); | |
| const id = value === '' ? null : parseInt(value, 10); |
Deploying webui with
|
| Latest commit: |
7f5c0ea
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://7380732b.webui-9fh.pages.dev |
| Branch Preview URL: | https://dev.webui-9fh.pages.dev |
[1.1.1] - 2026-05-03
Added
Fixed