Skip to content

Commit 7ececcb

Browse files
committed
fix: fixed bug when editing implicit parent categories, improved testing
fixes ActivityWatch/activitywatch#580
1 parent f8de6dd commit 7ececcb

3 files changed

Lines changed: 85 additions & 21 deletions

File tree

src/store/modules/categories.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import * as _ from 'lodash';
1+
import _ from 'lodash';
22
import {
33
saveClasses,
44
loadClasses,
55
defaultCategories,
66
build_category_hierarchy,
7+
createMissingParents,
78
} from '~/util/classes';
89

910
// initial state
@@ -19,6 +20,7 @@ const getters = {
1920
return _.sortBy(hier, [c => c.id || 0]);
2021
},
2122
all_categories: state => {
23+
// Returns a list of category names (a list of list of strings)
2224
return _.uniqBy(
2325
_.flatten(
2426
state.classes.map(c => {
@@ -52,6 +54,8 @@ const actions = {
5254
// mutations
5355
const mutations = {
5456
loadClasses(state, classes) {
57+
classes = createMissingParents(classes);
58+
5559
let i = 0;
5660
state.classes = classes.map(c => Object.assign(c, { id: i++ }));
5761
console.log('Loaded classes:', state.classes);
@@ -66,8 +70,8 @@ const mutations = {
6670
},
6771
updateClass(state, new_class) {
6872
console.log('Updating class:', new_class);
69-
7073
const old_class = _.cloneDeep(state.classes[new_class.id]);
74+
7175
if (new_class.id === undefined || new_class.id === null) {
7276
new_class.id = _.max(_.map(state.classes, 'id')) + 1;
7377
state.classes.push(new_class);
@@ -97,7 +101,11 @@ const mutations = {
97101
},
98102
restoreDefaultClasses(state) {
99103
let i = 0;
100-
state.classes = defaultCategories.map(c => Object.assign(c, { id: i++ }));
104+
state.classes = createMissingParents(defaultCategories).map(c => Object.assign(c, { id: i++ }));
105+
state.classes_unsaved_changes = true;
106+
},
107+
clearAll(state) {
108+
state.classes = [];
101109
state.classes_unsaved_changes = true;
102110
},
103111
saveCompleted(state) {

src/util/classes.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const _ = require('lodash');
1+
import _ from 'lodash';
22

33
const level_sep = '>';
44

@@ -20,6 +20,8 @@ export interface Category {
2020
children?: Category[];
2121
}
2222

23+
// The default categories
24+
// Should be run through createMissingParents before being used in most cases.
2325
export const defaultCategories: Category[] = [
2426
{
2527
name: ['Work'],
@@ -53,38 +55,44 @@ export const defaultCategories: Category[] = [
5355
{ name: ['Uncategorized'], rule: { type: null }, data: { color: '#ccc' } },
5456
];
5557

56-
export function build_category_hierarchy(classes: Category[]): Category[] {
57-
function annotate(c: Category) {
58-
const ch = c.name;
59-
c.name_pretty = ch.join(level_sep);
60-
c.subname = ch.slice(-1)[0];
61-
c.parent = ch.length > 1 ? ch.slice(0, -1) : null;
62-
c.depth = ch.length - 1;
63-
return c;
64-
}
65-
66-
const new_classes = classes.slice().map(c => annotate(c));
58+
function annotate(c: Category) {
59+
const ch = c.name;
60+
c.name_pretty = ch.join(level_sep);
61+
c.subname = ch.slice(-1)[0];
62+
c.parent = ch.length > 1 ? ch.slice(0, -1) : null;
63+
c.depth = ch.length - 1;
64+
return c;
65+
}
6766

68-
// Insert dangling/undefined parents
69-
const all_full_names = new Set(new_classes.map(c => c.name.join(level_sep)));
67+
export function createMissingParents(classes: Category[]): Category[] {
68+
// Creates parents for categories that are missing theirs (implicit parents)
69+
classes = _.cloneDeep(classes);
70+
classes = classes.slice().map(c => annotate(c));
71+
const all_full_names = new Set(classes.map(c => c.name.join(level_sep)));
7072

71-
function createMissingParents(children) {
73+
function _createMissing(children: Category[]) {
7274
children
7375
.map(c => c.parent)
7476
.filter(p => !!p)
7577
.map(p => {
7678
const name = p.join(level_sep);
7779
if (p && !all_full_names.has(name)) {
7880
const new_parent = annotate({ name: p, rule: { type: null } });
81+
//console.log('Creating missing parent:', new_parent);
7982
classes.push(new_parent);
8083
all_full_names.add(name);
8184
// New parent might not be top-level, so we need to recurse
82-
createMissingParents([new_parent]);
85+
_createMissing([new_parent]);
8386
}
8487
});
8588
}
8689

87-
createMissingParents(new_classes);
90+
_createMissing(classes);
91+
return classes;
92+
}
93+
94+
export function build_category_hierarchy(classes: Category[]): Category[] {
95+
classes = createMissingParents(classes);
8896

8997
function assignChildren(classes_at_level: Category[]) {
9098
return classes_at_level.map(cls => {

test/unit/store/categories.test.node.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import store from '~/store';
2+
import { createMissingParents, defaultCategories } from '~/util/classes';
23

3-
test('loads categories', () => {
4+
beforeEach(() => {
5+
store.commit('categories/clearAll');
6+
});
7+
8+
test('loads default categories', () => {
49
// Load categories
510
expect(store.state.categories.classes).toHaveLength(0);
611
store.commit('categories/restoreDefaultClasses');
@@ -24,3 +29,46 @@ test('loads categories', () => {
2429
expect(store.getters['categories/all_categories']).not.toHaveLength(0);
2530
expect(store.getters['categories/classes_hierarchy']).not.toHaveLength(0);
2631
});
32+
33+
test('loads custom categories', () => {
34+
expect(store.state.categories.classes).toHaveLength(0);
35+
store.commit('categories/loadClasses', [{ name: ['Test'] }]);
36+
expect(store.getters['categories/all_categories']).toHaveLength(1);
37+
});
38+
39+
test('get category hierarchy', () => {
40+
store.commit('categories/restoreDefaultClasses');
41+
const hier = store.getters['categories/classes_hierarchy'];
42+
expect(hier).not.toHaveLength(0);
43+
});
44+
45+
test('create missing parents', () => {
46+
const cats = createMissingParents([{ name: ['Test', 'Subcat'] }]);
47+
expect(cats).toHaveLength(2);
48+
});
49+
50+
test('update implicit parent category', () => {
51+
// The default categories have implicit Media and Comms categories (with 'No Rule')
52+
// Tests against https://github.com/ActivityWatch/activitywatch/issues/580
53+
store.commit('categories/restoreDefaultClasses');
54+
55+
// Check that the label is available
56+
expect(store.getters['categories/all_categories']).toContainEqual(['Media']);
57+
58+
// Get category and modify it
59+
const media_cat = store.getters['categories/get_category'](['Media']);
60+
expect(media_cat.id).not.toBeUndefined();
61+
const new_media_cat = { ...media_cat, name: ['Media2'], data: { test: true } };
62+
store.commit('categories/updateClass', new_media_cat);
63+
64+
// Check that category was modified correctly
65+
const media2_cat = store.getters['categories/get_category'](['Media2']);
66+
expect(media2_cat.data.test).toBe(true);
67+
68+
// Check that child was modified correctly when parent name changed
69+
const music_cat = store.getters['categories/get_category'](['Media2', 'Music']);
70+
expect(music_cat.id).not.toBeUndefined();
71+
72+
// Check that defaultCategories haven't mutated
73+
expect(defaultCategories.map(c => c.name)).toContainEqual(['Media', 'Music']);
74+
});

0 commit comments

Comments
 (0)