allow markdown in timesheet descriptions (#296)
This commit is contained in:
@@ -28,3 +28,11 @@
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
&.timesheet-description {
|
||||||
|
& >ul {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@ kimai:
|
|||||||
# All configs related to timesheet and record management
|
# All configs related to timesheet and record management
|
||||||
timesheet:
|
timesheet:
|
||||||
|
|
||||||
|
# render timesheet descriptions with markdown
|
||||||
|
markdown_content: false
|
||||||
|
|
||||||
# Whether we display start and end time columns (false) or durations only (true).
|
# Whether we display start and end time columns (false) or durations only (true).
|
||||||
# Setting this to true will also change the "edit timesheet" forms, more infos available in the configurations docu.
|
# Setting this to true will also change the "edit timesheet" forms, more infos available in the configurations docu.
|
||||||
duration_only: false
|
duration_only: false
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ services:
|
|||||||
arguments:
|
arguments:
|
||||||
$dashboard: "%kimai.dashboard%"
|
$dashboard: "%kimai.dashboard%"
|
||||||
|
|
||||||
|
App\Twig\MarkdownExtension:
|
||||||
|
arguments:
|
||||||
|
$timesheetAsMarkdown: "%kimai.timesheet.markdown%"
|
||||||
|
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
# DATABASE
|
# DATABASE
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build/app.js": "/build/app.js?a67489c4fc07f30e6f86",
|
"build/app.js": "/build/app.js?a67489c4fc07f30e6f86",
|
||||||
"build/app.css": "/build/app.css?e2bd74ce6119917ab0c8fd4de8d4b9c6",
|
"build/app.css": "/build/app.css?7ddba25b9e0ab7dec9690f128f8691c0",
|
||||||
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
|
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
|
||||||
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
||||||
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",
|
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
|||||||
$container->setParameter('kimai.timesheet.rates', $config['timesheet']['rates']);
|
$container->setParameter('kimai.timesheet.rates', $config['timesheet']['rates']);
|
||||||
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
|
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
|
||||||
$container->setParameter('kimai.timesheet.duration_only', $config['timesheet']['duration_only']);
|
$container->setParameter('kimai.timesheet.duration_only', $config['timesheet']['duration_only']);
|
||||||
|
$container->setParameter('kimai.timesheet.markdown', $config['timesheet']['markdown_content']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ class Configuration implements ConfigurationInterface
|
|||||||
->booleanNode('duration_only')
|
->booleanNode('duration_only')
|
||||||
->defaultValue(false)
|
->defaultValue(false)
|
||||||
->end()
|
->end()
|
||||||
|
->booleanNode('markdown_content')
|
||||||
|
->defaultValue(false)
|
||||||
|
->end()
|
||||||
->arrayNode('rounding')
|
->arrayNode('rounding')
|
||||||
->requiresAtLeastOneElement()
|
->requiresAtLeastOneElement()
|
||||||
->useAttributeAsKey('key')
|
->useAttributeAsKey('key')
|
||||||
|
|||||||
@@ -21,14 +21,19 @@ class MarkdownExtension extends \Twig_Extension
|
|||||||
* @var Markdown
|
* @var Markdown
|
||||||
*/
|
*/
|
||||||
private $markdown;
|
private $markdown;
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $timesheetIsMarkdown = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MarkdownExtension constructor.
|
* MarkdownExtension constructor.
|
||||||
* @param Markdown $parser
|
* @param Markdown $parser
|
||||||
*/
|
*/
|
||||||
public function __construct(Markdown $parser)
|
public function __construct(Markdown $parser, bool $timesheetAsMarkdown = false)
|
||||||
{
|
{
|
||||||
$this->markdown = $parser;
|
$this->markdown = $parser;
|
||||||
|
$this->timesheetIsMarkdown = $timesheetAsMarkdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,9 +43,25 @@ class MarkdownExtension extends \Twig_Extension
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new TwigFilter('md2html', [$this, 'markdownToHtml'], ['is_safe' => ['html']]),
|
new TwigFilter('md2html', [$this, 'markdownToHtml'], ['is_safe' => ['html']]),
|
||||||
|
new TwigFilter('desc2html', [$this, 'timesheetContent'], ['is_safe' => ['html']]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the timesheet description content into HTML.
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function timesheetContent(string $content): string
|
||||||
|
{
|
||||||
|
if ($this->timesheetIsMarkdown) {
|
||||||
|
return $this->markdown->toHtml($content, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nl2br($content);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms the given Markdown content into HTML
|
* Transforms the given Markdown content into HTML
|
||||||
*
|
*
|
||||||
@@ -49,6 +70,6 @@ class MarkdownExtension extends \Twig_Extension
|
|||||||
*/
|
*/
|
||||||
public function markdownToHtml(string $content): string
|
public function markdownToHtml(string $content): string
|
||||||
{
|
{
|
||||||
return $this->markdown->toHtml($content);
|
return $this->markdown->toHtml($content, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,12 @@ class Markdown
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $text
|
* @param string $text
|
||||||
|
* @param bool $safe
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function toHtml(string $text): string
|
public function toHtml(string $text, bool $safe = true): string
|
||||||
{
|
{
|
||||||
|
$this->parser->setSafeMode($safe);
|
||||||
return $this->parser->text($text);
|
return $this->parser->text($text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<a href="{{ path('admin_activity_edit', {'id': entry.activity.id}) }}">{{ widgets.label_activity(entry.activity) }}</a>
|
<a href="{{ path('admin_activity_edit', {'id': entry.activity.id}) }}">{{ widgets.label_activity(entry.activity) }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}"><a href="{{ path('user_profile'|route_alias, {'username' : entry.user.username}) }}">{{ widgets.label_user(entry.user) }}</a></td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}"><a href="{{ path('user_profile'|route_alias, {'username' : entry.user.username}) }}">{{ widgets.label_user(entry.user) }}</a></td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ entry.description }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ entry.description|nl2br }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% set actionButtons = {} %}
|
{% set actionButtons = {} %}
|
||||||
{% if is_granted('edit', entry) %}
|
{% if is_granted('edit', entry) %}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ entry.description|nl2br }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">{{ entry.description|default('')|desc2html }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})} %}
|
{% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})} %}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ class MarkdownExtensionTest extends TestCase
|
|||||||
{
|
{
|
||||||
$sut = new MarkdownExtension(new Markdown());
|
$sut = new MarkdownExtension(new Markdown());
|
||||||
$filters = $sut->getFilters();
|
$filters = $sut->getFilters();
|
||||||
$this->assertCount(1, $filters);
|
$this->assertCount(2, $filters);
|
||||||
$this->assertEquals('md2html', $filters[0]->getName());
|
$this->assertEquals('md2html', $filters[0]->getName());
|
||||||
|
$this->assertEquals('desc2html', $filters[1]->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMarkdownToHtml()
|
public function testMarkdownToHtml()
|
||||||
@@ -32,4 +33,20 @@ class MarkdownExtensionTest extends TestCase
|
|||||||
$this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*'));
|
$this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*'));
|
||||||
$this->assertEquals('<h1>foobar</h1>', $sut->markdownToHtml('# foobar'));
|
$this->assertEquals('<h1>foobar</h1>', $sut->markdownToHtml('# foobar'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTimesheetContent()
|
||||||
|
{
|
||||||
|
$sut = new MarkdownExtension(new Markdown(), false);
|
||||||
|
$this->assertEquals(
|
||||||
|
"- test<br />\n- foo",
|
||||||
|
$sut->timesheetContent("- test\n- foo")
|
||||||
|
);
|
||||||
|
|
||||||
|
$sut = new MarkdownExtension(new Markdown(), true);
|
||||||
|
$this->assertEquals(
|
||||||
|
"<ul>\n<li>test</li>\n<li>foo</li>\n</ul>\n<p>foo <strong>bar</strong></p>",
|
||||||
|
$sut->timesheetContent("- test\n- foo\n\nfoo __bar__")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,16 @@ admin_lte:
|
|||||||
|
|
||||||
## Timesheets (kimai.yaml)
|
## Timesheets (kimai.yaml)
|
||||||
|
|
||||||
|
### Descriptions with Markdown
|
||||||
|
|
||||||
|
The description for every timesheet entry can be formatted in two different ways, configured with the `markdown_content` setting.
|
||||||
|
|
||||||
|
- `false` - simple newlines in the description box will be displayed in the frontend as well (default)
|
||||||
|
- `true` - description will be rendered with a markdown engine, supporting simple lists and other HTML content
|
||||||
|
|
||||||
|
Allowing Markdown in timesheet descriptions is beautiful, but also could be a [security risk](https://github.com/erusev/parsedown/blob/master/README.md#security).
|
||||||
|
Kimai will only apply the markdown in the user timesheet and not in the admin section as additional security measure.
|
||||||
|
|
||||||
### Duration only
|
### Duration only
|
||||||
|
|
||||||
Kimai supports two modes for displaying and recording timesheet entries:
|
Kimai supports two modes for displaying and recording timesheet entries:
|
||||||
|
|||||||
Reference in New Issue
Block a user