Skip to content

Commit

Permalink
feature(views): lists can be rendered as tables
Browse files Browse the repository at this point in the history
The list view functions now accept `table` as a `list_type` value. This outputs
a bordered, one-column table with no headings.

Columns and their headings are specified via `Elgg\Views\TableColumn` objects. The
`ColumnFactory` class (accessible via `elgg()->table_columns`) includes methods for
creating columns based on core views, or properties/methods.

Newest users admin page now shows other useful info

Fixes Elgg#7684
Fixes Elgg#9629
  • Loading branch information
mrclay committed Sep 14, 2016
1 parent 7619375 commit 0abc57a
Show file tree
Hide file tree
Showing 31 changed files with 826 additions and 7 deletions.
5 changes: 5 additions & 0 deletions docs/guides/hooks-list.rst
Expand Up @@ -538,6 +538,11 @@ Views
Applies to request to ``/ajax/form/<form_name>``.
This hook can be used to modify response content, status code, forward URL, or set additional response headers.

**table_columns:call, <name>**
When the method ``elgg()->table_columns->$name()`` is called, this hook is called to allow
plugins to override or provide an implementation. Handlers receive the method arguments via
``$params['arguments']`` and should return an instance of ``Elgg\Views\TableColumn``.

Files
=====

Expand Down
13 changes: 12 additions & 1 deletion docs/guides/upgrading.rst
Expand Up @@ -80,6 +80,17 @@ Notifications
* ``elgg_get_notification_methods()`` can be used to obtain registered notification methods
* Added ``ElggUser::getNotificationSettings()`` and ``ElggUser::setNotificationSetting()``

Entity list functions can output tables
---------------------------------------

In functions like ``elgg_list_entities($options)``, table output is possible by setting
``$options['list_type'] = 'table'`` and providing an array of table columns as ``$options['columns']``.
Each column is an ``Elgg\Views\TableColumn`` object, usually created via methods on the service
``elgg()->table_columns``.

Plugins can provide or alter these factory methods (see ``Elgg\Views\TableColumn\ColumnFactory``).
See the view ``admin/users/newest`` for a usage example.

From 2.1 to 2.2
===============

Expand Down Expand Up @@ -137,7 +148,7 @@ Removed APIs
Just a warning that the private entity cache functions (e.g. ``_elgg_retrieve_cached_entity``) have been removed. Some plugins may have been using them. Plugins should not use private APIs as they will more often be removed without notice.

Improved ``elgg/ckeditor`` module
-----------------------------------
---------------------------------

:doc:`elgg/ckeditor module <javascript>` can now be used to add WYSIWYG to a textarea programmatically with ``elgg/ckeditor#bind``.

Expand Down
34 changes: 34 additions & 0 deletions docs/guides/views.rst
Expand Up @@ -503,6 +503,9 @@ you can improve performance a bit by preloading all owner entities:
See also :doc:`this background information on Elgg's database </design/database>`.
Rendering a list with an alternate view
---------------------------------------
Since 1.11, you can define an alternative view to render list items using ``'item_view'`` parameter.
In some cases, default entity views may be unsuitable for your needs.
Expand Down Expand Up @@ -537,6 +540,37 @@ Since invitations are not entities, they do not have their own views and can not
We are providing an alternative item view, that will use the group entity to display
an invitation that contains a group name and buttons to access or reject the invitation.
Rendering a list as a table
---------------------------
Since 2.3 you can render lists as tables. Set ``$options['list_type'] = 'table'`` and provide an array of
TableColumn objects as ``$options['columns']``. The service ``elgg()->table_columns`` provides several
methods to create column objects based around existing views (like ``page/components/column/*``), properties,
or methods.
In this example, we list the latest ``my_plugin`` objects in a table of 3 columns: entity icon, the display
name, and a friendly format of the time.
.. code-block:: php
echo elgg_list_entities([
'type' => 'object',
'subtype' => 'my_plugin',
'list_type' => 'table',
'columns' => [
elgg()->table_columns->icon(),
elgg()->table_columns->getDisplayName(),
elgg()->table_columns->time_created(null, [
'format' => 'friendly',
]),
],
]);
See the ``Elgg\Views\TableColumn\ColumnFactory`` class for more details on how columns are specified and
rendered. You can add or override methods of ``elgg()->table_columns`` in a variety of ways, based on views,
properties/methods on the items, or given functions.
Related
=======
Expand Down
2 changes: 2 additions & 0 deletions engine/classes/Elgg/Application.php
Expand Up @@ -16,6 +16,7 @@
* @since 2.0.0
*
* @property-read \Elgg\Menu\Service $menus
* @property-read \Elgg\Views\TableColumn\ColumnFactory $table_columns
*/
class Application {

Expand Down Expand Up @@ -44,6 +45,7 @@ class Application {
private static $public_services = [
//'config' => true,
'menus' => true,
'table_columns' => true,
];

/**
Expand Down
3 changes: 3 additions & 0 deletions engine/classes/Elgg/Di/ServiceProvider.php
Expand Up @@ -71,6 +71,7 @@
* @property-read \Elgg\Database\SubtypeTable $subtypeTable
* @property-read \Elgg\Cache\SystemCache $systemCache
* @property-read \Elgg\SystemMessagesService $systemMessages
* @property-read \Elgg\Views\TableColumn\ColumnFactory $table_columns
* @property-read \Elgg\Timer $timer
* @property-read \Elgg\I18n\Translator $translator
* @property-read \Elgg\UpgradeService $upgrades
Expand Down Expand Up @@ -401,6 +402,8 @@ public function __construct(\Elgg\Config $config) {
return new \Elgg\SystemMessagesService($c->session);
});

$this->setClassName('table_columns', \Elgg\Views\TableColumn\ColumnFactory::class);

$this->setClassName('timer', \Elgg\Timer::class);

$this->setClassName('translator', \Elgg\I18n\Translator::class);
Expand Down
28 changes: 28 additions & 0 deletions engine/classes/Elgg/Views/TableColumn.php
@@ -0,0 +1,28 @@
<?php
namespace Elgg\Views;

/**
* A renderer for a column of table cells and a header
*/
interface TableColumn {

/**
* Get the rendered heading cell as HTML. Cell will be auto-wrapped with a TH element if the
* returned string doesn't begin with "<th" or "<td".
*
* @return string e.g. "Title" or "<th>Title</th>". You must filter/escape any user content.
*/
public function renderHeading();

/**
* Render a value cell as HTML. Cell will be auto-wrapped with a TD element if the returned
* string doesn't begin with "<th" or "<td".
*
* @param mixed $item Object/row from which to pull the value
* @param string $type Type of object
* @param array $item_vars Parameters from the listing function
*
* @return string e.g. "My Great Title" or "<td>My Great Title</td>". You must filter/escape any user content.
*/
public function renderCell($item, $type, $item_vars);
}
45 changes: 45 additions & 0 deletions engine/classes/Elgg/Views/TableColumn/CallableColumn.php
@@ -0,0 +1,45 @@
<?php
namespace Elgg\Views\TableColumn;

use Elgg\Views\TableColumn;

/**
* Table column rendered by a function
*/
class CallableColumn implements TableColumn {

/**
* @var string
*/
private $heading;

/**
* @var callable
*/
private $renderer;

/**
* Constructor
*
* @param callable $renderer Rendering function
* @param string $heading Heading
*/
public function __construct(callable $renderer, $heading) {
$this->renderer = $renderer;
$this->heading = $heading;
}

/**
* {@inheritdoc}
*/
public function renderHeading() {
return $this->heading;
}

/**
* {@inheritdoc}
*/
public function renderCell($item, $type, $item_vars) {
return call_user_func($this->renderer, $item, $type, $item_vars);
}
}
165 changes: 165 additions & 0 deletions engine/classes/Elgg/Views/TableColumn/ColumnFactory.php
@@ -0,0 +1,165 @@
<?php
namespace Elgg\Views\TableColumn;

use Elgg\Values;
use Elgg\Views\TableColumn;

/**
* Factory for table column objects
*
* `elgg_list_entities()` can output tables by specifying `$options['list_type'] = 'table'` and
* by providing an array of TableColumn objects to `$options['columns']`. This service, available
* as `elgg()->table_columns` provides methods to create column objects based around existing views
* like `page/components/column/*`, properties, or methods.
*
* Numerous pre-existing methods are provided via `__call()` magic. See this method to find out how
* to add your own methods, override the existing ones, or completely replace a method via hook.
*
* @internal Use elgg()->table_columns to access the instance of this.
*
* @method TableColumn admin($heading = null, $vars = [])
* @method TableColumn banned($heading = null, $vars = [])
* @method TableColumn container($heading = null, $vars = [])
* @method TableColumn excerpt($heading = null, $vars = [])
* @method TableColumn icon($heading = null, $vars = [])
* @method TableColumn item($heading = null, $vars = [])
* @method TableColumn language($heading = null, $vars = [])
* @method TableColumn link($heading = null, $vars = [])
* @method TableColumn owner($heading = null, $vars = [])
* @method TableColumn time_created($heading = null, $vars = [])
* @method TableColumn time_updated($heading = null, $vars = [])
* @method TableColumn description($heading = null)
* @method TableColumn email($heading = null)
* @method TableColumn name($heading = null)
* @method TableColumn type($heading = null)
* @method TableColumn user($heading = null, $vars = [])
* @method TableColumn username($heading = null)
* @method TableColumn getSubtype($heading = null)
* @method TableColumn getDisplayName($heading = null)
* @method TableColumn getMimeType($heading = null)
* @method TableColumn getSimpleType($heading = null)
*/
class ColumnFactory {

/**
* Make a column from one of the page/components/column/* views.
*
* @param string $name Column name (view will be "page/components/column/$name")
* @param string $heading Optional heading
* @param array $vars View vars (item, item_vars, and type will be merged in)
*
* @return ViewColumn
*/
public function fromView($name, $heading = null, $vars = []) {
$view = "page/components/column/$name";

if (!is_string($heading)) {
if (elgg_language_key_exists("table_columns:fromView:$name")) {
$heading = elgg_echo("table_columns:fromView:$name");
} else {
$title = str_replace('_', ' ', $name);
$heading = elgg_ucwords($title);
}
}

return new ViewColumn($view, $heading, $vars);
}

/**
* Make a column by reading a property of the item
*
* @param string $name Property name. e.g. "description", "email", "type"
* @param string $heading Heading
*
* @return CallableColumn
*/
public function fromProperty($name, $heading = null) {
if (!is_string($heading)) {
if (elgg_language_key_exists("table_columns:fromProperty:$name")) {
$heading = elgg_echo("table_columns:fromProperty:$name");
} else {
$title = str_replace('_', ' ', $name);
$heading = elgg_ucwords($title);
}
}

$renderer = function ($item) use ($name) {
return $item->{$name};
};

return new CallableColumn($renderer, $heading);
}

/**
* Make a column by calling a method on the item
*
* @param string $name Method name. e.g. "getSubtype", "getDisplayName"
* @param string $heading Heading
* @param array $args Method arguments
*
* @return CallableColumn
*/
public function fromMethod($name, $heading = null, $args = []) {
if (!is_string($heading)) {
if (elgg_language_key_exists("table_columns:fromMethod:$name")) {
$heading = elgg_echo("table_columns:fromMethod:$name");
} else {
$title = str_replace('_', ' ', $name);
$heading = elgg_ucwords($title);
}
}

$renderer = function ($item) use ($name, $args) {
return call_user_func_array([$item, $name], $args);
};

return new CallableColumn($renderer, $heading);
}

/**
* Provide additional methods via hook and specified language keys.
*
* First, the hook `table_columns:call` is called. Details in `docs/guides/hooks-list.rst`.
*
* Then it checks existence of 3 language keys in order to defer processing to a local method:
*
* - "table_columns:fromView:$name" -> uses $this->fromView($name, ...).
* - "table_columns:fromProperty:$name" -> uses $this->fromProperty($name, ...).
* - "table_columns:fromMethod:$name" -> uses $this->fromMethod($name, ...).
*
* See the `from*()` methods for details.
*
* @param string $name Method name
* @param array $arguments Arguments
*
* @return TableColumn
*/
public function __call($name, $arguments) {
// allow hook to hijack magic methods
$column = elgg_trigger_plugin_hook('table_columns:call', $name, [
'arguments' => $arguments,
]);
if ($column instanceof TableColumn) {
return $column;
}

if (elgg_language_key_exists("table_columns:fromView:$name")) {
array_unshift($arguments, $name);
return call_user_func_array([$this, 'fromView'], $arguments);
}

if (elgg_language_key_exists("table_columns:fromProperty:$name")) {
array_unshift($arguments, $name);
return call_user_func_array([$this, 'fromProperty'], $arguments);
}

if (elgg_language_key_exists("table_columns:fromMethod:$name")) {
array_unshift($arguments, $name);
return call_user_func_array([$this, 'fromMethod'], $arguments);
}

// empty column and error
_elgg_services()->logger->error(__CLASS__ . ": No method defined '$name'");
return new CallableColumn([Values::class, 'getNull'], '');
}
}

0 comments on commit 0abc57a

Please sign in to comment.