Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ The **ZPL** panel at the bottom shows the generated ZPL, **Copy** puts it on the

- **File → Export ZPL:** saves a `.zpl` file, or `.prn` where the save dialog offers file types
- **File → Output:** opens the output dialog with two destinations.
- **Print** sends the code to a Zebra over the network, through the Zebra Browser Print agent in the browser, or through the system spooler or USB on desktop. Its **Other printer** way renders the label and hands it to the operating system's print dialog, which reaches any printer with a driver. On desktop, **Check printer** asks the printer for its state over the network or USB.
- **File** writes a **PDF** with one page per label page, rendered by the preview renderer. With the renderer off, ZPLab captures the canvas, so the PDF holds the current page only. The **Scope** option **One label per data row** needs the renderer and holds one page per row. **PNG** saves the canvas drawing of the current label.
- **Print** sends the code to a Zebra over the network, through the Zebra Browser Print agent in the browser, or through the system spooler or USB on desktop. Its **Other printer** way renders the label and hands it to the operating system's print dialog, which reaches any printer with a driver. Its **Renderer** select picks the preview renderer. On desktop, **Check printer** asks the printer for its state over the network or USB.
- **File** writes a **PDF** with one page per label page, rendered by the chosen **Renderer**. With **Built-in**, the PDF holds the current page only. The **Scope** option **One label per data row** needs the renderer and holds one page per row. **PNG** saves the canvas drawing of the current label.

### 5. Save the design

Expand Down Expand Up @@ -155,7 +155,7 @@ On desktop, **Connect data** also reads an Excel worksheet. **File → Settings
- Saved designs and label exports exclude **Setup Script** values such as printer name and locale.
- **Clear** resets the **Setup Script** values and keeps the uploads.

The **App → Printer** tab sets how labels are sent and to which printer. On desktop, **Adopt from printer** reads the printer's own settings and asks which of the differing values to adopt. **File → Output** opens with that method selected. The **App → Preview** tab selects the renderer: the connected printer (desktop), Labelary, or off. It takes a premium Labelary endpoint and API key. The key is stored in the OS keychain on desktop and in browser storage on the web.
The **App → Printer** tab sets how labels are sent and to which printer. On desktop, **Adopt from printer** reads the printer's own settings and asks which of the differing values to adopt. **File → Output** opens with that method selected. The **App → Preview** tab selects the renderer: the connected printer (desktop), Labelary, or **Built-in**. It takes a premium Labelary endpoint and API key. The key is stored in the OS keychain on desktop and in browser storage on the web.

### MCP server

Expand Down
27 changes: 27 additions & 0 deletions src/components/Output/OutputDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,33 @@ describe("OutputDialog", () => {
expect(output.compareDocumentPosition(notice) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});

it("offers the renderer where its image is used and writes the shared setting", () => {
act(() => {
useLabelStore.setState({ previewProvider: "labelary", labelaryNoticeAcknowledged: true });
});
const r = showDialog();
act(() => {
fireEvent.click(r.getByText(loc.tabSystem));
});
act(() => {
fireEvent.click(r.getByRole("button", { name: loc.rendererHeading }));
});
act(() => {
r.getByRole("option", { name: en.printerSettings.preview.providerPrinter }).click();
});
expect(useLabelStore.getState().previewProvider).toBe("printer");

act(() => {
useLabelStore.getState().setOutputChoice({ kind: "file", fileFormat: "pdf" });
});
expect(r.getByRole("button", { name: loc.rendererHeading })).toBeTruthy();
// The PNG is always the canvas, so there is nothing to pick.
act(() => {
useLabelStore.getState().setOutputChoice({ fileFormat: "png" });
});
expect(r.queryByRole("button", { name: loc.rendererHeading })).toBeNull();
});

it("stays and says that the label is rendering", async () => {
let finish: () => void = () => undefined;
const onPrintImage = vi.fn(() => new Promise<void>((resolve) => (finish = resolve)));
Expand Down
2 changes: 2 additions & 0 deletions src/components/Output/OutputDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { usePrintWays } from "../../hooks/usePrintWays";
import { useSendNotices } from "../../hooks/useSendNotices";
import { ActiveRowNotice, DatasetNotice } from "./DatasetNotice";
import { SystemPrintBody } from "./SystemPrintBody";
import { RendererSelect } from "./RendererSelect";
import { ZebraWayPanel } from "./ZebraWayPanel";

interface Props {
Expand Down Expand Up @@ -189,6 +190,7 @@ export function OutputDialog({ zpl, source, facts, onClose, onPrintImage, onExpo

{choice.fileFormat === "pdf" ? (
<>
<RendererSelect />
{scopeChoosable && (
// Flex on an inner box, because a legend inside a flex fieldset lays out per browser.
<fieldset className="min-w-0">
Expand Down
18 changes: 18 additions & 0 deletions src/components/Output/OutputDialog.web.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ describe("OutputDialog in the browser", () => {
expect(printButton(r.container)).toBeTruthy();
});

it("lists the printer renderer but locks it, like the settings do", () => {
const r = showDialog({ facts: webFacts });
act(() => {
fireEvent.click(r.getByRole("button", { name: loc.rendererHeading }));
});
expect(r.getByRole("option", { name: en.printerSettings.preview.providerPrinter }).getAttribute("aria-disabled")).toBe("true");
expect(r.getByRole("option", { name: en.printerSettings.preview.providerNone }).getAttribute("aria-disabled")).toBeNull();
});

it("names the renderer that draws when the stored one cannot", () => {
act(() => {
useLabelStore.setState({ previewProvider: "printer" });
});
const r = showDialog({ facts: webFacts });
// The stored printer choice degrades to Labelary off the desktop, not to the built-in renderer.
expect(r.getByText(`Currently active: ${en.printerSettings.preview.providerLabelary}`)).toBeTruthy();
});

it("runs the print path the rendered label takes", async () => {
const r = showDialog({ facts: webFacts });

Expand Down
37 changes: 37 additions & 0 deletions src/components/Output/RendererSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useId } from "react";
import { useT } from "../../hooks/useT";
import { formatTemplate } from "../../lib/formatTemplate";
import { previewProviderChoices, previewProviderLabel } from "../../lib/previewProviderLabel";
import { selectEffectivePreviewProvider, useLabelStore } from "../../store/labelStore";
import type { PreviewProvider } from "../../store/slices/uiSlice";
import { sectionHeadingCls } from "../ui/formStyles";
import { Select } from "../ui/Select";

/** The renderer choice beside the action that uses its image, writing the same setting as the settings tab. */
export function RendererSelect() {
const t = useT();
const id = useId();
const chosen = useLabelStore((s) => s.previewProvider);
const effective = useLabelStore(selectEffectivePreviewProvider);
const labelaryAvailable = useLabelStore((s) => s.thirdParty.labelary);
const setProvider = useLabelStore((s) => s.setPreviewProvider);
const choices = previewProviderChoices(t, { thirdParty: { labelary: labelaryAvailable } });
return (
<div className="flex flex-col gap-1">
<label htmlFor={id} className={sectionHeadingCls}>
{t.zebraPrint.rendererHeading}
</label>
<Select<PreviewProvider>
id={id}
value={chosen}
onChange={setProvider}
groups={[{ options: choices.map((c) => ({ value: c.provider, label: c.label, disabled: c.disabled, tooltip: c.reason })) }]}
/>
{effective !== chosen && (
<span className="text-[10px] text-muted">
{formatTemplate(t.printerSettings.preview.providerInUseFmt, { provider: previewProviderLabel(t, effective) })}
</span>
)}
</div>
);
}
2 changes: 2 additions & 0 deletions src/components/Output/SystemPrintBody.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useT } from "../../hooks/useT";
import { primaryButtonCls } from "../ui/formStyles";
import { Tooltip } from "../ui/Tooltip";
import { RendererSelect } from "./RendererSelect";

/** The system way's body, with no device to pick and nothing to address. */
export function SystemPrintBody({ onPrint, rendering, locked }: { onPrint: () => void; rendering: boolean; locked: boolean }) {
const t = useT();
const loc = t.zebraPrint;
return (
<div className="flex flex-col gap-3 p-4" aria-busy={rendering || undefined}>
<RendererSelect />
<p aria-live="polite" className="font-mono text-[10px] text-muted min-h-4">
{rendering ? loc.systemRendering : ""}
</p>
Expand Down
9 changes: 5 additions & 4 deletions src/components/PrinterSettings/PreviewSettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import { isDefaultHost } from "../../lib/labelary";
import { labelCls, inputCls, buttonCls, sectionHeadingCls } from "../ui/formStyles";
import { RadioOption } from "../ui/RadioOption";
import { formatTemplate } from "../../lib/formatTemplate";
import { previewProviderLabel } from "../../lib/previewProviderLabel";
import { previewProviderChoices, previewProviderLabel } from "../../lib/previewProviderLabel";

/** Preview configuration: which renderer draws the overlay and the Labelary privacy consent. */
export function PreviewSettingsTab() {
const t = useT();
const loc = t.printerSettings.preview;

// The radio shows the stored choice, so a click always commits. A degraded build names what renders instead.
// The radio shows the stored choice, so a click always commits. The note names what draws where the choice is out of reach.
const chosen = useLabelStore((s) => s.previewProvider);
const provider = useLabelStore(selectEffectivePreviewProvider);
const setProvider = useLabelStore((s) => s.setPreviewProvider);
const labelaryAvailable = useLabelStore((s) => s.thirdParty.labelary);
const locked = Object.fromEntries(previewProviderChoices(t, { thirdParty: { labelary: labelaryAvailable } }).map((c) => [c.provider, c.disabled]));
const labelaryConsent = useLabelStore((s) => s.labelaryNoticeAcknowledged);
const acknowledgeLabelaryNotice = useLabelStore((s) => s.acknowledgeLabelaryNotice);
const revokeLabelaryNotice = useLabelStore((s) => s.revokeLabelaryNotice);
Expand Down Expand Up @@ -84,7 +85,7 @@ export function PreviewSettingsTab() {
current={chosen}
onSelect={setProvider}
label={loc.providerLabelary}
disabled={!labelaryAvailable}
disabled={locked.labelary}
/>
<RadioOption
name="preview-provider"
Expand All @@ -93,7 +94,7 @@ export function PreviewSettingsTab() {
onSelect={setProvider}
label={loc.providerPrinter}
hint={isDesktopShell ? loc.providerPrinterHint : loc.providerPrinterDesktopOnly}
disabled={!isDesktopShell}
disabled={locked.printer}
/>
<RadioOption
name="preview-provider"
Expand Down
22 changes: 21 additions & 1 deletion src/lib/previewProviderLabel.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import type { Translations } from "../locales";
import type { LabelState } from "../store/labelStore";
import type { PreviewProvider } from "../store/slices/uiSlice";
import { isDesktopShell } from "./platform";

/** The one wording per renderer, shared by the settings and the PDF metadata. */
/** The one wording per renderer, so every place names it the same. */
export function previewProviderLabel(t: Translations, provider: PreviewProvider): string {
const loc = t.printerSettings.preview;
return { labelary: loc.providerLabelary, printer: loc.providerPrinter, none: loc.providerNone }[provider];
}

export interface PreviewProviderChoice {
provider: PreviewProvider;
label: string;
disabled: boolean;
/** Wording for the lock, absent where there is none to show. */
reason?: string;
}

/** The renderers as one choice list, so every place locks the same ones. */
export function previewProviderChoices(t: Translations, s: Pick<LabelState, "thirdParty">): PreviewProviderChoice[] {
const loc = t.printerSettings.preview;
return [
{ provider: "labelary", label: loc.providerLabelary, disabled: !s.thirdParty.labelary },
{ provider: "printer", label: loc.providerPrinter, disabled: !isDesktopShell, ...(isDesktopShell ? {} : { reason: loc.providerPrinterDesktopOnly }) },
{ provider: "none", label: loc.providerNone, disabled: false },
];
}
5 changes: 3 additions & 2 deletions src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,8 +855,8 @@ const ar = {
providerPrinter: 'الطابعة المتصلة',
providerPrinterHint: 'تعرض الطابعة الملصق باستخدام برنامجها الثابت الخاص؛ لا يتم طباعة أي شيء.',
providerPrinterDesktopOnly: 'يتطلب تطبيق سطح المكتب.',
providerNone: 'إيقاف',
providerNoneHint: 'يُخفي المعاينة. يستخدم تصدير PDF والطباعة كصورة اللوحة.',
providerNone: 'مدمج',
providerNoneHint: 'لا توجد طبقة معاينة. يستخدم تصدير PDF والطباعة كصورة رسم اللوحة، لذا يحتوي PDF على الصفحة الحالية فقط.',
providerInUseFmt: 'نشط حاليًا: {provider}',
printerTargetHint: 'يتم ضبط الطابعة ضمن التطبيق، الطابعة.',
privacyHeading: 'الخصوصية',
Expand Down Expand Up @@ -1887,6 +1887,7 @@ const ar = {
tabUsb: 'طابعة ZPL (USB)',
tabSystem: 'طابعة أخرى',
wayHeading: 'الطابعة',
rendererHeading: 'العارض',
wayNetwork: 'يرسل عبر TCP مباشرة إلى الطابعة، دون مشغّل أو قائمة انتظار. الطابعة تجيب، لذلك تعمل الحالة والمعاينة.',
wayNetworkWeb: 'يرسل الكود إلى منفذ الطابعة. لا يحصل المتصفح على رد، فلا يُقرأ أي شيء.',
wayBrowserPrint: 'يمر عبر Zebra Browser Print على الكمبيوتر الذي يشغّل المتصفح.',
Expand Down
5 changes: 3 additions & 2 deletions src/locales/bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,8 +855,8 @@ const bg = {
providerPrinter: 'Свързан принтер',
providerPrinterHint: 'Принтерът рендира етикета със собствения си фърмуер; нищо не се отпечатва.',
providerPrinterDesktopOnly: 'Изисква настолното приложение.',
providerNone: 'Изключено',
providerNoneHint: 'Скрива прегледа. Експортът на PDF и печатът като изображение използват платното.',
providerNone: 'Вграден',
providerNoneHint: 'Няма слой за преглед. Експортът на PDF и печатът като изображение използват рисунката на платното, затова PDF съдържа само текущата страница.',
providerInUseFmt: 'В момента активен: {provider}',
printerTargetHint: 'Принтерът се задава под Приложение, Принтер.',
privacyHeading: 'Поверителност',
Expand Down Expand Up @@ -1887,6 +1887,7 @@ const bg = {
tabUsb: 'ZPL принтер (USB)',
tabSystem: 'Друг принтер',
wayHeading: 'Принтер',
rendererHeading: 'Визуализатор',
wayNetwork: 'Изпраща директно към принтера по TCP, без драйвер и опашка. Принтерът отговаря, затова състоянието и визуализацията работят.',
wayNetworkWeb: 'Изпраща кода до порта на принтера. Браузърът не получава отговор, затова нищо не се чете обратно.',
wayBrowserPrint: 'Минава през Zebra Browser Print на компютъра, на който работи браузърът.',
Expand Down
5 changes: 3 additions & 2 deletions src/locales/cs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,8 +855,8 @@ const cs = {
providerPrinter: 'Připojená tiskárna',
providerPrinterHint: 'Tiskárna vykreslí štítek pomocí vlastního firmwaru; nic se netiskne.',
providerPrinterDesktopOnly: 'Vyžaduje desktopovou aplikaci.',
providerNone: 'Vypnuto',
providerNoneHint: 'Skryje náhled. Export PDF a tisk jako obrázek používají plátno.',
providerNone: 'Vestavěné',
providerNoneHint: 'Žádná vrstva náhledu. Export PDF a tisk jako obrázek používají kreslení plátna, takže PDF obsahuje jen aktuální stránku.',
providerInUseFmt: 'Aktuálně aktivní: {provider}',
printerTargetHint: 'Tiskárna se nastavuje pod Aplikace, Tiskárna.',
privacyHeading: 'Soukromí',
Expand Down Expand Up @@ -1887,6 +1887,7 @@ const cs = {
tabUsb: 'ZPL tiskárna (USB)',
tabSystem: 'Jiná tiskárna',
wayHeading: 'Tiskárna',
rendererHeading: 'Vykreslovač',
wayNetwork: 'Posílá přes TCP přímo na tiskárnu, bez ovladače a tiskové fronty. Tiskárna odpovídá, proto funguje stav i náhled.',
wayNetworkWeb: 'Odesílá kód na port tiskárny. Prohlížeč nedostane odpověď, takže se nic nenačte zpět.',
wayBrowserPrint: 'Jde přes Zebra Browser Print na počítači, na kterém běží prohlížeč.',
Expand Down
5 changes: 3 additions & 2 deletions src/locales/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,8 +855,8 @@ const da = {
providerPrinter: 'Tilsluttet printer',
providerPrinterHint: 'Printeren gengiver etiketten med sin egen firmware; der udskrives ikke noget.',
providerPrinterDesktopOnly: 'Kræver desktopappen.',
providerNone: 'Fra',
providerNoneHint: 'Skjuler forhåndsvisningen. PDF-eksport og udskrift som billede bruger lærredet.',
providerNone: 'Indbygget',
providerNoneHint: "Intet forhåndsvisningslag. PDF-eksport og udskrift som billede bruger lærredets optegning, så PDF'en kun indeholder den aktuelle side.",
providerInUseFmt: 'Aktiv nu: {provider}',
printerTargetHint: 'Printeren indstilles under App, Printer.',
privacyHeading: 'Privatliv',
Expand Down Expand Up @@ -1887,6 +1887,7 @@ const da = {
tabUsb: 'ZPL-printer (USB)',
tabSystem: 'Anden printer',
wayHeading: 'Printer',
rendererHeading: 'Renderer',
wayNetwork: 'Sender over TCP direkte til printeren, uden driver og kø. Printeren svarer, så status og forhåndsvisning virker.',
wayNetworkWeb: 'Sender koden til printerens port. Browseren får intet svar, så intet læses retur.',
wayBrowserPrint: 'Går via Zebra Browser Print på den computer, der kører browseren.',
Expand Down
5 changes: 3 additions & 2 deletions src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,8 +855,8 @@ const de = {
providerPrinter: 'Verbundener Drucker',
providerPrinterHint: 'Der Drucker rendert das Label mit seiner eigenen Firmware; es wird nichts gedruckt.',
providerPrinterDesktopOnly: 'Nur in der Desktop-App verfügbar.',
providerNone: 'Aus',
providerNoneHint: 'Blendet die Vorschau aus. PDF-Export und Bilddruck nutzen den Canvas.',
providerNone: 'Integriert',
providerNoneHint: 'Kein Vorschau-Overlay. PDF-Export und Bilddruck nutzen die Canvas-Zeichnung, daher enthält das PDF nur die aktuelle Seite.',
providerInUseFmt: 'Gerade aktiv: {provider}',
printerTargetHint: 'Der Drucker wird unter App, Drucker eingestellt.',
privacyHeading: 'Datenschutz',
Expand Down Expand Up @@ -1137,6 +1137,7 @@ const de = {
tabUsb: 'ZPL-Drucker (USB)',
tabSystem: 'Anderer Drucker',
wayHeading: 'Drucker',
rendererHeading: 'Renderer',
wayNetwork: 'Sendet per TCP direkt an den Drucker, ohne Treiber und Warteschlange. Der Drucker antwortet, darum funktionieren Status und Vorschau.',
wayNetworkWeb: 'Sendet den Code an den Druckerport. Der Browser bekommt keine Antwort, der Drucker antwortet also nicht.',
wayBrowserPrint: 'Läuft über Zebra Browser Print auf dem Rechner, der den Browser ausführt.',
Expand Down
Loading
Loading