This commit is contained in:
j3d1 2026-09-04 14:41:26 +02:00
parent 3cd9200d87
commit e88dc898b3
18 changed files with 298 additions and 84 deletions

View file

@ -9,7 +9,7 @@
<SearchBox/>
<div class="navbar-collapse collapse">
<ul class="navbar-nav navbar-align">
<router-link to="/scan?navigate=immediate" class="nav-icon">
<router-link :to="{name: 'scan', query: {navigate: 'immediate'}}" class="nav-icon">
<b-icon-qr-code-scan class="bi-valign-middle"></b-icon-qr-code-scan>
</router-link>
<Notifications :notifications="notifications"/>

View file

@ -1,7 +1,7 @@
<template>
<nav id="sidebar" class="sidebar">
<div class="sidebar-content">
<router-link to="/" class="sidebar-brand">
<router-link :to="{name: 'dashboard'}" class="sidebar-brand">
<img src="/src/assets/icons/toolshed-48x48.png" alt="Toolshed Logo" class="align-middle logo mr-2 h-75">
<span class="align-middle">Toolshed</span>
</router-link>
@ -10,61 +10,61 @@
Tools & Components
</li>
<li class="sidebar-item">
<router-link to="/inventory" class="sidebar-link">
<router-link :to="{name: 'inventory'}" class="sidebar-link">
<b-icon-archive class="bi-valign-middle"></b-icon-archive>
<span class="align-middle">Inventory</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/storage-location" class="sidebar-link">
<router-link :to="{name: 'locations'}" class="sidebar-link">
<b-icon-boxes class="bi-valign-middle"></b-icon-boxes>
<span class="align-middle">Storage Locations</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/workflows" class="sidebar-link">
<router-link :to="{name: 'workflows'}" class="sidebar-link">
<b-icon-diagram3 class="bi-valign-middle"></b-icon-diagram3>
<span class="align-middle">Workflows</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/friends" class="sidebar-link">
<router-link :to="{name: 'friends'}" class="sidebar-link">
<b-icon-people class="bi-valign-middle"></b-icon-people>
<span class="align-middle">Friends</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/groups" class="sidebar-link">
<router-link :to="{name: 'groups'}" class="sidebar-link">
<b-icon-people-fill class="bi-valign-middle"></b-icon-people-fill>
<span class="align-middle">Groups</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/files" class="sidebar-link">
<router-link :to="{name: 'files'}" class="sidebar-link">
<b-icon-folder class="bi-valign-middle"></b-icon-folder>
<span class="align-middle">Files</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/admin" class="sidebar-link">
<router-link :to="{name: 'admin'}" class="sidebar-link">
<b-icon-gear class="bi-valign-middle"></b-icon-gear>
<span class="align-middle">Admin</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/swatch" class="sidebar-link">
<router-link :to="{name: 'swatch'}" class="sidebar-link">
<b-icon-gear class="bi-valign-middle"></b-icon-gear>
<span class="align-middle">Swatch</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/print" class="sidebar-link">
<router-link :to="{name: 'print'}" class="sidebar-link">
<b-icon-gear class="bi-valign-middle"></b-icon-gear>
<span class="align-middle">Print</span>
</router-link>
</li>
<li class="sidebar-item">
<router-link to="/scan" class="sidebar-link">
<router-link :to="{name: 'scan'}" class="sidebar-link">
<b-icon-qr-code-scan class="bi-valign-middle"></b-icon-qr-code-scan>
<span class="align-middle">Scan</span>
</router-link>

View file

@ -18,11 +18,11 @@
</span>
</div>
<div class="dropdown-divider"></div>
<router-link to="/profile" class="dropdown-item">
<router-link :to="{name: 'profile'}" class="dropdown-item">
<b-icon-person class="bi-valign-middle mr-1"></b-icon-person>
Profile
</router-link>
<router-link to="/settings" class="dropdown-item">
<router-link :to="{name: 'settings'}" class="dropdown-item">
<b-icon-sliders class="bi-valign-middle mr-1"></b-icon-sliders>
Settings &
Privacy

View file

@ -51,35 +51,39 @@ export function decodeHandleFromUrl(segment) {
// See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
export function ownerOverviewRoute(item) {
return item.owner_group ? `/inventory/${encodeHandleForUrl(item.owner_group)}` : '/inventory';
return item.owner_group
? {name: 'inventory', params: {owner: encodeHandleForUrl(item.owner_group)}}
: {name: 'inventory'};
}
export function ownerLocationOverviewRoute(location) {
return location.owner_group ? `/storage-location/${encodeHandleForUrl(location.owner_group)}` : '/storage-location';
return location.owner_group
? {name: 'locations', params: {owner: encodeHandleForUrl(location.owner_group)}}
: {name: 'locations'};
}
const EXPANDED_ROUTE_BUILDERS = {
item: ({owner_identity_id, item_local_id}) => {
const handle = store.getters.identityHandleById[owner_identity_id];
return handle ? `/inventory/${encodeHandleForUrl(handle)}/${item_local_id}` : null;
return handle ? {name: 'inventory-detail', params: {handle: encodeHandleForUrl(handle), id: item_local_id}} : null;
},
group_item: ({owner_group_id, item_local_id}) => {
const handle = store.getters.groupHandleById[owner_group_id];
return handle ? `/inventory/${encodeHandleForUrl(handle)}/${item_local_id}` : null;
return handle ? {name: 'inventory-detail', params: {handle: encodeHandleForUrl(handle), id: item_local_id}} : null;
},
group: ({group_id}) => {
const handle = store.getters.groupHandleById[group_id];
return handle ? `/groups/${encodeHandleForUrl(handle)}` : null;
return handle ? {name: 'group-detail', params: {handle: encodeHandleForUrl(handle)}} : null;
},
storage_location: ({owner_identity_id, storage_location_id}) => {
const handle = store.getters.identityHandleById[owner_identity_id];
return handle ? `/storage-locations/${encodeHandleForUrl(handle)}/${storage_location_id}` : null;
return handle ? {name: 'locations-detail', params: {handle: encodeHandleForUrl(handle), id: storage_location_id}} : null;
},
group_storage_location: ({owner_group_id, storage_location_id}) => {
const handle = store.getters.groupHandleById[owner_group_id];
return handle ? `/storage-locations/${encodeHandleForUrl(handle)}/${storage_location_id}` : null;
return handle ? {name: 'locations-detail', params: {handle: encodeHandleForUrl(handle), id: storage_location_id}} : null;
},
workflow: ({workflow_id}) => `/workflows/${workflow_id}`,
workflow: ({workflow_id}) => ({name: 'workflow-detail', params: {id: workflow_id}}),
};
// Kinds whose route needs identityHandleById/groupHandleById (from state.idmap); ShortId.vue only waits on an idmap fetch for these.
@ -121,30 +125,33 @@ export async function domainQualifiedRoute(domain, ints) {
return undefined;
}
return owned.isLocation
? `/storage-locations/${encodeHandleForUrl(resolved.handle)}/${resolved.id}`
: `/inventory/${encodeHandleForUrl(resolved.handle)}/${resolved.id}`;
? {name: 'locations-detail', params: {handle: encodeHandleForUrl(resolved.handle), id: resolved.id}}
: {name: 'inventory-detail', params: {handle: encodeHandleForUrl(resolved.handle), id: resolved.id}};
}
const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, {
const routes = [{path: '/', name: 'dashboard', component: Dashboard, meta: {requiresAuth: true}}, {
// See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
path: '/inventory/:owner?',
name: 'inventory',
component: Inventory,
meta: {requiresAuth: true},
props: true
}, {
path: '/inventory/:handle/:id',
name: 'inventory-detail',
component: InventoryDetail,
meta: {requiresAuth: true},
props: true
}, {
path: '/inventory/:handle/:id/edit',
name: 'inventory-edit',
component: InventoryEdit,
meta: {requiresAuth: true},
props: true
}, {
// Label/short-link entry point; alias of /inventory/:handle/:id. See docs/implementation.md#item-short-link-redirect-route.
path: '/i/:handle/:id',
redirect: to => `/inventory/${to.params.handle}/${to.params.id}`
redirect: to => ({name: 'inventory-detail', params: to.params})
}, {
// beforeEnter, not redirect: falls through to ShortId.vue when the route can't resolve synchronously. See docs/implementation.md#beforeenter-guard-vs-redirect-for-short_id.
path: '/:short_id',
@ -160,28 +167,34 @@ const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, {
}, {
// See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
path: '/inventory/new/:group?',
name: 'inventory-new',
component: InventoryNew,
meta: {requiresAuth: true},
props: true
}, {
path: '/friends',
name: 'friends',
component: Friends,
meta: {requiresAuth: true}
}, {
path: '/groups',
name: 'groups',
component: Groups,
meta: {requiresAuth: true}
}, {
path: '/groups/new',
name: 'groups-new',
component: GroupNew,
meta: {requiresAuth: true}
}, {
path: '/groups/:handle',
name: 'group-detail',
component: GroupDetail,
meta: {requiresAuth: true},
props: true
}, {path: '/files', component: Files, meta: {requiresAuth: true}
}, {path: '/files', name: 'files', component: Files, meta: {requiresAuth: true}
}, {path: '/workflows',
name: 'workflows',
component: Workflows,
meta: {requiresAuth: true},
}, {path: '/workflows/:id/:step?',
@ -190,10 +203,12 @@ const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, {
meta: {requiresAuth: true},
props: true
}, {path: '/admin',
name: 'admin',
component: Admin,
meta: {requiresAuth: true}
}, {path: '/swatch', component: Swatch, meta: {requiresAuth: true}}, {
}, {path: '/swatch', name: 'swatch', component: Swatch, meta: {requiresAuth: true}}, {
path: '/print',
name: 'print',
component: Print,
meta: {requiresAuth: true},
props: route => {
@ -204,23 +219,27 @@ const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, {
}
}, {
path: '/scan',
name: 'scan',
component: Scan,
meta: {requiresAuth: true},
}, {
path: '/search/:query',
name: 'search',
component: Search,
meta: {requiresAuth: true},
props: true
}, {path: '/login', component: Login, meta: {requiresAuth: false}}, {
}, {path: '/login', name: 'login', component: Login, meta: {requiresAuth: false}}, {
path: '/register',
name: 'register',
component: Register,
meta: {requiresAuth: false}
}, {path: '/pairing', component: Pairing, meta: {requiresAuth: false}}, {
}, {path: '/pairing', name: 'pairing', component: Pairing, meta: {requiresAuth: false}}, {
path: '/profile',
name: 'profile',
component: Profile,
meta: {requiresAuth: true}
}, {
path: '/settings', component: Settings, meta: {requiresAuth: true}, children: [{
path: '/settings', name: 'settings', component: Settings, meta: {requiresAuth: true}, children: [{
path: '', name: 'account', component: Account, meta: {requiresAuth: true}
},{
path: 'password/', name: 'password', component: Password, meta: {requiresAuth: true}
@ -237,27 +256,31 @@ const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, {
}]
}, {
// See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
path: '/storage-location/:owner?',
path: '/locations/:owner?',
name: 'locations',
component: StorageLocation,
meta: {requiresAuth: true},
props: true
}, {
path: '/storage-locations/:handle/:id',
path: '/locations/:handle/:id',
name: 'locations-detail',
component: StorageLocationDetail,
meta: {requiresAuth: true},
props: true
}, {
path: '/storage-locations/:handle/:id/edit',
path: '/locations/:handle/:id/edit',
name: 'locations-edit',
component: StorageLocationEdit,
meta: {requiresAuth: true},
props: true
}, {
// Label/short-link entry point for storage locations, the counterpart of /i/:handle/:id. See docs/implementation.md#item-short-link-redirect-route.
path: '/s/:handle/:id',
redirect: to => `/storage-locations/${to.params.handle}/${to.params.id}`
redirect: to => ({name: 'locations-detail', params: to.params})
}, {
// See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
path: '/storage-locations/new/:group?',
path: '/locations/new/:group?',
name: 'locations-new',
component: StorageLocationNew,
meta: {requiresAuth: true},
props: true

View file

@ -2,17 +2,130 @@
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">Dashboard</h1>
<div class="d-flex justify-content-between align-items-center flex-wrap mb-3">
<h1 class="h3 mb-0">Welcome back, {{ user }}</h1>
<div class="btn-group">
<router-link :to="{name: 'inventory-new'}" class="btn btn-primary">
<b-icon-plus-lg></b-icon-plus-lg>
Add item
</router-link>
<router-link :to="{name: 'locations-new'}" class="btn btn-outline-secondary">
<b-icon-plus-lg></b-icon-plus-lg>
Add storage location
</router-link>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="col-6 col-md-3">
<router-link :to="{name: 'inventory'}" class="text-decoration-none">
<div class="card stat-card">
<div class="card-body d-flex align-items-center">
<b-icon-boxes class="stat-icon me-3"></b-icon-boxes>
<div>
<div class="stat-value">{{ inventory_items.length }}</div>
<div class="stat-label">Items</div>
</div>
</div>
</div>
</router-link>
</div>
<div class="col-6 col-md-3">
<router-link :to="{name: 'locations'}" class="text-decoration-none">
<div class="card stat-card">
<div class="card-body d-flex align-items-center">
<b-icon-geo-alt class="stat-icon me-3"></b-icon-geo-alt>
<div>
<div class="stat-value">{{ storage_locations.length }}</div>
<div class="stat-label">Storage locations</div>
</div>
</div>
</div>
</router-link>
</div>
<div class="col-6 col-md-3">
<router-link :to="{name: 'friends'}" class="text-decoration-none">
<div class="card stat-card">
<div class="card-body d-flex align-items-center">
<b-icon-people class="stat-icon me-3"></b-icon-people>
<div>
<div class="stat-value">{{ friends.length }}</div>
<div class="stat-label">Friends</div>
</div>
</div>
</div>
</router-link>
</div>
<div class="col-6 col-md-3">
<router-link :to="{name: 'groups'}" class="text-decoration-none">
<div class="card stat-card">
<div class="card-body d-flex align-items-center">
<b-icon-diagram3 class="stat-icon me-3"></b-icon-diagram3>
<div>
<div class="stat-value">{{ ownerGroups.length }}</div>
<div class="stat-label">Groups</div>
</div>
</div>
</div>
</router-link>
</div>
</div>
<div class="row">
<div class="col-12 col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Empty card</h5>
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Recent items</h5>
<router-link :to="{name: 'inventory'}" class="small">View all</router-link>
</div>
<div class="card-body">
<div class="logo">
<img src="/src/assets/icons/toolshed-48x48.png" alt="Toolshed logo">
<div class="row" v-if="recentItems.length > 0">
<div class="col-6 col-md-4 col-xl-3" v-for="item in recentItems" :key="item.id">
<router-link :to="itemRoute(item)" class="text-decoration-none">
<div class="card">
<authenticated-image v-if="only_images(item.files).length > 0"
:src="only_images(item.files)[0].name"
:owner="only_images(item.files)[0].owner"
class="card-img-top img-preview"/>
<div class="card-body">
<h6 class="card-title mb-0 text-truncate">{{ item.name }}</h6>
<span class="text-black-50">{{ item.owned_quantity }}</span>
</div>
</div>
</router-link>
</div>
</div>
<p class="text-muted mb-0" v-else>
No items yet.
<router-link :to="{name: 'inventory-new'}">Add your first one</router-link>.
</p>
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Needs your attention</h5>
</div>
<ul class="list-group list-group-flush" v-if="hasPendingItems">
<router-link v-if="active_workflows.length > 0" :to="{name: 'workflows'}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
Workflows in progress
<span class="badge bg-primary rounded-pill">{{ active_workflows.length }}</span>
</router-link>
<router-link v-if="friendRequests.length > 0" :to="{name: 'friends'}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
Friend requests
<span class="badge bg-primary rounded-pill">{{ friendRequests.length }}</span>
</router-link>
<router-link v-if="groupInvites.length > 0" :to="{name: 'groups'}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
Group invites
<span class="badge bg-primary rounded-pill">{{ groupInvites.length }}</span>
</router-link>
</ul>
<div class="card-body" v-else>
<p class="text-muted mb-0">You're all caught up.</p>
</div>
</div>
</div>
@ -23,18 +136,92 @@
</template>
<script>
import {mapActions, mapGetters, mapState} from 'vuex';
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import {encodeHandleForUrl} from "@/router";
export default {
name: 'Dashboard',
components: {
...BIcons,
BaseLayout
BaseLayout,
AuthenticatedImage
},
data() {
return {
friendRequests: []
}
},
computed: {
...mapState(['user', 'storage_locations', 'friends', 'groups', 'groupMemberships', 'groupInvites',
'active_workflows']),
...mapGetters(['inventory_items']),
// Groups hosted here plus GroupMembership-only ones - see 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]
},
recentItems() {
return [...this.inventory_items].sort((a, b) => b.id - a.id).slice(0, 8)
},
hasPendingItems() {
return this.active_workflows.length > 0 || this.friendRequests.length > 0 || this.groupInvites.length > 0
}
},
methods: {
...mapActions(['fetchInventoryItems', 'fetchStorageLocations', 'fetchFriends', 'fetchGroups',
'fetchGroupMemberships', 'fetchGroupInvites', 'fetchActiveWorkflows', 'fetchFriendRequests']),
itemRoute(item) {
return {name: 'inventory-detail', params: {handle: encodeHandleForUrl(item.owner_group || item.owner), id: item.id}}
},
only_images(files) {
return files.filter(file => file.mime_type.startsWith("image/"));
},
},
async mounted() {
await this.fetchInventoryItems()
await this.fetchStorageLocations()
await this.fetchFriends()
await this.fetchGroups()
await this.fetchGroupMemberships()
await this.fetchGroupInvites()
await this.fetchActiveWorkflows()
this.friendRequests = await this.fetchFriendRequests()
}
}
</script>
<style scoped>
.stat-card {
transition: transform .15s ease-in-out;
}
</style>
.stat-card:hover {
transform: translateY(-2px);
}
.stat-icon {
font-size: 1.75rem;
color: var(--bs-primary);
}
.stat-value {
font-size: 1.25rem;
font-weight: 600;
line-height: 1.2;
}
.stat-label {
font-size: .8rem;
color: var(--bs-text-3);
}
.img-preview {
object-fit: cover;
height: 100px;
width: 100%;
}
</style>

View file

@ -19,7 +19,7 @@
<b-icon-arrow-clockwise></b-icon-arrow-clockwise>
Refresh
</a>
<router-link to="/groups/new" class="align-middle">
<router-link :to="{name: 'groups-new'}" class="align-middle">
<b-icon-plus></b-icon-plus>
New group
</router-link>
@ -29,7 +29,7 @@
<tbody>
<tr v-for="group in allGroups" :key="group.key">
<td>
<router-link :to="`/groups/${encodeHandleForUrl(group.handle)}`">
<router-link :to="{name: 'group-detail', params: {handle: encodeHandleForUrl(group.handle)}}">
{{ group.handle }}
</router-link>
</td>

View file

@ -55,10 +55,10 @@
</td>
<td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
<td class="table-action">
<router-link v-if="canEdit" :to="`${itemRoute(item)}/edit`">
<router-link v-if="canEdit" :to="itemEditRoute(item)">
<b-icon-pencil-square></b-icon-pencil-square>
</router-link>
<a v-if="canEdit" :href="`${itemRoute(item)}/delete`" @click.prevent="tryDeleteItem(item)">
<a v-if="canEdit" href="#" @click.prevent="tryDeleteItem(item)">
<b-icon-trash></b-icon-trash>
</a>
<router-link v-if="shortIdLink(item)" :to="shortIdLink(item)">
@ -95,7 +95,7 @@
<button v-if="canEdit" class="btn btn-danger btn-sm"
@click="tryDeleteItem(item)">Delete
</button>
<router-link v-if="canEdit" :to="`${itemRoute(item)}/edit`"
<router-link v-if="canEdit" :to="itemEditRoute(item)"
class="btn btn-primary btn-sm">Edit
</router-link>
<router-link v-if="shortIdLink(item)" :to="shortIdLink(item)"
@ -166,8 +166,8 @@ export default {
return this.owner ? decodeHandleFromUrl(this.owner) : this.user
},
set(value) {
const path = value === this.user ? '/inventory' : `/inventory/${encodeHandleForUrl(value)}`
this.$router.replace(path)
const params = value === this.user ? {} : {owner: encodeHandleForUrl(value)}
this.$router.replace({name: 'inventory', params})
}
},
isGroupSelected() {
@ -182,9 +182,8 @@ export default {
return this.selectedOwner === this.user ? this.inventory_items : this.groupInventoryItems(this.selectedOwner)
},
addItemRoute() {
return this.selectedOwner === this.user
? '/inventory/new'
: `/inventory/new/${encodeHandleForUrl(this.selectedOwner)}`
const params = this.selectedOwner === this.user ? {} : {group: encodeHandleForUrl(this.selectedOwner)}
return {name: 'inventory-new', params}
}
},
methods: {
@ -203,7 +202,10 @@ export default {
},
// 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}`
return {name: 'inventory-detail', params: {handle: encodeHandleForUrl(item.owner_group || item.owner), id: item.id}}
},
itemEditRoute(item) {
return {name: 'inventory-edit', params: {handle: encodeHandleForUrl(item.owner_group || item.owner), id: item.id}}
},
only_images(files) {
return files.filter(file => file.mime_type.startsWith("image/"));
@ -225,7 +227,7 @@ export default {
printLinkFor(item) {
const userHandle = item.owner || item.owner_group
if (!userHandle) return null
return {path: '/print', query: {kind: 'item', userHandle, id: item.id}}
return {name: 'print', query: {kind: 'item', userHandle, id: item.id}}
},
},
watch: {

View file

@ -41,7 +41,7 @@
</div>
</div>
<div class="card" v-if="canEdit">
<button class="btn btn-primary" @click="$router.push(`/inventory/${handle}/${id}/edit`)">
<button class="btn btn-primary" @click="$router.push({name: 'inventory-edit', params: {handle, id}})">
<b-icon-pencil-square></b-icon-pencil-square>
Edit
</button>
@ -51,7 +51,7 @@
Delete
</button>
<button v-if="canPrint" class="btn btn-secondary"
@click="$router.push({path: '/print', query: {kind: 'item', userHandle: decodedHandle, id: id}})">
@click="$router.push({name: 'print', query: {kind: 'item', userHandle: decodedHandle, id: id}})">
<b-icon-printer></b-icon-printer>
Print label
</button>

View file

@ -53,11 +53,11 @@
<div class="text-center">
<p class="mb-0 text-muted">
Dont have an account?
<router-link to="/register">Sign up</router-link>
<router-link :to="{name: 'register'}">Sign up</router-link>
</p>
<p class="mb-0 text-muted">
Connect with existing account?
<router-link to="/pairing">Pairing</router-link>
<router-link :to="{name: 'pairing'}">Pairing</router-link>
</p>
</div>
</div>

View file

@ -97,11 +97,11 @@
<div class="text-center">
<p class="mb-0 text-muted">
Already have an account?
<router-link to="/login">Login</router-link>
<router-link :to="{name: 'login'}">Login</router-link>
</p>
<p class="mb-0 text-muted">
Dont have an account?
<router-link to="/register">Sign up</router-link>
<router-link :to="{name: 'register'}">Sign up</router-link>
</p>
</div>
</div>

View file

@ -77,11 +77,11 @@
<div class="text-center">
<p class="mb-0 text-muted">
Already have an account?
<router-link to="/login">Login</router-link>
<router-link :to="{name: 'login'}">Login</router-link>
</p>
<p class="mb-0 text-muted">
Connect with existing account?
<router-link to="/pairing">Pairing</router-link>
<router-link :to="{name: 'pairing'}">Pairing</router-link>
</p>
</div>
</div>

View file

@ -101,8 +101,8 @@ function classifyScanText(text) {
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}`;
? {name: 'locations-detail', params: {handle: encodeHandleForUrl(handle), id}}
: {name: 'inventory-detail', params: {handle: encodeHandleForUrl(handle), id}};
return {to, ownerQualifiedId: {handle, kind, id}};
}
return null;

View file

@ -112,7 +112,7 @@ export default {
// user handle - one route shape, no owner-is-me special case.
this.fetchSearchResults({query: this.query}).then((results) => {
this.search_results = results.map(e => (
{...e, route: `/inventory/${encodeHandleForUrl(e.owner)}/${e.id}`}))
{...e, route: {name: 'inventory-detail', params: {handle: encodeHandleForUrl(e.owner), id: e.id}}}))
});
}
},

View file

@ -55,10 +55,10 @@
<span class="badge bg-secondary text-white">{{ location.visibility_policy }}</span>
</td>
<td class="table-action">
<router-link :to="`${locationRoute(location)}/edit`">
<router-link :to="locationEditRoute(location)">
<b-icon-pencil-square></b-icon-pencil-square>
</router-link>
<a :href="`${locationRoute(location)}/delete`" @click.prevent="tryDeleteLocation(location)">
<a href="#" @click.prevent="tryDeleteLocation(location)">
<b-icon-trash></b-icon-trash>
</a>
<router-link v-if="shortIdLink(location)" :to="shortIdLink(location)">
@ -94,7 +94,7 @@
<button class="btn btn-danger btn-sm"
@click="tryDeleteLocation(location)">Delete
</button>
<router-link :to="`${locationRoute(location)}/edit`"
<router-link :to="locationEditRoute(location)"
class="btn btn-primary btn-sm">Edit
</router-link>
<router-link v-if="shortIdLink(location)" :to="shortIdLink(location)"
@ -129,7 +129,7 @@ import {shortenedRoute, encodeHandleForUrl, decodeHandleFromUrl} from "@/router"
export default {
name: "StorageLocation",
props: {
// Matches /storage-location/:owner; absent means "me". See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
// Matches /locations/:owner; absent means "me". See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
owner: {
type: String,
default: null
@ -163,17 +163,16 @@ export default {
return this.owner ? decodeHandleFromUrl(this.owner) : this.user
},
set(value) {
const path = value === this.user ? '/storage-location' : `/storage-location/${encodeHandleForUrl(value)}`
this.$router.replace(path)
const params = value === this.user ? {} : {owner: encodeHandleForUrl(value)}
this.$router.replace({name: 'locations', params})
}
},
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/${encodeHandleForUrl(this.selectedOwner)}`
const params = this.selectedOwner === this.user ? {} : {group: encodeHandleForUrl(this.selectedOwner)}
return {name: 'locations-new', params}
}
},
methods: {
@ -190,7 +189,10 @@ export default {
})
},
locationRoute(location) {
return `/storage-locations/${encodeHandleForUrl(location.owner_group || location.owner)}/${location.id}`
return {name: 'locations-detail', params: {handle: encodeHandleForUrl(location.owner_group || location.owner), id: location.id}}
},
locationEditRoute(location) {
return {name: 'locations-edit', params: {handle: encodeHandleForUrl(location.owner_group || location.owner), id: location.id}}
},
shortIdLink(location) {
if (location.owner_group) {
@ -208,7 +210,7 @@ export default {
// printLinkFor. See docs/implementation.md#print-link-shape.
printLinkFor(location) {
const userHandle = location.owner_group || location.owner
return {path: '/print', query: {kind: 'storage-location', userHandle, id: location.id}}
return {name: 'print', query: {kind: 'storage-location', userHandle, id: location.id}}
},
},
watch: {

View file

@ -34,7 +34,7 @@
</div>
</div>
<div class="card" v-if="canEdit">
<button class="btn btn-primary" @click="$router.push(`/storage-locations/${handle}/${id}/edit`)">
<button class="btn btn-primary" @click="$router.push({name: 'locations-edit', params: {handle, id}})">
<b-icon-pencil-square></b-icon-pencil-square>
Edit
</button>
@ -44,7 +44,7 @@
Delete
</button>
<button class="btn btn-secondary"
@click="$router.push({path: '/print', query: {kind: 'storage-location', userHandle: decodedHandle, id: id}})">
@click="$router.push({name: 'print', query: {kind: 'storage-location', userHandle: decodedHandle, id: id}})">
<b-icon-printer></b-icon-printer>
Print label
</button>

View file

@ -81,7 +81,7 @@ export default {
...BIcons
},
props: {
// Matches /storage-locations/new/:group?. See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
// Matches /locations/new/:group?. See docs/implementation.md#owner-filtered-overview-routes-use-path-segments-not-query-strings.
group: {
type: String,
default: null

View file

@ -8,7 +8,7 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<router-link to="/workflows" class="text-decoration-none">Workflows</router-link>
<router-link :to="{name: 'workflows'}" class="text-decoration-none">Workflows</router-link>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ workflowDefinition?.title || workflowInstance?.title || 'Loading...' }}</li>
</ol>

View file

@ -6,7 +6,7 @@ import StorageOptimizationWorkflow from '@/components/workflow/workflows/Storage
import MaintenanceScheduleWorkflow from '@/components/workflow/workflows/MaintenanceScheduleWorkflow.vue';
import ExpiryCheckWorkflow from '@/components/workflow/workflows/ExpiryCheckWorkflow.vue';
import BackupRestoreWorkflow from '@/components/workflow/workflows/BackupRestoreWorkflow.vue';
import BulkLabelPrintWorkflow from '@/components/workflow/workflows/BulkLabelPrintWorkflow.vue';
//import BulkLabelPrintWorkflow from '@/components/workflow/workflows/BulkLabelPrintWorkflow.vue';
const workflows = [
{...FotoFirstBulkImportWorkflow.meta, component: FotoFirstBulkImportWorkflow},
@ -16,7 +16,7 @@ const workflows = [
{...MaintenanceScheduleWorkflow.meta, component: MaintenanceScheduleWorkflow},
{...ExpiryCheckWorkflow.meta, component: ExpiryCheckWorkflow},
{...BackupRestoreWorkflow.meta, component: BackupRestoreWorkflow},
{...BulkLabelPrintWorkflow.meta, component: BulkLabelPrintWorkflow},
// {...BulkLabelPrintWorkflow.meta, component: BulkLabelPrintWorkflow},
];
export function getAllWorkflows() {