Skip to content
Open
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
38 changes: 37 additions & 1 deletion src/bases/basesFilterDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,47 @@ function normalizeFilterProperty(
options: BasesFilterDefaultOptions
): string | null {
let property = propertyExpression.trim();

// Strip a leading `&&`-joined left side. The generated default-relationships
// filter (see `formatProjectEntryLinkExpression` in `defaultBasesFiles.ts`,
// issue #2043) concatenates `file.hasLink(this.file) &&` before the
// `list(note.PROP).map(...)` expression so the regex above matches the
// whole conjunction. Keep only the right-hand operand.
const conjunctionIndex = property.lastIndexOf("&&");
if (conjunctionIndex !== -1) {
property = property.slice(conjunctionIndex + 2).trim();
}

// Strip generated `.map(...)` chains emitted by the relationship templates
// in `defaultBasesFiles.ts`. These wrap `list(note.PROP)` to normalize link
// formats (markdown links, `%20`, paths). The wrapper closes one balanced
// call, so find the last `.map(` and strip up to its matching `)`.
const mapStart = property.lastIndexOf(".map(");
if (mapStart !== -1) {
let depth = 0;
let endIndex = -1;
for (let i = mapStart + 5; i < property.length; i++) {
const ch = property[i];
if (ch === "(") {
depth++;
} else if (ch === ")") {
if (depth === 0) {
endIndex = i;
break;
}
depth--;
}
}
if (endIndex !== -1) {
property = property.slice(0, mapStart).trim();
}
}

const listMatch = property.match(/^list\((.+)\)$/);
if (listMatch) {
property = listMatch[1].trim();
}
property = property.replace(/^(note|task)\./, "");
property = property.replace(/^(note|task|this\.note)\./, "");

if (property === "tags" || property === "file.tags") {
return "tags";
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/bases/basesFilterDefaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,72 @@ describe("Bases filter defaults", () => {

expect(defaults).toEqual({});
});

// Regression: issue #2043. The default relationships Subtasks filter
// generated by `formatProjectEntryLinkExpression` in `defaultBasesFiles.ts`
// emits `file.hasLink(this.file) && list(note.PROP).map(<link-normalizer>).asLink()).contains(this.file.asLink())`.
// The `currentFileContainsMatch` regex captures the entire conjunction, so
// the parser must strip the `&&` left side and the generated `.map(...).asLink()`
// chain before extracting the property name.
it("extracts project default from generated `file.hasLink(this.file) &&` conjunction filter (#2043)", () => {
const generatedRule =
'file.hasLink(this.file) && list(note.projects).map(file(value.replace(/^\\[[^\\]]+\\]\\((.*)\\)$/, "$1").replace(/%20/g, " ")).asLink()).contains(this.file.asLink())';

const defaults = extractBasesFilterDefaults({
config: {
filters: {
rule: { text: generatedRule },
},
},
fieldMapper: createFieldMapper(),
taskTag: "task",
currentFileLink: "[[Current]]",
});

expect(defaults).toEqual({
projects: ["[[Current]]"],
});
});

it("extracts user-defined field from generated conjunction + map filter (#2043)", () => {
const generatedRule =
'file.hasLink(this.file) && list(note.customField).map(file(value.replace(/^\\[[^\\]]+\\]\\((.*)\\)$/, "$1").replace(/%20/g, " ")).asLink()).contains(this.file.asLink())';

const defaults = extractBasesFilterDefaults({
config: {
filters: {
rule: { text: generatedRule },
},
},
fieldMapper: createFieldMapper(),
taskTag: "task",
userFields: [{ key: "customField" }],
currentFileLink: "[[Current]]",
});

// Unknown user-defined fields fall through to scalar storage in
// `addFrontmatterDefault` (the list-merge path is reserved for the
// core list fields: tags, contexts, projects, blockedBy).
expect(defaults).toEqual({
customField: "[[Current]]",
});
});

it("falls back gracefully when current file link is missing for the generated filter (#2043)", () => {
const generatedRule =
'file.hasLink(this.file) && list(note.projects).map(file(value.replace(/^\\[[^\\]]+\\]\\((.*)\\)$/, "$1").replace(/%20/g, " ")).asLink()).contains(this.file.asLink())';

const defaults = extractBasesFilterDefaults({
config: {
filters: {
rule: { text: generatedRule },
},
},
fieldMapper: createFieldMapper(),
taskTag: "task",
currentFileLink: null,
});

expect(defaults).toEqual({});
});
});