Skip to content

fix(categories): filter null/empty segments before saving class name#902

Open
fanxing11 wants to merge 2 commits into
ActivityWatch:masterfrom
fanxing11:fix/category-editor-null-parent
Open

fix(categories): filter null/empty segments before saving class name#902
fanxing11 wants to merge 2 commits into
ActivityWatch:masterfrom
fanxing11:fix/category-editor-null-parent

Conversation

@fanxing11

Copy link
Copy Markdown
Contributor

跟进 ActivityWatch/activitywatch#1355 — 现在还没人在做。

问题

Category Editor 保存分类时,如果 parent 选 "None" 且 name 字段没被 touch,[].concat(this.editing.name) 会产生 [null]。原因是 this.editing.name 初始化自 cat.subname,对新建/已损坏的 class 可能是 null/undefined。

一旦 null 被写进 classes[].name,后续会被序列化进 categorize() query,query 解释器把它当变量名去查:

QueryInterpretException: Tried to reference variable 'null' which is not defined

所有用 categorize() 的 chart 全部挂掉。

修复

handleSubmit() 里加三道保护:

  • parent 不是数组就当空数组
  • 过滤掉最终 name 里的 null / undefined / 空串
  • 全部过滤完为空则 abort(既没 parent 也没 name,没啥可存)

不动 store 和 query 层,改动最小。

测试

npm test — 25/25 test suites 全过(121 tests)。
npm run lint — 0 errors(3 个 pre-existing warnings 与本 PR 无关)。

备注

issue 里提到后端 aw_query.q2_categorize 也可以做防御性 filter,那个应该单独一个 PR 到 aw-server-python(算 defense in depth)。这里先修根源,阻止前端再写坏数据。

user #s13mjl 已在 issue 里给出手动 workaround(改 settings.json 删 null),那部分对已有损坏数据仍然需要。

When editing a category with the Parent field set to "None" and the name
untouched, [].concat(this.editing.name) could produce [null] because
this.editing.name is initialized from cat.subname which may be null/undefined
for freshly-created or corrupted classes. Any such null value gets written
into settings.json under classes[].name and later serialized into the
categorize() query, where the query interpreter treats it as a variable
reference and raises

  QueryInterpretException: Tried to reference variable 'null' which is not defined

breaking every chart that uses categorize().

Guard handleSubmit() so that:
  - parent is coerced to [] if it is not an array
  - null/undefined/empty-string segments are dropped from the final name
  - if nothing remains (no parent, no name), save is aborted silently

Fixes ActivityWatch/activitywatch#1355
@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR tightens category-name saving in the Category Editor. The main changes are:

  • Treats a non-array parent selection as no parent.
  • Filters invalid category name segments before saving.
  • Skips saving when no valid category path remains.

Confidence Score: 4/5

The changed save path is mostly safe, but invalid empty child names can collapse into their parent.

  • The null and undefined filtering prevents the reported broken query state.
  • Empty leaf filtering can rewrite an existing child path to its parent path instead of rejecting it.

src/components/CategoryEditModal.vue

Important Files Changed

Filename Overview
src/components/CategoryEditModal.vue Normalizes category path segments before saving, with one edge case where filtering can collapse an invalid child name into its parent.

Reviews (1): Last reviewed commit: "fix(categories): filter null/empty segme..." | Re-trigger Greptile

Comment thread src/components/CategoryEditModal.vue Outdated
Comment on lines +155 to +158
const nameSegments = parent
.concat(this.editing.name)
.filter(s => s !== null && s !== undefined && s !== '');
if (nameSegments.length === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Empty Leaf Becomes Parent

When an existing child category has an empty subname, this filter removes only the leaf while keeping the parent path. Saving a color or rule change for ['Work', ''] then writes the class as ['Work'], which can duplicate or overwrite the parent category instead of rejecting the invalid name.

Suggested change
const nameSegments = parent
.concat(this.editing.name)
.filter(s => s !== null && s !== undefined && s !== '');
if (nameSegments.length === 0) {
const nameSegments = parent.concat(this.editing.name);
if (nameSegments.some(s => s === null || s === undefined || s === '')) {
// Nothing valid to save (invalid parent/name segment) — abort silently.
return;
}
if (nameSegments.length === 0) {

@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 38.63%. Comparing base (0a43547) to head (e6ce490).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #902   +/-   ##
=======================================
  Coverage   38.63%   38.63%           
=======================================
  Files          42       42           
  Lines        2226     2226           
  Branches      443      443           
=======================================
  Hits          860      860           
  Misses       1288     1288           
  Partials       78       78           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…parent

Address greptile review on ActivityWatch#902: the previous version filtered null/undef/''
from the concatenated (parent + name) array as a whole. If the user cleared
the Name field on a child category, the leaf would be dropped and the
resulting name would equal the parent's name, effectively overwriting the
child into a duplicate of its parent.

Split the check:
  - filter null/undef/'' out of the parent segments
  - abort the save entirely if the leaf (editing.name) itself is invalid,
    so an empty name never silently collapses a child into its parent.
@fanxing11

Copy link
Copy Markdown
Contributor Author

Good catch @greptile-apps — the collapse-into-parent edge case was real. Force-pushed e6ce490 which splits the check: parent segments still get null/undef/'' filtered, but the leaf name is validated on its own and the save is aborted if it's blank. That way an empty Name field can never silently rewrite a child into a duplicate of its parent.

npm test still 121/121, lint 0 errors.

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

The fix looks solid. The split between parent filtering and leaf validation is exactly right:

  • Parent segments: filter silently (a parent with empty segments is just noise, no information loss)
  • Leaf name: hard abort (empty = user error that must be surfaced, not papered over)

The isValid helper is clean and reused consistently. The comment block tying it back to issue #1355 is a nice touch for future maintainers.

One minor nit — the abort on invalid leaf name is silent right now (just return). A user who clears the Name field and clicks OK will see the modal not close but the ok event still fires with no feedback. Worth checking the flow:

handleOk(event) {
  event.preventDefault();
  this.handleSubmit();  // returns early if name invalid
  this.$emit('ok');     // fires regardless
},

Consider a small warning, e.g. setting a this.nameError = true flag and surfacing it in the template:

      if (!isValid(this.editing.name)) {
        this.nameError = true;
        return;
      }

...paired with a <div class="text-danger" v-if="nameError">Name is required.</div> in the template. Not a blocker — the core bug (null writes to the store) is correctly prevented — but the silent no-op could confuse users.

Otherwise the logic is correct and 121/121 tests passing gives confidence. LGTM from a correctness standpoint.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant