Page MenuHomeIn-Portal Phabricator

in-portal
No OneTemporary

File Metadata

Created
Thu, Feb 6, 5:19 PM

in-portal

Index: branches/5.0.x/core/units/custom_fields/custom_fields_config.php
===================================================================
--- branches/5.0.x/core/units/custom_fields/custom_fields_config.php (revision 13373)
+++ branches/5.0.x/core/units/custom_fields/custom_fields_config.php (revision 13374)
@@ -1,188 +1,188 @@
<?php
/**
* @version $Id$
* @package In-Portal
* @copyright Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/
defined('FULL_PATH') or die('restricted access!');
$config = Array(
'Prefix' => 'cf',
'ItemClass' => Array('class'=>'kDBItem','file'=>'','build_event'=>'OnItemBuild'),
'ListClass' => Array('class'=>'kDBList','file'=>'','build_event'=>'OnListBuild'),
'EventHandlerClass' => Array('class'=>'CustomFieldsEventHandler','file'=>'custom_fields_event_handler.php','build_event'=>'OnBuild'),
'TagProcessorClass' => Array('class'=>'CustomFieldsTagProcessor','file'=>'custom_fields_tag_processor.php','build_event'=>'OnBuild'),
'AutoLoad' => true,
'hooks' => Array(),
'QueryString' => Array(
1 => 'id',
2 => 'page',
3 => 'event',
4 => 'type',
5 => 'mode',
),
'Hooks' => Array(
Array(
'Mode' => hAFTER,
'Conditional' => false,
'HookToPrefix' => 'cf',
'HookToSpecial' => '*',
'HookToEvent' => Array('OnSave'), // edit cloned fields to made alters :)
'DoPrefix' => 'cf',
'DoSpecial' => '*',
'DoEvent' => 'OnSaveCustomField',
),
),
'IDField' => 'CustomFieldId',
'OrderField' => 'DisplayOrder',
'TitleField' => 'FieldName', // field, used in bluebar when editing existing item
'TitlePhrase' => 'la_title_CustomFields',
'TitlePresets' => Array(
'default' => Array( 'new_status_labels' => Array('cf'=>'!la_title_addingCustom!'),
'edit_status_labels' => Array('cf'=>'!la_title_Editing_CustomField!'),
'new_titlefield' => Array('cf'=>''),
),
'custom_fields_list'=>Array( 'prefixes' => Array('cf_List'),
'format' => "!la_tab_ConfigCustom!",
),
'custom_fields_edit'=>Array( 'prefixes' => Array('cf'),
'new_titlefield' => Array('cf'=>''),
'format' => "#cf_status# '#cf_titlefield#'",
),
),
'TableName' => TABLE_PREFIX.'CustomField',
'ListSQLs' => Array( ''=>'SELECT * FROM %s',
), // key - special, value - list select sql
'ListSortings' => Array(
'' => Array(
'ForcedSorting' => Array('DisplayOrder' => 'asc'),
'Sorting' => Array('FieldName' => 'asc'),
),
'general' => Array(
'Sorting' => Array('DisplayOrder' => 'asc')
),
),
'ItemSQLs' => Array( ''=>'SELECT * FROM %s',
),
'SubItems' => Array('confs-cf'),
'Fields' => Array (
'CustomFieldId' => Array('type' => 'int', 'not_null' => 1, 'default' => 0),
'Type' => Array('type' => 'int', 'not_null' => 1, 'default' => 0),
'FieldName' => Array('required'=>'1', 'type' => 'string','not_null' => 1,'default' => ''),
'FieldLabel' => Array('type' => 'string', 'required' => 1, 'default' => null),
'MultiLingual' => Array (
'type' => 'int',
'formatter' => 'kOptionsFormatter',
'options' => Array (0 => 'la_No', 1 => 'la_Yes'), 'use_phrases' => 1,
'not_null' => 1, 'default' => 1,
),
'Heading' => Array('type' => 'string', 'required' => 1, 'default' => null),
'Prompt' => Array('type' => 'string','default' => null),
'ElementType' => Array('required'=>'1', 'type'=>'string', 'not_null'=>1, 'default'=>'', 'formatter'=>'kOptionsFormatter', 'use_phrases' => 1, 'options'=>Array('text' => 'la_type_text', 'select' => 'la_type_select', 'multiselect' => 'la_type_multiselect', 'radio' => 'la_type_radio', 'checkbox' => 'la_type_checkbox', 'password' => 'la_type_password', 'textarea' => 'la_type_textarea', 'label' => 'la_type_label', 'date' => 'la_type_date', 'datetime' => 'la_type_datetime')),
'ValueList' => Array('type' => 'string','default' => null),
'DefaultValue' => Array ('type' => 'string', 'max_len' => 255, 'not_null' => 1, 'default' => ''),
'DisplayOrder' => Array('type' => 'int', 'not_null' => 1, 'default' => 0),
'OnGeneralTab' => Array (
'type' => 'int',
'formatter' => 'kOptionsFormatter',
'options' => Array (0 => 'la_No', 1 => 'la_Yes'), 'use_phrases' => 1,
'not_null' => 1, 'default' => 0,
),
'IsSystem' => Array (
'type' => 'int',
'formatter' => 'kOptionsFormatter',
'options' => Array (0 => 'la_No', 1 => 'la_Yes'), 'use_phrases' => 1,
'not_null' => 1, 'default' => 0,
),
'IsRequired' => Array (
'type' => 'int',
'formatter' => 'kOptionsFormatter',
'options' => Array (0 => 'la_No', 1 => 'la_Yes'), 'use_phrases' => 1,
'not_null' => 1, 'default' => 0,
),
),
'VirtualFields' => Array(
'Value' => Array('type' => 'string', 'default' => ''),
'OriginalValue' => Array('type' => 'string', 'default' => ''),
'Error' => Array('type' => 'string', 'default' => ''),
'DirectOptions' => Array('type' => 'string', 'default' => ''),
'SortValues' => Array (
'type' => 'int',
'formatter' => 'kOptionsFormatter',
'options' => Array (1 => 'la_Yes', 0 => 'la_No'), 'use_phrases' => 1,
'default' => 0,
),
// for ValueList field editing via "inp_edit_minput" control
- 'OptionKey' => Array ('type' => 'int', 'not_null' => 1, 'default' => ''),
+ 'OptionKey' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
'OptionTitle' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
'Options' => Array ('type' => 'string', 'not_null' => 1, 'default' => ''),
),
'Grids' => Array(
'Default' => Array (
'Icons' => Array (
'default' => 'icon16_item.png',
),
'Fields' => Array (
'CustomFieldId' => Array ( 'title'=>'la_col_Id', 'data_block' => 'grid_checkbox_td', 'filter_block' => 'grid_range_filter', 'width' => 70, ),
'FieldName' => Array ( 'title'=>'la_prompt_FieldName', 'width' => 250, ),
'FieldLabel' => Array ( 'title'=>'la_prompt_FieldLabel', 'data_block' => 'cf_grid_data_td', 'width' => 250, ),
'DisplayOrder' => Array ('title' => 'la_prompt_DisplayOrder', 'filter_block' => 'grid_range_filter', 'width' => 105, ),
// 'IsSystem' => Array ('title' => 'la_col_IsSystem', 'filter_block' => 'grid_options_filter'),
),
),
'SeparateTab' => Array (
'Icons' => Array (
'default' => 'icon16_item.png',
),
'Selector' => '',
'Fields' => Array (
'FieldName' => Array ( 'title'=>'la_col_FieldName', 'data_block' => 'grid_checkbox_td', 'filter_block' => 'grid_like_filter', 'width' => 200, ),
'Prompt' => Array ( 'title'=>'la_col_Prompt', 'data_block' => 'grid_data_label_ml_td', 'ElementTypeField' => 'ElementType', 'filter_block' => 'grid_empty_filter', 'width' => 200, ),
'Value' => Array ( 'title'=>'la_col_Value', 'data_block' => 'edit_custom_td', 'filter_block' => 'grid_empty_filter', 'width' => 200, ),
'Error' => Array ( 'title'=>'la_col_Error', 'data_block' => 'custom_error_td', 'filter_block' => 'grid_empty_filter', 'width' => 100, ),
),
),
'SeparateTabOriginal' => Array (
'Icons' => Array (
'default' => 'icon16_item.png',
),
'Selector' => '',
'Fields' => Array (
'FieldName' => Array ( 'title'=>'la_col_FieldName', 'data_block' => 'grid_icon_td', 'filter_block' => 'grid_like_filter'),
'Prompt' => Array ( 'title'=>'la_col_Prompt', 'data_block' => 'grid_data_label_ml_td', 'ElementTypeField' => 'ElementType', 'filter_block' => 'grid_empty_filter'),
'Value' => Array ( 'title'=>'la_col_Value', 'data_block' => 'edit_custom_td', 'filter_block' => 'grid_empty_filter'),
'OriginalValue' => Array ( 'title'=>'la_col_OriginalValue', 'data_block' => 'grid_original_td', 'filter_block' => 'grid_like_filter'),
),
),
),
);
\ No newline at end of file
Index: branches/5.0.x/core/admin_templates/js/form_controls.js
===================================================================
--- branches/5.0.x/core/admin_templates/js/form_controls.js (revision 13373)
+++ branches/5.0.x/core/admin_templates/js/form_controls.js (revision 13374)
@@ -1,540 +1,540 @@
function MultiInputControl($field_name, $field_mask, $field_labels, $result_mask) {
this.FieldName = $field_name;
this.ValidateURL = '';
this.FieldMask = $field_mask;
this.FieldLabels = $field_labels;
this.ResultMask = $result_mask; // format of record in list
this.Permissions = new Array (); // action groups allowed
this.Messages = new Array (); // various phrase (errors, confirmations, button titles)
this.Controls = {}; // controls used for editing list content
this.Records = new Array (); // data to be submitted (needs to be parsed using formatters)
this.Errors = {}; // error messages in fields
this.InEditing = false;
}
MultiInputControl.prototype.registerControl = function($field_name, $type, $required, $options) {
this.Controls[$field_name] = {'type' : $type, 'required' : $required, 'options' : $options};
}
MultiInputControl.prototype.getControl = function ($field, $appendix, $prepend) {
$appendix = isset($appendix) ? '_' + $appendix : '';
$prepend = isset($prepend) ? $prepend + '_' : '';
return document.getElementById( $prepend + this.FieldMask.replace('#FIELD_NAME#', $field) + $appendix );
}
MultiInputControl.prototype.getControlValue = function ($field) {
var $value = '';
switch (this.Controls[$field]['type']) {
case 'select':
var $control = this.getControl($field);
$value = $control.options[$control.selectedIndex].value;
break;
/*case 'datetime':
$value = this.getControl($field + '_date').value + ' ' + this.getControl($field + '_time').value;
break;*/
default:
$value = this.getControl($field).value;
break;
}
return $value;
}
MultiInputControl.prototype.setControlValue = function ($field, $value) {
switch (this.Controls[$field]['type']) {
case 'select':
var $i = 0;
var $control = this.getControl($field);
if ($value === null) {
$control.selectedIndex = 0;
}
while ($i < $control.options.length) {
if ($control.options[$i].value == $value) {
$control.selectedIndex = $i;
break;
}
$i++;
}
break;
case 'checkbox':
this.getControl($field).value = ($value === null) ? 0 : $value;
this.getControl($field, null, '_cb').checked = parseInt($value) == 1;
break;
/*case 'datetime':
$value = $value.split(' ');
this.getControl($field + '_date').value = $value[0];
this.getControl($field + '_time').value = $value[1];
break;*/
default:
this.getControl($field).value = ($value === null) ? '' : $value;
break;
}
}
MultiInputControl.prototype.formatValue = function ($field, $value) {
if (this.Controls[$field]['type'] == 'select') {
var $i = 0;
var $control = this.getControl($field);
while ($i < $control.options.length) {
if ($control.options[$i].value == $value) {
$value = $control.options[$i].innerHTML;
break;
}
$i++;
}
}
if (this.Controls[$field]['type'] == 'textbox') {
var $field_options = this.Controls[$field]['options'];
if ($field_options && parseInt($field_options.first_chars) > 0) {
$value = $value.substring(0, parseInt($field_options.first_chars));
}
}
if (this.Controls[$field]['type'] == 'checkbox') {
$value = this.Controls[$field]['options'][ parseInt($value) ];
}
return $value;
}
MultiInputControl.prototype.formatLine = function($record_index) {
var $ret = this.ResultMask;
for (var $field_name in this.Controls) {
var $value = this.Records[$record_index][$field_name];
$ret = $ret.replace('#' + $field_name + '#', this.formatValue($field_name, $value));
}
return this.htmlspecialchars($ret);
}
MultiInputControl.prototype._getRecordIndex = function ($selected_index) {
var $object = this.getControl(this.FieldName, 'minput');
if (!isset($selected_index)) {
$selected_index = $object.selectedIndex;
}
return $selected_index == -1 ? -1 : $object.options[$selected_index].value;
}
MultiInputControl.prototype.makeRequest = function($request_type, $record, $skip_index) {
var $url = this.ValidateURL;
for (var $field_name in $record) {
$url += '&' + this.FieldMask.replace('#FIELD_NAME#', $field_name) + '=' + escape($record[$field_name]);
}
Request.makeRequest($url, this.BusyRequest, '', this.successCallback, this.errorCallback, [$request_type, $record, $skip_index], this);
}
MultiInputControl.prototype.AddRecord = function() {
var $record = this.prepareRecord();
if (this.InEditing) {
// already in editing
var $record_index = this.getControl(this.FieldName, 'minput').selectedIndex;
this.makeRequest('SaveRecord', $record, $record_index);
return ;
}
if (this.hasPermission('add')) {
this.makeRequest('AddRecord', $record, false);
}
}
MultiInputControl.prototype.EditRecord = function() {
var $record_index = this._getRecordIndex(); // this.getControl(this.FieldName, 'minput').selectedIndex;
if ($record_index == -1 || this.InEditing) {
// no record selected
return ;
}
this.InEditing = true;
var $edit_record = this.Records[$record_index];
for (var $field_name in $edit_record) {
this.setControlValue($field_name, $edit_record[$field_name]);
}
this.getControl(this.FieldName, 'add_button').value = this.Messages['save_button'];
this.getControl(this.FieldName, 'minput').disabled = true;
this.SetButtonState('edit', false);
this.SetButtonState('delete', false);
}
MultiInputControl.prototype.ResetControls = function() {
for (var $field_name in this.Controls) {
this.setControlValue($field_name, null);
}
this.Errors = {};
}
MultiInputControl.prototype.CancelEditing = function() {
this.ResetControls();
this.getControl(this.FieldName, 'add_button').value = this.Messages['add_button'];
this.getControl(this.FieldName, 'minput').disabled = false;
this.SetButtonState('edit', true);
this.SetButtonState('delete', true);
this.InEditing = false;
}
MultiInputControl.prototype.ShowRecord = function($option_index) {
var $options = this.getControl(this.FieldName, 'minput').options;
if ($option_index < $options.length) {
// update existing record
$options[$option_index].innerHTML = this.formatLine( this._getRecordIndex($option_index) );
}
else {
// create new record
var $new_option = document.createElement('OPTION');
$options.add($new_option, $options.length);
$new_option.value = $option_index; // will be used in move up/down & sorting (if any)
$new_option.innerHTML = this.formatLine(this.Records.length - 1);
}
}
MultiInputControl.prototype.DeleteRecords = function() {
if (!confirm(this.Messages['delete_confirm'])) {
return ;
}
var $control = this.getControl(this.FieldName, 'minput');
var $i = $control.length - 1;
while ($i >= 0) {
if ($control.options[$i].selected == true) {
this.Records[$control.options[$i].value] = null; // preserves index, when removing element from middle of array. this.Records.splice($control.options[$i].value, 1);
$control.remove($i);
}
$i--;
}
this.SaveValues();
}
MultiInputControl.prototype.MoveRecordsUp = function() {
move_options_up(this.getControl(this.FieldName, 'minput'), 1);
this.SaveValues();
}
MultiInputControl.prototype.MoveRecordsDown = function() {
move_options_down(this.getControl(this.FieldName, 'minput'), 1);
this.SaveValues();
}
MultiInputControl.prototype.AddFromXML = function($xml) {
var $document = getDocumentFromXML($xml);
this.ProcessXMLNode($document);
}
MultiInputControl.prototype.ProcessXMLNode = function($node, $root_name) {
for (var $i = 0; $i < $node.childNodes.length; $i++) {
var $child = $node.childNodes.item($i);
if ($child.tagName == 'record') {
- this.Records[this.Records.length] = new Array ();
+ this.Records[this.Records.length] = {};
this.ProcessXMLNode($child, $root_name);
this.ShowRecord(this.Records.length - 1);
}
else if ($child.tagName == 'field') {
if ($root_name == 'records') {
// no firstChild, when node value is empty!
this.Records[this.Records.length - 1][$child.getAttribute('name')] = $child.firstChild ? $child.firstChild.nodeValue : '';
}
else if ($root_name == 'errors') {
this.Errors[$child.getAttribute('name')] = $child.firstChild.nodeValue;
}
}
else if ($child.tagName == 'records') {
this.ProcessXMLNode($child, $child.tagName);
}
else if ($child.tagName == 'errors') {
this.Errors = {};
this.ProcessXMLNode($child, $child.tagName);
}
}
}
MultiInputControl.prototype.LoadValues = function() {
var $current_value = this.getControl(this.FieldName).value;
if ($current_value) {
this.AddFromXML($current_value);
}
}
MultiInputControl.prototype.SaveValues = function() {
var $object = this.getControl(this.FieldName, 'minput');
var $record_index = 0;
var $xml = '';
var $i = 0;
while ($i < $object.options.length) {
$record_index = $object.options[$i].value;
$xml += '<record>';
for (var $field_name in this.Controls) {
$xml += '<field name="' + $field_name + '">' + this.htmlspecialchars(this.Records[$record_index][$field_name]) + '</field>';
}
$xml += '</record>';
$i++;
}
this.getControl(this.FieldName).value = $xml ? '<records>' + $xml + '</records>' : '';
}
MultiInputControl.prototype.htmlspecialchars = function (string) {
string = string.toString();
string = string.replace(/&/g, '&amp;');
string = string.replace(/</g, '&lt;');
string = string.replace(/>/g, '&gt;');
string = string.replace(/\"/g, '&quot;');
return string;
}
MultiInputControl.prototype.prepareRecord = function() {
var $record = {};
for (var $field_name in this.Controls) {
$record[$field_name] = this.getControlValue($field_name);
}
return $record;
}
MultiInputControl.prototype.ValidateRecord = function($record, $skip_index) {
var $valid = true;
$valid = $valid && this.ValidateRequired($record);
$valid = $valid && this.ValidateUnique($record, $skip_index);
return $valid;
}
MultiInputControl.prototype.ValidateRequired = function($record) {
for (var $field_name in $record) {
if (this.Controls[$field_name]['required'] && !$record[$field_name]) {
alert(this.Messages['required_error']);
return false;
}
}
return true;
}
MultiInputControl.prototype.compareRecords = function($record_a, $record_b) {
var $equals = true;
for (var $field_name in $record_a) {
if ($record_a[$field_name] !== $record_b[$field_name]) {
return false;
}
}
return $equals;
}
MultiInputControl.prototype.ValidateUnique = function($record, $skip_index) {
var $i = 0;
if (!isset($skip_index)) {
$skip_index = -1;
}
while ($i < this.Records.length) {
if (this.Records[$i] == null) {
// skip deleted records
$i++;
continue;
}
if ($i != $skip_index && this.compareRecords($record, this.Records[$i])) {
alert(this.Messages['unique_error']);
return false;
}
$i++;
}
return true;
}
MultiInputControl.prototype.displayErrors = function() {
var $has_errors = false;
var $field_label = '';
for (var $field_name in this.Errors) {
$has_errors = true;
alert(this.FieldLabels[$field_name] + ': ' + this.Errors[$field_name]);
}
return $has_errors;
}
MultiInputControl.prototype.successCallback = function($request, $params, $object) {
if (Request.processRedirect($request) === true) {
return ;
}
var $document = getDocumentFromXML($request.responseText);
$object.ProcessXMLNode($document);
if ($object.displayErrors()) {
return ;
}
// params: 0 - action type, 1 - record data, 2 - option index
switch ($params[0]) {
case 'AddRecord':
if (!$object.ValidateRecord($params[1])) {
return ;
}
$object.Records.push($params[1]);
$object.ShowRecord($object.Records.length - 1);
$object.ResetControls();
break;
case 'SaveRecord':
$record_index = $object._getRecordIndex($params[2]);
if (!$object.ValidateRecord($params[1], $record_index)) {
return ;
}
$object.Records[$record_index] = $params[1];
$object.ShowRecord($params[2]);
$object.CancelEditing();
break;
}
$object.SaveValues();
}
MultiInputControl.prototype.errorCallback = function($request, $params, $object) {
alert('AJAX Error; class: MultiInputControl; ' + Request.getErrorHtml($request));
}
MultiInputControl.prototype.SetMessage = function($pseudo, $message) {
this.Messages[$pseudo] = $message;
}
MultiInputControl.prototype.InitEvents = function() {
var $button = null;
var $var_name = this.FieldName;
$button = this.getControl(this.FieldName, 'add_button');
$button.onclick = function() { eval($var_name).AddRecord() };
if (this.hasPermission('add') || this.hasPermission('edit')) {
$button = this.getControl(this.FieldName, 'cancel_button');
$button.onclick = function() { eval($var_name).CancelEditing() };
}
if (this.hasPermission('edit')) {
$button = this.getControl(this.FieldName, 'edit_button');
$button.onclick = function() { eval($var_name).EditRecord() };
$button = this.getControl(this.FieldName, 'minput');
$button.ondblclick = function() { eval($var_name).EditRecord() };
}
if (this.hasPermission('delete')) {
$button = this.getControl(this.FieldName, 'delete_button');
$button.onclick = function() { eval($var_name).DeleteRecords() };
}
if (this.hasPermission('move')) {
$button = this.getControl(this.FieldName, 'moveup_button');
$button.onclick = function() { eval($var_name).MoveRecordsUp() };
$button = this.getControl(this.FieldName, 'movedown_button');
$button.onclick = function() { eval($var_name).MoveRecordsDown() };
}
}
MultiInputControl.prototype.hasPermission = function ($perm_name) {
return in_array($perm_name, this.Permissions);
}
MultiInputControl.prototype.SetPermission = function ($perm_name, $perm_value) {
var $perm_index = array_search($perm_name, this.Permissions);
if ($perm_index != -1) {
// permission found
if (!$perm_value) {
this.Permissions = this.Permissions.splice($perm_index, 1);
}
}
else if ($perm_value) {
// permission not found
this.Permissions.push($perm_name);
}
}
MultiInputControl.prototype.SetButtonState = function ($button, $mode) {
if (!this.hasPermission($button)) {
return ;
}
var $button = this.getControl(this.FieldName, $button + '_button');
$button.disabled = !$mode;
$button.className = $mode ? 'button' : 'button-disabled';
}
// =======================================================================================
function EditPickerControl($field_name, $field_mask) {
this.FieldName = $field_name;
this.FieldMask = $field_mask;
this.Messages = new Array ();
this.InitEvents();
select_sort( this.getControl('available') );
}
EditPickerControl.prototype.getControl = function ($type) {
var $control_id = this.FieldMask + (isset($type) ? '_' + $type : '');
return document.getElementById($control_id);
}
EditPickerControl.prototype.SetMessage = function ($pseudo, $message) {
this.Messages[$pseudo] = $message;
}
EditPickerControl.prototype.SaveValues = function () {
this.getControl().value = select_to_string(this.getControl('selected'));
this.getControl('available_field').value = select_to_string(this.getControl('available'));
}
EditPickerControl.prototype.MoveLeft = function () {
move_selected(this.getControl('available'), this.getControl('selected'), this.Messages['nothing_selected']);
this.SaveValues();
}
EditPickerControl.prototype.MoveRight = function () {
move_selected(this.getControl('selected'), this.getControl('available'), this.Messages['nothing_selected']);
select_sort( this.getControl('available') );
this.SaveValues();
}
EditPickerControl.prototype.InitEvents = function() {
var $button = null;
var $var_name = this.FieldName;
$button = this.getControl('move_left_button');
$button.onclick = function() { eval($var_name).MoveLeft() };
$button = this.getControl('move_right_button');
$button.onclick = function() { eval($var_name).MoveRight() };
}
\ No newline at end of file

Event Timeline