|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright 2026 The FOAM Authors. All Rights Reserved. |
| 4 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 5 | + */ |
| 6 | + |
| 7 | +foam.CLASS({ |
| 8 | + package: 'foam.parse.auto', |
| 9 | + name: 'ReferenceSuggester', |
| 10 | + extends: 'foam.u2.View', |
| 11 | + |
| 12 | + documentation: ` |
| 13 | + A suggester view for Reference properties in the AQL search bar. |
| 14 | + Shows records from the target DAO using CitationView. SmartView passes |
| 15 | + a 'filter' string (the text typed after the operator) which is used to |
| 16 | + narrow results. |
| 17 | + For searching, prefers CONTAINS_IC on the model's searchColumns axiom |
| 18 | + (mirroring RichChoiceView); falls back to KEYWORD if none declared. |
| 19 | + Selecting a record inserts its ID. |
| 20 | + `, |
| 21 | + |
| 22 | + requires: [ |
| 23 | + 'foam.u2.CitationView' |
| 24 | + ], |
| 25 | + |
| 26 | + properties: [ |
| 27 | + 'suggestText', |
| 28 | + { class: 'Class', name: 'of' }, |
| 29 | + { class: 'String', name: 'targetDAOKey' }, |
| 30 | + { class: 'String', name: 'filter' }, |
| 31 | + { class: 'Int', name: 'resultLimit', value: 10 } |
| 32 | + ], |
| 33 | + |
| 34 | + methods: [ |
| 35 | + function render() { |
| 36 | + this.addClass(); |
| 37 | + var self = this; |
| 38 | + var dao = this.__subContext__[this.targetDAOKey]; |
| 39 | + if ( ! dao ) return; |
| 40 | + |
| 41 | + var filtered = this.filter |
| 42 | + ? dao.where(this.buildFilterPredicate_(this.filter)) |
| 43 | + : dao; |
| 44 | + |
| 45 | + let isFirstElement = true; |
| 46 | + this |
| 47 | + .start() |
| 48 | + .select(filtered.limit(self.resultLimit), function(obj) { |
| 49 | + if ( ! isFirstElement ) { |
| 50 | + this.start().addClass('foam-parse-auto-SmartView-suggestionSeparator').end(); |
| 51 | + } |
| 52 | + isFirstElement = false; |
| 53 | + this.start(self.CitationView, { data: obj }) |
| 54 | + .addClass(self.myClass('row')) |
| 55 | + .on('click', function() { self.suggestText(obj.id + ' '); }) |
| 56 | + .end(); |
| 57 | + }) |
| 58 | + .end(); |
| 59 | + }, |
| 60 | + |
| 61 | + function buildFilterPredicate_(filter) { |
| 62 | + // Build predicates against columns in the model's searchColumns axiom: |
| 63 | + // - String columns → CONTAINS_IC(col, filter) |
| 64 | + // - Numeric columns → EQ(col, +filter) (when filter parses as number) |
| 65 | + // - Reference columns → resolved to the ID type they store, then above |
| 66 | + // - Enum/Date/Boolean/etc. are skipped (Binary.adapt would throw). |
| 67 | + // Falls back to KEYWORD when no usable columns are available. |
| 68 | + var self = this; |
| 69 | + var searchAxiom = this.of.getAxiomByName('searchColumns'); |
| 70 | + var cols = ( searchAxiom && searchAxiom.columns ) || []; |
| 71 | + |
| 72 | + var preds = cols |
| 73 | + .map(function(name) { return self.of.getAxiomByName(name); }) |
| 74 | + .filter(function(p) { return p; }) |
| 75 | + .map(function(p) { return self.columnPredicate_(p, filter); }) |
| 76 | + .filter(function(pred) { return pred; }); |
| 77 | + |
| 78 | + if ( preds.length > 0 ) return this.OR.apply(this, preds); |
| 79 | + |
| 80 | + console.warn( |
| 81 | + '[ReferenceSuggester] Falling back to KEYWORD for ' + this.of.id + |
| 82 | + ' - no usable searchColumns for filter "' + filter + '". ' + |
| 83 | + 'KEYWORD scans all keyword-indexed properties on the server which is ' + |
| 84 | + 'less efficient. Declare `searchColumns` with String/Int properties on ' + |
| 85 | + this.of.id + ' (e.g. name, email, id) for targeted filtering.' |
| 86 | + ); |
| 87 | + return this.KEYWORD(filter); |
| 88 | + }, |
| 89 | + |
| 90 | + function columnPredicate_(prop, filter) { |
| 91 | + // Return the best predicate for one column, or null if the column's |
| 92 | + // type can't be safely filtered by `filter`. |
| 93 | + var type = this.effectivePropertyType_(prop); |
| 94 | + |
| 95 | + if ( foam.lang.String.isInstance(type) ) { |
| 96 | + return this.CONTAINS_IC(prop, filter); |
| 97 | + } |
| 98 | + if ( foam.lang.Int.isInstance(type) ) { |
| 99 | + var n = this.parseNumber_(filter); |
| 100 | + return n === null ? null : this.EQ(prop, n); |
| 101 | + } |
| 102 | + return null; |
| 103 | + }, |
| 104 | + |
| 105 | + function effectivePropertyType_(prop) { |
| 106 | + // A Reference doesn't store the referenced object — it stores the ID. |
| 107 | + // Unwrap Reference → target model's ID property (resolving IDAlias for |
| 108 | + // compound-ID models) so callers can check the real stored type. |
| 109 | + if ( ! foam.lang.Reference.isInstance(prop) ) return prop; |
| 110 | + var id = prop.of.ID; |
| 111 | + if ( foam.lang.IDAlias.isInstance(id) ) { |
| 112 | + id = prop.of.getAxiomByName(id.propName); |
| 113 | + } |
| 114 | + return id; |
| 115 | + }, |
| 116 | + |
| 117 | + function parseNumber_(str) { |
| 118 | + if ( str === '' ) return null; |
| 119 | + var n = Number(str); |
| 120 | + return isNaN(n) ? null : n; |
| 121 | + } |
| 122 | + ] |
| 123 | +}); |
0 commit comments