This commit is contained in:
j3d1 2023-05-31 20:43:14 +02:00
parent 7e8f984ee2
commit a1fd49c5d1
11 changed files with 406 additions and 214 deletions

View file

@ -6,7 +6,7 @@
<a class="sidebar-toggle d-flex"> <a class="sidebar-toggle d-flex">
<i class="hamburger align-self-center"></i> <i class="hamburger align-self-center"></i>
</a> </a>
<SearchBox/> <SearchBox v-if="!hideSearch"/>
<div class="navbar-collapse collapse"> <div class="navbar-collapse collapse">
<ul class="navbar-nav navbar-align"> <ul class="navbar-nav navbar-align">
<Notifications :notifications="notifications"/> <Notifications :notifications="notifications"/>
@ -42,7 +42,14 @@ export default {
}, },
computed: { computed: {
...mapState(['messages']), ...mapState(['messages']),
...mapGetters(['notifications']) ...mapGetters(['notifications']),
},
props: {
hideSearch: {
type: Boolean,
required: false,
default: false
}
}, },
async mounted() { async mounted() {
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<form class="d-none d-sm-inline-block"> <form class="d-none d-sm-inline-block" @submit.prevent="search">
<div class="input-group input-group-navbar"> <div class="input-group input-group-navbar">
<input type="text" class="form-control" placeholder="Search…" aria-label="Search" v-model="query" ref="search-text" /> <input type="text" class="form-control" placeholder="Search…" aria-label="Search" v-model="query" ref="search-text" />
<button class="btn" type="button" @click.prevent="search"> <button class="btn" type="button" @click.prevent="search">
@ -18,6 +18,10 @@ export default {
components: { components: {
...BIcons ...BIcons
}, },
model: {
prop: "query",
event: "change"
},
data() { data() {
return { return {
query: "" query: ""
@ -31,6 +35,12 @@ export default {
this.$refs["search-text"].focus(); this.$refs["search-text"].focus();
} }
}, },
watch: {
query() {
//emit event
this.$emit("change", this.query);
}
},
mounted() { mounted() {
//console.log(this.$route) //console.log(this.$route)
//console.log(this.$route.params.query) //console.log(this.$route.params.query)

View file

@ -2,6 +2,7 @@
<nav id="sidebar" class="sidebar"> <nav id="sidebar" class="sidebar">
<div class="sidebar-content js-simplebar"> <div class="sidebar-content js-simplebar">
<router-link to="/" class="sidebar-brand"> <router-link to="/" class="sidebar-brand">
<!--img src="/src/assets/icons/toolshed-48x48.png" alt="Toolshed logo"-->
<span class="align-middle">Toolshed</span> <span class="align-middle">Toolshed</span>
</router-link> </router-link>
<ul class="sidebar-nav"> <ul class="sidebar-nav">

View file

@ -6,14 +6,50 @@ class ServerSet {
if (!unreachable_neighbors || typeof unreachable_neighbors.queryUnreachable !== 'function' || typeof unreachable_neighbors.unreachable !== 'function') { if (!unreachable_neighbors || typeof unreachable_neighbors.queryUnreachable !== 'function' || typeof unreachable_neighbors.unreachable !== 'function') {
throw new Error('no unreachable_neighbors') throw new Error('no unreachable_neighbors')
} }
this.servers = servers; this.servers = [... new Set(servers)] // deduplicate
this.unreachable_neighbors = unreachable_neighbors; this.unreachable_neighbors = unreachable_neighbors;
} }
add(server) { add(server) {
console.log('adding server', server)
if (!server || typeof server !== 'string') {
throw new Error('server must be a string')
}
if (server in this.servers) {
console.log('server already in set', server)
return
}
this.servers.push(server); this.servers.push(server);
} }
async get(auth, target) {
if (!auth || typeof auth.buildAuthHeader !== 'function') {
throw new Error('no auth')
}
for (const server of this.servers) {
try {
if (this.unreachable_neighbors.queryUnreachable(server)) {
continue
}
const url = "http://" + server + target // TODO https
return await fetch(url, {
method: 'GET',
headers: {
...auth.buildAuthHeader(url)
},
credentials: 'omit'
}).catch(err => {
console.error('get from server failed', server, err)
this.unreachable_neighbors.unreachable(server)
}
).then(response => response.json())
} catch (e) {
console.error('get from server failed', server, e)
}
}
throw new Error('all servers failed')
}
async post(auth, target, data) { async post(auth, target, data) {
if (!auth || typeof auth.buildAuthHeader !== 'function') { if (!auth || typeof auth.buildAuthHeader !== 'function') {
throw new Error('no auth') throw new Error('no auth')
@ -32,7 +68,10 @@ class ServerSet {
}, },
credentials: 'omit', credentials: 'omit',
body: JSON.stringify(data) body: JSON.stringify(data)
}).catch(err => this.unreachable_neighbors.unreachable(server) }).catch(err => {
console.error('post to server failed', server, err)
this.unreachable_neighbors.unreachable(server)
}
).then(response => response.json()) ).then(response => response.json())
} catch (e) { } catch (e) {
console.error('post to server failed', server, e) console.error('post to server failed', server, e)
@ -59,7 +98,10 @@ class ServerSet {
}, },
credentials: 'omit', credentials: 'omit',
body: JSON.stringify(data) body: JSON.stringify(data)
}).catch(err => this.unreachable_neighbors.unreachable(server) }).catch(err => {
console.error('patch to server failed', server, err)
this.unreachable_neighbors.unreachable(server)
}
).then(response => response.json()) ).then(response => response.json())
} catch (e) { } catch (e) {
console.error('patch to server failed', server, e) console.error('patch to server failed', server, e)
@ -68,56 +110,6 @@ class ServerSet {
throw new Error('all servers failed') throw new Error('all servers failed')
} }
async get(auth, target) {
if (!auth || typeof auth.buildAuthHeader !== 'function') {
throw new Error('no auth')
}
for (const server of this.servers) {
try {
if (this.unreachable_neighbors.queryUnreachable(server)) {
continue
}
const url = "http://" + server + target // TODO https
return await fetch(url, {
method: 'GET',
headers: {
...auth.buildAuthHeader(url)
},
credentials: 'omit'
}).catch(err => this.unreachable_neighbors.unreachable(server)
).then(response => response.json())
} catch (e) {
console.error('get from server failed', server, e)
}
}
throw new Error('all servers failed')
}
async delete(auth, target) {
if (!auth || typeof auth.buildAuthHeader !== 'function') {
throw new Error('no auth')
}
for (const server of this.servers) {
try {
if (this.unreachable_neighbors.queryUnreachable(server)) {
continue
}
const url = "http://" + server + target // TODO https
return await fetch(url, {
method: 'DELETE',
headers: {
...auth.buildAuthHeader(url)
},
credentials: 'omit'
}).catch(err => this.unreachable_neighbors.unreachable(server)
).then(response => response.json())
} catch (e) {
console.error('delete from server failed', server, e)
}
}
throw new Error('all servers failed')
}
async put(auth, target, data) { async put(auth, target, data) {
if (!auth || typeof auth.buildAuthHeader !== 'function') { if (!auth || typeof auth.buildAuthHeader !== 'function') {
throw new Error('no auth') throw new Error('no auth')
@ -136,7 +128,10 @@ class ServerSet {
}, },
credentials: 'omit', credentials: 'omit',
body: JSON.stringify(data) body: JSON.stringify(data)
}).catch(err => this.unreachable_neighbors.unreachable(server) }).catch(err => {
console.error('put to server failed', server, err)
this.unreachable_neighbors.unreachable(server)
}
).then(response => response.json()) ).then(response => response.json())
} catch (e) { } catch (e) {
console.error('put to server failed', server, e) console.error('put to server failed', server, e)
@ -144,8 +139,107 @@ class ServerSet {
} }
throw new Error('all servers failed') throw new Error('all servers failed')
} }
async delete(auth, target) {
if (!auth || typeof auth.buildAuthHeader !== 'function') {
throw new Error('no auth')
}
for (const server of this.servers) {
try {
if (this.unreachable_neighbors.queryUnreachable(server)) {
continue
}
const url = "http://" + server + target // TODO https
return await fetch(url, {
method: 'DELETE',
headers: {
...auth.buildAuthHeader(url)
},
credentials: 'omit'
}).catch(err => {
console.error('delete from server failed', server, err)
this.unreachable_neighbors.unreachable(server)
}
)
} catch (e) {
console.error('delete from server failed', server, e)
}
}
throw new Error('all servers failed')
}
} }
class ServerSetUnion {
constructor(serverSets) {
if (!serverSets || !Array.isArray(serverSets)) {
throw new Error('no serverSets')
}
this.serverSets = serverSets;
}
add(serverset) {
if (!serverset || !(serverset instanceof ServerSet)) {
throw new Error('no serverset')
}
if (this.serverSets.find(s => serverset.servers.every(s2 => s.servers.includes(s2)))) {
console.warn('serverset already in union', serverset)
return
}
this.serverSets.push(serverset)
}
async post(auth, target, data) {
try {
return await this.serverSets.reduce(async (acc, serverset) => {
return await serverset.post(auth, target, data)
}, Promise.resolve())
} catch (e) {
throw new Error('all servers failed')
}
}
async patch(auth, target, data) {
try {
return await this.serverSets.reduce(async (acc, serverset) => {
return await serverset.patch(auth, target, data)
}, Promise.resolve())
} catch (e) {
throw new Error('all servers failed')
}
}
async get(auth, target) {
try {
return await this.serverSets.reduce(async (acc, serverset) => {
return await serverset.get(auth, target)
}, Promise.resolve())
} catch (e) {
throw new Error('all servers failed')
}
}
async delete(auth, target) {
try {
return await this.serverSets.reduce(async (acc, serverset) => {
return await serverset.delete(auth, target)
}, Promise.resolve())
} catch (e) {
throw new Error('all servers failed')
}
}
async put(auth, target, data) {
try {
return await this.serverSets.reduce(async (acc, serverset) => {
return await serverset.put(auth, target, data)
}, Promise.resolve())
} catch (e) {
throw new Error('all servers failed')
}
}
}
class authMethod { class authMethod {
constructor(method, auth) { constructor(method, auth) {
this.method = method; this.method = method;
@ -167,7 +261,6 @@ function createSignAuth(username, signKey) {
return new authMethod(({signKey, username}, {url, data}) => { return new authMethod(({signKey, username}, {url, data}) => {
const json = JSON.stringify(data) const json = JSON.stringify(data)
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + (data ? json : "")), signKey) const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + (data ? json : "")), signKey)
console.log('sign', nacl.to_hex(signature), url, json)
return {'Authorization': 'Signature ' + username + ':' + nacl.to_hex(signature)} return {'Authorization': 'Signature ' + username + ':' + nacl.to_hex(signature)}
}, context) }, context)
} }
@ -188,6 +281,6 @@ function createNullAuth() {
}, {}) }, {})
} }
export {ServerSet, createSignAuth, createTokenAuth, createNullAuth}; export {ServerSet, ServerSetUnion, createSignAuth, createTokenAuth, createNullAuth};

View file

@ -18,11 +18,11 @@ const routes = [
{path: '/profile', component: Profile, meta: {requiresAuth: true}}, {path: '/profile', component: Profile, meta: {requiresAuth: true}},
{path: '/settings', component: Settings, meta: {requiresAuth: true}}, {path: '/settings', component: Settings, meta: {requiresAuth: true}},
{path: '/inventory', component: Inventory, meta: {requiresAuth: true}}, {path: '/inventory', component: Inventory, meta: {requiresAuth: true}},
{path: '/inventory/:id', component: InventoryDetail, meta: {requiresAuth: true}}, {path: '/inventory/:id', component: InventoryDetail, meta: {requiresAuth: true}, props: true},
{path: '/inventory/:id/edit', component: InventoryEdit, meta: {requiresAuth: true}}, {path: '/inventory/:id/edit', component: InventoryEdit, meta: {requiresAuth: true}, props: true},
{path: '/inventory/new', component: InventoryNew, meta: {requiresAuth: true}}, {path: '/inventory/new', component: InventoryNew, meta: {requiresAuth: true}},
{path: '/friends', component: Friends, meta: {requiresAuth: true}}, {path: '/friends', component: Friends, meta: {requiresAuth: true}},
{path: '/search/:query', component: Search, meta: {requiresAuth: true}}, {path: '/search/:query', component: Search, meta: {requiresAuth: true}, props: true},
{path: '/login', component: Login, meta: {requiresAuth: false}}, {path: '/login', component: Login, meta: {requiresAuth: false}},
{path: '/register', component: Register, meta: {requiresAuth: false}}, {path: '/register', component: Register, meta: {requiresAuth: false}},
] ]

View file

@ -2,7 +2,7 @@ import {createStore} from 'vuex';
import router from '@/router'; import router from '@/router';
import FallBackResolver from "@/dns"; import FallBackResolver from "@/dns";
import NeighborsCache from "@/neigbors"; import NeighborsCache from "@/neigbors";
import {createSignAuth, createTokenAuth, createNullAuth, ServerSet} from "@/federation"; import {createSignAuth, createTokenAuth, createNullAuth, ServerSet, ServerSetUnion} from "@/federation";
export default createStore({ export default createStore({
@ -50,26 +50,21 @@ export default createStore({
localStorage.setItem('remember', remember); localStorage.setItem('remember', remember);
}, },
setInventoryItems(state, {url, items}) { setInventoryItems(state, {url, items}) {
console.log('setInventoryItems', url, items)
state.item_map[url] = items; state.item_map[url] = items;
}, },
setFriends(state, friends) { setFriends(state, friends) {
state.friends = friends; state.friends = friends;
}, },
setHomeServers(state, home_servers) { setHomeServers(state, home_servers) {
console.log('setHomeServer', home_servers)
state.home_servers = home_servers; state.home_servers = home_servers;
}, },
setAllFriendsServers(state, servers) { setAllFriendsServers(state, servers) {
console.log('setAllFriendsServers', servers)
state.all_friends_servers = servers; state.all_friends_servers = servers;
}, },
setTags(state, tags) { setTags(state, tags) {
console.log('setTags', tags)
state.tags = tags; state.tags = tags;
}, },
setProperties(state, properties) { setProperties(state, properties) {
console.log('setProperties', properties)
state.properties = properties; state.properties = properties;
}, },
logout(state) { logout(state) {
@ -131,7 +126,7 @@ export default createStore({
if (domain === 'localhost') if (domain === 'localhost')
return ['127.0.0.1:8000']; return ['127.0.0.1:8000'];
if (domain === 'example.com') if (domain === 'example.com')
return ['10.23.42.128:8000']; return ['10.23.42.128:8000','10.23.42.128:8000'];
if (domain === 'example.jedi') if (domain === 'example.jedi')
return ['10.23.42.128:8000']; return ['10.23.42.128:8000'];
if (domain === 'example2.com') if (domain === 'example2.com')
@ -149,13 +144,14 @@ export default createStore({
return promise return promise
}, },
async getAllFriendsServers({state, dispatch, commit}) { async getAllFriendsServers({state, dispatch, commit}) {
const friends = await dispatch('fetchFriends')
if (state.all_friends_servers) if (state.all_friends_servers)
return state.all_friends_servers return state.all_friends_servers
const promise = (async () => { const promise = (async () => {
const servers = new ServerSet([], state.unreachable_neighbors) const servers = new ServerSetUnion([])
for (const friend of state.friends) { for (const friend of friends) {
const s = await dispatch('lookupServer', {username: friend}) const s = await dispatch('lookupServer', {username: friend.username})
servers.add(s) servers.add(new ServerSet(s, state.unreachable_neighbors))
} }
return servers return servers
})() })()
@ -177,31 +173,24 @@ export default createStore({
}, },
async createInventoryItem({state, dispatch, getters}, item) { async createInventoryItem({state, dispatch, getters}, item) {
const servers = await dispatch('getHomeServers') const servers = await dispatch('getHomeServers')
const data = {...item, owned_amount: 1, availability_policy: 'friends', category: 'other'} const data = {availability_policy: 'friends', category: 'other', ...item}
return await servers.post(getters.signAuth, '/api/inventory_items/', data) return await servers.post(getters.signAuth, '/api/inventory_items/', data)
}, },
async updateInventoryItem({state, dispatch, getters}, item) { async updateInventoryItem({state, dispatch, getters}, item) {
const servers = await dispatch('getHomeServers') const servers = await dispatch('getHomeServers')
return await servers.patch(getters.signAuth, '/api/inventory_items/' + item.id + '/', item) const data = {availability_policy: 'friends', category: 'other', ...item}
return await servers.patch(getters.signAuth, '/api/inventory_items/' + item.id + '/', data)
}, },
async deleteInventoryItem({state, dispatch, getters}, item) { async deleteInventoryItem({state, dispatch, getters}, item) {
const servers = await dispatch('getHomeServers') const servers = await dispatch('getHomeServers')
return await servers.delete(getters.signAuth, '/api/inventory_items/' + item.id + '/') const ret = await servers.delete(getters.signAuth, '/api/inventory_items/' + item.id + '/')
dispatch('fetchInventoryItems')
return ret
},
async fetchSearchResults({state, dispatch, getters}, {query}) {
const servers = await dispatch('getAllFriendsServers')
return await servers.get(getters.signAuth, '/api/search/?query=' + query)
}, },
/*async searchInventoryItems() {
try {
const servers = await this.fetchFriends().then(friends => friends.map(friend => this.lookupServer({username: friend.name})))
const urls = servers.map(server => server.then(s => {
return {host: s, target: "/api/inventory_items/"}
}))
urls.map(url => url.then(u => this.apiFederatedGet(u).then(items => {
this.setInventoryItems({url: u.domain, items})
}).catch(e => {
}))) // TODO: handle error
} catch (e) {
console.error(e)
}
},*/
async fetchFriends({commit, dispatch, getters, state}) { async fetchFriends({commit, dispatch, getters, state}) {
const servers = await dispatch('getHomeServers') const servers = await dispatch('getHomeServers')
const data = await servers.get(getters.signAuth, '/api/friends/') const data = await servers.get(getters.signAuth, '/api/friends/')
@ -252,7 +241,7 @@ export default createStore({
console.log('declining friend ' + args) console.log('declining friend ' + args)
}, },
async fetchTags({state, commit, dispatch, getters}) { async fetchTags({state, commit, dispatch, getters}) {
if(state.last_load.tags > Date.now() - 1000 * 60 * 60 * 24) { if (state.last_load.tags > Date.now() - 1000 * 60 * 60 * 24) {
return state.tags return state.tags
} }
const servers = await dispatch('getHomeServers') const servers = await dispatch('getHomeServers')
@ -262,7 +251,7 @@ export default createStore({
return data return data
}, },
async fetchProperties({state, commit, dispatch, getters}) { async fetchProperties({state, commit, dispatch, getters}) {
if(state.last_load.properties > Date.now() - 1000 * 60 * 60 * 24) { if (state.last_load.properties > Date.now() - 1000 * 60 * 60 * 24) {
return state.properties return state.properties
} }
const servers = await dispatch('getHomeServers') const servers = await dispatch('getHomeServers')
@ -270,6 +259,47 @@ export default createStore({
commit('setProperties', data) commit('setProperties', data)
state.last_load.properties = Date.now() state.last_load.properties = Date.now()
return data return data
},
async fetchCategories({state, commit, dispatch, getters}) {
if (state.last_load.categories > Date.now() - 1000 * 60 * 60 * 24) {
return state.categories
}
const servers = await dispatch('getHomeServers')
const data = await servers.get(getters.signAuth, '/api/categories/')
commit('setCategories', data)
state.last_load.categories = Date.now()
return data
},
async fetchAvailabilityPolicies({state, commit, dispatch, getters}) {
if (state.last_load.availability_policies > Date.now() - 1000 * 60 * 60 * 24) {
return state.availability_policies
}
const servers = await dispatch('getHomeServers')
const data = await servers.get(getters.signAuth, '/api/availability_policies/')
commit('setAvailabilityPolicies', data)
state.last_load.availability_policies = Date.now()
return data
},
async fetchInfo({state, commit, dispatch, getters}) {
const last_load_info = Math.min(
state.last_load.tags,
state.last_load.properties,
state.last_load.categories,
state.last_load.availability_policies)
if (last_load_info > Date.now() - 1000 * 60 * 60 * 24) {
return state.info
}
const servers = await dispatch('getHomeServers')
const data = await servers.get(getters.signAuth, '/api/info/')
commit('setTags', data.tags)
commit('setProperties', data.properties)
commit('setCategories', data.categories)
commit('setAvailabilityPolicies', data.availability_policies)
state.last_load.tags = Date.now()
state.last_load.properties = Date.now()
state.last_load.categories = Date.now()
state.last_load.availability_policies = Date.now()
return data
} }
}, },
getters: { getters: {
@ -287,11 +317,9 @@ export default createStore({
return state.user !== null && state.token !== null; return state.user !== null && state.token !== null;
}, },
signAuth(state) { signAuth(state) {
console.log('signAuth', state.user, state.keypair.signSk)
return createSignAuth(state.user, state.keypair.signSk) return createSignAuth(state.user, state.keypair.signSk)
}, },
tokenAuth(state) { tokenAuth(state) {
console.log('tokenAuth', state.token)
return createTokenAuth(state.token) return createTokenAuth(state.token)
}, },
nullAuth(state) { nullAuth(state) {

View file

@ -25,12 +25,12 @@
<router-link :to="`/inventory/${item.id}`">{{ item.name }}</router-link> <router-link :to="`/inventory/${item.id}`">{{ item.name }}</router-link>
</td> </td>
<td>{{ item.owner }}</td> <td>{{ item.owner }}</td>
<td class="d-none d-md-table-cell">{{ item.owned_amount }}</td> <td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
<td class="table-action"> <td class="table-action">
<router-link :to="`/inventory/${item.id}/edit`"> <router-link :to="`/inventory/${item.id}/edit`">
<b-icon-pencil-square></b-icon-pencil-square> <b-icon-pencil-square></b-icon-pencil-square>
</router-link> </router-link>
<a :href="`/inventory/${item.id}/delete`" @click.prevent="deleteItem(item.id)"> <a :href="`/inventory/${item.id}/delete`" @click.prevent="deleteInventoryItem(item)">
<b-icon-trash></b-icon-trash> <b-icon-trash></b-icon-trash>
</a> </a>
</td> </td>
@ -61,13 +61,10 @@ export default {
...BIcons ...BIcons
}, },
computed: { computed: {
...mapGetters(["inventory_items"]), ...mapGetters(["inventory_items"])
username() {
return this.$route.params.username
}
}, },
methods: { methods: {
...mapActions(["fetchInventoryItems"]), ...mapActions(["fetchInventoryItems", "deleteInventoryItem"]),
}, },
async mounted() { async mounted() {
await this.fetchInventoryItems() await this.fetchInventoryItems()

View file

@ -4,50 +4,50 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-header">Edit Item</div> <div class="card-header">{{ item.name }}</div>
<div class="card-body"> <div class="card-body">
<div class="mb-3"> <div class="mb-3">
<label for="description" class="form-label">Description</label>
{{ item.description }}
</div>
<div class="mb-3">
<label for="tags" class="form-label">Tags</label>
<span class="badge bg-dark" v-for="(tag, index) in item.tags" :key="index"> <span class="badge bg-dark" v-for="(tag, index) in item.tags" :key="index">
{{ tag }} {{ tag }}
</span> </span>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="property" class="form-label">Properties</label>
<span class="badge bg-dark" v-for="(property, index) in item.properties" :key="index"> <span class="badge bg-dark" v-for="(property, index) in item.properties" :key="index">
{{ property.name }}={{ property.value }} {{ property.name }}={{ property.value }}
</span> </span>
</div> </div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name"
placeholder="Enter item name" v-model="item.name">
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description"
placeholder="Enter description" v-model="item.description"></textarea>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="quantity" class="form-label">Quantity</label> <label for="quantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity" {{ item.owned_quantity }}
placeholder="Enter quantity" v-model="item.quantity">
</div> </div>
<div class="mb-3"> <!-- TODO -->
<label for="price" class="form-label">Price</label> <!--div class="mb-3">
<input type="number" class="form-control" id="price" name="price"
placeholder="Enter price" v-model="item.price">
</div>
<div class="mb-3">
<label for="image" class="form-label">Image</label> <label for="image" class="form-label">Image</label>
<input type="text" class="form-control" id="image" name="image" <input type="text" class="form-control" id="image" name="image"
placeholder="Enter image" v-model="item.image"> placeholder="Enter image" v-model="item.image">
</div>
</div>
<!--div class="card">
<button type="submit" class="btn btn-primary" @click="updateInventoryItem(item)">Update
</button>
</div--> </div-->
</div> </div>
</div> </div>
<!-- actions -->
<div class="card">
<a class="btn btn-primary" :href="'/inventory/' + id + '/edit'">
<b-icon-pencil-square></b-icon-pencil-square>
Edit
</a>
<button type="submit" class="btn btn-danger"
@click="deleteInventoryItem(item).then(() => $router.push('/inventory'))">
<b-icon-trash></b-icon-trash>
Delete
</button>
</div>
</div>
</div> </div>
</main> </main>
</BaseLayout> </BaseLayout>
@ -58,27 +58,33 @@ import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue"; import BaseLayout from "@/components/BaseLayout.vue";
import TagField from "@/components/TagField.vue"; import TagField from "@/components/TagField.vue";
import PropertyField from "@/components/PropertyField.vue"; import PropertyField from "@/components/PropertyField.vue";
import {mapActions, mapGetters} from "vuex";
export default { export default {
name: "InventoryDetail", name: "InventoryDetail",
data() {
return {
item: {
name: "",
description: "",
quantity: 0,
price: 0,
image: "",
tags: [],
properties: []
}
}
},
components: { components: {
PropertyField, TagField, PropertyField, TagField,
BaseLayout, BaseLayout,
...BIcons ...BIcons
}, },
props: {
id: {
type: String,
required: true
}
},
computed: {
...mapGetters(["inventory_items"]),
item() {
return this.inventory_items.find(item => item.id === parseInt(this.id)) || {}
}
},
methods: {
...mapActions(["fetchInventoryItems", "deleteInventoryItem"])
},
async mounted() {
await this.fetchInventoryItems()
}
} }
</script> </script>

View file

@ -6,6 +6,16 @@
<div class="card"> <div class="card">
<div class="card-header">Edit Item</div> <div class="card-header">Edit Item</div>
<div class="card-body"> <div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name"
placeholder="Enter item name" v-model="item.name">
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description"
placeholder="Enter description" v-model="item.description"></textarea>
</div>
<div class="mb-3"> <div class="mb-3">
<ul> <ul>
<li v-for="tag in item.tags" :key="tag"> <li v-for="tag in item.tags" :key="tag">
@ -24,30 +34,10 @@
<label for="property" class="form-label">Property</label> <label for="property" class="form-label">Property</label>
<property-field :value="item.properties"></property-field> <property-field :value="item.properties"></property-field>
</div> </div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name"
placeholder="Enter item name" v-model="item.name">
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description"
placeholder="Enter description" v-model="item.description"></textarea>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="quantity" class="form-label">Quantity</label> <label for="quantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity" <input type="number" class="form-control" id="quantity" name="quantity"
placeholder="Enter quantity" v-model="item.quantity"> placeholder="Enter quantity" v-model="item.owned_quantity">
</div>
<div class="mb-3">
<label for="price" class="form-label">Price</label>
<input type="number" class="form-control" id="price" name="price"
placeholder="Enter price" v-model="item.price">
</div>
<div class="mb-3">
<label for="image" class="form-label">Image</label>
<input type="text" class="form-control" id="image" name="image"
placeholder="Enter image" v-model="item.image">
</div> </div>
</div> </div>
<div class="card"> <div class="card">
@ -63,35 +53,45 @@
<script> <script>
import * as BIcons from "bootstrap-icons-vue"; import * as BIcons from "bootstrap-icons-vue";
import {mapActions} from "vuex"; import {mapActions, mapGetters} from "vuex";
import BaseLayout from "@/components/BaseLayout.vue"; import BaseLayout from "@/components/BaseLayout.vue";
import TagField from "@/components/TagField.vue"; import TagField from "@/components/TagField.vue";
import PropertyField from "@/components/PropertyField.vue"; import PropertyField from "@/components/PropertyField.vue";
export default { export default {
name: "InventoryEdit", name: "InventoryEdit",
data() {
return {
item: {
name: "",
description: "",
quantity: 0,
price: 0,
image: "",
tags: [],
properties: []
}
}
},
components: { components: {
BaseLayout, BaseLayout,
TagField, TagField,
PropertyField, PropertyField,
...BIcons ...BIcons
}, },
methods: { props: {
...mapActions(["updateInventoryItem"]), id: {
type: String,
required: true
}
}, },
computed: {
...mapGetters(["inventory_items"]),
item() {
return {
tags: [],
properties: [],
name: "",
description: "",
owned_quantity: 0,
image: "",
...this.inventory_items.find(item => item.id === parseInt(this.id))
}
}
},
methods: {
...mapActions(["fetchInventoryItems", "updateInventoryItem"])
},
async mounted() {
await this.fetchInventoryItems()
}
} }
</script> </script>

View file

@ -6,6 +6,16 @@
<div class="card"> <div class="card">
<div class="card-header">Create New Item</div> <div class="card-header">Create New Item</div>
<div class="card-body"> <div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name"
placeholder="Enter item name" v-model="item.name">
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description"
placeholder="Enter description" v-model="item.description"></textarea>
</div>
<div class="mb-3"> <div class="mb-3">
<ul> <ul>
<li v-for="tag in item.tags" :key="tag"> <li v-for="tag in item.tags" :key="tag">
@ -24,34 +34,21 @@
<label for="property" class="form-label">Property</label> <label for="property" class="form-label">Property</label>
<property-field :value="item.properties"></property-field> <property-field :value="item.properties"></property-field>
</div> </div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name"
placeholder="Enter item name" v-model="item.name">
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description"
placeholder="Enter description" v-model="item.description"></textarea>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="quantity" class="form-label">Quantity</label> <label for="quantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity" <input type="number" class="form-control" id="quantity" name="quantity"
placeholder="Enter quantity" v-model="item.quantity"> placeholder="Enter quantity" v-model="item.owned_quantity">
</div> </div>
<div class="mb-3"> <!-- TODO -->
<label for="price" class="form-label">Price</label> <!--div class="mb-3">
<input type="number" class="form-control" id="price" name="price"
placeholder="Enter price" v-model="item.price">
</div>
<div class="mb-3">
<label for="image" class="form-label">Image</label> <label for="image" class="form-label">Image</label>
<input type="text" class="form-control" id="image" name="image" <input type="text" class="form-control" id="image" name="image"
placeholder="Enter image" v-model="item.image"> placeholder="Enter image" v-model="item.image">
</div> </div-->
</div> </div>
<div class="card"> <div class="card">
<button type="submit" class="btn btn-primary" @click="createInventoryItem(item)">Add <button type="submit" class="btn btn-primary"
@click="createInventoryItem(item).then(() => $router.push('/inventory'))">Add
</button> </button>
</div> </div>
</div> </div>
@ -70,25 +67,24 @@ import PropertyField from "@/components/PropertyField.vue";
export default { export default {
name: "InventoryNew", name: "InventoryNew",
data() {
return {
item: {
name: "",
description: "",
quantity: 0,
price: 0,
image: "",
tags: [],
properties: []
}
}
},
components: { components: {
BaseLayout, BaseLayout,
TagField, TagField,
PropertyField, PropertyField,
...BIcons ...BIcons
}, },
data() {
return {
item: {
name: "",
description: "",
owned_quantity: 0,
image: "",
tags: [],
properties: []
}
}
},
methods: { methods: {
...mapActions(['createInventoryItem']) ...mapActions(['createInventoryItem'])
} }

View file

@ -1,18 +1,48 @@
<template> <template>
<BaseLayout> <BaseLayout hide-search>
<main class="content"> <main class="content">
<div class="container-fluid p-0"> <div class="container-fluid p-0">
<h1 class="h3 mb-3">Blank Page</h1> <h1 class="h3 mb-3">Search Inventories</h1>
<div class="row"> <div class="row">
<div class="col-md-3">
<div class="card">
<SearchBox @change="filterResults" />
</div>
</div>
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0">Empty card</h5> <h5 class="card-title mb-0">Results for "{{ query }}"</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="logo"> <table class="table table-striped">
<img src="/src/assets/icons/toolshed-48x48.png" alt="Toolshed logo"> <thead>
</div> <tr>
<th style="width:40%;">Name</th>
<th style="width:25%">Owner</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 search_results" :key="item.id">
<td>
<router-link :to="`/inventory/${item.id}`">{{ item.name }}</router-link>
</td>
<td>{{ item.owner }}</td>
<td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
<td class="table-action">
<!--<router-link :to="`/inventory/${item.id}/edit`">
<b-icon-pencil-square></b-icon-pencil-square>
</router-link>
<a :href="`/inventory/${item.id}/delete`"
@click.prevent="deleteInventoryItem(item)">
<b-icon-trash></b-icon-trash>
</a>-->
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
@ -23,16 +53,40 @@
</template> </template>
<script> <script>
import {mapGetters, mapMutations} 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 SearchBox from "@/components/SearchBox.vue";
export default { export default {
name: 'Search', name: 'Search',
components: { components: {
SearchBox,
...BIcons, ...BIcons,
BaseLayout BaseLayout
}, },
props: {
query: {
type: String,
required: true
}
},
data() {
return {
search_results: [],
}
},
methods: {
...mapActions(['fetchSearchResults']),
filterResults(query) {
this.localQuery = query;
}
},
async mounted() {
this.fetchSearchResults({query: this.query}).then((results) => {
this.search_results = results;
});
}
} }
</script> </script>