diff --git a/frontend/src/store.js b/frontend/src/store.js index 483072c..4e8141d 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -619,12 +619,16 @@ export default createStore({ const {domain} = splitGroupHandle(groupHandle) const servers = await dispatch('getFriendServers', {username: 'x@' + domain}) const items = await servers.get(getters.signAuth, '/api/v1/inventory_items/' + encodeHandleForUrl(groupHandle) + '/') - // Keyed by the full handle, not a bare id: a group pk is only unique within its own - // backend's database, so two different domains could otherwise collide on the same - // item_map key - the handle already carries the domain, so no separate namespacing is needed. commit('setInventoryItems', {url: '/' + groupHandle, items}) return items }, + async fetchFriendInventoryItems({commit, dispatch, getters}, {friendHandle}) { + const servers = await dispatch('getFriendServers', {username: friendHandle}) + const items = await servers.get(getters.signAuth, '/api/v1/inventory_items/' + encodeHandleForUrl(friendHandle) + '/') + items.map(item => item.files.map(file => file.owner = item.owner)) + commit('setInventoryItems', {url: '/' + friendHandle, items}) + return items + }, async fetchFiles({state, commit, dispatch, getters}) { if (state.last_load.files > Date.now() - 1000 * 60 * 60 * 24) { return state.files diff --git a/frontend/src/views/GroupDetail.vue b/frontend/src/views/GroupDetail.vue index b079b59..e50f6da 100644 --- a/frontend/src/views/GroupDetail.vue +++ b/frontend/src/views/GroupDetail.vue @@ -4,7 +4,7 @@

{{ group ? group.handle : '' }}

-
+
Members
@@ -50,136 +50,6 @@
-
-
-
-
Group inventory
- - -
- - - - - - - - - - - - - - - - - -
NameAvailability PolicyAmountActions
- {{ item.name }} - - {{ item.availability_policy }} - {{ item.owned_quantity }} - - - - - - - - - -
-
-
-
-
-
-
- - {{ item.name }} - -
-
- {{ item.availability_policy }} - {{ item.owned_quantity }} -
-
- - Edit - - - - -
-
-
-
-
-
-
-
- - Add - -
-
-
-
-
-
Group storage locations
-
- - - - - - - - - - - - - - - - - -
NamePathCategoryActions
- {{ location.name }} - - {{ location.path }} - - {{ location.category }} - - - - - - - - - - - - -
-
-
- - Add - -
-
@@ -187,11 +57,11 @@ diff --git a/frontend/src/views/Inventory.vue b/frontend/src/views/Inventory.vue index 15b8727..18f6023 100644 --- a/frontend/src/views/Inventory.vue +++ b/frontend/src/views/Inventory.vue @@ -2,12 +2,26 @@
-

Inventory Own & Friends"

+

Inventory

+
+
+ + +
+
-
{{ user }}'s Inventory
+
{{ selectedOwner }}'s Inventory
@@ -25,7 +39,7 @@ - + {{ item.name }} @@ -34,10 +48,10 @@ {{ item.owned_quantity }} - + - + @@ -52,7 +66,7 @@
-
@@ -70,10 +84,10 @@ {{ item.owned_quantity }}
- - Edit
- - Add + + Add
@@ -115,6 +129,7 @@ export default { data() { return { layout: "grid", + selectedOwner: null, } }, components: { @@ -123,15 +138,53 @@ export default { ...BIcons }, computed: { - ...mapGetters(["inventory_items", "loaded_items", "identityIdByHandle", "groupIdByHandle"]), - ...mapState(["user", "storage_locations"]), + ...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. + 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)) + }, + 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). + 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. + 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)}` + } }, methods: { - ...mapActions(["fetchInventoryItems", "deleteInventoryItem", "fetchStorageLocations", "fetchIdMap"]), - // Always the viewer's own items (fetchInventoryItems has no group filter); the owner - // handle is always included so /inventory/:handle/:id has exactly one shape. + ...mapActions(["fetchInventoryItems", "fetchGroupInventoryItems", "fetchFriendInventoryItems", + "deleteInventoryItem", "fetchStorageLocations", "fetchIdMap", "fetchGroups", + "fetchGroupMemberships", "fetchFriends"]), + fetchItemsForOwner() { + if (this.selectedOwner === this.user) return this.fetchInventoryItems() + if (this.isGroupSelected) return this.fetchGroupInventoryItems({groupHandle: this.selectedOwner}) + return this.fetchFriendInventoryItems({friendHandle: this.selectedOwner}) + }, + tryDeleteItem(item) { + this.deleteInventoryItem(item).then(() => { + 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. itemRoute(item) { - return `/inventory/${encodeHandleForUrl(this.user)}/${item.id}` + return `/inventory/${encodeHandleForUrl(item.owner_group || item.owner)}/${item.id}` }, only_images(files) { return files.filter(file => file.mime_type.startsWith("image/")); @@ -156,10 +209,22 @@ export default { return {path: '/print', query: {kind: 'item', userHandle, item: item.id}} }, }, + watch: { + selectedOwner() { + 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() { - await this.fetchInventoryItems() await this.fetchStorageLocations() await this.fetchIdMap() + await this.fetchGroups() + await this.fetchGroupMemberships() + await this.fetchFriends() } } diff --git a/frontend/src/views/StorageLocation.vue b/frontend/src/views/StorageLocation.vue index 0c2c2af..2e9869e 100644 --- a/frontend/src/views/StorageLocation.vue +++ b/frontend/src/views/StorageLocation.vue @@ -3,11 +3,22 @@

Storage Locations

+
+
+ + +
+
-
{{ user }}'s Storage Locations
+
{{ selectedOwner }}'s Storage Locations
@@ -25,7 +36,7 @@ - + {{ location.name }} @@ -40,7 +51,7 @@ - + @@ -55,7 +66,7 @@
-
@@ -73,7 +84,7 @@
Edit @@ -95,8 +106,8 @@
- - Add + + Add
@@ -116,6 +127,7 @@ export default { data() { return { layout: "grid", + selectedOwner: null, } }, components: { @@ -123,14 +135,37 @@ export default { ...BIcons }, computed: { - ...mapGetters(["identityIdByHandle", "groupIdByHandle"]), - ...mapState(["user", "storage_locations"]), + ...mapGetters(["identityIdByHandle", "groupIdByHandle", "groupStorageLocations"]), + ...mapState(["user", "storage_locations", "groups", "groupMemberships"]), + // Groups hosted here plus groups only known via a GroupMembership pointer - see + // Groups.vue's allGroups and Inventory.vue's ownerGroups 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)) + }, + locations() { + return this.selectedOwner === this.user ? this.storage_locations : this.groupStorageLocations(this.selectedOwner) + }, + addLocationRoute() { + return this.selectedOwner === this.user + ? '/storage-locations/new' + : `/storage-locations/new?group=${encodeHandleForUrl(this.selectedOwner)}` + } }, methods: { - ...mapActions(["fetchStorageLocations", "deleteStorageLocation", "fetchIdMap"]), - // This list is always the caller's own personal locations (fetchStorageLocations has no - // group content), so the owner_group branches below are currently unreachable here - kept - // for parity with Inventory.vue's shortIdLink/printLinkFor in case that ever changes. + ...mapActions(["fetchStorageLocations", "fetchGroupStorageLocations", "deleteStorageLocation", + "fetchIdMap", "fetchGroups", "fetchGroupMemberships"]), + fetchLocationsForOwner() { + return this.selectedOwner === this.user + ? this.fetchStorageLocations() + : this.fetchGroupStorageLocations({groupHandle: this.selectedOwner}) + }, + tryDeleteLocation(location) { + this.deleteStorageLocation(location).then(() => { + this.fetchLocationsForOwner() + }) + }, locationRoute(location) { return `/storage-locations/${encodeHandleForUrl(location.owner_group || location.owner)}/${location.id}` }, @@ -153,9 +188,20 @@ export default { return {path: '/print', query: {kind: 'storage-location', userHandle, location: location.id}} }, }, + watch: { + selectedOwner() { + this.fetchLocationsForOwner() + } + }, + created() { + // Set before the first render so addLocationRoute/locations never see selectedOwner=null + // while user is already populated (which would misroute to the group branch). + this.selectedOwner = this.user + }, async mounted() { - await this.fetchStorageLocations() await this.fetchIdMap() + await this.fetchGroups() + await this.fetchGroupMemberships() } }