allow markdown in timesheet descriptions (#296)
This commit is contained in:
@@ -28,3 +28,11 @@
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&.timesheet-description {
|
||||
& >ul {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ kimai:
|
||||
# All configs related to timesheet and record management
|
||||
timesheet:
|
||||
|
||||
# render timesheet descriptions with markdown
|
||||
markdown_content: false
|
||||
|
||||
# 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.
|
||||
duration_only: false
|
||||
|
||||
@@ -54,6 +54,10 @@ services:
|
||||
arguments:
|
||||
$dashboard: "%kimai.dashboard%"
|
||||
|
||||
App\Twig\MarkdownExtension:
|
||||
arguments:
|
||||
$timesheetAsMarkdown: "%kimai.timesheet.markdown%"
|
||||
|
||||
# ================================================================================
|
||||
# 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.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/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
||||
"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.rounding', $config['timesheet']['rounding']);
|
||||
$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')
|
||||
->defaultValue(false)
|
||||
->end()
|
||||
->booleanNode('markdown_content')
|
||||
->defaultValue(false)
|
||||
->end()
|
||||
->arrayNode('rounding')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
|
||||
@@ -21,14 +21,19 @@ class MarkdownExtension extends \Twig_Extension
|
||||
* @var Markdown
|
||||
*/
|
||||
private $markdown;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $timesheetIsMarkdown = false;
|
||||
|
||||
/**
|
||||
* MarkdownExtension constructor.
|
||||
* @param Markdown $parser
|
||||
*/
|
||||
public function __construct(Markdown $parser)
|
||||
public function __construct(Markdown $parser, bool $timesheetAsMarkdown = false)
|
||||
{
|
||||
$this->markdown = $parser;
|
||||
$this->timesheetIsMarkdown = $timesheetAsMarkdown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,9 +43,25 @@ class MarkdownExtension extends \Twig_Extension
|
||||
{
|
||||
return [
|
||||
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
|
||||
*
|
||||
@@ -49,6 +70,6 @@ class MarkdownExtension extends \Twig_Extension
|
||||
*/
|
||||
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 bool $safe
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<a href="{{ path('admin_activity_edit', {'id': entry.activity.id}) }}">{{ widgets.label_activity(entry.activity) }}</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>
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{% 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, 'description') }}">{{ entry.description|nl2br }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">{{ entry.description|default('')|desc2html }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})} %}
|
||||
|
||||
|
||||
@@ -22,8 +22,9 @@ class MarkdownExtensionTest extends TestCase
|
||||
{
|
||||
$sut = new MarkdownExtension(new Markdown());
|
||||
$filters = $sut->getFilters();
|
||||
$this->assertCount(1, $filters);
|
||||
$this->assertCount(2, $filters);
|
||||
$this->assertEquals('md2html', $filters[0]->getName());
|
||||
$this->assertEquals('desc2html', $filters[1]->getName());
|
||||
}
|
||||
|
||||
public function testMarkdownToHtml()
|
||||
@@ -32,4 +33,20 @@ class MarkdownExtensionTest extends TestCase
|
||||
$this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*'));
|
||||
$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)
|
||||
|
||||
### 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
|
||||
|
||||
Kimai supports two modes for displaying and recording timesheet entries:
|
||||
|
||||
Reference in New Issue
Block a user