示例#1
0
 /**
  * Filter the supplied Fields object using the filter's callback.
  *
  * @param Fields $fields
  * @return Fields
  */
 public function apply(Fields $fields)
 {
     $filteredFields = new Fields([], $fields->getUser());
     foreach ($fields as $field) {
         if (true === call_user_func($this->callback, $field)) {
             $filteredFields->add($field);
         }
     }
     return $filteredFields;
 }
 private function getFields()
 {
     $fields = new Fields();
     $fields->add('one')->setEditable(true)->setLabel('One')->assignHelperCallback('EditControl.Control', function ($helper, $view) {
         return 'FIELD_ONE';
     })->add('two')->setEditable(true)->setLabel('Two')->assignHelperCallback('EditControl.Control', function ($helper, $view) {
         return 'FIELD_TWO';
     })->add('three')->setEditable(false)->setLabel('Not Editable');
     return $fields;
 }
示例#3
0
 /**
  * When calling add() on this field, it will delegate the call back up to
  * the associated \Dewdrop\Fields object.  This allows for a very fluid
  * method chaining style when defining a large set of fields.
  *
  * @param mixed $field
  * @param string $modelName
  * @throws Exception
  * @return mixed
  */
 public function add($field, $modelName = null)
 {
     if (!$this->fieldsSet) {
         throw new Exception('Cannot add field because no \\Dewdrop\\Fields object is available');
     }
     return $this->fieldsSet->add($field, $modelName);
 }
示例#4
0
 public function testCanSetUserObject()
 {
     require_once __DIR__ . '/fields-test/TestUser.php';
     $user = new \DewdropTest\TestUser();
     $this->fields->setUser($user);
     $this->assertInstanceOf('Dewdrop\\Fields\\UserInterface', $this->fields->getUser());
 }
示例#5
0
 public function testCanOptionallySupplyCustomRenderer()
 {
     $fields = new Fields();
     $fields->add('custom')->setVisible(true)->setLabel('Custom')->assignHelperCallback('CsvCell.Content', function ($helper, $rowData) {
         return 'shouldnotberendered';
     })->add('not_visible')->setVisible(false)->setLabel('Unseen')->assignHelperCallback('CsvCell.Content', function ($helper, $rowData) {
         return 'content.unseen';
     });
     $renderer = $this->view->csvCellRenderer();
     $renderer->getContentRenderer()->assign('custom', function ($helper, $rowData) {
         return 'customrendering';
     });
     $output = $this->csvExportViewHelper->direct($fields, array(array('test_field' => 'FAFAFAFA')), $renderer);
     $this->assertContains('customrendering', $output);
     $this->assertNotContains('shouldnotberenderered', $output);
 }
示例#6
0
 /**
  * Remove a field from this group and the GroupedFields container.
  *
  * @param string $id
  * @return Group
  */
 public function remove($id)
 {
     // This check ensures we don't enter an infinite loop because the set will try to remove from the group as well
     if ($this->groupedFields->has($id)) {
         $this->groupedFields->remove($id);
     }
     return parent::remove($id);
 }
示例#7
0
 /**
  * Create any resources that need to be accessible both for processing
  * and rendering.
  *
  * @return void
  */
 public function init()
 {
     $this->model = Pimple::getResource('users-gateway');
     $this->fields = new Fields();
     $this->row = $this->model->find($this->request->getQuery('user_id'));
     $this->fields->add('password')->setLabel('Password')->setEditable(true)->add('confirm_password')->setLabel('Confirm Password')->setEditable(true);
     $this->inputFilter = new InputFilter();
     $password = new Input('password');
     $this->inputFilter->add($password);
     $password->setRequired(true)->getValidatorChain()->attach(new StringLength(array('min' => 6)));
     $confirm = new Input('confirm_password');
     $this->inputFilter->add($confirm);
     $validator = new Callback(array('callback' => function ($value) {
         return $value === $this->request->getPost('password');
     }));
     $validator->setMessage('Passwords do not match.');
     $confirm->setRequired(true)->getValidatorChain()->attach($validator);
 }
示例#8
0
 /**
  * Remove a field from this collection.  This will remove it from the
  * core collection and whatever group it is a member of.
  *
  * @param string $id
  * @return GroupedFields
  */
 public function remove($id)
 {
     parent::remove($id);
     foreach ($this->groups as $group) {
         if ($group->has($id)) {
             $group->remove($id);
         }
     }
     return $this;
 }
示例#9
0
 private function buildFields()
 {
     $fields = new Fields();
     $fields->add('file')->setLabel('File')->setVisible(true)->assignHelperCallback('TableCell.Content', function (TableCell $helper, array $rowData) {
         if (!isset($rowData['file'])) {
             return null;
         }
         return $helper->getView()->escapeHtml($rowData['file']);
     })->add('line')->setLabel('Line')->setVisible(true)->assignHelperCallback('TableCell.Content', function (TableCell $helper, array $rowData) {
         if (!isset($rowData['line'])) {
             return null;
         }
         return $helper->getView()->escapeHtml($rowData['line']);
     })->add('function')->setLabel('Function')->setVisible(true)->assignHelperCallback('TableCell.Content', function (TableCell $helper, array $rowData) {
         $out = '';
         if (!isset($rowData['class'])) {
             $out .= $helper->getView()->escapeHtml($rowData['function']);
         } else {
             $out .= $helper->getView()->escapeHtml("{$rowData['class']}{$rowData['type']}{$rowData['function']}");
         }
         $out .= '(';
         $argStrings = [];
         foreach ($rowData['args'] as $arg) {
             if (is_array($arg)) {
                 $argStrings[] = 'Array';
             } elseif (is_object($arg)) {
                 $argStrings[] = get_class($arg);
             } else {
                 $argStrings[] = var_export($arg, true);
             }
         }
         $out .= implode(', ', $argStrings);
         $out .= ')';
         return $out;
     });
     return $fields;
 }
示例#10
0
 /**
  * Build a Fields object that can be used for displaying or editing
  * subscriptions.
  *
  * @todo Refactor this into a separate class.
  *
  * @param string $editUrl
  * @param Fields $componentFields
  * @return Fields
  */
 public function buildFields($editUrl, Fields $componentFields)
 {
     $fields = new Fields();
     $fields->add('recipients')->setLabel('Recipients')->setNote('Enter one or more email addresses separated by commas.')->setVisible(true)->assignHelperCallback('TableCell.Content', function (TableCellHelper $helper, array $rowData) use($editUrl) {
         return $helper->getView()->escapeHtml($this->renderRecipients($rowData['dewdrop_notification_subscription_id']));
     })->setEditable(true)->assignHelperCallback('EditControl.Control', function ($helper, View $view) {
         return $view->inputText('recipients', $this->renderRecipients($view->getRequest()->getQuery('dewdrop_notification_subscription_id')), 'form-control');
     })->assignHelperCallback('InputFilter', function ($helper) {
         $input = new Input('recipients');
         $input->setAllowEmpty(false);
         return $input;
     })->add($this->field('dewdrop_notification_frequency_id'))->add('fields')->setLabel('Which fields would you like to include in the notification emails?')->setEditable(true)->assignHelperCallback('EditControl.Control', function ($helper, View $view) use($componentFields) {
         $options = array();
         foreach ($componentFields->getVisibleFields() as $id => $field) {
             $options[$id] = $field->getLabel();
         }
         return $view->checkboxList('fields', $options, $this->getSelectedFields($view->getRequest()->getQuery('dewdrop_notification_subscription_id'), $options));
     })->assignHelperCallback('InputFilter', function ($helper) {
         $input = new Input('fields');
         $input->setAllowEmpty(false);
         return $input;
     });
     return $fields;
 }
示例#11
0
 public function testSavingWillTraverseLinkedFieldsToHookRowsTogether()
 {
     $db = Pimple::getResource('db');
     $db->query('DELETE FROM dewdrop_test_fruits');
     $animalModel = new RowEditorAnimalModel();
     $this->fields->add($animalModel->field('name'));
     $this->fields->add($animalModel->field('is_fierce'));
     $this->fields->add($animalModel->field('is_cute'));
     $this->rowEditor->linkByQueryString('dewdrop_test_animals', 'dewdrop_test_animal_id');
     $this->rowEditor->linkByField('dewdrop_test_fruits', $animalModel->field('favorite_fruit_id'));
     $this->rowEditor->link();
     $this->assertTrue($this->rowEditor->isValid(array('dewdrop_test_fruits:name' => 'Banana', 'dewdrop_test_fruits:level_of_deliciousness' => 8, 'dewdrop_test_animals:name' => 'Gorilla', 'dewdrop_test_animals:is_fierce' => 1, 'dewdrop_test_animals:is_cute' => 1)));
     $this->rowEditor->save();
     $this->assertEquals($db->fetchOne('SELECT MAX(dewdrop_test_fruit_id) FROM dewdrop_test_fruits'), $db->fetchOne('SELECT favorite_fruit_id FROM dewdrop_test_animals ORDER BY dewdrop_test_animal_id DESC LIMIT 1'));
 }
示例#12
0
 public function testCanInsertAFieldIntoAPositionImmediatelyAfterAnExistingField()
 {
     // Insert a field after the first field
     $this->assertInstanceOf('Dewdrop\\Fields\\FieldInterface', $this->fields->insertAfter('second', 'visible'));
     $this->assertCount(5, $this->fields);
     $this->assertTrue($this->fields->has('visible', $visiblePosition));
     $this->assertSame(0, $visiblePosition);
     $this->assertTrue($this->fields->has('second', $secondPosition));
     $this->assertSame(1, $secondPosition);
     $this->assertTrue($this->fields->has('sortable', $sortablePosition));
     $this->assertSame(2, $sortablePosition);
     $this->assertTrue($this->fields->has('editable', $editablePosition));
     $this->assertSame(3, $editablePosition);
     $this->assertTrue($this->fields->has('filterable', $filterablePosition));
     $this->assertSame(4, $filterablePosition);
     // Insert a field after some field in the middle
     $this->assertInstanceOf('Dewdrop\\Fields\\FieldInterface', $this->fields->insertAfter('fourth', 'sortable'));
     $this->assertCount(6, $this->fields);
     $this->assertTrue($this->fields->has('visible', $visiblePosition));
     $this->assertSame(0, $visiblePosition);
     $this->assertTrue($this->fields->has('second', $secondPosition));
     $this->assertSame(1, $secondPosition);
     $this->assertTrue($this->fields->has('sortable', $sortablePosition));
     $this->assertSame(2, $sortablePosition);
     $this->assertTrue($this->fields->has('fourth', $sortablePosition));
     $this->assertSame(3, $sortablePosition);
     $this->assertTrue($this->fields->has('editable', $editablePosition));
     $this->assertSame(4, $editablePosition);
     $this->assertTrue($this->fields->has('filterable', $filterablePosition));
     $this->assertSame(5, $filterablePosition);
     // Insert a field after the last field
     $this->assertInstanceOf('Dewdrop\\Fields\\FieldInterface', $this->fields->insertAfter('last', 'filterable'));
     $this->assertCount(7, $this->fields);
     $this->assertTrue($this->fields->has('visible', $visiblePosition));
     $this->assertSame(0, $visiblePosition);
     $this->assertTrue($this->fields->has('second', $secondPosition));
     $this->assertSame(1, $secondPosition);
     $this->assertTrue($this->fields->has('sortable', $sortablePosition));
     $this->assertSame(2, $sortablePosition);
     $this->assertTrue($this->fields->has('fourth', $sortablePosition));
     $this->assertSame(3, $sortablePosition);
     $this->assertTrue($this->fields->has('editable', $editablePosition));
     $this->assertSame(4, $editablePosition);
     $this->assertTrue($this->fields->has('filterable', $filterablePosition));
     $this->assertSame(5, $filterablePosition);
     $this->assertTrue($this->fields->has('last', $filterablePosition));
     $this->assertSame(6, $filterablePosition);
 }
示例#13
0
 public function renderAjaxResponse()
 {
     if (!$this->request->isPost() && !$this->request->isGet()) {
         return ['result' => 'error', 'message' => 'AJAX edit requests must be POST or GET'];
     } elseif ($this->request->isPost() && !$this->invalidSubmission) {
         return ['result' => 'success', 'id' => $this->component->getListing()->getPrimaryKey()->getValue(), 'data' => $this->getData()];
     } elseif ($this->request->isGet()) {
         return $this->renderAjaxForm();
     } else {
         $messages = [];
         foreach ($this->fields->getEditableFields() as $field) {
             $messages[$field->getHtmlId()] = $this->rowEditor->getMessages($field);
         }
         return ['result' => 'invalid', 'messages' => $messages];
     }
 }
示例#14
0
 public function renderFieldsInTableRow(Fields $fields, InputFilter $inputFilter, Renderer $renderer)
 {
     $output = '<tr>';
     foreach ($fields->getEditableFields() as $field) {
         $output .= '<td>';
         $output .= $this->renderFieldContent($field, $inputFilter, $renderer, 100, false);
         $output .= '</td>';
     }
     $output .= '</tr>';
     return $output;
 }
示例#15
0
 /**
  * Build a Fields object that can be used when rendering the grouped counts.  Supplies a field
  * to render the actual HTML value from the original Fields object used when fetching data for
  * the listing and a field to render the count for that value.  We don't do any escaping here
  * because we assume that the original Fields object handled that in its rendering code.  The
  * fields object returned from this method supports both TableCell and CsvCell rendering.
  *
  * @return Fields
  */
 public function buildRenderFields()
 {
     $fields = new Fields();
     $fields->add('content')->setLabel($this->groupField->getLabel())->setVisible(true)->assignHelperCallback('TableCell.Content', function (TableCell\Content $helper, array $rowData) {
         // Not escaping here because we assume it was escaped by the original renderer in fetchData()
         return $rowData['content'];
     })->assignHelperCallback('CsvCell.Content', function (CsvCell\Content $helper, array $rowData) {
         return $rowData['content'];
     })->add('count')->setLabel('Count')->setVisible(true)->assignHelperCallback('TableCell.Content', function (TableCell\Content $helper, array $rowData) {
         return $helper->getView()->escapeHtml($rowData['count']);
     })->assignHelperCallback('CsvCell.Content', function (CsvCell\Content $helper, array $rowData) {
         return $rowData['count'];
     });
     return $fields;
 }
示例#16
0
 /**
  * Apply the filter to the supplied set of fields.  In return, you'll end
  * up with a \Dewdrop\Fields\GroupedFields object that reflects the sort
  * order and grouping preferences contained in this filter.  You can use
  * that object either as a normal \Dewdrop\Fields object or you can call
  * getGroups() on it to get the fields back in their assigned groups.
  *
  * @param Fields $currentFields
  * @return GroupedFields
  */
 public function apply(Fields $currentFields)
 {
     if (!$this->loadedDbData) {
         $this->loadedDbData = $this->load();
     }
     $groupedFields = new GroupedFields([], $currentFields->getUser());
     foreach ($this->loadedDbData as $fieldConfig) {
         // Ungrouped fields come after grouped fields
         if (!$fieldConfig['group_id']) {
             continue;
         }
         $fieldId = $fieldConfig['field_id'];
         $groupId = $fieldConfig['group_id'];
         if ($currentFields->has($fieldId)) {
             if (!$groupedFields->hasGroup($groupId)) {
                 $group = $groupedFields->addGroup($groupId);
                 $group->setTitle($fieldConfig['group_title']);
             }
             $groupedFields->getGroup($groupId)->add($currentFields->get($fieldId));
         }
     }
     $ungrouped = $groupedFields->addGroup('ungrouped');
     $ungrouped->setTitle('Other');
     foreach ($this->loadedDbData as $fieldConfig) {
         if (!$fieldConfig['group_id']) {
             $id = $fieldConfig['field_id'];
             if ($currentFields->has($id)) {
                 $ungrouped->add($currentFields->get($id));
             }
         }
     }
     foreach ($currentFields as $field) {
         if (!$groupedFields->has($field->getId())) {
             $ungrouped->add($field);
         }
     }
     return $groupedFields;
 }
 /**
  * Provide a Fields object with all the available fields the user can select
  * from along with a Fields object containing only those that have been
  * selected to be displayed already.  The helper will use these two Fields
  * objects to render a modal that enables a user to check/un-check columns
  * they'd like to have displayed.
  *
  * @param Fields $available
  * @param Fields $visible
  * @param string $actionUrl
  * @param boolean $filterByUser
  * @param string $id
  * @return string
  */
 public function direct(Fields $available, Fields $visible, $actionUrl, $filterByUser = false, $id = null)
 {
     return $this->partial('bootstrap-columns-modal.phtml', array('id' => $id ?: 'adjust-columns-modal', 'actionUrl' => $actionUrl, 'visible' => $visible->getVisibleFields(), 'available' => $available->getVisibleFields(), 'filterByUser' => $filterByUser));
 }
示例#18
0
 private function getFields()
 {
     $fields = new Fields();
     $fields->add($this->model->field('name'))->add($this->model->field('is_delicious'));
     return $fields;
 }
示例#19
0
 /**
  * Apply the field to the supplied set of fields.  If after filtering no
  * fields are left, we'll return the full set of fields as a fallback.
  * If no preferences are saved to the DB, we will use either pre-defined
  * defaults (@see setDefaults()) or the first 4 fields.
  *
  * @param Fields $fields
  * @return Fields
  */
 public function apply(Fields $fields)
 {
     if (!$this->selections) {
         $this->selections = $this->load();
     }
     if (0 !== count($this->defaultFields)) {
         $defaults = $this->defaultFields;
     } else {
         $defaults = [];
         foreach ($fields as $field) {
             if (4 > count($defaults)) {
                 $defaults[] = $field->getId();
             }
         }
     }
     $output = new Fields([], $fields->getUser());
     foreach ($fields as $field) {
         if (in_array($field->getId(), $this->selections) || !count($this->selections) && in_array($field->getId(), $defaults)) {
             $output[] = $field;
         }
     }
     // If there are no fields found in the output, return the original fields array as a fallback
     if (!count($output)) {
         return $fields;
     }
     return $output;
 }
示例#20
0
 private function decorateFields(Fields $fields, EditControl $renderer)
 {
     $control = $renderer->getControlRenderer();
     /* @var $field FieldInterface */
     foreach ($fields->getEditableFields() as $field) {
         $callback = $control->getFieldAssignment($field);
         $control->assign($field, function () use($callback, $control, $field) {
             return $control->getView()->importEditControl($field, $this->importFile, $this->request, $callback($control, $control->getView()));
         });
     }
     return $fields;
 }
示例#21
0
 /**
  * Given the supplied $fields and \Dewdrop\Request object, find the field
  * referenced in the query string and apply its sort callback to the query.
  *
  * @param Fields $fields
  * @param Select $select
  * @throws \Dewdrop\Fields\Exception
  * @return Select
  */
 public function modifySelect(Fields $fields, Select $select)
 {
     $this->sortedField = null;
     $this->sortedDirection = null;
     /* @var $field FieldInterface */
     foreach ($fields->getSortableFields() as $field) {
         if ($field->getQueryStringId() === urlencode($this->request->getQuery($this->prefix . 'sort'))) {
             if ('ASC' === $this->defaultDirection) {
                 $dir = 'DESC' === strtoupper($this->request->getQuery($this->prefix . 'dir')) ? 'DESC' : 'ASC';
             } else {
                 $dir = 'ASC' === strtoupper($this->request->getQuery($this->prefix . 'dir')) ? 'ASC' : 'DESC';
             }
             $select = call_user_func($this->getFieldAssignment($field), $select, $dir);
             if (!$select instanceof Select) {
                 throw new Exception('Your SelectSort callback must return the modified Select object.');
             }
             $this->sortedField = $field;
             $this->sortedDirection = $dir;
             return $select;
         }
     }
     // Sort by the first visible field that is also sortable, if no other sort was performed
     foreach ($fields->getVisibleFields() as $field) {
         if ($field->isSortable() && (null === $this->defaultField || $this->defaultField === $field)) {
             $this->sortedField = $field;
             $this->sortedDirection = $this->defaultDirection;
             return call_user_func($this->getFieldAssignment($field), $select, $this->defaultDirection);
         }
     }
     return $select;
 }
 /**
  * Render the filter controls inside a form tag that is rendered elsewhere.
  * No form HTML tags or buttons will be rendered by this method, only the
  * filter controls themselves.
  *
  * By default, this form will use GET so that it geneates a query string
  * that can be used to share search results, but you can use POST if needed
  * for your case.
  *
  * @param Fields $fields
  * @param SelectFilter $selectFilter
  * @param string $title
  * @param string $method
  * @param boolean $buttons
  * @return string
  */
 public function inline(Fields $fields, SelectFilter $selectFilter, $title, $method = 'GET', $buttons = false)
 {
     $this->view->headScript()->appendFile($this->view->bowerUrl('/dewdrop/www/js/filter/main.js'));
     $this->view->headLink()->appendStylesheet($this->view->bowerUrl('/dewdrop/www/css/filter.css'));
     return $this->partial('bootstrap-filter-controls.phtml', array('fields' => $fields->getFilterableFields(), 'typeHelper' => $selectFilter->getFilterTypeHelper(), 'values' => $selectFilter->getSelectModifier()->getCurrentFilters(), 'defaultVars' => $selectFilter->getDefaultVarsHelper(), 'title' => $title, 'method' => $method, 'paramPrefix' => $selectFilter->getPrefix(), 'showButtons' => $buttons));
 }
示例#23
0
 /**
  * Get any fields that return true when the supplied method is called  and pass
  * the supplied filters.  Note that you can either pass a single
  * \Dewdrop\Fields\Filter or an array of them.
  *
  * @param string $fieldMethodName
  * @param mixed $filters
  * @return Fields
  */
 protected function getFieldsPassingMethodCheck($fieldMethodName, $filters)
 {
     $fields = new Fields([], $this->user);
     foreach ($this->fields as $field) {
         if ($field->{$fieldMethodName}($this->user)) {
             $fields->add($field);
         }
     }
     return $this->applyFilters($fields, $filters);
 }
示例#24
0
 public function testAddingFieldToAdditionalFieldsetDoesNotOverrideOriginalSet()
 {
     $orig = new Fields();
     $orig->add($this->field);
     $second = new Fields();
     $second->add($this->field);
     $this->field->add('test');
     $this->assertTrue($orig->has('test'));
     $this->assertEquals(2, count($orig));
 }
示例#25
0
 /**
  * Get a model from the fields object by its model name.
  *
  * @throws Exception
  * @param string $modelName
  * @return \Dewdrop\Db\Table
  */
 public function getModel($modelName)
 {
     $models = $this->fields->getModelsByName();
     if (!isset($models[$modelName])) {
         throw new Exception("Could not find model with name '{$modelName}'");
     }
     return $models[$modelName];
 }
示例#26
0
 /**
  * Add password and confirmation fields to the given fields collection
  *
  * @param Fields $fields
  * @return Component
  */
 protected function addPasswordFields(Fields $fields)
 {
     static $passwordFieldName = 'password', $confirmFieldName = 'confirm_password', $minimumLength = 6;
     $request = $this->getRequest();
     $fields->add($passwordFieldName)->assignHelperCallback('EditControl.Control', function ($helper, View $view) use($passwordFieldName, $request) {
         return $view->inputText(['name' => $passwordFieldName, 'type' => 'password', 'value' => $request->getPost($passwordFieldName)]);
     })->assignHelperCallback('InputFilter', function () use($passwordFieldName, $minimumLength) {
         $input = new Input($passwordFieldName);
         $input->setRequired(true)->getValidatorChain()->attach(new StringLength(['min' => $minimumLength]));
         return $input;
     })->setLabel('Password')->setEditable(true);
     $this->fields->add('confirm_password')->assignHelperCallback('EditControl.Control', function ($helper, View $view) use($confirmFieldName, $request) {
         return $view->inputText(['name' => $confirmFieldName, 'type' => 'password', 'value' => $request->getPost($confirmFieldName)]);
     })->assignHelperCallback('InputFilter', function () use($request, $passwordFieldName, $confirmFieldName) {
         $input = new Input($confirmFieldName);
         $passwordsMatchValidator = new Callback(['callback' => function ($value) use($request, $passwordFieldName) {
             return $value === $request->getPost($passwordFieldName);
         }]);
         $passwordsMatchValidator->setMessage('Passwords do not match.');
         $input->setRequired(true)->getValidatorChain()->attach($passwordsMatchValidator);
         return $input;
     })->setLabel('Confirm Password')->setEditable(true);
     return $this;
 }
示例#27
0
 /**
  * Provide the Fields and Listing objects that will be tested.
  *
  * @param Fields $fields
  * @param Listing $listing
  * @throws Exception
  */
 public function __construct(Fields $fields, Listing $listing)
 {
     $this->fields = $fields->getSortableFields();
     $this->listing = $listing;
     $this->testSortHelperPresence();
 }
 /**
  * Render the supplied non-grouped fields in a Bootstrap table.
  *
  * @param Fields $fields
  * @param array $data
  * @param Renderer $renderer
  * @return string
  */
 protected function renderFields(Fields $fields, array $data, Renderer $renderer)
 {
     $rowIndex = 0;
     $out = '<div class="table-responsive">';
     $out .= '<table class="table">';
     foreach ($fields->getVisibleFields() as $field) {
         $out .= '<tr>';
         $content = $renderer->getContentRenderer()->render($field, $data, $rowIndex, 1);
         $classNames = $renderer->getTdClassNamesRenderer()->render($field, $data, $rowIndex, 1);
         $header = $renderer->getHeaderRenderer()->render($field);
         if (false !== stripos($content, '<table ')) {
             $out .= $this->renderTableContentInPanel($header, $classNames, $content);
         } else {
             $out .= $this->renderHeaderAndContent($header, $classNames, $content);
         }
         $out .= '</tr>';
         $rowIndex += 1;
     }
     $out .= '</table>';
     $out .= '</div>';
     return $out;
 }