stash
This commit is contained in:
parent
3cd9200d87
commit
e88dc898b3
18 changed files with 298 additions and 84 deletions
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,18 +2,131 @@
|
|||
<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>
|
||||
</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;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -53,11 +53,11 @@
|
|||
<div class="text-center">
|
||||
<p class="mb-0 text-muted">
|
||||
Don’t 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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
Don’t have an account?
|
||||
<router-link to="/register">Sign up</router-link>
|
||||
<router-link :to="{name: 'register'}">Sign up</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}}}))
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue