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

@ -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>