This commit is contained in:
j3d1 2026-08-31 19:09:58 +02:00
parent 9c9f0f2d9b
commit 6a9ed35bcd
11 changed files with 166 additions and 186 deletions

View file

@ -221,7 +221,8 @@ pair keeps resolving correctly even after being copied out of the instance it wa
a [User-Qualified ID](#user-qualified-id) or [Item URL](#item-url), it isn't limited to owned kinds
(any kind in the short id registry can be domain-qualified) and isn't meant to be openable with zero
context, no scheme, no path, opaque bit-packed payload, it belongs inside the app or between things
that already speak its short-id format, not on a physical label.
that already speak its short-id format - including a physical label meant for the app's own scanner,
not a link or code meant to be opened by something with no idea what a Toolshed short id is.
*See: [handles-and-shortids.md](handles-and-shortids.md#domain-qualified-short-id)*
**Item Label** (Implemented)

View file

@ -181,7 +181,8 @@ It isn't a URL and isn't meant to be openable by something that doesn't already
Toolshed short id is: there's no scheme, no path, and the payload after `:~` is bit-packed base64,
opaque to a human. It belongs to the same "context already makes clear it's Toolshed data" class as
a compact User-Qualified ID, meant for use inside the app (or between things that already speak the
short-id format), not for a physical label or a link shared outside it. What it adds over a bare
short-id format, including a physical label meant for the app's own scanner) - not a link or code
meant to be opened by something with no idea what a Toolshed short id is. What it adds over a bare
`~token` is that it no longer depends on "whichever backend I currently happen to be talking to" —
the domain travels with it, so it keeps resolving to the same entity once copied elsewhere. Unlike
a User-Qualified ID, it isn't limited to owned kinds: every kind in the short id registry can be

View file

@ -102,11 +102,14 @@ A template's `layout` is a tree as described in label.js, with leaves whose `typ
### Generated QR-Only Template Matrix
`QR_ONLY_TEMPLATES` is the full "just the code" matrix: every `{symbology, error-correction level, [rMQR] size strategy}` combination label.js's `QR_LEAF_TYPES` supports, one template each. A plain `id` (no suffix) is always anyd's own defaults: ecc `"M"` and, for rMQR, size `"balanced"`. Coverage isn't uniform (see `QR_LEAF_TYPES`): full QR gets all four ecc grades L/M/Q/H; Micro QR swaps `"L"` (QR's actual lowest) for the even-lower, M1-only, detection-only `"Detection"`, and has no `"H"` at all; rMQR only ever supports ecc `"M"` or `"H"`, each crossed with all three size strategies (balanced/min/max). The matrix is generated (rather than hand-writing every near-duplicate entry) so that a symbology/level/size combination it's missing is one new row here, not a new block to keep in sync with its neighbors. `id` doubles as the layout's leaf `type`, since that's exactly what `QR_LEAF_TYPES` is keyed by.
### Derived Vars Shape And Ordering
A derived var (`DERIVED_VARS`) is a format string calculated from other vars rather than typed directly - it doesn't get its own input, just a read-only, live-recalculated display next to the ones that do (see Print.vue and `withDerivedVars`). Its `inputs` name every var (base or, in principle, another derived one - e.g. `itemUrl`/`itemHandle`, which both read the derived `userHandle`) that `calc` reads. `inputs` is declared up front rather than inferred from `calc`'s body so `BASE_VARS` can include a var like `"webdomain"` that only feeds a calculation and that no template ever references directly. Declaration order matters here: `withDerivedVars` runs these in a single pass, so a derived var must be declared after every other derived var it depends on.
### Content Kinds Registry
`CONTENT_KINDS`/`CONTENT_KINDS_BY_ID` (`label-layouts.js`) is the single registry for everything that varies per content kind (currently `item`/`storage-location`): the handles-and-shortids.md kind letter (`typedPrefix`), the short-id.js schema name to use for an individually- vs. group-owned thing of that kind (`shortIdKind`/`groupShortIdKind`), the field name short-id.js's `serializeShortId` expects for its local id (`localIdField`), and a builder for the kind's long-form URL (`buildUrl`, `undefined` for a future kind added with no such route - see the `url` `DERIVED_VARS` entry). Every place that used to special-case "item vs. storage location" - `label.js`'s prefill builders, `label-layouts.js`'s `DERIVED_VARS`, Print.vue's `shortId()` - reads this one table instead, keyed by the Content card's `kind` field (a `<select>`, see Print.vue) rather than inferring which kind is meant from which id-shaped field happens to be populated.
### Location Id Known Var
`BASE_VARS` is computed purely from `KNOWN_VARS` (every template's `required_vars`, see `templateIsAvailable`) plus whatever `DERIVED_VARS` reads - there's no separate registry of "fields a prefill might supply". `label.js`'s `"storage-location"` `LABEL_FIELD_BUILDERS` entry has always produced a `locationId`, and Print.vue's `shortId()` method has always checked `f.locationId` to resolve a `storage_location` short link, but until the `"location-id"`/`"owner-id-text-location"` templates existed, no template's `required_vars` ever named `locationId` - so it was never in `BASE_VARS`, Print.vue's `fields()` computed (which only copies `BASE_VARS` keys out of `varValues`) silently dropped it, and `shortId()` could never see it. That meant `shortId`/`shortUrl` never resolved for a storage location, so none of the short-link templates - including `"short-url-qr"`, whose own description says it's "for this item or storage location" - were ever actually available for one, despite the plumbing existing end to end. Any future prefill field meant to reach `fields()`/`DERIVED_VARS` needs the same anchor: at least one template naming it in `required_vars`.
### Derived Vars Shape And Ordering
A derived var (`DERIVED_VARS`) is a format string calculated from other vars rather than typed directly - it doesn't get its own input, just a read-only, live-recalculated display next to the ones that do (see Print.vue and `withDerivedVars`). Its `inputs` name every var (base or, in principle, another derived one - e.g. `qualifiedHandle`/`url`, which both read the derived `userHandle`, and `qualifiedHandle` also reads the derived `typedPrefix`) that `calc` reads. `inputs` is declared up front rather than inferred from `calc`'s body so `BASE_VARS` can include a var like `"webdomain"` that only feeds a calculation and that no template ever references directly. Declaration order matters here: `withDerivedVars` runs these in a single pass, so a derived var must be declared after every other derived var it depends on - `typedPrefix` before `qualifiedHandle`, in particular.
### Kind And Id Known Vars
`BASE_VARS` is computed purely from `KNOWN_VARS` (every template's `required_vars`, see `templateIsAvailable`) plus whatever `DERIVED_VARS` reads - there's no separate registry of "fields a prefill might supply". `kind` and `id` reach `BASE_VARS` (and so the Content card's form, and `fields()`) the same way any other var does: named directly in several templates' `required_vars`, and `kind` also as an input of the derived `typedPrefix`. A content var meant to be prefillable/typeable needs that same anchor - at least one template naming it in `required_vars`, or some `DERIVED_VARS` entry reading it - or `fields()` (which only copies `BASE_VARS` keys out of `varValues`) silently drops it.
## Print View
@ -114,7 +117,9 @@ A derived var (`DERIVED_VARS`) is a format string calculated from other vars rat
The tape-fed preview draws two representations of the tape: `.tape-full` (in the template, wrapping `.label-preview`/the canvas) spans the tape's entire physical width (`tape.mediaWidthMm`), while the canvas inside it is only `printAreaPx` wide. A label printer's print head can't mark all the way to a tape's outer edges, so the true printable area is narrower than the tape itself, by an amount that isn't a fixed or predictable fraction of the tape width. `.tape-full` centers the canvas within itself (equal margin on both sides) and applies a faint background tint to that margin, so it reads as real, if unprintable, tape rather than empty page space. Because of this gap, the vertical ruler (`verticalRulerTicks`) is deliberately built from `tape.mediaWidthMm`, not from `printAreaPx`/`dpi` — ticking off only the printable width would run past the edge of the visible tape. The ruler's container is sized from `mediaWidthMm` too (see the template's inline `height`), so ticks can never run past what's actually drawn.
### Calculated Short Link Fields
`SHORT_URL_VAR` ("shortUrl") and `SHORT_ID_VAR` ("shortId") are Print.vue-local additions on top of label-layouts.js's own `DERIVED_VARS`. Resolving either needs the current `identityIdByHandle` map (populated by store.js's `fetchIdMap`) to turn a handle into the numeric `owner_identity_id` that short-id.js encodes — that dependency on Vuex state means neither can be a pure fields-to-value calculation like the rest of `DERIVED_VARS`, so both are computed here instead (see the `shortId` method and the `fields` computed). They're excluded from `baseVars`, unlike every other entry `KNOWN_VARS` normally picks up from a template's `required_vars`, because neither is ever typed directly into the form. `SHORT_URL_VAR` is filtered out again in the `baseVars`/`derivedVars` computed even though the "Short link (QR code)" template's `required_vars` puts it in `BASE_VARS` — it isn't actually a label-layouts.js `DERIVED_VARS` entry, it's calculated here. `SHORT_ID_VAR` isn't referenced by any template's `required_vars` today, but is filtered the same way in case one ever is. `SHORT_ID_VAR` itself is the bare short-id.js token (e.g. "~AbCd12") with no domain or leading "/" — for a label that wants just the compact code rather than a full scannable URL, printed only by "internal"-tagged templates meant for this same app/instance. `SHORT_URL_VAR` is not simply `webdomain + "/" + shortId` the way it looks: see ShortId Resolution below for why it's domain-qualified instead.
`SHORT_ID_VAR` ("shortId"), `DOMAIN_SHORT_ID_VAR` ("domainShortId") and `SHORT_URL_VAR` ("shortUrl") are Print.vue-local additions on top of label-layouts.js's own `DERIVED_VARS`. Resolving any of them needs the current `identityIdByHandle` map (populated by store.js's `fetchIdMap`) to turn a handle into the numeric `owner_identity_id` that short-id.js encodes — that dependency on Vuex state means none can be a pure fields-to-value calculation like the rest of `DERIVED_VARS`, so all three are computed here instead (see the `shortId` method and the `fields` computed). They're excluded from `baseVars`, unlike every other entry `KNOWN_VARS` normally picks up from a template's `required_vars`, because none is ever typed directly into the form - `SHORT_URL_VAR`/`DOMAIN_SHORT_ID_VAR` are filtered out again in the `baseVars`/`derivedVars` computed even though some template's `required_vars` puts them in `BASE_VARS`, since neither is actually a label-layouts.js `DERIVED_VARS` entry.
These target three scopes, by how much context a reader needs before the value is useful at all: `SHORT_ID_VAR` is the bare short-id.js token (e.g. "~AbCd12") with no domain or leading "/" - **Home-Instance** scope, only meaningful to a reader already logged in as the identity it was minted for (printed by "internal"-tagged templates); `DOMAIN_SHORT_ID_VAR` prepends the printing user's own home domain (`homeDomain + ":" + shortId`, e.g. "a.example.com:~AbCd12") with still no URL wrapper - **Any-Instance** scope, resolvable by any Toolshed frontend's own scanner/resolver via a cross-domain lookup (short-id.js's `isDomainQualifiedShortId`/`decodeDomainQualifiedShortId`, see Domain-Qualified Short ID Resolution below) regardless of who's logged in there; `SHORT_URL_VAR` wraps that same bare `shortId` in `webdomain` as a clickable URL. `webdomain` only picks which frontend loads - it says nothing about which backend the data lives on, since that's decided entirely by whoever ends up logged in once it does - so `SHORT_URL_VAR` shares `SHORT_ID_VAR`'s Home-Instance trust requirement (same identity, just a tap instead of typed/scanned into an already-open session), not a step up to a universally-openable link. The actual zero-context scope is served elsewhere, by the long-form Item/Location URL (`url`, see Content Kinds Registry above), which resolves by handle rather than by the opener's own idmap.
### Libweblabel Served Unbundled
`libweblabel.js` is served verbatim from `public/vendor/` rather than bundled by Vite. Its own emscripten glue resolves its `.wasm` sibling relative to *its own* `import.meta.url` at runtime, so both files need to keep sitting together, unhashed, at a stable URL rather than a Vite-fingerprinted asset path.
@ -126,12 +131,12 @@ The tape-fed preview draws two representations of the tape: `.tape-full` (in the
`RULER_TIERS` controls how far apart plain ticks and labeled/major ticks sit, both getting coarser the longer the ruler runs — tightly spaced ticks (and their labels) get too cramped to read or render once there are enough of them. The list is ordered smallest threshold first; `rulerTier` picks the last entry whose `aboveMm` the ruler's own length clears, so a finer or coarser tier is added there rather than growing a pile of separate constants. Every tier's `majorEveryMm` is a multiple of its own `tickMm`, so major ticks always land on a tick that's actually drawn. `rulerTier` itself is a single shared value keyed off whichever axis (`horizontalTotalMm`/`verticalTotalMm`) is physically longer, so a long label's ruler never ends up coarser or finer than the tape-width ruler right next to it just because the other axis happens to be shorter — both rulers coarsen together once *either* axis needs it.
### Fields Computed: Dropping Blank Values
The `fields` computed builds the named content fields templates draw from: the form's own base vars, plus every `DERIVED_VARS` format string calculated live from those, so typing a `userHandle`/`itemId` (by hand or via prefill) recalculates `itemUrl`/`itemHandle` the same way either way. A blank or uncalculated value is dropped entirely rather than passed through as an empty string, so it reads as *absent* to `templateIsAvailable`/`templateContent` the same way a prefill that never supplied it would — that's what LabelLayoutPreview.vue greys a template's thumbnail out on.
The `fields` computed builds the named content fields templates draw from: the form's own base vars, plus every `DERIVED_VARS` format string calculated live from those, so typing a `userHandle`/`kind`/`id` (by hand or via prefill) recalculates `url`/`qualifiedHandle` the same way either way. A blank or uncalculated value is dropped entirely rather than passed through as an empty string, so it reads as *absent* to `templateIsAvailable`/`templateContent` the same way a prefill that never supplied it would — that's what LabelLayoutPreview.vue greys a template's thumbnail out on.
### ShortId Resolution
The `shortId` method builds the same shortened link Inventory.vue's and StorageLocation.vue's own `shortIdLink` build for one of their rows, but returns the bare token (see short-id.js's `encodeShortId`) rather than a router target or full URL — `shortenedRoute`'s leading "/" is stripped since this is plain display/label content, not something this view itself navigates to. Which short-id.js kind applies (`item` vs `storage_location`) depends on which id field the current prefill's `LABEL_FIELD_BUILDERS` populated (see label.js) — `itemId` vs `locationId` — rather than trusting the prefill's own `kind` directly, so hand-typing a `userHandle`+`itemId` with no prefill at all still resolves the same way. It falls back to no value, the same as an unresolved `DERIVED_VARS` entry, until `identityIdByHandle` has loaded (see `mounted`'s `fetchIdMap`) or if the handle isn't in it.
The `shortId` method builds the same shortened link Inventory.vue's and StorageLocation.vue's own `shortIdLink` build for one of their rows, but returns the bare token (see short-id.js's `encodeShortId`) rather than a router target or full URL — `shortenedRoute`'s leading "/" is stripped since this is plain display/label content, not something this view itself navigates to. Which short-id.js kind applies depends directly on the Content card's `kind` field (via `CONTENT_KINDS_BY_ID`, see Content Kinds Registry above), not on which id-shaped field happens to be populated — so hand-typing a `userHandle`+`kind`+`id` with no prefill at all resolves exactly the same way a prefilled print link does. It falls back to no value, the same as an unresolved `DERIVED_VARS` entry, until `identityIdByHandle` has loaded (see `mounted`'s `fetchIdMap`) or if the handle isn't in it.
The `fields` computed doesn't just reattach *a* domain to build `shortUrl` from this same token, it reattaches the `homeDomain` computed specifically — the printing user's own home domain (`state.user`'s domain half), not `derived.domain` (the printed thing's own owner handle domain, which for a group-owned print is the group's domain, not necessarily the printer's). This matters because `owner_identity_id`/`owner_group_id` are resolved via `identityIdByHandle`/`groupIdByHandle`, both sourced from `state.idmap`, which always loads from the printing user's own home server (`getHomeServers`) regardless of whose item is being printed — so the domain a Domain-Qualified Short ID needs to carry is always the printer's home domain, the one whose KnownIdentity/Group numbering the encoded ints are actually scoped to (see docs/handles-and-shortids.md#domain-qualified-short-id). Getting this wrong (e.g. using `derived.domain` for a group print) would silently mint a token that resolves to the wrong backend, or to nothing, once scanned by someone else.
`DOMAIN_SHORT_ID_VAR` reattaches the `homeDomain` computed specifically — the printing user's own home domain (`state.user`'s domain half), not `derived.domain` (the printed thing's own owner handle domain, which for a group-owned print is the group's domain, not necessarily the printer's). This matters because `owner_identity_id`/`owner_group_id` are resolved via `identityIdByHandle`/`groupIdByHandle`, both sourced from `state.idmap`, which always loads from the printing user's own home server (`getHomeServers`) regardless of whose item is being printed — so the domain a Domain-Qualified Short ID needs to carry is always the printer's home domain, the one whose KnownIdentity/Group numbering the encoded ints are actually scoped to (see docs/handles-and-shortids.md#domain-qualified-short-id). Getting this wrong (e.g. using `derived.domain` for a group print) would silently mint a token that resolves to the wrong backend, or to nothing, once scanned by someone else.
### Domain-Qualified Short ID Resolution
A Domain-Qualified Short ID (`<domain>:~<token>`) resolving to something other than "the app I'm currently in" needs a real network round-trip, since the token's ints are only meaningful to whichever backend minted them (see docs/handles-and-shortids.md#domain-qualified-short-id). `router.js`'s `domainQualifiedRoute(domain, ints)` is the single chokepoint every entry surface (the `/:short_id` route's `beforeEnter` guard, `ShortId.vue`'s `resolveIfQualified`, `Scan.vue`'s `describeForeignShortId`) funnels through: if `domain` matches the caller's own home domain (`store.state.user`'s domain, read via `isLoggedIn` first to force its lazy hydration — see `getHomeServers`'s own comment on the same ordering issue) it resolves locally via the existing idmap-based `expandedRoute`, exactly like a bare token; otherwise it dispatches `resolveShortId` (`store.js`), which SRV-discovers `domain`'s backend (`getFriendServers`) and calls its new `GET /api/v1/resolve_short_id/<kind>/<owner_id>/<local_id>/` (`backend/toolshed/api/inventory.py`). That endpoint answers "what does this owner/local id pair mean" against its own KnownIdentity/Group numbering, applying the exact same friend-or-self/membership and `availability_policy` checks `InventoryItemViewSet.get_queryset()` already does (see Owner-Handle Scoped Routes below) — it only differs in being keyed by numeric owner id instead of a handle string, since the caller doesn't have a handle yet, that's exactly what it returns (`{handle, id}`). Only the four owned short-id kinds (`item`/`group_item`/`storage_location`/`group_storage_location`) are wired up; `group`/`category`/`file`/`workflow` return 400 and the frontend falls through to `ShortId.vue`'s debug view.
@ -161,7 +166,7 @@ These coexist safely with their more specific neighbors (`/inventory/new/:group?
`ownerOverviewRoute`/`ownerLocationOverviewRoute` (`router.js`) only ever pick between the caller and their own group, never a friend, since a friend's items/locations are read-only (see Perform Update Rejects Non-Owned Items Explicitly below).
### Item Short-Link Redirect Route
The self-contained label/short-link entry point (see `label.js`'s `LABEL_CONTENT_BUILDERS` and docs/design-in-progress/items-labels.md). `:handle` is already URL-escaped the same way `/inventory/:handle/:id` expects it, so this is just a shorter alias for that route, with no owner-is-the-viewer special case: `InventoryItemViewSet.get_queryset()` (see Owner-Handle Scoped Routes below) already treats "it's the viewer's own item" as one case of "the viewer may see this owner's item," not a separate path.
`/i/:handle/:id` and `/s/:handle/:id` are the self-contained label/short-link entry points for an item and a storage location respectively (see `label.js`'s `buildLabelContent`/`CONTENT_KINDS_BY_ID`, whose `buildUrl` for each `CONTENT_KINDS` entry builds exactly this shape, and docs/design-in-progress/items-labels.md). `:handle` is already URL-escaped the same way `/inventory/:handle/:id`/`/storage-locations/:handle/:id` expect it, so each is just a shorter alias for its own detail route, with no owner-is-the-viewer special case: `InventoryItemViewSet.get_queryset()`/`StorageLocationViewSet.get_queryset()` (see Owner-Handle Scoped Routes below) already treat "it's the viewer's own item/location" as one case of "the viewer may see this owner's item/location," not a separate path.
### `beforeEnter` Guard vs `redirect` for `/:short_id`
This route uses a `beforeEnter` guard, not `redirect`: `redirect` is called synchronously and its return value is used as-is (never awaited), and it also *must* resolve to a valid location on every match (an unresolvable one throws, see vue-router's `handleRedirectRecord`) - it can't itself wait on `fetchIdMap` (see `NEEDS_IDMAP`) for the `item`/`group_item` kinds whose owner handle isn't resolvable from the token alone. A guard can return `null`/undefined to mean "proceed to the component instead," which is exactly what's needed here: when `expandedRoute` can't resolve yet (or ever - an unrecognized kind), stay on this same URL and mount `ShortId.vue` in place, which has full component-lifecycle async support and takes it from there - fetch idmap, retry, redirect once resolved, or keep showing the decode view. Vue Router guards support returning a Promise, not just a value, which is what makes this the right chokepoint for a Domain-Qualified Short ID too (see Domain-Qualified Short ID Resolution above): the guard is `async`, and for a foreign domain it `await`s `domainQualifiedRoute` (a real network round-trip) before deciding whether to redirect or fall through to `ShortId.vue`, the exact same "resolve or fall through" shape as the synchronous idmap case.
@ -264,8 +269,5 @@ An inventory item's properties are encoded into the `properties` CSV cell as a c
### Video Stream Attach And Resize Sync
`setupVideoStream` in `Scan.vue` attaches a `MediaStream` to the `<video>` element and is shared by `startCamera` and the camera-switch/reconnect paths, so the `CameraScanner` (which just keeps reading frames off the same `<video>` element) never needs to be recreated when the camera changes. Right after `video.play()`, `video.videoWidth`/`videoHeight` are not populated yet - the browser only reports them once the video's own `resize` (and, on first load, `loadedmetadata`) event fires, confirming the intrinsic dimensions actually took effect. `onVideoResize` listens for both events and is the one place that resizes the overlay `<canvas>` to match the video's current rendered size, replacing whatever stale size it had from before a switch to a camera with a different native resolution/aspect ratio.
### Print Link Shape For Personal Items
`printLinkFor` in `Inventory.vue` routes to `Print.vue` with the item's raw identity (`userHandle` + `item`) rather than any pre-built link - the same shape `InventoryDetail.vue`'s own Print label button sends. That lets the print page derive every representation it needs (item handle, owner handle, item URL, short link, …) itself, instead of being tied to whichever one the calling button happened to construct. Group-owned items have no individual owner handle - `short-id.js`'s `group_item` kind resolves them via `owner_group` instead (see `shortIdLink`) - so there's no `{userHandle, item}` to build yet; `printLinkFor` returns `null` for them until group print support exists.
### Print Link Shape For Storage Locations
`printLinkFor` in `StorageLocation.vue` routes to `Print.vue` with the location's raw identity (`userHandle` + `location`), the same shape `Inventory.vue`'s `printLinkFor` sends for an item (see `label.js`'s `"storage-location"` entry in `LABEL_FIELD_BUILDERS`). Storage locations are always individually owned (see `StorageLocationViewSet.get_queryset`), so unlike the `Inventory.vue` version, this one never has to fall back to returning `null` for "no metadata available".
### Print Link Shape
`printLinkFor` in `Inventory.vue` and `StorageLocation.vue` (and the print buttons in `InventoryDetail.vue`/`StorageLocationDetail.vue`) all route to `Print.vue` with the same `{kind, userHandle, id}` query shape - the thing's raw identity - rather than any pre-built link. `kind` is one of `CONTENT_KINDS`' ids (`"item"`/`"storage-location"`, see Content Kinds Registry above); `label.js`'s `buildLabelFields` reads it generically since every kind's prefill now shares this one shape. That lets the print page derive every representation it needs (qualified handle, owner handle, URL, short link, …) itself, instead of being tied to whichever one the calling button happened to construct. Group-owned items have no individual owner handle - `short-id.js`'s `group_item` kind resolves them via `owner_group` instead (see `shortIdLink`) - so there's no `{userHandle, id}` to build yet; `Inventory.vue`'s `printLinkFor` returns `null` for them until group print support exists. Storage locations are always individually owned (see `StorageLocationViewSet.get_queryset`), so `StorageLocation.vue`'s version never has that fallback to make.

View file

@ -4,6 +4,23 @@ import {encodeHandleForUrl} from "@/router";
// docs/implementation.md#template-layout-tree.
const GAP = {type: "empty", "min-width": "1mm", "min-height": "1mm"};
// Single registry for everything that varies per content kind. See docs/implementation.md#content-kinds-registry.
export const CONTENT_KINDS = [
{
id: "item", label: "Item", typedPrefix: "i",
shortIdKind: "item", groupShortIdKind: "group_item", localIdField: "item_local_id",
buildUrl: ({origin, userHandle, id}) => `${origin}/i/${encodeHandleForUrl(userHandle)}/${id}`,
},
{
id: "storage-location", label: "Storage location", typedPrefix: "s",
shortIdKind: "storage_location", groupShortIdKind: "group_storage_location",
localIdField: "storage_location_id",
buildUrl: ({origin, userHandle, id}) => `${origin}/s/${encodeHandleForUrl(userHandle)}/${id}`,
},
];
export const CONTENT_KINDS_BY_ID = Object.fromEntries(CONTENT_KINDS.map(k => [k.id, k]));
// QR-types
// qr-l, qr, qr-q, qr-h, mqr-d, mqr-l, mqr, mqr-q, rmqr, rmqr-min, rmqr-max, rmqr-h, rmqr-h-min, rmqr-h-max
@ -22,53 +39,45 @@ export const LABEL_TEMPLATES = [
layout: [{type: "qr-h", content: c => c.shortUrl}]
},
{
id: "rmqr-url", name: "rMQR Token + Item ID",
description: "The code encoding the short link, with the item id printed alongside for a human to read.",
required_vars: ["shortUrl", "itemId"], tags: ["external"],
id: "rmqr-url", name: "rMQR Token + ID",
description: "The code encoding the short link, with the id printed alongside for a human to read.",
required_vars: ["shortUrl", "id"], tags: ["external"],
layout: [{type: "rmqr", content: c => c.shortUrl}, {
type: "text",
content: c => c.itemId?.toString().padStart(4, "0")
content: c => c.id?.toString().padStart(4, "0")
}]
},
{
id: "rmqr-url-url", name: "rMQR Token + Item ID + URL",
description: "The code encoding the short link, with the item id printed alongside for a human to read.",
required_vars: ["shortUrl", "itemId"], tags: ["external"],
id: "rmqr-url-url", name: "rMQR Token + ID + URL",
description: "The code encoding the short link, with the id printed alongside for a human to read.",
required_vars: ["shortUrl", "id", "typedPrefix", "url"], tags: ["external"],
layout: [[[{type: "rmqr", content: c => c.shortUrl}, {
type: "text",
content: c => "I"+c.itemId?.toString().padStart(4, "0")
}],{type: "text", content: c => c.itemUrl}]]
content: c => c.typedPrefix?.toUpperCase() + c.id?.toString().padStart(4, "0")
}],{type: "text", content: c => c.url}]]
},
{
id: "mqr-token-id", name: "MQR Token + Item ID",
description: "The code encoding the internal short id, with the item id printed alongside for a human to read.",
required_vars: ["shortId", "itemId"], tags: ["internal"],
id: "mqr-token-id", name: "MQR Token + ID",
description: "The code encoding the internal short id, with the id printed alongside for a human to read.",
required_vars: ["shortId", "id"], tags: ["internal"],
layout: [{type: "mqr", content: c => c.shortId}, {
type: "text",
content: c => c.itemId?.toString().padStart(4, "0")
content: c => c.id?.toString().padStart(4, "0")
}]
},
{
id: "location-rmqr-url", name: "rMQR Token + Location ID",
description: "The code encoding the short link, with the storage location id printed alongside for a "
+ "human to read.",
required_vars: ["shortUrl", "locationId"], tags: ["external"],
layout: [{type: "rmqr", content: c => c.shortUrl}, {
type: "text",
content: c => c.locationId?.toString().padStart(4, "0")
}]
id: "domain-short-id-qr", name: "Any-Instance Token (QR)",
description: "The code encoding the domain-qualified short id (domain:~token) - resolvable "
+ "from any Toolshed instance's own scanner, not just this one, with no full URL needed.",
required_vars: ["domainShortId"], tags: ["cross-instance"],
layout: [{type: "qr", content: c => c.domainShortId}]
},
{
id: "location-mqr-token-id",
name: "MQR Token + Location ID",
description: "The code encoding the internal short id, with the storage location id printed alongside "
+ "for a human to read.",
required_vars: ["shortId", "locationId"],
tags: ["internal"],
layout: [{type: "mqr", content: c => c.shortId}, {
type: "text",
content: c => c.locationId?.toString().padStart(4, "0")
}]
id: "domain-short-id-text", name: "Any-Instance Token",
description: "Just the domain-qualified short id (domain:~token) as text, not a URL - meant "
+ "to be typed or scanned into any Toolshed instance's own resolver.",
required_vars: ["domainShortId"], tags: ["cross-instance"],
layout: [{type: "text", content: c => c.domainShortId}]
},
{
@ -85,23 +94,18 @@ export const LABEL_TEMPLATES = [
{
id: "id-qr-text-vertical", name: "ID + QR code + text below",
description: "The code with the encoded text printed below it.",
required_vars: ["itemId", "text", "userHandle"],
layout: [[{type: "text", content: c => "Item: " + c.itemId}, {
required_vars: ["kind", "id", "text", "userHandle"],
layout: [[{type: "text", content: c => c.kind + ": " + c.id}, {
type: "qr",
content: c => c.text
}, {type: "text", content: c => c.userHandle}]]
},
{
id: "item-handle", name: "Item handle",
description: "The compact owner@domain:i<id> User-Qualified ID - meaningful in-app, not scannable on its own.",
required_vars: ["itemHandle"], tags: ["internal"],
layout: [{type: "text", content: c => c.itemHandle}]
},
{
id: "location-handle", name: "Storage location handle",
description: "The compact owner@domain:s<id> User-Qualified ID - meaningful in-app, not scannable on its own.",
required_vars: ["locationHandle"], tags: ["internal"],
layout: [{type: "text", content: c => c.locationHandle}]
id: "qualified-handle", name: "Qualified handle",
description: "The compact owner@domain:i<id> / owner@domain:s<id> User-Qualified ID - meaningful "
+ "in-app, not scannable on its own.",
required_vars: ["qualifiedHandle"], tags: ["internal"],
layout: [{type: "text", content: c => c.qualifiedHandle}]
},
{
id: "owner-handle", name: "Owner handle", description: "Just the owning user's handle, as text only.",
@ -109,56 +113,45 @@ export const LABEL_TEMPLATES = [
layout: [{type: "text", content: c => c.userHandle}]
},
{
id: "item-id", name: "Item ID", description: "Just the bare item id, as text only.",
required_vars: ["itemId"], tags: ["internal"],
layout: [{type: "text", content: c => c.itemId}]
id: "id", name: "ID", description: "Just the bare id, as text only.",
required_vars: ["id"], tags: ["internal"],
layout: [{type: "text", content: c => c.id}]
},
{
id: "owner-id-text-multiline", name: "Owner + item ID (same size)",
description: "The owner's handle and the item id, as two lines of text - no code.",
required_vars: ["userHandle", "itemId"], tags: ["internal"],
layout: [{type: "text", content: c => [c.userHandle, c.itemId]}]
id: "owner-id-text-multiline", name: "Owner + ID (same size)",
description: "The owner's handle and the id, as two lines of text - no code.",
required_vars: ["userHandle", "id"], tags: ["internal"],
layout: [{type: "text", content: c => [c.userHandle, c.id]}]
},
{
id: "owner-id-text", name: "Owner + item ID",
description: "The owner's handle and the item id, as two lines of text - no code.",
required_vars: ["userHandle", "itemId"], tags: ["internal"],
layout: [[{type: "text", content: c => [c.userHandle]},{type: "text", content: c => [c.itemId]}]]
id: "owner-id-text", name: "Owner + ID",
description: "The owner's handle and the id, as two lines of text - no code.",
required_vars: ["userHandle", "id"], tags: ["internal"],
layout: [[{type: "text", content: c => [c.userHandle]},{type: "text", content: c => [c.id]}]]
},
{
id: "location-id", name: "Location ID", description: "Just the bare storage location id, as text only.",
required_vars: ["locationId"], tags: ["internal"],
layout: [{type: "text", content: c => c.locationId}]
id: "url-qr-handle", name: "URL + handle",
description: "Scannable URL, with the compact handle printed alongside.",
required_vars: ["url", "qualifiedHandle"], tags: ["external"],
layout: [{type: "qr", content: c => c.url}, {type: "text", content: c => c.qualifiedHandle}]
},
{
id: "owner-id-text-location", name: "Owner + location ID",
description: "The owner's handle and the location id, as two lines of text - no code.",
required_vars: ["userHandle", "locationId"], tags: ["internal"],
layout: [{type: "text", content: c => [c.userHandle, c.locationId]}]
id: "url-qr-owner", name: "URL + owner",
description: "Scannable URL, with the owner's handle printed alongside.",
required_vars: ["url", "userHandle"], tags: ["external"],
layout: [{type: "qr", content: c => c.url}, {type: "text", content: c => c.userHandle}]
},
{
id: "item-url-qr-handle", name: "Item URL + handle",
description: "Scannable item URL, with the item's compact handle printed alongside.",
required_vars: ["itemUrl", "itemHandle"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, {type: "text", content: c => c.itemHandle}]
id: "url-qr-id", name: "URL + ID",
description: "Scannable URL, with the bare id printed alongside.",
required_vars: ["url", "id"], tags: ["external"],
layout: [{type: "qr", content: c => c.url}, {type: "text", content: c => c.id}]
},
{
id: "item-url-qr-owner", name: "Item URL + owner",
description: "Scannable item URL, with the owner's handle printed alongside.",
required_vars: ["itemUrl", "userHandle"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, {type: "text", content: c => c.userHandle}]
},
{
id: "item-url-qr-id", name: "Item URL + item ID",
description: "Scannable item URL, with the bare item id printed alongside.",
required_vars: ["itemUrl", "itemId"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, {type: "text", content: c => c.itemId}]
},
{
id: "item-url-qr-owner-id", name: "Item URL + owner + ID",
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, {type: "text", content: c => [c.userHandle, c.itemId]}]
id: "url-qr-owner-id", name: "URL + owner + ID",
description: "Scannable URL, with the owner's handle and the id on two lines alongside.",
required_vars: ["url", "userHandle", "id"], tags: ["external"],
layout: [{type: "qr", content: c => c.url}, {type: "text", content: c => [c.userHandle, c.id]}]
},
{
id: "short-url-qr", name: "Short link (QR code)",
@ -168,13 +161,13 @@ export const LABEL_TEMPLATES = [
layout: [{type: "qr", content: c => c.shortUrl}]
},
{
id: "item-url-qr-owner-id2", name: "Item URL + owner + ID",
description: "Scannable item URL, with the owner's handle and the item id on two lines alongside.",
required_vars: ["itemUrl", "userHandle", "itemId"], tags: ["external"],
layout: [{type: "qr", content: c => c.itemUrl}, [{
id: "url-qr-owner-id2", name: "URL + owner + ID",
description: "Scannable URL, with the owner's handle and the id on two lines alongside.",
required_vars: ["url", "userHandle", "id"], tags: ["external"],
layout: [{type: "qr", content: c => c.url}, [{
type: "text",
content: c => c.userHandle
}, {type: "text", content: c => c.itemId}]]
}, {type: "text", content: c => c.id}]]
},
];
@ -187,17 +180,19 @@ export const DERIVED_VARS = {
inputs: ["user", "domain"],
calc: (f) => `${f.user}@${f.domain}`,
},
itemUrl: {
inputs: ["webdomain", "userHandle", "itemId"],
calc: (f) => `${f.webdomain}/i/${encodeHandleForUrl(f.userHandle)}/${f.itemId}`,
// handles-and-shortids.md's kind letter (i/s) for f.kind - must stay declared before qualifiedHandle, which depends on it.
typedPrefix: {
inputs: ["kind"],
calc: (f) => CONTENT_KINDS_BY_ID[f.kind]?.typedPrefix,
},
itemHandle: {
inputs: ["userHandle", "itemId"],
calc: (f) => `${f.userHandle}:i${f.itemId}`,
qualifiedHandle: {
inputs: ["userHandle", "typedPrefix", "id"],
calc: (f) => `${f.userHandle}:${f.typedPrefix}${f.id}`,
},
locationHandle: {
inputs: ["userHandle", "locationId"],
calc: (f) => `${f.userHandle}:s${f.locationId}`,
// Undefined for a kind whose CONTENT_KINDS entry has no buildUrl - same "unresolved" signal templateIsAvailable already treats every other missing var as.
url: {
inputs: ["webdomain", "userHandle", "kind", "id"],
calc: (f) => CONTENT_KINDS_BY_ID[f.kind]?.buildUrl?.({origin: f.webdomain, userHandle: f.userHandle, id: f.id}),
},
};

View file

@ -1,7 +1,6 @@
import {loadAnyDCode} from "../vendor/anyd-qr.js";
import {encodeHandleForUrl} from "@/router"
import {drawPixelText, preloadPixelFontRenderer} from "@/pixel-font.js";
import {DERIVED_VARS} from "@/label-layouts.js";
import {DERIVED_VARS, CONTENT_KINDS_BY_ID} from "@/label-layouts.js";
// Re-exported so every caller can preload both the QR encoder and the bitmap-font rasterizer
// (see fontTierFor's `pixel` tiers below) the same way, alongside this file's own preloadQrEncoder.
@ -568,28 +567,17 @@ export function drawFallbackLabel(canvas, content, orientation = "along") {
return {textSizesPx, warnings, qrInfo, textInfo};
}
// Turns a {kind, components} prefill (see Print.vue's `prefill` prop) into the literal string a
// print label should show/encode; keyed by `kind` so each kind's format is defined in one place.
export const LABEL_CONTENT_BUILDERS = {
// The self-contained Item URL (see docs/design-in-progress/items-labels.md), built from just
// the prefill's {userHandle, item}; the short link (Print.vue's `shortUrl`) needs an async store
// lookup, so it stays a separate field/template rather than being baked in here.
"item": ({userHandle, item}) => `${window.location.origin}/i/${encodeHandleForUrl(userHandle)}/${item}`,
// Storage locations have no long-form URL route yet (see router.js), so `text` starts blank;
// the short link and any future location template still work via the base vars below.
};
// Turns a {kind, components} prefill (see Print.vue's `prefill` prop) into the literal string a print label should show/encode, via that kind's CONTENT_KINDS_BY_ID entry.
export function buildLabelContent(prefill) {
if (!prefill) {
const entry = prefill && CONTENT_KINDS_BY_ID[prefill.kind];
const {userHandle, id} = prefill?.components ?? {};
if (!entry?.buildUrl || !userHandle || !id) {
return "";
}
const build = LABEL_CONTENT_BUILDERS[prefill.kind];
return build ? build(prefill.components) : "";
return entry.buildUrl({origin: window.location.origin, userHandle, id});
}
// Splits a prefill's {userHandle, item/location} into label-layouts.js's user/domain base vars (the same way
// store.js's lookupServer does), tagging on whichever id field the resource's templates key
// required_vars by.
// Splits a prefill's {userHandle, id} into label-layouts.js's user/domain base vars (the same way store.js's lookupServer does).
function splitUserHandle(userHandle) {
if (!userHandle) {
return null;
@ -601,32 +589,17 @@ function splitUserHandle(userHandle) {
};
}
// Seeds label-layouts.js's BASE_VARS, keyed by `kind`; derived vars (userHandle, itemUrl, …) are
// computed live elsewhere (see DERIVED_VARS, Print.vue's `shortUrl`), and omitting a field
// (rather than leaving it present-but-empty) signals "not available" to contentIsAvailable.
const LABEL_FIELD_BUILDERS = {
"item": ({userHandle, item}) => {
const split = splitUserHandle(userHandle);
if (!split || !item) {
return {};
}
return {...split, itemId: String(item)};
},
"storage-location": ({userHandle, location}) => {
const split = splitUserHandle(userHandle);
if (!split || !location) {
return {};
}
return {...split, locationId: String(location)};
},
};
// Seeds label-layouts.js's BASE_VARS from a {kind, components: {userHandle, id}} prefill - one generic function suffices now that every kind's prefill shares this shape (see CONTENT_KINDS_BY_ID).
export function buildLabelFields(prefill) {
if (!prefill) {
if (!prefill || !(prefill.kind in CONTENT_KINDS_BY_ID)) {
return {};
}
const build = LABEL_FIELD_BUILDERS[prefill.kind];
return build ? build(prefill.components) : {};
const {userHandle, id} = prefill.components ?? {};
const split = splitUserHandle(userHandle);
if (!split || !id) {
return {};
}
return {...split, kind: prefill.kind, id: String(id)};
}
// Runs each DERIVED_VARS calc against `fields`, adding the result wherever its inputs are

View file

@ -251,6 +251,10 @@ const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, {
component: StorageLocationEdit,
meta: {requiresAuth: true},
props: true
}, {
// Label/short-link entry point for storage locations, the counterpart of /i/:handle/:id. See docs/implementation.md#item-short-link-redirect-route.
path: '/s/:handle/:id',
redirect: to => `/storage-locations/${to.params.handle}/${to.params.id}`
}, {
// See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
path: '/storage-locations/new/:group?',

View file

@ -220,7 +220,7 @@ export default {
printLinkFor(item) {
const userHandle = item.owner || item.owner_group
if (!userHandle) return null
return {path: '/print', query: {kind: 'item', userHandle, item: item.id}}
return {path: '/print', query: {kind: 'item', userHandle, id: item.id}}
},
},
watch: {

View file

@ -51,7 +51,7 @@
Delete
</button>
<button v-if="canPrint" class="btn btn-secondary"
@click="$router.push({path: '/print', query: {kind: 'item', userHandle: decodedHandle, item: id}})">
@click="$router.push({path: '/print', query: {kind: 'item', userHandle: decodedHandle, id: id}})">
<b-icon-printer></b-icon-printer>
Print label
</button>

View file

@ -153,6 +153,13 @@
<textarea v-if="v === 'text'" class="form-control" rows="3"
v-model="varValues[v]"
placeholder="https://example.com/…"></textarea>
<select v-else-if="v === 'kind'" class="form-control"
v-model="varValues[v]">
<option value=""></option>
<option v-for="k in contentKinds" :key="k.id" :value="k.id">{{
k.label
}}</option>
</select>
<input v-else type="text" class="form-control" v-model="varValues[v]">
</div>
</div>
@ -201,14 +208,15 @@ import {
withDerivedVars,
templateContent
} from "@/label.js";
import {LABEL_TEMPLATES, BASE_VARS, DERIVED_VARS} from "@/label-layouts.js";
import {LABEL_TEMPLATES, BASE_VARS, DERIVED_VARS, CONTENT_KINDS, CONTENT_KINDS_BY_ID} from "@/label-layouts.js";
import {shortenedRoute} from "@/router";
// Print.vue-local calculated fields on top of label-layouts.js's DERIVED_VARS. See
// docs/implementation.md#calculated-short-link-fields.
// Print.vue-local calculated fields on top of label-layouts.js's DERIVED_VARS. See docs/implementation.md#calculated-short-link-fields.
const SHORT_URL_VAR = "shortUrl";
// See docs/implementation.md#calculated-short-link-fields.
const SHORT_ID_VAR = "shortId";
// See docs/implementation.md#calculated-short-link-fields.
const DOMAIN_SHORT_ID_VAR = "domainShortId";
// Served unbundled so its wasm sibling stays resolvable. See docs/implementation.md#libweblabel-served-unbundled.
const BLOB_URL = "/vendor/libweblabel.js";
@ -275,7 +283,7 @@ export default {
// Scan-reliability messages from label.js's drawLabel (too few px per QR module, or too small in mm) - the label still rendered/prints fine, just flagged as a risk.
warnings: [],
// One input per BASE_VARS entry; derived vars (userHandle/itemUrl/itemHandle) are calculated-only (see the `fields` computed), never stored here. Prefilled from query params but left editable. queryFields is applied last so an explicit ?text=... etc. always wins over a `kind` builder's computed value.
// One input per BASE_VARS entry; derived vars (userHandle/url/qualifiedHandle) are calculated-only (see the `fields` computed), never stored here. Prefilled from query params but left editable. queryFields is applied last so an explicit ?text=... etc. always wins over buildLabelFields' computed value.
varValues: {
...Object.fromEntries(BASE_VARS.map(v => [v, ""])),
text: buildLabelContent(this.prefill),
@ -308,10 +316,13 @@ export default {
return at === -1 ? null : this.user.slice(at + 1);
},
baseVars() {
return BASE_VARS.filter(v => v !== SHORT_URL_VAR && v !== SHORT_ID_VAR);
return BASE_VARS.filter(v => v !== SHORT_URL_VAR && v !== SHORT_ID_VAR && v !== DOMAIN_SHORT_ID_VAR);
},
contentKinds() {
return CONTENT_KINDS;
},
derivedVars() {
return [...Object.keys(DERIVED_VARS), SHORT_ID_VAR, SHORT_URL_VAR];
return [...Object.keys(DERIVED_VARS), SHORT_ID_VAR, DOMAIN_SHORT_ID_VAR, SHORT_URL_VAR];
},
// Named content fields the templates draw from, dropping blank values. See docs/implementation.md#fields-computed-dropping-blank-values.
fields() {
@ -325,9 +336,12 @@ export default {
const shortId = this.shortId(derived);
if (shortId) {
derived[SHORT_ID_VAR] = shortId;
// Bare, same Home-Instance trust model as shortId - webdomain only picks a frontend, not a backend. See docs/implementation.md#calculated-short-link-fields.
if (derived.webdomain) {
derived[SHORT_URL_VAR] = derived.webdomain + "/"
+ (this.homeDomain ? this.homeDomain + ":" + shortId : shortId);
derived[SHORT_URL_VAR] = derived.webdomain + "/" + shortId;
}
if (this.homeDomain) {
derived[DOMAIN_SHORT_ID_VAR] = this.homeDomain + ":" + shortId;
}
}
return derived;
@ -447,12 +461,14 @@ export default {
methods: {
...mapActions(["fetchIdMap"]),
// camelCase -> Title Case (e.g. "itemHandle" -> "Item Handle") so a new template var needs no hand-written label.
// camelCase -> Title Case (e.g. "qualifiedHandle" -> "Qualified Handle") so a new template var needs no hand-written label.
varLabel(v) {
return v.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, c => c.toUpperCase());
},
// Which short-id.js kind applies depends on f.kind directly. See docs/implementation.md#content-kinds-registry.
shortId(f) {
if (!f.userHandle) {
const entry = f.kind && CONTENT_KINDS_BY_ID[f.kind];
if (!f.userHandle || !entry || !f.id) {
return null;
}
if (f.userHandle.startsWith("#")) {
@ -460,29 +476,17 @@ export default {
if (owner_group_id === undefined) {
return null;
}
if (f.itemId) {
return shortenedRoute({kind: "group_item", owner_group_id, item_local_id: f.itemId}).slice(1);
}
if (f.locationId) {
return shortenedRoute({
kind: "group_storage_location", owner_group_id, storage_location_id: f.locationId
}).slice(1);
}
return null;
return shortenedRoute({
kind: entry.groupShortIdKind, owner_group_id, [entry.localIdField]: f.id
}).slice(1);
}
const owner_identity_id = this.identityIdByHandle[f.userHandle];
if (owner_identity_id === undefined) {
return null;
}
if (f.itemId) {
return shortenedRoute({kind: "item", owner_identity_id, item_local_id: f.itemId}).slice(1);
}
if (f.locationId) {
return shortenedRoute({
kind: "storage_location", owner_identity_id, storage_location_id: f.locationId
}).slice(1);
}
return null;
return shortenedRoute({
kind: entry.shortIdKind, owner_identity_id, [entry.localIdField]: f.id
}).slice(1);
},
// Ticks 0..totalMm via tapePxPerMm, shared by both ruler computeds; spacing comes from the shared rulerTier, so both rulers coarsen together.

View file

@ -200,10 +200,10 @@ export default {
return shortenedRoute({kind: 'storage_location', owner_identity_id, storage_location_id: location.id})
},
// Routes to Print.vue with this location's raw identity, same shape as Inventory.vue's
// printLinkFor. See docs/implementation.md#print-link-shape-for-storage-locations.
// printLinkFor. See docs/implementation.md#print-link-shape.
printLinkFor(location) {
const userHandle = location.owner_group || location.owner
return {path: '/print', query: {kind: 'storage-location', userHandle, location: location.id}}
return {path: '/print', query: {kind: 'storage-location', userHandle, id: location.id}}
},
},
watch: {

View file

@ -38,7 +38,7 @@
Delete
</button>
<button class="btn btn-secondary"
@click="$router.push({path: '/print', query: {kind: 'storage-location', userHandle: decodedHandle, location: id}})">
@click="$router.push({path: '/print', query: {kind: 'storage-location', userHandle: decodedHandle, id: id}})">
<b-icon-printer></b-icon-printer>
Print label
</button>