Added tags

This commit is contained in:
Maximilian Giller 2021-01-05 02:00:10 +01:00
parent 1d5df2a4b1
commit b6d51eaae5
11 changed files with 349 additions and 15 deletions

View file

@ -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");

View file

@ -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");

View file

@ -27,6 +27,8 @@ class JsonBuilder
"duration" => "",
"user_id" => "",
"project_id" => "",
"running" => "",
"tags" => "",
"start_device_id" => ""
);

View 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>

View file

@ -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"
}
]
};

View 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>

View file

@ -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;
}

View file

@ -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;

View file

@ -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'

View file

@ -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>

View file

@ -53,6 +53,7 @@ export default {
},
created: () => {
store.dispatch("loadProjects");
store.dispatch("loadTags");
store.dispatch("loadRunningRecords");
store.dispatch("loadRecords", { limit: 10, finished: true });
}