From 6ee5ae38b10ace42830846c5f679e103e5d56837 Mon Sep 17 00:00:00 2001 From: jedi Date: Thu, 27 Aug 2026 01:39:03 +0200 Subject: [PATCH] stash --- backend/toolshed/api/files.py | 24 ++-- docs/glossary.md | 60 +++++--- docs/handles-and-shortids.md | 154 ++++++++++++++------- frontend/src/assets/fonts/pixel/LICENSE.md | 9 -- frontend/src/label-layouts.js | 19 ++- frontend/src/views/Print.vue | 18 +-- frontend/src/views/Scan.vue | 77 ++++++----- frontend/src/views/ShortId.vue | 38 +++-- 8 files changed, 246 insertions(+), 153 deletions(-) delete mode 100644 frontend/src/assets/fonts/pixel/LICENSE.md diff --git a/backend/toolshed/api/files.py b/backend/toolshed/api/files.py index 03b51d9..b8b57a8 100644 --- a/backend/toolshed/api/files.py +++ b/backend/toolshed/api/files.py @@ -11,29 +11,29 @@ from toolshed.models import InventoryItem, WorkflowInstance def _get_authorized_item(identity, item_id): - """Look up an item by its owner-scoped id and confirm identity may act on it - either as its - personal owner (requires a local ToolshedUser account) or as a current member of its owning - group (works for a remote member too, since group membership is identity-level, see - docs/design-in-progress/groups-mvp.md). id is only unique within one owner/group's own items, - so the lookup itself must be scoped rather than a bare global get. Returns None if not found - or not authorized, the same shape InventoryItem.DoesNotExist handling around it already - expects.""" + """Owner-or-group-scoped item lookup; returns None if identity may not act on it.""" if identity.user.exists(): try: return InventoryItem.objects.get(owner=identity.user.get(), id=item_id) except InventoryItem.DoesNotExist: pass - try: - return InventoryItem.objects.get(owner_group__in=identity.member_of_groups.all(), id=item_id) - except InventoryItem.DoesNotExist: - return None + # Checked one group at a time, not owner_group__in=, since id is only unique within one group's own items and a combined query could raise MultipleObjectsReturned on a collision. + for group in identity.member_of_groups.all(): + item = InventoryItem.objects.filter(owner_group=group, id=item_id).first() + if item: + return item + return None @api_view(['GET']) @permission_classes([IsAuthenticated]) @authentication_classes([SignatureAuthenticationLocal]) def list_all_files(request, format=None): # /files/ - files = File.objects.select_related().filter(connected_items__owner=request.user).distinct() + # request.user is a ToolshedUser here; reach group membership via public_identity. + files = File.objects.select_related().filter( + Q(connected_items__owner=request.user) | + Q(connected_items__owner_group__in=request.user.public_identity.member_of_groups.all()) + ).distinct() return Response(FileSerializer(files, many=True).data) diff --git a/docs/glossary.md b/docs/glossary.md index 2a3cc62..a186a98 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -47,7 +47,8 @@ and talks to it directly, regardless of where the frontend itself was loaded fro A name that's unique within its own scope and carries, as part of itself, enough information to say where it's authoritative, without needing a central registry to look it up. The general term covering [user handles](#user-handle), [group handles](#group-handle) (proposed), -[classification handles](#classification-handle), and [item handles](#item-handle) (proposed). +[classification handles](#classification-handle), [User-Qualified IDs](#user-qualified-id), and +[Domain-Qualified Short IDs](#domain-qualified-short-id). *See: [federation.md](federation.md#unique-handles)* **Strict resolution** (Implemented) @@ -109,7 +110,8 @@ The asymmetric keypair backing exactly one [user handle](#user-handle); together keypair backing it are what's called an [identity](#identity). The private key signs requests made as that user and never leaves their control; the public key is handed out to establish trust in the handle (at registration, or via a [friend](#friend-friendship) exchange) and is used to verify -signatures. Only user handles carry a keypair, not groups, classification handles, or item handles. +signatures. Only user handles carry a keypair, not groups, classification handles, or +User-Qualified IDs. *See: [federation.md](federation.md#cryptography)* **Membership list** (Proposed) @@ -157,8 +159,8 @@ a property alias specifically, also states a unit conversion (even if it's "iden **Classification handle** (Implemented) Umbrella term for a [tag](#tag-property-category), [property](#tag-property-category), or [category](#tag-property-category) handle, of the form `origin#type:name`, e.g. -`git:base#property:length`. Distinct from a [user handle](#user-handle) or [item -handle](#item-handle): it names a reusable classification concept, not an actor or an owned thing. +`git:base#property:length`. Distinct from a [user handle](#user-handle) or [User-Qualified +ID](#user-qualified-id): it names a reusable classification concept, not an actor or an owned thing. *See: [federation.md](federation.md#tags-properties-and-categories)* **Definition fingerprint** (Partially implemented) @@ -201,29 +203,47 @@ strings today. ## Items & Physical Labels -**Item Handle** (Proposed) -The compact identifier for a specific item that's meaningful outside its owner's own account: -`user@domain.tld:id`, the owner's [user handle](#user-handle) plus a [local id](#local-id). Used -where it's already clear from context that it's a Toolshed item, e.g. inside the app, in exports, -in logs, so it doesn't need to spell that out or be openable on its own. Contrast with [Item -URL](#item-url), the self-contained form for when no such context can be assumed. -*See: [items-labels.md](design-in-progress/items-labels.md#open-design-questions)* +**Domain-Qualified Short ID** (Implemented) +A [domain](#domain) paired with a [short id](handles-and-shortids.md#short-ids) token, e.g. +`toolsheddomain.tld:~DyU`. A bare short id token is opaque and scoped to whichever backend minted +it, nothing in the token itself says which backend's id numbering to read it against, so it only +means something inside the app instance that's currently talking to that backend. Prefixing it with +its domain supplies the missing piece, the same way any other handle's domain half does, so the +pair keeps resolving correctly even after being copied out of the instance it was minted on. Unlike +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. +*See: [handles-and-shortids.md](handles-and-shortids.md#domain-qualified-short-id)* -**Item Label** (Proposed) +**Item Label** (Implemented) A physical, scannable encoding (QR code, barcode, or similar) of an item's [Item URL](#item-url), meant to be printed and stuck on the physical object it refers to. *See: [items-labels.md](design-in-progress/items-labels.md#goals)* -**Item URL** (Proposed) -The self-contained URL form of an [Item Handle](#item-handle), for use with no context at all, e.g. -an [Item Label](#item-label): `https:///i/user@domain.tld/id`. Has to open directly -to the right frontend and land on the right item on its own, since the reader can't be assumed to -already know what it is or which server it belongs to. Any frontend can serve this URL, the host -named in it isn't part of the item's identity, only the handle in its path is. +**Item URL** (Implemented) +The self-contained URL form of an item's [User-Qualified ID](#user-qualified-id), for use with no +context at all, e.g. an [Item Label](#item-label): `https:///i/user@domain.tld/id`. +Has to open directly to the right frontend and land on the right item on its own, since the reader +can't be assumed to already know what it is or which server it belongs to. Any frontend can serve +this URL, the host named in it isn't part of the item's identity, only the handle in its path is. *See: [items-labels.md](design-in-progress/items-labels.md#open-design-questions)* **Local id** (Implemented) An item's identifier as it exists today: unique only within its owner's own inventory, not -meaningful outside that owner's account. The starting point both the [Item -Handle](#item-handle) and [Item URL](#item-url) build on. +meaningful outside that owner's account. The starting point both a [User-Qualified +ID](#user-qualified-id) and [Item URL](#item-url) build on. *See: [federation.md](federation.md#items), [items-labels.md](design-in-progress/items-labels.md#problem)* + +**User-Qualified ID** (Implemented) +The compact identifier for a specific owned thing (an item, a storage location, ...) that's +meaningful outside its owner's own account: `:`, e.g. +`alice@example.com:i42`, the owner's [user handle](#user-handle) (or [group +handle](#group-handle) for a group-owned thing) plus a one-letter kind tag (`i` for item, `s` for +storage location) and a [local id](#local-id). Used where it's already clear from context that it's +Toolshed data, e.g. inside the app, in exports, in logs, so it doesn't need to spell that out or be +openable on its own. Generalizes what used to be a bespoke, item-only "Item Handle" concept: the +kind letter is what an item-specific `user@domain.tld:id` shape was missing, nothing in that string +said it was specifically an item. Contrast with [Item URL](#item-url), the self-contained form for +when no such context can be assumed. +*See: [handles-and-shortids.md](handles-and-shortids.md#user-qualified-id)* diff --git a/docs/handles-and-shortids.md b/docs/handles-and-shortids.md index 324f8c0..db73635 100644 --- a/docs/handles-and-shortids.md +++ b/docs/handles-and-shortids.md @@ -4,8 +4,8 @@ This is the syntax-level reference for two related but separate naming schemes. "Unique Handles" section covers *why* Toolshed hands out handles at all and what each kind (user, group, tag/property/category) means conceptually; this document covers the parsing rules those handles have to follow once they're written down or embedded somewhere - legal characters and -escaping. It also covers short ids end to end: a separate, newer scheme for packing small integer -id chains into a compact token, implemented in `frontend/src/short-id.js`. +escaping. It also covers short ids end to end: a separate scheme for packing small integer id +chains into a compact token. ## Handle syntax @@ -15,14 +15,14 @@ A username ends up embedded, unescaped, in several composite formats beyond its can't contain any character that already means something else in one of those: `@` (the user/domain separator in a user handle), `#` (the group-handle prefix, and the origin/type separator in a classification handle, see federation.md's Tags, Properties, and Categories -section), `:` (the id delimiter in a proposed Item Handle, the type/name delimiter in a -classification handle, and the delimiter in a signed request's `Authorization` header), `+` -(reserved as the URL-embedding escape for `#`, see below), `~` (the short-id token prefix, see -Short IDs below, and a proposed collision-disambiguation suffix delimiter on a tag's origin), and -`/` (the path-segment delimiter every handle and id ultimately sits next to once embedded in a -URL). This has to be enforced by an explicit validator rather than left to Django's default -`UnicodeUsernameValidator` (`^[\w.@+-]+\Z`), which currently permits both `@` and `+` (its own -regex doesn't happen to allow `#`, `:`, or `/`, but that's incidental, not a designed restriction). +section), `:` (the kind/id delimiter in a User-Qualified ID (see below), the type/name delimiter in +a classification handle, the domain/token delimiter in a Domain-Qualified Short ID (see below), and +the delimiter in a signed request's `Authorization` header), `+` (reserved as the URL-embedding +escape for `#`, see below), `~` (the short-id token prefix, see Short IDs below, and a +collision-disambiguation suffix delimiter on a tag's origin), and `/` (the path-segment delimiter +every handle and id ultimately sits next to once embedded in a URL). This has to be enforced by an +explicit validator rather than left to a framework default, which doesn't draw the line in the same +place. ### Embedding a `#`-bearing handle in a URL @@ -44,17 +44,45 @@ otherwise forbidden in every field a handle is built from (see Reserved characte group name or tag name could itself contain a literal `+`, it would be indistinguishable from an escaped `#` once decoded. -Implemented in `frontend/src/handle-url.js` (`encodeHandleForUrl`/`decodeHandleFromUrl`). +### User-Qualified ID + +Owned things (an item, a storage location, ...) can be referred to outside their owner's own +account: anything whose id is only unique within one owner's own numbering needs an owner-qualified +form to resolve globally. + +A **User-Qualified ID** is an owner handle (a user handle, or a group handle for a group-owned +thing) with a kind letter and a local id appended, separated by a single `:`: +`:`, e.g. `alice@example.com:i42`. This is the same `origin#type:name` +pattern classification handles already use, built on `:` instead of `#` so it avoids the URL +fragment-escaping problem (see Embedding a `#`-bearing handle in a URL above) โ€” a User-Qualified ID +isn't meant to sit directly in a URL path segment. + +Its payload is a plain decimal local id with nothing self-describing baked in, unlike a Short ID's +bit-packed payload, so the owner is spelled out as a full handle rather than just a domain: +`example.com:i42` would be ambiguous between every user on that domain with local item id `42`; +`alice@example.com:i42` isn't. An owner handle already carries its domain, so a User-Qualified ID is +cross-domain-resolvable as written, with no separate domain-qualified wrapper needed. + +The kind letter is a small, closed registry. A group handle already looks visibly different from a +user handle (`#` prefix), so unlike the short id kind registry below, this one doesn't need separate +letters for a user-owned vs. group-owned kind โ€” the owner half already says which it is. + +| letter | kind | notes | +|---|---|---| +| `i` | item | covers both a user-owned item and a group-owned item | +| `s` | storage location | covers both a user-owned and a group-owned storage location | + +`workflow` has no letter assigned; the registry can grow without breaking anything already printed. +Kinds with no owner (`category`, `group`, `file`) don't fit this scheme: `category` and `group` each +already have their own dedicated handle form. `file` has neither an owner to qualify by nor a +dedicated handle of its own; a Domain-Qualified Short ID (below) is the fallback for it. ## Short IDs A general encoding for turning a small, fixed-shape list of integers into a compact, URL-safe token, with no server-side lookup table involved: the code *is* the data, nothing is stored -server-side to make it resolvable. Originally proposed to answer items-labels.md's open "what does -the handle/URL actually look like" question, but the encoding itself isn't item-specific; anything -currently addressed by a short chain of small integers is a candidate. Implemented and tested in -`frontend/src/short-id.js`; try it live at `/~` (`frontend/src/views/ShortId.vue`), which -decodes whatever token is in the URL and also lists worked examples for every registered kind. +server-side to make it resolvable. Not item-specific; anything currently addressed by a short chain +of small integers is a candidate. ### Shape: a kind tag, then a fixed list of integers @@ -67,29 +95,21 @@ range" - so kind 3 is encoded as escape + chunked-int `0`, kind 4 as escape + `1 costs nothing for a kind that already fits in the direct range, and keeps the tag itself extensible forever without ever having to widen it out from under codes that were already printed. A narrow tag only pays off if kind usage is actually skewed the way id values are (a few kinds dominate), -which is why the registry below is ordered by expected frequency, cheapest (most-used) kind first: +which is why the registry below is ordered by expected frequency, cheapest (most-used) kind first. +Kind ids are scoped **per arity**, not globally: the same id can (and does) name a different kind +depending on how many fields follow it, since the arity is always known before the kind id needs +disambiguating: -| kind | name | fields | notes | -|---|---|---|---| -| 0 | `item` | `owner_identity_id`, `item_local_id` | dominant case - the primary physical-label use case | -| 1 | `storage_location` | `owner_identity_id`, `storage_location_id` | also label-printed | -| 2 | `category` | `category_id` | label-adjacent (tagging); global, no owner | -| 3 | `workflow` | `owner_identity_id`, `workflow_id` | shared in-app, not printed - first to pay the escape's cost | -| 4 | `group` | `group_id` | shared even less often; global, no owner | -| 5 | `file` | `file_id` | least often shared standalone; global, deduplicated by content hash | - -`owner_identity_id` is `KnownIdentity.pk` (`backend/authentication/models.py`), not -`ToolshedUser.pk`. Every local account already has exactly one stable `KnownIdentity` row -(`ToolshedUser.public_identity`, created once at registration and never recreated), and every -friend this backend knows about - local or remote - is represented by that same table, unique on -`(username, domain)`. So one small integer already stands in for "this owner, as known by this -backend" for both cases, with no separate local-vs-remote branching needed, and it's the same row -federation.md's Cryptography section already treats as the trust anchor for a handle's public key. -It appears on `item`, `storage_location`, and `workflow` because their backing models -(`InventoryItem`, `StorageLocation`, `WorkflowInstance`) all FK `ToolshedUser` directly; `category`, -`group`, and `file` skip it because their models are global/unscoped (`Group` has an unowned -`members` M2M, `File` is deduplicated globally by content hash), so a bare row id is already -everything needed to look them up. +| kind id | arity | name | fields | notes | +|---|---|---|---|---| +| 0 | 2 | `item` | `owner_identity_id`, `item_local_id` | dominant case - a personally-owned item, the primary physical-label use case | +| 0 | 1 | `category` | `category_id` | label-adjacent (tagging); global, no owner - shares id 0 with `item` since the two never need the same arity | +| 1 | 2 | `group_item` | `owner_group_id`, `item_local_id` | same use case as `item`, but for a group-owned item | +| 1 | 1 | `group` | `group_id` | shared even less often; global, no owner | +| 2 | 2 | `storage_location` | `owner_identity_id`, `storage_location_id` | also label-printed | +| 2 | 1 | `file` | `file_id` | least often shared standalone; global, deduplicated by content hash | +| 3 | 2 | `workflow` | `owner_identity_id`, `workflow_id` | shared in-app, not printed - needs the escape range, since every other slot in the direct range (0-2) is already double-booked across the two arities in use | +| 4 | 2 | `group_storage_location` | `owner_group_id`, `storage_location_id` | same use case as `storage_location`, but for a group-owned location (see `group_item` above for the same owner/owner_group split); the direct range is fully double-booked, so this is the second kind that needs the escape range | A short id is inherently scoped to the backend that minted it (an "owner" field is a row that only exists in, and only means anything to, that one backend's database), not a portable replacement for @@ -119,13 +139,12 @@ Standard base64 assumes byte-aligned (8-bit) input, grouping 3 bytes into 4 outp padding to a byte boundary before encoding. Since there's no byte layer here to begin with, the bit-packed stream is instead packed directly into 6-bit groups and mapped straight onto the URL-safe base64 alphabet (RFC 4648 ยง5: `-` and `_` in place of `+` and `/`), with the final -character's unused low bits padded with zeros. That padding is safe by construction: the decoder -always knows exactly how many integers a given kind calls for, and a chunk's continuation bit is -`1 = more follows`, so a run of zero-padding at the very end can never be misread as "one more -chunk" - it decodes as a terminated chunk, at which point every field the schema called for has -already been produced and decoding simply stops. No `=` padding characters are needed either; those -exist in classic base64 purely to communicate trailing-byte padding, and there is no byte layer -here to need that. +character's unused low bits padded with zeros. That padding is safe by construction: it's always +0-5 zero bits (just enough to reach the next multiple of 6), and the decoder stops reading fields +the moment a chunk decodes to 0 with nothing left after it - by the last-field-nonzero rule above, +that can only be the padding, never a genuine field, so it's discarded rather than counted. No `=` +padding characters are needed either; those exist in classic base64 purely to communicate +trailing-byte padding, and there is no byte layer here to need that. ### The leading `~` @@ -140,6 +159,35 @@ short id can be handed to any part of the stack without first checking which enc there. +### Domain-Qualified Short ID + +A short id token is deliberately opaque and scoped to whichever backend minted it: handed a bare +`~DyU`, nothing in the token itself says which backend's numbering it should be read against. +That's fine as long as the token stays inside a context that already knows the answer (the app the +user is currently looking at), but not once a token needs to travel outside that context, e.g. +pasted into a message to a friend on a different domain, or logged somewhere not tied to one +backend. + +The fix is the same one every other cross-domain handle in this project already uses: pair the +opaque part with the domain that's authoritative for it. A **Domain-Qualified Short ID** is a short +id token prefixed with a domain and a literal `:`, e.g. `toolsheddomain.tld:~DyU`. The domain half +is exactly a handle's domain half (see federation.md's Unique Handles section): not a location, +just a statement of which backend to resolve the token against. Decoding still means handing the +`~token` half to that backend's decoder, same as ever; it's just no longer ambiguous which +backend's decoder to hand it to. + +This is deliberately not the same thing as a User-Qualified ID (`owner-handle:i42`) or an Item URL. +It isn't a URL and isn't meant to be openable by something that doesn't already know what a +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 +`~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 +domain-qualified the same way, since the domain only ever names which backend's token namespace +applies, not which kind the token decodes to or whether that kind has an owner. + ### Worked examples Encoding `kind = item` (0), `owner_identity_id = 7`, `item_local_id = 42`: @@ -152,17 +200,19 @@ Total: 17 meaningful bits, padded to the next multiple of 6 (18) with one zero b base64 characters: **`~DyU`**. The same worked-out form for one example of every registered kind - `Bits` is the same -space-separated segmentation (kind tag, escape offset if present, each field, then padding) the -Examples table on `/~` (`frontend/src/views/ShortId.vue`) shows for every registered kind: +space-separated segmentation: kind tag, escape offset if present, each field, then padding. Kinds +are grouped by shared id below to make the per-arity reuse visible: | Kind | Fields | Serialized | Bits | Token | |---|---|---|---|---| | `item` | `owner_identity_id: 7`, `item_local_id: 42` | `[0, 7, 42]` | `00 00111 1001001010 0` | `~DyU` | -| `storage_location` | `owner_identity_id: 3`, `storage_location_id: 1000` | `[1, 3, 1000]` | `01 00011 100111111001000 00` | `~Rz8g` | -| `category` | `category_id: 5` | `[2, 5]` | `10 00101 00000` | `~ig` | -| `workflow` | `owner_identity_id: 2`, `workflow_id: 9` | `[3, 2, 9]` | `11 00000 00010 01001 0` | `~wCS` | -| `group` | `group_id: 11` | `[4, 11]` | `11 00001 01011` | `~wr` | -| `file` | `file_id: 123` | `[5, 123]` | `11 00010 1011101011 0` | `~xXW` | +| `category` | `category_id: 5` | `[0, 5]` | `00 00101 00000` | `~Cg` | +| `group_item` | `owner_group_id: 5`, `item_local_id: 42` | `[1, 5, 42]` | `01 00101 1001001010 0` | `~SyU` | +| `group` | `group_id: 11` | `[1, 11]` | `01 01011 00000` | `~Vg` | +| `storage_location` | `owner_identity_id: 3`, `storage_location_id: 1000` | `[2, 3, 1000]` | `10 00011 100111111001000 00` | `~hz8g` | +| `file` | `file_id: 123` | `[2, 123]` | `10 1011101011` | `~rr` | +| `workflow` | `owner_identity_id: 2`, `workflow_id: 9` | `[3, 2, 9]` | `11 00000 00010 01001 0` | `~wiS` | +| `group_storage_location` | `owner_group_id: 5`, `storage_location_id: 1000` | `[4, 5, 1000]` | `11 00001 00101 100111111001000 000` | `~wln5A` | `workflow`, `group`, and `file` are kinds 3-5, so their `Bits` column shows the escape tag (`11`) followed by its own offset segment payload fields. \ No newline at end of file diff --git a/frontend/src/assets/fonts/pixel/LICENSE.md b/frontend/src/assets/fonts/pixel/LICENSE.md deleted file mode 100644 index c8e62bf..0000000 --- a/frontend/src/assets/fonts/pixel/LICENSE.md +++ /dev/null @@ -1,9 +0,0 @@ -# Pixel fonts used for small label text (see ../../../scss/_pixel-fonts.scss) - -- **Tom Thumb** (`TomThumb.ttf`) - by Brian Swetland, TTF conversion by gheja - (https://github.com/gheja/tom-thumb-ttf). Licensed CC0 or CC-BY 3.0 (original: - https://robey.lag.net/2010/01/23/tiny-monospace-font.html). -- **PICO-8** (`PICO-8.ttf`) - reproduction by Jacob Pierce - (https://github.com/jacobpierce/pico-8-font). MIT License, Copyright (c) 2016 Jacob Pierce. -- **Silkscreen** (`Silkscreen-Regular.woff2`) - by Jason Kottke, served via Google Fonts - (https://fonts.google.com/specimen/Silkscreen). SIL Open Font License 1.1. diff --git a/frontend/src/label-layouts.js b/frontend/src/label-layouts.js index 7d8fed1..a0d7381 100644 --- a/frontend/src/label-layouts.js +++ b/frontend/src/label-layouts.js @@ -149,10 +149,16 @@ export const LABEL_TEMPLATES = [ }, { id: "item-handle", name: "Item handle", - description: "The compact owner@domain:id handle - meaningful in-app, not scannable on its own.", + description: "The compact owner@domain:i 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 User-Qualified ID - meaningful in-app, not scannable on its own.", + required_vars: ["locationHandle"], tags: ["internal"], + layout: [{type: "text", content: c => c.locationHandle}] + }, { id: "item-url", name: "Item URL", description: "The full item URL as text, with no code - for copying rather than scanning.", @@ -252,11 +258,16 @@ export const DERIVED_VARS = { inputs: ["webdomain", "userHandle", "itemId"], calc: (f) => `${f.webdomain}/i/${encodeHandleForUrl(f.userHandle)}/${f.itemId}`, }, - // Compact "owner handle + id" form (see docs/design-in-progress/items-labels.md) - meaningful - // only where context already makes clear it's a Toolshed item, unlike itemUrl. + // Compact User-Qualified ID (see docs/handles-and-shortids.md#user-qualified-id) - owner + // handle + a one-letter kind tag + local id, meaningful only where context already makes + // clear it's Toolshed data, unlike itemUrl. `i` = item, `s` = storage location. itemHandle: { inputs: ["userHandle", "itemId"], - calc: (f) => `${f.userHandle}:${f.itemId}`, + calc: (f) => `${f.userHandle}:i${f.itemId}`, + }, + locationHandle: { + inputs: ["userHandle", "locationId"], + calc: (f) => `${f.userHandle}:s${f.locationId}`, }, }; diff --git a/frontend/src/views/Print.vue b/frontend/src/views/Print.vue index 4b1ffab..07afb53 100644 --- a/frontend/src/views/Print.vue +++ b/frontend/src/views/Print.vue @@ -234,7 +234,7 @@