chore: improved upload logo e2e test, minor label fix, bump version to 1.0.3, added changelog.md and blog post with 1.0.3 announcement (#202)
41
.github/screenshots/easy-invoice-logo.svg
vendored
|
Before Width: | Height: | Size: 174 KiB |
106
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Changelog
|
||||
|
||||
## [1.0.3] - 2026-03-29
|
||||
|
||||
### Added
|
||||
|
||||
- Email visibility toggle for seller and buyer sections — control whether the email address appears in the generated PDF
|
||||
- `ConfirmDiscardDialog` component to warn users about unsaved changes when closing the buyer/seller dialogs (replaces native `window.confirm`)
|
||||
- `useConfirmDiscard` reusable hook for managing discard confirmation state across buyer and seller dialogs
|
||||
- Knip CI workflow for automated dead-code and unused-dependency detection
|
||||
- `update-github-actions` script in `package.json` to streamline GitHub Actions version updates
|
||||
- Unit tests for the `generate-invoice` API route and core logic (`generate-invoice.ts`, `route.tsx`)
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactored `generate-invoice` API route: extracted core business logic into a standalone `generate-invoice.ts` module using a dependency-injection pattern for improved testability
|
||||
- Reworked seller and buyer information form sections with improved layout, locked-state banners, and cleaner field grouping
|
||||
- Buyer and seller dialogs now reset form values and pre-fill switch to their defaults when closed
|
||||
- Buyer and seller names are trimmed of whitespace before saving; whitespace-padded duplicates are rejected
|
||||
- Invalid localStorage entries for buyers and sellers are now validated and silently dropped instead of causing errors
|
||||
- Out-of-Date dates helper improved with more accurate state detection
|
||||
- Error message component layout and copy updated for better readability
|
||||
- Vitest config updated with JSX support (`esbuild.jsx: "automatic"`) to enable unit-testing JSX components
|
||||
- Restructured buyer and seller dialog components into dedicated feature directories under `sections/components/buyer` and `sections/components/seller`
|
||||
- GitHub Actions workflows updated to latest action versions; failure handling added to all CI jobs
|
||||
- Auto-scroll the invoice form on mobile when switching between tabs
|
||||
|
||||
### Fixed
|
||||
|
||||
- Pre-fill switch in buyer/seller dialogs no longer retains its state after the dialog is closed and reopened
|
||||
- Rate limit exceeded log upgraded from `console.log` to `console.error` for correct severity
|
||||
- Loading placeholder display fixed when switching invoice tabs on mobile
|
||||
|
||||
## [1.0.2] - 2026-03-10
|
||||
|
||||
### Added
|
||||
|
||||
- QR code generation for invoices with customizable descriptions and visibility toggles, supported in both default and Stripe templates
|
||||
- Logo upload for the default invoice template (previously available only in the Stripe template)
|
||||
- Searchable currency combobox with grouped categories, replacing the native dropdown for faster selection
|
||||
- Improved multi-page PDF support with automatic pagination and page breaks
|
||||
|
||||
### Changed
|
||||
|
||||
- Increased QR code size and improved rendering quality for better scannability
|
||||
- Enhanced invoice template text color and visuals for improved readability
|
||||
- Reorganized Stripe payment link input position in the form for better flow
|
||||
- Improved user feedback during invoice item deletion with better toast notification handling
|
||||
- Enhanced error handling to reset invoice metadata to defaults on errors
|
||||
- Clearer error messages when invoice sharing fails
|
||||
- Tooltip on the "Add invoice item" button for contextual guidance
|
||||
- Sentry error tracking integration for invoice sharing and GitHub stars features
|
||||
|
||||
### Fixed
|
||||
|
||||
- i18n issue when generating PDF via the API route
|
||||
- Delete invoice item flow not working correctly
|
||||
- Item name field validation too strict (now optional for flexibility)
|
||||
|
||||
## [1.0.1] - 2026-01-12
|
||||
|
||||
### Added
|
||||
|
||||
- Stripe-inspired invoice template with professional styling and layout optimizations
|
||||
- Dynamic template selection in the invoice form
|
||||
- Logo upload capability for the Stripe template with validation
|
||||
- Stripe payment URL field for enhanced invoice functionality
|
||||
- Customizable Tax/VAT label text (e.g., "VAT", "GST", "Sales Tax")
|
||||
- Customizable Tax Number label in buyer and seller information sections
|
||||
- Dynamic tax label updates based on selected invoice language
|
||||
|
||||
### Changed
|
||||
|
||||
- Landing page cleanup: refined About section and footer for better layout and accessibility
|
||||
- Call-to-action toasts: added custom, randomized CTA toasts encouraging user support
|
||||
- Added support for more currencies with improved date handling
|
||||
- Enhanced tooltips with detailed explanations and improved styling
|
||||
- Enhanced validation for VAT input to accept both numeric values and specific strings
|
||||
- Improved user interface messages for clarity regarding VAT input requirements
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug with accordion component
|
||||
- Error message for invoice link generation now includes a refresh suggestion
|
||||
|
||||
## [1.0.0] - 2025-11-19
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release of EasyInvoicePDF — a free, open-source invoice generator
|
||||
- Live preview: invoice updates in real-time as you make changes
|
||||
- Shareable links: generate secure links to share invoices directly with clients
|
||||
- Instant PDF download with one click
|
||||
- Multi-language support (English, Polish, German, Spanish, Portuguese, Russian, Ukrainian, French, Italian, Dutch)
|
||||
- Support for all major currencies with automatic locale-based formatting
|
||||
- European VAT calculation and formatting compliant with EU tax requirements
|
||||
- Complete seller and buyer information management with save-for-future-use
|
||||
- Detailed invoice items with descriptions, quantities, and pricing
|
||||
- Automatic tax calculations and totals
|
||||
- Invoice numbering, dating, and payment terms
|
||||
- No sign-up required — fully browser-based with no server-side data storage
|
||||
|
||||
[1.0.3]: https://github.com/VladSez/easy-invoice-pdf/compare/v1.0.2...v1.0.3
|
||||
[1.0.2]: https://github.com/VladSez/easy-invoice-pdf/compare/EasyInvoicePDF-1.0.1...v1.0.2
|
||||
[1.0.1]: https://github.com/VladSez/easy-invoice-pdf/compare/EasyInvoicePDF-v1.0.0...EasyInvoicePDF-1.0.1
|
||||
[1.0.0]: https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0
|
||||
48
README.md
|
|
@ -94,16 +94,52 @@ _**Choose between multiple professional templates** (Default and Stripe) to matc
|
|||
|
||||
</div>
|
||||
|
||||
## 📢 What's New
|
||||
|
||||
**v1.0.3 — Seller & Buyer Improvements (Mar 2026)**
|
||||
|
||||
- **Email visibility toggle** — control whether email addresses appear in the generated PDF
|
||||
- **Confirm discard dialog** — warns about unsaved changes when closing buyer/seller dialogs
|
||||
- **Improved seller & buyer forms** — reworked layout, locked-state banners, and cleaner field grouping
|
||||
|
||||
[Full release notes for v1.0.3](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.3)
|
||||
|
||||
---
|
||||
|
||||
**v1.0.2 — QR Codes & Multi-Page PDFs (Mar 2026)**
|
||||
|
||||
- **QR code support** — add payment QR codes with custom descriptions to both templates
|
||||
- **Logo upload for default template** — previously available only in the Stripe template
|
||||
- **Searchable currency combobox** — grouped categories replace the native dropdown
|
||||
- **Improved multi-page PDFs** — automatic pagination and page breaks for large invoices
|
||||
|
||||
[Full release notes for v1.0.2](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.2)
|
||||
|
||||
---
|
||||
|
||||
**v1.0.1 — Stripe Template & Tax Customization (Jan 2026)**
|
||||
|
||||
- **Stripe-inspired invoice template** — professional styling with logo upload and payment URL field
|
||||
- **Customizable tax labels** — set VAT, GST, Sales Tax, or any custom label per invoice language
|
||||
- **Improved i18n** — dynamic tax label updates and better locale-based currency handling
|
||||
|
||||
[Full release notes for v1.0.1](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-1.0.1)
|
||||
|
||||
---
|
||||
|
||||
**v1.0.0 — Initial Release (Nov 2025)**
|
||||
|
||||
- **Live preview** — invoice updates in real-time as you type
|
||||
- **Instant PDF download** — one-click, no sign-up required
|
||||
- **Shareable links** — send invoices directly to clients without attachments
|
||||
- **10 languages & 120+ currencies** — full multi-language and currency support out of the box
|
||||
|
||||
[Full release notes for v1.0.0](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0)
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
[](https://repostars.dev/?repos=VladSez%2Feasy-invoice-pdf&theme=dark)
|
||||
|
||||
## 📢 News & Updates
|
||||
|
||||
- **Mar 10, 2026**: Added QR code support, logo upload for the default template, searchable currency combobox, and improved multi-page PDF support. [Release notes for v1.0.2](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.2)
|
||||
- **Jan 11, 2026**: Added customizable tax/VAT labels, improved internationalization (i18n) translations, enhanced overall performance, and fixed multiple bugs. [Release notes for v1.0.1](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-1.0.1)
|
||||
- **Nov 19, 2025**: EasyInvoicePDF version 1.0.0 released! Create professional invoices in seconds. Welcome to try EasyInvoicePDF. [Release notes for v1.0.0](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0)
|
||||
|
||||
## 🎥 Demo Video
|
||||
|
||||
Watch a quick demo of EasyInvoicePDF in action to see how easy it is to create professional invoices in seconds. The video demonstrates key features like **Live Preview**, **Instant PDF Download**, and **Customization Options**.
|
||||
|
|
|
|||
|
|
@ -1099,61 +1099,4 @@ test.describe("Buyer management", () => {
|
|||
expect(parsedData).toHaveLength(1);
|
||||
expect(parsedData[0].name).toBe("Globex Corp");
|
||||
});
|
||||
|
||||
test("drops invalid localStorage entries and preserves valid ones on save", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Seed: one valid + one corrupt buyer (component already mounted with empty localStorage,
|
||||
// so "New Buyer" button is still visible — onSubmit reads localStorage fresh)
|
||||
await page.addInitScript(() => {
|
||||
const buyers = [
|
||||
{
|
||||
id: "pre-seeded-valid",
|
||||
name: "Pre-seeded Valid Buyer",
|
||||
address: "1 Valid Avenue",
|
||||
email: "valid@pre-seeded.com",
|
||||
emailFieldIsVisible: true,
|
||||
vatNo: "VATPRE",
|
||||
vatNoLabelText: "Tax Number",
|
||||
vatNoFieldIsVisible: true,
|
||||
notes: "",
|
||||
notesFieldIsVisible: true,
|
||||
},
|
||||
{ corrupt: true, notABuyer: 42 },
|
||||
];
|
||||
localStorage.setItem("EASY_INVOICE_PDF_BUYERS", JSON.stringify(buyers));
|
||||
});
|
||||
|
||||
await page.goto("/?template=default");
|
||||
await expect(page).toHaveURL("/?template=default");
|
||||
|
||||
await page.getByRole("button", { name: "New Buyer" }).click();
|
||||
|
||||
const manageBuyerDialog = page.getByTestId("manage-buyer-dialog");
|
||||
await manageBuyerDialog
|
||||
.getByRole("textbox", { name: "Name" })
|
||||
.fill("Brand New Buyer");
|
||||
await manageBuyerDialog
|
||||
.getByRole("textbox", { name: "Address" })
|
||||
.fill("99 Fresh Boulevard");
|
||||
|
||||
await manageBuyerDialog.getByRole("button", { name: "Save Buyer" }).click();
|
||||
|
||||
// Submit succeeds — no error toast
|
||||
await expect(
|
||||
page.getByText("Buyer added and applied to invoice", { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// localStorage: valid entry preserved, corrupt entry dropped, new entry added
|
||||
const storedData = (await page.evaluate(() =>
|
||||
localStorage.getItem("EASY_INVOICE_PDF_BUYERS"),
|
||||
)) as string;
|
||||
const parsedData = JSON.parse(storedData) as BuyerData[];
|
||||
|
||||
expect(parsedData).toHaveLength(2);
|
||||
expect(parsedData.some((b) => b.name === "Pre-seeded Valid Buyer")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(parsedData.some((b) => b.name === "Brand New Buyer")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ import { INITIAL_INVOICE_DATA } from "@/app/constants";
|
|||
import { INVOICE_PDF_TRANSLATIONS } from "@/app/(app)/pdf-i18n-translations/pdf-translations";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
SMALL_TEST_IMAGE_BASE64,
|
||||
uploadBase64LogoAsFile,
|
||||
} from "../stripe-invoice-template/utils";
|
||||
import { uploadLogoFile } from "../stripe-invoice-template/utils";
|
||||
|
||||
// IMPORTANT: we use custom extended test fixture that provides a temporary download directory for each test
|
||||
import { test, expect } from "../utils/extended-playwright-test";
|
||||
|
|
@ -19,7 +16,7 @@ test.describe("Default Invoice Template", () => {
|
|||
// we set the system time to a fixed date, so that the invoice number and other dates are consistent across tests
|
||||
await page.clock.setSystemTime(new Date("2025-12-17T00:00:00Z"));
|
||||
|
||||
await page.goto("/");
|
||||
await page.goto("/?template=default");
|
||||
await expect(page).toHaveURL("/?template=default");
|
||||
});
|
||||
|
||||
|
|
@ -1579,7 +1576,7 @@ test.describe("Default Invoice Template", () => {
|
|||
const generalInfoSection = page.getByTestId("general-information-section");
|
||||
|
||||
// Upload a valid logo
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
// Verify logo preview is visible
|
||||
await expect(page.getByText("Logo uploaded successfully!")).toBeVisible();
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 146 KiB |
BIN
e2e/fixtures/app-logo.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
|
|
@ -1165,67 +1165,4 @@ test.describe("Seller management", () => {
|
|||
expect(parsedData).toHaveLength(1);
|
||||
expect(parsedData[0].name).toBe("Acme Corp");
|
||||
});
|
||||
|
||||
test("drops invalid localStorage entries and preserves valid ones on save", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Seed: one valid + one corrupt seller (component already mounted with empty localStorage,
|
||||
// so "New Seller" button is still visible — onSubmit reads localStorage fresh)
|
||||
await page.evaluate(() => {
|
||||
const sellers = [
|
||||
{
|
||||
id: "pre-seeded-valid",
|
||||
name: "Pre-seeded Valid Seller",
|
||||
address: "1 Valid Road",
|
||||
email: "valid@pre-seeded.com",
|
||||
emailFieldIsVisible: true,
|
||||
vatNo: "VATPRE",
|
||||
vatNoLabelText: "Tax Number",
|
||||
vatNoFieldIsVisible: true,
|
||||
accountNumber: "ACCTPRE",
|
||||
accountNumberFieldIsVisible: true,
|
||||
swiftBic: "SWIFTPRE",
|
||||
swiftBicFieldIsVisible: true,
|
||||
notes: "",
|
||||
notesFieldIsVisible: true,
|
||||
},
|
||||
{ corrupt: true, notASeller: 42 },
|
||||
];
|
||||
localStorage.setItem("EASY_INVOICE_PDF_SELLERS", JSON.stringify(sellers));
|
||||
});
|
||||
|
||||
await page.goto("/?template=default");
|
||||
await expect(page).toHaveURL("/?template=default");
|
||||
|
||||
await page.getByRole("button", { name: "New Seller" }).click();
|
||||
|
||||
const manageSellerDialog = page.getByTestId("manage-seller-dialog");
|
||||
await manageSellerDialog
|
||||
.getByRole("textbox", { name: "Name" })
|
||||
.fill("Brand New Seller");
|
||||
await manageSellerDialog
|
||||
.getByRole("textbox", { name: "Address" })
|
||||
.fill("99 Fresh Lane");
|
||||
|
||||
await manageSellerDialog
|
||||
.getByRole("button", { name: "Save Seller" })
|
||||
.click();
|
||||
|
||||
// Submit succeeds — no error toast
|
||||
await expect(
|
||||
page.getByText("Seller added and applied to invoice", { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// localStorage: valid entry preserved, corrupt entry dropped, new entry added
|
||||
const storedData = (await page.evaluate(() =>
|
||||
localStorage.getItem("EASY_INVOICE_PDF_SELLERS"),
|
||||
)) as string;
|
||||
const parsedData = JSON.parse(storedData) as SellerData[];
|
||||
|
||||
expect(parsedData).toHaveLength(2);
|
||||
expect(parsedData.some((s) => s.name === "Pre-seeded Valid Seller")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(parsedData.some((s) => s.name === "Brand New Seller")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { PDF_DATA_LOCAL_STORAGE_KEY, type InvoiceData } from "@/app/schema";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { SMALL_TEST_IMAGE_BASE64, uploadBase64LogoAsFile } from "./utils";
|
||||
import { uploadLogoFile } from "./utils";
|
||||
|
||||
test.describe("Stripe Invoice Sharing Logic", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.goto("/?template=default");
|
||||
});
|
||||
|
||||
test("can share invoice with Stripe template and *WITHOUT* logo", async ({
|
||||
|
|
@ -67,13 +67,13 @@ test.describe("Stripe Invoice Sharing Logic", () => {
|
|||
|
||||
// Verify logo upload section is visible (but empty since no logo was shared)
|
||||
await expect(
|
||||
newPageGeneralInfoSection.getByText("Company Logo (Optional)"),
|
||||
newPageGeneralInfoSection.getByText("Company Logo", { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// Verify payment URL section is visible
|
||||
await expect(
|
||||
newPageGeneralInfoSection.getByRole("textbox", {
|
||||
name: "Payment Link URL (Optional)",
|
||||
name: "Payment Link URL",
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
|
|||
.selectOption("stripe");
|
||||
|
||||
// Upload logo
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
// Wait for logo to be uploaded
|
||||
const generalInfoSection = page.getByTestId("general-information-section");
|
||||
|
|
@ -148,7 +148,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
|
|||
.selectOption("stripe");
|
||||
|
||||
// Upload logo
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
// Wait debounce timeout
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
|
|
@ -223,7 +223,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
|
|||
.selectOption("stripe");
|
||||
|
||||
// Upload logo
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
// Verify share button becomes disabled
|
||||
await expect(shareButton).toHaveAttribute("data-disabled", "true");
|
||||
|
|
@ -259,7 +259,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
|
|||
await page.waitForURL("/?template=stripe");
|
||||
|
||||
// Upload logo
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
// Wait for upload and verify share button is disabled
|
||||
const generalInfoSection = page.getByTestId("general-information-section");
|
||||
|
|
@ -287,9 +287,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
|
|||
|
||||
const parsedData = JSON.parse(storedData) as InvoiceData;
|
||||
|
||||
expect(parsedData).toMatchObject({
|
||||
logo: SMALL_TEST_IMAGE_BASE64,
|
||||
} satisfies Pick<InvoiceData, "logo">);
|
||||
expect(parsedData.logo).toBeTruthy();
|
||||
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
|
|
@ -330,7 +328,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
|
|||
.selectOption("stripe");
|
||||
|
||||
// Upload logo
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
// Wait for logo to be uploaded
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "@/app/schema";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { SMALL_TEST_IMAGE_BASE64, uploadBase64LogoAsFile } from "./utils";
|
||||
import { uploadLogoFile } from "./utils";
|
||||
|
||||
// IMPORTANT: we use custom extended test fixture that provides a temporary download directory for each test
|
||||
import { expect, test } from "../utils/extended-playwright-test";
|
||||
|
|
@ -21,7 +21,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
// we set the system time to a fixed date, so that the invoice number and other dates are consistent across tests
|
||||
await page.clock.setSystemTime(new Date("2025-12-17T00:00:00Z"));
|
||||
|
||||
await page.goto("/");
|
||||
await page.goto("/?template=default");
|
||||
});
|
||||
|
||||
test("displays correct OG meta tags for Stripe template", async ({
|
||||
|
|
@ -83,7 +83,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
|
||||
// Logo section is visible on default template
|
||||
await expect(
|
||||
generalInfoSection.getByText("Company Logo (Optional)"),
|
||||
generalInfoSection.getByText("Company Logo", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
generalInfoSection.getByTestId("logo-upload-input"),
|
||||
|
|
@ -92,7 +92,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
// Payment URL section should not be visible on default template
|
||||
await expect(
|
||||
generalInfoSection.getByRole("textbox", {
|
||||
name: "Payment Link URL (Optional)",
|
||||
name: "Payment Link URL",
|
||||
}),
|
||||
).toBeHidden();
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
generalInfoSection.getByText("Company Logo (Optional)"),
|
||||
generalInfoSection.getByText("Company Logo", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
generalInfoSection.getByText("Click to upload your company logo"),
|
||||
|
|
@ -124,7 +124,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
// Payment URL section should now be visible on Stripe template
|
||||
await expect(
|
||||
generalInfoSection.getByRole("textbox", {
|
||||
name: "Payment Link URL (Optional)",
|
||||
name: "Payment Link URL",
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
|
||||
// Logo section remains visible on default template
|
||||
await expect(
|
||||
generalInfoSection.getByText("Company Logo (Optional)"),
|
||||
generalInfoSection.getByText("Company Logo", { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
|
|
@ -145,7 +145,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
// Payment URL section should be hidden again on default template
|
||||
await expect(
|
||||
generalInfoSection.getByRole("textbox", {
|
||||
name: "Payment Link URL (Optional)",
|
||||
name: "Payment Link URL",
|
||||
}),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
|
@ -226,7 +226,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
const generalInfoSection = page.getByTestId("general-information-section");
|
||||
|
||||
// Upload a valid small image
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
// Should show success toast
|
||||
await expect(page.getByText("Logo uploaded successfully!")).toBeVisible();
|
||||
|
|
@ -259,7 +259,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
.selectOption("stripe");
|
||||
|
||||
// Upload a valid small image
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
const generalInfoSection = page.getByTestId("general-information-section");
|
||||
|
||||
|
|
@ -304,7 +304,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
|
||||
const generalInfoSection = page.getByTestId("general-information-section");
|
||||
const paymentUrlInput = generalInfoSection.getByRole("textbox", {
|
||||
name: "Payment Link URL (Optional)",
|
||||
name: "Payment Link URL",
|
||||
});
|
||||
|
||||
// Try invalid URL
|
||||
|
|
@ -337,11 +337,11 @@ test.describe("Stripe Invoice Template", () => {
|
|||
|
||||
// Add payment URL
|
||||
await generalInfoSection
|
||||
.getByRole("textbox", { name: "Payment Link URL (Optional)" })
|
||||
.getByRole("textbox", { name: "Payment Link URL" })
|
||||
.fill("https://buy.stripe.com/test_payment_link");
|
||||
|
||||
// Upload logo
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
// Wait for logo to be uploaded and PDF to regenerate
|
||||
await expect(page.getByText("Logo uploaded successfully!")).toBeVisible();
|
||||
|
|
@ -369,9 +369,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
|
||||
const parsedData = JSON.parse(storedData) as InvoiceData;
|
||||
|
||||
expect(parsedData).toMatchObject({
|
||||
logo: SMALL_TEST_IMAGE_BASE64,
|
||||
} satisfies Pick<InvoiceData, "logo">);
|
||||
expect(parsedData.logo).toBeTruthy();
|
||||
|
||||
// Reload page
|
||||
await page.reload();
|
||||
|
|
@ -386,7 +384,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
// Verify payment URL persists
|
||||
await expect(
|
||||
generalInfoSection.getByRole("textbox", {
|
||||
name: "Payment Link URL (Optional)",
|
||||
name: "Payment Link URL",
|
||||
}),
|
||||
).toHaveValue("https://buy.stripe.com/test_payment_link");
|
||||
|
||||
|
|
@ -900,7 +898,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
await expect(page).toHaveURL("/?template=stripe");
|
||||
|
||||
// Upload a valid logo
|
||||
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
|
||||
await uploadLogoFile(page);
|
||||
|
||||
// Wait for logo to be uploaded and PDF to regenerate
|
||||
await expect(page.getByText("Logo uploaded successfully!")).toBeVisible();
|
||||
|
|
@ -917,7 +915,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
|
||||
// Add payment URL
|
||||
await generalInfoSection
|
||||
.getByRole("textbox", { name: "Payment Link URL (Optional)" })
|
||||
.getByRole("textbox", { name: "Payment Link URL" })
|
||||
.fill("https://buy.stripe.com/test_payment_link");
|
||||
|
||||
// Wait a moment for any debounced localStorage updates
|
||||
|
|
@ -933,9 +931,7 @@ test.describe("Stripe Invoice Template", () => {
|
|||
|
||||
const parsedData = JSON.parse(storedData) as InvoiceData;
|
||||
|
||||
expect(parsedData).toMatchObject({
|
||||
logo: SMALL_TEST_IMAGE_BASE64,
|
||||
} satisfies Pick<InvoiceData, "logo">);
|
||||
expect(parsedData.logo).toBeTruthy();
|
||||
|
||||
const downloadPDFButton = page.getByRole("link", {
|
||||
name: "Download PDF in English",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 149 KiB |
|
|
@ -1,51 +1,8 @@
|
|||
/**
|
||||
* Create a small test image in base64 format (1x1 pixel PNG)
|
||||
*/
|
||||
export const SMALL_TEST_IMAGE_BASE64 =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
|
||||
import path from "node:path";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Upload a base64 image to the logo input
|
||||
*/
|
||||
export const uploadBase64LogoAsFile = (base64Data: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const fileInput = document.querySelector(
|
||||
"#logoUpload",
|
||||
) as HTMLInputElement | null;
|
||||
const LOGO_FIXTURE_PATH = path.join(__dirname, "../fixtures/app-logo.png");
|
||||
|
||||
if (!fileInput) {
|
||||
throw new Error('Logo upload input "#logoUpload" was not found');
|
||||
}
|
||||
|
||||
if (!base64Data) {
|
||||
throw new Error("Base64 data is required");
|
||||
}
|
||||
|
||||
// Convert base64 data to binary string by removing metadata and decoding
|
||||
const byteString = atob(base64Data.split(",")[1]);
|
||||
|
||||
// Extract MIME type from base64 metadata (e.g. "image/png")
|
||||
const mimeString = base64Data.split(",")[0].split(":")[1].split(";")[0];
|
||||
|
||||
// Create ArrayBuffer to store binary data
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
|
||||
// Create Uint8Array view to write bytes to ArrayBuffer
|
||||
const ia = new Uint8Array(ab);
|
||||
|
||||
// Convert binary string to bytes and write to array
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// Create File object from ArrayBuffer with name and MIME type
|
||||
const file = new File([ab], "test-logo.png", { type: mimeString });
|
||||
|
||||
// Create DataTransfer to simulate file input
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
|
||||
// Set file input's files property and trigger change event
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
export async function uploadLogoFile(page: Page) {
|
||||
await page.setInputFiles("#logoUpload", LOGO_FIXTURE_PATH);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pdf-invoice-generator",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a",
|
||||
"pnpm": {
|
||||
|
|
|
|||
|
|
@ -656,7 +656,7 @@ export const GeneralInformation = memo(function GeneralInformation({
|
|||
{/* Logo Upload */}
|
||||
<div className="">
|
||||
<Label htmlFor="logoUpload" className="mb-2">
|
||||
Company Logo (Optional)
|
||||
Company Logo
|
||||
</Label>
|
||||
|
||||
{logo ? (
|
||||
|
|
@ -718,7 +718,7 @@ export const GeneralInformation = memo(function GeneralInformation({
|
|||
{template === "stripe" && (
|
||||
<div className="">
|
||||
<Label htmlFor={`stripePayOnlineUrl`} className="">
|
||||
Payment Link URL (Optional)
|
||||
Payment Link URL
|
||||
</Label>
|
||||
|
||||
<Controller
|
||||
|
|
|
|||
|
|
@ -73,3 +73,5 @@ Automatically calculate European VAT rates and totals for your invoices. Complia
|
|||
Start creating professional invoices in seconds with our free, open-source tool at [easyinvoicepdf.com](https://easyinvoicepdf.com)
|
||||
|
||||
**GitHub Repository**: [VladSez/easy-invoice-pdf](https://github.com/VladSez/easy-invoice-pdf)
|
||||
|
||||
[View Release Notes for v1.0.0 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0)
|
||||
|
|
|
|||
|
|
@ -57,3 +57,5 @@ export const metadata = {
|
|||
---
|
||||
|
||||
**Full Changelog**: [`EasyInvoicePDF-1.0.1...v1.0.2`](https://github.com/VladSez/easy-invoice-pdf/compare/EasyInvoicePDF-1.0.1...v1.0.2)
|
||||
|
||||
[View Release Notes for v1.0.2 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.2)
|
||||
|
|
|
|||
55
src/app/changelog/content/seller-buyer-improvements.mdx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
export const metadata = {
|
||||
title: "Seller & Buyer Improvements, Email Visibility Toggle and More",
|
||||
description:
|
||||
"Email visibility toggle for seller/buyer sections, reworked form layouts with locked-state banners, ConfirmDiscardDialog, and auto-scroll on mobile",
|
||||
date: "2026-03-29",
|
||||
version: "1.0.3",
|
||||
type: "minor",
|
||||
};
|
||||
|
||||
## ✨ Highlights
|
||||
|
||||
- **Email visibility toggle** for seller and buyer sections — control whether the email address appears in the generated PDF
|
||||
- **`ConfirmDiscardDialog`** component to warn users about unsaved changes when closing the buyer/seller dialogs
|
||||
- **Reworked seller and buyer information form sections** with improved layout, locked-state banners, and cleaner field grouping
|
||||
- **Auto-scroll the invoice form on mobile** when switching between tabs (UX improvement)
|
||||
- **Out-of-Date dates helper** improved with more accurate state detection
|
||||
|
||||
<video
|
||||
src="https://github.com/user-attachments/assets/1b39eb6f-e2be-493f-9825-cbce3dc6fa16"
|
||||
controls
|
||||
style={{ width: "100%", borderRadius: "8px" }}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
### Changed
|
||||
|
||||
- Invalid localStorage entries for buyers and sellers are now validated and silently dropped instead of causing errors
|
||||
- Error message component layout and copy updated for better readability
|
||||
- GitHub Actions workflows updated to latest action versions; failure handling added to all CI jobs
|
||||
- Added knip GitHub CI job for automated dead-code and unused-dependency detection
|
||||
|
||||
### Fixed
|
||||
|
||||
- Pre-fill switch in buyer/seller dialogs no longer retains its state after the dialog is closed and reopened
|
||||
- Buyer and seller dialogs now reset form values and pre-fill switch to their defaults when closed
|
||||
- Buyer and seller names are trimmed of whitespace before saving; whitespace-padded duplicates are rejected
|
||||
|
||||
### What's Changed
|
||||
|
||||
- feat: auto-scroll form on mobile when switching between tabs, fix loading placeholder by @VladSez in [#191](https://github.com/VladSez/easy-invoice-pdf/pull/191)
|
||||
- refactor: update layout and improve accessibility in invoice-related components by @VladSez in [#192](https://github.com/VladSez/easy-invoice-pdf/pull/192)
|
||||
- refactor: improve layout in BuyerDialog and SellerDialog components by @VladSez in [#193](https://github.com/VladSez/easy-invoice-pdf/pull/193)
|
||||
- refactor: streamline toast management in InvoiceForm component by @VladSez in [#194](https://github.com/VladSez/easy-invoice-pdf/pull/194)
|
||||
- feat: enhance buyer and seller management with new functionality and improved toast notifications by @VladSez in [#195](https://github.com/VladSez/easy-invoice-pdf/pull/195)
|
||||
- feat: reworked seller/buyer sections, email visibility switch field, shared invoice indicator, improved Out-of-Date helper + minor things by @VladSez in [#197](https://github.com/VladSez/easy-invoice-pdf/pull/197)
|
||||
- feat: add cancel confirm dialog for seller/buyer dialog, update gh actions, added new script update-github-actions in package.json by @VladSez in [#198](https://github.com/VladSez/easy-invoice-pdf/pull/198)
|
||||
- fix: update GitHub Actions script and fix buyer/seller dialog pre-fill bug by @VladSez in [#199](https://github.com/VladSez/easy-invoice-pdf/pull/199)
|
||||
- feat: replaced window.confirm with alert discard dialog for buyer and seller management by @VladSez in [#200](https://github.com/VladSez/easy-invoice-pdf/pull/200)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [`v1.0.2...v1.0.3`](https://github.com/VladSez/easy-invoice-pdf/compare/v1.0.2...v1.0.3)
|
||||
|
||||
[View Release Notes for v1.0.3 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.3)
|
||||
|
|
@ -45,3 +45,5 @@ export const metadata = {
|
|||
---
|
||||
|
||||
**Full Changeset**: [`63b097b...7ae733b`](https://github.com/VladSez/easy-invoice-pdf/compare/63b097b...7ae733b)
|
||||
|
||||
[View Release Notes for v1.0.1 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-1.0.1)
|
||||
|
|
|
|||