Skip to content
Merged
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
66 changes: 63 additions & 3 deletions src/components/CategoryEditModal.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template lang="pug">
// The category edit modal
b-modal(id="edit" ref="edit" title="Edit category" @show="resetModal" @hidden="hidden" @ok="handleOk" @keydown.native.enter="handleEnter" :ok-disabled="editing.rule.type === 'regex' && !validPattern")
b-modal(id="edit" ref="edit" title="Edit category" @show="resetModal" @hidden="hidden" @ok="handleOk" @keydown.native.enter="handleEnter" :ok-disabled="!canSubmit")
div.my-1
b-input-group.my-1(prepend="Name")
b-form-input(v-model="editing.name")
Expand Down Expand Up @@ -34,6 +34,16 @@ b-modal(id="edit" ref="edit" title="Edit category" @show="resetModal" @hidden="h
:value="key"
) {{ key }}
small.text-muted Leave blank to match every string field (default).
b-input-group.my-1(:prepend="$t('settings.categorization.priority')")
b-form-input(
v-model="editing.priority"
type="number"
step="1"
:placeholder="$t('settings.categorization.priorityDefault')"
:state="priorityState"
)
small.text-danger(v-if="!validPriority")
| {{ $t('settings.categorization.priorityInteger') }}

hr
div.my-1
Expand Down Expand Up @@ -91,6 +101,7 @@ export default {
inherit_score: true,
score: null,
match_fields: [],
priority: null,
},
};
},
Expand All @@ -106,14 +117,36 @@ export default {
];
},
valid: function () {
return this.editing.rule.type !== 'none' && this.validPattern;
return this.canSubmit;
},
canSubmit: function () {
if (this.editing.rule.type === 'regex') {
return this.validPattern && this.validPriority;
}
return true;
},
validPattern: function () {
return this.editing.rule.type === 'regex' && validateRegex(this.editing.rule.regex || '');
},
broad_pattern: function () {
return this.editing.rule.type === 'regex' && isRegexBroad(this.editing.rule.regex || '');
},
validPriority: function () {
if (this.editing.rule.type !== 'regex') {
return true;
}
return this.priorityFromInput(this.editing.priority) !== undefined;
},
priorityState: function () {
if (
this.editing.priority === null ||
this.editing.priority === undefined ||
this.editing.priority === ''
) {
return null;
}
return this.validPriority;
},
fieldOptions: function () {
const extra = (this.editing.match_fields || []).filter(
k => !(CANONICAL_SELECT_KEYS as readonly string[]).includes(k)
Expand Down Expand Up @@ -146,6 +179,9 @@ export default {
this.categoryStore.removeClass(this.categoryId);
},
checkFormValidity() {
if (this.editing.rule.type === 'regex' && !this.validPriority) {
return false;
}
if (this.editing.rule.type === 'regex') {
return this.validPattern;
}
Expand All @@ -160,7 +196,7 @@ export default {
return;
}
// Mirrors the :ok-disabled condition on the modal
if (this.editing.rule.type === 'regex' && !this.validPattern) {
if (!this.canSubmit) {
return;
}
event.preventDefault();
Expand Down Expand Up @@ -204,8 +240,17 @@ export default {
} else {
rule.select_keys = selected;
}
const priority = this.priorityFromInput(this.editing.priority);
delete rule.weight;
if (priority === null) {
delete rule.priority;
} else {
rule.priority = priority;
}
} else {
delete rule.select_keys;
delete rule.priority;
delete rule.weight;
}
const new_class = {
id: this.editing.id,
Expand Down Expand Up @@ -241,8 +286,23 @@ export default {
score,
inherit_score,
match_fields: storedKeys ? [...storedKeys] : [],
priority: this.priorityFromRule(rule),
};
},
priorityFromInput(value) {
if (value === null || value === undefined || value === '') {
return null;
}
if (typeof value === 'string' && value.trim() === '') {
return null;
}
const priority = Number(value);
return Number.isInteger(priority) ? priority : undefined;
},
priorityFromRule(rule) {
const value = rule.priority !== undefined ? rule.priority : rule.weight;
return typeof value === 'number' && Number.isInteger(value) ? value : null;
},
},
};
</script>
3 changes: 3 additions & 0 deletions src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ export default {
discard: 'Verwerfen',
addCategory: 'Kategorie hinzufügen',
categories: 'Kategorien',
priority: 'Priorität',
priorityDefault: 'Standard',
priorityInteger: 'Die Priorität muss eine ganze Zahl sein.',
builderTitle: 'Kategorie-Builder',
builderSubtitle: 'Regeln aus unkategorisierter Aktivität erzeugen',
openBuilder: 'Builder öffnen',
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ export default {
discard: 'Discard',
addCategory: 'Add category',
categories: 'Categories',
priority: 'Priority',
priorityDefault: 'Default',
priorityInteger: 'Priority must be an integer.',
builderTitle: 'Category builder',
builderSubtitle: 'Generate rules from uncategorized activity',
openBuilder: 'Open builder',
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ export default {
discard: 'Отбросить',
addCategory: 'Добавить категорию',
categories: 'Категории',
priority: 'Приоритет',
priorityDefault: 'По умолчанию',
priorityInteger: 'Приоритет должен быть целым числом.',
builderTitle: 'Конструктор категорий',
builderSubtitle: 'Создание правил из некатегоризированной активности',
openBuilder: 'Открыть конструктор',
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/sv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ export default {
discard: 'Ignorera',
addCategory: 'Lägg till kategori',
categories: 'Kategorier',
priority: 'Prioritet',
priorityDefault: 'Standard',
priorityInteger: 'Prioriteten måste vara ett heltal.',
builderTitle: 'Kategoribyggare',
builderSubtitle: 'Skapa regler från okategoriserad aktivitet',
openBuilder: 'Öppna byggaren',
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/uk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ export default {
discard: 'Відкинути',
addCategory: 'Додати категорію',
categories: 'Категорії',
priority: 'Пріоритет',
priorityDefault: 'Типовий',
priorityInteger: 'Пріоритет має бути цілим числом.',
builderTitle: 'Конструктор категорій',
builderSubtitle: 'Створення правил із некатегоризованої активності',
openBuilder: 'Відкрити конструктор',
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ export default {
discard: '放弃',
addCategory: '添加分类',
categories: '分类',
priority: '优先级',
priorityDefault: '默认',
priorityInteger: '优先级必须是整数。',
builderTitle: '分类构建器',
builderSubtitle: '从未分类活动生成规则',
openBuilder: '打开构建器',
Expand Down
20 changes: 15 additions & 5 deletions src/util/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface Rule {
type: 'regex' | 'none';
regex?: string;
ignore_case?: boolean;
priority?: number;
weight?: number;
/** When set, only these event.data keys are tested. Absent = all string fields. */
select_keys?: string[];
}
Expand Down Expand Up @@ -361,8 +363,16 @@ export function loadCategories(): { sets: CategorySet[]; activeIds: string[] } {
return { sets, activeIds };
}

function pickDeepest(categories: Category[]) {
return _.maxBy(categories, c => c.name.length);
function categoryRank(category: Category): number {
const explicit = category.rule.priority ?? category.rule.weight;
if (typeof explicit === 'number' && Number.isInteger(explicit)) {
return explicit;
}
return category.name.length * 10;
}

function pickHighestRanked(categories: Category[]) {
return _.maxBy(categories, categoryRank);
}

export function matchString(
Expand All @@ -387,7 +397,7 @@ export function matchString(
});

// Find the matching category.
// If several categories match the event, the deepest category will be chosen.
// If several categories match, explicit priority wins; otherwise depth wins.
const matchingCats: [Category, RegExp][] = regexes.filter(([category, re]) => {
const selectKeys = normalizeSelectKeys(category.rule.select_keys);
if (event && selectKeys) {
Expand All @@ -399,7 +409,7 @@ export function matchString(
return re.test(str);
});
if (matchingCats.length > 0) {
return pickDeepest(matchingCats.map(c => c[0]));
return pickHighestRanked(matchingCats.map(c => c[0]));
}
return null;
}
Expand All @@ -423,7 +433,7 @@ export function classifyEvents(events: IEvent[], categories: Category[]): IEvent
});
e.data.$category =
matchingCats.length > 0
? pickDeepest(matchingCats.map(([category]) => category)).name
? pickHighestRanked(matchingCats.map(([category]) => category)).name
: UNCATEGORIZED;
return e;
});
Expand Down
18 changes: 18 additions & 0 deletions src/util/presetCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ function parseRule(raw: unknown, context: string): Rule | null {
}
const rule: Rule = { type: 'regex', regex: raw.regex };
if (raw.ignore_case === true) rule.ignore_case = true;
const priority = parsePresetPriority(raw, context);
if (priority === null) return null;
if (priority !== undefined) rule.priority = priority;
// Inlined from classes.normalizeSelectKeys to avoid a runtime cycle
// (classes.ts imports this module). Empty/duplicate lists are dropped so
// the rust parser never sees `select_keys: []`.
Expand All @@ -80,6 +83,21 @@ function parseRule(raw: unknown, context: string): Rule | null {
return rule;
}

function parsePresetPriority(
raw: Record<string, unknown>,
context: string
): number | null | undefined {
const value = raw.priority !== undefined ? raw.priority : raw.weight;
if (value === undefined) {
return undefined;
}
if (typeof value !== 'number' || !Number.isInteger(value)) {
console.warn(`[presets] ${context}: priority/weight must be an integer, skipping`);
return null;
}
return value;
}

function normalizePresetSelectKeys(keys: unknown): string[] | undefined {
if (!Array.isArray(keys) || keys.length === 0) {
return undefined;
Expand Down
Loading
Loading