This commit is contained in:
j3d1 2026-09-06 01:51:19 +02:00
parent 240a508306
commit 7135fce421
18 changed files with 288 additions and 112 deletions

View file

@ -24,10 +24,10 @@
<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 = 'masonry'" class="btn">
<b-icon-list></b-icon-list>
<b-icon-columns-gap></b-icon-columns-gap>
</button>
<button v-else-if="layout === 'masonry'" @click="layout = 'table'" class="btn">
<b-icon-columns-gap></b-icon-columns-gap>
<b-icon-list></b-icon-list>
</button>
<button v-else @click="layout = 'grid'" class="btn">
<b-icon-grid></b-icon-grid>
@ -311,6 +311,10 @@ export default {
object-fit: cover;
}
.table-action {
white-space: nowrap;
}
.masonry-grid {
column-count: 2;
column-gap: 1rem;

View file

@ -20,7 +20,10 @@
<div class="btn-group">
<button class="btn" @click="fetchLocationsForOwner">Refresh</button>
<router-link :to="addLocationRoute" class="btn btn-primary">Add</router-link>
<button v-if="layout === 'grid'" @click="layout = 'table'" class="btn">
<button v-if="layout === 'grid'" @click="layout = 'tree'" class="btn">
<b-icon-list-nested></b-icon-list-nested>
</button>
<button v-else-if="layout === 'tree'" @click="layout = 'table'" class="btn">
<b-icon-list></b-icon-list>
</button>
<button v-else @click="layout = 'grid'" class="btn">
@ -73,6 +76,40 @@
</tr>
</tbody>
</table>
<div class="card-body" v-else-if="layout === 'tree'">
<TreeView :items="locationTree">
<template #default="{item: location}">
<div class="tree-location-row">
<router-link :to="locationRoute(location)" class="tree-location-name text-truncate">
{{ location.name }}
</router-link>
<span class="tree-location-category">
<span class="badge bg-info text-white" v-if="location.category">{{ location.category }}</span>
<span class="text-muted" v-else>-</span>
</span>
<span class="tree-location-visibility">
<span class="badge bg-secondary text-white">{{ location.visibility_policy }}</span>
</span>
<div class="tree-location-actions table-action">
<router-link :to="locationEditRoute(location)" class="me-1" title="Edit">
<b-icon-pencil-square></b-icon-pencil-square>
</router-link>
<guarded-button tag="a" class="me-1" title="Delete"
:confirm-message='`Are you sure you want to delete "${location.name}"? This cannot be undone.`'
@confirm="tryDeleteLocation(location)">
<b-icon-trash></b-icon-trash>
</guarded-button>
<router-link v-if="shortIdLink(location)" :to="shortIdLink(location)" class="me-1">
<b-icon-link></b-icon-link>
</router-link>
<router-link v-if="printLinkFor(location)" :to="printLinkFor(location)" title="Print Label">
<b-icon-qr-code></b-icon-qr-code>
</router-link>
</div>
</div>
</template>
</TreeView>
</div>
<div class="card-body" v-else>
<div class="row">
<div class="col-12 col-md-4 col-lg-3 col-xl-2 d-flex" v-for="location in locations"
@ -131,6 +168,7 @@ import {mapActions, mapGetters, mapState} from "vuex";
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import GuardedButton from "@/components/GuardedButton.vue";
import TreeView from "@/components/TreeView.vue";
import {shortenedRoute, encodeHandleForUrl, decodeHandleFromUrl} from "@/router";
export default {
@ -145,6 +183,7 @@ export default {
components: {
BaseLayout,
GuardedButton,
TreeView,
...BIcons
},
computed: {
@ -178,6 +217,21 @@ export default {
locations() {
return this.selectedOwner === this.user ? this.storage_locations : this.groupStorageLocations(this.selectedOwner)
},
// See docs/implementation.md#category-tree-reconstruction-from-flat-path-strings.
locationTree() {
const nodesById = {}
for (const location of this.locations) {
nodesById[location.id] = {...location, children: []}
}
const roots = []
for (const location of [...this.locations].sort((a, b) => a.name.localeCompare(b.name))) {
const node = nodesById[location.id]
// Falls back to root if the parent isn't in this owner's own location list.
const parent = location.parent != null ? nodesById[location.parent] : null
;(parent ? parent.children : roots).push(node)
}
return roots
},
addLocationRoute() {
const params = this.selectedOwner === this.user ? {} : {group: encodeHandleForUrl(this.selectedOwner)}
return {name: 'locations-new', params}
@ -240,6 +294,29 @@ export default {
font-size: 0.8rem;
}
.tree-location-row {
display: grid;
/* Category/Visibility match the table's 20% th widths; actions is max-content so it never wraps. */
grid-template-columns: minmax(0, 1fr) 20% 20% auto;
align-items: center;
column-gap: 0.5rem;
width: 100%;
}
.tree-location-name {
min-width: 0;
}
.tree-location-actions {
display: flex;
align-items: center;
}
/* The th widths above leave Actions only the leftover 5% - without this it wraps mid-icon. */
.table-action {
white-space: nowrap;
}
.btn-group.mt-auto {
width: 100%;
}