OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
170
app/Domain/Ideas/Controllers/AdvancedBoards.php
Normal file
170
app/Domain/Ideas/Controllers/AdvancedBoards.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Ideas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Ideas\Permissions\IdeasPermissions;
|
||||
use Leantime\Domain\Ideas\Services\Ideas as IdeaService;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AdvancedBoards extends Controller
|
||||
{
|
||||
private ProjectService $projectService;
|
||||
|
||||
private IdeaService $ideaService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(
|
||||
IdeaService $ideaService,
|
||||
ProjectService $projectService
|
||||
): void {
|
||||
$this->ideaService = $ideaService;
|
||||
$this->projectService = $projectService;
|
||||
|
||||
session(['lastPage' => CURRENT_URL]);
|
||||
session(['lastIdeaView' => 'kanban']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the advanced (kanban) idea boards view.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$allCanvas = $this->ideaService->getAllBoards(session('currentProject'));
|
||||
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
|
||||
|
||||
$this->assignTemplateVars($currentCanvasId, $allCanvas);
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->display('ideas.advancedBoards');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles idea board mutations (create, edit, search).
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$allCanvas = $this->ideaService->getAllBoards(session('currentProject'));
|
||||
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
|
||||
|
||||
if (isset($_POST['searchCanvas'])) {
|
||||
$currentCanvasId = (int) $_POST['searchCanvas'];
|
||||
session(['currentIdeaCanvas' => $currentCanvasId]);
|
||||
}
|
||||
|
||||
if (isset($_POST['newCanvas'])) {
|
||||
$result = $this->handleNewCanvas($currentCanvasId, $allCanvas);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['editCanvas']) && $currentCanvasId > 0) {
|
||||
$result = $this->handleEditCanvas($currentCanvasId);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assignTemplateVars($currentCanvasId, $allCanvas);
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->display('ideas.advancedBoards');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the current canvas ID from session or request parameters.
|
||||
*/
|
||||
private function resolveCurrentCanvasId(array $allCanvas, array $params): int
|
||||
{
|
||||
if (session()->exists('currentIdeaCanvas')) {
|
||||
$currentCanvasId = session('currentIdeaCanvas');
|
||||
} else {
|
||||
$currentCanvasId = -1;
|
||||
session(['currentIdeaCanvas' => '']);
|
||||
}
|
||||
|
||||
if (count($allCanvas) > 0 && session('currentIdeaCanvas') == '') {
|
||||
$currentCanvasId = $allCanvas[0]['id'];
|
||||
session(['currentIdeaCanvas' => $currentCanvasId]);
|
||||
}
|
||||
|
||||
if (isset($params['id'])) {
|
||||
$currentCanvasId = (int) $params['id'];
|
||||
session(['currentIdeaCanvas' => $currentCanvasId]);
|
||||
}
|
||||
|
||||
return $currentCanvasId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles creating a new idea board.
|
||||
*/
|
||||
private function handleNewCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentCanvasId = $this->ideaService->createBoard(
|
||||
$_POST['canvastitle'],
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id')
|
||||
);
|
||||
$allCanvas = $this->ideaService->getAllBoards(session('currentProject'));
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.idea_board_created'), 'success', 'ideaboard_created');
|
||||
session(['currentIdeaCanvas' => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/advancedBoards/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles editing an idea board title.
|
||||
*/
|
||||
private function handleEditCanvas(int &$currentCanvasId): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentCanvasId = $this->ideaService->updateBoard($currentCanvasId, $_POST['canvastitle']);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_edited'), 'success', 'ideaboard_edited');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/advancedBoards/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables.
|
||||
*/
|
||||
private function assignTemplateVars(int $currentCanvasId, array $allCanvas): void
|
||||
{
|
||||
$this->tpl->assign('currentCanvas', $currentCanvasId);
|
||||
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
|
||||
$this->tpl->assign('allCanvas', $allCanvas);
|
||||
$this->tpl->assign('canvasItems', $this->ideaService->getBoardItems($currentCanvasId));
|
||||
$this->tpl->assign('canvasLabels', $this->ideaService->getBoardLabels());
|
||||
}
|
||||
}
|
||||
145
app/Domain/Ideas/Controllers/BoardDialog.php
Normal file
145
app/Domain/Ideas/Controllers/BoardDialog.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Ideas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Ideas\Permissions\IdeasPermissions;
|
||||
use Leantime\Domain\Ideas\Services\Ideas as IdeaService;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class BoardDialog extends Controller
|
||||
{
|
||||
/**
|
||||
* Constant that must be redefined.
|
||||
*/
|
||||
protected const CANVAS_NAME = '??';
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
private IdeaService $ideaService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(ProjectService $projectService, IdeaService $ideaService): void
|
||||
{
|
||||
$this->projectService = $projectService;
|
||||
$this->ideaService = $ideaService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the board dialog form.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$currentCanvasId = '';
|
||||
$canvasTitle = '';
|
||||
|
||||
if (isset($params['id'])) {
|
||||
$currentCanvasId = (int) $params['id'];
|
||||
$canvasTitle = $this->ideaService->getBoardTitle($currentCanvasId);
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
}
|
||||
|
||||
$this->tpl->assign('canvasTitle', $canvasTitle);
|
||||
$this->tpl->assign('currentCanvas', $currentCanvasId);
|
||||
$this->tpl->assign('canvasname', 'idea');
|
||||
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->displayPartial('ideas.boardDialog');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles board creation and editing.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$currentCanvasId = '';
|
||||
$canvasTitle = '';
|
||||
|
||||
if (isset($params['id'])) {
|
||||
$currentCanvasId = (int) $params['id'];
|
||||
$canvasTitle = $this->ideaService->getBoardTitle($currentCanvasId);
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
}
|
||||
|
||||
if (isset($_POST['newCanvas'])) {
|
||||
$result = $this->handleNewCanvas($currentCanvasId);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['editCanvas']) && $currentCanvasId > 0) {
|
||||
$result = $this->handleEditCanvas($currentCanvasId);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl->assign('canvasTitle', $canvasTitle);
|
||||
$this->tpl->assign('currentCanvas', $currentCanvasId);
|
||||
$this->tpl->assign('canvasname', 'idea');
|
||||
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->displayPartial('ideas.boardDialog');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles creating a new board.
|
||||
*/
|
||||
private function handleNewCanvas(int|string &$currentCanvasId): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentCanvasId = $this->ideaService->createBoardFromDialog(
|
||||
$_POST['canvastitle'],
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id')
|
||||
);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_created'), 'success', static::CANVAS_NAME.'board_created');
|
||||
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/boardDialog/'.$currentCanvasId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles editing a board title.
|
||||
*/
|
||||
private function handleEditCanvas(int $currentCanvasId): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->ideaService->updateBoard($currentCanvasId, $_POST['canvastitle']);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_edited'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/boardDialog/'.$currentCanvasId);
|
||||
}
|
||||
}
|
||||
56
app/Domain/Ideas/Controllers/DelCanvas.php
Normal file
56
app/Domain/Ideas/Controllers/DelCanvas.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Ideas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Ideas\Permissions\IdeasPermissions;
|
||||
use Leantime\Domain\Ideas\Services\Ideas as IdeaService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DelCanvas extends Controller
|
||||
{
|
||||
private IdeaService $ideaService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(IdeaService $ideaService): void
|
||||
{
|
||||
$this->ideaService = $ideaService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the delete idea board confirmation.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::DELETE)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
return $this->tpl->display('ideas.delCanvas');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles idea board deletion. The controller gate defers (entityScoped) to the service's
|
||||
* deleteCanvas(), which authorizes DELETE against the board's REAL project.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::DELETE, entityScoped: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
|
||||
|
||||
if (isset($_POST['del']) && $id > 0) {
|
||||
$this->ideaService->deleteCanvas($id);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.idea_board_deleted'), 'success', 'ideaboard_deleted');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/showBoards');
|
||||
}
|
||||
|
||||
return $this->tpl->display('ideas.delCanvas');
|
||||
}
|
||||
}
|
||||
56
app/Domain/Ideas/Controllers/DelCanvasItem.php
Normal file
56
app/Domain/Ideas/Controllers/DelCanvasItem.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Ideas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Ideas\Permissions\IdeasPermissions;
|
||||
use Leantime\Domain\Ideas\Services\Ideas as IdeaService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DelCanvasItem extends Controller
|
||||
{
|
||||
private IdeaService $ideaService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(IdeaService $ideaService): void
|
||||
{
|
||||
$this->ideaService = $ideaService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the delete idea item confirmation.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::DELETE)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
return $this->tpl->displayPartial('ideas.delCanvasItem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles idea item deletion. The controller gate defers (entityScoped) to the service's
|
||||
* deleteCanvasItem(), which authorizes DELETE against the item's REAL project.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::DELETE, entityScoped: true)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
|
||||
|
||||
if (isset($_POST['del']) && $id > 0) {
|
||||
$this->ideaService->deleteCanvasItem($id);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.idea_board_item_deleted'), 'success', 'ideaitem_deleted');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/showBoards');
|
||||
}
|
||||
|
||||
return $this->tpl->displayPartial('ideas.delCanvasItem');
|
||||
}
|
||||
}
|
||||
181
app/Domain/Ideas/Controllers/IdeaDialog.php
Normal file
181
app/Domain/Ideas/Controllers/IdeaDialog.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Ideas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Ideas\Permissions\IdeasPermissions;
|
||||
use Leantime\Domain\Ideas\Services\Ideas as IdeaService;
|
||||
|
||||
class IdeaDialog extends Controller
|
||||
{
|
||||
private IdeaService $ideaService;
|
||||
|
||||
/**
|
||||
* init - initialize private variables
|
||||
*/
|
||||
public function init(IdeaService $ideaService): void
|
||||
{
|
||||
$this->ideaService = $ideaService;
|
||||
}
|
||||
|
||||
/**
|
||||
* get - handle get requests. The embedded ?delComment / ?removeMilestone mutations are fenced
|
||||
* by the service (removeIdeaComment = author-or-moderate; removeMilestone = edit) in-body.
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function get($params)
|
||||
{
|
||||
if (isset($params['id'])) {
|
||||
// Delete comment
|
||||
if (isset($params['delComment']) === true) {
|
||||
$commentId = (int) ($params['delComment']);
|
||||
$this->ideaService->removeIdeaComment($commentId);
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_deleted'), 'success', 'ideacomment_deleted');
|
||||
}
|
||||
|
||||
// Delete milestone relationship
|
||||
if (isset($params['removeMilestone']) === true) {
|
||||
$this->ideaService->removeMilestone((int) $params['id']);
|
||||
$this->tpl->setNotification($this->language->__('notifications.milestone_detached'), 'success');
|
||||
}
|
||||
|
||||
$canvasItem = $this->ideaService->getIdeaItem((int) $params['id']);
|
||||
$comments = $this->ideaService->getIdeaComments('idea', $canvasItem['id']);
|
||||
$this->tpl->assign('numComments', $this->ideaService->countIdeaComments('ideas', $canvasItem['id']));
|
||||
|
||||
} else {
|
||||
|
||||
$type = $params['type'] ?? 'idea';
|
||||
|
||||
$canvasItem = $this->ideaService->getIdeaItem(null, $type);
|
||||
|
||||
$comments = [];
|
||||
}
|
||||
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
$this->tpl->assign('milestones', $this->ideaService->getProjectMilestones((int) session('currentProject')));
|
||||
$this->tpl->assign('canvasTypes', $this->ideaService->getCanvasTypes());
|
||||
$this->tpl->assign('canvasItem', $canvasItem);
|
||||
$this->tpl->assign('currentCanvas', (int) session('currentIdeaCanvas'));
|
||||
|
||||
return $this->tpl->displayPartial('ideas.ideaDialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* post - handle post requests. Create/update/comment all defer to the service methods, which
|
||||
* authorize the correct verb (ideas.create / ideas.edit / comments.create) against the entity's
|
||||
* real project.
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function post($params)
|
||||
{
|
||||
|
||||
if (isset($params['comment']) === true) {
|
||||
if ($params['text'] != '') {
|
||||
$this->ideaService->addIdeaComment(
|
||||
$params['text'],
|
||||
(int) $_GET['id'],
|
||||
$params['father'],
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id')
|
||||
);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_create_success'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/ideaDialog/'.(int) $_GET['id']);
|
||||
}
|
||||
}
|
||||
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
// changeItem is set for new or edited item changes.
|
||||
if (isset($params['changeItem'])) {
|
||||
if (isset($params['itemId']) && $params['itemId'] != '') {
|
||||
if (isset($params['description']) === true) {
|
||||
$currentCanvasId = (int) ($params['canvasId'] ?? session('currentIdeaCanvas'));
|
||||
|
||||
$input = [
|
||||
'box' => $params['box'],
|
||||
'description' => $params['description'],
|
||||
'status' => $params['status'],
|
||||
'data' => $params['data'],
|
||||
'tags' => $params['tags'],
|
||||
'itemId' => $params['itemId'],
|
||||
'canvasId' => $currentCanvasId,
|
||||
'milestoneId' => $params['milestoneId'],
|
||||
'newMilestone' => $params['newMilestone'] ?? '',
|
||||
'existingMilestone' => $params['existingMilestone'] ?? '',
|
||||
];
|
||||
|
||||
$this->ideaService->updateIdeaItem(
|
||||
$input,
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id')
|
||||
);
|
||||
|
||||
$comments = $this->ideaService->getIdeaComments('leancanvasitem', (int) $params['itemId']);
|
||||
$this->tpl->assign(
|
||||
'numComments',
|
||||
$this->ideaService->countIdeaComments('leancanvasitem', (int) $params['itemId'])
|
||||
);
|
||||
$this->tpl->assign('comments', $comments);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.idea_edited'), 'success');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/ideaDialog/'.(int) $params['itemId']);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/ideaDialog/');
|
||||
}
|
||||
} else {
|
||||
if (isset($_POST['description']) === true) {
|
||||
$currentCanvasId = (int) ($params['canvasId'] ?? session('currentIdeaCanvas'));
|
||||
|
||||
$input = [
|
||||
'box' => $params['box'],
|
||||
'description' => $params['description'],
|
||||
'status' => $params['status'],
|
||||
'data' => $params['data'],
|
||||
'canvasId' => $currentCanvasId,
|
||||
];
|
||||
|
||||
$id = $this->ideaService->createIdeaItem(
|
||||
$input,
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id')
|
||||
);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.idea_created'), 'success', 'idea_created');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/ideaDialog/'.(int) $id);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/ideaDialog/');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$canvasItem = $this->ideaService->getRawIdeaItem($id !== null ? (int) $id : null);
|
||||
|
||||
$this->tpl->assign('canvasTypes', $this->ideaService->getCanvasTypes());
|
||||
$this->tpl->assign('canvasItem', $canvasItem);
|
||||
|
||||
return $this->tpl->displayPartial('ideas.ideaDialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* put - handle put requests
|
||||
*/
|
||||
public function put($params) {}
|
||||
|
||||
/**
|
||||
* delete - handle delete requests
|
||||
*/
|
||||
public function delete($params) {}
|
||||
}
|
||||
181
app/Domain/Ideas/Controllers/ShowBoards.php
Normal file
181
app/Domain/Ideas/Controllers/ShowBoards.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Ideas\Controllers;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Ideas\Permissions\IdeasPermissions;
|
||||
use Leantime\Domain\Ideas\Services\Ideas as IdeaService;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ShowBoards extends Controller
|
||||
{
|
||||
private IdeaService $ideaService;
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(IdeaService $ideaService, ProjectService $projectService): void
|
||||
{
|
||||
$this->ideaService = $ideaService;
|
||||
$this->projectService = $projectService;
|
||||
|
||||
session(['lastPage' => CURRENT_URL]);
|
||||
session(['lastIdeaView' => 'board']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the idea boards view.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$allCanvas = $this->ideaService->getAllBoards(session('currentProject'));
|
||||
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
|
||||
|
||||
$this->assignTemplateVars($currentCanvasId, $allCanvas);
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->display('ideas.showBoards');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles idea board mutations (create, edit, search).
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$allCanvas = $this->ideaService->getAllBoards(session('currentProject'));
|
||||
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
|
||||
|
||||
if (isset($_POST['searchCanvas'])) {
|
||||
$currentCanvasId = (int) $_POST['searchCanvas'];
|
||||
session(['currentIdeaCanvas' => $currentCanvasId]);
|
||||
}
|
||||
|
||||
if (isset($_POST['newCanvas'])) {
|
||||
$result = $this->handleNewCanvas($currentCanvasId, $allCanvas);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['editCanvas']) && $currentCanvasId > 0) {
|
||||
$result = $this->handleEditCanvas($currentCanvasId);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assignTemplateVars($currentCanvasId, $allCanvas);
|
||||
|
||||
if (! isset($_GET['raw'])) {
|
||||
return $this->tpl->display('ideas.showBoards');
|
||||
}
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the current canvas ID from session or request parameters.
|
||||
*
|
||||
* Auto-creates a default board (via the service) when the project has none.
|
||||
*/
|
||||
private function resolveCurrentCanvasId(array &$allCanvas, array $params): int
|
||||
{
|
||||
if (! $allCanvas) {
|
||||
$currentCanvasId = $this->ideaService->ensureBoardExists(
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id'),
|
||||
$allCanvas
|
||||
);
|
||||
$allCanvas = $this->ideaService->getAllBoards(session('currentProject'));
|
||||
|
||||
return $currentCanvasId;
|
||||
}
|
||||
|
||||
if (session()->exists('currentIdeaCanvas')) {
|
||||
$currentCanvasId = session('currentIdeaCanvas');
|
||||
} else {
|
||||
$currentCanvasId = -1;
|
||||
session(['currentIdeaCanvas' => '']);
|
||||
}
|
||||
|
||||
if (session('currentIdeaCanvas') == '') {
|
||||
$currentCanvasId = $allCanvas[0]['id'];
|
||||
session(['currentIdeaCanvas' => $currentCanvasId]);
|
||||
}
|
||||
|
||||
if (isset($params['id'])) {
|
||||
$currentCanvasId = (int) $params['id'];
|
||||
session(['currentIdeaCanvas' => $currentCanvasId]);
|
||||
}
|
||||
|
||||
return $currentCanvasId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles creating a new idea board.
|
||||
*/
|
||||
private function handleNewCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentCanvasId = $this->ideaService->createBoard(
|
||||
$_POST['canvastitle'],
|
||||
(int) session('currentProject'),
|
||||
(int) session('userdata.id')
|
||||
);
|
||||
$allCanvas = $this->ideaService->getAllBoards(session('currentProject'));
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.idea_board_created'), 'success', 'idea_board_created');
|
||||
session(['currentIdeaCanvas' => $currentCanvasId]);
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/ideas/showBoards/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles editing an idea board title.
|
||||
*/
|
||||
private function handleEditCanvas(int &$currentCanvasId): ?Response
|
||||
{
|
||||
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentCanvasId = $this->ideaService->updateBoard($currentCanvasId, $_POST['canvastitle']);
|
||||
|
||||
$this->tpl->setNotification($this->language->__('notification.board_edited'), 'success', 'idea_board_edited');
|
||||
|
||||
return $this->tpl->display('canvas.boardDialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns common template variables.
|
||||
*/
|
||||
private function assignTemplateVars(int $currentCanvasId, array $allCanvas): void
|
||||
{
|
||||
$this->tpl->assign('currentCanvas', $currentCanvasId);
|
||||
$this->tpl->assign('canvasLabels', $this->ideaService->getBoardLabels());
|
||||
$this->tpl->assign('allCanvas', $allCanvas);
|
||||
$this->tpl->assign('canvasItems', $this->ideaService->getBoardItems($currentCanvasId));
|
||||
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
|
||||
}
|
||||
}
|
||||
300
app/Domain/Ideas/Js/ideasController.js
Normal file
300
app/Domain/Ideas/Js/ideasController.js
Normal file
@@ -0,0 +1,300 @@
|
||||
leantime.ideasController = (function () {
|
||||
|
||||
var closeModal = false;
|
||||
|
||||
//Variables
|
||||
var canvasoptions = function () {
|
||||
return {
|
||||
sizes: {
|
||||
minW: 700,
|
||||
minH: 1000,
|
||||
},
|
||||
resizable: true,
|
||||
autoSizable: true,
|
||||
callbacks: {
|
||||
beforeShowCont: function () {
|
||||
jQuery(".showDialogOnLoad").show();
|
||||
if (closeModal == true) {
|
||||
closeModal = false;
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
afterShowCont: function () {
|
||||
|
||||
jQuery(".ideaModal, #commentForm, #commentForm .deleteComment, .leanCanvasMilestone .deleteMilestone").nyroModal(canvasoptions());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Functions
|
||||
|
||||
var _initModals = function () {
|
||||
jQuery(".ideaModal, #commentForm, #commentForm .deleteComment, .leanCanvasMilestone .deleteMilestone").nyroModal(canvasoptions());
|
||||
};
|
||||
|
||||
var openModalManually = function (url) {
|
||||
jQuery.nmManual(url, canvasoptions);
|
||||
};
|
||||
|
||||
var initMasonryWall = function () {
|
||||
|
||||
var $grid = jQuery('#ideaMason').packery({
|
||||
// options
|
||||
itemSelector: '.ticketBox',
|
||||
columnWidth: 260,
|
||||
isResizable: true
|
||||
});
|
||||
|
||||
$grid.imagesLoaded().progress(function () {
|
||||
$grid.packery('layout');
|
||||
});
|
||||
|
||||
var $items = $grid.find('.ticketBox').draggable({
|
||||
start: function (event, ui) {
|
||||
ui.helper.addClass('tilt');
|
||||
tilt_direction(ui.helper);
|
||||
},
|
||||
stop: function (event, ui) {
|
||||
ui.helper.removeClass("tilt");
|
||||
jQuery("html").unbind('mousemove', ui.helper.data("move_handler"));
|
||||
ui.helper.removeData("move_handler");
|
||||
},
|
||||
});
|
||||
|
||||
function tilt_direction(item)
|
||||
{
|
||||
var left_pos = item.position().left,
|
||||
move_handler = function (e) {
|
||||
if (e.pageX >= left_pos) {
|
||||
item.addClass("right");
|
||||
item.removeClass("left");
|
||||
} else {
|
||||
item.addClass("left");
|
||||
item.removeClass("right");
|
||||
}
|
||||
left_pos = e.pageX;
|
||||
};
|
||||
jQuery("html").bind("mousemove", move_handler);
|
||||
item.data("move_handler", move_handler);
|
||||
}
|
||||
// bind drag events to Packery
|
||||
$grid.packery('bindUIDraggableEvents', $items);
|
||||
|
||||
function orderItems()
|
||||
{
|
||||
var ideaSort = [];
|
||||
|
||||
var itemElems = $grid.packery('getItemElements');
|
||||
jQuery(itemElems).each(function ( i, itemElem ) {
|
||||
var sortIndex = i + 1;
|
||||
var ideaId = jQuery(itemElem).attr("data-value");
|
||||
ideaSort.push({"id":ideaId, "sortIndex":sortIndex});
|
||||
});
|
||||
|
||||
leantime.rpc('Ideas.Ideas.reorderIdeas', { payload: ideaSort })
|
||||
.catch(function (e) { console.error('Could not reorder ideas', e); });
|
||||
}
|
||||
|
||||
|
||||
$grid.on('dragItemPositioned',orderItems);
|
||||
};
|
||||
|
||||
var initBoardControlModal = function () {
|
||||
|
||||
|
||||
};
|
||||
|
||||
var initWallImageModals = function () {
|
||||
|
||||
jQuery('.mainIdeaContent img').each(function () {
|
||||
jQuery(this).wrap("<a href='" + jQuery(this).attr("src") + "' class='imageModal'></a>");
|
||||
});
|
||||
|
||||
jQuery(".imageModal").nyroModal();
|
||||
|
||||
};
|
||||
|
||||
|
||||
var toggleMilestoneSelectors = function (trigger) {
|
||||
if (trigger == 'existing') {
|
||||
jQuery('#newMilestone, #milestoneSelectors').hide('fast');
|
||||
jQuery('#existingMilestone').show();
|
||||
_initModals();
|
||||
}
|
||||
if (trigger == 'new') {
|
||||
jQuery('#newMilestone').show();
|
||||
jQuery('#existingMilestone, #milestoneSelectors').hide('fast');
|
||||
_initModals();
|
||||
}
|
||||
|
||||
if (trigger == 'hide') {
|
||||
jQuery('#newMilestone, #existingMilestone').hide('fast');
|
||||
jQuery('#milestoneSelectors').show('fast');
|
||||
}
|
||||
};
|
||||
|
||||
var setCloseModal = function () {
|
||||
closeModal = true;
|
||||
};
|
||||
|
||||
var initUserDropdown = function () {
|
||||
|
||||
jQuery("body").on(
|
||||
"click",
|
||||
".userDropdown .dropdown-menu a",
|
||||
function () {
|
||||
|
||||
var dataValue = jQuery(this).attr("data-value").split("_");
|
||||
var dataLabel = jQuery(this).attr('data-label');
|
||||
|
||||
if (dataValue.length == 3) {
|
||||
var canvasId = dataValue[0];
|
||||
var userId = dataValue[1];
|
||||
var profileImageId = dataValue[2];
|
||||
|
||||
leantime.rpc('Ideas.Ideas.patchIdeaItem', { id: canvasId, params: { author: userId } })
|
||||
.then(function (success) {
|
||||
if (! success) { return; }
|
||||
jQuery("#userDropdownMenuLink" + canvasId + " span.text span#userImage" + canvasId + " img").attr("src", leantime.appUrl + "/users/profileImage/" + encodeURIComponent(userId));
|
||||
|
||||
jQuery.growl({message: leantime.i18n.__("short_notifications.user_updated")});
|
||||
})
|
||||
.catch(function (e) { console.error('Could not update idea author', e); });
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var initStatusDropdown = function () {
|
||||
|
||||
jQuery("body").on(
|
||||
"click",
|
||||
".statusDropdown .dropdown-menu a",
|
||||
function () {
|
||||
|
||||
var dataValue = jQuery(this).attr("data-value").split("_");
|
||||
var dataLabel = jQuery(this).attr('data-label');
|
||||
|
||||
if (dataValue.length == 3) {
|
||||
var canvasItemId = dataValue[0];
|
||||
var status = dataValue[1];
|
||||
var statusClass = dataValue[2];
|
||||
|
||||
|
||||
leantime.rpc('Ideas.Ideas.patchIdeaItem', { id: canvasItemId, params: { box: status } })
|
||||
.then(function (success) {
|
||||
if (! success) { return; }
|
||||
jQuery("#statusDropdownMenuLink" + canvasItemId + " span.text").text(dataLabel);
|
||||
jQuery("#statusDropdownMenuLink" + canvasItemId).removeClass().addClass("" + statusClass + " dropdown-toggle f-left status ");
|
||||
jQuery.growl({message: leantime.i18n.__("short_notifications.status_updated")});
|
||||
})
|
||||
.catch(function (e) { console.error('Could not update idea status', e); });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
var setKanbanHeights = function () {
|
||||
|
||||
var maxHeight = 0;
|
||||
|
||||
var height = jQuery("html").height() - 320;
|
||||
jQuery("#sortableIdeaKanban .column .contentInner").css("height", height);
|
||||
|
||||
};
|
||||
|
||||
var initIdeaKanban = function (statusList) {
|
||||
|
||||
jQuery("#sortableIdeaKanban").disableSelection();
|
||||
|
||||
jQuery("#sortableIdeaKanban .ticketBox").hover(function () {
|
||||
jQuery(this).css("background", "var(--kanban-card-hover)");
|
||||
},function () {
|
||||
jQuery(this).css("background", "var(--kanban-card-bg)");
|
||||
});
|
||||
|
||||
jQuery("#sortableIdeaKanban .contentInner").sortable({
|
||||
connectWith: ".contentInner",
|
||||
items: "> .moveable",
|
||||
tolerance: 'pointer',
|
||||
placeholder: "ui-state-highlight",
|
||||
forcePlaceholderSize: true,
|
||||
cancel: ".portlet-toggle",
|
||||
start: function (event, ui) {
|
||||
ui.item.addClass('tilt');
|
||||
tilt_direction(ui.item);
|
||||
},
|
||||
stop: function (event, ui) {
|
||||
ui.item.removeClass("tilt");
|
||||
jQuery("html").unbind('mousemove', ui.item.data("move_handler"));
|
||||
ui.item.removeData("move_handler");
|
||||
},
|
||||
update: function (event, ui) {
|
||||
|
||||
|
||||
var statusPostData = {
|
||||
action: "statusUpdate",
|
||||
payload: {}
|
||||
};
|
||||
|
||||
for (var i = 0; i < statusList.length; i++) {
|
||||
if (jQuery(".contentInner.status_" + statusList[i]).length) {
|
||||
statusPostData.payload[statusList[i]] = jQuery(".contentInner.status_" + statusList[i]).sortable('serialize');
|
||||
}
|
||||
}
|
||||
|
||||
leantime.rpc('Ideas.Ideas.bulkUpdateStatus', { payload: statusPostData.payload })
|
||||
.catch(function (e) { console.error('Could not update idea statuses', e); });
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
function tilt_direction(item)
|
||||
{
|
||||
var left_pos = item.position().left,
|
||||
move_handler = function (e) {
|
||||
if (e.pageX >= left_pos) {
|
||||
item.addClass("right");
|
||||
item.removeClass("left");
|
||||
} else {
|
||||
item.addClass("left");
|
||||
item.removeClass("right");
|
||||
}
|
||||
left_pos = e.pageX;
|
||||
};
|
||||
jQuery("html").bind("mousemove", move_handler);
|
||||
item.data("move_handler", move_handler);
|
||||
}
|
||||
|
||||
jQuery(".portlet")
|
||||
.addClass("ui-widget ui-widget-content ui-helper-clearfix ui-corner-all")
|
||||
.find(".portlet-header")
|
||||
.addClass("ui-widget-header ui-corner-all")
|
||||
.prepend("<span class='ui-icon ui-icon-minusthick portlet-toggle'></span>");
|
||||
|
||||
jQuery(".portlet-toggle").click(function () {
|
||||
var icon = jQuery(this);
|
||||
icon.toggleClass("ui-icon-minusthick ui-icon-plusthick");
|
||||
icon.closest(".portlet").find(".portlet-content").toggle();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Make public what you want to have public, everything else is private
|
||||
return {
|
||||
setCloseModal:setCloseModal,
|
||||
toggleMilestoneSelectors: toggleMilestoneSelectors,
|
||||
openModalManually:openModalManually,
|
||||
initMasonryWall:initMasonryWall,
|
||||
initBoardControlModal:initBoardControlModal,
|
||||
initWallImageModals:initWallImageModals,
|
||||
initUserDropdown:initUserDropdown,
|
||||
initStatusDropdown:initStatusDropdown,
|
||||
setKanbanHeights:setKanbanHeights,
|
||||
initIdeaKanban:initIdeaKanban
|
||||
};
|
||||
})();
|
||||
41
app/Domain/Ideas/Permissions/IdeasPermissions.php
Normal file
41
app/Domain/Ideas/Permissions/IdeasPermissions.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Ideas\Permissions;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\Permission;
|
||||
use Leantime\Core\Auth\Permissions\ProvidesPermissions;
|
||||
|
||||
/**
|
||||
* The Ideas permission vocabulary — the verbs only.
|
||||
*
|
||||
* Idea boards and items are PROJECT-scoped (each board belongs to one project; items belong to a
|
||||
* board), so every capability is evaluated against the user's role IN that project (projectScoped =
|
||||
* true, the default). The standard verbs auto-grant via the central matrix (readonly = view; editor
|
||||
* = create/edit/delete; manager+ = all), so no
|
||||
* {@see \Leantime\Core\Auth\Permissions\DefaultRolePermissions} change is required.
|
||||
*/
|
||||
final class IdeasPermissions implements ProvidesPermissions
|
||||
{
|
||||
public const VIEW = 'ideas.view';
|
||||
|
||||
public const CREATE = 'ideas.create';
|
||||
|
||||
public const EDIT = 'ideas.edit';
|
||||
|
||||
public const DELETE = 'ideas.delete';
|
||||
|
||||
public function domain(): string
|
||||
{
|
||||
return 'ideas';
|
||||
}
|
||||
|
||||
public function permissions(): array
|
||||
{
|
||||
return [
|
||||
new Permission(self::VIEW, 'View idea boards'),
|
||||
new Permission(self::CREATE, 'Create idea boards and ideas'),
|
||||
new Permission(self::EDIT, 'Edit ideas'),
|
||||
new Permission(self::DELETE, 'Delete idea boards and ideas'),
|
||||
];
|
||||
}
|
||||
}
|
||||
452
app/Domain/Ideas/Repositories/Ideas.php
Normal file
452
app/Domain/Ideas/Repositories/Ideas.php
Normal file
@@ -0,0 +1,452 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Ideas\Repositories;
|
||||
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Leantime\Core\Db\DatabaseHelper;
|
||||
use Leantime\Core\Db\Db as DbCore;
|
||||
use Leantime\Core\Language as LanguageCore;
|
||||
|
||||
class Ideas
|
||||
{
|
||||
public ?object $result = null;
|
||||
|
||||
public ?object $tickets = null;
|
||||
|
||||
private ConnectionInterface $db;
|
||||
|
||||
public array $canvasTypes = [
|
||||
'idea' => 'status.ideation',
|
||||
'research' => 'status.discovery',
|
||||
'prototype' => 'status.delivering',
|
||||
'validation' => 'status.inreview',
|
||||
'implemented' => 'status.accepted',
|
||||
'deferred' => 'status.deferred',
|
||||
];
|
||||
|
||||
public array $statusClasses = ['idea' => 'label-info', 'validation' => 'label-warning', 'prototype' => 'label-warning', 'research' => 'label-warning', 'implemented' => 'label-success', 'deferred' => 'label-default'];
|
||||
|
||||
private LanguageCore $language;
|
||||
|
||||
private DatabaseHelper $dbHelper;
|
||||
|
||||
/**
|
||||
* __construct - get db connection
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(DbCore $db, LanguageCore $language, DatabaseHelper $dbHelper)
|
||||
{
|
||||
$this->db = $db->getConnection();
|
||||
$this->language = $language;
|
||||
$this->dbHelper = $dbHelper;
|
||||
}
|
||||
|
||||
public function getSingleCanvas(int $canvasId): false|array
|
||||
{
|
||||
$results = $this->db->table('zp_canvas')
|
||||
->select(
|
||||
'zp_canvas.id',
|
||||
'zp_canvas.title',
|
||||
'zp_canvas.author',
|
||||
'zp_canvas.created',
|
||||
'zp_canvas.projectId',
|
||||
't1.firstname AS authorFirstname',
|
||||
't1.lastname AS authorLastname'
|
||||
)
|
||||
->leftJoin('zp_user AS t1', 'zp_canvas.author', '=', 't1.id')
|
||||
->where('type', 'idea')
|
||||
->where('zp_canvas.id', $canvasId)
|
||||
->orderBy('zp_canvas.title')
|
||||
->orderBy('zp_canvas.created')
|
||||
->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getCanvasLabels(): mixed
|
||||
{
|
||||
if (session()->exists('projectsettings.idealabels')) {
|
||||
return session('projectsettings.idealabels');
|
||||
} else {
|
||||
$result = $this->db->table('zp_settings')
|
||||
->select('value')
|
||||
->where('key', 'projectsettings.'.session('currentProject').'.idealabels')
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
$labels = [];
|
||||
|
||||
// preseed state labels with default values
|
||||
foreach ($this->canvasTypes as $key => $label) {
|
||||
$labels[$key] = [
|
||||
'name' => $this->language->__($label),
|
||||
'class' => $this->statusClasses[$key],
|
||||
];
|
||||
}
|
||||
|
||||
if ($result !== null) {
|
||||
foreach (safe_unserialize($result->value, []) as $key => $label) {
|
||||
// Ignore keys we have no status class for. Idea labels are keyed by the
|
||||
// canvasTypes strings, but a bug in the rename dialog used to persist an
|
||||
// integer 0 key, and reading it back threw "Undefined array key 0" on every
|
||||
// render — permanently 500ing the board with no way to undo it from the UI
|
||||
// (#3685). Skipping unknown keys lets an already-broken board heal itself.
|
||||
if (! isset($this->statusClasses[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$labels[$key] = [
|
||||
'name' => $label,
|
||||
'class' => $this->statusClasses[$key],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
session(['projectsettings.idealabels' => $labels]);
|
||||
|
||||
return $labels;
|
||||
}
|
||||
}
|
||||
|
||||
public function getAllCanvas(int $projectId): false|array
|
||||
{
|
||||
$results = $this->db->table('zp_canvas')
|
||||
->select(
|
||||
'zp_canvas.id',
|
||||
'zp_canvas.title',
|
||||
'zp_canvas.author',
|
||||
'zp_canvas.created',
|
||||
't1.firstname AS authorFirstname',
|
||||
't1.lastname AS authorLastname'
|
||||
)
|
||||
->leftJoin('zp_user AS t1', 'zp_canvas.author', '=', 't1.id')
|
||||
->where('type', 'idea')
|
||||
->where('projectId', $projectId)
|
||||
->orderBy('zp_canvas.title')
|
||||
->orderBy('zp_canvas.created')
|
||||
->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
|
||||
public function deleteCanvas(int $id): void
|
||||
{
|
||||
// Shared zp_canvas/zp_canvas_items: early-return unless this id is an idea board, so the
|
||||
// items-delete (by canvasId) can't drop another canvas family's items before the
|
||||
// type-guarded board delete runs.
|
||||
$isIdea = $this->db->table('zp_canvas')
|
||||
->where('id', $id)
|
||||
->where('type', 'idea')
|
||||
->exists();
|
||||
|
||||
if (! $isIdea) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->table('zp_canvas_items')
|
||||
->where('canvasId', $id)
|
||||
->delete();
|
||||
|
||||
$this->db->table('zp_canvas')
|
||||
->where('id', $id)
|
||||
->where('type', 'idea')
|
||||
->delete();
|
||||
}
|
||||
|
||||
public function addCanvas(array $values): false|string
|
||||
{
|
||||
$id = $this->db->table('zp_canvas')->insertGetId([
|
||||
'title' => $values['title'],
|
||||
'author' => $values['author'],
|
||||
'created' => now(),
|
||||
'type' => 'idea',
|
||||
'projectId' => $values['projectId'],
|
||||
]);
|
||||
|
||||
return (string) $id;
|
||||
}
|
||||
|
||||
public function updateCanvas(array $values): mixed
|
||||
{
|
||||
// type guard (shared zp_canvas across all canvas families): only ever rename an idea board.
|
||||
return $this->db->table('zp_canvas')
|
||||
->where('id', $values['id'])
|
||||
->where('type', 'idea')
|
||||
->update(['title' => $values['title']]);
|
||||
}
|
||||
|
||||
public function editCanvasItem(array $values): void
|
||||
{
|
||||
$this->db->table('zp_canvas_items')
|
||||
->where('id', $values['itemId'])
|
||||
->update([
|
||||
'description' => $values['description'],
|
||||
'assumptions' => $values['assumptions'],
|
||||
'data' => $values['data'],
|
||||
'conclusion' => $values['conclusion'],
|
||||
'modified' => now(),
|
||||
'status' => $values['status'],
|
||||
'milestoneId' => $values['milestoneId'],
|
||||
'tags' => $values['tags'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function patchCanvasItem(int $id, array $params): bool
|
||||
{
|
||||
if (isset($params['act'])) {
|
||||
unset($params['act']);
|
||||
}
|
||||
|
||||
$updateData = [];
|
||||
foreach ($params as $key => $value) {
|
||||
$sanitizedKey = DbCore::sanitizeToColumnString($key);
|
||||
$updateData[$sanitizedKey] = $value;
|
||||
}
|
||||
|
||||
return $this->db->table('zp_canvas_items')
|
||||
->where('id', $id)
|
||||
->update($updateData) >= 0;
|
||||
}
|
||||
|
||||
public function updateIdeaSorting(array $sortingArray): bool
|
||||
{
|
||||
foreach ($sortingArray as $idea) {
|
||||
$this->db->table('zp_canvas_items')
|
||||
->updateOrInsert(
|
||||
['id' => (int) $idea['id']],
|
||||
['sortindex' => (int) $idea['sortIndex']]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCanvasItemsById(int $id): false|array
|
||||
{
|
||||
$results = $this->db->table('zp_canvas_items')
|
||||
->select(
|
||||
'zp_canvas_items.id',
|
||||
'zp_canvas_items.description',
|
||||
'zp_canvas_items.assumptions',
|
||||
'zp_canvas_items.data',
|
||||
'zp_canvas_items.conclusion',
|
||||
'zp_canvas_items.box',
|
||||
'zp_canvas_items.author',
|
||||
'zp_canvas_items.created',
|
||||
'zp_canvas_items.modified',
|
||||
'zp_canvas_items.canvasId',
|
||||
'zp_canvas_items.sortindex',
|
||||
'zp_canvas_items.milestoneId',
|
||||
't1.firstname AS authorFirstname',
|
||||
't1.lastname AS authorLastname',
|
||||
't1.profileId AS authorProfileId',
|
||||
'milestone.headline as milestoneHeadline',
|
||||
'milestone.editTo as milestoneEditTo'
|
||||
)
|
||||
->selectRaw("CASE WHEN zp_canvas_items.status IS NULL THEN 'idea' ELSE zp_canvas_items.status END as status")
|
||||
->selectRaw('COUNT(DISTINCT zp_comment.id) AS '.$this->dbHelper->wrapColumn('commentCount'))
|
||||
->leftJoin('zp_user AS t1', 'zp_canvas_items.author', '=', 't1.id')
|
||||
->leftJoin('zp_tickets AS milestone', function ($join) {
|
||||
$join->on('zp_canvas_items.milestoneId', '=', $this->db->raw($this->dbHelper->castAs($this->dbHelper->wrapColumn('milestone.id'), 'text')));
|
||||
})
|
||||
->leftJoin('zp_comment', function ($join) {
|
||||
$join->on('zp_canvas_items.id', '=', 'zp_comment.moduleId')
|
||||
->where('zp_comment.module', '=', 'idea');
|
||||
})
|
||||
->where('zp_canvas_items.canvasId', $id)
|
||||
->groupBy(
|
||||
'zp_canvas_items.id',
|
||||
't1.firstname',
|
||||
't1.lastname',
|
||||
't1.profileId',
|
||||
'milestone.headline',
|
||||
'milestone.editTo',
|
||||
'zp_canvas_items.status'
|
||||
)
|
||||
->orderBy('zp_canvas_items.sortindex')
|
||||
->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
|
||||
public function getSingleCanvasItem(int $id): mixed
|
||||
{
|
||||
$result = $this->db->table('zp_canvas_items')
|
||||
->select(
|
||||
'zp_canvas_items.id',
|
||||
'zp_canvas_items.description',
|
||||
'zp_canvas_items.assumptions',
|
||||
'zp_canvas_items.data',
|
||||
'zp_canvas_items.conclusion',
|
||||
'zp_canvas_items.box',
|
||||
'zp_canvas_items.author',
|
||||
'zp_canvas_items.created',
|
||||
'zp_canvas_items.modified',
|
||||
'zp_canvas_items.canvasId',
|
||||
'zp_canvas_items.sortindex',
|
||||
'zp_canvas_items.status',
|
||||
'zp_canvas_items.tags',
|
||||
'zp_canvas_items.milestoneId',
|
||||
't1.firstname AS authorFirstname',
|
||||
't1.lastname AS authorLastname',
|
||||
'milestone.headline as milestoneHeadline',
|
||||
'milestone.editTo as milestoneEditTo'
|
||||
)
|
||||
->leftJoin('zp_tickets AS milestone', function ($join) {
|
||||
$join->on('zp_canvas_items.milestoneId', '=', $this->db->raw($this->dbHelper->castAs($this->dbHelper->wrapColumn('milestone.id'), 'text')));
|
||||
})
|
||||
->leftJoin('zp_user AS t1', 'zp_canvas_items.author', '=', 't1.id')
|
||||
->where('zp_canvas_items.id', $id)
|
||||
->first();
|
||||
|
||||
return $result ? (array) $result : false;
|
||||
}
|
||||
|
||||
public function addCanvasItem(array $values): false|string
|
||||
{
|
||||
$id = $this->db->table('zp_canvas_items')->insertGetId([
|
||||
'description' => $values['description'],
|
||||
'assumptions' => $values['assumptions'] ?? '',
|
||||
'data' => $values['data'] ?? '',
|
||||
'conclusion' => $values['conclusion'] ?? '',
|
||||
'box' => $values['box'] ?? 'idea',
|
||||
'author' => $values['author'] ?? session('userdata.id'),
|
||||
'created' => now(),
|
||||
'modified' => now(),
|
||||
'canvasId' => $values['canvasId'],
|
||||
'status' => $values['status'] ?? '',
|
||||
'milestoneId' => $values['milestoneId'] ?? '',
|
||||
]);
|
||||
|
||||
return (string) $id;
|
||||
}
|
||||
|
||||
public function delCanvasItem(int $id): void
|
||||
{
|
||||
$this->db->table('zp_canvas_items')
|
||||
->where('id', $id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
public function updateIdeaStatus(int $ideaId, string $status): bool
|
||||
{
|
||||
return $this->db->table('zp_canvas_items')
|
||||
->where('id', $ideaId)
|
||||
->update(['box' => $status]) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|mixed
|
||||
*/
|
||||
public function getNumberOfIdeas(?int $projectId = null): mixed
|
||||
{
|
||||
$query = $this->db->table('zp_canvas_items')
|
||||
->leftJoin('zp_canvas AS canvasBoard', 'zp_canvas_items.canvasId', '=', 'canvasBoard.id')
|
||||
->where('canvasBoard.type', 'idea');
|
||||
|
||||
if ($projectId !== null) {
|
||||
$query->where('canvasBoard.projectId', $projectId);
|
||||
}
|
||||
|
||||
return $query->count('zp_canvas_items.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|mixed
|
||||
*/
|
||||
public function getNumberOfBoards(?int $projectId = null): mixed
|
||||
{
|
||||
$query = $this->db->table('zp_canvas')
|
||||
->where('type', 'idea');
|
||||
|
||||
if ($projectId !== null) {
|
||||
$query->where('projectId', $projectId);
|
||||
}
|
||||
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
public function bulkUpdateIdeaStatus(array $params): bool
|
||||
{
|
||||
// Jquery sortable serializes the array for kanban in format
|
||||
// statusKey: item[]=X&item[]=X2...,
|
||||
// statusKey2: item[]=X&item[]=X2...,
|
||||
// This represents status & kanban sorting
|
||||
foreach ($params as $status => $ideaList) {
|
||||
$ideas = explode('&', $ideaList);
|
||||
|
||||
foreach ($ideas as $key => $ideaString) {
|
||||
if (strlen($ideaString) > 0) {
|
||||
$id = substr($ideaString, 7);
|
||||
|
||||
if ($this->updateIdeaStatus((int) $id, $status) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAllIdeas(?int $projectId, ?int $boardId): array|false
|
||||
{
|
||||
$userId = session('userdata.id') ?? -1;
|
||||
$clientId = session('userdata.clientId') ?? -1;
|
||||
$requesterRole = session()->exists('userdata') ? session('userdata.role') : -1;
|
||||
|
||||
$query = $this->db->table('zp_canvas_items')
|
||||
->select(
|
||||
'zp_canvas_items.id',
|
||||
'zp_canvas_items.description',
|
||||
'zp_canvas_items.assumptions',
|
||||
'zp_canvas_items.data',
|
||||
'zp_canvas_items.conclusion',
|
||||
'zp_canvas_items.box',
|
||||
'zp_canvas_items.author',
|
||||
'zp_canvas_items.created',
|
||||
'zp_canvas_items.modified',
|
||||
'zp_canvas_items.canvasId',
|
||||
'zp_canvas_items.sortindex',
|
||||
'zp_canvas_items.status',
|
||||
'zp_canvas_items.tags',
|
||||
'zp_canvas_items.milestoneId',
|
||||
'zp_canvas.projectId'
|
||||
)
|
||||
->leftJoin('zp_canvas', 'zp_canvas_items.canvasId', '=', 'zp_canvas.id')
|
||||
->leftJoin('zp_projects', 'zp_canvas.projectId', '=', 'zp_projects.id')
|
||||
->where('zp_canvas_items.box', 'idea')
|
||||
->where(function ($q) use ($userId, $clientId, $requesterRole) {
|
||||
$q->whereIn('zp_canvas.projectId', function ($subquery) use ($userId) {
|
||||
$subquery->select('projectId')
|
||||
->from('zp_relationuserproject')
|
||||
->where('userId', $userId);
|
||||
})
|
||||
->orWhere('zp_projects.psettings', 'all')
|
||||
->orWhere(function ($q2) use ($clientId) {
|
||||
$q2->where('zp_projects.psettings', 'clients')
|
||||
->where('zp_projects.clientId', $clientId);
|
||||
});
|
||||
// Admin and manager roles have access to all projects
|
||||
if (in_array($requesterRole, ['admin', 'manager'])) {
|
||||
$q->orWhereRaw('1=1');
|
||||
}
|
||||
});
|
||||
|
||||
if (isset($projectId) && $projectId > 0) {
|
||||
$query->where('zp_canvas.projectId', $projectId);
|
||||
}
|
||||
|
||||
if (isset($boardId) && $boardId > 0) {
|
||||
$query->where('zp_canvas.id', $boardId);
|
||||
}
|
||||
|
||||
$results = $query->get();
|
||||
|
||||
return array_map(fn ($item) => (array) $item, $results->toArray());
|
||||
}
|
||||
}
|
||||
983
app/Domain/Ideas/Services/Ideas.php
Normal file
983
app/Domain/Ideas/Services/Ideas.php
Normal file
@@ -0,0 +1,983 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Ideas\Services;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Domains\BaseService;
|
||||
use Leantime\Core\Language as LanguageCore;
|
||||
use Leantime\Core\Mailer as MailerCore;
|
||||
use Leantime\Domain\Comments\Permissions\CommentsPermissions;
|
||||
use Leantime\Domain\Comments\Repositories\Comments as CommentRepository;
|
||||
use Leantime\Domain\Ideas\Permissions\IdeasPermissions;
|
||||
use Leantime\Domain\Ideas\Repositories\Ideas as IdeasRepository;
|
||||
use Leantime\Domain\Notifications\Models\Notification as NotificationModel;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Queue\Repositories\Queue as QueueRepository;
|
||||
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
|
||||
|
||||
/**
|
||||
* Ideas service - business logic for idea boards and idea items.
|
||||
*/
|
||||
class Ideas extends BaseService
|
||||
{
|
||||
private IdeasRepository $ideasRepository;
|
||||
|
||||
private CommentRepository $commentsRepository;
|
||||
|
||||
private ProjectService $projectService;
|
||||
|
||||
private TicketService $ticketService;
|
||||
|
||||
private LanguageCore $language;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
IdeasRepository $ideasRepository,
|
||||
CommentRepository $commentsRepository,
|
||||
ProjectService $projectService,
|
||||
TicketService $ticketService,
|
||||
LanguageCore $language
|
||||
) {
|
||||
$this->ideasRepository = $ideasRepository;
|
||||
$this->commentsRepository = $commentsRepository;
|
||||
$this->projectService = $projectService;
|
||||
$this->ticketService = $ticketService;
|
||||
$this->language = $language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an idea board's owning project. Boards are zp_canvas rows (type 'idea').
|
||||
*
|
||||
* @param int $boardId The board (canvas) id
|
||||
* @return int|null The board's project id, or null when the board does not exist
|
||||
*/
|
||||
private function boardProjectId(int $boardId): ?int
|
||||
{
|
||||
// getSingleCanvas() returns a list of row arrays (not a flat row).
|
||||
$rows = $this->ideasRepository->getSingleCanvas($boardId);
|
||||
|
||||
return isset($rows[0]['projectId']) ? (int) $rows[0]['projectId'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an idea item's owning project (item -> its board/canvas -> project).
|
||||
*
|
||||
* The repository operates by item id with NO project scoping, and zp_canvas_items is shared
|
||||
* across all canvas types, so callers MUST fail closed on a null result before any read/write.
|
||||
*
|
||||
* @param int $itemId The canvas item id
|
||||
* @return int|null The item's project id, or null when it does not resolve to an idea board
|
||||
*/
|
||||
private function canvasItemProjectId(int $itemId): ?int
|
||||
{
|
||||
$item = $this->ideasRepository->getSingleCanvasItem($itemId);
|
||||
if (! is_array($item) || empty($item['canvasId'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->boardProjectId((int) $item['canvasId']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorized JSON-RPC entry point: persist a new idea sort order.
|
||||
*
|
||||
* Requires editor+ and per-item project access (JSON-RPC has no controller
|
||||
* gate, and the repository sorts by item id with no project scoping).
|
||||
*
|
||||
* @param array $payload List of { id, sortIndex } entries
|
||||
* @return bool True on success, false if unauthorized or the update failed
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::EDIT, entityScoped: true)]
|
||||
public function reorderIdeas(array $payload): bool
|
||||
{
|
||||
// Per-item project fence: reject the whole batch unless the user may edit EVERY item's
|
||||
// project (non-throwing can(), since a batch should fail gracefully). null project = the id
|
||||
// is not an idea item (shared zp_canvas_items) -> reject.
|
||||
foreach ($payload as $idea) {
|
||||
if (! isset($idea['id'])) {
|
||||
return false;
|
||||
}
|
||||
$projectId = $this->canvasItemProjectId((int) $idea['id']);
|
||||
if ($projectId === null || ! $this->can(IdeasPermissions::EDIT, $projectId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->ideasRepository->updateIdeaSorting($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorized JSON-RPC entry point: bulk status/sort update from the idea kanban.
|
||||
*
|
||||
* Requires editor+ and project access for every item in the (jQuery-sortable
|
||||
* serialized) payload.
|
||||
*
|
||||
* @param array $payload Map of statusKey => "item[]=ID&item[]=ID2..."
|
||||
* @return bool True on success, false if unauthorized or the update failed
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::EDIT, entityScoped: true)]
|
||||
public function bulkUpdateStatus(array $payload): bool
|
||||
{
|
||||
// Per-item project fence over the jQuery-sortable serialized payload.
|
||||
foreach ($payload as $itemList) {
|
||||
foreach (explode('&', (string) $itemList) as $itemString) {
|
||||
// jQuery sortable serializes as "item[]=ID"; strip the prefix.
|
||||
$id = (int) substr($itemString, 7);
|
||||
if ($id <= 0) {
|
||||
continue;
|
||||
}
|
||||
$projectId = $this->canvasItemProjectId($id);
|
||||
if ($projectId === null || ! $this->can(IdeasPermissions::EDIT, $projectId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->ideasRepository->bulkUpdateIdeaStatus($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorized JSON-RPC entry point: patch a single idea/canvas item.
|
||||
*
|
||||
* Requires editor+ and access to the item's project.
|
||||
*
|
||||
* @param int $id The canvas item id
|
||||
* @param array $params Fields to update
|
||||
* @return bool True on success, false if unauthorized or the update failed
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::EDIT, entityScoped: true)]
|
||||
public function patchIdeaItem(int $id, array $params): bool
|
||||
{
|
||||
// Fail closed: resolve the item's REAL project (shared zp_canvas_items) and require edit
|
||||
// there before patching — null means the id is not an idea item, so refuse.
|
||||
$projectId = $this->canvasItemProjectId($id);
|
||||
if ($projectId === null) {
|
||||
return false;
|
||||
}
|
||||
$this->authorize(IdeasPermissions::EDIT, $projectId);
|
||||
|
||||
// Strip relocation/identity fields: patchCanvasItem updates any column it receives, so a
|
||||
// JSON-RPC caller could otherwise patch canvasId to move the item to another board/project
|
||||
// (bypassing the project scoping just authorized) or rewrite its id/author.
|
||||
unset($params['canvasId'], $params['id'], $params['author']);
|
||||
|
||||
return $this->ideasRepository->patchCanvasItem($id, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls for new ideas in a project / board, normalizing dates for the API.
|
||||
*
|
||||
* @param int|null $projectId Project to filter by, or null for all accessible projects.
|
||||
* @param int|null $board Board to filter by, or null for all boards.
|
||||
* @return array<int, array<string, mixed>> List of ideas with ISO8601 dates.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, projectIdParam: 'projectId')]
|
||||
public function pollForNewIdeas(?int $projectId = null, ?int $board = null): array
|
||||
{
|
||||
$ideas = $this->ideasRepository->getAllIdeas($projectId, $board);
|
||||
|
||||
foreach ($ideas as $key => $idea) {
|
||||
$ideas[$key] = $this->prepareDatesForApiResponse($idea);
|
||||
}
|
||||
|
||||
return $ideas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls for updated ideas, appending the modified timestamp to the id for change detection.
|
||||
*
|
||||
* @param int|null $projectId Project to filter by, or null for all accessible projects.
|
||||
* @param int|null $board Board to filter by, or null for all boards.
|
||||
* @return array<int, array<string, mixed>> List of ideas with ISO8601 dates and composite ids.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, projectIdParam: 'projectId')]
|
||||
public function pollForUpdatedIdeas(?int $projectId = null, ?int $board = null): array
|
||||
{
|
||||
$ideas = $this->ideasRepository->getAllIdeas($projectId, $board);
|
||||
|
||||
foreach ($ideas as $key => $idea) {
|
||||
$ideas[$key] = $this->prepareDatesForApiResponse($idea);
|
||||
$ideas[$key]['id'] = $idea['id'].'-'.$idea['modified'];
|
||||
}
|
||||
|
||||
return $ideas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a single idea's created/modified dates to ISO8601 Zulu strings.
|
||||
*
|
||||
* @param array<string, mixed> $idea Raw idea row.
|
||||
* @return array<string, mixed> Idea with normalized dates.
|
||||
*/
|
||||
private function prepareDatesForApiResponse(array $idea): array
|
||||
{
|
||||
if (dtHelper()->isValidDateString($idea['created'])) {
|
||||
$idea['created'] = dtHelper()->parseDbDateTime($idea['created'])->toIso8601ZuluString();
|
||||
} else {
|
||||
$idea['created'] = null;
|
||||
}
|
||||
|
||||
if (dtHelper()->isValidDateString($idea['modified'])) {
|
||||
$idea['modified'] = dtHelper()->parseDbDateTime($idea['modified'])->toIso8601ZuluString();
|
||||
} else {
|
||||
$idea['modified'] = null;
|
||||
}
|
||||
|
||||
return $idea;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all idea boards for a project.
|
||||
*
|
||||
* @param int $projectId Project id.
|
||||
* @return array<int, array<string, mixed>> List of boards.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, projectIdParam: 'projectId')]
|
||||
public function getAllBoards(int $projectId): array
|
||||
{
|
||||
$allCanvas = $this->ideasRepository->getAllCanvas($projectId);
|
||||
|
||||
return $allCanvas === false ? [] : $allCanvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single idea board by id.
|
||||
*
|
||||
* @param int $id Board id.
|
||||
* @return array<int, array<string, mixed>> Single board rows (matching repository shape).
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, entityScoped: true)]
|
||||
public function getBoard(int $id): array
|
||||
{
|
||||
$singleCanvas = $this->ideasRepository->getSingleCanvas($id);
|
||||
if (empty($singleCanvas)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// IDOR fence: authorize VIEW against the board's real project (single-entity-by-id read).
|
||||
$this->authorize(IdeasPermissions::VIEW, isset($singleCanvas[0]['projectId']) ? (int) $singleCanvas[0]['projectId'] : null);
|
||||
|
||||
return $singleCanvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title of a single idea board, or an empty string if not found.
|
||||
*
|
||||
* @param int $id Board id.
|
||||
* @return string Board title.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, entityScoped: true)]
|
||||
public function getBoardTitle(int $id): string
|
||||
{
|
||||
// Delegates to getBoard(), which performs the in-body VIEW authorize against the board's project.
|
||||
$singleCanvas = $this->getBoard($id);
|
||||
|
||||
return $singleCanvas[0]['title'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the canvas items for a board.
|
||||
*
|
||||
* @param int $boardId Board id.
|
||||
* @return array<int, array<string, mixed>> Canvas items.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, entityScoped: true)]
|
||||
public function getBoardItems(int $boardId): array
|
||||
{
|
||||
// IDOR fence: authorize VIEW against the board's real project before listing its items.
|
||||
$projectId = $this->boardProjectId($boardId);
|
||||
if ($projectId === null) {
|
||||
return [];
|
||||
}
|
||||
$this->authorize(IdeasPermissions::VIEW, $projectId);
|
||||
|
||||
$items = $this->ideasRepository->getCanvasItemsById($boardId);
|
||||
|
||||
return $items === false ? [] : $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured idea status labels.
|
||||
*
|
||||
* @return mixed Label structure (array keyed by status).
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function getBoardLabels(): mixed
|
||||
{
|
||||
return $this->ideasRepository->getCanvasLabels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new idea board, queuing a creation notification to project users.
|
||||
*
|
||||
* @param string $title Board title.
|
||||
* @param int $projectId Project the board belongs to.
|
||||
* @param int $authorId Author user id.
|
||||
* @return int The new board id.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::CREATE, entityScoped: true)]
|
||||
public function createBoard(string $title, int $projectId, int $authorId): int
|
||||
{
|
||||
// A board belongs directly to $projectId; authorize CREATE there before writing.
|
||||
$this->authorize(IdeasPermissions::CREATE, $projectId);
|
||||
|
||||
$values = [
|
||||
'title' => $title,
|
||||
'author' => $authorId,
|
||||
'projectId' => $projectId,
|
||||
];
|
||||
|
||||
$boardId = (int) $this->ideasRepository->addCanvas($values);
|
||||
|
||||
$this->notifyBoardCreated($title, $projectId);
|
||||
|
||||
return $boardId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new idea board from the board dialog, queuing the dialog-specific
|
||||
* creation notification to project users.
|
||||
*
|
||||
* This intentionally uses a different notification subject/message than
|
||||
* {@see self::createBoard()} to preserve the historical board-dialog behavior.
|
||||
*
|
||||
* @param string $title Board title.
|
||||
* @param int $projectId Project the board belongs to.
|
||||
* @param int $authorId Author user id.
|
||||
* @return int The new board id.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::CREATE, entityScoped: true)]
|
||||
public function createBoardFromDialog(string $title, int $projectId, int $authorId): int
|
||||
{
|
||||
// A board belongs directly to $projectId; authorize CREATE there before writing.
|
||||
$this->authorize(IdeasPermissions::CREATE, $projectId);
|
||||
|
||||
$values = [
|
||||
'title' => $title,
|
||||
'author' => $authorId,
|
||||
'projectId' => $projectId,
|
||||
];
|
||||
|
||||
$boardId = (int) $this->ideasRepository->addCanvas($values);
|
||||
|
||||
$this->notifyBoardCreatedFromDialog($title, $projectId);
|
||||
|
||||
return $boardId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an idea board title.
|
||||
*
|
||||
* @param int $id Board id.
|
||||
* @param string $title New title.
|
||||
* @return mixed Result of the underlying update.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::EDIT, entityScoped: true)]
|
||||
public function updateBoard(int $id, string $title): mixed
|
||||
{
|
||||
// Fail closed: resolve the board's real project and require edit there before renaming.
|
||||
$projectId = $this->boardProjectId($id);
|
||||
if ($projectId === null) {
|
||||
return false;
|
||||
}
|
||||
$this->authorize(IdeasPermissions::EDIT, $projectId);
|
||||
|
||||
return $this->ideasRepository->updateCanvas(['title' => $title, 'id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures at least one board exists for a project, creating a default one if none do.
|
||||
*
|
||||
* Returns the id of the newly created default board if one was created, or 0 if boards
|
||||
* already existed (no board created).
|
||||
*
|
||||
* @param int $projectId Project id.
|
||||
* @param int $authorId Author user id for the default board.
|
||||
* @param array<int, array<string, mixed>> $allBoards Already-loaded boards for the project.
|
||||
* @return int Newly created board id, or 0 if none was created.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, projectIdParam: 'projectId')]
|
||||
public function ensureBoardExists(int $projectId, int $authorId, array $allBoards): int
|
||||
{
|
||||
if (count($allBoards) > 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Bootstrap only: the default board is created as a SIDE EFFECT of viewing a board-less
|
||||
// project, so it is VIEW-gated and writes straight to the repository — gating it CREATE
|
||||
// would 403 a readonly viewer (mirrors Wiki's getAllProjectWikis default-notebook bootstrap).
|
||||
$values = [
|
||||
'title' => $this->language->__('label.board'),
|
||||
'author' => $authorId,
|
||||
'projectId' => $projectId,
|
||||
];
|
||||
|
||||
return (int) $this->ideasRepository->addCanvas($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends (queues) a board-creation notification to project users.
|
||||
*
|
||||
* @param string $title Board title.
|
||||
* @param int $projectId Project id.
|
||||
*/
|
||||
private function notifyBoardCreated(string $title, int $projectId): void
|
||||
{
|
||||
$mailer = app()->make(MailerCore::class);
|
||||
$mailer->setContext('idea_board_created');
|
||||
$users = $this->projectService->getUsersToNotify($projectId);
|
||||
|
||||
$mailer->setSubject($this->language->__('email_notifications.idea_board_created_subject'));
|
||||
$message = sprintf(
|
||||
$this->language->__('email_notifications.idea_board_created_message'),
|
||||
session('userdata.name'),
|
||||
"<a href='".CURRENT_URL."'>".strip_tags($title).'</a>.<br />'
|
||||
);
|
||||
$mailer->setHtml($message);
|
||||
|
||||
$queue = app()->make(QueueRepository::class);
|
||||
$queue->queueMessageToUsers(
|
||||
$users,
|
||||
$message,
|
||||
$this->language->__('email_notifications.idea_board_created_subject'),
|
||||
$projectId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends (queues) the board-dialog-specific board-creation notification to project users.
|
||||
*
|
||||
* @param string $title Board title.
|
||||
* @param int $projectId Project id.
|
||||
*/
|
||||
private function notifyBoardCreatedFromDialog(string $title, int $projectId): void
|
||||
{
|
||||
$mailer = app()->make(MailerCore::class);
|
||||
$users = $this->projectService->getUsersToNotify($projectId);
|
||||
|
||||
$mailer->setSubject($this->language->__('notification.board_created'));
|
||||
$message = sprintf(
|
||||
$this->language->__('email_notifications.canvas_created_message'),
|
||||
session('userdata.name'),
|
||||
"<a href='".CURRENT_URL."'>".strip_tags($title).'</a>'
|
||||
);
|
||||
$mailer->setHtml($message);
|
||||
|
||||
$queue = app()->make(QueueRepository::class);
|
||||
$queue->queueMessageToUsers(
|
||||
$users,
|
||||
$message,
|
||||
$this->language->__('notification.board_created'),
|
||||
$projectId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a single idea item for display, normalizing the box value and
|
||||
* returning an empty-item default when no id is supplied.
|
||||
*
|
||||
* @param int|null $id Idea item id, or null when creating a new item.
|
||||
* @param string $type Default box/type for a new item.
|
||||
* @return array<string, mixed> Idea item (normalized) or empty-item default.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, entityScoped: true)]
|
||||
public function getIdeaItem(?int $id, string $type = 'idea'): array
|
||||
{
|
||||
if ($id === null) {
|
||||
return [
|
||||
'id' => '',
|
||||
'box' => $type,
|
||||
'tags' => '',
|
||||
'description' => '',
|
||||
'status' => 'idea',
|
||||
'assumptions' => '',
|
||||
'data' => '',
|
||||
'conclusion' => '',
|
||||
'milestoneHeadline' => '',
|
||||
'milestoneId' => '',
|
||||
];
|
||||
}
|
||||
|
||||
// IDOR fence (single-entity-by-id read): fetch the item ONCE, derive its project from the
|
||||
// owning board, authorize VIEW, then return the already-fetched row — no duplicate query.
|
||||
$canvasItem = $this->ideasRepository->getSingleCanvasItem($id);
|
||||
if (! is_array($canvasItem) || empty($canvasItem['canvasId'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$projectId = $this->boardProjectId((int) $canvasItem['canvasId']);
|
||||
if ($projectId === null) {
|
||||
return [];
|
||||
}
|
||||
$this->authorize(IdeasPermissions::VIEW, $projectId);
|
||||
|
||||
if (isset($canvasItem['box']) && $canvasItem['box'] == '0') {
|
||||
$canvasItem['box'] = 'idea';
|
||||
}
|
||||
|
||||
return $canvasItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single raw idea item without the display normalization applied by
|
||||
* {@see self::getIdeaItem()}. A null id is coerced to 0 (no match), mirroring
|
||||
* the historical repository call.
|
||||
*
|
||||
* @param int|null $id Idea item id.
|
||||
* @return mixed The idea item array, or false when not found.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, entityScoped: true)]
|
||||
public function getRawIdeaItem(?int $id): mixed
|
||||
{
|
||||
// IDOR fence (single-entity-by-id read): fetch the item ONCE, derive its project from the
|
||||
// owning board, authorize VIEW, then return the already-fetched row — no duplicate query.
|
||||
$canvasItem = $this->ideasRepository->getSingleCanvasItem((int) $id);
|
||||
if (! is_array($canvasItem) || empty($canvasItem['canvasId'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$projectId = $this->boardProjectId((int) $canvasItem['canvasId']);
|
||||
if ($projectId === null) {
|
||||
return false;
|
||||
}
|
||||
$this->authorize(IdeasPermissions::VIEW, $projectId);
|
||||
|
||||
return $canvasItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the canvas type map (status keys to label keys).
|
||||
*
|
||||
* @return array<string, string> Canvas types.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW)]
|
||||
public function getCanvasTypes(): array
|
||||
{
|
||||
return $this->ideasRepository->canvasTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new idea item and queues a creation notification to project users.
|
||||
*
|
||||
* @param array<string, mixed> $input Idea item input (box, description, status, data, canvasId, ...).
|
||||
* @param int $projectId Project id for the notification.
|
||||
* @param int $authorId Author user id.
|
||||
* @return int The new idea item id.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::CREATE, entityScoped: true)]
|
||||
public function createIdeaItem(array $input, int $projectId, int $authorId): int
|
||||
{
|
||||
// Authorize CREATE against the TARGET board's real project (resolved from canvasId; the
|
||||
// passed projectId is untrusted). FAIL CLOSED if the canvasId is not an idea board — never
|
||||
// fall back to the caller-supplied projectId, or a foreign/non-idea canvasId could be
|
||||
// created against the caller's own project.
|
||||
$boardProjectId = $this->boardProjectId((int) ($input['canvasId'] ?? 0));
|
||||
if ($boardProjectId === null) {
|
||||
return 0;
|
||||
}
|
||||
$this->authorize(IdeasPermissions::CREATE, $boardProjectId);
|
||||
|
||||
// Normalize to the resolved board project so the notification below targets the correct
|
||||
// project's users even if a mismatched/forged projectId was supplied.
|
||||
$projectId = $boardProjectId;
|
||||
|
||||
$canvasItem = [
|
||||
'box' => $input['box'],
|
||||
'author' => $authorId,
|
||||
'description' => $input['description'],
|
||||
'status' => $input['status'],
|
||||
'assumptions' => '',
|
||||
'data' => $input['data'],
|
||||
'conclusion' => '',
|
||||
'canvasId' => $input['canvasId'],
|
||||
];
|
||||
|
||||
$id = (int) $this->ideasRepository->addCanvasItem($canvasItem);
|
||||
$canvasItem['id'] = $id;
|
||||
|
||||
$subject = $this->language->__('email_notifications.idea_created_subject');
|
||||
$actualLink = BASE_URL.'#/ideas/ideaDialog/'.$id;
|
||||
$message = sprintf(
|
||||
$this->language->__('email_notifications.idea_created_message'),
|
||||
session('userdata.name'),
|
||||
strip_tags($input['description'])
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => $actualLink,
|
||||
'text' => $this->language->__('email_notifications.idea_created_subject'),
|
||||
];
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = 'ideas';
|
||||
$notification->action = 'created';
|
||||
$notification->projectId = $projectId;
|
||||
$notification->subject = $subject;
|
||||
$notification->authorId = $authorId;
|
||||
$notification->message = $message;
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing idea item, optionally quick-adding or attaching a milestone,
|
||||
* then queues an edit notification to project users.
|
||||
*
|
||||
* @param array<string, mixed> $input Idea item input including itemId and optional milestone fields.
|
||||
* @param int $projectId Project id for the notification.
|
||||
* @param int $authorId Author user id.
|
||||
* @return int The idea item id that was updated.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::EDIT, entityScoped: true)]
|
||||
public function updateIdeaItem(array $input, int $projectId, int $authorId): int
|
||||
{
|
||||
$itemId = (int) $input['itemId'];
|
||||
|
||||
// Fail closed: resolve the EXISTING item's real project (shared zp_canvas_items) and require
|
||||
// edit there before writing — the passed projectId/canvasId are untrusted, so an editor in
|
||||
// project A cannot edit or relocate an item in project B.
|
||||
$existingProjectId = $this->canvasItemProjectId($itemId);
|
||||
if ($existingProjectId === null) {
|
||||
return 0;
|
||||
}
|
||||
$this->authorize(IdeasPermissions::EDIT, $existingProjectId);
|
||||
|
||||
// The write persists $input['canvasId'], which can RELOCATE the item to another board. The
|
||||
// target must be an idea board the user can also edit, or the relocation is a cross-project
|
||||
// move. Fail closed if it's not an idea board; require edit on the target if it differs.
|
||||
$targetProjectId = $this->boardProjectId((int) ($input['canvasId'] ?? 0));
|
||||
if ($targetProjectId === null) {
|
||||
return 0;
|
||||
}
|
||||
if ($targetProjectId !== $existingProjectId) {
|
||||
$this->authorize(IdeasPermissions::EDIT, $targetProjectId);
|
||||
}
|
||||
|
||||
// Normalize to the resolved board project for the notification below.
|
||||
$projectId = $targetProjectId;
|
||||
|
||||
$canvasItem = [
|
||||
'box' => $input['box'],
|
||||
'author' => $authorId,
|
||||
'description' => $input['description'],
|
||||
'status' => $input['status'],
|
||||
'assumptions' => '',
|
||||
'data' => $input['data'],
|
||||
'conclusion' => '',
|
||||
'tags' => $input['tags'],
|
||||
'itemId' => $input['itemId'],
|
||||
'canvasId' => $input['canvasId'],
|
||||
'milestoneId' => $input['milestoneId'],
|
||||
'id' => $input['itemId'],
|
||||
];
|
||||
|
||||
if (isset($input['newMilestone']) && $input['newMilestone'] != '') {
|
||||
$milestone = [];
|
||||
$milestone['headline'] = $input['newMilestone'];
|
||||
$milestone['tags'] = '#ccc';
|
||||
$milestone['editFrom'] = dtHelper()->userNow()->formatDateForUser();
|
||||
$milestone['editTo'] = dtHelper()->userNow()->addDays(7)->formatDateForUser();
|
||||
$milestoneId = $this->ticketService->quickAddMilestone($milestone);
|
||||
if ($milestoneId !== false) {
|
||||
$canvasItem['milestoneId'] = $milestoneId;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($input['existingMilestone']) && $input['existingMilestone'] != '') {
|
||||
$canvasItem['milestoneId'] = $input['existingMilestone'];
|
||||
}
|
||||
|
||||
$this->ideasRepository->editCanvasItem($canvasItem);
|
||||
|
||||
$subject = $this->language->__('email_notifications.idea_edited_subject');
|
||||
$actualLink = BASE_URL.'#/ideas/ideaDialog/'.$itemId;
|
||||
$message = sprintf(
|
||||
$this->language->__('notification.idea_edited'),
|
||||
session('userdata.name'),
|
||||
strip_tags($input['description'])
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => $actualLink,
|
||||
'text' => $this->language->__('email_notifications.idea_edited_cta'),
|
||||
];
|
||||
$notification->entity = $canvasItem;
|
||||
$notification->module = 'ideas';
|
||||
$notification->action = 'updated';
|
||||
$notification->projectId = $projectId;
|
||||
$notification->subject = $subject;
|
||||
$notification->authorId = $authorId;
|
||||
$notification->message = $message;
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
|
||||
return $itemId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches a milestone from an idea item.
|
||||
*
|
||||
* @param int $ideaItemId Idea item id.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::EDIT, entityScoped: true)]
|
||||
public function removeMilestone(int $ideaItemId): void
|
||||
{
|
||||
// Fail closed: resolve the item's real project and require edit there before detaching.
|
||||
$projectId = $this->canvasItemProjectId($ideaItemId);
|
||||
if ($projectId === null) {
|
||||
return;
|
||||
}
|
||||
$this->authorize(IdeasPermissions::EDIT, $projectId);
|
||||
|
||||
$this->ideasRepository->patchCanvasItem($ideaItemId, ['milestoneId' => '']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all milestones available to attach to an idea item, for the current project.
|
||||
*
|
||||
* @param int $projectId Project id.
|
||||
* @return array<int, mixed>|false Milestones.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, projectIdParam: 'projectId')]
|
||||
public function getProjectMilestones(int $projectId): array|false
|
||||
{
|
||||
return $this->ticketService->getAllMilestones([
|
||||
'sprint' => '',
|
||||
'type' => 'milestone',
|
||||
'currentProject' => $projectId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a comment to an idea item and queues a notification to project users.
|
||||
*
|
||||
* @param string $text Comment text.
|
||||
* @param int $ideaItemId Idea item (module) id.
|
||||
* @param int|string $parentCommentId Parent comment id ('father').
|
||||
* @param int $projectId Project id for the notification.
|
||||
* @param int $authorId Author user id.
|
||||
* @return false|string The new comment id, or false on failure.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(CommentsPermissions::CREATE, entityScoped: true)]
|
||||
public function addIdeaComment(string $text, int $ideaItemId, int|string $parentCommentId, int $projectId, int $authorId): false|string
|
||||
{
|
||||
// Commenting on an idea is a commenter+ capability in the idea's project (resolved from the
|
||||
// item; the passed projectId is untrusted). FAIL CLOSED if the id is not an idea item —
|
||||
// never fall back to the caller-supplied projectId.
|
||||
$itemProjectId = $this->canvasItemProjectId($ideaItemId);
|
||||
if ($itemProjectId === null) {
|
||||
return false;
|
||||
}
|
||||
$this->authorize(CommentsPermissions::CREATE, $itemProjectId);
|
||||
|
||||
$values = [
|
||||
'text' => $text,
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
'userId' => $authorId,
|
||||
'moduleId' => $ideaItemId,
|
||||
'commentParent' => $parentCommentId,
|
||||
];
|
||||
|
||||
$commentId = $this->commentsRepository->addComment($values, 'idea');
|
||||
$values['id'] = $commentId;
|
||||
|
||||
$subject = $this->language->__('email_notifications.new_comment_idea_subject');
|
||||
$actualLink = BASE_URL.'#/ideas/ideaDialog/'.$ideaItemId;
|
||||
$message = sprintf(
|
||||
$this->language->__('email_notifications.new_comment_idea_message'),
|
||||
session('userdata.name')
|
||||
);
|
||||
|
||||
$notification = app()->make(NotificationModel::class);
|
||||
$notification->url = [
|
||||
'url' => $actualLink,
|
||||
'text' => $this->language->__('email_notifications.new_comment_idea_cta'),
|
||||
];
|
||||
$notification->entity = $values;
|
||||
$notification->module = 'comments';
|
||||
$notification->action = 'commented';
|
||||
$notification->projectId = $projectId;
|
||||
$notification->subject = $subject;
|
||||
$notification->authorId = $authorId;
|
||||
$notification->message = $message;
|
||||
|
||||
$this->projectService->notifyProjectUsers($notification);
|
||||
|
||||
return $commentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a comment.
|
||||
*
|
||||
* @param int $commentId Comment id.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(CommentsPermissions::MODERATE, entityScoped: true)]
|
||||
public function removeIdeaComment(int $commentId): void
|
||||
{
|
||||
// Was an UNGATED delete-by-id (reachable via the ?delComment GET param) — any user could
|
||||
// delete any comment by id. Fence: the comment author may delete their own; otherwise
|
||||
// require moderate in the idea's project (resolved from the comment's idea item).
|
||||
$comment = $this->commentsRepository->getComment($commentId);
|
||||
if (! $comment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((int) ($comment['userId'] ?? 0) !== (int) session('userdata.id')) {
|
||||
// Non-author: require moderate in the idea's project. FAIL CLOSED if the comment's item
|
||||
// does not resolve to an idea board — never downgrade to a role-only moderate check.
|
||||
$projectId = $this->canvasItemProjectId((int) ($comment['moduleId'] ?? 0));
|
||||
if ($projectId === null) {
|
||||
return;
|
||||
}
|
||||
$this->authorize(CommentsPermissions::MODERATE, $projectId);
|
||||
}
|
||||
|
||||
$this->commentsRepository->deleteComment($commentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the comments for a module/entity.
|
||||
*
|
||||
* @param string $module Comment module key.
|
||||
* @param int $entityId Entity id.
|
||||
* @return array<int, mixed>|false Comments.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, entityScoped: true)]
|
||||
public function getIdeaComments(string $module, int $entityId): array|false
|
||||
{
|
||||
// Fail closed: a null project means $entityId is not an idea item, so refuse rather than
|
||||
// authorize against null (role-only pass) and leak another project's comment thread by id.
|
||||
$projectId = $this->canvasItemProjectId($entityId);
|
||||
if ($projectId === null) {
|
||||
return [];
|
||||
}
|
||||
$this->authorize(IdeasPermissions::VIEW, $projectId);
|
||||
|
||||
return $this->commentsRepository->getComments($module, $entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of comments for a module/entity.
|
||||
*
|
||||
* @param string $module Comment module key.
|
||||
* @param int $entityId Entity id.
|
||||
* @return mixed Comment count.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::VIEW, entityScoped: true)]
|
||||
public function countIdeaComments(string $module, int $entityId): mixed
|
||||
{
|
||||
// Fail closed: a null project means $entityId is not an idea item.
|
||||
$projectId = $this->canvasItemProjectId($entityId);
|
||||
if ($projectId === null) {
|
||||
return 0;
|
||||
}
|
||||
$this->authorize(IdeasPermissions::VIEW, $projectId);
|
||||
|
||||
return $this->commentsRepository->countComments($module, $entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an idea board, fencing the operation against the board's project.
|
||||
*
|
||||
* Replaces the previous controller->repository direct delete (which only had a global-role
|
||||
* gate and no project scoping, so an editor in any project could delete another's board).
|
||||
*
|
||||
* @param int $id Board (canvas) id.
|
||||
* @return bool True when deleted, false when the board does not resolve to a project.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::DELETE, entityScoped: true)]
|
||||
public function deleteCanvas(int $id): bool
|
||||
{
|
||||
$projectId = $this->boardProjectId($id);
|
||||
if ($projectId === null) {
|
||||
return false;
|
||||
}
|
||||
$this->authorize(IdeasPermissions::DELETE, $projectId);
|
||||
|
||||
$this->ideasRepository->deleteCanvas($id);
|
||||
session()->forget('currentIdeaCanvas');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an idea item, fencing the operation against the item's project.
|
||||
*
|
||||
* Replaces the previous controller->repository direct delete (id-only, no project scoping).
|
||||
*
|
||||
* @param int $id Canvas item id.
|
||||
* @return bool True when deleted, false when the item does not resolve to a project.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(IdeasPermissions::DELETE, entityScoped: true)]
|
||||
public function deleteCanvasItem(int $id): bool
|
||||
{
|
||||
$projectId = $this->canvasItemProjectId($id);
|
||||
if ($projectId === null) {
|
||||
return false;
|
||||
}
|
||||
$this->authorize(IdeasPermissions::DELETE, $projectId);
|
||||
|
||||
$this->ideasRepository->delCanvasItem($id);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
299
app/Domain/Ideas/Templates/advancedBoards.blade.php
Normal file
299
app/Domain/Ideas/Templates/advancedBoards.blade.php
Normal file
@@ -0,0 +1,299 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$allCanvas = $allCanvas ?? [];
|
||||
$canvasLabels = $canvasLabels ?? [];
|
||||
$canvasTitle = '';
|
||||
|
||||
// All states >0 (<1 is archive)
|
||||
$numberofColumns = count($canvasLabels);
|
||||
$size = floor((100 / $numberofColumns) * 100) / 100;
|
||||
|
||||
// get canvas title
|
||||
foreach ($allCanvas as $canvasRow) {
|
||||
if ($canvasRow['id'] == ($currentCanvas ?? '')) {
|
||||
$canvasTitle = $canvasRow['title'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><i class="far fa-lightbulb"></i></div>
|
||||
<div class="pagetitle">
|
||||
@if (count($allCanvas) > 0)
|
||||
<x-global::subjectSwitcher
|
||||
:parent="__('headlines.idea_management')"
|
||||
:current="$canvasTitle">
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<li><a href="javascript:void(0)" class="addCanvasLink">{!! __('links.icon.create_new_board') !!}</a></li>
|
||||
@endif
|
||||
<li class="border"></li>
|
||||
@foreach ($allCanvas as $canvasRow)
|
||||
<li><a href='{{ BASE_URL }}/ideas/showBoards/{{ $canvasRow['id'] }}'>{{ $tpl->escape($canvasRow['title']) }}</a></li>
|
||||
@endforeach
|
||||
</x-global::subjectSwitcher>
|
||||
@else
|
||||
<h1>{!! __('headlines.idea_management') !!}</h1>
|
||||
@endif
|
||||
</div>
|
||||
@if (count($allCanvas) > 0)
|
||||
<div class="pageheader-right">
|
||||
<span class="dropdown dropdownWrapper headerEditDropdown">
|
||||
<a href="javascript:void(0)" class="dropdown-toggle btn btn-transparent" data-toggle="dropdown"><i class="fa-solid fa-ellipsis-v"></i></a>
|
||||
<ul class="dropdown-menu editCanvasDropdown">
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<li><a href="javascript:void(0)" class="editCanvasLink ">{!! __('links.icon.edit') !!}</a></li>
|
||||
<li><a href="{{ BASE_URL }}/ideas/delCanvas/{{ $currentCanvas }}" class="delete">{!! __('links.icon.delete') !!}</a></li>
|
||||
@endif
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div><!--pageheader-->
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
@if (count($allCanvas) > 0)
|
||||
<x-global::forms.button tag="a" link="#/ideas/ideaDialog?type=idea" contentRole="primary" id="customersegment"><span
|
||||
class="far fa-lightbulb"></span>{!! __('buttons.add_idea') !!}</x-global::forms.button>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 center">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="pull-right">
|
||||
<div class="btn-group viewDropDown">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">{!! __('buttons.idea_kanban') !!} {!! __('links.view') !!}</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{{ BASE_URL }}/ideas/showBoards" >{!! __('buttons.idea_wall') !!}</a></li>
|
||||
<li><a href="{{ BASE_URL }}/ideas/advancedBoards" class="active">{!! __('buttons.idea_kanban') !!}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@if (count($allCanvas) > 0)
|
||||
<div id="sortableIdeaKanban" class="sortableTicketList">
|
||||
|
||||
<div class="row-fluid">
|
||||
|
||||
@foreach ($canvasLabels as $key => $statusRow)
|
||||
<div class="column" style="width:{{ $size }}%;">
|
||||
|
||||
<h4 class="widgettitle title-primary">
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<a href="#/setting/editBoxLabel?module=idealabels&label={{ $key }}"
|
||||
class="editHeadline"><i class="fas fa-edit"></i></a>
|
||||
@endif
|
||||
{{ $statusRow['name'] }}
|
||||
</h4>
|
||||
|
||||
<div class="contentInner status_{{ $key }}">
|
||||
|
||||
@foreach ($canvasItems as $row)
|
||||
@if ($row['box'] == $key)
|
||||
<div class="ticketBox moveable" id="item_{{ $row['id'] }}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<div class="inlineDropDownContainer" style="float:right;">
|
||||
|
||||
<a href="javascript:void(0);" class="dropdown-toggle ticketDropDown" data-toggle="dropdown">
|
||||
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li class="nav-header">{!! __('subtitles.edit') !!}</li>
|
||||
<li><a href="#/ideas/ideaDialog/{{ $row['id'] }}" class="" data="item_{{ $row['id'] }}"> {!! __('links.edit_canvas_item') !!}</a></li>
|
||||
<li><a href="#/ideas/delCanvasItem/{{ $row['id'] }}" class="delete" data="item_{{ $row['id'] }}"> {!! __('links.delete_canvas_item') !!}</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<h4><a href="{{ BASE_URL }}/ideas/advancedBoards/#/ideas/ideaDialog/{{ $row['id'] }}" class=""
|
||||
data="item_{{ $row['id'] }}">{{ $tpl->escape($row['description']) }}</a></h4>
|
||||
|
||||
<div class="mainIdeaContent">
|
||||
|
||||
<div class="kanbanCardContent">
|
||||
|
||||
<div class="kanbanContent" style="margin-bottom: 20px">
|
||||
{!! $tpl->escapeMinimal($row['data']) !!}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix" style="padding-bottom: 8px;"></div>
|
||||
|
||||
<div class="dropdown ticketDropdown userDropdown noBg show right lastDropdown dropRight">
|
||||
<a class="dropdown-toggle f-left" href="javascript:void(0);" role="button" id="userDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">
|
||||
@if ($row['authorFirstname'] != '')
|
||||
{!! "<span id='userImage" . $row['id'] . "'><img src='" . BASE_URL . "/api/users?profileImage=" . $row['author'] . "' width='25' style='vertical-align: middle;'/></span><span id='user" . $row['id'] . "'></span>" !!}
|
||||
@else
|
||||
{!! "<span id='userImage" . $row['id'] . "'><img src='" . BASE_URL . "/api/users?profileImage=false' width='25' style='vertical-align: middle;'/></span><span id='user" . $row['id'] . "'></span>" !!}
|
||||
@endif
|
||||
</span>
|
||||
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="userDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_user') !!}</li>
|
||||
|
||||
@foreach ($users as $user)
|
||||
{!! "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='" . sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname'])) . "' data-value='" . $row['id'] . '_' . $user['id'] . '_' . $user['profileId'] . "' id='userStatusChange" . $row['id'] . $user['id'] . "' ><img src='" . BASE_URL . "/api/users?profileImage=" . $user['id'] . "' width='25' style='vertical-align: middle; margin-right:5px;'/>" . sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname'])) . "</a></li>" !!}
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="pull-right" style="margin-right:10px;">
|
||||
|
||||
<a href="#/ideas/ideaDialog/{{ $row['id'] }}"
|
||||
data="item_{{ $row['id'] }}"
|
||||
{!! $row['commentCount'] == 0 ? 'style="color: grey;"' : '' !!}>
|
||||
<span class="fas fa-comments"></span></a> <small>{{ $row['commentCount'] }}</small>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($row['milestoneHeadline'] != '')
|
||||
<br/>
|
||||
<div hx-trigger="load"
|
||||
hx-indicator=".htmx-indicator"
|
||||
hx-get="{{ BASE_URL }}/hx/tickets/milestones/showCard?milestoneId={{ $row['milestoneId'] }}">
|
||||
<div class="htmx-indicator">
|
||||
{!! __('label.loading_milestone') !!}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@else
|
||||
<br/><br/>
|
||||
<div class='center'>
|
||||
<div style='width:50%' class='svgContainer'>
|
||||
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_new_ideas_jdea.svg') !!}
|
||||
</div>
|
||||
|
||||
<br/><h4>{!! __('headlines.have_an_idea') !!}</h4><br/>
|
||||
{!! __('subtitles.start_collecting_ideas') !!}<br/><br/>
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<x-global::forms.button tag="a" link="javascript:void(0);"
|
||||
class="addCanvasLink" contentRole="primary">{!! __('buttons.start_new_idea_board') !!}</x-global::forms.button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endif
|
||||
<!-- Modals -->
|
||||
|
||||
<div class="modal fade bs-example-modal-lg" id="addCanvas">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form action="" method="post">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{!! __('headlines.start_new_idea_board') !!}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label>{!! __('label.topic_idea_board') !!}</label>
|
||||
<x-global::forms.text-input name="canvastitle"
|
||||
placeholder="{{ __('input.placeholders.name_for_idea_board') }}"
|
||||
style="width:90%" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default"
|
||||
data-dismiss="modal">{!! __('buttons.close') !!}</button>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary"
|
||||
:labelText="__('buttons.create_board')" name="newCanvas"/>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<div class="modal fade bs-example-modal-lg" id="editCanvas">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form action="" method="post">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{!! __('headlines.edit_board_name') !!}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label>{!! __('label.title_idea_board') !!}</label>
|
||||
<x-global::forms.text-input name="canvastitle" value="{{ $canvasTitle }}"
|
||||
style="width:90%" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default"
|
||||
data-dismiss="modal">{!! __('buttons.close') !!}</button>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')"
|
||||
name="editCanvas"/>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function () {
|
||||
|
||||
leantime.ideasController.initBoardControlModal();
|
||||
leantime.ideasController.setKanbanHeights();
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
var ideaStatusList = [@foreach ($canvasLabels as $key => $statusRow)'{{ $key }}',@endforeach];
|
||||
leantime.ideasController.initIdeaKanban(ideaStatusList);
|
||||
leantime.ideasController.initUserDropdown();
|
||||
@else
|
||||
leantime.authController.makeInputReadonly(".maincontentinner");
|
||||
@endif
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
34
app/Domain/Ideas/Templates/boardDialog.php
Normal file
34
app/Domain/Ideas/Templates/boardDialog.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* modals.inc template - Generic template for create / edit / clone modals
|
||||
*/
|
||||
foreach ($__data as $var => $val) {
|
||||
$$var = $val; // necessary for blade refactor
|
||||
}
|
||||
$allCanvas = $tpl->get('allCanvas');
|
||||
$canvasTitle = $tpl->get('canvasTitle');
|
||||
$canvasName = $tpl->get('canvasName');
|
||||
?>
|
||||
|
||||
<form action="<?= BASE_URL ?>/ideas/boardDialog<?= isset($_GET['id']) ? '/'.(int) $_GET['id'] : ''?>" method="post" class="formModal">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title"><i class='fa fa-plus'></i> <?= $tpl->__('subtitles.create_new_board') ?></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label><?= $tpl->__('label.title_new') ?></label><br />
|
||||
<input type="text" name="canvastitle" value="<?= $tpl->escape($canvasTitle) ?>" placeholder="<?= $tpl->__('input.placeholders.enter_title_for_board') ?>"
|
||||
style="width: 100%"/>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<?php if (isset($_GET['id'])) {?>
|
||||
<input type="submit" class="btn btn-primary" value="<?= $tpl->__('buttons.save_board') ?>" name="newCanvas" />
|
||||
<input type="hidden" name="editCanvas" value="<?= (int) $_GET['id'] ?>">
|
||||
<?php } else { ?>
|
||||
<input type="hidden" name="newCanvas" value="true">
|
||||
<input type="submit" class="btn btn-primary" value="<?= $tpl->__('buttons.create_board') ?>" name="newCanvas" />
|
||||
<?php } ?>
|
||||
<button type="button" class="btn btn-default" onclick="jQuery.nmTop().close();"><?= $tpl->__('buttons.close') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
26
app/Domain/Ideas/Templates/delCanvas.blade.php
Normal file
26
app/Domain/Ideas/Templates/delCanvas.blade.php
Normal file
@@ -0,0 +1,26 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa fa-trash"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{{ session('currentProjectClient') . ' // ' . session('currentProjectName') }}</h5>
|
||||
<h1>{!! __('headline.delete_board') !!}</h1>
|
||||
</div>
|
||||
</div><!--pageheader-->
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
<h4 class="widget widgettitle">{!! __('subtitles.delete') !!}</h4>
|
||||
<div class="widgetcontent">
|
||||
<form method="post" action="{{ BASE_URL }}/ideas/delCanvas/{{ $tpl->escape($_GET['id']) }}">
|
||||
<p>{!! __('text.are_you_sure_delete_idea_board') !!}</p>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
|
||||
<x-global::forms.button tag="a" contentRole="tertiary" link="{{ BASE_URL }}/ideas/showBoards">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
13
app/Domain/Ideas/Templates/delCanvasItem.blade.php
Normal file
13
app/Domain/Ideas/Templates/delCanvasItem.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<h4 class="widgettitle title-light"><i class="fa fa-trash"></i> {!! __('buttons.delete') !!}</h4>
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/ideas/delCanvasItem/{{ (int) $_GET['id'] }}">
|
||||
<p>{!! __('text.are_you_sure_delete_idea') !!}</p><br />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
|
||||
<x-global::forms.button tag="a" contentRole="tertiary" link="{{ BASE_URL }}/ideas/showBoards/">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
</form>
|
||||
|
||||
@endsection
|
||||
175
app/Domain/Ideas/Templates/ideaDialog.blade.php
Normal file
175
app/Domain/Ideas/Templates/ideaDialog.blade.php
Normal file
@@ -0,0 +1,175 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$canvasItem = $canvasItem ?? [];
|
||||
$canvasTypes = $canvasTypes ?? [];
|
||||
|
||||
$id = '';
|
||||
if (isset($canvasItem['id']) && $canvasItem['id'] != '') {
|
||||
$id = $canvasItem['id'];
|
||||
}
|
||||
@endphp
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<form class="formModal" method="post" action="{{ BASE_URL }}/ideas/ideaDialog/{{ $id }}">
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-8">
|
||||
|
||||
<input type="hidden" value="{{ $currentCanvas }}" name="canvasId"/>
|
||||
<input type="hidden" value="{{ $tpl->escape($canvasItem['box']) }}" name="box" id="box"/>
|
||||
<input type="hidden" value="{{ $id }}" name="itemId" id="itemId"/>
|
||||
<input type="hidden" name="status" value="{{ $canvasItem['status'] }}" />
|
||||
<input type="hidden" value="{{ $id }}" name="id" autocomplete="off" readonly/>
|
||||
|
||||
<input type="hidden" name="milestoneId" value="{{ $canvasItem['milestoneId'] }}"/>
|
||||
<input type="hidden" name="changeItem" value="1"/>
|
||||
|
||||
<x-global::forms.text-input name="description" variant="headline" style="width:99%;" value="{{ $tpl->escape($canvasItem['description']) }}"
|
||||
placeholder="{{ __('input.placeholders.short_name') }}" /><br/>
|
||||
|
||||
<input type="text" value="{{ $tpl->escape($canvasItem['tags']) }}" name="tags" id="tags" />
|
||||
|
||||
<textarea rows="3" cols="10" name="data" class="tiptapComplex"
|
||||
placeholder="">{!! $tpl->escapeMinimal($canvasItem['data']) !!}</textarea><br/>
|
||||
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" id="primaryCanvasSubmitButton" />
|
||||
<x-global::forms.button contentRole="secondary" inputType="submit" value="closeModal" id="saveAndClose">{!! __('buttons.save_and_close') !!}</x-global::forms.button>
|
||||
|
||||
@if ($id !== '')
|
||||
<br/>
|
||||
<hr>
|
||||
<input type="hidden" name="comment" value="1"/>
|
||||
|
||||
<h4 class="widgettitle title-light"><span class="fa fa-submodules.generalComment"></span>{!! __('subtitles.discussion') !!}</h4>
|
||||
@include('comments::submodules.generalComment', ['formUrl' => BASE_URL . '/ideas/ideaDialog/' . $id])
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
@if ($id !== '')
|
||||
<br/><br/>
|
||||
<h4 class="widgettitle title-light"><span
|
||||
class="fa fa-link"></span> {!! __('headlines.linked_milestone') !!} <i class="fa fa-question-circle-o helperTooltip" data-tippy-content="{{ __('tooltip.link_milestones_tooltip') }}"></i></h4>
|
||||
|
||||
<ul class="sortableTicketList" style="width:99%">
|
||||
@if ($canvasItem['milestoneId'] == '')
|
||||
<li class="ui-state-default center" id="milestone_0">
|
||||
<h4>{!! __('headlines.no_milestone_link') !!}</h4>
|
||||
{!! __('text.use_milestone_to_track_idea') !!}<br/>
|
||||
<div class="row" id="milestoneSelectors">
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<div class="col-md-12">
|
||||
<a href="javascript:void(0);"
|
||||
onclick="leantime.ideasController.toggleMilestoneSelectors('new');">{!! __('links.create_link_milestone') !!}</a>
|
||||
| <a href="javascript:void(0);"
|
||||
onclick="leantime.ideasController.toggleMilestoneSelectors('existing');">{!! __('links.link_existing_milestone') !!}</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="row" id="newMilestone" style="display:none;">
|
||||
<div class="col-md-12">
|
||||
<x-global::forms.textarea name="newMilestone"></x-global::forms.textarea><br/>
|
||||
<input type="hidden" name="type" value="milestone"/>
|
||||
<input type="hidden" name="leancanvasitemid" value="{{ $id }} "/>
|
||||
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.save')" onclick="jQuery('#primaryCanvasSubmitButton').click()"
|
||||
contentRole="primary"/>
|
||||
<a href="javascript:void(0);"
|
||||
onclick="leantime.ideasController.toggleMilestoneSelectors('hide');">
|
||||
<i class="fas fa-times"></i> {!! __('links.cancel') !!}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="existingMilestone" style="display:none;">
|
||||
<div class="col-md-12">
|
||||
<select data-placeholder="{{ __('input.placeholders.filter_by_milestone') }}"
|
||||
name="existingMilestone" class="user-select">
|
||||
<option value="">{!! __('text.all_milestones') !!}</option>
|
||||
@foreach ($milestones as $milestoneRow)
|
||||
<option value="{{ $milestoneRow->id }}"
|
||||
@if (isset($searchCriteria['milestone']) && ($searchCriteria['milestone'] == $milestoneRow->id))
|
||||
selected='selected'
|
||||
@endif
|
||||
>{{ $tpl->escape($milestoneRow->headline) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="hidden" name="type" value="milestone"/>
|
||||
<input type="hidden" name="leancanvasitemid" value="{{ $id }} "/>
|
||||
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.save')" onclick="jQuery('#primaryCanvasSubmitButton').click()"
|
||||
contentRole="primary"/>
|
||||
<a href="javascript:void(0);"
|
||||
onclick="leantime.ideasController.toggleMilestoneSelectors('hide');">
|
||||
<i class="fas fa-times"></i> {!! __('links.cancel') !!}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@else
|
||||
<li class="ui-state-default" id="milestone_{{ $canvasItem['milestoneId'] }}"
|
||||
class="leanCanvasMilestone">
|
||||
|
||||
<div hx-trigger="load"
|
||||
hx-indicator=".htmx-indicator"
|
||||
hx-get="{{ BASE_URL }}/hx/tickets/milestones/showCard?milestoneId={{ $canvasItem['milestoneId'] }}">
|
||||
<div class="htmx-indicator">
|
||||
{!! __('label.loading_milestone') !!}
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ CURRENT_URL }}?removeMilestone={{ $canvasItem['milestoneId'] }}" class="ideaCanvasModal delete formModal"><i class="fa fa-close"></i> {!! __('links.remove') !!}</a>
|
||||
|
||||
</li>
|
||||
@endif
|
||||
|
||||
</ul>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="showDialogOnLoad" >
|
||||
@if ($id != '')
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/ideas/delCanvasItem/{{ $id }}" class="ideaModal delete right" state="danger" variant="outline"><i
|
||||
class="fa fa-trash"></i> {!! __('links.delete') !!}</x-global::forms.button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
window.onload = function () {
|
||||
if (!window.jQuery) {
|
||||
//It's not a modal
|
||||
location.href = "{{ BASE_URL }}/ideas/showBoards?showIdeaModal={{ $canvasItem['id'] }}";
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
if (window.leantime && window.leantime.tiptapController) {
|
||||
leantime.tiptapController.initComplexEditor();
|
||||
}
|
||||
leantime.ticketsController.initTagsInput();
|
||||
|
||||
@if (!$login::userIsAtLeast($roles::$editor))
|
||||
leantime.authController.makeInputReadonly(".nyroModalCont");
|
||||
@endif
|
||||
|
||||
@if ($login::userHasRole([$roles::$commenter]))
|
||||
leantime.submodules.generalCommentController.enableCommenterForms();
|
||||
@endif
|
||||
|
||||
})
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
307
app/Domain/Ideas/Templates/showBoards.blade.php
Normal file
307
app/Domain/Ideas/Templates/showBoards.blade.php
Normal file
@@ -0,0 +1,307 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$allCanvas = $allCanvas ?? [];
|
||||
$canvasTitle = '';
|
||||
$canvasLabels = $canvasLabels ?? [];
|
||||
|
||||
// get canvas title
|
||||
foreach ($allCanvas as $canvasRow) {
|
||||
if ($canvasRow['id'] == ($currentCanvas ?? '')) {
|
||||
$canvasTitle = $canvasRow['title'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><i class="far fa-lightbulb"></i></div>
|
||||
<div class="pagetitle">
|
||||
@if (count($allCanvas) > 0)
|
||||
<x-global::subjectSwitcher
|
||||
:parent="__('headlines.ideas')"
|
||||
:current="$canvasTitle">
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<li><a href="#/ideas/boardDialog">{!! __('links.icon.create_new_board') !!}</a></li>
|
||||
@endif
|
||||
<li class="border"></li>
|
||||
@foreach ($allCanvas as $canvasRow)
|
||||
<li><a href='{{ BASE_URL }}/ideas/showBoards/{{ $canvasRow['id'] }}'>{{ $tpl->escape($canvasRow['title']) }}</a></li>
|
||||
@endforeach
|
||||
</x-global::subjectSwitcher>
|
||||
@else
|
||||
<h1>{!! __('headlines.ideas') !!}</h1>
|
||||
@endif
|
||||
</div>
|
||||
@if (count($allCanvas) > 0)
|
||||
<div class="pageheader-right">
|
||||
<span class="dropdown dropdownWrapper headerEditDropdown">
|
||||
<a href="javascript:void(0)" class="dropdown-toggle btn btn-transparent" data-toggle="dropdown"><i class="fa-solid fa-ellipsis-v"></i></a>
|
||||
<ul class="dropdown-menu editCanvasDropdown ">
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<li><a href="#/ideas/boardDialog/{{ $currentCanvas }}">{!! __('links.icon.edit') !!}</a></li>
|
||||
<li><a href="{{ BASE_URL }}/ideas/delCanvas/{{ $currentCanvas }}" class="delete">{!! __('links.icon.delete') !!}</a></li>
|
||||
@endif
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div><!--pageheader-->
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner" id="ideaBoards" style="min-height:350px;">
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
@if (count($allCanvas) > 0)
|
||||
<x-global::forms.button tag="a" link="#/ideas/ideaDialog?type=idea" contentRole="primary" id="customersegment"><span
|
||||
class="far fa-lightbulb"></span>{!! __('buttons.add_idea') !!}</x-global::forms.button>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 center">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="pull-right">
|
||||
<div class="btn-group viewDropDown">
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">{!! __('buttons.idea_wall') !!} {!! __('links.view') !!}</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{{ BASE_URL }}/ideas/showBoards" class="active">{!! __('buttons.idea_wall') !!}</a></li>
|
||||
<li><a href="{{ BASE_URL }}/ideas/advancedBoards" class="">{!! __('buttons.idea_kanban') !!}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@if (count($allCanvas) > 0)
|
||||
<div id="ideaMason" class="sortableTicketList" style="padding-top:10px;">
|
||||
|
||||
@foreach ($canvasItems as $row)
|
||||
<div class="ticketBox" id="item_{{ $row['id'] }}" data-value="{{ $row['id'] }}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<div class="inlineDropDownContainer" style="float:right;">
|
||||
|
||||
<a href="javascript:void(0);" class="dropdown-toggle ticketDropDown" data-toggle="dropdown">
|
||||
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li class="nav-header">{!! __('subtitles.edit') !!}</li>
|
||||
<li><a href="#/ideas/ideaDialog/{{ $row['id'] }}" class="" data="item_{{ $row['id'] }}"> {!! __('links.edit_canvas_item') !!}</a></li>
|
||||
<li><a href="#/ideas/delCanvasItem/{{ $row['id'] }}" class="delete" data="item_{{ $row['id'] }}"> {!! __('links.delete_canvas_item') !!}</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<h4><a href="#/ideas/ideaDialog/{{ $row['id'] }}"
|
||||
data="item_{{ $row['id'] }}">{{ $tpl->escape($row['description']) }}</a></h4>
|
||||
|
||||
<div class="mainIdeaContent">
|
||||
<div class="kanbanCardContent">
|
||||
|
||||
<div class="kanbanContent" style="margin-bottom: 20px; max-height:none;">
|
||||
{!! $tpl->escapeMinimal($row['data']) !!}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix" style="padding-bottom: 8px;"></div>
|
||||
|
||||
<div class="dropdown ticketDropdown statusDropdown show firstDropdown colorized">
|
||||
<a class="dropdown-toggle f-left status {{ $canvasLabels[$row['box']]['class'] }} " href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">{{ $canvasLabels[$row['box']]['name'] }}</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="statusDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_status') !!}</li>
|
||||
|
||||
@foreach ($canvasLabels as $key => $label)
|
||||
{!! "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' class='" . $label['class'] . "' data-label='" . $tpl->escape($label['name']) . "' data-value='" . $row['id'] . '_' . $key . '_' . $label['class'] . "' id='ticketStatusChange" . $row['id'] . $key . "' >" . $tpl->escape($label['name']) . "</a></li>" !!}
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="dropdown ticketDropdown userDropdown noBg show right lastDropdown dropRight">
|
||||
<a class="dropdown-toggle f-left" href="javascript:void(0);" role="button" id="userDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">
|
||||
@if ($row['authorFirstname'] != '')
|
||||
{!! "<span id='userImage" . $row['id'] . "'><img src='" . BASE_URL . "/api/users?profileImage=" . $row['author'] . "' width='25' style='vertical-align: middle;'/></span><span id='user" . $row['id'] . "'></span>" !!}
|
||||
@else
|
||||
{!! "<span id='userImage" . $row['id'] . "'><img src='" . BASE_URL . "/api/users?profileImage=false' width='25' style='vertical-align: middle;'/></span><span id='user" . $row['id'] . "'></span>" !!}
|
||||
@endif
|
||||
</span>
|
||||
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="userDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_user') !!}</li>
|
||||
|
||||
@foreach ($users as $user)
|
||||
{!! "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='" . sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname'])) . "' data-value='" . $row['id'] . '_' . $user['id'] . '_' . $user['profileId'] . "' id='userStatusChange" . $row['id'] . $user['id'] . "' ><img src='" . BASE_URL . "/api/users?profileImage=" . $user['id'] . "' width='25' style='vertical-align: middle; margin-right:5px;'/>" . sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname'])) . "</a></li>" !!}
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="pull-right" style="margin-right:10px;">
|
||||
|
||||
<a href="#/ideas/ideaDialog/{{ $row['id'] }}"
|
||||
class="" data="item_{{ $row['id'] }}"
|
||||
{!! $row['commentCount'] == 0 ? 'style="color: grey;"' : '' !!}>
|
||||
<span class="fas fa-comments"></span></a> <small>{{ $row['commentCount'] }}</small>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($row['milestoneHeadline'] != '')
|
||||
<br/>
|
||||
<div hx-trigger="load"
|
||||
hx-indicator=".htmx-indicator"
|
||||
hx-get="{{ BASE_URL }}/hx/tickets/milestones/showCard?milestoneId={{ $row['milestoneId'] }}">
|
||||
|
||||
<div class="htmx-indicator">
|
||||
{!! __('label.loading_milestone') !!}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
@if (count($canvasItems) == 0)
|
||||
<div class='center'>
|
||||
<div style='width:30%' class='svgContainer'>
|
||||
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_new_ideas_jdea.svg') !!}
|
||||
</div>
|
||||
|
||||
<h3>{!! __('headlines.have_an_idea') !!}</h3><br />
|
||||
{!! __('subtitles.start_collecting_ideas') !!}<br/><br/>
|
||||
</div>
|
||||
@endif
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@else
|
||||
<br/><br/>
|
||||
<div class='center'>
|
||||
<div style='width:30%' class='svgContainer'>
|
||||
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_new_ideas_jdea.svg') !!}
|
||||
</div>
|
||||
|
||||
<h3>{!! __('headlines.have_an_idea') !!}</h3><br />
|
||||
{!! __('subtitles.start_collecting_ideas') !!}<br/><br/>
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<x-global::forms.button tag="a" link="javascript:void(0)"
|
||||
class="addCanvasLink" contentRole="primary">{!! __('links.icon.create_new_board') !!}</x-global::forms.button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endif
|
||||
<!-- Modals -->
|
||||
|
||||
<div class="modal fade bs-example-modal-lg" id="addCanvas">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form action="" method="post">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{!! __('headlines.start_new_idea_board') !!}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label>{!! __('label.topic_idea_board') !!}</label>
|
||||
<x-global::forms.text-input name="canvastitle" placeholder="{{ __('input.placeholders.name_for_idea_board') }}"
|
||||
style="width:90%" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<x-global::forms.button inputType="button" contentRole="tertiary"
|
||||
data-dismiss="modal">{!! __('buttons.close') !!}</x-global::forms.button>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.create_board')" name="newCanvas"/>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<div class="modal fade bs-example-modal-lg" id="editCanvas">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form action="" method="post">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">{!! __('headlines.edit_board_name') !!}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label>{!! __('label.title_idea_board') !!}</label>
|
||||
<x-global::forms.text-input name="canvastitle" value="{{ $canvasTitle }}"
|
||||
style="width:90%" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<x-global::forms.button inputType="button" contentRole="tertiary"
|
||||
data-dismiss="modal">{!! __('buttons.close') !!}</x-global::forms.button>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="editCanvas"/>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function () {
|
||||
|
||||
leantime.ideasController.initMasonryWall();
|
||||
leantime.ideasController.initBoardControlModal();
|
||||
leantime.ideasController.initWallImageModals();
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
leantime.ideasController.initStatusDropdown();
|
||||
leantime.ideasController.initUserDropdown();
|
||||
@else
|
||||
leantime.authController.makeInputReadonly(".maincontentinner");
|
||||
@endif
|
||||
|
||||
@if (isset($_GET['showIdeaModal']))
|
||||
@php
|
||||
if ($_GET['showIdeaModal'] == '') {
|
||||
$modalUrl = '&type=idea';
|
||||
} else {
|
||||
$modalUrl = '/' . (int) $_GET['showIdeaModal'];
|
||||
}
|
||||
@endphp
|
||||
|
||||
leantime.ideasController.openModalManually("{{ BASE_URL }}/ideas/ideaDialog{{ $modalUrl }}");
|
||||
window.history.pushState({}, document.title, '{{ BASE_URL }}/ideas/showBoards');
|
||||
|
||||
@endif
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
Reference in New Issue
Block a user