improved play and stop button (#2692)

* improved play and stop button if only one record is allowed
* stop button first in action menu
* fix skin colors
* autofocus term field in search form
* fix responsive column layout in invoice listing
* improve responsive header for small screens
This commit is contained in:
Kevin Papst
2021-07-30 19:57:57 +02:00
committed by GitHub
parent 37e02365c7
commit df62c8a428
19 changed files with 353 additions and 245 deletions

View File

@@ -24,18 +24,14 @@ export default class KimaiActiveRecords extends KimaiPlugin {
}
init() {
const menu = document.querySelector(this.selector);
this.menu = document.querySelector(this.selector);
// the menu can be hidden if user has no permissions to see it
if (menu === null) {
if (this.menu === null) {
return;
}
const dropdown = menu.querySelector('ul.dropdown-menu');
this.attributes = dropdown.dataset;
this.itemList = dropdown.querySelector('li > ul.menu');
this.label = menu.querySelector('a > span.label');
this.attributes = this.menu.dataset;
const self = this;
const handle = function() { self.reloadActiveRecords(); };
@@ -50,15 +46,10 @@ export default class KimaiActiveRecords extends KimaiPlugin {
document.addEventListener('kimai.customerDelete', handle);
}
emptyList() {
this.itemList.innerHTML = '';
}
_toggleMenu(hasEntries) {
const menu = document.querySelector(this.selector);
const menuEmpty = document.querySelector(this.selectorEmpty);
this.menu.style.display = hasEntries ? 'inline-block' : 'none';
menu.style.display = hasEntries ? 'inline-block' : 'none';
const menuEmpty = document.querySelector(this.selectorEmpty);
if (menuEmpty !== null) {
menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none';
}
@@ -67,42 +58,52 @@ export default class KimaiActiveRecords extends KimaiPlugin {
setEntries(entries) {
this._toggleMenu(entries.length > 0);
const template = this.menu.querySelector('[data-template="active-record"]');
const label = this.menu.querySelector('a > span.label');
if (label !== null) {
label.innerText = entries.length === 0 ? '' : entries.length;
}
if (entries.length === 0) {
this.label.innerText = '';
this.emptyList();
return;
}
let htmlToInsert = '';
const durations = this.getContainer().getPlugin('timesheet-duration');
for (let timesheet of entries) {
htmlToInsert +=
`<li>` +
`<a href="${ this.attributes['href'].replace('000', timesheet.id) }" data-event="kimai.timesheetStop kimai.timesheetUpdate" class="api-link" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">` +
`<div class="pull-left">` +
`<i class="${ this.attributes['icon'] } fa-2x"></i>` +
`</div>` +
`<h4>` +
`<span>${ timesheet.activity.name }</span>` +
`<small>` +
`<span data-title="true" data-since="${ timesheet.begin }">${ durations.formatDuration(timesheet.duration) }</span>` +
`</small>` +
`</h4>` +
`<p>${ timesheet.project.name } (${ timesheet.project.customer.name })</p>` +
`</a>` +
`</li>`;
}
if (this.label.dataset.warning < entries.length) {
this.label.classList = 'label label-danger';
if (template === null) {
this._replaceInNode(this.menu, entries[0]);
} else {
this.label.classList = 'label label-warning';
}
this.label.innerText = entries.length;
this.itemList.innerHTML = htmlToInsert;
const container = template.parentElement;
container.innerHTML = '';
durations.updateRecords();
for (let timesheet of entries) {
const newNode = template.cloneNode(true);
container.appendChild(this._replaceInNode(newNode, timesheet));
}
}
this.getContainer().getPlugin('timesheet-duration').updateRecords();
}
_replaceInNode(node, timesheet) {
const date = this.getContainer().getPlugin('date');
const allReplacer = node.querySelectorAll('[data-replacer]');
for (let node of allReplacer) {
const replacerName = node.dataset['replacer'];
if (replacerName === 'url') {
node.href = this.attributes['href'].replace('000', timesheet.id);
} else if (replacerName === 'activity') {
node.innerText = timesheet.activity.name;
} else if (replacerName === 'project') {
node.innerText = timesheet.project.name;
} else if (replacerName === 'customer') {
node.innerText = timesheet.project.customer.name;
} else if (replacerName === 'duration') {
node.dataset['since'] = timesheet.begin;
node.innerText = date.formatDuration(timesheet.duration);
}
}
return node;
}
reloadActiveRecords() {

View File

@@ -31,36 +31,28 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
this.isDirty = false;
this.modal = jQuery('#remote_form_modal');
this.modal.on('hide.bs.modal', function (e) {
if (self.isDirty) {
if (jQuery('#remote_form_modal .modal-body .remote_modal_is_dirty_warning').length === 0) {
const msg = self.getContainer().getTranslation().get('modal.dirty');
jQuery('#remote_form_modal .modal-body').prepend('<p class="'+(self.modal.hasClass('modal-danger') ? 'well well-sm ' : '') + 'text-danger small remote_modal_is_dirty_warning">' + msg + '</p>');
this.modal
.on('hide.bs.modal', function (e) {
if (self.isDirty) {
if (jQuery('#remote_form_modal .modal-body .remote_modal_is_dirty_warning').length === 0) {
const msg = self.getContainer().getTranslation().get('modal.dirty');
jQuery('#remote_form_modal .modal-body').prepend('<p class="'+(self.modal.hasClass('modal-danger') ? 'well well-sm ' : '') + 'text-danger small remote_modal_is_dirty_warning">' + msg + '</p>');
}
e.preventDefault();
return;
}
e.preventDefault();
return;
}
jQuery(self._getFormIdentifier()).off('change', self._isDirtyHandler);
self.isDirty = false;
self.getContainer().getPlugin('event').trigger('modal-hide');
});
this.modal.on('hidden.bs.modal', function () {
// kill all references, so GC can kick in
self.getContainer().getPlugin('form').destroyForm(self._getFormIdentifier());
jQuery('#remote_form_modal .modal-body').replaceWith('');
});
this.modal.on('show.bs.modal', function () {
self.getContainer().getPlugin('event').trigger('modal-show');
});
this.modal.on('shown.bs.modal', function () {
// workaround for autofocus attribute, as the modal "steals" it
let formAutofocus = jQuery(self._getFormIdentifier()).find('[autofocus]');
if (formAutofocus.length < 1) {
formAutofocus = jQuery(self._getFormIdentifier()).find('input[type=text],textarea,select');
}
formAutofocus.filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
});
jQuery(self._getFormIdentifier()).off('change', self._isDirtyHandler);
self.isDirty = false;
self.getContainer().getPlugin('event').trigger('modal-hide');
})
.on('hidden.bs.modal', function () {
// kill all references, so GC can kick in
self.getContainer().getPlugin('form').destroyForm(self._getFormIdentifier());
jQuery('#remote_form_modal .modal-body').replaceWith('');
})
.on('show.bs.modal', function () {
self.getContainer().getPlugin('event').trigger('modal-show');
});
this._addClickHandler(this.selector, function(href) {
self.openUrlInModal(href);

View File

@@ -24,6 +24,30 @@ export default class KimaiThemeInitializer extends KimaiPlugin {
// activate all form plugins
this.getContainer().getPlugin('form').activateForm('.content-wrapper form', 'body');
this.getContainer().getPlugin('form').activateForm('form.searchform', 'body');
this.registerModalAutofocus('#modal_search');
this.registerModalAutofocus('#remote_form_modal');
}
/**
* workaround for autofocus attribute, as the modal "steals" it
*
* @param {string} selector
*/
registerModalAutofocus(selector) {
let modal = jQuery(selector);
if (modal.length === 0) {
return;
}
modal.on('shown.bs.modal', function () {
let form = modal.find('form');
let formAutofocus = form.find('[autofocus]');
if (formAutofocus.length < 1) {
formAutofocus = form.find('input[type=text],textarea,select');
}
formAutofocus.filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
});
}
/**

View File

@@ -15,43 +15,21 @@ $avatar-base-size: 30;
line-height: 1;
position: relative;
font-weight: bold;
font-size: ($avatar-base-size / 2 - 2)+px;
top: ($avatar-base-size / 4 - 1)+px;
font-size: ceil($avatar-base-size / 2 - 2)+px;
top: ceil($avatar-base-size / 4 - 1)+px;
}
.avatar-xs {
width: ($avatar-base-size * 0.75)+px;
height: ($avatar-base-size * 0.75)+px;
.initials {
font-size: ($avatar-base-size * .75 / 2 - 2)+px;
top: ($avatar-base-size * .75 / 4 - 1)+px;
}
}
$avatarSizes: "xs" .75, "sm" 1.25, "md" 1.5, "lg" 2;
.avatar-sm {
width: ($avatar-base-size * 1.25)+px;
height: ($avatar-base-size * 1.25)+px;
.initials {
font-size: ($avatar-base-size * 1.25 / 2 - 2)+px;
top: ($avatar-base-size * 1.25 / 4 - 1)+px;
}
}
@each $avatarSizeName, $avatarBaseSize in $avatarSizes {
.avatar-#{$avatarSizeName} {
width: ceil($avatar-base-size * $avatarBaseSize)+px;
height: ceil($avatar-base-size * $avatarBaseSize)+px;
.avatar-md {
width: ($avatar-base-size * 1.5)+px;
height: ($avatar-base-size * 1.5)+px;
.initials {
font-size: ($avatar-base-size * 1.5 / 2 - 2)+px;
top: ($avatar-base-size * 1.5 / 4 - 1)+px;
}
}
.avatar-lg {
width: ($avatar-base-size * 2)+px;
height: ($avatar-base-size * 2)+px;
.initials {
font-size: ($avatar-base-size * 2 / 2 - 2)+px;
top: ($avatar-base-size * 2 / 4 - 1)+px;
.initials {
font-size: ceil($avatar-base-size * $avatarBaseSize / 2 - 2)+px;
top: ceil($avatar-base-size * $avatarBaseSize / 4 - 1)+px;
}
}
}
@@ -59,11 +37,11 @@ $avatar-base-size: 30;
border-radius: 50%;
border: 3px solid #fff;
.avatar {
width: ($avatar-base-size * 2.75)+px;
height: ($avatar-base-size * 2.75)+px;
width: ceil($avatar-base-size * 2.75)+px;
height: ceil($avatar-base-size * 2.75)+px;
.initials {
font-size: ($avatar-base-size * 2.75 / 2 - 2)+px;
top: ($avatar-base-size * 2.75 / 4 - 1)+px;
font-size: ceil($avatar-base-size * 2.75 / 2 - 2)+px;
top: ceil($avatar-base-size * 2.75 / 4 - 1)+px;
}
}
}

View File

@@ -32,6 +32,9 @@
}
@media (max-width: $screen-md-min) {
.content-header h1 {
font-size: 1.5em;
}
.content-header>.breadcrumb {
margin-top: 0;
}

View File

@@ -5,22 +5,34 @@
* file that was distributed with this source code.
*/
.navbar-custom-menu {
.navbar-nav {
li>.ddt-small {
padding-top: 10px;
padding-bottom: 10px;
}
}
}
.navbar-nav {
img.img-circle {
max-height: 26px;
margin-top: -2px;
}
.avatar {
max-height: 26px;
max-width: 26px;
margin-top: -2px;
max-height: 30px;
max-width: 30px;
height: 30px;
width: 30px;
margin-top: 0;
.initials {
top: 4px;
top: 5px;
left: 0;
}
}
li>a.ddt-large {
padding: 15px 12px 11px 10px;
font-size: 10px;
li>.ddt-small {
padding-top: 10px;
padding-bottom: 10px;
}
li.messages-menu ul.menu li {
a:hover .pull-left i {
@@ -43,17 +55,13 @@
width: unset !important;
}
.user-menu {
margin-right: 10px;
ul {
li {
a {
color: #444;
&:hover {
background-color: #f4f4f4;
i {
color: $blue;
}
}
color: #444;
i {
padding-right: 10px;
}
@@ -64,9 +72,6 @@
}
}
}
.notifications-menu>.dropdown-menu>li .menu>li>a:hover i {
color: $green;
}
}
@media (max-width: $screen-sm-min) {
@@ -80,25 +85,69 @@
padding: 5px 25px 5px 25px
}
}
/*
.ticktac:hover i.running{
color: #4ff131;
@keyframes ticktac-blink {
0% { opacity: 1; }
5% { opacity: 0.95; }
10% { opacity: 0.9; }
15% { opacity: 0.85; }
20% { opacity: 0.8; }
25% { opacity: 0.75; }
30% { opacity: 0.7; }
35% { opacity: 0.65; }
40% { opacity: 0.6; }
45% { opacity: 0.65; }
50% { opacity: 0.7; }
55% { opacity: 0.75; }
60% { opacity: 0.8; }
65% { opacity: 0.85; }
70% { opacity: 0.9; }
75% { opacity: 0.95; }
100% { opacity: 1; }
}
.ticktac-single {
display: block;
position: relative;
padding-left: 15px;
padding-right: 15px;
i {
animation: ticktac-blink 2s step-end infinite;
}
span {
font-size: 1.8em;
line-height: 1em;
padding-left: 5px;
padding-right: 0;
}
&:hover {
background: rgba(0,0,0,.3);
}
}
body.fixed {
.navbar-nav {
.user-menu {
margin-right: 10px;
}
}
}
li.open .ticktac i.running{
color: #4ff131;
@media (max-width: $screen-sm-min) {
.ticktac-single {
padding-left: 5px;
padding-right: 5px;
}
.navbar-custom-menu .navbar-nav li>.ddt-small {
padding-left: 5px;
padding-right: 5px;
}
}
.ticktac i.stopped{
color: #fff;
@media (max-width: $screen-md-min) {
.ticktac-single {
padding-left: 10px;
padding-right: 10px;
}
.navbar-custom-menu .navbar-nav li>.ddt-small {
padding-left: 10px;
padding-right: 10px;
}
}
.ticktac:hover i.stopped{
color: #4ff131;
}
.user-panel>.info {
left: 40px;
padding-top: 0px;
}
*/

View File

@@ -1,26 +1,69 @@
$skins: blue, blue, black, green, purple, red, yellow;
$skins:
"blue" $yellow #fff $yellow $red,
"black" $yellow #000 $green $red,
"green" $yellow #fff $yellow $red,
"purple" $yellow #fff $yellow $red,
"red" $yellow #fff $yellow $yellow,
"yellow" $blue #fff $green $red
;
@each $skinColor in $skins {
@media (max-width: $screen-sm-min) {
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li.divider {
background-color: #ccc;
}
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li a {
color: #444;
}
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li a:hover {
background: #fff;
}
@each $skinColor, $navHighlight, $tickTacColor, $startButton, $stopButton in $skins {
@each $prefix in (".skin-#{$skinColor}", ".skin-#{$skinColor}-light") {
@media (max-width: $screen-sm-min) {
#{$prefix} {
.main-header .navbar .dropdown-menu li.divider {
background-color: #ccc;
}
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li.divider {
background-color: #ccc;
.main-header .navbar .dropdown-menu li a {
color: #444;
}
.main-header .navbar .dropdown-menu li a:hover {
background: #fff;
}
}
}
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li a {
color: #444;
}
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li a:hover {
background: #fff;
#{$prefix} {
.navbar-nav {
.user-menu {
ul {
li {
a {
&:hover {
i {
color: $navHighlight;
}
}
}
}
}
}
.notifications-menu > .dropdown-menu > li .menu > li > a:hover i {
color: $navHighlight;
}
}
.ticktac-single {
i {
color: $tickTacColor;
}
span {
color: $tickTacColor;
}
&:hover {
.ticktac-stop {
i {
color: $stopButton;
}
}
.ticktac-start {
i {
color: $startButton;
}
}
}
}
}
}
}

View File

@@ -25,7 +25,7 @@ $green: #00a65a;
// Info
//$aqua: #00c0ef;
// Warning
//$yellow: #f39c12;
$yellow: #f0ad4e;
$blue: #0073b7;
//$navy: #001F3F;
//$teal: #39CCCC;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,10 +3,10 @@
"app": {
"js": [
"build/runtime.b8e7bb04.js",
"build/app.07c264a7.js"
"build/app.65f97645.js"
],
"css": [
"build/app.7aa10842.css"
"build/app.beac1734.css"
]
},
"invoice": {

View File

@@ -1,6 +1,6 @@
{
"build/app.css": "build/app.7aa10842.css",
"build/app.js": "build/app.07c264a7.js",
"build/app.css": "build/app.beac1734.css",
"build/app.js": "build/app.65f97645.js",
"build/invoice.css": "build/invoice.ff32661a.css",
"build/invoice.js": "build/invoice.19f36eca.js",
"build/invoice-pdf.css": "build/invoice-pdf.9a7468ef.css",

View File

@@ -24,11 +24,6 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
/** @var Timesheet $timesheet */
$timesheet = $payload['timesheet'];
if ($timesheet->getId() !== null) {
if ($this->isGranted('edit', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['url' => $this->path($routeEdit, ['id' => $timesheet->getId()]), 'class' => $class]);
}
if ($timesheet->isRunning() && $this->isGranted('stop', $timesheet)) {
$event->addAction('stop', ['url' => $this->path('stop_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-event' => 'kimai.timesheetStop kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.stop.error', 'data-msg-success' => 'timesheet.stop.success']]);
}
@@ -37,6 +32,11 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
$event->addAction('repeat', ['url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
}
if ($this->isGranted('edit', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['url' => $this->path($routeEdit, ['id' => $timesheet->getId()]), 'class' => $class]);
}
if ($this->isGranted('duplicate', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('copy', ['url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class]);

View File

@@ -88,82 +88,100 @@
<!-- Page rendered on {{ 'now'|date_full }} -->
{% endblock %}
{% block navbar_start %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{% if is_granted('view_own_timesheet') %}
{#
{% block navbar_toggle %}
<a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button">
<span class="sr-only">{{ 'Toggle navigation'|trans({}, 'AdminLTEBundle') }}</span>
</a>
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') and is_granted('view_own_timesheet') %}
<div class="navbar-custom-menu" style="float:left">
<ul class="nav navbar-nav">
{% if app.user.preferenceValue('login.initial_view') == 'calendar' %}
<li class="visible-xs-inline-block">
<a href="{{ path('calendar') }}" class="ddt-large">
<li class="pull-left visible-xs-inline-block">
<a href="{{ path('calendar') }}" class="ddt-small">
<i class="{{ 'calendar'|icon }} fa-2x"></i>
</a>
</li>
{% else %}
<li class="visible-xs-inline-block">
<a href="{{ path('timesheet') }}" class="ddt-large">
<li class="pull-left visible-xs-inline-block">
<a href="{{ path('timesheet') }}" class="ddt-small">
<i class="{{ 'timesheet'|icon }} fa-2x"></i>
</a>
</li>
{% endif %}
{% endif %}
{% if is_granted('view_invoice') %}
<li class="visible-xs-inline-block">
<a href="{{ path('invoice') }}" class="ddt-large">
<i class="{{ 'invoice'|icon }} fa-2x"></i>
</a>
</li>
{% endif %}
{% if is_granted('create_export') %}
<li class="visible-xs-inline-block">
<a href="{{ path('export') }}" class="ddt-large">
<i class="{{ 'export'|icon }} fa-2x"></i>
</a>
</li>
{% endif %}
{% block navbar_extensions %}{% endblock %}
{% set active_timesheets = active_timesheets(app.user) %}
{% set active_limit = kimai_config.timesheetActiveEntriesHardLimit %}
<li class="dropdown messages-menu" style="{% if active_timesheets is empty %}display:none{% endif %}">
<a href="#" class="dropdown-toggle ddt-large ticktac" data-toggle="dropdown">
<i class="{{ 'start'|icon }} fa-2x"></i>
<span data-warning="{{ active_limit }}" class="label label-{% if active_timesheets|length > active_limit %}danger{% else %}warning{% endif %}">{% if active_timesheets|length > 0 %}{{ active_timesheets|length }}{% endif %}</span>
</a>
<ul class="dropdown-menu"
data-api="{{ path('active_timesheet') }}"
data-href="{{ path('stop_timesheet', {'id' : '000'}) }}"
data-icon="{{ 'stop-small'|icon }}">
<li class="header">
{{ 'active.entries'|trans }}
</li>
<li>
<ul class="menu">
{% for entry in active_timesheets %}
<li>
<a href="{{ path('stop_timesheet', {'id' : entry.id}) }}" class="api-link" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<div class="pull-left">
<i class="{{ 'stop-small'|icon }} fa-2x"></i>
</div>
<h4>
<span>{{ entry.activity.name }}</span>
<small>
<span data-title="true" data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</small>
</h4>
<p>{{ entry.project.name }} ({{ entry.project.customer.name }})</p>
</a>
</li>
{% endfor %}
</ul>
</li>
{% if is_granted('create_own_timesheet') %}
<li class="footer"><a href="{{ path('timesheet_create') }}" class="modal-ajax-form">{{ 'timesheet.start'|trans }}</a></li>
{% endif %}
</ul>
</li>
</div>
{% endif %}
{% endblock %}
#}
{% block navbar_start %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{% block navbar_extensions %}{% endblock %}
{% if is_granted('create_own_timesheet') %}
<li class="messages-menu-empty" style="{% if active_timesheets is not empty %}display:none{% endif %}">
<a href="{{ path('timesheet_create') }}" class="ddt-large modal-ajax-form">
<i class="{{ 'start'|icon }} fa-2x"></i>
</a>
{% set active_timesheets = active_timesheets(app.user) %}
{% set hasActiveRecords = active_timesheets is not empty %}
{% if not hasActiveRecords %}
{# fake entry, because at least one html template node is needed #}
{% set active_timesheets = [{'id': '000', 'begin': create_date('now'), 'activity': {'name': ''}, 'project': {'name': '', 'customer': {'name': ''}}}] %}
{% endif %}
{% set active_limit = kimai_config.timesheetActiveEntriesHardLimit %}
<li class="messages-menu {% if active_limit > 1 and hasActiveRecords %}dropdown{% endif %}" data-api="{{ path('active_timesheet') }}" data-href="{{ path('stop_timesheet', {'id' : '000'}) }}" data-icon="{{ 'stop-small'|icon }}" style="{% if not hasActiveRecords %}display:none{% endif %}">
{% if active_limit == 1 %}
{% set entry = active_timesheets[0] %}
<div class="ddt-small ticktac-single ticktac-running">
<div class="ticktac-stop">
<a data-replacer="url" class="api-link" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<i class="{{ 'stop-small'|icon }} fa-2x"></i><span data-replacer="duration" data-title="true" data-since="{{ entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</a>
</div>
</div>
{% else %}
<a href="#" class="dropdown-toggle ddt-small ticktac" data-toggle="dropdown">
<i class="{{ 'start'|icon }} fa-2x"></i>
<span data-warning="{{ active_limit }}" class="label label-warning">{% if hasActiveRecords %}{{ active_timesheets|length }}{% endif %}</span>
</a>
<ul class="dropdown-menu">
<li class="header">
{{ 'active.entries'|trans }}
</li>
<li>
<ul class="menu">
{% for entry in active_timesheets %}
<li data-template="active-record">
<a data-replacer="url" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" class="api-link" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<div class="pull-left">
<i class="{{ 'stop-small'|icon }} fa-2x"></i>
</div>
<h4>
<span data-replacer="activity">{{ entry.activity.name }}</span>
<small>
<span data-replacer="duration" data-title="true" data-since="{{ entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</small>
</h4>
<p><span data-replacer="project">{{ entry.project.name }}</span> (<span data-replacer="customer">{{ entry.project.customer.name }}</span>)</p>
</a>
</li>
{% endfor %}
</ul>
</li>
<li class="footer"><a href="{{ path('timesheet_create') }}" class="modal-ajax-form">{{ 'timesheet.start'|trans }}</a></li>
</ul>
{% endif %}
</li>
<li class="messages-menu-empty" style="{% if hasActiveRecords %}display:none{% endif %}">
{% if active_limit == 1 %}
<div class="ddt-small ticktac-single ticktac-stopped">
<a href="{{ path('timesheet_create') }}" class="modal-ajax-form ticktac-start">
<i class="{{ 'start'|icon }} fa-2x"></i>
<span>{{ 0|duration }}</span>
</a>
</div>
{% else %}
<a href="{{ path('timesheet_create') }}" class="ddt-small ticktac-start modal-ajax-form">
<i class="{{ 'start'|icon }} fa-2x"></i>
</a>
{% endif %}
</li>
{% endif %}
{% endif %}
@@ -188,7 +206,7 @@
{% endif %}
{% import "macros/widgets.html.twig" as widgets %}
<li class="dropdown user-menu">
<a href="#" class="dropdown-toggle ddt-large" data-toggle="dropdown">
<a href="#" class="dropdown-toggle ddt-small" data-toggle="dropdown">
{{ widgets.user_avatar(app.user, false) }}
</a>
<ul class="dropdown-menu">

View File

@@ -6,7 +6,7 @@
{% import "invoice/macros.html.twig" as macros %}
{% set columns = {
'date': {'class': 'alwaysVisible w-min', 'orderBy': false},
'date': {'class': 'alwaysVisible', 'orderBy': false},
'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false},
'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
'invoice_number': {'class': 'hidden-xs hidden-sm text-center w-min', 'title': 'invoice.number'|trans, 'orderBy': false},

View File

@@ -1,7 +1,7 @@
{% if is_granted('start_own_timesheet') %}
<li class="dropdown notifications-menu">
<a href="#" class="dropdown-toggle ddt-large" data-toggle="dropdown">
<i class="{{ 'activity'|icon }} fa-2x"></i>
<a href="#" class="dropdown-toggle ddt-small" data-toggle="dropdown">
<i class="{{ 'repeat'|icon }} fa-2x"></i>
</a>
<ul class="dropdown-menu"
data-api="{{ path('recent_timesheet', {'size' : 10}) }}"

View File

@@ -82,8 +82,8 @@ class LayoutControllerTest extends ControllerBaseTest
$content = $client->getResponse()->getContent();
self::assertStringContainsString('<li class="dropdown messages-menu" style="display:none">', $content);
self::assertStringContainsString('<ul class="dropdown-menu"', $content);
self::assertStringContainsString('<li class="messages-menu', $content);
self::assertStringContainsString('<div class="ddt-small ticktac-single ', $content);
self::assertStringContainsString('data-api="', $content);
self::assertStringContainsString('data-href="', $content);
self::assertStringContainsString('data-icon=', $content);