lucos_search_component 4.0.2 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ permissions:
13
13
 
14
14
  jobs:
15
15
  reusable:
16
- uses: lucas42/.github/.github/workflows/reusable-code-reviewer-auto-merge.yml@v1.19.0
16
+ uses: lucas42/.github/.github/workflows/reusable-code-reviewer-auto-merge.yml@v1.21.0
17
17
  secrets:
18
18
  LUCOS_CI_APP_ID: ${{ secrets.LUCOS_CI_APP_ID }}
19
19
  LUCOS_CI_PRIVATE_KEY: ${{ secrets.LUCOS_CI_PRIVATE_KEY }}
@@ -46,7 +46,7 @@ jobs:
46
46
 
47
47
  steps:
48
48
  - name: Checkout repository
49
- uses: actions/checkout@v6
49
+ uses: actions/checkout@v7
50
50
 
51
51
  # Initializes the CodeQL tools for scanning.
52
52
  - name: Initialize CodeQL
@@ -9,4 +9,4 @@ permissions:
9
9
 
10
10
  jobs:
11
11
  convention-check:
12
- uses: lucas42/.github/.github/workflows/reusable-convention-check.yml@v1.19.0
12
+ uses: lucas42/.github/.github/workflows/reusable-convention-check.yml@v1.21.0
@@ -9,7 +9,7 @@ permissions:
9
9
 
10
10
  jobs:
11
11
  dependabot:
12
- uses: lucas42/.github/.github/workflows/reusable-dependabot-auto-merge.yml@v1.19.0
12
+ uses: lucas42/.github/.github/workflows/reusable-dependabot-auto-merge.yml@v1.21.0
13
13
  secrets:
14
14
  LUCOS_CI_APP_ID: ${{ secrets.LUCOS_CI_APP_ID }}
15
15
  LUCOS_CI_PRIVATE_KEY: ${{ secrets.LUCOS_CI_PRIVATE_KEY }}
package/README.md CHANGED
@@ -32,7 +32,7 @@ The following attributes can be added to the lucos-search span:
32
32
  * **data-api-key** \[required\] — a valid API key for the production instance of [lucos_arachne](https://github.com/lucas42/lucos_arachne), as generated by [lucos_creds](https://github.com/lucas42/lucos_creds).
33
33
  * **data-types** — A comma separated list of item types to search for (defaults to all types).
34
34
  * **data-exclude_types** — A comma separated list of item types to exclude from the search (ignored if `data-types` is set).
35
- * **data-is-contact** — Filter results to contacts only (`"true"`) or non-contacts only (`"false"`). Omitting the attribute applies no filter. Composes with `data-types` when both are set.
35
+ * **data-is-contact** — Filter results to contacts only (`"true"`) or non-contacts only (`"false"`). Omitting the attribute applies no filter. Composes with `data-types` when both are set. When `"true"`, selected options use the entity's **`lucos_contacts` URI** (`contact_uri`) as their value, and the selected-item lozenge links through to that same `lucos_contacts` URI instead of the eolas knowledge page — form value and lozenge link stay consistent with each other.
36
36
  * **data-label-override-zxx** — Override the displayed label for the `https://eolas.l42.eu/metadata/language/zxx/` entry in both the dropdown and selected lozenge. Has no effect on whether `zxx` appears in the list — that depends on whether `zxx` is present in the search index. Only meaningful when `data-types="Language"`.
37
37
  * **data-common** — A comma separated list of item URIs to pin in a "Common" group at the top of the list, above the normal results. Only meaningful when `data-types="Language"`.
38
38
  * **data-preload** — If present, all options are loaded upfront rather than fetched on search. Suitable for small, finite datasets such as languages.
package/dist/index.js CHANGED
@@ -5890,10 +5890,19 @@ class LucosSearchComponent extends HTMLSpanElement {
5890
5890
  ...(component.isLanguageMode || component.getAttribute("data-common") ? { optgroupField: 'lang_family', lockOptgroupOrder: true } : {}),
5891
5891
  ...(component.hasAttribute("data-create") ? {
5892
5892
  create: function(input) {
5893
- return { id: input, pref_label: input, created: true };
5893
+ // Key must match valueField below (contact_uri in contact mode) — a
5894
+ // hardcoded `id` key here would leave the created option unaddressable
5895
+ // by TomSelect when data-is-contact="true".
5896
+ const created = { pref_label: input, created: true };
5897
+ created[component.valueFieldName] = input;
5898
+ return created;
5894
5899
  },
5895
5900
  } : {}),
5896
- valueField: 'id',
5901
+ // In contact mode, the form value, TomSelect's option key, and the lozenge
5902
+ // click-through/link target (render.item below, onItemSelect further down) all
5903
+ // use the lucos_contacts URI instead of the eolas knowledge URI — see
5904
+ // lucos_arachne#712 and the #190 review discussion.
5905
+ valueField: component.valueFieldName,
5897
5906
  labelField: 'pref_label',
5898
5907
  searchField: [],
5899
5908
  closeAfterSelect: true,
@@ -5907,7 +5916,7 @@ class LucosSearchComponent extends HTMLSpanElement {
5907
5916
  component._searchAbortController = abortController;
5908
5917
 
5909
5918
  errorMessage.setAttribute('hidden', '');
5910
- const commonSet = new Set((component._commonOptions || []).map(o => o.id));
5919
+ const commonSet = new Set((component._commonOptions || []).map(o => o[component.valueFieldName]));
5911
5920
  // When preloaded, filter locally instead of hitting Typesense
5912
5921
  if (component._preloadedOptions) {
5913
5922
  const q = query.toLowerCase();
@@ -5917,7 +5926,7 @@ class LucosSearchComponent extends HTMLSpanElement {
5917
5926
  (r.labels && r.labels.some(l => l.toLowerCase().includes(q)))
5918
5927
  )
5919
5928
  : [...component._preloadedOptions];
5920
- results = results.filter(r => !commonSet.has(r.id));
5929
+ results = results.filter(r => !commonSet.has(r[component.valueFieldName]));
5921
5930
  this.clearOptions();
5922
5931
  if (component._commonOptions) {
5923
5932
  const filteredCommon = q
@@ -5947,7 +5956,7 @@ class LucosSearchComponent extends HTMLSpanElement {
5947
5956
  this.clearOptions();
5948
5957
  // Remove common items from results to avoid duplication; filter by query when non-empty
5949
5958
  if (component._commonOptions) {
5950
- results = results.filter(r => !commonSet.has(r.id));
5959
+ results = results.filter(r => !commonSet.has(r[component.valueFieldName]));
5951
5960
  const q = query.toLowerCase();
5952
5961
  const filteredCommon = q
5953
5962
  ? component._commonOptions.filter(o =>
@@ -5977,14 +5986,14 @@ class LucosSearchComponent extends HTMLSpanElement {
5977
5986
  },
5978
5987
  onFocus: function() {
5979
5988
  this.clearOptions();
5980
- const commonSet = new Set((component._commonOptions || []).map(o => o.id));
5989
+ const commonSet = new Set((component._commonOptions || []).map(o => o[component.valueFieldName]));
5981
5990
  if (component._commonOptions) {
5982
5991
  component._commonOptions.forEach(opt => this.addOption(opt));
5983
5992
  }
5984
5993
  // Re-add preloaded options (excluding common items which are shown separately)
5985
5994
  if (component._preloadedOptions) {
5986
5995
  component._preloadedOptions
5987
- .filter(opt => !commonSet.has(opt.id))
5996
+ .filter(opt => !commonSet.has(opt[component.valueFieldName]))
5988
5997
  .forEach(opt => this.addOption(opt));
5989
5998
  }
5990
5999
  },
@@ -5997,7 +6006,7 @@ class LucosSearchComponent extends HTMLSpanElement {
5997
6006
  this.addOptionGroup('x-common', { label: 'Common' });
5998
6007
  const commonParams = new URLSearchParams({
5999
6008
  q: '*',
6000
- filter_by: `id:[${commonIds.join(",")}]`,
6009
+ filter_by: `${component.valueFieldName}:[${commonIds.join(",")}]`,
6001
6010
  per_page: commonIds.length,
6002
6011
  });
6003
6012
  const commonResults = await component.searchRequest(commonParams);
@@ -6024,35 +6033,42 @@ class LucosSearchComponent extends HTMLSpanElement {
6024
6033
  if (filterValue) preloadParams.set("filter_by", filterValue);
6025
6034
  const preloaded = await component.searchRequest(preloadParams);
6026
6035
  component._preloadedOptions = preloaded;
6027
- const commonSet = new Set((component._commonOptions || []).map(o => o.id));
6028
- preloaded.filter(r => !commonSet.has(r.id)).forEach(r => this.addOption(r));
6036
+ const commonSet = new Set((component._commonOptions || []).map(o => o[component.valueFieldName]));
6037
+ preloaded.filter(r => !commonSet.has(r[component.valueFieldName])).forEach(r => this.addOption(r));
6029
6038
  }
6030
6039
  if (ids.length < 1) return;
6031
- // Fetch real options from Typesense, excluding common/preloaded items
6032
- const preloadedIds = component._preloadedOptions ? new Set(component._preloadedOptions.map(r => r.id)) : new Set();
6040
+ // Fetch real options from Typesense, excluding common/preloaded items.
6041
+ // `ids` are keyed by whatever valueField is configured (contact_uri in
6042
+ // contact mode, id otherwise), so every set/filter/updateOption below must
6043
+ // use the same key — a stray `.id` here is a silent no-op, not an error
6044
+ // (see lucos_search_component#190 review: updateOption(result.id, ...)
6045
+ // looks up tom-select's options map by valueField, not literally `id`).
6046
+ const preloadedIds = component._preloadedOptions ? new Set(component._preloadedOptions.map(r => r[component.valueFieldName])) : new Set();
6033
6047
  const excludeIds = new Set([...commonIds, ...preloadedIds]);
6034
6048
  const idsToFetch = ids.filter(id => !excludeIds.has(id));
6035
6049
  if (idsToFetch.length > 0) {
6036
6050
  const searchParams = new URLSearchParams({
6037
6051
  q: '*',
6038
- filter_by: `id:[${idsToFetch.join(",")}]`,
6052
+ filter_by: `${component.valueFieldName}:[${idsToFetch.join(",")}]`,
6039
6053
  per_page: idsToFetch.length,
6040
6054
  });
6041
6055
  const results = await component.searchRequest(searchParams);
6042
6056
  results.forEach(result => {
6043
- this.updateOption(result.id, result);
6057
+ this.updateOption(result[component.valueFieldName], result);
6044
6058
  });
6045
6059
  }
6046
6060
  // Update any pre-selected common items with fresh data
6047
6061
  if (component._commonOptions) {
6048
6062
  component._commonOptions.forEach(opt => {
6049
- if (ids.includes(opt.id)) this.updateOption(opt.id, opt);
6063
+ const key = opt[component.valueFieldName];
6064
+ if (ids.includes(key)) this.updateOption(key, opt);
6050
6065
  });
6051
6066
  }
6052
6067
  // Update any pre-selected preloaded items with fresh data
6053
6068
  if (component._preloadedOptions) {
6054
6069
  component._preloadedOptions.forEach(opt => {
6055
- if (ids.includes(opt.id)) this.updateOption(opt.id, opt);
6070
+ const key = opt[component.valueFieldName];
6071
+ if (ids.includes(key)) this.updateOption(key, opt);
6056
6072
  });
6057
6073
  }
6058
6074
  },
@@ -6097,7 +6113,10 @@ class LucosSearchComponent extends HTMLSpanElement {
6097
6113
  if (data.created) {
6098
6114
  return `<div class="lozenge lozenge-pending" data-type="" data-category="">${escape(displayLabel)}</div>`;
6099
6115
  }
6100
- return `<div class="lozenge" data-type="${escape(data.type)}" data-category="${escape(data.category)}"><a href="${data.id}" target="_blank">${escape(displayLabel)}</a></div>`;
6116
+ // Link target matches the form value (valueFieldName) in contact mode
6117
+ // that's contact_uri, not the eolas id, per lucas42's review on #190:
6118
+ // everything should link to the same URI for consistency.
6119
+ return `<div class="lozenge" data-type="${escape(data.type)}" data-category="${escape(data.category)}"><a href="${data[component.valueFieldName]}" target="_blank">${escape(displayLabel)}</a></div>`;
6101
6120
  },
6102
6121
  option_create: function(data, escape) {
6103
6122
  const noun = getCreateNoun();
@@ -6200,6 +6219,17 @@ class LucosSearchComponent extends HTMLSpanElement {
6200
6219
  if (!types) return false;
6201
6220
  return types.split(",").map(t => t.trim()).includes("Language");
6202
6221
  }
6222
+ // The document field TomSelect uses as its option key/form value: contact_uri when
6223
+ // data-is-contact="true", id otherwise. Every place that reads/writes TomSelect's
6224
+ // internal options map (keyed by valueField) or filters Typesense by that key must
6225
+ // go through this — hardcoding `.id` breaks in contact mode (lucos_search_component#190
6226
+ // review: `updateOption(result.id, ...)` silently no-op'd because tom-select hashes
6227
+ // on valueField, not literally `id`). Checks the attribute directly rather than via a
6228
+ // separate isContactMode getter — data-is-contact is also read directly elsewhere
6229
+ // (e.g. buildFilterBy's callers), so a getter here would look more reusable than it is.
6230
+ get valueFieldName() {
6231
+ return this.getAttribute("data-is-contact") === "true" ? 'contact_uri' : 'id';
6232
+ }
6203
6233
  get commonIds() {
6204
6234
  const common = this.getAttribute("data-common");
6205
6235
  if (!common) return [];
@@ -6245,7 +6275,7 @@ class LucosSearchComponent extends HTMLSpanElement {
6245
6275
  searchParams.set('query_by_weights', "10,8,3,1");
6246
6276
  searchParams.set('sort_by', "_text_match:desc,pref_label:asc");
6247
6277
  searchParams.set('prioritize_num_matching_fields', false);
6248
- searchParams.set('include_fields', "id,pref_label,type,category,labels,lang_family");
6278
+ searchParams.set('include_fields', "id,pref_label,type,category,labels,lang_family,contact_uri");
6249
6279
  searchParams.set('enable_highlight_v1', false);
6250
6280
  searchParams.set('highlight_start_tag', '<span class="highlight">');
6251
6281
  searchParams.set('highlight_end_tag', '</span>');
@@ -69,6 +69,16 @@
69
69
  <button type="submit">Submit</button>
70
70
  </form>
71
71
  <pre id="create-output"></pre>
72
+ <h1>Contact mode (data-is-contact)</h1>
73
+ <p>Person field restricted to <code>lucos_contacts</code> records (<code>data-is-contact="true"</code>). The selected-item lozenge links through to the <code>lucos_contacts</code> URI (<code>contact_uri</code>), and the value serialised on submit is that same URI — not the eolas URI — see lucos_search_component#189.</p>
74
+ <form id="contact-form" onsubmit="handleContactSubmit(event)">
75
+ <label for="linked-contact">Linked contact:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-is-contact="true"><select id="linked-contact" name="linked_contact" multiple></select></span>
76
+ <button type="submit">Submit</button>
77
+ </form>
78
+ <pre id="contact-output"></pre>
79
+ <label for="linked-contact-preselected">Linked contact (pre-selected on page load):</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-is-contact="true"><select id="linked-contact-preselected" multiple>
80
+ <option selected>https://contacts.l42.eu/people/2</option>
81
+ </select></span>
72
82
  <script>
73
83
  function handleSubmit(event) {
74
84
  event.preventDefault();
@@ -90,6 +100,16 @@
90
100
  }
91
101
  document.getElementById('create-output').textContent = JSON.stringify(output, null, 2);
92
102
  }
103
+ function handleContactSubmit(event) {
104
+ event.preventDefault();
105
+ const data = new FormData(event.target);
106
+ const output = {};
107
+ for (const [key, value] of data.entries()) {
108
+ if (!output[key]) output[key] = [];
109
+ output[key].push(value);
110
+ }
111
+ document.getElementById('contact-output').textContent = JSON.stringify(output, null, 2);
112
+ }
93
113
  </script>
94
114
  <script src="./built.js"></script>
95
115
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucos_search_component",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "description": "Web Components for searching lucOS data",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -144,10 +144,19 @@ class LucosSearchComponent extends HTMLSpanElement {
144
144
  ...(component.isLanguageMode || component.getAttribute("data-common") ? { optgroupField: 'lang_family', lockOptgroupOrder: true } : {}),
145
145
  ...(component.hasAttribute("data-create") ? {
146
146
  create: function(input) {
147
- return { id: input, pref_label: input, created: true };
147
+ // Key must match valueField below (contact_uri in contact mode) — a
148
+ // hardcoded `id` key here would leave the created option unaddressable
149
+ // by TomSelect when data-is-contact="true".
150
+ const created = { pref_label: input, created: true };
151
+ created[component.valueFieldName] = input;
152
+ return created;
148
153
  },
149
154
  } : {}),
150
- valueField: 'id',
155
+ // In contact mode, the form value, TomSelect's option key, and the lozenge
156
+ // click-through/link target (render.item below, onItemSelect further down) all
157
+ // use the lucos_contacts URI instead of the eolas knowledge URI — see
158
+ // lucos_arachne#712 and the #190 review discussion.
159
+ valueField: component.valueFieldName,
151
160
  labelField: 'pref_label',
152
161
  searchField: [],
153
162
  closeAfterSelect: true,
@@ -161,7 +170,7 @@ class LucosSearchComponent extends HTMLSpanElement {
161
170
  component._searchAbortController = abortController;
162
171
 
163
172
  errorMessage.setAttribute('hidden', '');
164
- const commonSet = new Set((component._commonOptions || []).map(o => o.id));
173
+ const commonSet = new Set((component._commonOptions || []).map(o => o[component.valueFieldName]));
165
174
  // When preloaded, filter locally instead of hitting Typesense
166
175
  if (component._preloadedOptions) {
167
176
  const q = query.toLowerCase();
@@ -171,7 +180,7 @@ class LucosSearchComponent extends HTMLSpanElement {
171
180
  (r.labels && r.labels.some(l => l.toLowerCase().includes(q)))
172
181
  )
173
182
  : [...component._preloadedOptions];
174
- results = results.filter(r => !commonSet.has(r.id));
183
+ results = results.filter(r => !commonSet.has(r[component.valueFieldName]));
175
184
  this.clearOptions();
176
185
  if (component._commonOptions) {
177
186
  const filteredCommon = q
@@ -201,7 +210,7 @@ class LucosSearchComponent extends HTMLSpanElement {
201
210
  this.clearOptions();
202
211
  // Remove common items from results to avoid duplication; filter by query when non-empty
203
212
  if (component._commonOptions) {
204
- results = results.filter(r => !commonSet.has(r.id));
213
+ results = results.filter(r => !commonSet.has(r[component.valueFieldName]));
205
214
  const q = query.toLowerCase();
206
215
  const filteredCommon = q
207
216
  ? component._commonOptions.filter(o =>
@@ -231,14 +240,14 @@ class LucosSearchComponent extends HTMLSpanElement {
231
240
  },
232
241
  onFocus: function() {
233
242
  this.clearOptions();
234
- const commonSet = new Set((component._commonOptions || []).map(o => o.id));
243
+ const commonSet = new Set((component._commonOptions || []).map(o => o[component.valueFieldName]));
235
244
  if (component._commonOptions) {
236
245
  component._commonOptions.forEach(opt => this.addOption(opt));
237
246
  }
238
247
  // Re-add preloaded options (excluding common items which are shown separately)
239
248
  if (component._preloadedOptions) {
240
249
  component._preloadedOptions
241
- .filter(opt => !commonSet.has(opt.id))
250
+ .filter(opt => !commonSet.has(opt[component.valueFieldName]))
242
251
  .forEach(opt => this.addOption(opt));
243
252
  }
244
253
  },
@@ -251,7 +260,7 @@ class LucosSearchComponent extends HTMLSpanElement {
251
260
  this.addOptionGroup('x-common', { label: 'Common' });
252
261
  const commonParams = new URLSearchParams({
253
262
  q: '*',
254
- filter_by: `id:[${commonIds.join(",")}]`,
263
+ filter_by: `${component.valueFieldName}:[${commonIds.join(",")}]`,
255
264
  per_page: commonIds.length,
256
265
  });
257
266
  const commonResults = await component.searchRequest(commonParams);
@@ -278,35 +287,42 @@ class LucosSearchComponent extends HTMLSpanElement {
278
287
  if (filterValue) preloadParams.set("filter_by", filterValue);
279
288
  const preloaded = await component.searchRequest(preloadParams);
280
289
  component._preloadedOptions = preloaded;
281
- const commonSet = new Set((component._commonOptions || []).map(o => o.id));
282
- preloaded.filter(r => !commonSet.has(r.id)).forEach(r => this.addOption(r));
290
+ const commonSet = new Set((component._commonOptions || []).map(o => o[component.valueFieldName]));
291
+ preloaded.filter(r => !commonSet.has(r[component.valueFieldName])).forEach(r => this.addOption(r));
283
292
  }
284
293
  if (ids.length < 1) return;
285
- // Fetch real options from Typesense, excluding common/preloaded items
286
- const preloadedIds = component._preloadedOptions ? new Set(component._preloadedOptions.map(r => r.id)) : new Set();
294
+ // Fetch real options from Typesense, excluding common/preloaded items.
295
+ // `ids` are keyed by whatever valueField is configured (contact_uri in
296
+ // contact mode, id otherwise), so every set/filter/updateOption below must
297
+ // use the same key — a stray `.id` here is a silent no-op, not an error
298
+ // (see lucos_search_component#190 review: updateOption(result.id, ...)
299
+ // looks up tom-select's options map by valueField, not literally `id`).
300
+ const preloadedIds = component._preloadedOptions ? new Set(component._preloadedOptions.map(r => r[component.valueFieldName])) : new Set();
287
301
  const excludeIds = new Set([...commonIds, ...preloadedIds]);
288
302
  const idsToFetch = ids.filter(id => !excludeIds.has(id));
289
303
  if (idsToFetch.length > 0) {
290
304
  const searchParams = new URLSearchParams({
291
305
  q: '*',
292
- filter_by: `id:[${idsToFetch.join(",")}]`,
306
+ filter_by: `${component.valueFieldName}:[${idsToFetch.join(",")}]`,
293
307
  per_page: idsToFetch.length,
294
308
  });
295
309
  const results = await component.searchRequest(searchParams);
296
310
  results.forEach(result => {
297
- this.updateOption(result.id, result);
311
+ this.updateOption(result[component.valueFieldName], result);
298
312
  });
299
313
  }
300
314
  // Update any pre-selected common items with fresh data
301
315
  if (component._commonOptions) {
302
316
  component._commonOptions.forEach(opt => {
303
- if (ids.includes(opt.id)) this.updateOption(opt.id, opt);
317
+ const key = opt[component.valueFieldName];
318
+ if (ids.includes(key)) this.updateOption(key, opt);
304
319
  });
305
320
  }
306
321
  // Update any pre-selected preloaded items with fresh data
307
322
  if (component._preloadedOptions) {
308
323
  component._preloadedOptions.forEach(opt => {
309
- if (ids.includes(opt.id)) this.updateOption(opt.id, opt);
324
+ const key = opt[component.valueFieldName];
325
+ if (ids.includes(key)) this.updateOption(key, opt);
310
326
  });
311
327
  }
312
328
  },
@@ -351,7 +367,10 @@ class LucosSearchComponent extends HTMLSpanElement {
351
367
  if (data.created) {
352
368
  return `<div class="lozenge lozenge-pending" data-type="" data-category="">${escape(displayLabel)}</div>`;
353
369
  }
354
- return `<div class="lozenge" data-type="${escape(data.type)}" data-category="${escape(data.category)}"><a href="${data.id}" target="_blank">${escape(displayLabel)}</a></div>`;
370
+ // Link target matches the form value (valueFieldName) in contact mode
371
+ // that's contact_uri, not the eolas id, per lucas42's review on #190:
372
+ // everything should link to the same URI for consistency.
373
+ return `<div class="lozenge" data-type="${escape(data.type)}" data-category="${escape(data.category)}"><a href="${data[component.valueFieldName]}" target="_blank">${escape(displayLabel)}</a></div>`;
355
374
  },
356
375
  option_create: function(data, escape) {
357
376
  const noun = getCreateNoun();
@@ -454,6 +473,17 @@ class LucosSearchComponent extends HTMLSpanElement {
454
473
  if (!types) return false;
455
474
  return types.split(",").map(t => t.trim()).includes("Language");
456
475
  }
476
+ // The document field TomSelect uses as its option key/form value: contact_uri when
477
+ // data-is-contact="true", id otherwise. Every place that reads/writes TomSelect's
478
+ // internal options map (keyed by valueField) or filters Typesense by that key must
479
+ // go through this — hardcoding `.id` breaks in contact mode (lucos_search_component#190
480
+ // review: `updateOption(result.id, ...)` silently no-op'd because tom-select hashes
481
+ // on valueField, not literally `id`). Checks the attribute directly rather than via a
482
+ // separate isContactMode getter — data-is-contact is also read directly elsewhere
483
+ // (e.g. buildFilterBy's callers), so a getter here would look more reusable than it is.
484
+ get valueFieldName() {
485
+ return this.getAttribute("data-is-contact") === "true" ? 'contact_uri' : 'id';
486
+ }
457
487
  get commonIds() {
458
488
  const common = this.getAttribute("data-common");
459
489
  if (!common) return [];
@@ -499,7 +529,7 @@ class LucosSearchComponent extends HTMLSpanElement {
499
529
  searchParams.set('query_by_weights', "10,8,3,1");
500
530
  searchParams.set('sort_by', "_text_match:desc,pref_label:asc");
501
531
  searchParams.set('prioritize_num_matching_fields', false);
502
- searchParams.set('include_fields', "id,pref_label,type,category,labels,lang_family");
532
+ searchParams.set('include_fields', "id,pref_label,type,category,labels,lang_family,contact_uri");
503
533
  searchParams.set('enable_highlight_v1', false);
504
534
  searchParams.set('highlight_start_tag', '<span class="highlight">')
505
535
  searchParams.set('highlight_end_tag', '</span>');