\n";
-
- /**
- * Collection of data rendered by renderCallback()
- * @var array
- */
- protected $_renderCallbackData;
-
- /**
- * Mock for row()
- *
- * This is a simplified row renderer. Columns are separated by "|". Rows are
- * terminated by "\n". Headers are converted uppercase.
- *
- * @param string[] $columns
- * @param bool $isHeader
- * @return string
- */
- public function mockRow(array $columns, $isHeader)
- {
- if ($isHeader) {
- $columns = array_map('strtoupper', $columns);
- }
- return implode('|', $columns) . "\n";
- }
-
- /**
- * Sample callback for cell rendering
- *
- * Cell data is returned unchanged, but also appended to
- * $_renderCallbackData which can be evaluated after the table is rendered.
- *
- * @param \Zend\View\Renderer\RendererInterface $view
- * @param array $rowData
- * @param string $key
- * @return mixed
- */
- public function renderCallback(\Zend\View\Renderer\RendererInterface $view, array $rowData, $key)
- {
- $this->_renderCallbackData[] = $rowData[$key];
- return $rowData[$key];
- }
-
public function setUp()
{
$this->_escapeHtml = $this->createMock('Zend\View\Helper\EscapeHtml');
@@ -142,198 +96,210 @@ public function testInvokeNoData()
$this->assertEquals('', $table(array(), $this->_headers));
}
- public function testInvokeBasic()
+ public function testInvokeWithoutSortableHeadersDefaultParams()
{
- $this->_escapeHtml->expects($this->exactly(4)) // once per non-header cell
- ->method('__invoke')
- ->will($this->returnArgument(0));
$table = $this->getMockBuilder(static::_getHelperClass())
->setConstructorArgs(
- array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat)
+ [$this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat]
)
- ->setMethods(array('sortableHeader', 'row'))
+ ->setMethods(['sortableHeader', 'row', 'dataRows', 'tag'])
->getMock();
- $table->expects($this->never()) // No sortable headers in this test
- ->method('sortableHeader');
- $table->expects($this->exactly(3))
- ->method('row')
- ->will($this->returnCallback(array($this, 'mockRow')));
- $this->assertEquals($this->_expected, $table($this->_data, $this->_headers));
+ $table->expects($this->never())->method('sortableHeader');
+ $table->method('row')->with($this->_headers, true, [])->willReturn('');
+ $table->method('dataRows')->with($this->_data, ['column1', 'column2'], [], [], null)->willReturn('');
+ $table->method('tag')->with('')->willReturn('tableTag');
+
+ $this->assertEquals('tableTag', $table($this->_data, $this->_headers));
}
- public function testInvokeWithSortablHeaders()
+ public function testInvokeWithoutSortableHeadersExplicitParams()
{
- // The row() invocations are tested explicitly because the passed keys are significant.
- $this->_escapeHtml->expects($this->exactly(4)) // once per non-header cell
- ->method('__invoke')
- ->will($this->returnArgument(0));
$table = $this->getMockBuilder(static::_getHelperClass())
->setConstructorArgs(
- array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat)
+ [$this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat]
)
- ->setMethods(array('sortableHeader', 'row'))
+ ->setMethods(['sortableHeader', 'row', 'dataRows', 'tag'])
->getMock();
- $table->expects($this->exactly(2)) // once per column
- ->method('sortableHeader')
- ->will($this->returnArgument(0));
- $table->expects($this->at(2))
- ->method('row')
- ->with($this->_headers, true)
- ->will($this->returnCallback(array($this, 'mockRow')));
- $table->expects($this->at(3))
- ->method('row')
- ->with(array('column1' => 'value1a', 'column2' => 'value2a'), false)
- ->will($this->returnCallback(array($this, 'mockRow')));
- $table->expects($this->at(4))
- ->method('row')
- ->with(array('column1' => 'value1b', 'column2' => 'value2b'), false)
- ->will($this->returnCallback(array($this, 'mockRow')));
+
+ $table->expects($this->never())->method('sortableHeader');
+ $table->method('row')->with($this->_headers, true, ['columnClasses'])->willReturn('');
+ $table->method('dataRows')->with(
+ $this->_data,
+ ['column1', 'column2'],
+ ['renderCallbacks'],
+ ['columnClasses'],
+ ['rowClassCallback']
+ )->willReturn('');
+ $table->method('tag')->with('')->willReturn('tableTag');
$this->assertEquals(
- $this->_expected,
- $table(
- $this->_data,
- $this->_headers,
- array('order' => 'column1', 'direction' => 'asc')
- )
+ 'tableTag',
+ $table($this->_data, $this->_headers, [], ['renderCallbacks'], ['columnClasses'], ['rowClassCallback'])
);
}
- public function testInvokeWithRenderCallback()
+ public function testInvokeWithSortableHeaders()
{
- // Test with render callback on column2.
- $this->_escapeHtml->expects($this->exactly(2)) // once per non-header cell that is not rendered via callback
- ->method('__invoke')
- ->will($this->returnArgument(0));
$table = $this->getMockBuilder(static::_getHelperClass())
->setConstructorArgs(
- array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat)
+ [$this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat]
)
- ->setMethods(array('sortableHeader', 'row'))
+ ->setMethods(['sortableHeader', 'row', 'dataRows', 'tag'])
->getMock();
- $table->expects($this->never()) // No sortable headers in this test
- ->method('sortableHeader');
- $table->expects($this->exactly(3))
- ->method('row')
- ->with($this->anything(), $this->anything(), array())
- ->will($this->returnCallback(array($this, 'mockRow')));
- $table->setView($this->createMock('Zend\View\Renderer\PhpRenderer'));
-
- $this->_renderCallbackData = array();
+
+ $table->method('sortableHeader')->withConsecutive(
+ ['header1', 'column1', 'column2', 'desc'],
+ ['header2', 'column2', 'column2', 'desc']
+ )->willReturnOnConsecutiveCalls('sort1', 'sort2');
+ $table->method('row')->with(['column1' => 'sort1', 'column2' => 'sort2'], true, [])->willReturn('');
+ $table->method('dataRows')->with($this->_data, ['column1', 'column2'], [], [], null)->willReturn('');
+ $table->method('tag')->with('')->willReturn('tableTag');
+
$this->assertEquals(
- $this->_expected,
- $table(
- $this->_data,
- $this->_headers,
- array(),
- array('column2' => array($this, 'renderCallback'))
- )
+ 'tableTag',
+ $table($this->_data, $this->_headers, ['order' => 'column2', 'direction' => 'desc'])
);
- $this->assertEquals(array('value2a', 'value2b'), $this->_renderCallbackData);
}
- public function testInvokeWithColumnClasses()
+ public function testTag()
{
- // Test with column class set on column 2. The row() invocations are
- // tested explicitly because the passed keys are significant.
- $columnClasses = array('column2' => 'test');
- $this->_escapeHtml->expects($this->exactly(4)) // once per non-header cell
- ->method('__invoke')
- ->will($this->returnArgument(0));
+ $this->_htmlElement->method('__invoke')
+ ->with('table', 'table_content', ['class' => 'alternating'])
+ ->willReturn('table_tag');
+
+ $class = static::_getHelperClass();
+ $table = new $class($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat);
+
+ $this->assertEquals('table_tag', $table->tag('table_content'));
+ }
+
+ public function testDataRowsWithDefaultParams()
+ {
+ $this->_escapeHtml->method('__invoke')->willReturnOnConsecutiveCalls('1a', '2a', '1b', '2b');
+
$table = $this->getMockBuilder(static::_getHelperClass())
->setConstructorArgs(
array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat)
)
- ->setMethods(array('sortableHeader', 'row'))
+ ->setMethods(['row'])
->getMock();
- $table->expects($this->at(0))
- ->method('row')
- ->with($this->_headers, true, $columnClasses)
- ->will($this->returnCallback(array($this, 'mockRow')));
- $table->expects($this->at(1))
- ->method('row')
- ->with(array('column1' => 'value1a', 'column2' => 'value2a'), false, $columnClasses)
- ->will($this->returnCallback(array($this, 'mockRow')));
- $table->expects($this->at(2))
- ->method('row')
- ->with(array('column1' => 'value1b', 'column2' => 'value2b'), false, $columnClasses)
- ->will($this->returnCallback(array($this, 'mockRow')));
-
- $this->assertEquals($this->_expected, $table($this->_data, $this->_headers, array(), array(), $columnClasses));
+
+ $table->method('row')
+ ->withConsecutive(
+ [['column1' => '1a', 'column2' => '2a'], false, [], null],
+ [['column1' => '1b', 'column2' => '2b'], false, [], null]
+ )
+ ->willReturnOnConsecutiveCalls('', '');
+
+ $this->assertEquals('', $table->dataRows($this->_data, ['column1', 'column2']));
}
- public function testInvokeWithRowClassCallback()
+ public function testDataRowsWithColumnClasses()
{
- $rowClassCallback = function ($columns) {
- static $counter = 0;
- if ($counter++) {
- return "$columns[column1]+$columns[column2]";
- } else {
- return '';
- }
- };
- $this->_escapeHtml->expects($this->exactly(4)) // once per non-header cell that is not rendered via callback
- ->method('__invoke')
- ->will($this->returnArgument(0));
+ $this->_escapeHtml->method('__invoke')->willReturnOnConsecutiveCalls('1a', '2a', '1b', '2b');
+
$table = $this->getMockBuilder(static::_getHelperClass())
->setConstructorArgs(
array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat)
)
- ->setMethods(array('sortableHeader', 'row'))
+ ->setMethods(['row'])
->getMock();
- $table->expects($this->never()) // No sortable headers in this test
- ->method('sortableHeader');
- $table->expects($this->at(0))
- ->method('row')
- ->with($this->_headers, true, array(), null)
- ->will($this->returnCallback(array($this, 'mockRow')));
- $table->expects($this->at(1))
- ->method('row')
- ->with(array('column1' => 'value1a', 'column2' => 'value2a'), false, array(), '')
- ->will($this->returnCallback(array($this, 'mockRow')));
- $table->expects($this->at(2))
- ->method('row')
- ->with(array('column1' => 'value1b', 'column2' => 'value2b'), false, array(), 'value1b+value2b')
- ->will($this->returnCallback(array($this, 'mockRow')));
+
+ $table->method('row')
+ ->withConsecutive(
+ [['column1' => '1a', 'column2' => '2a'], false, ['column1' => 'class'], null],
+ [['column1' => '1b', 'column2' => '2b'], false, ['column1' => 'class'], null]
+ )
+ ->willReturnOnConsecutiveCalls('', '');
$this->assertEquals(
- $this->_expected,
- $table(
- $this->_data,
- $this->_headers,
- array(),
- array(),
- array(),
- $rowClassCallback
- )
+ '',
+ $table->dataRows($this->_data, ['column1', 'column2'], [], ['column1' => 'class'])
);
}
- public function testDateTimeFormat()
+ public function testDataRowsWithRowClassCallback()
{
- $date = new \DateTime;
- $data = array(
- array(1388567012, $date),
- array($date, $date),
- array($date, 1388567012),
- );
- $this->_dateFormat->expects($this->exactly(2)) // column 0 should be rendered by callback
- ->method('__invoke')
- ->with($date, \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT);
- $callback = function () {
+ $this->_escapeHtml->method('__invoke')->willReturnOnConsecutiveCalls('1a', '2a', '1b', '2b');
+
+ $table = $this->getMockBuilder(static::_getHelperClass())
+ ->setConstructorArgs(
+ array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat)
+ )
+ ->setMethods(['row'])
+ ->getMock();
+
+ $table->method('row')
+ ->withConsecutive(
+ [['column1' => '1a', 'column2' => '2a'], false, [], 'VALUE1A'],
+ [['column1' => '1b', 'column2' => '2b'], false, [], 'VALUE1B']
+ )
+ ->willReturnOnConsecutiveCalls('', '');
+
+ $rowClassCallback = function ($rowData) {
+ $this->assertContains($rowData, $this->_data);
+ return strtoupper($rowData['column1']);
};
- $helper = new \Console\View\Helper\Table(
- $this->_escapeHtml,
- $this->_htmlElement,
- $this->_consoleUrl,
- $this->_dateFormat
- );
- $helper(
- $data,
- array('col1', 'col2'),
- array(),
- array(0 => $callback)
+
+ $this->assertEquals('', $table->dataRows($this->_data, ['column1', 'column2'], [], [], $rowClassCallback));
+ }
+
+ public function testDataRowsWithDateTime()
+ {
+ $date = $this->createMock('DateTime');
+
+ $this->_dateFormat->method('__invoke')
+ ->with($date, \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT)
+ ->willReturn('date_formatted');
+
+ $this->_escapeHtml->method('__invoke')->with('date_formatted')->willReturn('escaped_date');
+
+ $table = $this->getMockBuilder(static::_getHelperClass())
+ ->setConstructorArgs(
+ array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat)
+ )
+ ->setMethods(['row'])
+ ->getMock();
+
+ $table->method('row')
+ ->with(['column1' => 'escaped_date'], false, [], null)
+ ->willReturn('');
+
+ $this->assertEquals('', $table->dataRows([['column1' => $date]], ['column1']));
+ }
+
+ public function testDataRowsWithRenderCallbackPrecedesDateTime()
+ {
+ $view = $this->createMock('Zend\View\Renderer\PhpRenderer');
+ $date = $this->createMock('DateTime');
+
+ $this->_dateFormat->expects($this->never())->method('__invoke');
+ $this->_escapeHtml->expects($this->never())->method('__invoke');
+
+ $table = $this->getMockBuilder(static::_getHelperClass())
+ ->setConstructorArgs(
+ array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat)
+ )
+ ->setMethods(['row', 'getView'])
+ ->getMock();
+
+ $table->method('row')
+ ->with(['column1' => 'callback_return'], false, [], null)
+ ->willReturn('');
+
+ $table->method('getView')->willReturn($view);
+
+ $renderCallback = function ($view2, $rowData, $key) use ($view, $date) {
+ $this->assertSame($view2, $view);
+ $this->assertEquals(['column1' => $date], $rowData);
+ $this->assertEquals('column1', $key);
+ return 'callback_return';
+ };
+
+ $this->assertEquals(
+ '',
+ $table->dataRows([['column1' => $date]], ['column1'], ['column1' => $renderCallback])
);
}
diff --git a/module/Console/View/Helper/Form/Software.php b/module/Console/View/Helper/Form/Software.php
index 73cd1da3..c3ed61bc 100644
--- a/module/Console/View/Helper/Form/Software.php
+++ b/module/Console/View/Helper/Form/Software.php
@@ -84,53 +84,60 @@ public function renderSoftwareFieldset($fieldset, $software, $sorting)
{
$view = $this->getView();
+ $consoleUrl = $view->plugin('consoleUrl');
$formRow = $view->plugin('formRow');
- $translate = $view->plugin('translate');
+ $htmlElement = $view->plugin('htmlElement');
$table = $view->plugin('table');
+ $translate = $view->plugin('translate');
// Checkbox labels are software names and must not be translated
$translatorEnabled = $formRow->isTranslatorEnabled();
$formRow->setTranslatorEnabled(false);
- $output = $table(
- $software,
+ $output = $table->row(
[
- 'name' => $translate('Name'),
- 'num_clients' => $translate('Count'),
+ 'name' => '' . $table->sortableHeader(
+ $translate('Name'),
+ 'name',
+ $sorting['order'],
+ $sorting['direction']
+ ),
+ 'num_clients' => $table->sortableHeader(
+ $translate('Count'),
+ 'num_clients',
+ $sorting['order'],
+ $sorting['direction']
+ ),
],
- $sorting,
- [
- 'name' => function ($view, $software) use ($fieldset, $formRow) {
- $element = $fieldset->get(base64_encode($software['name']));
- return $formRow($element, \Zend\Form\View\Helper\FormRow::LABEL_APPEND);
- },
- 'num_clients' => function ($view, $software) {
- $htmlElement = $view->plugin('htmlElement');
- $consoleUrl = $view->plugin('consoleUrl');
-
- return $htmlElement(
+ true
+ );
+ foreach ($software as $row) {
+ $element = $fieldset->get(base64_encode($row['name']));
+ $output .= $table->row(
+ [
+ 'name' => $formRow($element, \Zend\Form\View\Helper\FormRow::LABEL_APPEND),
+ 'num_clients' => $htmlElement(
'a',
- $software['num_clients'],
- array(
- 'href' => $consoleUrl(
- 'client',
- 'index',
- array(
- 'columns' => 'Name,UserName,LastContactDate,InventoryDate,Software.Version',
- 'jumpto' => 'software',
- 'filter' => 'Software',
- 'search' => $software['name'],
- )
- ),
- ),
+ $row['num_clients'],
+ ['href' => $consoleUrl(
+ 'client',
+ 'index',
+ [
+ 'columns' => 'Name,UserName,LastContactDate,InventoryDate,Software.Version',
+ 'jumpto' => 'software',
+ 'filter' => 'Software',
+ 'search' => $row['name'],
+ ]
+ )],
true
- );
- }
- ],
- ['num_clients' => 'textright']
- );
+ ),
+ ],
+ false,
+ ['num_clients' => 'textright']
+ );
+ }
$formRow->setTranslatorEnabled($translatorEnabled);
- return $output;
+ return $table->tag($output);
}
}
diff --git a/module/Console/View/Helper/Table.php b/module/Console/View/Helper/Table.php
index a9194377..50cbcf65 100644
--- a/module/Console/View/Helper/Table.php
+++ b/module/Console/View/Helper/Table.php
@@ -77,48 +77,16 @@ public function __construct(
* match corresponding fields in the other arguments. For each header, a
* corresponding field must be set in the table data or in $renderCallbacks.
*
- * $data is an array of row objects. Row objects are typically associative
- * arrays or objects implementing the \ArrayAccess interface. A default
- * rendering method is available for these types. For any other type, all
- * columns must be rendered by a callback. If no rows are present, an
- * empty string is returned.
- *
- * By default, cell data is retrieved from $data and escaped automatically.
- * \DateTime objects are rendered as short timestamps (yy-mm-dd hh:mm). The
- * application's default locale controls the date/time format.
- * Alternatively, a callback can be provided in the $renderCallbacks array.
- * If a callback is defined for a column, the callback is responsible for
- * escaping cell data. It gets called with the following arguments:
- *
- * 1. The view renderer
- * 2. The row object
- * 3. The key of the column to be rendered. This is useful for callbacks
- * that render more than 1 column.
- *
- * The optional $columnClasses array may contain values for a "class"
- * attribute which gets applied to all cells of a specified column. The
- * $columnClasses keys are matched against the keys of each row.
- *
- * $rowClassCallback, if given, is called for every non-header row. It
- * receives the unprocessed column data for each row and delivers a string
- * that is set as the row's class attribute if it is not empty.
- *
* If the optional $sorting array contains the "order" and "direction"
- * elements (other elements are ignored), headers are generated as
- * hyperlinks with "order" and "direction" parameters set to the
- * corresponding column. The values denote the sorting in effect for the
- * current request - the header will be marked with an arrow indicating the
- * current sorting. The controller action should evaluate these parameters,
- * sort the data and provide the sorting to the view renderer. The
- * \Console\Mvc\Controller\Plugin\GetOrder controller plugin simplifies
- * these tasks.
+ * elements (other elements are ignored), headers are generated via
+ * sortableHeader().
*
- * @param array|\Traversable $data
- * @param array $headers
- * @param array $sorting
- * @param array $renderCallbacks
- * @param string[] $columnClasses Optional class attributes to apply to columns (keys are matched against $row)
- * @param callable $rowClassCallback Optional callback to provide row class attributes
+ * @param array|\Traversable $data see dataRows()
+ * @param string[] $headers
+ * @param string[] $sorting
+ * @param callable[] $renderCallbacks see dataRows()
+ * @param string[] $columnClasses see row()
+ * @param callable $rowClassCallback see dataRows()
* @return string HTML table
*/
public function __invoke(
@@ -133,26 +101,68 @@ public function __invoke(
return '';
}
- $table = "
\n";
-
// Generate header row
if (isset($sorting['order']) and isset($sorting['direction'])) {
- $row = array();
- foreach ($headers as $key => $label) {
- $row[$key] = $this->sortableHeader($label, $key, $sorting['order'], $sorting['direction']);
+ foreach ($headers as $key => &$label) {
+ $label = $this->sortableHeader($label, $key, $sorting['order'], $sorting['direction']);
}
- $table .= $this->row($row, true, $columnClasses);
- } else {
- $table .= $this->row($headers, true, $columnClasses);
}
+ $content = $this->row($headers, true, $columnClasses);
+
+ $content .= $this->dataRows($data, array_keys($headers), $renderCallbacks, $columnClasses, $rowClassCallback);
- // Generate data rows
- $keys = array_keys($headers);
+ return $this->tag($content);
+ }
+
+ /**
+ * Wrap given content in "table" tag
+ *
+ * @param string $content
+ * @return string
+ */
+ public function tag($content)
+ {
+ return $this->_htmlElement->__invoke('table', $content, ['class' => 'alternating']);
+ }
+
+ /**
+ * Generate table rows
+ *
+ * $data is an array or iterator of row objects. Row objects are typically
+ * associative arrays or objects implementing the \ArrayAccess interface. A
+ * default rendering method is available for these types. For any other
+ * type, all columns must be rendered by a callback.
+ *
+ * The default renderer escapes cell content automatically. \DateTime
+ * objects are rendered as short timestamps (yy-mm-dd hh:mm). The
+ * application's default locale controls the date/time format.
+ * Alternatively, a callback can be provided in the $renderCallbacks array.
+ * If a callback is defined for a column, the callback is responsible for
+ * escaping cell data. It gets called with the following arguments:
+ *
+ * 1. The view renderer
+ * 2. The row object
+ * 3. The key of the column to be rendered. This is useful for callbacks
+ * that render more than 1 column.
+ *
+ * $rowClassCallback, if given, is called for each row. It receives the
+ * row object from $data. Its return value is passed to row().
+ *
+ * @param array|\Traversable $data
+ * @param string[] $keys Column keys
+ * @param callable[] $renderCallbacks
+ * @param string $columnClasses see row()
+ * @param callable $rowClassCallback
+ * @return string
+ */
+ public function dataRows($data, $keys, $renderCallbacks = [], $columnClasses = [], $rowClassCallback = null)
+ {
+ $rows = '';
foreach ($data as $rowData) {
$row = array();
foreach ($keys as $key) {
if (isset($renderCallbacks[$key])) {
- $row[$key] = $renderCallbacks[$key]($this->view, $rowData, $key);
+ $row[$key] = $renderCallbacks[$key]($this->getView(), $rowData, $key);
} elseif ($rowData[$key] instanceof \DateTime) {
$row[$key] = $this->_escapeHtml->__invoke(
$this->_dateFormat->__invoke(
@@ -165,21 +175,25 @@ public function __invoke(
$row[$key] = $this->_escapeHtml->__invoke($rowData[$key]);
}
}
- $table .= $this->row(
+ $rows .= $this->row(
$row,
false,
$columnClasses,
$rowClassCallback ? $rowClassCallback($rowData) : null
);
}
-
- $table .= "
\n";
- return $table;
+ return $rows;
}
/**
* Generate a header hyperlink
*
+ * The link URL points to the current action with the "order" and
+ * "direction" query parameters set accordingly. The action should evaluate
+ * these parameters, sort the data and provide the sorting to the view
+ * renderer. The \Console\Mvc\Controller\Plugin\GetOrder controller plugin
+ * simplifies these tasks.
+ *
* @param string $label Header text. An arrow will be added to the currently sorted column.
* @param string $key Sort key to be used in the URL
* @param string $order Current order
@@ -220,7 +234,7 @@ public function sortableHeader($label, $key, $order, $direction)
*
* @param array $columns Column data
* @param bool $isHeader Use "th" tag instead of "td". Default: false
- * @param string[] $columnClasses Optional class attributes to apply to cells (keys are matched against $row)
+ * @param string[] $columnClasses Optional class attributes to apply to cells (keys are matched against $columns)
* @param string $rowClass Optional class attribute for the row
* @return string HTML table row
*/
diff --git a/public/braintacle.js b/public/braintacle.js
index 6d068667..9a6f9488 100644
--- a/public/braintacle.js
+++ b/public/braintacle.js
@@ -24,4 +24,9 @@ $(window).on('load', function() {
});
}).change();
+
+ // Check/uncheck all checkboxes within the same form
+ $('.form_software .checkAll').change(function() {
+ $('input[type=checkbox][name]', this.form).prop('checked', this.checked);
+ });
});