1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8/**
9 * Handler class for ItemsList
10 *
11 * Letter key: ~l~
12 *
13 */
14class Tracker_Field_ItemsList extends Tracker_Field_Abstract implements Tracker_Field_Exportable
15{
16	public static function getTypes()
17	{
18		return [
19			'l' => [
20				'name' => tr('Items List'),
21				'description' => tr('Display a list of field values from another tracker that has a relation with this tracker.'),
22				'readonly' => true,
23				'help' => 'Items List and Item Link Tracker Fields',
24				'prefs' => ['trackerfield_itemslist'],
25				'tags' => ['advanced'],
26				'default' => 'n',
27				'params' => [
28					'trackerId' => [
29						'name' => tr('Tracker ID'),
30						'description' => tr('Tracker from which to list items'),
31						'filter' => 'int',
32						'legacy_index' => 0,
33						'profile_reference' => 'tracker',
34					],
35					'fieldIdThere' => [
36						'name' => tr('Link Field ID'),
37						'description' => tr('Field ID from the other tracker containing an item link pointing to the item in this tracker or some other value to be matched.'),
38						'filter' => 'int',
39						'legacy_index' => 1,
40						'profile_reference' => 'tracker_field',
41						'parent' => 'trackerId',
42						'parentkey' => 'tracker_id',
43						'sort_order' => 'position_nasc',
44					],
45					'fieldIdHere' => [
46						'name' => tr('Value Field ID'),
47						'description' => tr('Field ID from this tracker matching the value in the link field ID from the other tracker if the field above is not an item link. If the field chosen here is an ItemLink, Link Field ID above can be left empty.'),
48						'filter' => 'int',
49						'legacy_index' => 2,
50						'profile_reference' => 'tracker_field',
51						'parent' => 'input[name=trackerId]',
52						'parentkey' => 'tracker_id',
53						'sort_order' => 'position_nasc',
54					],
55					'displayFieldIdThere' => [
56						'name' => tr('Fields to display'),
57						'description' => tr('Display alternate fields from the other tracker instead of the item title'),
58						'filter' => 'int',
59						'separator' => '|',
60						'legacy_index' => 3,
61						'profile_reference' => 'tracker_field',
62						'parent' => 'trackerId',
63						'parentkey' => 'tracker_id',
64						'sort_order' => 'position_nasc',
65					],
66					'displayFieldIdThereFormat' => [
67						'name' => tr('Format for customising fields to display'),
68						'description' => tr('Uses the translate function to replace %0 etc with the field values. E.g. "%0 any text %1"'),
69						'filter' => 'text',
70					],
71					'sortField' => [
72						'name' => tr('Sort Fields'),
73						'description' => tr('Order results by one or more fields from the other tracker.'),
74						'filter' => 'int',
75						'separator' => '|',
76						'legacy_index' => 6,
77						'profile_reference' => 'tracker_field',
78						'parent' => 'trackerId',
79						'parentkey' => 'tracker_id',
80						'sort_order' => 'position_nasc',
81					],
82					'linkToItems' => [
83						'name' => tr('Display'),
84						'description' => tr('How the link to the items should be rendered'),
85						'filter' => 'int',
86						'options' => [
87							0 => tr('Value'),
88							1 => tr('Link'),
89						],
90						'legacy_index' => 4,
91					],
92					'status' => [
93						'name' => tr('Status Filter'),
94						'description' => tr('Limit the available items to a selected set'),
95						'filter' => 'alpha',
96						'options' => [
97							'opc' => tr('all'),
98							'o' => tr('open'),
99							'p' => tr('pending'),
100							'c' => tr('closed'),
101							'op' => tr('open, pending'),
102							'pc' => tr('pending, closed'),
103						],
104						'legacy_index' => 5,
105					],
106					'editable' => [
107						'name' => tr('Add, Edit, Remove Items'),
108						'description' => tr('Master switch for enabling Add, Edit, Remove Items'),
109						'filter' => 'int',
110						'options' => [
111							0 => tr('No'),
112							1 => tr('Yes'),
113						],
114					],
115					'addItemText' => [ // having text switches adding on/off
116						'name' => tr('Add Item text'),
117						'description' => tr('Text to show on a button to add new items. Also enables adding items.'),
118						'filter' => 'text',
119						'depends' => [
120							'field' => 'editable'
121						],
122					],
123					'editItem' => [
124						'name' => tr('Edit Item'),
125						'description' => tr('Enable editing items'),
126						'filter' => 'int',
127						'options' => [
128							0 => tr('No'),
129							1 => tr('Yes'),
130						],
131						'depends' => [
132							'field' => 'editable'
133						],
134					],
135					'deleteItem' => [
136						'name' => tr('Delete Item'),
137						'description' => tr('Allow deleting items'),
138						'filter' => 'int',
139						'options' => [
140							0 => tr('No'),
141							1 => tr('Yes'),
142						],
143						'depends' => [
144							'field' => 'editable'
145						],
146					],
147					'editInViewMode' => [
148						'name' => tr('Edit in View Mode'),
149						'description' => tr('Whether the edit buttons and icons also appear when viewing an item'),
150						'filter' => 'int',
151						'options' => [
152							0 => tr('No'),
153							1 => tr('Yes'),
154						],
155						'depends' => [
156							'field' => 'editable'
157						],
158					],
159					// TODO:
160					/*'addItemWikiTpl' => [
161						'name' => tr('Add Item Template Page'),
162						'description' => tr('Wiki page to use as a Pretty Tracker template'),
163						'filter' => 'pagename',
164						'profile_reference' => 'wiki_page',
165						'depends' => [
166							'field' => 'editable'
167						],
168					],*/
169				],
170			],
171		];
172	}
173
174
175	/**
176	 * Get field data
177	 * @see Tracker_Field_Interface::getFieldData()
178	 *
179	 */
180	function getFieldData(array $requestData = [])
181	{
182		$items = $this->getItemIds();
183		$list = $this->getItemLabels($items);
184
185		$ret = [
186			'value' => '',
187			'items' => $list,
188		];
189
190		return $ret;
191	}
192
193	function renderInput($context = [])
194	{
195		if (empty($this->getOption('fieldIdHere'))) {
196			return $this->renderOutput(['render_input' => 'y']);
197		} else {
198			TikiLib::lib('header')->add_jq_onready(
199				'
200$("input[name=ins_' . $this->getOption('fieldIdHere') . '], select[name=ins_' . $this->getOption('fieldIdHere') . ']").change(function(e, initial) {
201	if(initial == "initial" && $(this).data("triggered-' . $this->getInsertId() . '")) {
202		return;
203	}
204	$(this).data("triggered-' . $this->getInsertId() . '", true);
205	$.getJSON(
206		"tiki-ajax_services.php",
207		{
208			controller: "tracker",
209			action: "itemslist_output",
210			field: "' . $this->getConfiguration('fieldId') . '",
211			fieldIdHere: "' . $this->getOption('fieldIdHere') . '",
212			value: $(this).val()
213		},
214		function(data, status) {
215			$ddl = $("div[name=' . $this->getInsertId() . ']");
216			$ddl.html(data);
217			if (jqueryTiki.chosen) {
218				$ddl.trigger("chosen:updated");
219			}
220			$ddl.trigger("change");
221		}
222	);
223});
224			'
225			);
226			// this is smart enough to attach only once even if multiple fields attach the same code
227			TikiLib::lib('header')->add_jq_onready('
228$("input[name=ins_' . $this->getOption('fieldIdHere') . '], select[name=ins_' . $this->getOption('fieldIdHere') . ']").trigger("change", "initial");
229', 1);
230			return '<div name="' . $this->getInsertId() . '"></div>';
231		}
232	}
233
234	function renderOutput($context = [])
235	{
236		if (isset($context['search_render']) && $context['search_render'] == 'y') {
237			$itemIds = $this->getData($this->getConfiguration('fieldId'));
238		} else {
239			$itemIds = $this->getItemIds();
240		}
241
242		$list = $this->getItemLabels($itemIds, $context);
243
244		// if nothing found check definition for previous list (used for output render)
245		if (empty($list)) {
246			$list = $this->getConfiguration('items', []);
247			$itemIds = array_keys($list);
248		}
249
250		if (isset($context['list_mode']) && $context['list_mode'] === 'csv') {
251			return implode('%%%', $list);
252		} else {
253			$data = [
254				'links' => (bool)$this->getOption('linkToItems'),
255				'raw' => (bool)$this->getOption('displayFieldIdThere'),
256				'itemIds' => implode(',', $itemIds),
257				'items' => $list,
258				'num' => count($list),
259				'itemPermissions' => [],
260				'addItemText' => '',
261				'otherFieldPermName' => '',
262				'parentItemId' => 0,
263			];
264			// Either it has been called from renderInput (edit mode) or we're rendering it as well in view mode
265			if ((isset($context['render_input']) && $context['render_input'] === 'y') || $this->getOption('editInViewMode')) {
266				$editmode = true;
267			} else {
268				$editmode = false;
269			}
270			if ($this->getOption('editable') && $editmode) {
271				$trackerThere = Tracker_Definition::get($this->getOption('trackerId'));
272				$fieldThere = $trackerThere->getField($this->getOption('fieldIdThere'));
273				// trackerPerms overide field Options
274				$trackerPerms = Perms::get('tracker', $this->getOption('trackerId'));
275				$canCreate = $trackerPerms->create_tracker_items; // tracker/global permission; However canEdit and canRemove are Item permissions, see below
276				$itemPermissions = [];
277				foreach ($itemIds as $itemId) {
278					$item = Tracker_Item::fromId($itemId);
279					$itemPermissions[$itemId]['can_remove'] = $item->canRemove();
280					$itemPermissions[$itemId]['can_modify'] = $item->canModify();
281				}
282				$data['itemPermissions'] = $itemPermissions;
283				$data['addItemText'] = $canCreate ? $this->getOption('addItemText') : '';
284				$data['otherFieldPermName'] = $fieldThere['permName'];
285				$data['parentItemId'] = $this->getItemId();
286			}
287			return $this->renderTemplate(
288				'trackeroutput/itemslist.tpl',
289				$context,
290				$data
291			);
292		}
293	}
294
295	function itemsRequireRefresh($trackerId, $modifiedFields)
296	{
297		if ($this->getOption('trackerId') != $trackerId) {
298			return false;
299		}
300
301		$displayFields = $this->getOption('displayFieldIdThere');
302		if (! is_array($displayFields)) {
303			$displayFields = [$displayFields];
304		}
305
306		$usedFields = array_merge(
307			[$this->getOption('fieldIdThere')],
308			$displayFields
309		);
310
311		$intersect = array_intersect($usedFields, $modifiedFields);
312
313		return count($intersect) > 0;
314	}
315
316	function watchCompare($old, $new)
317	{
318		$o = '';
319		$items = $this->getItemIds();
320		$n = $this->getItemLabels($items);
321
322		return parent::watchCompare($o, $n);	// then compare as text
323	}
324
325	function getDocumentPart(Search_Type_Factory_Interface $typeFactory)
326	{
327		$baseKey = $this->getBaseKey();
328		$items = $this->getItemIds();
329
330		$list = $this->getItemLabels($items);
331		$listtext = implode(' ', $list);
332
333		return [
334			$baseKey => $typeFactory->multivalue($items),
335			"{$baseKey}_text" => $typeFactory->sortable($listtext),
336		];
337	}
338
339	function getProvidedFields()
340	{
341		$baseKey = $this->getBaseKey();
342		return [
343			$baseKey,
344			"{$baseKey}_text",
345		];
346	}
347
348	function getGlobalFields()
349	{
350		return [];
351	}
352
353	function getTabularSchema()
354	{
355		$schema = new Tracker\Tabular\Schema($this->getTrackerDefinition());
356		$permName = $this->getConfiguration('permName');
357		$name = $this->getConfiguration('name');
358
359		$schema->addNew($permName, 'multi-id')
360			->setLabel($name)
361			->setReadOnly(true)
362			->setRenderTransform(function ($value) {
363				return implode(';', $value);
364			})
365			->setParseIntoTransform(function (& $info, $value) use ($permName) {
366				$info['fields'][$permName] = $value;
367			});
368
369		$schema->addNew($permName, 'multi-name')
370			->setLabel($name)
371			->addQuerySource('itemId', 'object_id')
372			->setReadOnly(true)
373			->setRenderTransform(function ($value, $extra) {
374
375				if (is_string($value) && empty($value)) {
376					// ItemsLists have no stored value, so when called from \Tracker\Tabular\Source\TrackerSourceEntry...
377					// we have to: get a copy of this field
378					$field = $this->getTrackerDefinition()->getFieldFromPermName($this->getConfiguration('permName'));
379					// get a new handler for it
380					$factory = $this->getTrackerDefinition()->getFieldFactory();
381					$handler = $factory->getHandler($field, ['itemId' => $extra['itemId']]);
382					// for which we can then get the itemIds array of the "linked" items
383					$value = $handler->getItemIds();
384					// and then get the labels from the id's we've now found as if they were the field's data
385				}
386
387				$labels = $this->getItemLabels($value, ['list_mode' => 'csv']);
388				return implode(';', $labels);
389			})
390			->setParseIntoTransform(function (& $info, $value) use ($permName) {
391				$info['fields'][$permName] = $value;
392			});
393
394		// json format for export and import (which will recreate missing linked items)
395
396		$fieldIdHere = $this->getOption('fieldIdHere');
397		$definition = $this->getTrackerDefinition();
398		$fieldHere = $definition->getField($fieldIdHere);
399		$extraFieldName = "tracker_field_{$fieldHere['permName']}";
400
401		$fieldIdThere = $this->getOption('fieldIdThere');
402		$trackerIdThere = $this->getOption('trackerId');
403		$trackerThere = Tracker_Definition::get($trackerIdThere);
404		$fieldThere = $trackerThere->getField($fieldIdThere);
405		$queryFieldName = "tracker_field_{$fieldThere['permName']}";
406
407		if ($fieldHere['type'] === 'r' && $fieldThere['type'] !== 'r') {
408			$extraFieldName .= '_text';
409		}
410
411		// cache the other tracker's items to test when importing
412		$itemsThereLookup = new Tracker\Tabular\Schema\CachedLookupHelper();
413		$tiki_tracker_items = TikiDb::get()->table('tiki_tracker_items');
414		$itemsThereLookup->setInit(
415			function ($count) use ($tiki_tracker_items, $trackerIdThere) {
416				return $tiki_tracker_items->fetchMap(
417					'itemId', 'status',
418					[
419						'trackerId' => $trackerIdThere,
420					],
421					$count, 0
422				);
423			}
424		);
425		$itemsThereLookup->setLookup(
426			function ($value) use ($tiki_tracker_items, $trackerIdThere) {
427				return $tiki_tracker_items->fetchOne(
428					'itemId', [
429						'trackerId' => $trackerIdThere,
430						'itemId'    => $value,
431					]
432				);
433			}
434		);
435
436		$attributelib = TikiLib::lib('attribute');
437		$unifiedsearchlib = TikiLib::lib('unifiedsearch');
438		$trackerUtilities = new Services_Tracker_Utilities;
439
440		$schema->addNew($permName, 'multi-json')
441			->setLabel($name)
442			// these query sources appear in the $extra array in the render transform fn
443			->addQuerySource('itemId', 'object_id')
444			->addQuerySource('fieldIdHere', $extraFieldName)
445			->setRenderTransform(
446				function ($value, $extra) use ($trackerIdThere, $queryFieldName, $unifiedsearchlib) {
447
448					if (! empty($extra['fieldIdHere'])) {
449						$content = $extra['fieldIdHere'];
450					} else {
451						$content = (string)$extra['itemId'];
452					}
453
454					$query = $unifiedsearchlib->buildQuery(
455						[
456							'type'          => 'trackeritem',
457							'tracker_id'    => (string)$trackerIdThere,
458							$queryFieldName => $content,
459						]
460					);
461
462					$result = $query->search($unifiedsearchlib->getIndex());
463					$out = [];
464
465					if ($result->count()) {
466						foreach ($result as $entry) {
467							$item = Tracker_Item::fromId($entry['object_id']);
468							$data = $item->getData();
469							$data['fields'] = array_filter($data['fields']);
470							$out[] = $data;
471						}
472					}
473
474					if ($out) {
475						$out = json_encode($out);
476					}
477
478					return $out;
479				}
480			)
481			->setParseIntoTransform(
482				function (& $info, $value) use ($permName, $trackerUtilities, $trackerThere, $itemsThereLookup, $attributelib, $fieldThere, $schema) {
483					static $newItemsThereCreated = [];
484
485					$data = json_decode($value, true);
486
487					if ($data && is_array($data)) {
488
489						foreach ($data as $row) {
490							if (! empty($row['itemId'])) {
491
492								// check the old itemId as an attribute to avoid repeat imports
493								$attr = $attributelib->find_objects_with('tiki.trackeritem.olditemid', $row['itemId']);
494
495								// not done this time?
496								if (! isset($newItemsThereCreated[$row['itemId']])) {
497
498									if (! isset($row['created']) && ! empty($row['creation_date'])) {
499										$row['created'] = $row['creation_date'];	// convert from index to database field name
500									}
501
502									$item = Tracker_Item::fromInfo($row);
503
504									// FIXME $schema here doesn't know if it's a transaction type so this never executes
505									if ($schema->isImportTransaction()) {
506										$trackerThereId = $trackerThere->getConfiguration('trackerId');
507										if (! $item->canModify()) {
508											throw new \Tracker\Tabular\Exception\Exception(tr(
509												'Permission denied importing into linked tracker %0',
510												$trackerThereId
511											));
512										}
513										$errors = $trackerUtilities->validateItem($trackerThere, $item->getData());
514										if ($errors) {
515											throw new \Tracker\Tabular\Exception\Exception(tr(
516												'Errors occurred importing into linked tracker %0',
517												$trackerThereId
518											));
519										}
520									}
521
522									$itemData = $item->getData();
523
524									// no item with this itemId and we didn't create it before? so let's make one!
525									if (! $itemsThereLookup->get($row['itemId']) && empty($attr)) {
526
527										// needs to be done after the new main item has been created
528										if (! isset($info['postprocess'])) {
529											$info['postprocess'] = [];
530										}
531										$info['postprocess'][] = function ($newMainItemId) use ($trackerUtilities, $trackerThere, $itemData, $fieldThere, $attributelib) {
532
533											// fix the ItemLink there to point at our new item
534											if ($fieldThere['type'] === 'r') {
535												$itemData['fields'][$fieldThere['permName']] = $newMainItemId;
536											}
537
538											$newItemId = $trackerUtilities->insertItem($trackerThere, $itemData);
539
540											if ($newItemId) {
541												$newItemsThereCreated[$itemData['itemId']] = $newItemId;
542												// store the old itemId as an attribute of this item so we don't import it again
543												$attributelib->set_attribute(
544													'trackeritem',
545													$newItemId,
546													'tiki.trackeritem.olditemid',
547													$itemData['itemId']
548												);
549
550											} else {
551												Feedback::error(
552													tr(
553														'Creating replacement linked item for itemId %0 for ItemsList field "%1" import failed on item #%2',
554														$itemData['itemId'], $this->getConfiguration('permName'), $this->getItemId()
555													)
556												);
557											}
558										};
559
560									} else if ($itemsThereLookup->get($row['itemId'])) {    // linked item exists, so update it
561										$item = Tracker_Item::fromInfo($row);
562										$itemData = $item->getData();
563										$result = $trackerUtilities->updateItem($trackerThere, $itemData);
564										if (! $result) {
565											Feedback::error(
566												tr(
567													'Updating linked item for itemId %0 for ItemsList field "%1" import failed on item #%2',
568													$itemData['itemId'], $this->getConfiguration('permName'), $this->getItemId()
569												)
570											);
571										}
572									}
573
574								}
575
576							}
577						}
578					}
579					$info['fields'][$permName] = '';
580				}
581			);
582
583		return $schema;
584	}
585
586
587
588	private function getItemIds()
589	{
590		$trklib = TikiLib::lib('trk');
591		$trackerId = (int) $this->getOption('trackerId');
592
593		$filterFieldIdHere = (int) $this->getOption('fieldIdHere');
594		$filterFieldIdThere = (int) $this->getOption('fieldIdThere');
595
596		$filterFieldHere = $this->getTrackerDefinition()->getField($filterFieldIdHere);
597		$filterFieldThere = $trklib->get_tracker_field($filterFieldIdThere);
598
599		$sortFieldIds = $this->getOption('sortField');
600		if (is_array($sortFieldIds)) {
601			$sortFieldIds = array_filter($sortFieldIds);
602		} else {
603			$sortFieldIds = [];
604		}
605		$status = $this->getOption('status', 'opc');
606		$tracker = Tracker_Definition::get($trackerId);
607
608
609
610		// note: if itemlink or dynamic item list is used, than the final value to compare with must be calculated based on the current itemid
611
612		$technique = 'value';
613
614		// not sure this is working
615		// r = item link
616		if ($tracker && $filterFieldThere && (! $filterFieldIdHere || $filterFieldThere['type'] === 'r' || $filterFieldThere['type'] === 'w')) {
617			if ($filterFieldThere['type'] === 'r' || $filterFieldThere['type'] === 'w') {
618				$technique = 'id';
619			}
620		}
621
622		// not sure this is working
623		// q = Autoincrement
624		if ($filterFieldHere['type'] == 'q' && isset($filterFieldHere['options_array'][3]) && $filterFieldHere['options_array'][3] == 'itemId') {
625			$technique = 'id';
626		}
627
628		if ($technique == 'id') {
629			$itemId = $this->getItemId();
630			if (! $itemId) {
631				$items = [];
632			} else {
633				$items = $trklib->get_items_list($trackerId, $filterFieldIdThere, $itemId, $status, false, $sortFieldIds);
634			}
635		} else {
636			// when this is an item link or dynamic item list field, localvalue contains the target itemId
637			$localValue = $this->getData($filterFieldIdHere);
638			if (! $localValue) {
639				// in some cases e.g. pretty tracker $this->getData($filterFieldIdHere) is not reliable as the info is not there
640				// Note: this fix only works if the itemId is passed via the template
641				$itemId = $this->getItemId();
642				$localValue = $trklib->get_item_value($trackerId, $itemId, $filterFieldIdHere);
643			}
644			if (! $filterFieldThere && $filterFieldHere && ( $filterFieldHere['type'] === 'r' || $filterFieldHere['type'] === 'w' ) && $localValue) {
645				// itemlink/dynamic item list field in this tracker pointing directly to an item in the other tracker
646				return [$localValue];
647			}
648			// r = item link - not sure this is working
649			if ($filterFieldHere['type'] == 'r' && isset($filterFieldHere['options_array'][0]) && isset($filterFieldHere['options_array'][1])) {
650				$localValue = $trklib->get_item_value($filterFieldHere['options_array'][0], $localValue, $filterFieldHere['options_array'][1]);
651			}
652
653			// w = dynamic item list - localvalue is the itemid of the target item. so rewrite.
654			if ($filterFieldHere['type'] == 'w') {
655				$localValue = $trklib->get_item_value($trackerId, $localValue, $filterFieldIdThere);
656			}
657			// u = user selector, might be mulitple users so need to find multiple values
658			if ($filterFieldHere['type'] == 'u' && ! empty($filterFieldHere['options_map']['multiple'])
659				&& $localValue
660			) {
661				if (! is_array($localValue)) {
662					$theUsers = explode(',', $localValue);
663				} else {
664					$theUsers = $localValue;
665				}
666				$items = [];
667				foreach ($theUsers as $theUser) {
668					$items = array_merge(
669						$items,
670						$trklib->get_items_list($trackerId, $filterFieldIdThere, $theUser, $status, false, $sortFieldIds)
671					);
672				}
673
674				return $items;
675			}
676			// Skip nulls
677			if ($localValue) {
678				$items = $trklib->get_items_list($trackerId, $filterFieldIdThere, $localValue, $status, false, $sortFieldIds);
679			} else {
680				$items = [];
681			}
682		}
683
684		return $items;
685	}
686
687	/**
688	 * Get value of displayfields from given array of itemIds
689	 * @param array $items
690	 * @param array $context
691	 * @return array array of values by itemId
692	 */
693	private function getItemLabels($items, $context = ['list_mode' => ''])
694	{
695		$displayFields = $this->getOption('displayFieldIdThere');
696		$trackerId = (int) $this->getOption('trackerId');
697		$status = $this->getOption('status', 'opc');
698
699		$definition = Tracker_Definition::get($trackerId);
700		if (! $definition) {
701			return [];
702		}
703
704		$list = [];
705		$trklib = TikiLib::lib('trk');
706		foreach ($items as $itemId) {
707			if ($displayFields && $displayFields[0]) {
708				$list[$itemId] = $trklib->concat_item_from_fieldslist(
709					$trackerId,
710					$itemId,
711					$displayFields,
712					$status,
713					' ',
714					isset($context['list_mode']) ? $context['list_mode'] : '',
715					$this->getOption('linkToItems'),
716					$this->getOption('displayFieldIdThereFormat'),
717					$trklib->get_tracker_item($itemId)
718				);
719			} else {
720				$list[$itemId] = $trklib->get_isMain_value($trackerId, $itemId);
721			}
722		}
723
724		return $list;
725	}
726
727	/**
728	 * Get remote items' values in an array as opposed to a string label.
729	 * Useful in Math calculations where individual field values are needed.
730	 * @return array associated array of field names and values
731	 */
732	public function getItemValues()
733	{
734		$displayFields = $this->getOption('displayFieldIdThere');
735		$trackerId = (int) $this->getOption('trackerId');
736
737		$definition = Tracker_Definition::get($trackerId);
738		if (! $definition) {
739			return [];
740		}
741
742		$itemsValues = [];
743
744		$items = $this->getItemIds();
745		foreach ($items as $itemId) {
746			$item = TikiLib::lib('trk')->get_tracker_item($itemId);
747			$itemValues = [];
748			if ($displayFields) {
749				foreach ($displayFields as $fieldId) {
750					$field = $definition->getField($fieldId);
751					if (isset($item[$fieldId])) {
752						if ($field['type'] == 'l') {
753							$factory = $definition->getFieldFactory();
754							$handler = $factory->getHandler($field, $item);
755							$itemValues[$field['permName']] = $handler->renderOutput(['list_mode' => 'csv']);
756						} else {
757							$itemValues[$field['permName']] = $item[$fieldId];
758						}
759					} else {
760						$itemValues[$field['permName']] = '';
761					}
762				}
763			}
764			$itemsValues[] = $itemValues;
765		}
766
767		return $itemsValues;
768	}
769}
770