Added tags
This commit is contained in:
parent
1d5df2a4b1
commit
b6d51eaae5
11 changed files with 349 additions and 15 deletions
|
@ -13,6 +13,7 @@ class AddTagToRecordBranch extends ApiBranch
|
|||
|
||||
function post(ParamCleaner $params)
|
||||
{
|
||||
// !! Security issue: Could combined arbitrary tag and record, not restricted by user
|
||||
$record_id = $params->get("record_id");
|
||||
$tag_id = $params->get("tag_id");
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ class RemoveTagFromRecordBranch extends ApiBranch
|
|||
|
||||
function post(ParamCleaner $params)
|
||||
{
|
||||
// !! Security issue: Could combined arbitrary tag and record, not restricted by user
|
||||
$record_id = $params->get("record_id");
|
||||
$tag_id = $params->get("tag_id");
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ class JsonBuilder
|
|||
"duration" => "",
|
||||
"user_id" => "",
|
||||
"project_id" => "",
|
||||
"running" => "",
|
||||
"tags" => "",
|
||||
"start_device_id" => ""
|
||||
);
|
||||
|
||||
|
|
69
src/components/forms/FormTagAdd.vue
Normal file
69
src/components/forms/FormTagAdd.vue
Normal file
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<form @submit="submitForm">
|
||||
<b-input-group id="form">
|
||||
<b-form-input id="name" v-model="form.name" placeholder="Tag name" trim>
|
||||
</b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button variant="outline-secondary" type="submit" :disabled="working">
|
||||
<b-spinner v-if="working" small />
|
||||
Add tag
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from "@/store";
|
||||
|
||||
export default {
|
||||
name: "FormTagAdd",
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
name: ""
|
||||
},
|
||||
working: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Submits the form. Assupmtion: Form is valid, based on required flags.
|
||||
*/
|
||||
submitForm: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.form.name == "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.failed = false;
|
||||
this.working = true;
|
||||
|
||||
// Try to login
|
||||
store
|
||||
.dispatch("addTag", {
|
||||
name: this.form.name
|
||||
})
|
||||
.then(() => {
|
||||
this.form.name = "";
|
||||
this.working = false;
|
||||
this.$emit("submit");
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e);
|
||||
|
||||
this.working = false;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
#form
|
||||
max-width: 20rem
|
||||
margin: auto
|
||||
</style>
|
|
@ -26,8 +26,16 @@
|
|||
{{ getDurationTimestamp(data.item.duration) }}
|
||||
</template>
|
||||
|
||||
<template #cell(tags)="data">
|
||||
<JugglTagField :recordId="data.item.record_id" />
|
||||
</template>
|
||||
|
||||
<template #cell(details)="data">
|
||||
<b-button size="sm" @click="data.toggleDetails" variant="outline-dark">
|
||||
<b-button
|
||||
size="sm"
|
||||
@click="data.toggleDetails"
|
||||
variant="outline-secondary"
|
||||
>
|
||||
<b-icon class="icon-btn" icon="gear" />
|
||||
</b-button>
|
||||
</template>
|
||||
|
@ -55,14 +63,16 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import store from "@/store";
|
||||
import FormRecordDetails from "@/components/forms/FormRecordDetails";
|
||||
import JugglTagField from "@/components/juggl/JugglTagField";
|
||||
import { helperService } from "@/services/helper.service.js";
|
||||
import store from "@/store";
|
||||
|
||||
export default {
|
||||
name: "JugglRecordsList",
|
||||
components: {
|
||||
FormRecordDetails
|
||||
FormRecordDetails,
|
||||
JugglTagField
|
||||
},
|
||||
props: {
|
||||
records: {
|
||||
|
@ -96,6 +106,10 @@ export default {
|
|||
key: "duration",
|
||||
label: "Duration"
|
||||
},
|
||||
{
|
||||
key: "tags",
|
||||
label: "Tags"
|
||||
},
|
||||
{
|
||||
key: "details",
|
||||
label: "Details"
|
||||
|
@ -104,7 +118,7 @@ export default {
|
|||
runningFields: [
|
||||
{
|
||||
key: "stop",
|
||||
label: "Stop"
|
||||
label: "Finish"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
145
src/components/juggl/JugglTagField.vue
Normal file
145
src/components/juggl/JugglTagField.vue
Normal file
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<div :id="containerId" class="tag-container">
|
||||
<div
|
||||
class="tag-item"
|
||||
v-for="tag in addedTags"
|
||||
:key="tag.record_tag_id"
|
||||
@click="() => removeTag(tag)"
|
||||
>
|
||||
{{ tag.name }}
|
||||
</div>
|
||||
|
||||
<div :id="btnId">
|
||||
<b-icon icon="plus" />
|
||||
</div>
|
||||
|
||||
<b-popover
|
||||
:target="btnId"
|
||||
triggers="click"
|
||||
placement="auto"
|
||||
:container="containerId"
|
||||
>
|
||||
<div>
|
||||
<div class="tag-container">
|
||||
<div
|
||||
class="tag-item"
|
||||
v-for="tag in unusedTags"
|
||||
:key="tag.record_tag_id"
|
||||
@click="() => addTag(tag)"
|
||||
>
|
||||
{{ tag.name }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<FormTagAdd @submit="() => reloadTags()" />
|
||||
</div>
|
||||
</b-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormTagAdd from "@/components/forms/FormTagAdd";
|
||||
import store from "@/store";
|
||||
|
||||
export default {
|
||||
name: "JugglTagField",
|
||||
components: {
|
||||
FormTagAdd
|
||||
},
|
||||
props: {
|
||||
recordId: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
record: {}
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.record = store.getters.getRecordById(this.recordId);
|
||||
},
|
||||
methods: {
|
||||
getTagById: function(id) {
|
||||
return store.getters.getTagById(id);
|
||||
},
|
||||
addTag: function(tag) {
|
||||
this.record.tags.push(tag);
|
||||
store.dispatch("addTagToRecord", {
|
||||
tagId: tag.record_tag_id,
|
||||
recordId: this.recordId
|
||||
});
|
||||
},
|
||||
reloadTags: function() {
|
||||
store.dispatch("loadTags");
|
||||
},
|
||||
removeTag: function(tag) {
|
||||
const index = this.record.tags.indexOf(tag);
|
||||
if (index > -1) {
|
||||
this.record.tags.splice(index, 1);
|
||||
store.dispatch("removeTagFromRecord", {
|
||||
tagId: tag.record_tag_id,
|
||||
recordId: this.recordId
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
unusedTags: function() {
|
||||
return Object.values(this.allTags).filter(
|
||||
tag => !this.usedTagIds.includes(tag.record_tag_id)
|
||||
);
|
||||
},
|
||||
allTags: function() {
|
||||
return store.getters.tags;
|
||||
},
|
||||
addedTags: function() {
|
||||
return this.record.tags;
|
||||
},
|
||||
usedTagIds: function() {
|
||||
var ids = [];
|
||||
Object.values(this.addedTags).forEach(tag => {
|
||||
ids.push(tag.record_tag_id);
|
||||
});
|
||||
return ids;
|
||||
},
|
||||
containerId: function() {
|
||||
return "added-tags-for-" + this.record.record_id;
|
||||
},
|
||||
btnId: function() {
|
||||
return "add-tags-for-" + this.record.record_id;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
@import '@/style/theme.sass'
|
||||
|
||||
.tag-container
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex-wrap: wrap
|
||||
justify-content: left
|
||||
align-content: flex-start
|
||||
|
||||
> *
|
||||
margin: 2px
|
||||
border-radius: 5px
|
||||
padding: 1px 6px
|
||||
text-align: center
|
||||
cursor: pointer
|
||||
border: 1px solid $grey
|
||||
|
||||
&:hover
|
||||
background-color: $grey
|
||||
color: $background-primary
|
||||
|
||||
> .tag-item
|
||||
border: 1px solid $primary
|
||||
|
||||
&:hover
|
||||
background-color: $primary
|
||||
color: $white
|
||||
</style>
|
|
@ -28,6 +28,15 @@ export const jugglService = {
|
|||
});
|
||||
},
|
||||
|
||||
getTags() {
|
||||
return apiService.post("/getRecordTags.php").then(r => {
|
||||
return {
|
||||
data: r.data,
|
||||
msg: ""
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getRecord(recordId) {
|
||||
return apiService
|
||||
.post("/getRecord.php", {
|
||||
|
@ -109,6 +118,47 @@ export const jugglService = {
|
|||
});
|
||||
},
|
||||
|
||||
addTag(name) {
|
||||
return apiService
|
||||
.post("/addRecordTag.php", {
|
||||
tag_name: name
|
||||
})
|
||||
.then(r => {
|
||||
return {
|
||||
data: r.data,
|
||||
msg: ""
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
addTagToRecord(tagId, recordId) {
|
||||
return apiService
|
||||
.post("/addTagToRecord.php", {
|
||||
tag_id: tagId,
|
||||
record_id: recordId
|
||||
})
|
||||
.then(r => {
|
||||
return {
|
||||
data: r.data,
|
||||
msg: ""
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
removeTagFromRecord(tagId, recordId) {
|
||||
return apiService
|
||||
.post("/removeTagFromRecord.php", {
|
||||
tag_id: tagId,
|
||||
record_id: recordId
|
||||
})
|
||||
.then(r => {
|
||||
return {
|
||||
data: r.data,
|
||||
msg: ""
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
startRecord(projectId, startTime = null) {
|
||||
if (startTime == null) startTime = new Date();
|
||||
return apiService
|
||||
|
@ -142,11 +192,11 @@ export const jugglService = {
|
|||
|
||||
function processRecords(data) {
|
||||
Object.values(data.records).forEach(rec => {
|
||||
rec.running = rec.end_time === null;
|
||||
|
||||
if (rec.running) {
|
||||
rec.duration = helperService.calcDurationInSeconds(rec.start_time);
|
||||
}
|
||||
|
||||
rec.tags = Object.values(rec.tags);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,9 @@ import { jugglService } from "@/services/juggl.service.js";
|
|||
export const juggl = {
|
||||
state: {
|
||||
apiUrl: "https://juggl.giller.dev/api",
|
||||
projects: {},
|
||||
records: {},
|
||||
projects: [],
|
||||
records: [],
|
||||
tags: [],
|
||||
user: undefined,
|
||||
auth: undefined,
|
||||
recordsLimit: 0
|
||||
|
@ -16,6 +17,9 @@ export const juggl = {
|
|||
setRecords(state, records) {
|
||||
state.records = records;
|
||||
},
|
||||
setTags(state, tags) {
|
||||
state.tags = tags;
|
||||
},
|
||||
setRecordsLimit(state, limit) {
|
||||
state.recordsLimit = limit;
|
||||
},
|
||||
|
@ -46,6 +50,7 @@ export const juggl = {
|
|||
isLoggedIn: state => !!state.auth,
|
||||
records: state => state.records,
|
||||
projects: state => state.projects,
|
||||
tags: state => state.tags,
|
||||
projectIds: state => {
|
||||
var projectIds = [];
|
||||
Object.values(state.projects).forEach(project => {
|
||||
|
@ -84,6 +89,9 @@ export const juggl = {
|
|||
project => project.project_id === id
|
||||
);
|
||||
},
|
||||
getTagById: (state, getters) => id => {
|
||||
return Object.values(getters.tags).find(tag => tag.record_tag_id === id);
|
||||
},
|
||||
getRecordById: (state, getters) => id => {
|
||||
return Object.values(getters.records).find(
|
||||
record => record.record_id === id
|
||||
|
@ -101,6 +109,11 @@ export const juggl = {
|
|||
commit("setProjects", r.data.projects);
|
||||
});
|
||||
},
|
||||
loadTags({ commit }) {
|
||||
return jugglService.getTags().then(r => {
|
||||
commit("setTags", r.data.record_tags);
|
||||
});
|
||||
},
|
||||
loadUser({ commit }) {
|
||||
return jugglService
|
||||
.getUser()
|
||||
|
@ -196,6 +209,39 @@ export const juggl = {
|
|||
return true;
|
||||
});
|
||||
},
|
||||
addTag(context, { name }) {
|
||||
return jugglService
|
||||
.addTag(name)
|
||||
.catch(() => {
|
||||
return false;
|
||||
})
|
||||
.then(() => {
|
||||
this.dispatch("loadTags");
|
||||
return true;
|
||||
});
|
||||
},
|
||||
addTagToRecord(context, { tagId, recordId }) {
|
||||
return jugglService
|
||||
.addTagToRecord(tagId, recordId)
|
||||
.catch(() => {
|
||||
return false;
|
||||
})
|
||||
.then(() => {
|
||||
// TODO: Manualy add tag
|
||||
return true;
|
||||
});
|
||||
},
|
||||
removeTagFromRecord(context, { tagId, recordId }) {
|
||||
return jugglService
|
||||
.removeTagFromRecord(tagId, recordId)
|
||||
.catch(() => {
|
||||
return false;
|
||||
})
|
||||
.then(() => {
|
||||
// TODO: Manualy remove tag
|
||||
return true;
|
||||
});
|
||||
},
|
||||
startRecord(context, projectId) {
|
||||
if (projectId === undefined) {
|
||||
return false;
|
||||
|
|
|
@ -62,9 +62,12 @@ a
|
|||
border-radius: 0
|
||||
background: #0000
|
||||
|
||||
.dropdown-menu
|
||||
background-color: $background-primary !important
|
||||
border: 1px solid $primary !important
|
||||
.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after
|
||||
border-bottom-color: $white !important
|
||||
|
||||
.dropdown-menu, .b-popover
|
||||
background-color: $background-secondary !important
|
||||
border: 1px solid $white !important
|
||||
color: $primary
|
||||
|
||||
.b-time *,
|
||||
|
@ -134,6 +137,9 @@ div.card
|
|||
pre
|
||||
color: $font-primary !important
|
||||
|
||||
hr
|
||||
border-color: $white !important
|
||||
|
||||
|
||||
// Import Bootstrap and BootstrapVue source SCSS files
|
||||
@import '~bootstrap/scss/bootstrap.scss'
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<template>
|
||||
<LayoutNavbarPrivate>
|
||||
<LayoutNavbarPrivate title="History">
|
||||
<section>
|
||||
<h2 class="center">History</h2>
|
||||
<div class="center">
|
||||
<b-spinner v-if="working"></b-spinner>
|
||||
<div class="center" v-if="working">
|
||||
<b-spinner></b-spinner>
|
||||
</div>
|
||||
<JugglRecordsList :records="finishedRecords" v-if="!working" />
|
||||
</section>
|
||||
|
|
|
@ -53,6 +53,7 @@ export default {
|
|||
},
|
||||
created: () => {
|
||||
store.dispatch("loadProjects");
|
||||
store.dispatch("loadTags");
|
||||
store.dispatch("loadRunningRecords");
|
||||
store.dispatch("loadRecords", { limit: 10, finished: true });
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue