stash
This commit is contained in:
parent
d56784eb8d
commit
6ee5ae38b1
8 changed files with 246 additions and 153 deletions
|
|
@ -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();
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue