stash
This commit is contained in:
parent
4787acd8eb
commit
67c7415c3b
6 changed files with 691 additions and 407 deletions
196
frontend/src/label-layouts.js
Normal file
196
frontend/src/label-layouts.js
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Each template's `layout` is a tree as described in label.js, with "qrcode"/"text" leaves'
|
||||
// `content` a function from the resolved field values (see label.js's buildLabelFields) to what
|
||||
// they render - `null`/`undefined` from that function means the field isn't available yet (see
|
||||
// templateIsAvailable below). A template is only selectable once every leaf's `content` resolves
|
||||
// to a value.
|
||||
const GAP = {type: "empty", "min-width": "1mm", "min-height": "1mm"};
|
||||
|
||||
export const LABEL_TEMPLATES = [
|
||||
{
|
||||
id: "qr", name: "QR code only", description: "Just the code - smallest label, prints fastest.",
|
||||
required_vars: ["value"],
|
||||
layout: [{type: "qrcode", content: c => c.value}]
|
||||
},
|
||||
{
|
||||
id: "qr-text", name: "QR code + text", description: "The code with the encoded text printed next to it.",
|
||||
required_vars: ["value"],
|
||||
layout: [{type: "qrcode", content: c => c.value}, GAP, {type: "text", content: c => c.value?.split("\n")}]
|
||||
},
|
||||
{
|
||||
id: "qr-text-below", name: "QR code + text below",
|
||||
description: "The code with the encoded text printed below it.",
|
||||
required_vars: ["value"],
|
||||
layout: [[{type: "qrcode", content: c => c.value}, GAP, {type: "text", content: c => c.value?.split("\n")}]]
|
||||
},
|
||||
{
|
||||
id: "id-qr-text-vertical", name: "ID + QR code + text below",
|
||||
description: "The code with the encoded text printed below it.",
|
||||
required_vars: ["itemId", "value", "userHandle"],
|
||||
layout: [[{type: "text", content: c => "Item: "+c.itemId}, GAP, {
|
||||
type: "qrcode",
|
||||
content: c => c.value
|
||||
}, GAP, {type: "text", content: c => c.userHandle}]]
|
||||
},
|
||||
{
|
||||
id: "text", name: "Text only", description: "No code, just the text itself, as large as it fits.",
|
||||
required_vars: ["value"],
|
||||
layout: [{type: "text", content: c => c.value?.split("\n")}]
|
||||
},
|
||||
{
|
||||
id: "item-handle", name: "Item handle",
|
||||
description: "The compact owner@domain:id handle - meaningful in-app, not scannable on its own.",
|
||||
required_vars: ["itemHandle"],
|
||||
layout: [{type: "text", content: c => c.itemHandle}]
|
||||
},
|
||||
{
|
||||
id: "item-url", name: "Item URL",
|
||||
description: "The full item URL as text, with no code - for copying rather than scanning.",
|
||||
required_vars: ["itemUrl"],
|
||||
layout: [{type: "text", content: c => c.itemUrl}]
|
||||
},
|
||||
{
|
||||
id: "owner-handle", name: "Owner handle", description: "Just the owning user's handle, as text only.",
|
||||
required_vars: ["userHandle"],
|
||||
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"],
|
||||
layout: [{type: "text", content: c => c.itemId}]
|
||||
},
|
||||
{
|
||||
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"],
|
||||
layout: [{type: "text", content: c => [c.userHandle, c.itemId]}]
|
||||
},
|
||||
{
|
||||
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"],
|
||||
layout: [{type: "qrcode", content: c => c.itemUrl}, GAP, {type: "text", content: c => c.itemHandle}]
|
||||
},
|
||||
{
|
||||
id: "item-url-qr-owner", name: "Item URL + owner",
|
||||
description: "Scannable item URL, with the owner's handle printed alongside.",
|
||||
required_vars: ["itemUrl", "userHandle"],
|
||||
layout: [{type: "qrcode", content: c => c.itemUrl}, GAP, {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"],
|
||||
layout: [{type: "qrcode", content: c => c.itemUrl}, GAP, {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"],
|
||||
layout: [{type: "qrcode", content: c => c.itemUrl}, GAP, {type: "text", content: c => [c.userHandle, c.itemId]}]
|
||||
},
|
||||
{
|
||||
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"],
|
||||
layout: [{type: "qrcode", content: c => c.itemUrl}, GAP, [{
|
||||
type: "text",
|
||||
content: c => c.userHandle
|
||||
}, GAP, {type: "text", content: c => c.itemId}]]
|
||||
},
|
||||
];
|
||||
|
||||
// Every field name any template's required_vars names, in first-seen order.
|
||||
export const KNOWN_VARS = [...new Set(LABEL_TEMPLATES.flatMap(t => t.required_vars))];
|
||||
|
||||
// A derived var 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 below). `inputs` names every var (base or, in principle,
|
||||
// derived) `calc` reads - declared up front rather than inferred from calc's body so BASE_VARS
|
||||
// below can include a var like "webdomain" that only feeds a calculation and that no template
|
||||
// ever references directly.
|
||||
export const DERIVED_VARS = {
|
||||
// The self-contained Item URL (see docs/design-in-progress/items-labels.md) - what a printed
|
||||
// label actually encodes, since scanning it has to resolve the right frontend/backend/item
|
||||
// with no other context, not just this browser's history. `webdomain` defaults to this
|
||||
// browser's own origin (see Print.vue) but is editable, since any frontend can resolve any
|
||||
// handle - the label doesn't have to point back at whichever frontend happened to print it.
|
||||
itemUrl: {
|
||||
inputs: ["webdomain", "userHandle", "itemId"],
|
||||
calc: (f) => `${f.webdomain}/i/${f.userHandle}/${f.itemId}`,
|
||||
},
|
||||
// The compact "owner handle + id" form from docs/design-in-progress/items-labels.md -
|
||||
// meaningful only where context already makes clear it's a Toolshed item, unlike itemUrl.
|
||||
itemHandle: {
|
||||
inputs: ["userHandle", "itemId"],
|
||||
calc: (f) => `${f.userHandle}:${f.itemId}`,
|
||||
},
|
||||
};
|
||||
|
||||
// What Print.vue's content form offers a plain text input for: every KNOWN_VAR a template
|
||||
// references directly, minus the derived ones, plus every var a DERIVED_VARS calculation itself
|
||||
// needs (like "webdomain", which no template ever names). A template still lights up only once
|
||||
// every one of its own required_vars, base or derived, has a value (see templateIsAvailable
|
||||
// below).
|
||||
export const BASE_VARS = [...new Set([
|
||||
...KNOWN_VARS.filter(v => !(v in DERIVED_VARS)),
|
||||
...Object.values(DERIVED_VARS).flatMap(d => d.inputs).filter(v => !(v in DERIVED_VARS)),
|
||||
])];
|
||||
|
||||
// Runs every DERIVED_VARS calculation against `fields` (already holding the base vars - see
|
||||
// Print.vue), returning a copy with each one's result added wherever all of its own inputs are
|
||||
// present, so a caller never has to know DERIVED_VARS' internal {inputs, calc} shape.
|
||||
export function withDerivedVars(fields) {
|
||||
const result = {...fields};
|
||||
for (const [name, {inputs, calc}] of Object.entries(DERIVED_VARS)) {
|
||||
if (inputs.every(v => result[v])) {
|
||||
result[name] = calc(result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function walkLeaves(node, fn) {
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach(child => walkLeaves(child, fn));
|
||||
} else {
|
||||
fn(node);
|
||||
}
|
||||
}
|
||||
|
||||
function mapTree(node, fn) {
|
||||
return Array.isArray(node) ? node.map(child => mapTree(child, fn)) : fn(node);
|
||||
}
|
||||
|
||||
// A content leaf's resolved value counts as present only if every part of it is - a single
|
||||
// string for "qrcode"/plain "text", every line for a multi-line "text" (see LABEL_TEMPLATES'
|
||||
// "owner-id-text" and "item-url-qr-owner-id").
|
||||
function isResolved(value) {
|
||||
return Array.isArray(value) ? value.every(isResolved) : value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
// LabelLayoutPreview.vue's thumbnail grid and Print.vue's big preview both resolve a template
|
||||
// through these two functions rather than each re-implementing the leaf-walking/field-resolving
|
||||
// logic itself.
|
||||
export function templateIsAvailable(t, fields) {
|
||||
let available = true;
|
||||
walkLeaves(t.layout, leaf => {
|
||||
if (leaf.type !== "empty" && !isResolved(leaf.content(fields))) {
|
||||
available = false;
|
||||
}
|
||||
});
|
||||
return available;
|
||||
}
|
||||
|
||||
/* Resolves a template's `content` functions against actual field values, turning its layout
|
||||
tree into one ready for label.js's drawLabel/drawFallbackLabel - or null if there's nothing to
|
||||
render yet (every leaf's value is still empty, e.g. before the user has typed anything). */
|
||||
export function templateContent(t, fields) {
|
||||
const tree = mapTree(t.layout, leaf => leaf.type === "empty" ? leaf : {...leaf, value: leaf.content(fields)});
|
||||
let hasContent = false;
|
||||
walkLeaves(tree, leaf => {
|
||||
if (leaf.type !== "empty" && leaf.value && (!Array.isArray(leaf.value) || leaf.value.some(Boolean))) {
|
||||
hasContent = true;
|
||||
}
|
||||
});
|
||||
return hasContent ? tree : null;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue