Skip to content

Commit

Permalink
Merge pull request #320 from ActivityWatch/dev/edit-timeline
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikBjare committed Mar 3, 2022
2 parents 3afeb65 + f65a035 commit 5ace2bf
Show file tree
Hide file tree
Showing 9 changed files with 2,032 additions and 1,734 deletions.
6 changes: 5 additions & 1 deletion babel.config.js
@@ -1,5 +1,9 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: ['lodash', '@babel/plugin-syntax-dynamic-import'],
plugins: [
'lodash',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-nullish-coalescing-operator',
],
comments: false,
};
3,582 changes: 1,891 additions & 1,691 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -67,6 +67,7 @@
"devDependencies": {
"@babel/cli": "^7.16.0",
"@babel/core": "^7.16.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/preset-env": "^7.16.4",
Expand Down
109 changes: 69 additions & 40 deletions src/components/EventEditor.vue
@@ -1,53 +1,65 @@
<template lang="pug">
b-modal(:id="'edit-modal-' + event.id", ref="eventEditModal", title="Edit event", centered, hide-footer)
table(style="width: 100%")
tr
th Bucket
td {{ bucket_id }}
tr
th ID
td {{ event.id }}
tr
th Start
datetime(type="datetime" v-model="start")
tr
th End
datetime(type="datetime" v-model="end")
tr
th Duration
td {{ editedEvent.duration | friendlyduration }}
div(v-if="!editedEvent")
| Loading event...

hr
div(v-else)
table(style="width: 100%")
tr
th Bucket
td {{ bucket_id }}
tr
th ID
td {{ event.id }}
tr
th Start
datetime(type="datetime" v-model="start")
tr
th End
datetime(type="datetime" v-model="end")
tr
th Duration
td {{ editedEvent.duration | friendlyduration }}

table(style="width: 100%")
tr
th Key
th Value
tr(v-for="(v, k) in event.data")
td
b-input(disabled, :value="k", size="sm")
td
b-checkbox(v-if="typeof event.data[k] === typeof true", v-model="editedEvent.data[k]", style="margin: 0.25em")
b-input(v-if="typeof event.data[k] === typeof 'string'", v-model="editedEvent.data[k]", size="sm")
hr

hr
table(style="width: 100%")
tr
th Key
th Value
tr(v-for="(v, k) in editedEvent.data" :key="k")
td
b-input(disabled, :value="k", size="sm")
td
b-checkbox(v-if="typeof event.data[k] === typeof true", v-model="editedEvent.data[k]", style="margin: 0.25em")
b-input(v-if="typeof event.data[k] === typeof 'string'", v-model="editedEvent.data[k]", size="sm")

div.float-left
b-button.mx-1(@click="delete_(); close();" variant="danger")
icon.mx-1(name="trash")
| Delete
div.float-right
b-button.mx-1(@click="close")
icon.mx-1(name="times")
| Cancel
b-button.mx-1(@click="save(); close();", variant="primary")
icon.mx-1(name="save")
| Save
hr

div.float-left
b-button.mx-1(@click="delete_(); close();" variant="danger")
icon.mx-1(name="trash")
| Delete
div.float-right
b-button.mx-1(@click="close")
icon.mx-1(name="times")
| Cancel
b-button.mx-1(@click="save(); close();", variant="primary")
icon.mx-1(name="save")
| Save
</template>

<style lang="scss"></style>

<script>
// This EventEditor can be used to edit events in a specific bucket.
//
// It is used in:
// - Stopwatch
// - Bucket viewer
// - Timeline (on event-click)
// - Search (soon)
import moment from 'moment';
import 'vue-awesome/icons/times';
Expand All @@ -62,7 +74,7 @@ export default {
},
data() {
return {
editedEvent: JSON.parse(JSON.stringify(this.event)),
editedEvent: null,
};
},
computed: {
Expand All @@ -86,17 +98,34 @@ export default {
},
},
},
watch: {
async event() {
await this.getEvent();
},
},
mounted: async function () {
await this.getEvent();
},
methods: {
async save() {
// This emit needs to be called first, otherwise it won't occur for some reason
// FIXME: but what if the replace fails? Then UI will incorrectly think event was replaced?
this.$emit('save', this.editedEvent);
await this.$aw.replaceEvent(this.bucket_id, this.editedEvent);
},
async delete_() {
// This emit needs to be called first, otherwise it won't occur for some reason
// FIXME: but what if the replace fails? Then UI will incorrectly think event was deleted?
this.$emit('delete', this.event);
await this.$aw.deleteEvent(this.bucket_id, this.event.id);
},
async getEvent() {
if (this.event.id) {
this.editedEvent = await this.$aw.getEvent(this.bucket_id, this.event.id);
} else {
this.editedEvent = null;
}
},
close() {
this.$refs.eventEditModal.hide();
this.$emit('close', this.event);
Expand Down
2 changes: 1 addition & 1 deletion src/components/InputTimeInterval.vue
Expand Up @@ -34,7 +34,7 @@ div
input(type="date", v-model="end")
button(
class="btn btn-outline-dark btn-sm",
type="button",
type="button",
:disabled="mode == 'range' && (invalidDaterange || emptyDaterange || daterangeTooLong)",
@click="valueChanged"
) Update
Expand Down
1 change: 1 addition & 0 deletions src/views/Timeline.vue
Expand Up @@ -43,6 +43,7 @@ export default {
},
methods: {
getBuckets: async function () {
if (this.daterange == null) return;
this.buckets = await this.$store.dispatch('buckets/getBucketsWithEvents', {
start: this.daterange[0].format(),
end: this.daterange[1].format(),
Expand Down
2 changes: 2 additions & 0 deletions src/visualizations/EventList.vue
Expand Up @@ -29,7 +29,9 @@ div
icon(name="edit")
| Edit

// TODO: Don't create one event-editor per event
event-editor(
v-if="editable"
:event="event", :bucket_id="bucket_id",
@save="(e) => $emit('save', e)", @delete="removeEvent"
)
Expand Down
62 changes: 61 additions & 1 deletion src/visualizations/VisTimeline.vue
@@ -1,6 +1,12 @@
<template lang="pug">
div
div#visualization

div.small.my-2
i Buckets with no events in the queried range will be hidden.

div(v-if="editingEvent")
EventEditor(:event="editingEvent" :bucket_id="editingEventBucket")
</template>

<style lang="scss">
Expand Down Expand Up @@ -36,8 +42,12 @@ import { getColorFromString, getTitleAttr } from '../util/color';
import { Timeline } from 'vis-timeline/esnext';
import 'vis-timeline/styles/vis-timeline-graph2d.css';
import EventEditor from '~/components/EventEditor.vue';
export default {
components: {
EventEditor,
},
props: {
buckets: { type: Array },
showRowLabels: { type: Boolean },
Expand All @@ -48,6 +58,8 @@ export default {
return {
timeline: null,
filterShortEvents: true,
items: [],
groups: [],
options: {
zoomMin: 1000 * 60, // 10min in milliseconds
zoomMax: 1000 * 60 * 60 * 24 * 31 * 3, // about three months in milliseconds
Expand All @@ -58,6 +70,8 @@ export default {
delay: 0,
},
},
editingEvent: null,
editingEventBucket: null,
};
},
computed: {
Expand All @@ -82,6 +96,7 @@ export default {
new Date(e.timestamp),
new Date(moment(e.timestamp).add(e.duration, 'seconds').valueOf()),
getColorFromString(getTitleAttr(bucket, e)),
e,
]);
});
});
Expand All @@ -97,7 +112,7 @@ export default {
}
// Build groups
const groups = _.map(this.buckets, (bucket, bidx) => {
let groups = _.map(this.buckets, (bucket, bidx) => {
return { id: bidx, content: this.showRowLabels ? bucket.id : '' };
});
Expand Down Expand Up @@ -148,15 +163,60 @@ export default {
this.options.max = end;
this.timeline.setOptions(this.options);
this.timeline.setWindow(start, end);
// Hide buckets with no events in the queried range
const count = _.countBy(items, i => i.group);
groups = _.filter(groups, g => {
return count[g.id] && count[g.id] > 0;
});
this.timeline.setData({ groups: groups, items: items });
this.items = items;
this.groups = groups;
}
},
},
mounted() {
this.$nextTick(() => {
const el = this.$el.querySelector('#visualization');
this.timeline = new Timeline(el, [], [], this.options);
this.timeline.on('select', properties => {
// Sends both 'press' and 'tap' events, only one should trigger
if (properties.event.type == 'tap') {
this.onSelect(properties);
}
});
});
},
methods: {
openEditor: function () {
const id = 'edit-modal-' + this.editingEvent.id;
this.$bvModal.show(id);
},
onSelect: async function (properties) {
if (properties.items.length == 0) {
return;
} else if (properties.items.length == 1) {
const event = this.chartData[properties.items[0]][6];
const groupId = this.items[properties.items[0]].group;
const bucketId = _.find(this.groups, g => g.id == groupId).content;
// We retrieve the full event to ensure if's not cut-off by the query range
// See: https://github.com/ActivityWatch/aw-webui/pull/320#issuecomment-1056921587
this.editingEvent = await this.$aw.getEvent(bucketId, event.id);
this.editingEventBucket = bucketId;
this.$nextTick(() => {
console.log('Editing event', event, ', in bucket', bucketId);
this.openEditor();
});
alert(
"Note: Changes won't be reflected in the timeline until the page is refreshed. This will be improved in a future version."
);
} else {
alert('selected multiple items: ' + JSON.stringify(properties.items));
}
},
},
};
</script>
1 change: 1 addition & 0 deletions vue.config.js
Expand Up @@ -57,5 +57,6 @@ module.exports = {
transpileDependencies: [
// can be string or regex
'vis-data',
'vis-timeline',
],
};

1 comment on commit 5ace2bf

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are screenshots of this commit:

Screenshots using aw-server v0.11.0 (click to expand)

Screenshots using aw-server-rust master (click to expand)

Screenshots using aw-server-rust v0.11.0 (click to expand)

CML watermark

Please sign in to comment.