stash
This commit is contained in:
parent
ebd0db4b43
commit
2bb6624d50
4 changed files with 156 additions and 216 deletions
|
|
@ -619,12 +619,16 @@ export default createStore({
|
||||||
const {domain} = splitGroupHandle(groupHandle)
|
const {domain} = splitGroupHandle(groupHandle)
|
||||||
const servers = await dispatch('getFriendServers', {username: 'x@' + domain})
|
const servers = await dispatch('getFriendServers', {username: 'x@' + domain})
|
||||||
const items = await servers.get(getters.signAuth, '/api/v1/inventory_items/' + encodeHandleForUrl(groupHandle) + '/')
|
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})
|
commit('setInventoryItems', {url: '/' + groupHandle, items})
|
||||||
return 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}) {
|
async fetchFiles({state, commit, dispatch, getters}) {
|
||||||
if (state.last_load.files > Date.now() - 1000 * 60 * 60 * 24) {
|
if (state.last_load.files > Date.now() - 1000 * 60 * 60 * 24) {
|
||||||
return state.files
|
return state.files
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
<h1 class="h3 mb-3">{{ group ? group.handle : '' }}</h1>
|
<h1 class="h3 mb-3">{{ group ? group.handle : '' }}</h1>
|
||||||
<div class="row" v-if="group">
|
<div class="row" v-if="group">
|
||||||
<div class="col-12 col-xl-6">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title">Members</h5>
|
<h5 class="card-title">Members</h5>
|
||||||
|
|
@ -50,136 +50,6 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-xl-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">Group 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>
|
|
||||||
<table class="table table-striped" v-if="layout === 'table'">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width:40%;">Name</th>
|
|
||||||
<th style="width:25%">Availability Policy</th>
|
|
||||||
<th class="d-none d-md-table-cell" style="width:25%">Amount</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="item in items" :key="item.id">
|
|
||||||
<td>
|
|
||||||
<router-link :to="itemRoute(item)">{{ item.name }}</router-link>
|
|
||||||
</td>
|
|
||||||
<td class="d-none d-md-table-cell">
|
|
||||||
<span class="badge bg-secondary text-white">{{ item.availability_policy }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
|
|
||||||
<td class="table-action">
|
|
||||||
<router-link :to="`${itemRoute(item)}/edit`">
|
|
||||||
<b-icon-pencil-square></b-icon-pencil-square>
|
|
||||||
</router-link>
|
|
||||||
<a :href="`${itemRoute(item)}/delete`" @click.prevent="tryDeleteItem(item)">
|
|
||||||
<b-icon-trash></b-icon-trash>
|
|
||||||
</a>
|
|
||||||
<router-link v-if="printLinkFor(item)" :to="printLinkFor(item)">
|
|
||||||
<b-icon-qr-code></b-icon-qr-code>
|
|
||||||
</router-link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="card-body" v-else>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-md-4 col-lg-3 col-xl-2" v-for="item in items" :key="item.id">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title mb-0">
|
|
||||||
<router-link :to="itemRoute(item)">
|
|
||||||
{{ item.name }}
|
|
||||||
</router-link>
|
|
||||||
</h5>
|
|
||||||
<div class="card-text text-black-50">
|
|
||||||
<span class="badge bg-secondary text-white">{{ item.availability_policy }}</span>
|
|
||||||
<span class="float-right">{{ item.owned_quantity }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn btn-danger btn-sm" @click="tryDeleteItem(item)">
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
<router-link :to="`${itemRoute(item)}/edit`"
|
|
||||||
class="btn btn-primary btn-sm">Edit
|
|
||||||
</router-link>
|
|
||||||
<router-link v-if="printLinkFor(item)" :to="printLinkFor(item)"
|
|
||||||
class="btn btn-secondary btn-sm">
|
|
||||||
<b-icon-qr-code></b-icon-qr-code>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<button class="btn" @click="fetchItems">Refresh</button>
|
|
||||||
<router-link :to="`/inventory/new?group=${encodeHandleForUrl(group.handle)}`"
|
|
||||||
class="btn btn-primary">Add
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-xl-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">Group storage locations</h5>
|
|
||||||
</div>
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width:40%;">Name</th>
|
|
||||||
<th style="width:25%">Path</th>
|
|
||||||
<th class="d-none d-md-table-cell" style="width:25%">Category</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="location in storageLocations" :key="location.id">
|
|
||||||
<td>
|
|
||||||
<router-link :to="locationRoute(location)">{{ location.name }}</router-link>
|
|
||||||
</td>
|
|
||||||
<td class="d-none d-md-table-cell">
|
|
||||||
<span class="text-muted">{{ location.path }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="d-none d-md-table-cell">
|
|
||||||
<span class="badge bg-info text-white" v-if="location.category">{{ location.category }}</span>
|
|
||||||
<span class="text-muted" v-else>-</span>
|
|
||||||
</td>
|
|
||||||
<td class="table-action">
|
|
||||||
<router-link :to="`${locationRoute(location)}/edit`">
|
|
||||||
<b-icon-pencil-square></b-icon-pencil-square>
|
|
||||||
</router-link>
|
|
||||||
<a :href="`${locationRoute(location)}/delete`" @click.prevent="tryDeleteLocation(location)">
|
|
||||||
<b-icon-trash></b-icon-trash>
|
|
||||||
</a>
|
|
||||||
<router-link v-if="locationPrintLinkFor(location)" :to="locationPrintLinkFor(location)">
|
|
||||||
<b-icon-qr-code></b-icon-qr-code>
|
|
||||||
</router-link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<button class="btn" @click="fetchLocations">Refresh</button>
|
|
||||||
<router-link :to="`/storage-locations/new?group=${encodeHandleForUrl(group.handle)}`"
|
|
||||||
class="btn btn-primary">Add
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -187,11 +57,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapActions, mapGetters} from "vuex";
|
import {mapActions} from "vuex";
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
import BaseLayout from "@/components/BaseLayout.vue";
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
import UserNameTag from "@/components/UserNameTag.vue";
|
import UserNameTag from "@/components/UserNameTag.vue";
|
||||||
import {decodeHandleFromUrl, encodeHandleForUrl} from "@/router";
|
import {decodeHandleFromUrl} from "@/router";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GroupDetail',
|
name: 'GroupDetail',
|
||||||
|
|
@ -209,7 +79,6 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
group: null,
|
group: null,
|
||||||
layout: "grid",
|
|
||||||
show_invite: false,
|
show_invite: false,
|
||||||
invitee: ""
|
invitee: ""
|
||||||
}
|
}
|
||||||
|
|
@ -217,19 +86,10 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
decodedHandle() {
|
decodedHandle() {
|
||||||
return decodeHandleFromUrl(this.handle)
|
return decodeHandleFromUrl(this.handle)
|
||||||
},
|
}
|
||||||
items() {
|
|
||||||
return this.group ? this.groupInventoryItems(this.group.handle) : []
|
|
||||||
},
|
|
||||||
storageLocations() {
|
|
||||||
return this.group ? this.groupStorageLocations(this.group.handle) : []
|
|
||||||
},
|
|
||||||
...mapGetters(['groupInventoryItems', 'groupStorageLocations'])
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
encodeHandleForUrl,
|
...mapActions(['fetchGroup', 'removeGroupMember', 'inviteToGroup']),
|
||||||
...mapActions(['fetchGroup', 'removeGroupMember', 'inviteToGroup', 'fetchGroupInventoryItems',
|
|
||||||
'deleteInventoryItem', 'fetchGroupStorageLocations', 'deleteStorageLocation']),
|
|
||||||
// Loads by handle regardless of which backend actually hosts the group - see
|
// Loads by handle regardless of which backend actually hosts the group - see
|
||||||
// store.js's fetchGroup.
|
// store.js's fetchGroup.
|
||||||
refresh() {
|
refresh() {
|
||||||
|
|
@ -237,33 +97,6 @@ export default {
|
||||||
this.group = group
|
this.group = group
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchItems() {
|
|
||||||
if (this.group) {
|
|
||||||
this.fetchGroupInventoryItems({groupHandle: this.group.handle})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemRoute(item) {
|
|
||||||
return `/inventory/${encodeHandleForUrl(this.group.handle)}/${item.id}`
|
|
||||||
},
|
|
||||||
printLinkFor(item) {
|
|
||||||
return {path: '/print', query: {kind: 'item', userHandle: this.group.handle, item: item.id}}
|
|
||||||
},
|
|
||||||
fetchLocations() {
|
|
||||||
if (this.group) {
|
|
||||||
this.fetchGroupStorageLocations({groupHandle: this.group.handle})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
locationRoute(location) {
|
|
||||||
return `/storage-locations/${encodeHandleForUrl(this.group.handle)}/${location.id}`
|
|
||||||
},
|
|
||||||
locationPrintLinkFor(location) {
|
|
||||||
return {path: '/print', query: {kind: 'storage-location', userHandle: this.group.handle, location: location.id}}
|
|
||||||
},
|
|
||||||
tryDeleteLocation(location) {
|
|
||||||
this.deleteStorageLocation(location).then(() => {
|
|
||||||
this.fetchLocations()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
tryInvite() {
|
tryInvite() {
|
||||||
this.inviteToGroup({groupHandle: this.group.handle, invitee: this.invitee}).then((ok) => {
|
this.inviteToGroup({groupHandle: this.group.handle, invitee: this.invitee}).then((ok) => {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
|
@ -279,18 +112,10 @@ export default {
|
||||||
this.refresh()
|
this.refresh()
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
})
|
})
|
||||||
},
|
|
||||||
tryDeleteItem(item) {
|
|
||||||
this.deleteInventoryItem(item).then(() => {
|
|
||||||
this.fetchItems()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.refresh().then(() => {
|
this.refresh()
|
||||||
this.fetchItems()
|
|
||||||
this.fetchLocations()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,26 @@
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
<h1 class="h3 mb-3">Inventory Own & Friends"</h1>
|
<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="row">
|
||||||
<div class="col-12 col-xl-12">
|
<div class="col-12 col-xl-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title">{{ user }}'s Inventory</h5>
|
<h5 class="card-title">{{ selectedOwner }}'s Inventory</h5>
|
||||||
<button v-if="layout === 'grid'" @click="layout = 'table'" class="btn">
|
<button v-if="layout === 'grid'" @click="layout = 'table'" class="btn">
|
||||||
<b-icon-list></b-icon-list>
|
<b-icon-list></b-icon-list>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -25,7 +39,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="item in inventory_items" :key="item.id">
|
<tr v-for="item in items" :key="item.id">
|
||||||
<td>
|
<td>
|
||||||
<router-link :to="itemRoute(item)">{{ item.name }}</router-link>
|
<router-link :to="itemRoute(item)">{{ item.name }}</router-link>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -34,10 +48,10 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
|
<td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
|
||||||
<td class="table-action">
|
<td class="table-action">
|
||||||
<router-link :to="`${itemRoute(item)}/edit`">
|
<router-link v-if="canEdit" :to="`${itemRoute(item)}/edit`">
|
||||||
<b-icon-pencil-square></b-icon-pencil-square>
|
<b-icon-pencil-square></b-icon-pencil-square>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a :href="`${itemRoute(item)}/delete`" @click.prevent="deleteInventoryItem(item)">
|
<a v-if="canEdit" :href="`${itemRoute(item)}/delete`" @click.prevent="tryDeleteItem(item)">
|
||||||
<b-icon-trash></b-icon-trash>
|
<b-icon-trash></b-icon-trash>
|
||||||
</a>
|
</a>
|
||||||
<router-link v-if="shortIdLink(item)" :to="shortIdLink(item)">
|
<router-link v-if="shortIdLink(item)" :to="shortIdLink(item)">
|
||||||
|
|
@ -52,7 +66,7 @@
|
||||||
</table>
|
</table>
|
||||||
<div class="card-body" v-else>
|
<div class="card-body" v-else>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-4 col-lg-3 col-xl-2" v-for="item in inventory_items"
|
<div class="col-12 col-md-4 col-lg-3 col-xl-2" v-for="item in items"
|
||||||
:key="item.id">
|
:key="item.id">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<img :src="item.image" class="" :alt="item.image">
|
<img :src="item.image" class="" :alt="item.image">
|
||||||
|
|
@ -70,10 +84,10 @@
|
||||||
<span class="float-right">{{ item.owned_quantity }}</span>
|
<span class="float-right">{{ item.owned_quantity }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-danger btn-sm"
|
<button v-if="canEdit" class="btn btn-danger btn-sm"
|
||||||
@click="deleteInventoryItem(item)">Delete
|
@click="tryDeleteItem(item)">Delete
|
||||||
</button>
|
</button>
|
||||||
<router-link :to="`${itemRoute(item)}/edit`"
|
<router-link v-if="canEdit" :to="`${itemRoute(item)}/edit`"
|
||||||
class="btn btn-primary btn-sm">Edit
|
class="btn btn-primary btn-sm">Edit
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link v-if="shortIdLink(item)" :to="shortIdLink(item)"
|
<router-link v-if="shortIdLink(item)" :to="shortIdLink(item)"
|
||||||
|
|
@ -93,8 +107,8 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<button class="btn" @click="fetchInventoryItems">Refresh</button>
|
<button class="btn" @click="fetchItemsForOwner">Refresh</button>
|
||||||
<router-link to="/inventory/new" class="btn btn-primary">Add</router-link>
|
<router-link v-if="canEdit" :to="addItemRoute" class="btn btn-primary">Add</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -115,6 +129,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
layout: "grid",
|
layout: "grid",
|
||||||
|
selectedOwner: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -123,15 +138,53 @@ export default {
|
||||||
...BIcons
|
...BIcons
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(["inventory_items", "loaded_items", "identityIdByHandle", "groupIdByHandle"]),
|
...mapGetters(["inventory_items", "groupInventoryItems", "loaded_items", "identityIdByHandle", "groupIdByHandle"]),
|
||||||
...mapState(["user", "storage_locations"]),
|
...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: {
|
methods: {
|
||||||
...mapActions(["fetchInventoryItems", "deleteInventoryItem", "fetchStorageLocations", "fetchIdMap"]),
|
...mapActions(["fetchInventoryItems", "fetchGroupInventoryItems", "fetchFriendInventoryItems",
|
||||||
// Always the viewer's own items (fetchInventoryItems has no group filter); the owner
|
"deleteInventoryItem", "fetchStorageLocations", "fetchIdMap", "fetchGroups",
|
||||||
// handle is always included so /inventory/:handle/:id has exactly one shape.
|
"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) {
|
itemRoute(item) {
|
||||||
return `/inventory/${encodeHandleForUrl(this.user)}/${item.id}`
|
return `/inventory/${encodeHandleForUrl(item.owner_group || item.owner)}/${item.id}`
|
||||||
},
|
},
|
||||||
only_images(files) {
|
only_images(files) {
|
||||||
return files.filter(file => file.mime_type.startsWith("image/"));
|
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}}
|
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() {
|
async mounted() {
|
||||||
await this.fetchInventoryItems()
|
|
||||||
await this.fetchStorageLocations()
|
await this.fetchStorageLocations()
|
||||||
await this.fetchIdMap()
|
await this.fetchIdMap()
|
||||||
|
await this.fetchGroups()
|
||||||
|
await this.fetchGroupMemberships()
|
||||||
|
await this.fetchFriends()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,22 @@
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
<h1 class="h3 mb-3">Storage Locations</h1>
|
<h1 class="h3 mb-3">Storage Locations</h1>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="ownerSelect" class="form-label">Viewing storage locations 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>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-xl-12">
|
<div class="col-12 col-xl-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title">{{ user }}'s Storage Locations</h5>
|
<h5 class="card-title">{{ selectedOwner }}'s Storage Locations</h5>
|
||||||
<button v-if="layout === 'grid'" @click="layout = 'table'" class="btn">
|
<button v-if="layout === 'grid'" @click="layout = 'table'" class="btn">
|
||||||
<b-icon-list></b-icon-list>
|
<b-icon-list></b-icon-list>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -25,7 +36,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="location in storage_locations" :key="location.id">
|
<tr v-for="location in locations" :key="location.id">
|
||||||
<td>
|
<td>
|
||||||
<router-link :to="locationRoute(location)">{{ location.name }}</router-link>
|
<router-link :to="locationRoute(location)">{{ location.name }}</router-link>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -40,7 +51,7 @@
|
||||||
<router-link :to="`${locationRoute(location)}/edit`">
|
<router-link :to="`${locationRoute(location)}/edit`">
|
||||||
<b-icon-pencil-square></b-icon-pencil-square>
|
<b-icon-pencil-square></b-icon-pencil-square>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a :href="`${locationRoute(location)}/delete`" @click.prevent="deleteStorageLocation(location)">
|
<a :href="`${locationRoute(location)}/delete`" @click.prevent="tryDeleteLocation(location)">
|
||||||
<b-icon-trash></b-icon-trash>
|
<b-icon-trash></b-icon-trash>
|
||||||
</a>
|
</a>
|
||||||
<router-link v-if="shortIdLink(location)" :to="shortIdLink(location)">
|
<router-link v-if="shortIdLink(location)" :to="shortIdLink(location)">
|
||||||
|
|
@ -55,7 +66,7 @@
|
||||||
</table>
|
</table>
|
||||||
<div class="card-body" v-else>
|
<div class="card-body" v-else>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-4 col-lg-3 col-xl-2" v-for="location in storage_locations"
|
<div class="col-12 col-md-4 col-lg-3 col-xl-2" v-for="location in locations"
|
||||||
:key="location.id">
|
:key="location.id">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
@ -73,7 +84,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group mt-2">
|
<div class="btn-group mt-2">
|
||||||
<button class="btn btn-danger btn-sm"
|
<button class="btn btn-danger btn-sm"
|
||||||
@click="deleteStorageLocation(location)">Delete
|
@click="tryDeleteLocation(location)">Delete
|
||||||
</button>
|
</button>
|
||||||
<router-link :to="`${locationRoute(location)}/edit`"
|
<router-link :to="`${locationRoute(location)}/edit`"
|
||||||
class="btn btn-primary btn-sm">Edit
|
class="btn btn-primary btn-sm">Edit
|
||||||
|
|
@ -95,8 +106,8 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<button class="btn" @click="fetchStorageLocations">Refresh</button>
|
<button class="btn" @click="fetchLocationsForOwner">Refresh</button>
|
||||||
<router-link to="/storage-locations/new" class="btn btn-primary">Add</router-link>
|
<router-link :to="addLocationRoute" class="btn btn-primary">Add</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -116,6 +127,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
layout: "grid",
|
layout: "grid",
|
||||||
|
selectedOwner: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -123,14 +135,37 @@ export default {
|
||||||
...BIcons
|
...BIcons
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(["identityIdByHandle", "groupIdByHandle"]),
|
...mapGetters(["identityIdByHandle", "groupIdByHandle", "groupStorageLocations"]),
|
||||||
...mapState(["user", "storage_locations"]),
|
...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: {
|
methods: {
|
||||||
...mapActions(["fetchStorageLocations", "deleteStorageLocation", "fetchIdMap"]),
|
...mapActions(["fetchStorageLocations", "fetchGroupStorageLocations", "deleteStorageLocation",
|
||||||
// This list is always the caller's own personal locations (fetchStorageLocations has no
|
"fetchIdMap", "fetchGroups", "fetchGroupMemberships"]),
|
||||||
// group content), so the owner_group branches below are currently unreachable here - kept
|
fetchLocationsForOwner() {
|
||||||
// for parity with Inventory.vue's shortIdLink/printLinkFor in case that ever changes.
|
return this.selectedOwner === this.user
|
||||||
|
? this.fetchStorageLocations()
|
||||||
|
: this.fetchGroupStorageLocations({groupHandle: this.selectedOwner})
|
||||||
|
},
|
||||||
|
tryDeleteLocation(location) {
|
||||||
|
this.deleteStorageLocation(location).then(() => {
|
||||||
|
this.fetchLocationsForOwner()
|
||||||
|
})
|
||||||
|
},
|
||||||
locationRoute(location) {
|
locationRoute(location) {
|
||||||
return `/storage-locations/${encodeHandleForUrl(location.owner_group || location.owner)}/${location.id}`
|
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}}
|
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() {
|
async mounted() {
|
||||||
await this.fetchStorageLocations()
|
|
||||||
await this.fetchIdMap()
|
await this.fetchIdMap()
|
||||||
|
await this.fetchGroups()
|
||||||
|
await this.fetchGroupMemberships()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue