mirror of
https://github.com/kamilwylegala/cakephp2-php8.git
synced 2025-09-04 10:32:40 +00:00
Merge branch '1.2' into 1.2-merger
Conflicts: cake/VERSION.txt cake/config/config.php cake/dispatcher.php cake/libs/controller/scaffold.php cake/libs/inflector.php cake/libs/view/view.php cake/tests/cases/libs/controller/scaffold.test.php cake/tests/cases/libs/inflector.test.php cake/tests/cases/libs/view/view.test.php cake/tests/fixtures/counter_cache_post_fixture.php cake/tests/fixtures/counter_cache_user_fixture.php
This commit is contained in:
commit
326424592d
17 changed files with 191 additions and 60 deletions
|
@ -17,5 +17,4 @@
|
|||
// @license MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
// +--------------------------------------------------------------------------------------------+ //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
1.3.0.0
|
|
@ -121,7 +121,7 @@ class ApiShell extends Shell {
|
|||
$this->out($list);
|
||||
|
||||
$methods = array_keys($parsed);
|
||||
while ($number = $this->in(__('Select a number to see the more information about a specific method. q to quit. l to list.', true), null, 'q')) {
|
||||
while ($number = strtolower($this->in(__('Select a number to see the more information about a specific method. q to quit. l to list.', true), null, 'q'))) {
|
||||
if ($number === 'q') {
|
||||
$this->out(__('Done', true));
|
||||
$this->_stop();
|
||||
|
|
|
@ -82,18 +82,18 @@ class I18nShell extends Shell {
|
|||
$this->out(__('[H]elp', true));
|
||||
$this->out(__('[Q]uit', true));
|
||||
|
||||
$choice = strtoupper($this->in(__('What would you like to do?', true), array('E', 'I', 'H', 'Q')));
|
||||
$choice = strtolower($this->in(__('What would you like to do?', true), array('E', 'I', 'H', 'Q')));
|
||||
switch ($choice) {
|
||||
case 'E':
|
||||
case 'e':
|
||||
$this->Extract->execute();
|
||||
break;
|
||||
case 'I':
|
||||
case 'i':
|
||||
$this->initdb();
|
||||
break;
|
||||
case 'H':
|
||||
case 'h':
|
||||
$this->help();
|
||||
break;
|
||||
case 'Q':
|
||||
case 'q':
|
||||
exit(0);
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -135,7 +135,7 @@ class SchemaShell extends Shell {
|
|||
|
||||
if (!$snapshot && file_exists($this->Schema->path . DS . $this->params['file'])) {
|
||||
$snapshot = true;
|
||||
$result = $this->in("Schema file exists.\n [O]verwrite\n [S]napshot\n [Q]uit\nWould you like to do?", array('o', 's', 'q'), 's');
|
||||
$result = strtolower($this->in("Schema file exists.\n [O]verwrite\n [S]napshot\n [Q]uit\nWould you like to do?", array('o', 's', 'q'), 's'));
|
||||
if ($result === 'q') {
|
||||
return $this->_stop();
|
||||
}
|
||||
|
@ -380,8 +380,7 @@ class SchemaShell extends Shell {
|
|||
Configure::write('debug', 2);
|
||||
$db =& ConnectionManager::getDataSource($this->Schema->connection);
|
||||
$db->fullDebug = true;
|
||||
|
||||
$errors = array();
|
||||
|
||||
foreach ($contents as $table => $sql) {
|
||||
if (empty($sql)) {
|
||||
$this->out(sprintf(__('%s is up to date.', true), $table));
|
||||
|
@ -393,15 +392,16 @@ class SchemaShell extends Shell {
|
|||
if (!$Schema->before(array($event => $table))) {
|
||||
return false;
|
||||
}
|
||||
if (!$db->_execute($sql)) {
|
||||
$error = null;
|
||||
if (!$db->execute($sql)) {
|
||||
$error = $table . ': ' . $db->lastError();
|
||||
}
|
||||
|
||||
$Schema->after(array($event => $table, 'errors'=> $errors));
|
||||
$Schema->after(array($event => $table, 'errors' => $error));
|
||||
|
||||
if (isset($error)) {
|
||||
if (!empty($error)) {
|
||||
$this->out($error);
|
||||
} elseif ($this->__dry !== true) {
|
||||
} else {
|
||||
$this->out(sprintf(__('%s updated.', true), $table));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -318,22 +318,28 @@ class Dispatcher extends Object {
|
|||
$params['url'] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($_FILES as $name => $data) {
|
||||
if ($name != 'data') {
|
||||
$params['form'][$name] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_FILES['data'])) {
|
||||
foreach ($_FILES['data'] as $key => $data) {
|
||||
foreach ($data as $model => $fields) {
|
||||
foreach ($fields as $field => $value) {
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $k => $v) {
|
||||
$params['data'][$model][$field][$k][$key] = $v;
|
||||
if (is_array($fields)) {
|
||||
foreach ($fields as $field => $value) {
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $k => $v) {
|
||||
$params['data'][$model][$field][$k][$key] = $v;
|
||||
}
|
||||
} else {
|
||||
$params['data'][$model][$field][$key] = $value;
|
||||
}
|
||||
} else {
|
||||
$params['data'][$model][$field][$key] = $value;
|
||||
}
|
||||
} else {
|
||||
$params['data'][$model][$key] = $fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -641,7 +647,7 @@ class Dispatcher extends Object {
|
|||
|
||||
if ($pos > 0) {
|
||||
$plugin = substr($url, 0, $pos - 1);
|
||||
$url = str_replace($plugin . '/', '', $url);
|
||||
$url = preg_replace('/^' . preg_quote($plugin, '/') . '\//i', '', $url);
|
||||
$pluginPaths = App::path('plugins');
|
||||
$count = count($pluginPaths);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
|
|
|
@ -498,7 +498,7 @@ class DboOracle extends DboSource {
|
|||
* @access public
|
||||
*/
|
||||
function describe(&$model) {
|
||||
$table = $model->fullTableName($model, false);
|
||||
$table = $this->fullTableName($model, false);
|
||||
|
||||
if (!empty($model->sequence)) {
|
||||
$this->_sequenceMap[$table] = $model->sequence;
|
||||
|
|
|
@ -86,8 +86,8 @@ class DboSource extends DataSource {
|
|||
* @access protected
|
||||
*/
|
||||
var $_commands = array(
|
||||
'begin' => 'BEGIN',
|
||||
'commit' => 'COMMIT',
|
||||
'begin' => 'BEGIN',
|
||||
'commit' => 'COMMIT',
|
||||
'rollback' => 'ROLLBACK'
|
||||
);
|
||||
|
||||
|
@ -429,14 +429,15 @@ class DboSource extends DataSource {
|
|||
$data[$i] = str_replace($this->startQuote . $this->startQuote, $this->startQuote, $data[$i]);
|
||||
$data[$i] = str_replace($this->startQuote . '(', '(', $data[$i]);
|
||||
$data[$i] = str_replace(')' . $this->startQuote, ')', $data[$i]);
|
||||
$alias = !empty($this->alias) ? $this->alias : 'AS ';
|
||||
|
||||
if (preg_match('/\s+AS\s+/', $data[$i])) {
|
||||
if (preg_match('/\w+\s+AS\s+/', $data[$i])) {
|
||||
$quoted = $this->endQuote . ' AS ' . $this->startQuote;
|
||||
$data[$i] = str_replace(' AS ', $quoted, $data[$i]);
|
||||
if (preg_match('/\s+' . $alias . '\s*/', $data[$i])) {
|
||||
if (preg_match('/\w+\s+' . $alias . '\s*/', $data[$i])) {
|
||||
$quoted = $this->endQuote . ' ' . $alias . $this->startQuote;
|
||||
$data[$i] = str_replace(' ' . $alias, $quoted, $data[$i]);
|
||||
} else {
|
||||
$quoted = ' AS ' . $this->startQuote;
|
||||
$data[$i] = str_replace(' AS ', $quoted, $data[$i]) . $this->endQuote;
|
||||
$quoted = $alias . $this->startQuote;
|
||||
$data[$i] = str_replace($alias, $quoted, $data[$i]) . $this->endQuote;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1745,7 +1746,9 @@ class DboSource extends DataSource {
|
|||
|
||||
if ($count >= 1 && !in_array($fields[0], array('*', 'COUNT(*)'))) {
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if (!preg_match('/^.+\\(.*\\)/', $fields[$i])) {
|
||||
if (preg_match('/^\(.*\)\s' . $this->alias . '.*/i', $fields[$i])){
|
||||
continue;
|
||||
} elseif (!preg_match('/^.+\\(.*\\)/', $fields[$i])) {
|
||||
$prepend = '';
|
||||
|
||||
if (strpos($fields[$i], 'DISTINCT') !== false) {
|
||||
|
|
|
@ -397,7 +397,10 @@ class Set extends Object {
|
|||
$contexts = $data;
|
||||
$options = array_merge(array('flatten' => true), $options);
|
||||
if (!isset($contexts[0])) {
|
||||
$contexts = array($data);
|
||||
$current = current($data);
|
||||
if ((is_array($current) && count($data) <= 1) || !is_array($current)) {
|
||||
$contexts = array($data);
|
||||
}
|
||||
}
|
||||
$tokens = array_slice(preg_split('/(?<!=)\/(?![a-z-]*\])/', $path), 1);
|
||||
|
||||
|
|
|
@ -315,8 +315,8 @@ class TimeHelper extends AppHelper {
|
|||
* Relative dates look something like this:
|
||||
* 3 weeks, 4 days ago
|
||||
* 15 seconds ago
|
||||
* Formatted dates look like this:
|
||||
* on 02/18/2004
|
||||
*
|
||||
* Default date formatting is d/m/yy e.g: on 18/2/09
|
||||
*
|
||||
* The returned string includes 'ago' or 'on' and assumes you'll properly add a word
|
||||
* like 'Posted ' before the function output.
|
||||
|
|
|
@ -708,7 +708,6 @@ class XmlNode extends Object {
|
|||
if ($child->attributes) {
|
||||
$value = array_merge(array('value' => $value), $child->attributes);
|
||||
}
|
||||
|
||||
if (isset($out[$child->name]) || isset($multi[$key])) {
|
||||
if (!isset($multi[$key])) {
|
||||
$multi[$key] = array($out[$child->name]);
|
||||
|
@ -721,15 +720,16 @@ class XmlNode extends Object {
|
|||
continue;
|
||||
} elseif (count($child->children) === 0 && $child->value == '') {
|
||||
$value = $child->attributes;
|
||||
|
||||
if (isset($out[$child->name]) || isset($multi[$key])) {
|
||||
if (!isset($multi[$key])) {
|
||||
$multi[$key] = array($out[$child->name]);
|
||||
unset($out[$child->name]);
|
||||
}
|
||||
$multi[$key][] = $value;
|
||||
} else {
|
||||
} elseif (!empty($value)) {
|
||||
$out[$key] = $value;
|
||||
} else {
|
||||
$out[$child->name] = $value;
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
|
@ -851,19 +851,19 @@ class Xml extends XmlNode {
|
|||
* Constructor. Sets up the XML parser with options, gives it this object as
|
||||
* its XML object, and sets some variables.
|
||||
*
|
||||
* ### Options
|
||||
* - 'root': The name of the root element, defaults to '#document'
|
||||
* - 'version': The XML version, defaults to '1.0'
|
||||
* - 'encoding': Document encoding, defaults to 'UTF-8'
|
||||
* - 'namespaces': An array of namespaces (as strings) used in this document
|
||||
* - 'format': Specifies the format this document converts to when parsed or
|
||||
* rendered out as text, either 'attributes' or 'tags', defaults to 'attributes'
|
||||
* - 'tags': An array specifying any tag-specific formatting options, indexed
|
||||
* by tag name. See XmlNode::normalize().
|
||||
* @param mixed $input The content with which this XML document should be initialized. Can be a
|
||||
* string, array or object. If a string is specified, it may be a literal XML
|
||||
* document, or a URL or file path to read from.
|
||||
* @param array $options Options to set up with, valid options are as follows:
|
||||
* - 'root': The name of the root element, defaults to '#document'
|
||||
* - 'version': The XML version, defaults to '1.0'
|
||||
* - 'encoding': Document encoding, defaults to 'UTF-8'
|
||||
* - 'namespaces': An array of namespaces (as strings) used in this document
|
||||
* - 'format': Specifies the format this document converts to when parsed or
|
||||
* rendered out as text, either 'attributes' or 'tags',
|
||||
* defaults to 'attributes'
|
||||
* - 'tags': An array specifying any tag-specific formatting options, indexed
|
||||
* by tag name. See XmlNode::normalize().
|
||||
* string, array or object. If a string is specified, it may be a literal XML
|
||||
* document, or a URL or file path to read from.
|
||||
* @param array $options Options to set up with, for valid options see above:
|
||||
* @see XmlNode::normalize()
|
||||
*/
|
||||
function __construct($input = null, $options = array()) {
|
||||
|
@ -892,9 +892,6 @@ class Xml extends XmlNode {
|
|||
$Root->append($input, $options);
|
||||
}
|
||||
}
|
||||
// if (Configure::read('App.encoding') !== null) {
|
||||
// $this->encoding = Configure::read('App.encoding');
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -938,8 +935,8 @@ class Xml extends XmlNode {
|
|||
$this->__initParser();
|
||||
$this->__rawData = trim($this->__rawData);
|
||||
$this->__header = trim(str_replace(
|
||||
a('<' . '?', '?' . '>'),
|
||||
a('', ''),
|
||||
array('<' . '?', '?' . '>'),
|
||||
array('', ''),
|
||||
substr($this->__rawData, 0, strpos($this->__rawData, '?' . '>'))
|
||||
));
|
||||
|
||||
|
@ -950,7 +947,6 @@ class Xml extends XmlNode {
|
|||
for ($i = 0; $i < $count; $i++) {
|
||||
$data = $vals[$i];
|
||||
$data += array('tag' => null, 'value' => null, 'attributes' => array());
|
||||
|
||||
switch ($data['type']) {
|
||||
case "open" :
|
||||
$xml =& $xml->createElement($data['tag'], $data['value'], $data['attributes']);
|
||||
|
|
|
@ -940,6 +940,32 @@ class DispatcherTest extends CakeTestCase {
|
|||
)
|
||||
);
|
||||
$this->assertEqual($result['data'], $expected);
|
||||
|
||||
|
||||
$_FILES = array(
|
||||
'data' => array(
|
||||
'name' => array('birth_cert' => 'born on.txt'),
|
||||
'type' => array('birth_cert' => 'application/octet-stream'),
|
||||
'tmp_name' => array('birth_cert' => '/private/var/tmp/phpbsUWfH'),
|
||||
'error' => array('birth_cert' => 0),
|
||||
'size' => array('birth_cert' => 123)
|
||||
)
|
||||
);
|
||||
|
||||
$Dispatcher =& new Dispatcher();
|
||||
$result = $Dispatcher->parseParams('/');
|
||||
|
||||
$expected = array(
|
||||
'birth_cert' => array(
|
||||
'name' => 'born on.txt',
|
||||
'type' => 'application/octet-stream',
|
||||
'tmp_name' => '/private/var/tmp/phpbsUWfH',
|
||||
'error' => 0,
|
||||
'size' => 123
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertEqual($result['data'], $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1780,6 +1806,20 @@ class DispatcherTest extends CakeTestCase {
|
|||
$this->assertEqual('this is the test asset css file', $result);
|
||||
|
||||
|
||||
ob_start();
|
||||
$Dispatcher->cached('test_plugin/js/test_plugin/test.js');
|
||||
$result = ob_get_clean();
|
||||
$this->assertEqual('alert("Test App");', $result);
|
||||
|
||||
|
||||
Configure::write('debug', 0);
|
||||
$Dispatcher->params = $Dispatcher->parseParams('test_plugin/js/test_plugin/test.js');
|
||||
ob_start();
|
||||
$Dispatcher->cached('test_plugin/js/test_plugin/test.js');
|
||||
$result = ob_get_clean();
|
||||
$this->assertEqual('alert("Test App");', $result);
|
||||
|
||||
|
||||
Configure::write('debug', 0);
|
||||
$Dispatcher->params = $Dispatcher->parseParams('test_plugin/css/test_plugin_asset.css');
|
||||
ob_start();
|
||||
|
@ -1787,6 +1827,7 @@ class DispatcherTest extends CakeTestCase {
|
|||
$result = ob_get_clean();
|
||||
$this->assertEqual('this is the test plugin asset css file', $result);
|
||||
|
||||
|
||||
Configure::write('debug', 0);
|
||||
$Dispatcher->params = $Dispatcher->parseParams('test_plugin/img/cake.icon.gif');
|
||||
ob_start();
|
||||
|
|
|
@ -621,7 +621,6 @@ class ScaffoldTest extends CakeTestCase {
|
|||
* @access public
|
||||
*/
|
||||
var $fixtures = array('core.article', 'core.user', 'core.comment', 'core.posts_tag', 'core.tag');
|
||||
|
||||
/**
|
||||
* startTest method
|
||||
*
|
||||
|
|
|
@ -113,6 +113,7 @@ class InflectorTest extends CakeTestCase {
|
|||
$this->assertEqual(Inflector::singularize('faxes'), 'fax');
|
||||
$this->assertEqual(Inflector::singularize('waxes'), 'wax');
|
||||
$this->assertEqual(Inflector::singularize('niches'), 'niche');
|
||||
$this->assertEqual(Inflector::singularize('waves'), 'wave');
|
||||
$this->assertEqual(Inflector::singularize(''), '');
|
||||
}
|
||||
|
||||
|
@ -158,6 +159,7 @@ class InflectorTest extends CakeTestCase {
|
|||
$this->assertEqual(Inflector::pluralize('glove'), 'gloves');
|
||||
$this->assertEqual(Inflector::pluralize('crisis'), 'crises');
|
||||
$this->assertEqual(Inflector::pluralize('tax'), 'taxes');
|
||||
$this->assertEqual(Inflector::pluralize('wave'), 'waves');
|
||||
$this->assertEqual(Inflector::pluralize(''), '');
|
||||
}
|
||||
|
||||
|
|
|
@ -2850,6 +2850,10 @@ class DboSourceTest extends CakeTestCase {
|
|||
);
|
||||
$this->assertEqual($result, $expected);
|
||||
|
||||
$result = $this->testDb->fields($this->Model, null, "(`Provider`.`star_total` / `Provider`.`total_ratings`) as `rating`");
|
||||
$expected = array("(`Provider`.`star_total` / `Provider`.`total_ratings`) as `rating`");
|
||||
$this->assertEqual($result, $expected);
|
||||
|
||||
$result = $this->testDb->fields($this->Model, 'Post');
|
||||
$expected = array(
|
||||
'`Post`.`id`', '`Post`.`client_id`', '`Post`.`name`', '`Post`.`login`',
|
||||
|
@ -2927,7 +2931,7 @@ class DboSourceTest extends CakeTestCase {
|
|||
$expected = array(
|
||||
'`Foo`.`id`',
|
||||
'`Foo`.`title`',
|
||||
'(user_count + discussion_count + post_count) AS `score`'
|
||||
'(user_count + discussion_count + post_count) AS score'
|
||||
);
|
||||
$this->assertEqual($result, $expected);
|
||||
}
|
||||
|
|
|
@ -545,6 +545,30 @@ class SetTest extends CakeTestCase {
|
|||
$r = Set::extract('/User/@*', $tricky);
|
||||
$this->assertEqual($r, $expected);
|
||||
|
||||
$nonZero = array(
|
||||
1 => array(
|
||||
'User' => array(
|
||||
'id' => 1,
|
||||
'name' => 'John',
|
||||
)
|
||||
),
|
||||
2 => array(
|
||||
'User' => array(
|
||||
'id' => 2,
|
||||
'name' => 'Bob',
|
||||
)
|
||||
),
|
||||
3 => array(
|
||||
'User' => array(
|
||||
'id' => 3,
|
||||
'name' => 'Tony',
|
||||
)
|
||||
)
|
||||
);
|
||||
$expected = array(1, 2, 3);
|
||||
$r = Set::extract('/User/id', $nonZero);
|
||||
$this->assertEqual($r, $expected);
|
||||
|
||||
$common = array(
|
||||
array(
|
||||
'Article' => array(
|
||||
|
@ -979,7 +1003,6 @@ class SetTest extends CakeTestCase {
|
|||
$expected = array(array('name' => 'zipfile.zip','type' => 'application/zip','tmp_name' => '/tmp/php178.tmp','error' => 0,'size' => '564647'));
|
||||
$r = Set::extract('/file/.[type=application/zip]', $f);
|
||||
$this->assertEqual($r, $expected);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2423,7 +2446,7 @@ class SetTest extends CakeTestCase {
|
|||
array(
|
||||
'Item' => array(
|
||||
'title' => 'An example of a correctly reversed XMLNode',
|
||||
'Desc' => array(),
|
||||
'desc' => array(),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -782,6 +782,60 @@ class XmlTest extends CakeTestCase {
|
|||
$this->assertEqual($result, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* test that empty values do not casefold collapse
|
||||
*
|
||||
* @see http://code.cakephp.org/tickets/view/8
|
||||
* @return void
|
||||
**/
|
||||
function testCaseFoldingWithEmptyValues() {
|
||||
$filledValue = '<method name="set_user_settings">
|
||||
<title>update user information</title>
|
||||
<user>1</user>
|
||||
<User>
|
||||
<id>1</id>
|
||||
<name>varchar(45)</name>
|
||||
</User>
|
||||
</method>';
|
||||
$xml =& new XML($filledValue);
|
||||
$expected = array(
|
||||
'Method' => array(
|
||||
'name' => 'set_user_settings',
|
||||
'title' => 'update user information',
|
||||
'user' => '1',
|
||||
'User' => array(
|
||||
'id' => 1,
|
||||
'name' => 'varchar(45)',
|
||||
),
|
||||
)
|
||||
);
|
||||
$result = $xml->toArray();
|
||||
$this->assertEqual($result, $expected);
|
||||
|
||||
$emptyValue ='<method name="set_user_settings">
|
||||
<title>update user information</title>
|
||||
<user></user>
|
||||
<User>
|
||||
<id>1</id>
|
||||
<name>varchar(45)</name>
|
||||
</User>
|
||||
</method>';
|
||||
|
||||
$xml =& new XML($emptyValue);
|
||||
$expected = array(
|
||||
'Method' => array(
|
||||
'name' => 'set_user_settings',
|
||||
'title' => 'update user information',
|
||||
'user' => array(),
|
||||
'User' => array(
|
||||
'id' => 1,
|
||||
'name' => 'varchar(45)',
|
||||
),
|
||||
)
|
||||
);
|
||||
$result = $xml->toArray();
|
||||
$this->assertEqual($result, $expected);
|
||||
}
|
||||
/**
|
||||
* testMixedParsing method
|
||||
*
|
||||
|
@ -950,7 +1004,7 @@ class XmlTest extends CakeTestCase {
|
|||
'Example' => array(
|
||||
'Item' => array(
|
||||
'title' => 'An example of a correctly reversed XMLNode',
|
||||
'Desc' => array(),
|
||||
'desc' => array(),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
1
cake/tests/test_app/plugins/test_plugin/vendors/js/test_plugin/test.js
vendored
Normal file
1
cake/tests/test_app/plugins/test_plugin/vendors/js/test_plugin/test.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
alert("Test App");
|
Loading…
Add table
Add a link
Reference in a new issue