stash
This commit is contained in:
parent
d56784eb8d
commit
6ee5ae38b1
8 changed files with 246 additions and 153 deletions
|
|
@ -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.
|
||||
|
|
@ -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<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: "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}`,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@
|
|||
<script>
|
||||
import * as BIcons from "bootstrap-icons-vue";
|
||||
import {markRaw, nextTick} from "vue";
|
||||
import {mapActions, mapGetters} from "vuex";
|
||||
import {mapActions, mapGetters, mapState} from "vuex";
|
||||
import BaseLayout from "@/components/BaseLayout.vue";
|
||||
import LabelLayoutPreview from "@/components/LabelLayoutPreview.vue";
|
||||
import printerManager from "@/printerManager.js";
|
||||
|
|
@ -337,7 +337,12 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["user"]),
|
||||
...mapGetters(["identityIdByHandle", "groupIdByHandle"]),
|
||||
homeDomain() {
|
||||
const at = this.user ? this.user.indexOf("@") : -1;
|
||||
return at === -1 ? null : this.user.slice(at + 1);
|
||||
},
|
||||
baseVars() {
|
||||
return BASE_VARS.filter(v => v !== SHORT_URL_VAR && v !== SHORT_ID_VAR);
|
||||
},
|
||||
|
|
@ -357,7 +362,8 @@ export default {
|
|||
if (shortId) {
|
||||
derived[SHORT_ID_VAR] = shortId;
|
||||
if (derived.webdomain) {
|
||||
derived[SHORT_URL_VAR] = derived.webdomain + "/" + shortId;
|
||||
derived[SHORT_URL_VAR] = derived.webdomain + "/"
|
||||
+ (this.homeDomain ? this.homeDomain + ":" + shortId : shortId);
|
||||
}
|
||||
}
|
||||
return derived;
|
||||
|
|
@ -502,9 +508,6 @@ export default {
|
|||
return ticks;
|
||||
},
|
||||
|
||||
// Reads the recently-printed template ids back from localStorage; same try/catch shape as
|
||||
// cameraManager.js's getRecentCameras since either a disabled/full localStorage shouldn't
|
||||
// break printing.
|
||||
loadRecentTemplateIds() {
|
||||
try {
|
||||
const saved = localStorage.getItem(RECENT_TEMPLATES_KEY);
|
||||
|
|
@ -514,10 +517,7 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
// Moves `id` to the front of the recent-templates list (deduping any earlier occurrence),
|
||||
// capped to MAX_RECENT_TEMPLATES, so LabelLayoutPreview.vue's grid always bubbles the
|
||||
// last four printed/downloaded layouts to the top.
|
||||
rememberPrintedTemplate(id) {
|
||||
rememberPrintedTemplate(id) {
|
||||
const updated = [id, ...this.recentTemplateIds.filter(t => t !== id)].slice(0, MAX_RECENT_TEMPLATES);
|
||||
this.recentTemplateIds = updated;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -70,17 +70,10 @@
|
|||
import {mapActions} from "vuex";
|
||||
import BaseLayout from "@/components/BaseLayout.vue";
|
||||
import CameraScanner from "@/components/CameraScanner.vue";
|
||||
import {encodeHandleForUrl, expandedRoute} from "@/router";
|
||||
import {decodeShortId, deserializeShortId} from "@/short-id";
|
||||
import {encodeHandleForUrl, expandedRoute, OWNED_KIND_FIELDS} from "@/router";
|
||||
import {decodeShortId, deserializeShortId, isDomainQualifiedShortId, decodeDomainQualifiedShortId} from "@/short-id";
|
||||
|
||||
// A scanned label can encode any of these (see label-layouts.js's DERIVED_VARS): a full
|
||||
// self-contained URL (itemUrl/shortUrl), a bare short-id.js token with no domain at all (the
|
||||
// "mqr"/id-only layout), or the compact no-URL "<userHandle>:<itemId>" form (itemHandle). Detects
|
||||
// which and returns a link target, or null if the text doesn't match any known format. `decoded`/
|
||||
// `itemHandle` carry enough of the parsed token for describeLink (below) to also resolve a
|
||||
// human-readable "what this points at" - not needed for the two URL cases, which link to
|
||||
// somewhere already showing that.
|
||||
const ITEM_HANDLE_RE = /^(#?[^\s@:/#+~]+@[^\s@:/#+~]+):(\d+)$/;
|
||||
const OWNER_QUALIFIED_ID_RE = /^(#?[^\s@:/#+~]+@[^\s@:/#+~]+):([is])(\d+)$/;
|
||||
|
||||
function classifyScanText(text) {
|
||||
if (!text) {
|
||||
|
|
@ -88,32 +81,29 @@ function classifyScanText(text) {
|
|||
}
|
||||
try {
|
||||
const url = new URL(text);
|
||||
// Same-origin URLs (the common case - a label printed by this same app) get routed
|
||||
// in-app instead of forcing a full page reload through an <a> tag. `immediate`: a URL
|
||||
// needs no async lookup to confirm it's real (unlike the token/handle formats below), so
|
||||
// it's already "resolved" the moment it's classified - see resolveDescription and
|
||||
// maybeVisitFirstMatch.
|
||||
return url.origin === window.location.origin
|
||||
? {to: url.pathname + url.search + url.hash, immediate: true}
|
||||
: {href: url.href};
|
||||
} catch {
|
||||
// Not an absolute URL - fall through to the other known formats below.
|
||||
}
|
||||
if (text.startsWith('~')) {
|
||||
if (text.startsWith('~') || isDomainQualifiedShortId(text)) {
|
||||
try {
|
||||
const decoded = deserializeShortId(decodeShortId(text));
|
||||
// expandedRoute needs idmap already loaded to resolve item/group_item (see
|
||||
// router.js's NEEDS_IDMAP) - fall back to the token's own URL (ShortId.vue resolves
|
||||
// it from there, same as a cold-opened short link) when it can't yet.
|
||||
return {to: expandedRoute(decoded) || '/' + text, decoded};
|
||||
const qualified = isDomainQualifiedShortId(text) ? decodeDomainQualifiedShortId(text) : null;
|
||||
const decoded = deserializeShortId(qualified ? qualified.ints : decodeShortId(text));
|
||||
return {to: (!qualified && expandedRoute(decoded)) || '/' + text, decoded, domain: qualified?.domain};
|
||||
} catch {
|
||||
return null; // starts with '~' but isn't a real short id - leave as plain text
|
||||
return null; // looked like a short id but isn't a real one - leave as plain text
|
||||
}
|
||||
}
|
||||
const handleMatch = text.match(ITEM_HANDLE_RE);
|
||||
if (handleMatch) {
|
||||
const [, handle, id] = handleMatch;
|
||||
return {to: `/inventory/${encodeHandleForUrl(handle)}/${id}`, itemHandle: {handle, id}};
|
||||
const ownerMatch = text.match(OWNER_QUALIFIED_ID_RE);
|
||||
if (ownerMatch) {
|
||||
const [, handle, kindLetter, id] = ownerMatch;
|
||||
const kind = kindLetter === "s" ? "storage_location" : "item";
|
||||
const to = kind === "storage_location"
|
||||
? `/storage-locations/${encodeHandleForUrl(handle)}/${id}`
|
||||
: `/inventory/${encodeHandleForUrl(handle)}/${id}`;
|
||||
return {to, ownerQualifiedId: {handle, kind, id}};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -154,7 +144,7 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions(["fetchItemByHandle", "fetchStorageLocationByHandle", "fetchIdMap"]),
|
||||
...mapActions(["fetchItemByHandle", "fetchStorageLocationByHandle", "fetchIdMap", "resolveShortId"]),
|
||||
|
||||
resolveDescription(entry) {
|
||||
const {link, text} = entry;
|
||||
|
|
@ -190,13 +180,17 @@ export default {
|
|||
},
|
||||
|
||||
async describeLink(link) {
|
||||
if (link.itemHandle) {
|
||||
return this.describeItem(link.itemHandle.handle, link.itemHandle.id);
|
||||
if (link.ownerQualifiedId) {
|
||||
const {handle, kind, id} = link.ownerQualifiedId;
|
||||
return kind === "storage_location" ? this.describeLocation(handle, id) : this.describeItem(handle, id);
|
||||
}
|
||||
const decoded = link.decoded;
|
||||
if (!decoded) {
|
||||
return null;
|
||||
}
|
||||
if (link.domain) {
|
||||
return this.describeForeignShortId(link.domain, decoded);
|
||||
}
|
||||
if (decoded.kind === "item" || decoded.kind === "group_item") {
|
||||
const byId = decoded.kind === "item"
|
||||
? this.$store.getters.identityHandleById
|
||||
|
|
@ -236,6 +230,22 @@ export default {
|
|||
return null; // workflow, category, file: no per-item title lookup wired up yet
|
||||
},
|
||||
|
||||
async describeForeignShortId(domain, decoded) {
|
||||
const owned = OWNED_KIND_FIELDS[decoded.kind];
|
||||
if (!owned) {
|
||||
return null; // no owner id to resolve a foreign backend against yet (group/category/file/workflow)
|
||||
}
|
||||
const resolved = await this.resolveShortId({
|
||||
domain, kind: decoded.kind, ownerId: decoded[owned.ownerField], localId: decoded[owned.localField],
|
||||
});
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
return owned.isLocation
|
||||
? this.describeLocation(resolved.handle, resolved.id)
|
||||
: this.describeItem(resolved.handle, resolved.id);
|
||||
},
|
||||
|
||||
async describeItem(handle, id) {
|
||||
const item = await this.fetchItemByHandle({handle, id});
|
||||
return item ? `[#${item.id}] ${item.name}` : null;
|
||||
|
|
@ -263,20 +273,11 @@ export default {
|
|||
error: null,
|
||||
};
|
||||
this.cameraLog.unshift(entry);
|
||||
// Resolve against cameraLog[0], not the plain `entry` object above: Vue's reactivity
|
||||
// tracks property sets through the reactive proxy unshift() just installed, and
|
||||
// mutating the pre-insertion raw object later bypasses that proxy entirely, so the
|
||||
// description would never appear to update (stuck on "resolving...") even once the
|
||||
// lookup actually finished.
|
||||
this.resolveDescription(this.cameraLog[0]);
|
||||
// Caps the log (old entries trimmed) rather than growing forever, matching
|
||||
// CameraScanner's own dedupe window that makes them stale for re-matching.
|
||||
this.cameraLog.length = Math.min(this.cameraLog.length, 20);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// Not component data: resolveDescription's cached values are read back into each entry's
|
||||
// own (reactive) `description` field, so the cache itself never needs to be reactive.
|
||||
this.descriptionCache = new Map();
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
<BaseLayout hide-search>
|
||||
<main class="content">
|
||||
<div class="container-fluid p-0">
|
||||
<div v-if="qualified" class="card">
|
||||
<div class="card-header">Domain</div>
|
||||
<div class="card-body">{{ qualified.domain }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">Decoded</div>
|
||||
<div class="card-body">
|
||||
|
|
@ -64,8 +68,11 @@
|
|||
<script>
|
||||
import {mapActions} from 'vuex';
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import {decodeShortId, deserializeShortId, encodeShortId, serializeShortId} from '@/short-id';
|
||||
import {expandedRoute as buildExpandedRoute, NEEDS_IDMAP} from '@/router';
|
||||
import {
|
||||
decodeShortId, deserializeShortId, encodeShortId, serializeShortId,
|
||||
isDomainQualifiedShortId, decodeDomainQualifiedShortId
|
||||
} from '@/short-id';
|
||||
import {expandedRoute as buildExpandedRoute, NEEDS_IDMAP, domainQualifiedRoute} from '@/router';
|
||||
|
||||
const EXAMPLES = [
|
||||
{kind: 'item', owner_identity_id: 7, item_local_id: 42},
|
||||
|
|
@ -130,12 +137,17 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
error: null
|
||||
error: null,
|
||||
foreignRoute: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// '<domain>:~<token>' vs a bare '~<token>' - see docs/handles-and-shortids.md#domain-qualified-short-id.
|
||||
qualified() {
|
||||
return isDomainQualifiedShortId(this.short_id) ? decodeDomainQualifiedShortId(this.short_id) : null;
|
||||
},
|
||||
decoded() {
|
||||
return decodeShortId(this.short_id);
|
||||
return this.qualified ? this.qualified.ints : decodeShortId(this.short_id);
|
||||
},
|
||||
deserialized() {
|
||||
return deserializeShortId(this.decoded);
|
||||
|
|
@ -145,7 +157,7 @@ export default {
|
|||
return fields;
|
||||
},
|
||||
expandedRoute() {
|
||||
return buildExpandedRoute(this.deserialized);
|
||||
return this.qualified ? (this.foreignRoute || null) : buildExpandedRoute(this.deserialized);
|
||||
},
|
||||
examples() {
|
||||
return EXAMPLES.map(named => {
|
||||
|
|
@ -165,13 +177,21 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchIdMap'])
|
||||
...mapActions(['fetchIdMap']),
|
||||
async resolveIfQualified() {
|
||||
if (this.qualified) {
|
||||
this.foreignRoute = await domainQualifiedRoute(this.qualified.domain, this.decoded) || null;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.resolveIfQualified();
|
||||
if (NEEDS_IDMAP.has(this.deserialized.kind) && !this.$store.state.idmapLoaded) {
|
||||
this.fetchIdMap().catch(e => {
|
||||
this.error = e.message;
|
||||
});
|
||||
this.fetchIdMap()
|
||||
.then(() => this.resolveIfQualified())
|
||||
.catch(e => {
|
||||
this.error = e.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue