addJs('js/october.relation.js', 'core'); $this->addCss('css/relation.css', 'core'); /* * Build configuration */ $this->config = $this->originalConfig = $this->makeConfig($controller->relationConfig, $this->requiredConfig); } /** * Prepare the widgets used by this behavior * @param Model $model * @param string $field * @return void */ public function initRelation($model, $field = null) { if ($field == null) { $field = post(self::PARAM_FIELD); } $this->config = $this->originalConfig; $this->model = $model; $this->field = $field; if ($field == null) { return; } if (!$this->model) { throw new ApplicationException(Lang::get( 'backend::lang.relation.missing_model', ['class'=>get_class($this->controller)] )); } if (!$this->model instanceof Model) { throw new ApplicationException(Lang::get( 'backend::lang.model.invalid_class', ['model'=>get_class($this->model), 'class'=>get_class($this->controller)] )); } if (!$this->getConfig($field)) { throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field'))); } $this->alias = camel_case('relation ' . $field); $this->config = $this->makeConfig($this->getConfig($field), $this->requiredRelationProperties); /* * Relationship details */ $this->relationName = $field; $this->relationType = $this->model->getRelationType($field); $this->relationObject = $this->model->{$field}(); $this->relationModel = $this->relationObject->getRelated(); $this->readOnly = $this->getConfig('readOnly'); $this->deferredBinding = $this->getConfig('deferredBinding') || !$this->model->exists; $this->toolbarButtons = $this->evalToolbarButtons(); $this->viewMode = $this->evalViewMode(); $this->manageMode = $this->evalManageMode(); $this->manageId = post('manage_id'); /* * Toolbar widget */ if ($this->toolbarWidget = $this->makeToolbarWidget()) { $this->toolbarWidget->bindToController(); } /* * View widget */ if ($this->viewWidget = $this->makeViewWidget()) { $this->viewWidget->bindToController(); } /* * Manage widget */ if ($this->manageWidget = $this->makeManageWidget()) { $this->manageWidget->bindToController(); } /* * Pivot widget */ if ($this->manageMode == 'pivot') { if ($this->pivotWidget = $this->makePivotWidget()) { $this->pivotWidget->bindToController(); } } } /** * Renders the relationship manager. * @param string $field The relationship field. * @param array $options * @return string Rendered HTML for the relationship manager. */ public function relationRender($field, $options = []) { $field = $this->validateField($field); if (is_string($options)) { $options = ['sessionKey' => $options]; } $this->prepareVars(); /* * Session key */ if (isset($options['sessionKey'])) { $this->sessionKey = $options['sessionKey']; } /* * Determine the partial to use based on the supplied section option */ $section = (isset($options['section'])) ? $options['section'] : null; switch (strtolower($section)) { case 'toolbar': return $this->toolbarWidget ? $this->toolbarWidget->render() : null; case 'view': return $this->relationMakePartial('view'); default: return $this->relationMakePartial('container'); } } /** * Refreshes the relation container only, useful for returning in custom AJAX requests. * @param string $field Relation definition. * @return array The relation element selector as the key, and the relation view contents are the value. */ public function relationRefresh($field = null) { $field = $this->validateField($field); $result = ['#'.$this->relationGetId('view') => $this->relationRenderView($field)]; if ($toolbar = $this->relationRenderToolbar($field)) { $result['#'.$this->relationGetId('toolbar')] = $toolbar; } if ($eventResult = $this->controller->relationExtendRefreshResults($field)) { $result = $eventResult + $result; } return $result; } /** * Renders the toolbar only. * @param string $field The relationship field. * @return string Rendered HTML for the toolbar. */ public function relationRenderToolbar($field = null) { return $this->relationRender($field, ['section' => 'toolbar']); } /** * Renders the view only. * @param string $field The relationship field. * @return string Rendered HTML for the view. */ public function relationRenderView($field = null) { return $this->relationRender($field, ['section' => 'view']); } /** * Validates the supplied field and initializes the relation manager. * @param string $field The relationship field. * @return string The active field name. */ protected function validateField($field = null) { $field = $field ?: post(self::PARAM_FIELD); if ($field && $field != $this->field) { $this->initRelation($this->model, $field); } if (!$field && !$this->field) { throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field'))); } return $field ?: $this->field; } /** * Prepares the view data. * @return void */ public function prepareVars() { $this->vars['relationManageId'] = $this->manageId; $this->vars['relationLabel'] = $this->config->label ?: $this->field; $this->vars['relationField'] = $this->field; $this->vars['relationType'] = $this->relationType; $this->vars['relationSearchWidget'] = $this->searchWidget; $this->vars['relationToolbarWidget'] = $this->toolbarWidget; $this->vars['relationManageMode'] = $this->manageMode; $this->vars['relationManageWidget'] = $this->manageWidget; $this->vars['relationToolbarButtons'] = $this->toolbarButtons; $this->vars['relationViewMode'] = $this->viewMode; $this->vars['relationViewWidget'] = $this->viewWidget; $this->vars['relationViewModel'] = $this->viewModel; $this->vars['relationPivotWidget'] = $this->pivotWidget; $this->vars['relationSessionKey'] = $this->relationGetSessionKey(); } /** * The controller action is responsible for supplying the parent model * so it's action must be fired. Additionally, each AJAX request must * supply the relation's field name (_relation_field). */ protected function beforeAjax() { if ($this->initialized) { return; } $this->controller->pageAction(); $this->validateField(); $this->prepareVars(); $this->initialized = true; } /** * Controller accessor for making partials within this behavior. * @param string $partial * @param array $params * @return string Partial contents */ public function relationMakePartial($partial, $params = []) { $contents = $this->controller->makePartial('relation_'.$partial, $params + $this->vars, false); if (!$contents) { $contents = $this->makePartial($partial, $params); } return $contents; } /** * Returns a unique ID for this relation and field combination. * @param string $suffix A suffix to use with the identifier. * @return string */ public function relationGetId($suffix = null) { $id = class_basename($this); if ($this->field) { $id .= '-' . $this->field; } if ($suffix !== null) { $id .= '-' . $suffix; } return $this->controller->getId($id); } /** * Returns the existing record IDs for the relation. */ protected function findExistingRelationIds($checkIds = null) { $foreignKeyName = $this->relationModel->getQualifiedKeyName(); $results = $this->relationObject ->getBaseQuery() ->select($foreignKeyName); if ($checkIds !== null && is_array($checkIds) && count($checkIds)) { $results = $results->whereIn($foreignKeyName, $checkIds); } return $results->lists($foreignKeyName); } // // Overrides // /** * Controller override: Extend the query used for populating the list * after the default query is processed. * @param October\Rain\Database\Builder $query * @param string $field */ public function relationExtendQuery($query, $field) { } public function relationExtendRefreshResults($field) { } // // AJAX (Buttons) // public function onRelationButtonAdd() { $this->eventTarget = 'button-add'; return $this->onRelationManageForm(); } public function onRelationButtonCreate() { $this->eventTarget = 'button-create'; return $this->onRelationManageForm(); } public function onRelationButtonDelete() { return $this->onRelationManageDelete(); } public function onRelationButtonLink() { $this->eventTarget = 'button-link'; return $this->onRelationManageForm(); } public function onRelationButtonUnlink() { return $this->onRelationManageRemove(); } public function onRelationButtonRemove() { return $this->onRelationManageRemove(); } public function onRelationButtonUpdate() { $this->eventTarget = 'button-update'; return $this->onRelationManageForm(); } // // AJAX (List events) // public function onRelationClickManageList() { return $this->onRelationManageAdd(); } public function onRelationClickManageListPivot() { return $this->onRelationManagePivotForm(); } public function onRelationClickViewList() { $this->eventTarget = 'list'; return $this->onRelationManageForm(); } // // AJAX // public function onRelationManageForm() { $this->beforeAjax(); if ($this->manageMode == 'pivot' && $this->manageId) { return $this->onRelationManagePivotForm(); } // The form should not share its session key with the parent $this->vars['newSessionKey'] = str_random(40); $view = 'manage_' . $this->manageMode; return $this->relationMakePartial($view); } /** * Create a new related model */ public function onRelationManageCreate() { $this->forceManageMode = 'form'; $this->beforeAjax(); $saveData = $this->manageWidget->getSaveData(); if ($this->viewMode == 'multi') { $sessionKey = $this->deferredBinding ? $this->relationGetSessionKey(true) : null; if ($this->relationType == 'hasMany') { $newModel = $this->relationObject->create($saveData, $sessionKey); } elseif ($this->relationType == 'belongsToMany') { $newModel = $this->relationObject->create($saveData, [], $sessionKey); } $newModel->commitDeferred($this->manageWidget->getSessionKey()); } elseif ($this->viewMode == 'single') { $newModel = $this->viewModel; $this->viewWidget->setFormValues($saveData); if ($this->relationType == 'belongsTo') { $newModel->save(); $this->relationObject->associate($newModel); $this->relationObject->getParent()->save(); } elseif ($this->relationType == 'hasOne') { $this->relationObject->add($newModel); } } return $this->relationRefresh(); } /** * Updated an existing related model's fields */ public function onRelationManageUpdate() { $this->forceManageMode = 'form'; $this->beforeAjax(); $saveData = $this->manageWidget->getSaveData(); if ($this->viewMode == 'multi') { $model = $this->relationObject->find($this->manageId); $model->save($saveData, $this->manageWidget->getSessionKey()); } elseif ($this->viewMode == 'single') { $this->viewWidget->setFormValues($saveData); $this->viewModel->save(); } return ['#'.$this->relationGetId('view') => $this->relationRenderView()]; } /** * Delete an existing related model completely */ public function onRelationManageDelete() { $this->beforeAjax(); /* * Multiple (has many, belongs to many) */ if ($this->viewMode == 'multi') { if (($checkedIds = post('checked')) && is_array($checkedIds)) { $relatedModel = $this->relationObject->getRelated(); foreach ($checkedIds as $relationId) { if (!$obj = $relatedModel->find($relationId)) { continue; } $obj->delete(); } } } /* * Single (belongs to, has one) */ elseif ($this->viewMode == 'single') { $relatedModel = $this->viewModel; if ($relatedModel->exists) { $relatedModel->delete(); } $this->viewWidget->setFormValues([]); $this->viewModel = $this->relationModel; } return $this->relationRefresh(); } /** * Add an existing related model to the primary model */ public function onRelationManageAdd() { $this->beforeAjax(); $recordId = post('record_id'); /* * Add */ if ($this->viewMode == 'multi') { $checkedIds = $recordId ? [$recordId] : post('checked'); if (is_array($checkedIds)) { /* * Remove existing relations from the array */ $existingIds = $this->findExistingRelationIds($checkedIds); $checkedIds = array_diff($checkedIds, $existingIds); $foreignKeyName = $this->relationModel->getKeyName(); $models = $this->relationModel->whereIn($foreignKeyName, $checkedIds)->get(); foreach ($models as $model) { $sessionKey = $this->deferredBinding ? $this->relationGetSessionKey() : null; $this->relationObject->add($model, $sessionKey); } } } /* * Link */ elseif ($this->viewMode == 'single') { if ($recordId && ($model = $this->relationModel->find($recordId))) { if ($this->relationType == 'belongsTo') { $this->relationObject->associate($model); $this->relationObject->getParent()->save(); } elseif ($this->relationType == 'hasOne') { $this->relationObject->add($model); } $this->viewWidget->setFormValues($model->attributes); } } return $this->relationRefresh(); } /** * Remove an existing related model from the primary model */ public function onRelationManageRemove() { $this->beforeAjax(); $recordId = post('record_id'); /* * Remove */ if ($this->viewMode == 'multi') { $checkedIds = $recordId ? [$recordId] : post('checked'); if (is_array($checkedIds)) { if ($this->relationType == 'belongsToMany') { $this->relationObject->detach($checkedIds); } elseif ($this->relationType == 'hasMany') { $relatedModel = $this->relationObject->getRelated(); foreach ($checkedIds as $relationId) { if ($obj = $relatedModel->find($relationId)) { $this->relationObject->remove($obj); } } } } } /* * Unlink */ elseif ($this->viewMode == 'single') { if ($this->relationType == 'belongsTo') { $this->relationObject->dissociate(); $this->relationObject->getParent()->save(); } elseif ($this->relationType == 'hasOne') { if ($obj = $this->relationModel->find($recordId)) { $this->relationObject->remove($obj); } elseif ($this->viewModel->exists) { $this->relationObject->remove($this->viewModel); } } $this->viewWidget->setFormValues([]); } return $this->relationRefresh(); } public function onRelationManagePivotForm() { $this->beforeAjax(); $this->vars['foreignId'] = post('foreign_id'); return $this->relationMakePartial('pivot_form'); } public function onRelationManagePivotCreate() { $this->beforeAjax(); $foreignId = post('foreign_id'); $foreignModel = $this->relationModel->find($foreignId); $saveData = $this->pivotWidget->getSaveData(); /* * Check for existing relation */ $foreignKeyName = $this->relationModel->getQualifiedKeyName(); $existing = $this->relationObject->where($foreignKeyName, $foreignId)->count(); if (!$existing) { $this->relationObject->add($foreignModel, null, $saveData); } return ['#'.$this->relationGetId('view') => $this->relationRenderView()]; } public function onRelationManagePivotUpdate() { $this->beforeAjax(); $foreignKeyName = $this->relationModel->getQualifiedKeyName(); $hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first(); $saveData = $this->pivotWidget->getSaveData(); $modelsToSave = $this->prepareModelsToSave($hydratedModel, $saveData); foreach ($modelsToSave as $modelToSave) { $modelToSave->save(); } return ['#'.$this->relationGetId('view') => $this->relationRenderView()]; } // // Widgets // protected function makeSearchWidget() { $config = $this->makeConfig(); $config->alias = $this->alias . 'ManageSearch'; $config->growable = false; $config->prompt = 'backend::lang.list.search_prompt'; $widget = $this->makeWidget('Backend\Widgets\Search', $config); $widget->cssClasses[] = 'recordfinder-search'; return $widget; } protected function makeToolbarWidget() { $defaultConfig = []; /* * Add buttons to toolbar */ $defaultButtons = null; if (!$this->readOnly) { $defaultButtons = '~/modules/backend/behaviors/relationcontroller/partials/_toolbar.htm'; } $defaultConfig['buttons'] = $this->getConfig('view[toolbarPartial]', $defaultButtons); /* * Make config */ $toolbarConfig = $this->makeConfig($this->getConfig('toolbar', $defaultConfig)); $toolbarConfig->alias = $this->alias . 'Toolbar'; /* * Add search to toolbar */ $useSearch = $this->viewMode == 'multi' && $this->getConfig('view[showSearch]'); if ($useSearch) { $toolbarConfig->search = [ 'prompt' => 'backend::lang.list.search_prompt' ]; } /* * No buttons, no search should mean no toolbar */ if (empty($toolbarConfig->search) && empty($toolbarConfig->buttons)) return; $toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig); $toolbarWidget->cssClasses[] = 'list-header'; return $toolbarWidget; } protected function makeViewWidget() { /* * Multiple (has many, belongs to many) */ if ($this->viewMode == 'multi') { $config = $this->makeConfigForMode('view', 'list'); $config->model = $this->relationModel; $config->alias = $this->alias . 'ViewList'; $config->showSorting = $this->getConfig('view[showSorting]', true); $config->defaultSort = $this->getConfig('view[defaultSort]'); $config->recordsPerPage = $this->getConfig('view[recordsPerPage]'); $config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly); $defaultOnClick = sprintf( "$.oc.relationBehavior.clickViewListRecord(':id', '%s', '%s')", $this->field, $this->relationGetSessionKey() ); $config->recordOnClick = $this->getConfig('view[recordOnClick]', $defaultOnClick); $config->recordUrl = $this->getConfig('view[recordUrl]', null); if ($emptyMessage = $this->getConfig('emptyMessage')) { $config->noRecordsMessage = $emptyMessage; } /* * Constrain the query by the relationship and deferred items */ $widget = $this->makeWidget('Backend\Widgets\Lists', $config); $widget->bindEvent('list.extendQuery', function ($query) { $this->controller->relationExtendQuery($query, $this->field); $this->relationObject->setQuery($query); if ($sessionKey = $this->relationGetSessionKey()) { $this->relationObject->withDeferred($sessionKey); } elseif ($this->model->exists) { $this->relationObject->addConstraints(); } /* * Allows pivot data to enter the fray */ if ($this->relationType == 'belongsToMany') { $this->relationObject->setQuery($query->getQuery()); return $this->relationObject; } }); /* * Constrain the list by the search widget, if available */ if ($this->toolbarWidget && $this->getConfig('view[showSearch]')) { if ($searchWidget = $this->toolbarWidget->getSearchWidget()) { $searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) { $widget->setSearchTerm($searchWidget->getActiveTerm()); return $widget->onRefresh(); }); $searchWidget->setActiveTerm(null); } } } /* * Single (belongs to, has one) */ elseif ($this->viewMode == 'single') { $query = $this->relationObject; $this->controller->relationExtendQuery($query, $this->field); $this->viewModel = $query->getResults() ?: $this->relationModel; $config = $this->makeConfigForMode('view', 'form'); $config->model = $this->viewModel; $config->arrayName = class_basename($this->relationModel); $config->context = 'relation'; $config->alias = $this->alias . 'ViewForm'; $widget = $this->makeWidget('Backend\Widgets\Form', $config); $widget->previewMode = true; } return $widget; } protected function makeManageWidget() { $widget = null; /* * Pivot */ if ($this->manageMode == 'pivot' && isset($this->config->pivot)) { $config = $this->makeConfigForMode('manage', 'list'); $config->model = $this->relationModel; $config->alias = $this->alias . 'ManagePivotList'; $config->showSetup = false; $config->showSorting = $this->getConfig('manage[showSorting]', false); $config->defaultSort = $this->getConfig('manage[defaultSort]'); $config->recordsPerPage = $this->getConfig('manage[recordsPerPage]'); $config->recordOnClick = sprintf( "$.oc.relationBehavior.clickManagePivotListRecord(:id, '%s', '%s')", $this->field, $this->relationGetSessionKey() ); $widget = $this->makeWidget('Backend\Widgets\Lists', $config); /* * Link the Search Widget to the List Widget */ if ($this->getConfig('pivot[showSearch]')) { $this->searchWidget = $this->makeSearchWidget(); $this->searchWidget->bindToController(); $this->searchWidget->bindEvent('search.submit', function () use ($widget) { $widget->setSearchTerm($this->searchWidget->getActiveTerm()); return $widget->onRefresh(); }); $this->searchWidget->setActiveTerm(null); } } /* * List */ elseif ($this->manageMode == 'list' && isset($this->config->list)) { $config = $this->makeConfigForMode('manage', 'list'); $config->model = $this->relationModel; $config->alias = $this->alias . 'ManageList'; $config->showSetup = false; $config->showCheckboxes = true; $config->showSorting = $this->getConfig('manage[showSorting]', true); $config->defaultSort = $this->getConfig('manage[defaultSort]'); $config->recordsPerPage = $this->getConfig('manage[recordsPerPage]'); if ($this->viewMode == 'single') { $config->showCheckboxes = false; $config->recordOnClick = sprintf( "$.oc.relationBehavior.clickManageListRecord(:id, '%s', '%s')", $this->field, $this->relationGetSessionKey() ); } $widget = $this->makeWidget('Backend\Widgets\Lists', $config); /* * Link the Search Widget to the List Widget */ if ($this->getConfig('manage[showSearch]')) { $this->searchWidget = $this->makeSearchWidget(); $this->searchWidget->bindToController(); $this->searchWidget->bindEvent('search.submit', function () use ($widget) { $widget->setSearchTerm($this->searchWidget->getActiveTerm()); return $widget->onRefresh(); }); $this->searchWidget->setActiveTerm(null); } } /* * Form */ elseif ($this->manageMode == 'form' && isset($this->config->form)) { /* * Determine supplied form context */ $manageConfig = isset($this->config->manage) ? $this->config->manage : []; if ($context = array_get($manageConfig, 'context')) { if (is_array($context)) { $context = $this->manageId ? array_get($context, 'update') : array_get($context, 'create'); } } $config = $this->makeConfigForMode('manage', 'form'); $config->model = $this->relationModel; $config->arrayName = class_basename($this->relationModel); $config->context = $context ?: 'relation'; $config->alias = $this->alias . 'ManageForm'; /* * Existing record */ if ($this->manageId) { $config->model = $config->model->find($this->manageId); if (!$config->model) { throw new ApplicationException(Lang::get('backend::lang.model.not_found', [ 'class' => get_class($config->model), 'id' => $this->manageId ])); } } $widget = $this->makeWidget('Backend\Widgets\Form', $config); } if (!$widget) { return null; } /* * Exclude existing relationships */ if ($this->manageMode == 'pivot' || $this->manageMode == 'list') { $widget->bindEvent('list.extendQuery', function ($query) { $this->controller->relationExtendQuery($query, $this->field); /* * Where not in the current list of related records */ $existingIds = $this->findExistingRelationIds(); if (count($existingIds)) { $query->whereNotIn($this->relationModel->getQualifiedKeyName(), $existingIds); } }); } return $widget; } protected function makePivotWidget() { $config = $this->makeConfigForMode('pivot', 'form'); $config->model = $this->relationModel; $config->arrayName = class_basename($this->relationModel); $config->context = 'relation'; $config->alias = $this->alias . 'ManagePivotForm'; /* * Existing record */ if ($this->manageId) { $foreignKeyName = $this->relationModel->getQualifiedKeyName(); $hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first(); $config->model = $hydratedModel; if (!$config->model) { throw new ApplicationException(Lang::get('backend::lang.model.not_found', [ 'class' => get_class($config->model), 'id' => $this->manageId ])); } } $widget = $this->makeWidget('Backend\Widgets\Form', $config); return $widget; } /** * Returns the active session key. */ public function relationGetSessionKey($force = false) { if ($this->sessionKey && !$force) { return $this->sessionKey; } if (post('_relation_session_key')) { return $this->sessionKey = post('_relation_session_key'); } if (post('_session_key')) { return $this->sessionKey = post('_session_key'); } return $this->sessionKey = FormHelper::getSessionKey(); } // // Helpers // /** * Determine the default buttons based on the model relationship type. * @return string */ protected function evalToolbarButtons() { if ($buttons = $this->getConfig('view[toolbarButtons]')) { return is_array($buttons) ? $buttons : array_map('trim', explode('|', $buttons)); } switch ($this->relationType) { case 'hasMany': case 'belongsToMany': return ['create', 'add', 'delete', 'remove']; case 'hasOne': case 'belongsTo': return ['create', 'update', 'link', 'delete', 'unlink']; } } /** * Determine the view mode based on the model relationship type. * @return string */ protected function evalViewMode() { if ($this->forceViewMode) { return $this->forceViewMode; } switch ($this->relationType) { case 'hasMany': case 'belongsToMany': return 'multi'; case 'hasOne': case 'belongsTo': return 'single'; } } /** * Determine the management mode based on the relation type and settings. * @return string */ protected function evalManageMode() { if ($mode = post(self::PARAM_MODE)) { return $mode; } if ($this->forceManageMode) { return $this->forceManageMode; } switch ($this->eventTarget) { case 'button-create': case 'button-update': return 'form'; case 'button-link': return 'list'; } switch ($this->relationType) { case 'belongsTo': return 'list'; case 'belongsToMany': if (isset($this->config->pivot)) return 'pivot'; elseif ($this->eventTarget == 'list') return 'form'; else return 'list'; case 'hasOne': case 'hasMany': if ($this->eventTarget == 'button-add') return 'list'; else return 'form'; } } /** * Returns the configuration for a mode (view, manage, pivot) for an * expected type (list, form). Uses fallback configuration. */ protected function makeConfigForMode($mode = 'view', $type = 'list') { $config = null; /* * Look for $this->config->view['list'] */ if ( isset($this->config->{$mode}) && array_key_exists($type, $this->config->{$mode}) ) { $config = $this->config->{$mode}[$type]; } /* * Look for $this->config->list */ elseif (isset($this->config->{$type})) { $config = $this->config->{$type}; } /* * Apply substitutes: * * - view.list => manage.list */ if (!$config) { if ($mode == 'manage' && $type == 'list') { return $this->makeConfigForMode('view', $type); } throw new ApplicationException('Missing configuration for '.$mode.'.'.$type.' in RelationController definition '.$this->field); } return $this->makeConfig($config); } }