Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/SelectableVisualization.vue
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export default {

const datasets = buildBarchartDataset(
this.activityStore.category.by_period,
this.categoryStore.classes
this.categoryStore.category_set.categories
);

// Return dataset if data found, else return null (indicating no data)
Expand Down
113 changes: 67 additions & 46 deletions src/stores/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,54 @@ import _ from 'lodash';
import {
saveClasses,
loadClasses,
loadSets,
cleanCategory,
defaultCategories,
build_category_hierarchy,
createMissingParents,
annotate,
Category,
Rule,
CategorySet,
} from '~/util/classes';
import { getColorFromCategory } from '~/util/color';
import { defineStore } from 'pinia';

interface State {
classes: Category[];
classes_unsaved_changes: boolean;
// The classes of the currently active category set
category_set: CategorySet;

// A list of IDs for existing category sets
category_sets: CategorySet[];

unsaved_changes: boolean;
}

export const useCategoryStore = defineStore('categories', {
state: (): State => ({
classes: [],
classes_unsaved_changes: false,
category_set: { id: '', categories: [] },
category_sets: [],
unsaved_changes: false,
}),

// getters
getters: {
classes_clean(): Category[] {
return this.classes.map(cleanCategory);
},
classes_hierarchy() {
const hier = build_category_hierarchy(_.cloneDeep(this.classes));
classes_hierarchy(this: State) {
const hier = build_category_hierarchy(_.cloneDeep(this.category_set.categories));
return _.sortBy(hier, [c => c.id || 0]);
},
classes_for_query(): [string[], Rule][] {
return this.classes
classes_for_query(this: State): [string[], Rule][] {
return this.category_set.categories
.filter(c => c.rule.type !== null)
.map(c => {
return [c.name, c.rule];
});
},
all_categories(): string[][] {
all_categories(this: State): string[][] {
// Returns a list of category names (a list of list of strings)
return _.uniqBy(
_.flatten(
this.classes.map((c: Category) => {
this.category_set.categories.map((c: Category) => {
const l = [];
for (let i = 1; i <= c.name.length; i++) {
l.push(c.name.slice(0, i));
Expand All @@ -60,7 +65,7 @@ export const useCategoryStore = defineStore('categories', {
if (typeof category_arr === 'string' || category_arr instanceof String)
console.error('Passed category was string, expected array. Lookup will fail.');

const match = this.classes.find(c => _.isEqual(c.name, category_arr));
const match = this.category_set.categories.find(c => _.isEqual(c.name, category_arr));
if (!match) {
if (!_.isEqual(category_arr, ['Uncategorized']))
console.error("Couldn't find category: ", category_arr);
Expand All @@ -72,12 +77,14 @@ export const useCategoryStore = defineStore('categories', {
},
get_category_by_id(this: State) {
return (id: number) => {
return annotate(_.cloneDeep(this.classes.find((c: Category) => c.id == id)));
return annotate(
_.cloneDeep(this.category_set.categories.find((c: Category) => c.id == id))
);
};
},
get_category_color() {
return (cat: string[]) => {
return getColorFromCategory(this.get_category(cat), this.classes);
return getColorFromCategory(this.get_category(cat), this.category_set.categories);
};
},
category_select() {
Expand All @@ -101,71 +108,85 @@ export const useCategoryStore = defineStore('categories', {
},

actions: {
load(this: State, classes: Category[] = null) {
if (classes === null) {
classes = loadClasses();
load(this: State, category_set?: CategorySet) {
if (category_set) {
this.category_set = category_set;
this.category_sets = [this.category_set];
} else {
// loadSets always returns the current set first
this.category_sets = loadSets();
this.category_set = this.category_sets[0];
}
classes = createMissingParents(classes);

let i = 0;
this.classes = classes.map(c => Object.assign(c, { id: i++ }));
this.classes_unsaved_changes = false;
this.unsaved_changes = false;
},
save() {
const r = saveClasses(this.classes);
this.classes_unsaved_changes = false;
save(this: State) {
const r = saveClasses(this.category_set);
this.unsaved_changes = false;
return r;
},

// mutations
import(this: State, classes: Category[]) {
let i = 0;
// overwrite id even if already set
this.classes = classes.map(c => Object.assign(c, { id: i++ }));
this.classes_unsaved_changes = true;
import(this: State, category_set: CategorySet) {
this.category_sets.push(category_set);
this.unsaved_changes = true;
},
updateClass(this: State, new_class: Category) {
console.log('Updating class:', new_class);
const old_class = this.classes.find((c: Category) => c.id === new_class.id);
const old_class = this.category_set.categories.find((c: Category) => c.id === new_class.id);
const old_name = old_class.name;
const parent_depth = old_class.name.length;

if (new_class.id === undefined || new_class.id === null) {
new_class.id = _.max(_.map(this.classes, 'id')) + 1;
this.classes.push(new_class);
new_class.id = _.max(_.map(this.category_set.categories, 'id')) + 1;
this.category_set.categories.push(new_class);
} else {
Object.assign(old_class, new_class);
}

// When a parent category is renamed, we also need to rename the children
_.map(this.classes, c => {
_.map(this.category_set.categories, c => {
if (_.isEqual(old_name, c.name.slice(0, parent_depth))) {
c.name = new_class.name.concat(c.name.slice(parent_depth));
console.log('Renamed child:', c.name);
}
});

this.classes_unsaved_changes = true;
this.unsaved_changes = true;
},
addClass(this: State, new_class: Category) {
new_class.id = _.max(_.map(this.classes, 'id')) + 1;
this.classes.push(new_class);
this.classes_unsaved_changes = true;
new_class.id = (_.max(_.map(this.category_set.categories, 'id')) || 0) + 1;
this.category_set.categories.push(annotate(new_class));
this.unsaved_changes = true;
},
removeClass(this: State, classId: number) {
this.classes = this.classes.filter((c: Category) => c.id !== classId);
this.classes_unsaved_changes = true;
this.category_set.categories = this.category_set.categories.filter(
(c: Category) => c.id !== classId
);
this.unsaved_changes = true;
},
restoreDefaultClasses(this: State) {
let i = 0;
this.classes = createMissingParents(defaultCategories).map(c =>
this.category_set.categories = createMissingParents(defaultCategories).map(c =>
Object.assign(c, { id: i++ })
);
this.classes_unsaved_changes = true;
this.unsaved_changes = true;
},
clearAll(this: State) {
this.classes = [];
this.classes_unsaved_changes = true;
this.category_set.categories = [];
this.unsaved_changes = true;
},
createSet(id: string) {
this.category_sets.push({ id, categories: [] });
this.setCurrentSet(id);
this.unsaved_changes = true;
},
setCurrentSet(this: State, id: string) {
const new_set = this.category_sets.find(s => s.id === id);
if (!new_set) {
console.error('Could not find set with id', id);
return;
}
this.category_set = new_set;
},
},
});
67 changes: 58 additions & 9 deletions src/util/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export interface Category {
children?: Category[];
}

export type CategorySet = {
id: string;
categories: Category[];
};

const COLOR_UNCAT = '#CCC';

// The default categories
Expand Down Expand Up @@ -92,7 +97,7 @@ export const defaultCategories: Category[] = [
{ name: ['Uncategorized'], rule: { type: null }, data: { color: COLOR_UNCAT } },
];

export function annotate(c: Category) {
export function annotate(c: Category): Category {
const ch = c.name;
c.name_pretty = ch.join(level_sep);
c.subname = ch.slice(-1)[0];
Expand Down Expand Up @@ -160,13 +165,17 @@ function areWeTesting() {
return process.env.NODE_ENV === 'test';
}

export function saveClasses(classes: Category[]) {
export function saveClasses(set: CategorySet) {
if (areWeTesting()) {
console.log('Not saving classes in test mode');
return;
}
localStorage.classes = JSON.stringify(classes.map(cleanCategory));
console.log('Saved classes', localStorage.classes);
const key = 'classes.' + set.id;
localStorage[key] = JSON.stringify({
...set,
categories: set.categories.map(cleanCategory),
});
console.log('Saved classes', localStorage[key]);
}

export function cleanCategory(cat: Category): Category {
Expand All @@ -179,13 +188,53 @@ export function cleanCategory(cat: Category): Category {
return cat;
}

export function loadClasses(): Category[] {
const classes_json = localStorage.classes;
export function loadClasses(name?: string): CategorySet {
const current_set = name || localStorage.current_set || 'default';
const key = 'classes.' + current_set;
const classes_json = localStorage[key];

let set: CategorySet = { id: current_set, categories: [] };
if (classes_json && classes_json.length >= 1) {
return JSON.parse(classes_json).map(cleanCategory);
set = JSON.parse(classes_json);
} else if (current_set == 'default') {
if (localStorage.classes) {
// Fall back to
set.categories = JSON.parse(localStorage.classes);
} else {
set.categories = defaultCategories;
}
} else {
return defaultCategories;
console.error('No classes found for category set', current_set);
}

// Clean
set.categories = set.categories.map(cleanCategory);

// Assign IDs
let i = 0;
set.categories = set.categories.map(c => Object.assign(c, { id: i++ }));

return set;
}

export function loadSets(): CategorySet[] {
// Returns a list of category set IDs
// Always returns the current category as the first item
const setIDs = new Set(
[localStorage.current_set, 'default']
.filter(x => x)
.concat(
Object.keys(localStorage)
.filter(key => key.startsWith('classes.'))
.map(key => key.slice(8))
)
);
console.log(setIDs);
return Array.from(setIDs).map(id => loadClasses(id));
}

export function setCurrentSet(name: string) {
localStorage.current_set = name;
}

function pickDeepest(categories: Category[]) {
Expand All @@ -197,7 +246,7 @@ export function matchString(str: string, categories: Category[] | null): Categor
console.log(
'Categories not passed, loading... (if you see this outside of a test, you should probably pass them)'
);
categories = loadClasses();
categories = loadClasses().categories;
}

// Compile regexes
Expand Down
2 changes: 1 addition & 1 deletion src/util/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function getColorFromCategory(c: Category, allCats: Category[]): string {
// TODO: Move into vuex?
export function getCategoryColorFromString(str: string): string {
// TODO: Don't load classes on every call
const allCats = loadClasses();
const allCats = loadClasses().categories;
const c = matchString(str, allCats);
if (c !== null) {
return getColorFromCategory(c, allCats);
Expand Down
2 changes: 1 addition & 1 deletion src/views/activity/ActivityView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ div(v-if="view")
span Edit view
</template>

<script>
<script lang="ts">
import 'vue-awesome/icons/save';
import 'vue-awesome/icons/times';
import 'vue-awesome/icons/trash';
Expand Down
Loading