Skip to content

Query Report Dynamic Filters Architecture Fix - #292

Open
Sendipad wants to merge 1 commit into
developfrom
enhance_fix_dynamic_link
Open

Query Report Dynamic Filters Architecture Fix#292
Sendipad wants to merge 1 commit into
developfrom
enhance_fix_dynamic_link

Conversation

@Sendipad

Copy link
Copy Markdown
Owner

1. Dynamic Link Resolution in ControlRegistry

The Dynamic Link fieldtype mapping in control_registry.js now elegantly falls back to querying its sibling filters:

doctype = context.doc?.[df.options] || context.filters?.[df.options] || window.frappe?.query_report?.get_filter_value(df.options) || "";

This ensures that if a user sets the target DocType of a Link dynamically (e.g., through a party_type field), the ControlRegistry correctly resolves the current selected value.

Because ControlFactory operates as a computed property bounded to context, Vue natively re-evaluates ControlRegistry.resolve the moment the party_type filter is updated.

2. API Compatibility Layer in Meta Store

We've decoupled Frappe-specific get_query API parsing from the generic UI controls.
A new method execute_get_query was added to useMetaStore.js:

async function execute_get_query(df, search, reference_doctype, start = 0, page_length = 40, filters = {}) {
    // ...
    const query_config = await df.get_query(search || "", filters);
    // Properly inspects if query_config is an Array or an Object
    // Dynamically executes frappe.desk.search.search_link with custom `query` and `filters` overrides
}

This entirely patches the silent error where Frappe Query Reports returned a configuration object (e.g., { query: '...', filters: {...} }) instead of a flat array of rows.

3. Top-Down Reactive Dependency Graph

The core issue underlying stale caches for dependent filters was the failure to notify child controls that their underlying data had changed. We resolved this by explicitly injecting the reactive report_filter_values graph into the context:

  1. Injection: QueryRecordsConfig.vue now injects filters: report_filter_values into FlexValueControl's context.
  2. Prop Drilling: FlexValueControl.vue passes this down via :filters="context?.filters || {}" and :context="context".
  3. Reactive Invalidation: MultiSelectList.vue deeply watches the incoming props.filters:
    watch(() => props.filters, () => {
        reset(); // Wipes internal pagination/loading state
        if (canInteract.value) clearAll(); // Clears any invalid selected UI badges
        if (showExpanded.value || isDropdownOpen.value) fetchOptions(query.value || "");
    }, { deep: true });
    This completely eliminates the need for manual dependencyVersion tracking.

4. Bypassing ControlFactory for Multi-Selects

In FlexValueControl.vue, the isMultiSelect computed property was previously only true when there was an operator like "in" or "not in". We updated it to also natively handle MultiSelectList, MultiSelect, and MultiCheck fieldtypes.

This ensures that report filters using these types are rendered directly via <MultiSelectList> instead of being proxied through <ControlFactory> (which previously swallowed the context and get_data props).

5. Resolving Sibling Filter References

We enhanced referenceDoctype in FlexValueControl.vue to resolve dependencies dynamically. When a field (like party) specifies its target DocType as the name of another filter (e.g. options: "party_type"), it now reads the actual value (e.g. "Customer") from context.filters:

if (opts && typeof opts === "string" && props.context?.filters) {
	const filterVal = props.context.filters[opts];
	// Unwraps { mode: "static", value: "Customer" } → "Customer"
}

6. Breaking the Reactivity Feedback Loop

We fixed a severe flicker bug where selecting a value in a MultiSelectList would instantly reset the field.

The issue occurred because the component watched the global props.filters object. When a user selected a value, it updated report_filter_values, which triggered the props.filters deep watcher inside the same component, causing it to wipe its own state via clearAll().

We broke this loop in both MultiSelectList.vue and ComboBoxControl.vue by making the watcher inspect the before/after values of props.filters and aborting the reset if the only changed key was the component's own df.fieldname.

Summary

The combination of explicit filters injection, native multi-select rendering, dynamic reference resolution, and loop-safe reactivity guarantees that dependent filters work harmoniously. Every time an upstream parent filter changes, any child Links, Dynamic Links, and MultiSelectLists will autonomously wipe their caches and re-fetch options without manual event emitters, while safely persisting user selections.

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant