add item edit page
This commit is contained in:
parent
b8f1942ab1
commit
f01d513803
12 changed files with 707 additions and 0 deletions
204
frontend/src/components/BadgeSelectField.vue
Normal file
204
frontend/src/components/BadgeSelectField.vue
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
<template>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="badge-list form-control form-control-lg" style="position: relative;">
|
||||||
|
<span v-for="(option, index) in value" :key="index">
|
||||||
|
<slot :option="option" :index="index"></slot>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="invisible-input" v-model="textinput" ref="input"
|
||||||
|
@keydown="keyDown">
|
||||||
|
<ul class="dropdown-menu" style="position: absolute; top: 100%; left: 0; width: 100%; z-index: 100;"
|
||||||
|
v-if="filteredOption.length > 0">
|
||||||
|
<li v-for="(option, index) in filteredOption" :key="index"
|
||||||
|
@mousedown="chooseOption(option)" @mouseover="selectOption(option)"
|
||||||
|
:class="{chosen: localValue.includes(option), selected: selectedTag === option, 'dropdown-item': true}">
|
||||||
|
{{ option }}
|
||||||
|
<b>
|
||||||
|
<b-icon-check class="checkmark"></b-icon-check>
|
||||||
|
<b-icon-x class="cross"></b-icon-x>
|
||||||
|
<b-icon-plus class="plus"></b-icon-plus>
|
||||||
|
</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.badge-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0 .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-list > * {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
padding: .25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-list > input.invisible-input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.invisible-input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: #495057;
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right .7rem center;
|
||||||
|
background-size: 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible-input:focus ~ .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
display: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark, .cross, .plus {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.selected {
|
||||||
|
background-color: var(--bs-primary);
|
||||||
|
color: var(--bs-light);
|
||||||
|
|
||||||
|
& .plus {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.chosen {
|
||||||
|
& .checkmark {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.chosen.selected {
|
||||||
|
background-color: var(--bs-danger);
|
||||||
|
color: var(--bs-light);
|
||||||
|
|
||||||
|
& .cross {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .checkmark, & .plus {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "BadgeSelectField",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
textinput: "",
|
||||||
|
selectedTag: ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: "value",
|
||||||
|
event: "addElement"
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
availableTags() {
|
||||||
|
return this.options.filter(option => !this.localValue.includes(option));
|
||||||
|
},
|
||||||
|
filteredOption() {
|
||||||
|
return this.options.filter(option => option.includes(this.textinput) || option === this.selectedTag);
|
||||||
|
},
|
||||||
|
localValue: {
|
||||||
|
get() {
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
chooseOption(option) {
|
||||||
|
this.$emit("addElement", option);
|
||||||
|
},
|
||||||
|
resetSelection() {
|
||||||
|
this.textinput = "";
|
||||||
|
this.selectedTag = null;
|
||||||
|
},
|
||||||
|
selectOption(option) {
|
||||||
|
this.selectedTag = option;
|
||||||
|
},
|
||||||
|
keyDown(event) {
|
||||||
|
const key = event.key;
|
||||||
|
if (key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.chooseOption(this.selectedTag);
|
||||||
|
this.$refs.input.blur();
|
||||||
|
} else if (key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.selectedTag === "") {
|
||||||
|
this.selectedTag = this.filteredOption[0];
|
||||||
|
} else {
|
||||||
|
const index = this.filteredOption.indexOf(this.selectedTag);
|
||||||
|
if (index < this.filteredOption.length - 1) {
|
||||||
|
this.selectedTag = this.filteredOption[index + 1];
|
||||||
|
} else {
|
||||||
|
this.selectedTag = this.filteredOption[0];
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
const element = this.$el.querySelector(".selected");
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({block: "nearest"});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
} else if (key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.selectedTag === "") {
|
||||||
|
this.selectedTag = this.filteredOption[this.filteredOption.length - 1];
|
||||||
|
} else {
|
||||||
|
const index = this.filteredOption.indexOf(this.selectedTag);
|
||||||
|
if (index > 0) {
|
||||||
|
this.selectedTag = this.filteredOption[index - 1];
|
||||||
|
} else {
|
||||||
|
this.selectedTag = this.filteredOption[this.filteredOption.length - 1];
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
const element = this.$el.querySelector(".selected");
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({block: "nearest"});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
} else if (key === "ArrowRight") {
|
||||||
|
if (event.target.selectionStart === this.textinput.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.chooseOption(this.selectedTag);
|
||||||
|
}
|
||||||
|
} else if (key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.$refs.input.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
115
frontend/src/components/PropertyBadge.vue
Normal file
115
frontend/src/components/PropertyBadge.vue
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<template>
|
||||||
|
<div class="input-group" :title="prettyDescription">
|
||||||
|
<label class="input-group-text form-control-inline form-control-sm text-light border-dark bg-dark badge">
|
||||||
|
{{ property.name }} =
|
||||||
|
</label>
|
||||||
|
<input type="number" min="0"
|
||||||
|
class="form-control form-control-inline form-control-sm text-light border-dark bg-dark badge"
|
||||||
|
placeholder="Enter value" @input.prevent="inputChange"
|
||||||
|
:value="localValue" :style="inputStyle"/>
|
||||||
|
<span class="input-group-text form-control-sm text-light border-dark bg-dark badge"
|
||||||
|
v-if="property.unit_symbol" :title="(property.unit_name?property.unit_name:property.unit_symbol)">
|
||||||
|
{{ property.unit_symbol }}
|
||||||
|
</span>
|
||||||
|
<span class="input-group-text form-control-sm text-light border-dark bg-dark badge pl-1">
|
||||||
|
<b-icon-x-circle @click="removeProperty()"></b-icon-x-circle>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-control-inline {
|
||||||
|
display: inline;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.badge {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text.badge {
|
||||||
|
padding: .3rem 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .badge:first-child {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .badge:last-child {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.badge {
|
||||||
|
--width: 0em;
|
||||||
|
min-height: calc(1.2rem);
|
||||||
|
line-height: 1;
|
||||||
|
min-width: calc(var(--width) + 1.35em + 18px);
|
||||||
|
width: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=number].badge {
|
||||||
|
}
|
||||||
|
|
||||||
|
input.badge:empty {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import {mapActions, mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PropertyBadge",
|
||||||
|
components: {
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
localValue: this.property.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
property: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
property: {
|
||||||
|
prop: "property",
|
||||||
|
event: "modelChange"
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
prettyDescription() {
|
||||||
|
var d = this.property.description ? this.property.description : this.property.name
|
||||||
|
if (this.property.unit_name) {
|
||||||
|
d += " (" + this.property.unit_name + ")"
|
||||||
|
}
|
||||||
|
return (d || "").replace(/<[^>]*>?/gm, '')
|
||||||
|
},
|
||||||
|
inputStyle() {
|
||||||
|
const w = this.property.value ? Math.max(1, this.property.value.toString().length) : 1
|
||||||
|
if (this.property.value === undefined) {
|
||||||
|
console.log("No value for property", this.property)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"--width": w + "ex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
inputChange(event) {
|
||||||
|
this.localValue = event.target.value
|
||||||
|
this.$emit("modelChange", {...this.property, value: this.localValue})
|
||||||
|
},
|
||||||
|
removeProperty() {
|
||||||
|
this.$emit("remove")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
89
frontend/src/components/PropertyField.vue
Normal file
89
frontend/src/components/PropertyField.vue
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<badge-select-field :value="this.splicedValue" :options="this.properties.map(p => p.name)" @addElement="addProperty"
|
||||||
|
ref="badgeSelect">
|
||||||
|
<template v-slot:default="{option, index}">
|
||||||
|
<PropertyBadge :key="index"
|
||||||
|
:property="option"
|
||||||
|
@modelChange="setValue(index, $event)"
|
||||||
|
@remove="removeProperty(index)"/>
|
||||||
|
</template>
|
||||||
|
</badge-select-field>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import {mapActions, mapState} from "vuex";
|
||||||
|
import PropertyBadge from "@/components/PropertyBadge.vue";
|
||||||
|
import BadgeSelectField from "@/components/BadgeSelectField.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PropertyField",
|
||||||
|
components: {
|
||||||
|
BadgeSelectField,
|
||||||
|
PropertyBadge,
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: "value",
|
||||||
|
event: "input"
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(["properties"]),
|
||||||
|
availableProperties() {
|
||||||
|
if (!this.properties) return [];
|
||||||
|
if (!this.value) return this.properties;
|
||||||
|
return this.properties.filter(property => !this.value.map(p => p.name).includes(property.name));
|
||||||
|
},
|
||||||
|
localValue: {
|
||||||
|
get() {
|
||||||
|
if (!this.value) return [];
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("input", value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splicedValue() {
|
||||||
|
if (!this.value || !this.properties) return [];
|
||||||
|
return this.value.map(property => {
|
||||||
|
return {
|
||||||
|
...this.properties.find(p => p.name === property.name),
|
||||||
|
value: property.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(["fetchProperties"]),
|
||||||
|
addProperty(property) {
|
||||||
|
if (property !== "") {
|
||||||
|
this.localValue.push({name: property, value: 0});
|
||||||
|
this.property = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeProperty(index) {
|
||||||
|
this.localValue.splice(index, 1);
|
||||||
|
},
|
||||||
|
setValue(index, value) {
|
||||||
|
if (value.target)
|
||||||
|
return;
|
||||||
|
this.localValue[index] = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
77
frontend/src/components/TagField.vue
Normal file
77
frontend/src/components/TagField.vue
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<badge-select-field :value="this.value" :options="this.tags" @addElement="addTag" ref="badgeSelect">
|
||||||
|
<template v-slot:default="{option, index}">
|
||||||
|
<span class="badge bg-dark" @click="removeTag(index)">
|
||||||
|
{{ option }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</badge-select-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.badge {
|
||||||
|
padding: .3rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge:hover {
|
||||||
|
background-color: var(--bs-danger) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import {mapActions, mapState} from "vuex";
|
||||||
|
import BadgeSelectField from "@/components/BadgeSelectField.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TagField",
|
||||||
|
components: {
|
||||||
|
BadgeSelectField,
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: "value",
|
||||||
|
event: "input"
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(["tags"]),
|
||||||
|
availableTags() {
|
||||||
|
return this.tags.filter(tag => !this.localValue.includes(tag));
|
||||||
|
},
|
||||||
|
localValue: {
|
||||||
|
get() {
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("input", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(["fetchTags"]),
|
||||||
|
addTag(tag) {
|
||||||
|
if (this.localValue.includes(tag)) {
|
||||||
|
this.localValue.splice(this.localValue.indexOf(tag), 1);
|
||||||
|
this.$refs.badgeSelect.resetSelection();
|
||||||
|
} else if (this.availableTags.includes(tag)) {
|
||||||
|
this.localValue.push(tag);
|
||||||
|
this.$refs.badgeSelect.resetSelection();
|
||||||
|
}else{
|
||||||
|
// input does not match any tag
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeTag(index) {
|
||||||
|
this.localValue.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchTags();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -6,12 +6,14 @@ import store from '@/store';
|
||||||
import Friends from "@/views/Friends.vue";
|
import Friends from "@/views/Friends.vue";
|
||||||
import Inventory from '@/views/Inventory.vue';
|
import Inventory from '@/views/Inventory.vue';
|
||||||
import InventoryDetail from "@/views/InventoryDetail.vue";
|
import InventoryDetail from "@/views/InventoryDetail.vue";
|
||||||
|
import InventoryEdit from "./views/InventoryEdit.vue";
|
||||||
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{path: '/', component: Dashboard, meta: {requiresAuth: true}},
|
{path: '/', component: Dashboard, 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}, props: true},
|
{path: '/inventory/:id', component: InventoryDetail, meta: {requiresAuth: true}, props: true},
|
||||||
|
{path: '/inventory/:id/edit', component: InventoryEdit, meta: {requiresAuth: true}, props: true},
|
||||||
{path: '/friends', component: Friends, meta: {requiresAuth: true}},
|
{path: '/friends', component: Friends, meta: {requiresAuth: 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}},
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
.card-header {
|
.card-header {
|
||||||
background-color: map-get($theme-colors, background-1);
|
background-color: map-get($theme-colors, background-1);
|
||||||
border-bottom: 0 solid transparent;
|
border-bottom: 0 solid transparent;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
|
|
3
frontend/src/scss/_tags.scss
Normal file
3
frontend/src/scss/_tags.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.badge {
|
||||||
|
color: #fff;
|
||||||
|
}
|
|
@ -90,6 +90,7 @@ $body-color: $gray-700;
|
||||||
|
|
||||||
@import "card";
|
@import "card";
|
||||||
@import "forms";
|
@import "forms";
|
||||||
|
@import "tags";
|
||||||
|
|
||||||
#root, body, html {
|
#root, body, html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -19,6 +19,10 @@ export default createStore({
|
||||||
all_friends_servers: null,
|
all_friends_servers: null,
|
||||||
resolver: new FallBackResolver(),
|
resolver: new FallBackResolver(),
|
||||||
unreachable_neighbors: new NeighborsCache(),
|
unreachable_neighbors: new NeighborsCache(),
|
||||||
|
tags: [],
|
||||||
|
properties: [],
|
||||||
|
files: [],
|
||||||
|
categories: [],
|
||||||
availability_policies: [],
|
availability_policies: [],
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -58,9 +62,21 @@ export default createStore({
|
||||||
setAllFriendsServers(state, servers) {
|
setAllFriendsServers(state, servers) {
|
||||||
state.all_friends_servers = servers;
|
state.all_friends_servers = servers;
|
||||||
},
|
},
|
||||||
|
setTags(state, tags) {
|
||||||
|
state.tags = tags;
|
||||||
|
},
|
||||||
|
setProperties(state, properties) {
|
||||||
|
state.properties = properties;
|
||||||
|
},
|
||||||
|
setCategories(state, categories) {
|
||||||
|
state.categories = categories;
|
||||||
|
},
|
||||||
setAvailabilityPolicies(state, availability_policies) {
|
setAvailabilityPolicies(state, availability_policies) {
|
||||||
state.availability_policies = availability_policies;
|
state.availability_policies = availability_policies;
|
||||||
},
|
},
|
||||||
|
setDomains(state, domains) {
|
||||||
|
state.domains = domains;
|
||||||
|
},
|
||||||
logout(state) {
|
logout(state) {
|
||||||
state.user = null;
|
state.user = null;
|
||||||
state.token = null;
|
state.token = null;
|
||||||
|
@ -144,6 +160,18 @@ export default createStore({
|
||||||
commit('setInventoryItems', {url: '/', items})
|
commit('setInventoryItems', {url: '/', items})
|
||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
|
async updateInventoryItem({state, dispatch, getters}, item) {
|
||||||
|
const servers = await dispatch('getHomeServers')
|
||||||
|
const data = {availability_policy: 'friends', ...item}
|
||||||
|
data.files = data.files.map(file => file.id)
|
||||||
|
return await servers.patch(getters.signAuth, '/api/inventory_items/' + item.id + '/', data)
|
||||||
|
},
|
||||||
|
async deleteInventoryItem({state, dispatch, getters}, item) {
|
||||||
|
const servers = await dispatch('getHomeServers')
|
||||||
|
const ret = await servers.delete(getters.signAuth, '/api/inventory_items/' + item.id + '/')
|
||||||
|
dispatch('fetchInventoryItems')
|
||||||
|
return ret
|
||||||
|
},
|
||||||
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/')
|
||||||
|
@ -197,6 +225,71 @@ export default createStore({
|
||||||
const servers = await dispatch('getHomeServers')
|
const servers = await dispatch('getHomeServers')
|
||||||
return await servers.delete(getters.signAuth, '/api/friends/' + id + '/')
|
return await servers.delete(getters.signAuth, '/api/friends/' + id + '/')
|
||||||
},
|
},
|
||||||
|
async fetchTags({state, commit, dispatch, getters}) {
|
||||||
|
if (state.last_load.tags > Date.now() - 1000 * 60 * 60 * 24) {
|
||||||
|
return state.tags
|
||||||
|
}
|
||||||
|
const servers = await dispatch('getHomeServers')
|
||||||
|
const data = await servers.get(getters.signAuth, '/api/tags/')
|
||||||
|
commit('setTags', data)
|
||||||
|
state.last_load.tags = Date.now()
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async fetchProperties({state, commit, dispatch, getters}) {
|
||||||
|
if (state.last_load.properties > Date.now() - 1000 * 60 * 60 * 24) {
|
||||||
|
return state.properties
|
||||||
|
}
|
||||||
|
const servers = await dispatch('getHomeServers')
|
||||||
|
const data = await servers.get(getters.signAuth, '/api/properties/')
|
||||||
|
commit('setProperties', data)
|
||||||
|
state.last_load.properties = Date.now()
|
||||||
|
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.map(policy => ({slug: policy[0], text: policy[1]})))
|
||||||
|
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.map(policy => ({
|
||||||
|
slug: policy[0],
|
||||||
|
text: policy[1]
|
||||||
|
})))
|
||||||
|
commit('setDomains', data.domains)
|
||||||
|
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: {
|
||||||
isLoggedIn(state) {
|
isLoggedIn(state) {
|
||||||
|
|
|
@ -25,6 +25,12 @@
|
||||||
<td class="d-none d-md-table-cell">{{ item.availability_policy }}</td>
|
<td class="d-none d-md-table-cell">{{ item.availability_policy }}</td>
|
||||||
<td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
|
<td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
|
||||||
<td class="table-action">
|
<td class="table-action">
|
||||||
|
<router-link :to="`/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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -28,6 +28,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<button class="btn btn-primary" @click="$router.push('/inventory/' + id + '/edit')">
|
||||||
|
<b-icon-pencil-square></b-icon-pencil-square>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
104
frontend/src/views/InventoryEdit.vue
Normal file
104
frontend/src/views/InventoryEdit.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<main class="content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Edit Item</div>
|
||||||
|
<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">
|
||||||
|
<label for="tags" class="form-label">Tags</label>
|
||||||
|
<tag-field :value="item.tags"></tag-field>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="property" class="form-label">Property</label>
|
||||||
|
<property-field :value="item.properties"></property-field>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="quantity" class="form-label">Quantity</label>
|
||||||
|
<input type="number" class="form-control" id="quantity" name="quantity"
|
||||||
|
placeholder="Enter quantity" v-model="item.owned_quantity">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="availablity_policy" class="form-label">Availablity Policy</label>
|
||||||
|
<select class="form-select" id="availablity_policy" name="availablity_policy"
|
||||||
|
v-model="item.availability_policy">
|
||||||
|
<option v-for="policy in availability_policies" :value="policy.slug"
|
||||||
|
:selected="policy.slug === item.availability_policy">
|
||||||
|
{{ policy.text }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%"
|
||||||
|
@click="updateInventoryItem(item)">Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import {mapActions, mapGetters, mapState} from "vuex";
|
||||||
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
import TagField from "@/components/TagField.vue";
|
||||||
|
import PropertyField from "@/components/PropertyField.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "InventoryEdit",
|
||||||
|
components: {
|
||||||
|
BaseLayout,
|
||||||
|
TagField,
|
||||||
|
PropertyField,
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["inventory_items"]),
|
||||||
|
...mapState(["availability_policies"]),
|
||||||
|
item() {
|
||||||
|
return {
|
||||||
|
tags: [],
|
||||||
|
properties: [],
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
owned_quantity: 0,
|
||||||
|
image: "",
|
||||||
|
files: [],
|
||||||
|
...this.inventory_items.find(item => item.id === parseInt(this.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(["fetchInventoryItems", "updateInventoryItem", "fetchInfo"]),
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.fetchInfo();
|
||||||
|
await this.fetchInventoryItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
Loading…
Reference in a new issue