This commit is contained in:
j3d1 2026-08-27 20:41:19 +02:00
parent 2bb6624d50
commit 7276750c66
15 changed files with 552 additions and 169 deletions

View file

@ -3,31 +3,34 @@
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">Inventory</h1>
<div class="row mb-3">
<div class="col-12 col-md-4">
<label for="ownerSelect" class="form-label">Viewing inventory for</label>
<select id="ownerSelect" class="form-select" v-model="selectedOwner">
<option :value="user">{{ user }} (you)</option>
<option v-for="group in ownerGroups" :value="group.handle" :key="group.handle">
{{ group.handle }}
</option>
<option v-for="friend in friends" :value="friend.handle" :key="friend.handle">
{{ friend.handle }}
</option>
</select>
</div>
</div>
<div class="row">
<div class="col-12 col-xl-12">
<div class="card">
<div class="card-header">
<h5 class="card-title">{{ selectedOwner }}'s Inventory</h5>
<button v-if="layout === 'grid'" @click="layout = 'table'" class="btn">
<b-icon-list></b-icon-list>
</button>
<button v-else @click="layout = 'grid'" class="btn">
<b-icon-grid></b-icon-grid>
</button>
<div class="card-header d-flex justify-content-between align-items-center flex-wrap">
<h5 class="card-title mb-0">{{ selectedOwner }}'s Inventory</h5>
<div class="d-flex align-items-center">
<!--label for="ownerSelect" class="visually-hidden">Viewing inventory for</label-->
<select id="ownerSelect" class="form-select form-select-sm me-2" style="width:auto"
v-model="selectedOwner">
<option :value="user">{{ user }} (you)</option>
<option v-for="group in ownerGroups" :value="group.handle" :key="group.handle">
{{ group.handle }}
</option>
<option v-for="friend in friends" :value="friend.handle" :key="friend.handle">
{{ friend.handle }}
</option>
</select>
<div class="btn-group">
<button class="btn" @click="fetchItemsForOwner">Refresh</button>
<router-link v-if="canEdit" :to="addItemRoute" class="btn btn-primary">Add</router-link>
<button v-if="layout === 'grid'" @click="layout = 'table'" class="btn">
<b-icon-list></b-icon-list>
</button>
<button v-else @click="layout = 'grid'" class="btn">
<b-icon-grid></b-icon-grid>
</button>
</div>
</div>
</div>
<table class="table table-striped" v-if="layout === 'table'">
<thead>
@ -106,10 +109,6 @@
</div>
</div>
<div class="card">
<button class="btn" @click="fetchItemsForOwner">Refresh</button>
<router-link v-if="canEdit" :to="addItemRoute" class="btn btn-primary">Add</router-link>
</div>
</div>
</div>
</div>
@ -122,14 +121,20 @@ import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import AuthenticatedImage from "../components/AuthenticatedImage.vue";
import {shortenedRoute, encodeHandleForUrl} from "@/router";
import {shortenedRoute, encodeHandleForUrl, decodeHandleFromUrl} from "@/router";
export default {
name: "Inventory",
props: {
// Matches /inventory/:owner; absent means "me". See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
owner: {
type: String,
default: null
}
},
data() {
return {
layout: "grid",
selectedOwner: null,
}
},
components: {
@ -140,31 +145,37 @@ export default {
computed: {
...mapGetters(["inventory_items", "groupInventoryItems", "loaded_items", "identityIdByHandle", "groupIdByHandle"]),
...mapState(["user", "storage_locations", "groups", "groupMemberships", "friends"]),
// Groups hosted here plus groups only known via a GroupMembership pointer - see
// Groups.vue's allGroups and InventoryNew.vue's ownerGroups for the same merge/dedupe.
// Groups hosted here plus GroupMembership-only ones - see Groups.vue's allGroups for the same merge/dedupe.
ownerGroups() {
const hostedHandles = new Set(this.groups.map(group => group.handle))
const foreign = this.groupMemberships.filter(m => !hostedHandles.has(m.handle))
return [...this.groups, ...foreign].sort((a, b) => a.handle.localeCompare(b.handle))
},
// See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
selectedOwner: {
get() {
return this.owner ? decodeHandleFromUrl(this.owner) : this.user
},
set(value) {
const path = value === this.user ? '/inventory' : `/inventory/${encodeHandleForUrl(value)}`
this.$router.replace(path)
}
},
isGroupSelected() {
return !!this.selectedOwner && this.selectedOwner.startsWith('#')
},
// Own items and group items can be created/edited/deleted here; a friend's items are
// shown for browsing only - the backend rejects writes for anyone but the owner or a
// fellow group member (see inventory.py's perform_create/update/destroy).
// Friend items are browse-only - inventory.py's perform_create/update/destroy reject writes from anyone but the owner/a fellow group member.
canEdit() {
return this.selectedOwner === this.user || this.isGroupSelected
},
items() {
// item_map is keyed by owner handle regardless of whether that owner is a group or a
// friend, so the same getter serves both - see store.js's groupInventoryItems.
// item_map is keyed by owner handle regardless of group vs friend - see store.js's groupInventoryItems.
return this.selectedOwner === this.user ? this.inventory_items : this.groupInventoryItems(this.selectedOwner)
},
addItemRoute() {
return this.selectedOwner === this.user
? '/inventory/new'
: `/inventory/new?group=${encodeHandleForUrl(this.selectedOwner)}`
: `/inventory/new/${encodeHandleForUrl(this.selectedOwner)}`
}
},
methods: {
@ -181,8 +192,7 @@ export default {
this.fetchItemsForOwner()
})
},
// The owner handle is derived from the item itself, not the current selection, so a
// link stays correct even if the dropdown selection changes underneath it.
// Derived from the item's own owner, not the current selection, so the link stays correct if the dropdown changes underneath it.
itemRoute(item) {
return `/inventory/${encodeHandleForUrl(item.owner_group || item.owner)}/${item.id}`
},
@ -210,16 +220,12 @@ export default {
},
},
watch: {
selectedOwner() {
owner() {
this.fetchItemsForOwner()
}
},
created() {
// Set before the first render so addItemRoute/items never see selectedOwner=null
// while user is already populated (which would misroute to the group branch).
this.selectedOwner = this.user
},
async mounted() {
this.fetchItemsForOwner()
await this.fetchStorageLocations()
await this.fetchIdMap()
await this.fetchGroups()