From bb563c26cc5eb39df29a10ed300f82c1f7bbcc82 Mon Sep 17 00:00:00 2001 From: Craig Heydenburg Date: Tue, 21 Jan 2020 17:10:27 -0500 Subject: [PATCH] initial commit --- .gitignore | 2 + Column.php | 150 ++++++++++++++++++++++ README.md | 5 +- SortableColumns.php | 231 ++++++++++++++++++++++++++++++++++ Tests/ColumnTest.php | 127 +++++++++++++++++++ Tests/SortableColumnsTest.php | 222 ++++++++++++++++++++++++++++++++ composer.json | 32 +++++ phpunit.xml.dist | 28 +++++ 8 files changed, 796 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 Column.php create mode 100644 SortableColumns.php create mode 100644 Tests/ColumnTest.php create mode 100644 Tests/SortableColumnsTest.php create mode 100644 composer.json create mode 100644 phpunit.xml.dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8153b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/composer.lock +/vendor/ diff --git a/Column.php b/Column.php new file mode 100644 index 0000000..bf8adf9 --- /dev/null +++ b/Column.php @@ -0,0 +1,150 @@ +name = $name; + $this->currentSortDirection = !empty($currentSortDirection) ? $currentSortDirection : self::DIRECTION_ASCENDING; + $this->reverseSortDirection = $this->reverse($this->currentSortDirection); + $this->defaultSortDirection = !empty($defaultSortDirection) ? $defaultSortDirection : self::DIRECTION_ASCENDING; + $this->cssClassString = self::CSS_CLASS_UNSORTED; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getDefaultSortDirection(): string + { + return $this->defaultSortDirection; + } + + public function setDefaultSortDirection(string $defaultSortDirection): void + { + $this->defaultSortDirection = $defaultSortDirection; + } + + public function getCurrentSortDirection(): string + { + return $this->currentSortDirection; + } + + public function setCurrentSortDirection(string $currentSortDirection): void + { + $this->currentSortDirection = $currentSortDirection; + $this->setCssClassString($this->cssFromDirection($currentSortDirection)); + $this->reverseSortDirection = $this->reverse($currentSortDirection); + } + + public function getReverseSortDirection(): string + { + return $this->reverseSortDirection; + } + + public function setReverseSortDirection(string $reverseSortDirection): void + { + $this->reverseSortDirection = $reverseSortDirection; + } + + public function getCssClassString(): string + { + return $this->cssClassString; + } + + public function setCssClassString(string $cssClassString): void + { + $this->cssClassString = $cssClassString; + } + + public function isSortColumn(): bool + { + return $this->isSortColumn; + } + + public function setSortColumn(bool $isSortColumn): void + { + $this->isSortColumn = $isSortColumn; + } + + /** + * Reverse the direction constants. + */ + private function reverse(string $direction): string + { + return (self::DIRECTION_ASCENDING === $direction) ? self::DIRECTION_DESCENDING : self::DIRECTION_ASCENDING; + } + + /** + * Determine a css class based on the direction. + */ + private function cssFromDirection(string $direction): string + { + return (self::DIRECTION_ASCENDING === $direction) ? self::CSS_CLASS_ASCENDING : self::CSS_CLASS_DESCENDING; + } +} diff --git a/README.md b/README.md index bd620c4..4be7f66 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # SortableColumns -SortableColumns is a zikula component to help manage data table column headings that can be clicked to sort the data +SortableColumns is a Zikula component to help manage data table column headings that can be clicked to sort the data. +The collection is an `Doctrine\Common\Collections\ArrayCollection` of `Zikula\Component\SortableColumns\Column` objects. +Use `SortableColumns::generateSortableColumns` to create an array of attributes (url, css class) indexed by column name +which can be used in the generation of table headings/links. diff --git a/SortableColumns.php b/SortableColumns.php new file mode 100644 index 0000000..99617bc --- /dev/null +++ b/SortableColumns.php @@ -0,0 +1,231 @@ +router = $router; + $this->routeName = $routeName; + $this->sortFieldName = $sortFieldName; + $this->directionFieldName = $directionFieldName; + $this->columnCollection = new ArrayCollection(); + } + + /** + * Create an array of column definitions indexed by column name + * + * ['a' => + * ['url' => '/foo?sort-direction=ASC&sort-field=a', + * 'class' => 'z-order-unsorted' + * ], + * ] + * + */ + public function generateSortableColumns(): array + { + $resultArray = []; + /** @var Column $column */ + foreach ($this->columnCollection as $column) { + $this->additionalUrlParameters[$this->directionFieldName] = $column->isSortColumn() ? $column->getReverseSortDirection() : $column->getCurrentSortDirection(); + $this->additionalUrlParameters[$this->sortFieldName] = $column->getName(); + $resultArray[$column->getName()] = [ + 'url' => $this->router->generate($this->routeName, $this->additionalUrlParameters), + 'class' => $column->getCssClassString(), + ]; + } + + return $resultArray; + } + + /** + * Add one column. + */ + public function addColumn(Column $column): void + { + $this->columnCollection->set($column->getName(), $column); + } + + /** + * Shortcut to add an array of columns. + */ + public function addColumns(array $columns = []): void + { + foreach ($columns as $column) { + if ($column instanceof Column) { + $this->addColumn($column); + } else { + throw new InvalidArgumentException('Columns must be an instance of \Zikula\Component\SortableColumns\Column.'); + } + } + } + + public function removeColumn(string $name): void + { + $this->columnCollection->remove($name); + } + + public function getColumn(?string $name): ?Column + { + return $this->columnCollection->get($name); + } + + /** + * Set the column to sort by and the sort direction. + */ + public function setOrderBy(Column $sortColumn = null, string $sortDirection = null): void + { + $sortColumn = $sortColumn ?: $this->getDefaultColumn(); + if (null === $sortColumn) { + return; + } + $sortDirection = $sortDirection ?: Column::DIRECTION_ASCENDING; + $this->setSortDirection($sortDirection); + $this->setSortColumn($sortColumn); + } + + /** + * Shortcut to set OrderBy using the Request object. + */ + public function setOrderByFromRequest(Request $request): void + { + if (null === $this->getDefaultColumn()) { + return; + } + $sortColumnName = $request->get($this->sortFieldName, $this->getDefaultColumn()->getName()); + $sortDirection = $request->get($this->directionFieldName, Column::DIRECTION_ASCENDING); + $this->setOrderBy($this->getColumn($sortColumnName), $sortDirection); + } + + public function getSortColumn(): ?Column + { + return $this->sortColumn ?? $this->getDefaultColumn(); + } + + private function setSortColumn(Column $sortColumn): void + { + if ($this->columnCollection->contains($sortColumn)) { + $this->sortColumn = $sortColumn; + $sortColumn->setSortColumn(true); + $sortColumn->setCurrentSortDirection($this->getSortDirection()); + } + } + + public function getSortDirection(): string + { + return $this->sortDirection; + } + + private function setSortDirection(string $sortDirection): void + { + if (in_array($sortDirection, [Column::DIRECTION_ASCENDING, Column::DIRECTION_DESCENDING], true)) { + $this->sortDirection = $sortDirection; + } + } + + public function getDefaultColumn(): ?Column + { + if (!empty($this->defaultColumn)) { + return $this->defaultColumn; + } + + return $this->columnCollection->first(); + } + + public function setDefaultColumn(Column $defaultColumn): void + { + $this->defaultColumn = $defaultColumn; + } + + public function getAdditionalUrlParameters(): array + { + return $this->additionalUrlParameters; + } + + public function setAdditionalUrlParameters(array $additionalUrlParameters = []): void + { + $this->additionalUrlParameters = $additionalUrlParameters; + } +} diff --git a/Tests/ColumnTest.php b/Tests/ColumnTest.php new file mode 100644 index 0000000..88f19d6 --- /dev/null +++ b/Tests/ColumnTest.php @@ -0,0 +1,127 @@ +column = new Column('foo'); + } + + /** + * @covers Column::getName + */ + public function testGetName(): void + { + $this->assertEquals('foo', $this->column->getName()); + } + + /** + * @covers Column::setName + */ + public function testSetName(): void + { + $this->column->setName('bar'); + $this->assertEquals('bar', $this->column->getName()); + } + + /** + * @covers Column::getDefaultSortDirection + */ + public function testGetDefaultSortDirection(): void + { + $this->assertEquals(Column::DIRECTION_ASCENDING, $this->column->getDefaultSortDirection()); + } + + /** + * @covers Column::setDefaultSortDirection + */ + public function getSetDefaultSortDirection(): void + { + $this->column->setDefaultSortDirection(Column::DIRECTION_DESCENDING); + $this->assertEquals(Column::DIRECTION_DESCENDING, $this->column->getDefaultSortDirection()); + } + + /** + * @covers Column::getCurrentSortDirection + */ + public function testGetCurrentSortDirection(): void + { + $this->assertEquals(Column::DIRECTION_ASCENDING, $this->column->getCurrentSortDirection()); + } + + /** + * @covers Column::setCurrentSortDirection + */ + public function testSetCurrentSortDirection(): void + { + $this->column->setCurrentSortDirection(Column::DIRECTION_DESCENDING); + $this->assertEquals(Column::DIRECTION_DESCENDING, $this->column->getCurrentSortDirection()); + $this->assertEquals(Column::DIRECTION_ASCENDING, $this->column->getReverseSortDirection()); + $this->assertEquals(Column::CSS_CLASS_DESCENDING, $this->column->getCssClassString()); + } + + /** + * @covers Column::getReverseSortDirection + */ + public function testGetReverseSortDirection(): void + { + $this->assertEquals(Column::DIRECTION_DESCENDING, $this->column->getReverseSortDirection()); + } + + /** + * @covers Column::getCssClassString + */ + public function testGetCssClassString(): void + { + $this->assertEquals(Column::CSS_CLASS_UNSORTED, $this->column->getCssClassString()); + } + + /** + * @covers Column::setCssClassString + */ + public function testSetCssClassString(): void + { + $this->column->setCssClassString(Column::CSS_CLASS_ASCENDING); + $this->assertEquals(Column::CSS_CLASS_ASCENDING, $this->column->getCssClassString()); + $this->column->setCssClassString(Column::CSS_CLASS_DESCENDING); + $this->assertEquals(Column::CSS_CLASS_DESCENDING, $this->column->getCssClassString()); + } + + /** + * @covers Column::isSortColumn + */ + public function testIsSortColumn(): void + { + $this->assertFalse($this->column->isSortColumn()); + } + + /** + * @covers Column::setSortColumn + */ + public function testSetSortColumn(): void + { + $this->column->setSortColumn(true); + $this->assertTrue($this->column->isSortColumn()); + } +} diff --git a/Tests/SortableColumnsTest.php b/Tests/SortableColumnsTest.php new file mode 100644 index 0000000..837af5c --- /dev/null +++ b/Tests/SortableColumnsTest.php @@ -0,0 +1,222 @@ +getMockBuilder(RouterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $router + ->method('generate') + ->willReturnCallback(static function ($id, $params) { + return '/foo?' . http_build_query($params); + }); + + $this->sortableColumns = new SortableColumns($router, 'foo'); + $this->sortableColumns->addColumn(new Column('a')); + $this->sortableColumns->addColumn(new Column('b')); + $this->sortableColumns->addColumn(new Column('c')); + } + + /** + * @covers SortableColumns::getColumn + */ + public function testGetColumn(): void + { + $a = $this->sortableColumns->getColumn('a'); + $this->assertInstanceOf(Column::class, $a); + $this->assertEquals('a', $a->getName()); + } + + /** + * @covers SortableColumns::addColumn + */ + public function testAddColumn(): void + { + $d = new Column('d'); + $this->sortableColumns->addColumn($d); + $this->assertEquals($d, $this->sortableColumns->getColumn('d')); + } + + /** + * @covers SortableColumns::addColumns + */ + public function testAddColumns(): void + { + $e = new Column('e'); + $f = new Column('f'); + $g = new Column('g'); + $this->sortableColumns->addColumns([$e, $f, $g]); + $this->assertEquals($e, $this->sortableColumns->getColumn('e')); + $this->assertEquals($f, $this->sortableColumns->getColumn('f')); + $this->assertEquals($g, $this->sortableColumns->getColumn('g')); + } + + /** + * @covers SortableColumns::getDefaultColumn + */ + public function testGetDefaultColumn(): void + { + $a = $this->sortableColumns->getColumn('a'); + $b = $this->sortableColumns->getColumn('b'); + $this->assertEquals($a, $this->sortableColumns->getDefaultColumn()); + $this->assertNotEquals($b, $this->sortableColumns->getDefaultColumn()); + } + + /** + * @covers SortableColumns::removeColumn + */ + public function testRemoveColumn(): void + { + $this->sortableColumns->removeColumn('b'); + $this->assertNull($this->sortableColumns->getColumn('b')); + } + + /** + * @covers SortableColumns::getSortDirection + */ + public function testGetSortDirection(): void + { + $this->assertEquals(Column::DIRECTION_ASCENDING, $this->sortableColumns->getSortDirection()); + } + + /** + * @covers SortableColumns::getSortColumn + */ + public function testGetSortColumn(): void + { + $a = $this->sortableColumns->getColumn('a'); + $this->assertEquals($a, $this->sortableColumns->getSortColumn()); + } + + /** + * @covers SortableColumns::setOrderBy + */ + public function testSetOrderBy(): void + { + $c = $this->sortableColumns->getColumn('c'); + $this->sortableColumns->setOrderBy($c, Column::DIRECTION_DESCENDING); + $this->assertEquals(Column::DIRECTION_DESCENDING, $this->sortableColumns->getSortDirection()); + $this->assertEquals($c, $this->sortableColumns->getSortColumn()); + } + + /** + * @covers SortableColumns::setOrderByFromRequest + */ + public function testSetOrderByFromRequest(): void + { + $request = new Request([ + 'sort-field' => 'b', + 'sort-direction' => 'DESC' + ]); + $this->sortableColumns->setOrderByFromRequest($request); + $b = $this->sortableColumns->getColumn('b'); + $this->assertEquals($b, $this->sortableColumns->getSortColumn()); + $this->assertEquals(Column::DIRECTION_DESCENDING, $this->sortableColumns->getSortDirection()); + } + + /** + * @covers SortableColumns::setAdditionalUrlParameters + * @covers SortableColumns::getAdditionalUrlParameters + */ + public function testAdditionalUrlParameters(): void + { + $this->sortableColumns->setAdditionalUrlParameters(['x' => 1, 'z' => 0]); + $this->assertEquals(['x' => 1, 'z' => 0], $this->sortableColumns->getAdditionalUrlParameters()); + } + + /** + * @dataProvider columnDefProvider + * @covers SortableColumns::generateSortableColumns + */ + public function testGenerateSortableColumns(?string $col, string $dir, array $columnDef = []): void + { + $this->sortableColumns->setOrderBy($this->sortableColumns->getColumn($col), $dir); + $this->assertEquals($columnDef, $this->sortableColumns->generateSortableColumns()); + } + + /** + * @covers SortableColumns::generateSortableColumns + * @covers SortableColumns::setAdditionalUrlParameters + */ + public function testGenerateSortableColumnsWithAdditionalUrlParameters(): void + { + $expected = [ + 'a' => ['url' => '/foo?x=1&z=0&sort-direction=' . Column::DIRECTION_DESCENDING . '&sort-field=a', 'class' => Column::CSS_CLASS_ASCENDING], + 'b' => ['url' => '/foo?x=1&z=0&sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=b', 'class' => Column::CSS_CLASS_UNSORTED], + 'c' => ['url' => '/foo?x=1&z=0&sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=c', 'class' => Column::CSS_CLASS_UNSORTED], + ]; + + $this->sortableColumns->setOrderBy($this->sortableColumns->getColumn('a'), Column::DIRECTION_ASCENDING); + $this->sortableColumns->setAdditionalUrlParameters(['x' => 1, 'z' => 0]); + $this->assertEquals($expected, $this->sortableColumns->generateSortableColumns()); + } + + public function columnDefProvider(): array + { + return [ + [null, '', + [ + 'a' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_DESCENDING . '&sort-field=a', 'class' => Column::CSS_CLASS_ASCENDING], + 'b' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=b', 'class' => Column::CSS_CLASS_UNSORTED], + 'c' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=c', 'class' => Column::CSS_CLASS_UNSORTED], + ] + ], + ['a', Column::DIRECTION_ASCENDING, + [ + 'a' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_DESCENDING . '&sort-field=a', 'class' => Column::CSS_CLASS_ASCENDING], + 'b' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=b', 'class' => Column::CSS_CLASS_UNSORTED], + 'c' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=c', 'class' => Column::CSS_CLASS_UNSORTED], + ] + ], + ['a', Column::DIRECTION_DESCENDING, + [ + 'a' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=a', 'class' => Column::CSS_CLASS_DESCENDING], + 'b' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=b', 'class' => Column::CSS_CLASS_UNSORTED], + 'c' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=c', 'class' => Column::CSS_CLASS_UNSORTED], + ] + ], + ['c', Column::DIRECTION_ASCENDING, + [ + 'a' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=a', 'class' => Column::CSS_CLASS_UNSORTED], + 'b' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=b', 'class' => Column::CSS_CLASS_UNSORTED], + 'c' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_DESCENDING . '&sort-field=c', 'class' => Column::CSS_CLASS_ASCENDING], + ] + ], + ['b', Column::DIRECTION_DESCENDING, + [ + 'a' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=a', 'class' => Column::CSS_CLASS_UNSORTED], + 'b' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=b', 'class' => Column::CSS_CLASS_DESCENDING], + 'c' => ['url' => '/foo?sort-direction=' . Column::DIRECTION_ASCENDING . '&sort-field=c', 'class' => Column::CSS_CLASS_UNSORTED], + ] + ], + ]; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3e1a013 --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "zikula/sortable-columns", + "description": "SortableColumns is a zikula component to help manage data table column headings that can be clicked to sort the data", + "keywords": ["symfony", "columns", "sortable"], + "type": "library", + "homepage": "https://ziku.la", + "license": "MIT", + "authors": [ + { + "name": "Craig Heydenburg", + "email": "info@ziku.la" + } + ], + "autoload": { + "psr-4": { "Zikula\\Component\\SortableColumns\\": "" } + }, + "autoload-dev": { + "psr-4" : { + "Zikula\\Component\\SortableColumns\\Tests\\" : "tests/" + } + }, + "require": { + "php": ">=7.2.0", + "symfony/routing": "4.*|5.*", + "symfony/http-foundation": "4.*|5.*", + "doctrine/common": "2.*" + }, + "require-dev": { + "phpunit/phpunit": "^8.4", + "symfony/phpunit-bridge": "^4.4|^5.0" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..0a9ad74 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + + + +