mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
ColUDA: Split ColUDA into ColUDA{String,Numeric,Date,Duration} to make use of ColType*::modify
This commit is contained in:
parent
73d789c593
commit
6f4f468d0d
3 changed files with 401 additions and 88 deletions
|
@ -36,19 +36,18 @@
|
||||||
extern Context context;
|
extern Context context;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
ColumnUDA::ColumnUDA ()
|
ColumnUDAString::ColumnUDAString ()
|
||||||
{
|
{
|
||||||
_name = "<uda>";
|
_name = "<uda>";
|
||||||
_type = "string";
|
|
||||||
_style = "default";
|
_style = "default";
|
||||||
_label = "";
|
_label = "";
|
||||||
_uda = true;
|
_uda = true;
|
||||||
_hyphenate = (_type == "string") ? true : false;
|
_hyphenate = true;
|
||||||
_styles = {_style, "indicator"};
|
_styles = {_style, "indicator"};
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
bool ColumnUDA::validate (std::string& value)
|
bool ColumnUDAString::validate (std::string& value)
|
||||||
{
|
{
|
||||||
// No restrictions.
|
// No restrictions.
|
||||||
if (_values.size () == 0)
|
if (_values.size () == 0)
|
||||||
|
@ -66,7 +65,7 @@ bool ColumnUDA::validate (std::string& value)
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Set the minimum and maximum widths for the value.
|
// Set the minimum and maximum widths for the value.
|
||||||
//
|
//
|
||||||
void ColumnUDA::measure (Task& task, unsigned int& minimum, unsigned int& maximum)
|
void ColumnUDAString::measure (Task& task, unsigned int& minimum, unsigned int& maximum)
|
||||||
{
|
{
|
||||||
minimum = maximum = 0;
|
minimum = maximum = 0;
|
||||||
|
|
||||||
|
@ -77,35 +76,9 @@ void ColumnUDA::measure (Task& task, unsigned int& minimum, unsigned int& maximu
|
||||||
std::string value = task.get (_name);
|
std::string value = task.get (_name);
|
||||||
if (value != "")
|
if (value != "")
|
||||||
{
|
{
|
||||||
if (_type == "date")
|
std::string stripped = Color::strip (value);
|
||||||
{
|
maximum = longestLine (stripped);
|
||||||
// Determine the output date format, which uses a hierarchy of definitions.
|
minimum = longestWord (stripped);
|
||||||
// rc.report.<report>.dateformat
|
|
||||||
// rc.dateformat.report
|
|
||||||
// rc.dateformat
|
|
||||||
ISO8601d date ((time_t) strtol (value.c_str (), NULL, 10));
|
|
||||||
std::string format = context.config.get ("report." + _report + ".dateformat");
|
|
||||||
if (format == "")
|
|
||||||
format = context.config.get ("dateformat.report");
|
|
||||||
if (format == "")
|
|
||||||
format = context.config.get ("dateformat");
|
|
||||||
|
|
||||||
minimum = maximum = ISO8601d::length (format);
|
|
||||||
}
|
|
||||||
else if (_type == "duration")
|
|
||||||
{
|
|
||||||
minimum = maximum = ISO8601p (value).format ().length ();
|
|
||||||
}
|
|
||||||
else if (_type == "string")
|
|
||||||
{
|
|
||||||
std::string stripped = Color::strip (value);
|
|
||||||
maximum = longestLine (stripped);
|
|
||||||
minimum = longestWord (stripped);
|
|
||||||
}
|
|
||||||
else if (_type == "numeric")
|
|
||||||
{
|
|
||||||
minimum = maximum = value.length ();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_style == "indicator")
|
else if (_style == "indicator")
|
||||||
|
@ -127,7 +100,7 @@ void ColumnUDA::measure (Task& task, unsigned int& minimum, unsigned int& maximu
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
void ColumnUDA::render (
|
void ColumnUDAString::render (
|
||||||
std::vector <std::string>& lines,
|
std::vector <std::string>& lines,
|
||||||
Task& task,
|
Task& task,
|
||||||
int width,
|
int width,
|
||||||
|
@ -138,36 +111,304 @@ void ColumnUDA::render (
|
||||||
if (_style == "default")
|
if (_style == "default")
|
||||||
{
|
{
|
||||||
std::string value = task.get (_name);
|
std::string value = task.get (_name);
|
||||||
if (_type == "date")
|
std::vector <std::string> raw;
|
||||||
{
|
wrapText (raw, value, width, _hyphenate);
|
||||||
// Determine the output date format, which uses a hierarchy of definitions.
|
|
||||||
// rc.report.<report>.dateformat
|
|
||||||
// rc.dateformat.report
|
|
||||||
// rc.dateformat.
|
|
||||||
std::string format = context.config.get ("report." + _report + ".dateformat");
|
|
||||||
if (format == "")
|
|
||||||
{
|
|
||||||
format = context.config.get ("dateformat.report");
|
|
||||||
if (format == "")
|
|
||||||
format = context.config.get ("dateformat");
|
|
||||||
}
|
|
||||||
|
|
||||||
renderStringLeft (lines, width, color, ISO8601d ((time_t) strtol (value.c_str (), NULL, 10)).toString (format));
|
for (auto& i : raw)
|
||||||
}
|
renderStringLeft (lines, width, color, i);
|
||||||
else if (_type == "duration")
|
}
|
||||||
renderStringRight (lines, width, color, ISO8601p (value).format ());
|
else if (_style == "indicator")
|
||||||
|
{
|
||||||
else if (_type == "string")
|
if (task.has (_name))
|
||||||
{
|
{
|
||||||
std::vector <std::string> raw;
|
auto indicator = context.config.get ("uda." + _name + ".indicator");
|
||||||
wrapText (raw, value, width, _hyphenate);
|
if (indicator == "")
|
||||||
|
indicator = "U";
|
||||||
for (auto& i : raw)
|
|
||||||
renderStringLeft (lines, width, color, i);
|
renderStringRight (lines, width, color, indicator);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if (_type == "numeric")
|
}
|
||||||
renderStringRight (lines, width, color, value);
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
ColumnUDANumeric::ColumnUDANumeric ()
|
||||||
|
{
|
||||||
|
_name = "<uda>";
|
||||||
|
_type = "numeric";
|
||||||
|
_style = "default";
|
||||||
|
_label = "";
|
||||||
|
_uda = true;
|
||||||
|
_hyphenate = false;
|
||||||
|
_styles = {_style, "indicator"};
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
bool ColumnUDANumeric::validate (std::string& value)
|
||||||
|
{
|
||||||
|
// No restrictions.
|
||||||
|
if (_values.size () == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Look for exact match value.
|
||||||
|
for (auto& i : _values)
|
||||||
|
if (i == value)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Fail if not found.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Set the minimum and maximum widths for the value.
|
||||||
|
//
|
||||||
|
void ColumnUDANumeric::measure (Task& task, unsigned int& minimum, unsigned int& maximum)
|
||||||
|
{
|
||||||
|
minimum = maximum = 0;
|
||||||
|
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
if (_style == "default")
|
||||||
|
{
|
||||||
|
std::string value = task.get (_name);
|
||||||
|
if (value != "")
|
||||||
|
minimum = maximum = value.length ();
|
||||||
|
}
|
||||||
|
else if (_style == "indicator")
|
||||||
|
{
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
auto indicator = context.config.get ("uda." + _name + ".indicator");
|
||||||
|
if (indicator == "")
|
||||||
|
indicator = "U";
|
||||||
|
|
||||||
|
minimum = maximum = utf8_width (indicator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
minimum = maximum = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw format (STRING_COLUMN_BAD_FORMAT, _name, _style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
void ColumnUDANumeric::render (
|
||||||
|
std::vector <std::string>& lines,
|
||||||
|
Task& task,
|
||||||
|
int width,
|
||||||
|
Color& color)
|
||||||
|
{
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
if (_style == "default")
|
||||||
|
{
|
||||||
|
std::string value = task.get (_name);
|
||||||
|
renderStringRight (lines, width, color, value);
|
||||||
|
}
|
||||||
|
else if (_style == "indicator")
|
||||||
|
{
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
auto indicator = context.config.get ("uda." + _name + ".indicator");
|
||||||
|
if (indicator == "")
|
||||||
|
indicator = "U";
|
||||||
|
|
||||||
|
renderStringRight (lines, width, color, indicator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
ColumnUDADate::ColumnUDADate ()
|
||||||
|
{
|
||||||
|
_name = "<uda>";
|
||||||
|
_type = "date";
|
||||||
|
_style = "default";
|
||||||
|
_label = "";
|
||||||
|
_uda = true;
|
||||||
|
_hyphenate = false;
|
||||||
|
_styles = {_style, "indicator"};
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
bool ColumnUDADate::validate (std::string& value)
|
||||||
|
{
|
||||||
|
// No restrictions.
|
||||||
|
if (_values.size () == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Look for exact match value.
|
||||||
|
for (auto& i : _values)
|
||||||
|
if (i == value)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Fail if not found.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Set the minimum and maximum widths for the value.
|
||||||
|
//
|
||||||
|
void ColumnUDADate::measure (Task& task, unsigned int& minimum, unsigned int& maximum)
|
||||||
|
{
|
||||||
|
minimum = maximum = 0;
|
||||||
|
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
if (_style == "default")
|
||||||
|
{
|
||||||
|
std::string value = task.get (_name);
|
||||||
|
if (value != "")
|
||||||
|
{
|
||||||
|
// Determine the output date format, which uses a hierarchy of definitions.
|
||||||
|
// rc.report.<report>.dateformat
|
||||||
|
// rc.dateformat.report
|
||||||
|
// rc.dateformat
|
||||||
|
ISO8601d date ((time_t) strtol (value.c_str (), NULL, 10));
|
||||||
|
std::string format = context.config.get ("report." + _report + ".dateformat");
|
||||||
|
if (format == "")
|
||||||
|
format = context.config.get ("dateformat.report");
|
||||||
|
if (format == "")
|
||||||
|
format = context.config.get ("dateformat");
|
||||||
|
|
||||||
|
minimum = maximum = ISO8601d::length (format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_style == "indicator")
|
||||||
|
{
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
auto indicator = context.config.get ("uda." + _name + ".indicator");
|
||||||
|
if (indicator == "")
|
||||||
|
indicator = "U";
|
||||||
|
|
||||||
|
minimum = maximum = utf8_width (indicator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
minimum = maximum = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw format (STRING_COLUMN_BAD_FORMAT, _name, _style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
void ColumnUDADate::render (
|
||||||
|
std::vector <std::string>& lines,
|
||||||
|
Task& task,
|
||||||
|
int width,
|
||||||
|
Color& color)
|
||||||
|
{
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
if (_style == "default")
|
||||||
|
{
|
||||||
|
std::string value = task.get (_name);
|
||||||
|
|
||||||
|
// Determine the output date format, which uses a hierarchy of definitions.
|
||||||
|
// rc.report.<report>.dateformat
|
||||||
|
// rc.dateformat.report
|
||||||
|
// rc.dateformat.
|
||||||
|
std::string format = context.config.get ("report." + _report + ".dateformat");
|
||||||
|
if (format == "")
|
||||||
|
{
|
||||||
|
format = context.config.get ("dateformat.report");
|
||||||
|
if (format == "")
|
||||||
|
format = context.config.get ("dateformat");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderStringLeft (lines, width, color, ISO8601d ((time_t) strtol (value.c_str (), NULL, 10)).toString (format));
|
||||||
|
}
|
||||||
|
else if (_style == "indicator")
|
||||||
|
{
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
auto indicator = context.config.get ("uda." + _name + ".indicator");
|
||||||
|
if (indicator == "")
|
||||||
|
indicator = "U";
|
||||||
|
|
||||||
|
renderStringRight (lines, width, color, indicator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
ColumnUDADuration::ColumnUDADuration ()
|
||||||
|
{
|
||||||
|
_name = "<uda>";
|
||||||
|
_type = "duration";
|
||||||
|
_style = "default";
|
||||||
|
_label = "";
|
||||||
|
_uda = true;
|
||||||
|
_hyphenate = false;
|
||||||
|
_styles = {_style, "indicator"};
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
bool ColumnUDADuration::validate (std::string& value)
|
||||||
|
{
|
||||||
|
// No restrictions.
|
||||||
|
if (_values.size () == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Look for exact match value.
|
||||||
|
for (auto& i : _values)
|
||||||
|
if (i == value)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Fail if not found.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Set the minimum and maximum widths for the value.
|
||||||
|
//
|
||||||
|
void ColumnUDADuration::measure (Task& task, unsigned int& minimum, unsigned int& maximum)
|
||||||
|
{
|
||||||
|
minimum = maximum = 0;
|
||||||
|
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
if (_style == "default")
|
||||||
|
{
|
||||||
|
std::string value = task.get (_name);
|
||||||
|
if (value != "")
|
||||||
|
minimum = maximum = ISO8601p (value).format ().length ();
|
||||||
|
}
|
||||||
|
else if (_style == "indicator")
|
||||||
|
{
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
auto indicator = context.config.get ("uda." + _name + ".indicator");
|
||||||
|
if (indicator == "")
|
||||||
|
indicator = "U";
|
||||||
|
|
||||||
|
minimum = maximum = utf8_width (indicator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
minimum = maximum = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw format (STRING_COLUMN_BAD_FORMAT, _name, _style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
void ColumnUDADuration::render (
|
||||||
|
std::vector <std::string>& lines,
|
||||||
|
Task& task,
|
||||||
|
int width,
|
||||||
|
Color& color)
|
||||||
|
{
|
||||||
|
if (task.has (_name))
|
||||||
|
{
|
||||||
|
if (_style == "default")
|
||||||
|
{
|
||||||
|
std::string value = task.get (_name);
|
||||||
|
renderStringRight (lines, width, color, ISO8601p (value).format ());
|
||||||
}
|
}
|
||||||
else if (_style == "indicator")
|
else if (_style == "indicator")
|
||||||
{
|
{
|
||||||
|
|
|
@ -27,12 +27,64 @@
|
||||||
#ifndef INCLUDED_COLUDA
|
#ifndef INCLUDED_COLUDA
|
||||||
#define INCLUDED_COLUDA
|
#define INCLUDED_COLUDA
|
||||||
|
|
||||||
#include <Column.h>
|
#include <ColTypeString.h>
|
||||||
|
#include <ColTypeNumeric.h>
|
||||||
|
#include <ColTypeDate.h>
|
||||||
|
#include <ColTypeDuration.h>
|
||||||
|
|
||||||
class ColumnUDA : public Column
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
class ColumnUDAString : public ColumnTypeString
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ColumnUDA ();
|
ColumnUDAString ();
|
||||||
|
bool validate (std::string&);
|
||||||
|
void measure (Task&, unsigned int&, unsigned int&);
|
||||||
|
void render (std::vector <std::string>&, Task&, int, Color&);
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::vector <std::string> _values;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool _hyphenate;
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
class ColumnUDANumeric : public ColumnTypeNumeric
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ColumnUDANumeric ();
|
||||||
|
bool validate (std::string&);
|
||||||
|
void measure (Task&, unsigned int&, unsigned int&);
|
||||||
|
void render (std::vector <std::string>&, Task&, int, Color&);
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::vector <std::string> _values;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool _hyphenate;
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
class ColumnUDADate : public ColumnTypeDate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ColumnUDADate ();
|
||||||
|
bool validate (std::string&);
|
||||||
|
void measure (Task&, unsigned int&, unsigned int&);
|
||||||
|
void render (std::vector <std::string>&, Task&, int, Color&);
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::vector <std::string> _values;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool _hyphenate;
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
class ColumnUDADuration : public ColumnTypeDuration
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ColumnUDADuration ();
|
||||||
bool validate (std::string&);
|
bool validate (std::string&);
|
||||||
void measure (Task&, unsigned int&, unsigned int&);
|
void measure (Task&, unsigned int&, unsigned int&);
|
||||||
void render (std::vector <std::string>&, Task&, int, Color&);
|
void render (std::vector <std::string>&, Task&, int, Color&);
|
||||||
|
|
|
@ -175,29 +175,49 @@ void Column::uda (std::map <std::string, Column*>& all)
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
Column* Column::uda (const std::string& name)
|
Column* Column::uda (const std::string& name)
|
||||||
{
|
{
|
||||||
ColumnUDA* c = new ColumnUDA ();
|
auto type = context.config.get ("uda." + name + ".type");
|
||||||
c->_name = name;
|
auto label = context.config.get ("uda." + name + ".label");
|
||||||
|
auto values = context.config.get ("uda." + name + ".values");
|
||||||
|
|
||||||
std::string key = "uda." + name + ".type";
|
if (type == "string")
|
||||||
c->_type = context.config.get (key);
|
{
|
||||||
if (c->_type == "")
|
auto c = new ColumnUDAString ();
|
||||||
context.error (format (STRING_UDA_TYPE_MISSING, name));
|
c->_name = name;
|
||||||
|
c->_label = label;
|
||||||
|
if (values != "")
|
||||||
|
split (c->_values, values, ',');
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
else if (type == "numeric")
|
||||||
|
{
|
||||||
|
auto c = new ColumnUDANumeric ();
|
||||||
|
c->_name = name;
|
||||||
|
c->_label = label;
|
||||||
|
if (values != "")
|
||||||
|
split (c->_values, values, ',');
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
else if (type == "date")
|
||||||
|
{
|
||||||
|
auto c = new ColumnUDADate ();
|
||||||
|
c->_name = name;
|
||||||
|
c->_label = label;
|
||||||
|
if (values != "")
|
||||||
|
split (c->_values, values, ',');
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
else if (type == "duration")
|
||||||
|
{
|
||||||
|
auto c = new ColumnUDADuration ();
|
||||||
|
c->_name = name;
|
||||||
|
c->_label = label;
|
||||||
|
if (values != "")
|
||||||
|
split (c->_values, values, ',');
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
if (c->_type != "string" &&
|
throw std::string (STRING_UDA_TYPE);
|
||||||
c->_type != "date" &&
|
return NULL;
|
||||||
c->_type != "duration" &&
|
|
||||||
c->_type != "numeric")
|
|
||||||
context.error (STRING_UDA_TYPE);
|
|
||||||
|
|
||||||
key = "uda." + name + ".label";
|
|
||||||
if (context.config.get (key) != "")
|
|
||||||
c->_label = context.config.get (key);
|
|
||||||
|
|
||||||
key = "uda." + name + ".values";
|
|
||||||
if (context.config.get (key) != "")
|
|
||||||
split (c->_values, context.config.get (key), ',');
|
|
||||||
|
|
||||||
return c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue