Plugins support (#592)
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
templates/invoice/renderer/.~lock*
|
templates/invoice/renderer/.~lock*
|
||||||
|
|
||||||
/var/data/*.sqlite
|
/var/data/*
|
||||||
/var/data/*.sqlite-journal
|
!/var/data/.gitkeep
|
||||||
!/var/data/kimai_test.sqlite
|
!/var/data/kimai_test.sqlite
|
||||||
/var/coverage/
|
/var/coverage/
|
||||||
/var/cache/*
|
/var/cache/*
|
||||||
@@ -17,6 +17,9 @@ templates/invoice/renderer/.~lock*
|
|||||||
|
|
||||||
/var/invoices/*
|
/var/invoices/*
|
||||||
|
|
||||||
|
/var/plugins/*
|
||||||
|
!var/plugins/.gitkeep
|
||||||
|
|
||||||
/var/log/*
|
/var/log/*
|
||||||
!var/log/.gitkeep
|
!var/log/.gitkeep
|
||||||
|
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Kimai 2 - Time-tracking made easy
|
# Kimai 2 - Time-tracking made easy
|
||||||
|
|
||||||
Kimai - the open source time-tracker application with a mobile-first approach (read more at the [official website](http://v2.kimai.org)).
|
Kimai - the open source time-tracker application with a mobile-first approach (read more at the [official website](https://www.kimai.org)).
|
||||||
|
|
||||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||||
@@ -30,53 +30,51 @@ Kimai is a [multi-language application](var/docs/translations.md) and already tr
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Looking for more information about Kimai 2? Check out our detailed [documentation](var/docs/).
|
Looking for more information about Kimai 2? Check out our detailed [documentation](https://www.kimai.org/documentation/).
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
There are multiple ways to install Kimai, all of them described in the [installation docu](var/docs/installation.md):
|
There are multiple ways to install Kimai, all of them described in the [installation docu](https://www.kimai.org/documentation/installation.html):
|
||||||
|
|
||||||
- [Recommended installation with GIT and Composer](var/docs/installation.md#recommended-setup)
|
- [Recommended installation with GIT and Composer](https://www.kimai.org/documentation/installation.html#recommended-setup)
|
||||||
- [Development setup](var/docs/installation.md#development-installation)
|
- [Development setup](https://www.kimai.org/documentation/installation.html#development-installation)
|
||||||
- [Docker](var/docs/docker.md)
|
- [Docker](https://www.kimai.org/documentation/docker.html)
|
||||||
- [1-click installations](var/docs/installation.md#hosting-and-1-click-installations)
|
- [1-click installations](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations)
|
||||||
- [FTP](var/docs/installation.md#ftp-installation)
|
- [FTP](https://www.kimai.org/documentation/installation.html#ftp-installation)
|
||||||
|
|
||||||
### Updating Kimai
|
### Updating Kimai
|
||||||
|
|
||||||
Read the following documentations before you start your upgrade:
|
Read the following documentations before you start your upgrade:
|
||||||
|
|
||||||
- The [update documentation](var/docs/updates.md)
|
- The [update documentation](https://www.kimai.org/documentation/updates.html)
|
||||||
- The version specific [UPGRADING guide](UPGRADING.md)
|
- The version specific [UPGRADING guide](UPGRADING.md)
|
||||||
- The [release information](https://github.com/kevinpapst/kimai2/releases)
|
- The [release information](https://github.com/kevinpapst/kimai2/releases)
|
||||||
|
|
||||||
## Roadmap and releases
|
## Roadmap and releases
|
||||||
|
|
||||||
You can see our development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
|
You can see a rough development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
|
||||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome!
|
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome!
|
||||||
|
|
||||||
> Kimai 2 uses a rolling release concept for delivering updates.
|
> Kimai 2 uses a rolling release concept for delivering updates.
|
||||||
> You can upgrade Kimai at any time, you don't need to wait for the next official release. Read the [upgrade docs](UPGRADING.md) first!
|
> You can upgrade Kimai at any time, you don't need to wait for the next official release. Read the [upgrade docs](UPGRADING.md) first!
|
||||||
|
|
||||||
Release versions will be created on a regular base and you can use these tags if you are familiar with Git,
|
Release versions will be created on a regular base (approx. 1 per month) and you can use these tags if you are familiar with Git,
|
||||||
but we will not provide support for any specific version.
|
but we cannot provide support for any specific version (especially older ones).
|
||||||
Every code change, whether it's a new feature or a bug fix, will be done on the master branch.
|
Every code change, whether it's a new feature or a bug fix, will be done on the master branch.
|
||||||
I have to do it this way, as I develop Kimai in my free time and want to put most
|
I have to do it this way, as I develop Kimai in my free time and want to put most
|
||||||
effort into the software instead of backporting changes for old versions.
|
effort into the software instead of backporting changes for old versions.
|
||||||
|
|
||||||
## Extensions for Kimai 2
|
## Plugins for Kimai 2
|
||||||
|
|
||||||
As Kimai 2 was built on top of Symfony, it can be extended like every other Symfony application.
|
Kimai 2 was built on top of the Symfony framework and can be extended in various ways:
|
||||||
We call these extensions bundles, but you might also know them as add-ons, extensions or plugins.
|
|
||||||
|
|
||||||
All available Kimai 2 bundles can be found at the [Kimai recipes](https://github.com/kimai/recipes) repository.
|
- Users looking for existing plugins go to our [plugin marketplace](https://www.kimai.org/store/)
|
||||||
|
- Developer start with the [developer documentation](https://www.kimai.org/documentation/developers.html)
|
||||||
|
|
||||||
## Developer
|
## Credits
|
||||||
|
|
||||||
Kimai 2 is developed with modern frameworks like [Symfony v4](https://github.com/symfony/symfony), [Doctrine](https://github.com/doctrine/),
|
Kimai 2 is developed with modern frameworks like
|
||||||
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/) and [many](composer.json) [more](package.json).
|
[Symfony v4](https://github.com/symfony/symfony),
|
||||||
|
[Doctrine](https://github.com/doctrine/),
|
||||||
If you want to start developing for Kimai 2, please read the following documentation:
|
[AdminLTEBundle](https://github.com/kevinpapst/AdminLTEBundle/) (based on [AdminLTE theme](https://github.com/almasaeed2010/AdminLTE)) and
|
||||||
|
[many](composer.json) [more](package.json).
|
||||||
- an example on how to extend Kimai 2 can be found in this [GitHub repository](https://github.com/kevinpapst/kimai2-invoice)
|
|
||||||
- the [developer documentation](var/docs/developers.md) is available both on GitHub and your local installation
|
|
||||||
|
|||||||
@@ -68,7 +68,8 @@
|
|||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\": "src/"
|
"App\\": "src/",
|
||||||
|
"KimaiPlugin\\": "var/plugins/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
|
|||||||
67
src/Controller/EventController.php
Normal file
67
src/Controller/EventController.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the AdminLTE bundle.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Event\ThemeEvent;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class EventController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var EventDispatcherInterface
|
||||||
|
*/
|
||||||
|
protected $eventDispatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param EventDispatcherInterface $dispatcher
|
||||||
|
*/
|
||||||
|
public function __construct(EventDispatcherInterface $dispatcher)
|
||||||
|
{
|
||||||
|
$this->eventDispatcher = $dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return EventDispatcherInterface
|
||||||
|
*/
|
||||||
|
protected function getDispatcher()
|
||||||
|
{
|
||||||
|
return $this->eventDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $eventName
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function hasListener($eventName)
|
||||||
|
{
|
||||||
|
return $this->getDispatcher()->hasListeners($eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param string $eventName
|
||||||
|
* @return ThemeEvent|Response
|
||||||
|
*/
|
||||||
|
public function trigger(Request $request, string $event)
|
||||||
|
{
|
||||||
|
if (!$this->hasListener($event)) {
|
||||||
|
return new Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$themeEvent = new ThemeEvent($request, $this->getUser());
|
||||||
|
$this->getDispatcher()->dispatch($event, $themeEvent);
|
||||||
|
|
||||||
|
return new Response($themeEvent->getContent());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,10 +32,10 @@ class CustomerFixtures extends Fixture
|
|||||||
public const MAX_BUDGET = 100000;
|
public const MAX_BUDGET = 100000;
|
||||||
public const MIN_GLOBAL_ACTIVITIES = 5;
|
public const MIN_GLOBAL_ACTIVITIES = 5;
|
||||||
public const MAX_GLOBAL_ACTIVITIES = 50;
|
public const MAX_GLOBAL_ACTIVITIES = 50;
|
||||||
public const MIN_PROJECTS_PER_CUSTOMER = 10;
|
public const MIN_PROJECTS_PER_CUSTOMER = 1;
|
||||||
public const MAX_PROJECTS_PER_CUSTOMER = 50;
|
public const MAX_PROJECTS_PER_CUSTOMER = 25;
|
||||||
public const MIN_ACTIVITIES_PER_PROJECT = 0;
|
public const MIN_ACTIVITIES_PER_PROJECT = 0;
|
||||||
public const MAX_ACTIVITIES_PER_PROJECT = 15;
|
public const MAX_ACTIVITIES_PER_PROJECT = 25;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ObjectManager $manager
|
* @param ObjectManager $manager
|
||||||
@@ -46,18 +46,18 @@ class CustomerFixtures extends Fixture
|
|||||||
|
|
||||||
$amountCustomers = rand(self::MIN_CUSTOMERS, self::MAX_CUSTOMERS);
|
$amountCustomers = rand(self::MIN_CUSTOMERS, self::MAX_CUSTOMERS);
|
||||||
for ($c = 1; $c <= $amountCustomers; $c++) {
|
for ($c = 1; $c <= $amountCustomers; $c++) {
|
||||||
$visibleCustomer = 0 != $c % 3;
|
$visibleCustomer = 0 != $c % 5;
|
||||||
$customer = $this->createCustomer($faker, $visibleCustomer);
|
$customer = $this->createCustomer($faker, $visibleCustomer);
|
||||||
|
|
||||||
$projectForCustomer = rand(self::MIN_PROJECTS_PER_CUSTOMER, self::MAX_PROJECTS_PER_CUSTOMER);
|
$projectForCustomer = rand(self::MIN_PROJECTS_PER_CUSTOMER, self::MAX_PROJECTS_PER_CUSTOMER);
|
||||||
for ($p = 1; $p <= $projectForCustomer; $p++) {
|
for ($p = 1; $p <= $projectForCustomer; $p++) {
|
||||||
$visibleProject = 0 != $p % 3;
|
$visibleProject = 0 != $p % 7;
|
||||||
$project = $this->createProject($faker, $customer, $visibleProject);
|
$project = $this->createProject($faker, $customer, $visibleProject);
|
||||||
$manager->persist($project);
|
$manager->persist($project);
|
||||||
|
|
||||||
$activityForProject = rand(self::MIN_ACTIVITIES_PER_PROJECT, self::MAX_ACTIVITIES_PER_PROJECT);
|
$activityForProject = rand(self::MIN_ACTIVITIES_PER_PROJECT, self::MAX_ACTIVITIES_PER_PROJECT);
|
||||||
for ($a = 1; $a <= $activityForProject; $a++) {
|
for ($a = 1; $a <= $activityForProject; $a++) {
|
||||||
$visibleActivity = 0 != $a % 3;
|
$visibleActivity = 0 != $a % 6;
|
||||||
$activity = $this->createActivity($faker, $project, $visibleActivity);
|
$activity = $this->createActivity($faker, $project, $visibleActivity);
|
||||||
$manager->persist($activity);
|
$manager->persist($activity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
|||||||
$config = [];
|
$config = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// safe alternatives to %kernel.project_dir%
|
||||||
|
$container->setParameter('kimai.data_dir', $container->getParameter('kernel.project_dir') . '/var/data');
|
||||||
|
$container->setParameter('kimai.plugin_dir', $container->getParameter('kernel.project_dir') . '/var/plugins');
|
||||||
|
|
||||||
$container->setParameter('kimai.languages', $config['languages']);
|
$container->setParameter('kimai.languages', $config['languages']);
|
||||||
$container->setParameter('kimai.calendar', $config['calendar']);
|
$container->setParameter('kimai.calendar', $config['calendar']);
|
||||||
$container->setParameter('kimai.dashboard', $config['dashboard']);
|
$container->setParameter('kimai.dashboard', $config['dashboard']);
|
||||||
|
|||||||
78
src/Event/ThemeEvent.php
Normal file
78
src/Event/ThemeEvent.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
class ThemeEvent extends Event
|
||||||
|
{
|
||||||
|
public const JAVASCRIPT = 'app.theme.javascript';
|
||||||
|
public const STYLESHEET = 'app.theme.css';
|
||||||
|
public const HTML_HEAD = 'app.theme.html_head';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*/
|
||||||
|
protected $user;
|
||||||
|
/**
|
||||||
|
* @var Request
|
||||||
|
*/
|
||||||
|
protected $request;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $content = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param User $user
|
||||||
|
*/
|
||||||
|
public function __construct(Request $request, User $user)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return User
|
||||||
|
*/
|
||||||
|
public function getUser()
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Request
|
||||||
|
*/
|
||||||
|
public function getRequest(): Request
|
||||||
|
{
|
||||||
|
return $this->request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getContent(): string
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $content
|
||||||
|
* @return ThemeEvent
|
||||||
|
*/
|
||||||
|
public function addContent(string $content)
|
||||||
|
{
|
||||||
|
$this->content .= $content;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
|||||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
use Symfony\Component\Finder\SplFileInfo;
|
||||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||||
use Symfony\Component\Routing\RouteCollectionBuilder;
|
use Symfony\Component\Routing\RouteCollectionBuilder;
|
||||||
|
|
||||||
@@ -64,6 +66,30 @@ class Kernel extends BaseKernel
|
|||||||
yield new $class();
|
yield new $class();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pluginsDir = $this->getProjectDir() . '/var/plugins';
|
||||||
|
if (!file_exists($pluginsDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finder = new Finder();
|
||||||
|
$finder->ignoreUnreadableDirs()->directories()->name('*Bundle');
|
||||||
|
/** @var SplFileInfo $bundleDir */
|
||||||
|
foreach ($finder->in($pluginsDir) as $bundleDir) {
|
||||||
|
$bundleName = $bundleDir->getRelativePathname();
|
||||||
|
|
||||||
|
$bundleFilename = $bundleDir->getRealPath() . '/' . $bundleName . '.php';
|
||||||
|
if (!file_exists($bundleFilename)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pluginClass = 'KimaiPlugin\\' . $bundleName . '\\' . $bundleName;
|
||||||
|
if (!class_exists($pluginClass)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield new $pluginClass;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
|
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
|
||||||
@@ -106,6 +132,10 @@ class Kernel extends BaseKernel
|
|||||||
|
|
||||||
// load application routes
|
// load application routes
|
||||||
$routes->import($confDir . '/routes' . self::CONFIG_EXTS, '/', 'glob');
|
$routes->import($confDir . '/routes' . self::CONFIG_EXTS, '/', 'glob');
|
||||||
|
|
||||||
|
// load plugin routes
|
||||||
|
$pluginsDir = $this->getProjectDir() . '/var/plugins';
|
||||||
|
$routes->import($pluginsDir . '/*Bundle/Resources/config/routes'. self::CONFIG_EXTS, '/', 'glob');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configureFosUserRoutes(RouteCollectionBuilder $routes)
|
protected function configureFosUserRoutes(RouteCollectionBuilder $routes)
|
||||||
|
|||||||
@@ -42,14 +42,14 @@
|
|||||||
{% if entry.project and entry.project.customer %}
|
{% if entry.project and entry.project.customer %}
|
||||||
<a href="{{ path('admin_customer_edit', {'id' : entry.project.customer.id}) }}">{{ widgets.label_customer(entry.project.customer) }}</a>
|
<a href="{{ path('admin_customer_edit', {'id' : entry.project.customer.id}) }}">{{ widgets.label_customer(entry.project.customer) }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{#{ widgets.label('x', 'success') }#}
|
{# GLOBAL ACTIVTITY DOES NOT HAVE A CUSTOMER #}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
||||||
{% if entry.project %}
|
{% if entry.project %}
|
||||||
<a href="{{ path('admin_project_edit', {'id' : entry.project.id}) }}">{{ widgets.label_project(entry.project) }}</a>
|
<a href="{{ path('admin_project_edit', {'id' : entry.project.id}) }}">{{ widgets.label_project(entry.project) }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{#{ widgets.label('x', 'success') }#}
|
{# GLOBAL ACTIVTITY DOES NOT HAVE A PROJECT #}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
|
||||||
|
|||||||
@@ -89,12 +89,14 @@
|
|||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
|
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
|
||||||
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
|
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
|
||||||
|
{{ render(controller('App\\Controller\\EventController::trigger', {'request':app.request, 'event':constant('App\\Event\\ThemeEvent::STYLESHEET')})) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
<script src="{{ asset('build/app.js') }}"></script>
|
<script src="{{ asset('build/app.js') }}"></script>
|
||||||
{% include 'partials/head.html.twig' %}
|
{% include 'partials/head.html.twig' %}
|
||||||
|
{{ render(controller('App\\Controller\\EventController::trigger', {'request':app.request, 'event':constant('App\\Event\\ThemeEvent::HTML_HEAD')})) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
@@ -119,4 +121,5 @@
|
|||||||
{# $.kimai.pauseRecord('li.messages-menu ul.menu li'); #}
|
{# $.kimai.pauseRecord('li.messages-menu ul.menu li'); #}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
{{ render(controller('App\\Controller\\EventController::trigger', {'request':app.request, 'event':constant('App\\Event\\ThemeEvent::JAVASCRIPT')})) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
1
var/plugins/.gitkeep
Normal file
1
var/plugins/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This directory can store as Kimai plugin/bundle folder.
|
||||||
Reference in New Issue
Block a user