Move recent activities to modal (#3864)
* automatically remove deleted timesheets from bookmarks
This commit is contained in:
@@ -22,7 +22,6 @@ import KimaiAPI from "./plugins/KimaiAPI";
|
|||||||
import KimaiAlternativeLinks from "./plugins/KimaiAlternativeLinks";
|
import KimaiAlternativeLinks from "./plugins/KimaiAlternativeLinks";
|
||||||
import KimaiAjaxModalForm from "./plugins/KimaiAjaxModalForm";
|
import KimaiAjaxModalForm from "./plugins/KimaiAjaxModalForm";
|
||||||
import KimaiActiveRecords from "./plugins/KimaiActiveRecords";
|
import KimaiActiveRecords from "./plugins/KimaiActiveRecords";
|
||||||
import KimaiRecentActivities from "./plugins/KimaiRecentActivities";
|
|
||||||
import KimaiEvent from "./plugins/KimaiEvent";
|
import KimaiEvent from "./plugins/KimaiEvent";
|
||||||
import KimaiAPILink from "./plugins/KimaiAPILink";
|
import KimaiAPILink from "./plugins/KimaiAPILink";
|
||||||
import KimaiAlert from "./plugins/KimaiAlert";
|
import KimaiAlert from "./plugins/KimaiAlert";
|
||||||
@@ -41,6 +40,7 @@ import KimaiCopyDataForm from "./forms/KimaiCopyDataForm";
|
|||||||
import KimaiDateNowForm from "./forms/KimaiDateNowForm";
|
import KimaiDateNowForm from "./forms/KimaiDateNowForm";
|
||||||
import KimaiNotification from "./plugins/KimaiNotification";
|
import KimaiNotification from "./plugins/KimaiNotification";
|
||||||
import KimaiHotkeys from "./plugins/KimaiHotkeys";
|
import KimaiHotkeys from "./plugins/KimaiHotkeys";
|
||||||
|
import KimaiRemoteModal from "./plugins/KimaiRemoteModal";
|
||||||
|
|
||||||
export default class KimaiLoader {
|
export default class KimaiLoader {
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ export default class KimaiLoader {
|
|||||||
kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action'));
|
kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action'));
|
||||||
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
||||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
||||||
kimai.registerPlugin(new KimaiRecentActivities());
|
kimai.registerPlugin(new KimaiRemoteModal());
|
||||||
kimai.registerPlugin(new KimaiActiveRecords());
|
kimai.registerPlugin(new KimaiActiveRecords());
|
||||||
kimai.registerPlugin(new KimaiAPILink('api-link'));
|
kimai.registerPlugin(new KimaiAPILink('api-link'));
|
||||||
kimai.registerPlugin(new KimaiMultiUpdateTable());
|
kimai.registerPlugin(new KimaiMultiUpdateTable());
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* [KIMAI] KimaiRecentActivities: responsible to reload the users recent activities
|
|
||||||
*/
|
|
||||||
|
|
||||||
import KimaiPlugin from '../KimaiPlugin';
|
|
||||||
|
|
||||||
export default class KimaiRecentActivities extends KimaiPlugin {
|
|
||||||
|
|
||||||
constructor()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this._selector = '.notifications-menu';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getId()
|
|
||||||
{
|
|
||||||
return 'recent-activities';
|
|
||||||
}
|
|
||||||
|
|
||||||
init()
|
|
||||||
{
|
|
||||||
const menus = document.querySelectorAll(this._selector);
|
|
||||||
|
|
||||||
// the menu can be hidden if user has no permissions to see it
|
|
||||||
// or no timesheet was recorded yet
|
|
||||||
if (menus.length === 0 || menus[0].dataset['reload'] === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handle = () => {
|
|
||||||
// TODO this works but using the first menu is not ideal, pass in the reload URL?
|
|
||||||
this._reloadMenu(menus[0].dataset['reload']);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('kimai.recentActivities', handle);
|
|
||||||
document.addEventListener('kimai.timesheetUpdate', handle);
|
|
||||||
document.addEventListener('kimai.timesheetDelete', handle);
|
|
||||||
document.addEventListener('kimai.activityUpdate', handle);
|
|
||||||
document.addEventListener('kimai.activityDelete', handle);
|
|
||||||
document.addEventListener('kimai.projectUpdate', handle);
|
|
||||||
document.addEventListener('kimai.projectDelete', handle);
|
|
||||||
document.addEventListener('kimai.customerUpdate', handle);
|
|
||||||
document.addEventListener('kimai.customerDelete', handle);
|
|
||||||
|
|
||||||
this._attachAddRemoveFavorite();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_attachAddRemoveFavorite()
|
|
||||||
{
|
|
||||||
[].slice.call(document.querySelectorAll(this._selector + ' a.list-group-item-actions')).map((element) => {
|
|
||||||
element.addEventListener('click', (event) => {
|
|
||||||
this._reloadMenu(event.currentTarget.href);
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload all ercent activities and update the existing menus.
|
|
||||||
*
|
|
||||||
* @param {string} url
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_reloadMenu(url)
|
|
||||||
{
|
|
||||||
this.fetch(url, {method: 'GET'})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.text().then(html => {
|
|
||||||
const newFormHtml = document.createElement('div');
|
|
||||||
newFormHtml.innerHTML = html;
|
|
||||||
|
|
||||||
for (let menu of document.querySelectorAll(this._selector)) {
|
|
||||||
menu.replaceWith(newFormHtml.firstElementChild.cloneNode(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
this._attachAddRemoveFavorite();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
console.log('Failed to log recent activities', reason);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
109
assets/js/plugins/KimaiRemoteModal.js
Normal file
109
assets/js/plugins/KimaiRemoteModal.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* [KIMAI] KimaiRecentActivities: responsible to reload the users recent activities
|
||||||
|
*/
|
||||||
|
|
||||||
|
import KimaiPlugin from '../KimaiPlugin';
|
||||||
|
import { Modal } from 'bootstrap';
|
||||||
|
|
||||||
|
export default class KimaiRemoteModal extends KimaiPlugin {
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._selector = 'a.remote-modal-load';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getId()
|
||||||
|
{
|
||||||
|
return 'remote-modal';
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
{
|
||||||
|
this.handle = (event) => {
|
||||||
|
this._showModal(event.currentTarget);
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let link of document.querySelectorAll(this._selector)) {
|
||||||
|
link.addEventListener('click', this.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('kimai.closeRemoteModal', () => { this._hide(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_initElement(element)
|
||||||
|
{
|
||||||
|
for (let link of element.querySelectorAll('a.remote-modal-reload')) {
|
||||||
|
link.addEventListener('click', this.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hide()
|
||||||
|
{
|
||||||
|
this._getModal().hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
_getModalElement()
|
||||||
|
{
|
||||||
|
return document.getElementById('remote_modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Modal}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getModal()
|
||||||
|
{
|
||||||
|
return Modal.getOrCreateInstance(this._getModalElement());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLLinkElement} element
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_showModal(element)
|
||||||
|
{
|
||||||
|
this.fetch(element.href, {method: 'GET'})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text().then(html => {
|
||||||
|
const newFormHtml = document.createElement('div');
|
||||||
|
newFormHtml.classList.add('modal-body');
|
||||||
|
newFormHtml.classList.add('p-0');
|
||||||
|
newFormHtml.innerHTML = html;
|
||||||
|
|
||||||
|
this._initElement(newFormHtml);
|
||||||
|
|
||||||
|
const modal = this._getModalElement();
|
||||||
|
modal.querySelector('.modal-body').replaceWith(newFormHtml);
|
||||||
|
if (element.dataset['modalTitle'] !== undefined) {
|
||||||
|
modal.querySelector('.modal-title').textContent = element.dataset['modalTitle'];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getModal().show();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((reason) => {
|
||||||
|
console.log('Failed to load remote modal', reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,15 +6,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
|
||||||
.notifications-menu .dropdown-menu-card {
|
|
||||||
width: 100vw;
|
|
||||||
right: 0;
|
|
||||||
position: fixed;
|
|
||||||
top: 56px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 360px) {
|
@media (min-width: 360px) {
|
||||||
.inline-search {
|
.inline-search {
|
||||||
max-width: 235px;
|
max-width: 235px;
|
||||||
|
|||||||
@@ -6965,11 +6965,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Timesheet/DateTimeFactory.php
|
path: src/Timesheet/DateTimeFactory.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Timesheet\\\\FavoriteRecordService\\:\\:favoriteEntries\\(\\) should return array\\<App\\\\Model\\\\FavoriteTimesheet\\> but returns array\\<int, App\\\\Model\\\\FavoriteTimesheet\\|bool\\>\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Timesheet/FavoriteRecordService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$isFavorite of class App\\\\Model\\\\FavoriteTimesheet constructor expects bool, App\\\\Model\\\\FavoriteTimesheet\\|bool given\\.$#"
|
message: "#^Parameter \\#2 \\$isFavorite of class App\\\\Model\\\\FavoriteTimesheet constructor expects bool, App\\\\Model\\\\FavoriteTimesheet\\|bool given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
2
public/build/app.46a6055b.js
Normal file
2
public/build/app.46a6055b.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"js": [
|
"js": [
|
||||||
"/build/runtime.f0079159.js",
|
"/build/runtime.f0079159.js",
|
||||||
"/build/app.50e94a4a.js"
|
"/build/app.46a6055b.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/app.2c6c5920.css"
|
"/build/app.2c6c5920.css"
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
},
|
},
|
||||||
"integrity": {
|
"integrity": {
|
||||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||||
"/build/app.50e94a4a.js": "sha384-HR3cG9DvdBAyo08dhybZE8eNOvFY+3Yvp2zxFC43hcwTK6dGoAOLTyNhTdoGi8CR",
|
"/build/app.46a6055b.js": "sha384-4FR2SCr6YUmX48AWUg8rqFrfHSOTRj6tIqZJK/CjrNx0XV1km7nn2BCEVFNo0UkA",
|
||||||
"/build/app.2c6c5920.css": "sha384-qiubu7JONCsPTbVOb8bSJ4GYxioCwIUvlsI0usMmIUyoTxawn6jsjG7KK64iej5S",
|
"/build/app.2c6c5920.css": "sha384-qiubu7JONCsPTbVOb8bSJ4GYxioCwIUvlsI0usMmIUyoTxawn6jsjG7KK64iej5S",
|
||||||
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
|
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
|
||||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build/app.css": "/build/app.2c6c5920.css",
|
"build/app.css": "/build/app.2c6c5920.css",
|
||||||
"build/app.js": "/build/app.50e94a4a.js",
|
"build/app.js": "/build/app.46a6055b.js",
|
||||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||||
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
|
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
|
||||||
"build/invoice.css": "/build/invoice.3c80ee80.css",
|
"build/invoice.css": "/build/invoice.3c80ee80.css",
|
||||||
|
|||||||
@@ -20,27 +20,27 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|||||||
final class FavoriteController extends AbstractController
|
final class FavoriteController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route(path: '/timesheet/', name: 'favorites_timesheets', methods: ['GET'])]
|
#[Route(path: '/timesheet/', name: 'favorites_timesheets', methods: ['GET'])]
|
||||||
#[IsGranted('view_own_timesheet')]
|
#[IsGranted('start_own_timesheet')]
|
||||||
public function favoriteAction(): Response
|
public function favoriteAction(): Response
|
||||||
{
|
{
|
||||||
return $this->render('partials/recent-activities.html.twig');
|
return $this->render('favorite/index.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/timesheet/add/{id}', name: 'favorites_timesheets_add', methods: ['GET'])]
|
#[Route(path: '/timesheet/add/{id}', name: 'favorites_timesheets_add', methods: ['GET'])]
|
||||||
#[IsGranted('view_own_timesheet')]
|
#[IsGranted('start_own_timesheet')]
|
||||||
public function add(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
|
public function add(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
|
||||||
{
|
{
|
||||||
$favoriteRecordService->addFavorite($timesheet);
|
$favoriteRecordService->addFavorite($timesheet);
|
||||||
|
|
||||||
return $this->render('partials/recent-activities.html.twig');
|
return $this->render('favorite/index.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/timesheet/remove/{id}', name: 'favorites_timesheets_remove', methods: ['GET'])]
|
#[Route(path: '/timesheet/remove/{id}', name: 'favorites_timesheets_remove', methods: ['GET'])]
|
||||||
#[IsGranted('view_own_timesheet')]
|
#[IsGranted('start_own_timesheet')]
|
||||||
public function remove(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
|
public function remove(Timesheet $timesheet, FavoriteRecordService $favoriteRecordService): Response
|
||||||
{
|
{
|
||||||
$favoriteRecordService->removeFavorite($timesheet);
|
$favoriteRecordService->removeFavorite($timesheet);
|
||||||
|
|
||||||
return $this->render('partials/recent-activities.html.twig');
|
return $this->render('favorite/index.html.twig');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,26 +32,44 @@ final class FavoriteRecordService
|
|||||||
*/
|
*/
|
||||||
public function favoriteEntries(User $user, int $limit = 5): array
|
public function favoriteEntries(User $user, int $limit = 5): array
|
||||||
{
|
{
|
||||||
|
/** @var array<int> $favIds */
|
||||||
$favIds = $this->getBookmark($user)->getContent();
|
$favIds = $this->getBookmark($user)->getContent();
|
||||||
$recentIds = [];
|
$recentIds = [];
|
||||||
if (\count($favIds) < 5) {
|
if (\count($favIds) < 5) {
|
||||||
$recentIds = $this->repository->getRecentActivityIds($user, null, $limit);
|
$recentIds = $this->repository->getRecentActivityIds($user, null, $limit);
|
||||||
}
|
}
|
||||||
|
/** @var array<int> $ids */
|
||||||
$ids = \array_slice(array_unique(array_merge($favIds, $recentIds)), 0, $limit);
|
$ids = \array_slice(array_unique(array_merge($favIds, $recentIds)), 0, $limit);
|
||||||
|
|
||||||
|
/** @var array<int, bool|FavoriteTimesheet> $favorites */
|
||||||
$favorites = [];
|
$favorites = [];
|
||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
$favorites[$id] = \in_array($id, $favIds);
|
$favorites[$id] = \in_array($id, $favIds, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$all = [];
|
||||||
if (\count($ids) > 0) {
|
if (\count($ids) > 0) {
|
||||||
$timesheets = $this->repository->findTimesheetsById($ids, false, false);
|
$timesheets = $this->repository->findTimesheetsById($ids, false, false);
|
||||||
foreach ($timesheets as $timesheet) {
|
foreach ($timesheets as $timesheet) {
|
||||||
$favorites[$timesheet->getId()] = new FavoriteTimesheet($timesheet, $favorites[$timesheet->getId()]);
|
$id = $timesheet->getId();
|
||||||
|
if ($id === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$favorites[$id] = new FavoriteTimesheet($timesheet, $favorites[$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($favorites as $id => $favorite) {
|
||||||
|
if (!$favorite instanceof FavoriteTimesheet) {
|
||||||
|
// auto cleanup in case someone deleted a bookmarked timesheet
|
||||||
|
$this->removeFavoriteById($user, $id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$all[$id] = $favorite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_values($favorites);
|
return array_values($all);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getBookmark(User $user): Bookmark
|
private function getBookmark(User $user): Bookmark
|
||||||
@@ -95,16 +113,25 @@ final class FavoriteRecordService
|
|||||||
throw new \InvalidArgumentException('Cannot favorite timesheet without user');
|
throw new \InvalidArgumentException('Cannot favorite timesheet without user');
|
||||||
}
|
}
|
||||||
|
|
||||||
$bookmark = $this->getBookmark($timesheet->getUser());
|
if ($timesheet->getId() === null) {
|
||||||
|
throw new \InvalidArgumentException('Cannot favorite unsaved timesheet');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->removeFavoriteById($timesheet->getUser(), $timesheet->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeFavoriteById(User $user, int $timesheetId): void
|
||||||
|
{
|
||||||
|
$bookmark = $this->getBookmark($user);
|
||||||
$ids = $bookmark->getContent();
|
$ids = $bookmark->getContent();
|
||||||
|
|
||||||
if (!\in_array($timesheet->getId(), $ids)) {
|
if (!\in_array($timesheetId, $ids)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$newIds = [];
|
$newIds = [];
|
||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
if ($id !== $timesheet->getId()) {
|
if ($id !== $timesheetId) {
|
||||||
$newIds[] = $id;
|
$newIds[] = $id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
{% block modal_body %}{% endblock %}
|
{% block modal_body %}{% endblock %}
|
||||||
{% block modal_footer %}{% endblock %}
|
{% block modal_footer %}{% endblock %}
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
|
{% embed '@theme/embeds/modal.html.twig' %}
|
||||||
|
{% block modal_id %}remote_modal{% endblock %}
|
||||||
|
{% block modal_title %}{% endblock %}
|
||||||
|
{% block modal_body %}{% endblock %}
|
||||||
|
{% block modal_footer %}
|
||||||
|
<button type="button" class="btn btn-cancel" data-bs-dismiss="modal">{{ 'action.close'|trans }}</button>
|
||||||
|
{% endblock %}
|
||||||
|
{% endembed %}
|
||||||
{% block page_search %}{% endblock %}
|
{% block page_search %}{% endblock %}
|
||||||
<div id="toast-container" class="toast-container position-fixed top-0 start-50 translate-middle-x p-3" style="z-index: 11"></div>
|
<div id="toast-container" class="toast-container position-fixed top-0 start-50 translate-middle-x p-3" style="z-index: 11"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
39
templates/favorite/index.html.twig
Normal file
39
templates/favorite/index.html.twig
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{% set favorites = favorite_timesheets(app.user) %}
|
||||||
|
{% if favorites|length > 0 %}
|
||||||
|
{% from 'macros/widgets.html.twig' import label_customer, label_project, label_activity %}
|
||||||
|
{% set class = "btn-outline-white" %}
|
||||||
|
{% if tabler_bundle.isDarkMode() or tabler_bundle.isNavbarOverlapping() %}
|
||||||
|
{% set class = "btn-dark" %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="list-group list-group-flush list-group-hoverable menu">
|
||||||
|
{% for favorite in favorites %}
|
||||||
|
{% set entry = favorite.getTimesheet() %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col text-truncate" >
|
||||||
|
<a class="api-link text-decoration-none text-body d-block" href="{{ path('restart_timesheet', {'id': entry.id}) }}"
|
||||||
|
data-event="kimai.timesheetStart kimai.timesheetUpdate kimai.closeRemoteModal" data-method="PATCH" data-msg-error="timesheet.start.error"
|
||||||
|
data-msg-success="timesheet.start.success">
|
||||||
|
{{ label_activity(entry.activity) }}
|
||||||
|
<div class="d-block text-truncate mt-n1">
|
||||||
|
{{ label_project(entry.project) }}
|
||||||
|
{{ label_customer(entry.project.customer) }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
{% if favorite.isFavorite() %}
|
||||||
|
<a href="{{ path('favorites_timesheets_remove', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions show remote-modal-reload">
|
||||||
|
<i class="text-yellow {{ 'bookmarked'|icon }}"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ path('favorites_timesheets_add', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions remote-modal-reload">
|
||||||
|
<i class="{{ 'bookmark'|icon }}"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -1,56 +1,11 @@
|
|||||||
{% if is_granted('start_own_timesheet') %}
|
{% if is_granted('start_own_timesheet') %}
|
||||||
{% set favorites = favorite_timesheets(app.user) %}
|
|
||||||
{% if favorites|length > 0 %}
|
|
||||||
{% from 'macros/widgets.html.twig' import label_customer, label_project, label_activity %}
|
|
||||||
{% set class = "btn-outline-white" %}
|
{% set class = "btn-outline-white" %}
|
||||||
{% if tabler_bundle.isDarkMode() or tabler_bundle.isNavbarOverlapping() %}
|
{% if tabler_bundle.isDarkMode() or tabler_bundle.isNavbarOverlapping() %}
|
||||||
{% set class = "btn-dark" %}
|
{% set class = "btn-dark" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="nav-item dropdown d-flex me-sm-3 me-1 notifications-menu" data-reload="{{ path('favorites_timesheets') }}">
|
<div class="nav-item d-flex me-sm-3 me-1">
|
||||||
<a href="#" class="btn btn-icon {{ class}} px-0" data-bs-toggle="dropdown" tabindex="-1" aria-label="{{ 'recent.activities'|trans }}">
|
<a href="{{ path('favorites_timesheets') }}" class="btn btn-icon {{ class}} px-0 remote-modal-load" data-modal-title="{{ 'recent.activities'|trans }}" tabindex="-1" aria-label="{{ 'recent.activities'|trans }}">
|
||||||
{{ icon('repeat') }}
|
{{ icon('repeat') }}
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-card">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
{{ 'recent.activities'|trans }}
|
|
||||||
<div class="card-actions">
|
|
||||||
<button type="button" class="btn-close{% if tabler_bundle.isDarkMode() %} btn-close-white{% endif %}" data-bs-dismiss="modal" aria-label="{{ 'action.close'|trans }}"></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="list-group list-group-flush list-group-hoverable menu">
|
|
||||||
{% for favorite in favorites %}
|
|
||||||
{% set entry = favorite.getTimesheet() %}
|
|
||||||
<div class="list-group-item">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col text-truncate" >
|
|
||||||
<a class="api-link text-decoration-none text-body d-block"
|
|
||||||
href="{{ path('restart_timesheet', {'id': entry.id}) }}" data-event="kimai.timesheetStart kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.start.error"
|
|
||||||
data-msg-success="timesheet.start.success">
|
|
||||||
{{ label_activity(entry.activity) }}
|
|
||||||
<div class="d-block text-truncate mt-n1">
|
|
||||||
{{ label_project(entry.project) }}
|
|
||||||
{{ label_customer(entry.project.customer) }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
{% if favorite.isFavorite() %}
|
|
||||||
<a href="{{ path('favorites_timesheets_remove', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions show">
|
|
||||||
<i class="text-yellow {{ 'bookmarked'|icon }}"></i>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ path('favorites_timesheets_add', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions">
|
|
||||||
<i class="{{ 'bookmark'|icon }}"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ class FavoriteControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$content = $client->getResponse()->getContent();
|
$content = $client->getResponse()->getContent();
|
||||||
self::assertNotFalse($content);
|
self::assertNotFalse($content);
|
||||||
self::assertStringContainsString('<div class="nav-item dropdown d-flex me-sm-3 me-1 notifications-menu" data-reload="/en/favorite/timesheet/">', $content);
|
self::assertStringContainsString('<a class="api-link text-decoration-none text-body d-block" href="/api/timesheets/', $content);
|
||||||
self::assertStringContainsString('<div class="card-header">', $content);
|
self::assertStringContainsString('data-event="kimai.timesheetStart kimai.timesheetUpdate kimai.closeRemoteModal" data-method="PATCH" data-msg-error="timesheet', $content);
|
||||||
self::assertStringContainsString('Restart one of your last activities', $content);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user