OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
<?php
namespace Leantime\Domain\Canvas\Controllers;
use Illuminate\Support\Str;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Core\Mailer as MailerCore;
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
use Leantime\Domain\Projects\Services\Projects as ProjectService;
use Leantime\Domain\Queue\Repositories\Queue as QueueRepository;
use Symfony\Component\HttpFoundation\Response;
/**
* Handles canvas board create/edit dialog.
*/
class BoardDialog extends Controller
{
/**
* Constant that must be redefined by subclasses.
*/
protected const CANVAS_NAME = '??';
private ProjectService $projectService;
private BlueprintsService $blueprintsService;
private object $canvasRepo;
/**
* Initializes dependencies.
*/
public function init(ProjectService $projectService): void
{
$this->projectService = $projectService;
$this->blueprintsService = app()->make(BlueprintsService::class);
$canvasName = Str::studly(static::CANVAS_NAME).'canvas';
$repoName = app()->getNamespace()."Domain\\$canvasName\\Repositories\\$canvasName";
$this->canvasRepo = app()->make($repoName);
}
/**
* Displays the board dialog form.
*
* @param array $params Request parameters
*/
#[RequiresPermission(BlueprintsPermissions::VIEW, entityScoped: true)]
public function get(array $params): Response
{
$currentCanvasId = '';
$canvasTitle = '';
if (isset($params['id'])) {
// getBoard authorizes VIEW against the board's real project; false = missing/foreign/
// unauthorized — don't expose the title or switch the active board (no session poison).
$singleCanvas = $this->blueprintsService->getBoard((int) $params['id'], static::CANVAS_NAME.'canvas');
if ($singleCanvas !== false) {
$currentCanvasId = (int) $params['id'];
$canvasTitle = $singleCanvas[0]['title'] ?? '';
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
}
}
$this->assignTemplateVars($currentCanvasId, $canvasTitle);
if (! isset($_GET['raw'])) {
return $this->tpl->displayPartial('canvas.boardDialog');
}
return new Response;
}
/**
* Handles board creation and editing.
*
* @param array $params Request parameters
*/
#[RequiresPermission(BlueprintsPermissions::EDIT, entityScoped: true)]
public function post(array $params): Response
{
$currentCanvasId = '';
$canvasTitle = '';
if (isset($params['id'])) {
$singleCanvas = $this->blueprintsService->getBoard((int) $params['id'], static::CANVAS_NAME.'canvas');
if ($singleCanvas !== false) {
$currentCanvasId = (int) $params['id'];
$canvasTitle = $singleCanvas[0]['title'] ?? '';
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->assignTemplateVars($currentCanvasId, $canvasTitle);
if (! isset($_GET['raw'])) {
return $this->tpl->displayPartial('canvas.boardDialog');
}
return new Response;
}
/**
* Handles creating a new canvas 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;
}
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
return null;
}
$values = [
'title' => $_POST['canvastitle'],
'author' => session('userdata.id'),
'projectId' => session('currentProject'),
];
// createBoard authorizes CREATE against the target (current) project.
$currentCanvasId = $this->blueprintsService->createBoard($values, static::CANVAS_NAME.'canvas');
$this->notifyBoardCreated($values['title']);
$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.'/'.static::CANVAS_NAME.'canvas/boardDialog/'.$currentCanvasId);
}
/**
* Handles editing a canvas 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;
}
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
return null;
}
// renameBoard authorizes EDIT against the board's real project.
$this->blueprintsService->renameBoard($currentCanvasId, $_POST['canvastitle'], static::CANVAS_NAME.'canvas');
$this->tpl->setNotification($this->language->__('notification.board_edited'), 'success');
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/boardDialog/'.$currentCanvasId);
}
/**
* Sends board creation notifications to project users.
*/
private function notifyBoardCreated(string $title): void
{
$mailer = app()->make(MailerCore::class);
$users = $this->projectService->getUsersToNotify(session('currentProject'));
$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'),
session('currentProject')
);
}
/**
* Assigns common template variables.
*/
private function assignTemplateVars(int|string $currentCanvasId, string $canvasTitle): void
{
$this->tpl->assign('currentCanvas', $currentCanvasId);
$this->tpl->assign('canvasName', static::CANVAS_NAME);
$this->tpl->assign('canvasTitle', $canvasTitle);
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Leantime\Domain\Canvas\Controllers;
use Illuminate\Support\Str;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
use Symfony\Component\HttpFoundation\Response;
/**
* Handles deletion of a canvas board.
*/
class DelCanvas extends Controller
{
/**
* Constant that must be redefined by subclasses.
*/
protected const CANVAS_NAME = '??';
private mixed $canvasRepo;
private BlueprintsService $blueprintsService;
/**
* Initializes dependencies.
*
* Note: no `void` return type so plugin subclasses that override init()
* without a return type (this class is the deprecated plugin shim) stay
* signature-compatible.
*/
public function init()
{
$this->blueprintsService = app()->make(BlueprintsService::class);
$canvasName = Str::studly(static::CANVAS_NAME).'canvas';
$repoName = app()->getNamespace()."Domain\\$canvasName\\Repositories\\$canvasName";
$this->canvasRepo = app()->make($repoName);
}
/**
* Displays the delete canvas confirmation.
*
* @param array $params Request parameters
*/
#[RequiresPermission(BlueprintsPermissions::DELETE)]
public function get(array $params): Response
{
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.delCanvas');
}
/**
* Handles canvas board deletion.
*
* @param array $params Request parameters
*/
#[RequiresPermission(BlueprintsPermissions::DELETE, entityScoped: true)]
public function post(array $params): Response
{
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
if (isset($_POST['del']) && $id > 0) {
// The service resolves the board's REAL project and authorizes DELETE against it
// (throwing for a missing/foreign board) — closing the by-id board-delete IDOR the
// previous role-only Auth::authOrRedirect left open.
$this->blueprintsService->deleteBoard($id, static::CANVAS_NAME.'canvas');
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $allCanvas[0]['id'] ?? -1]);
$this->tpl->setNotification($this->language->__('notification.board_deleted'), 'success', strtoupper(static::CANVAS_NAME).'canvas_deleted');
if (! $allCanvas || count($allCanvas) == 0) {
return Frontcontroller::redirect(BASE_URL.'/blueprints/showBoards');
}
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas');
}
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.delCanvas');
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Leantime\Domain\Canvas\Controllers;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
use Symfony\Component\HttpFoundation\Response;
/**
* Handles deletion of a canvas item.
*/
class DelCanvasItem extends Controller
{
/**
* Constant that must be redefined by subclasses.
*/
protected const CANVAS_NAME = '??';
private BlueprintsService $blueprintsService;
/**
* Initializes dependencies.
*/
public function init(): void
{
$this->blueprintsService = app()->make(BlueprintsService::class);
}
/**
* Displays the delete canvas item confirmation.
*
* @param array $params Request parameters
*/
#[RequiresPermission(BlueprintsPermissions::DELETE)]
public function get(array $params): Response
{
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.delCanvasItem');
}
/**
* Handles canvas item deletion.
*
* @param array $params Request parameters
*/
#[RequiresPermission(BlueprintsPermissions::DELETE, entityScoped: true)]
public function post(array $params): Response
{
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
if (isset($_POST['del']) && $id > 0) {
// The service resolves the item's REAL project and authorizes DELETE against it
// (throwing for a missing/foreign item) — closing the by-id item-delete IDOR.
$this->blueprintsService->deleteCanvasItem($id, static::CANVAS_NAME.'canvas');
$this->tpl->setNotification($this->language->__('notification.element_deleted'), 'success', strtoupper(static::CANVAS_NAME).'canvasitem_deleted');
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas');
}
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.delCanvasItem');
}
}

View File

@@ -0,0 +1,291 @@
<?php
/**
* editCanvasComment class - Generic canvas controller / Edit Comments
*/
namespace Leantime\Domain\Canvas\Controllers;
use Illuminate\Support\Str;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
use Leantime\Domain\Comments\Repositories\Comments as CommentRepository;
use Leantime\Domain\Notifications\Models\Notification as NotificationModel;
use Leantime\Domain\Projects\Services\Projects as ProjectService;
class EditCanvasComment extends Controller
{
/**
* Constant that must be redefined
*/
protected const CANVAS_NAME = '??';
private CommentRepository $commentsRepo;
private ProjectService $projectService;
private BlueprintsService $blueprintsService;
private object $canvasRepo;
/**
* init - initialize private variables
*/
public function init(
CommentRepository $commentsRepo,
ProjectService $projectService
) {
$this->commentsRepo = $commentsRepo;
$this->projectService = $projectService;
$this->blueprintsService = app()->make(BlueprintsService::class);
$canvasName = Str::studly(static::CANVAS_NAME).'canvas';
$repoName = app()->getNamespace()."Domain\\$canvasName\\Repositories\\$canvasName";
$this->canvasRepo = app()->make($repoName);
}
/**
* get - handle get requests
*/
#[RequiresPermission(BlueprintsPermissions::VIEW, entityScoped: true)]
public function get($params)
{
$canvasType = static::CANVAS_NAME.'canvas';
$canvasTypes = $this->canvasRepo->getCanvasTypes();
if (isset($params['id'])) {
// Resolve + VIEW-authorize the item against its real project first.
$canvasItem = $this->blueprintsService->getCanvasItem((int) $params['id'], $canvasType);
if (! $canvasItem) {
return $this->tpl->displayPartial('errors.error404');
}
// Delete comment — only when it belongs to THIS gated item (module + moduleId).
if (isset($params['delComment']) === true) {
$commentId = (int) ($params['delComment']);
$comment = $this->commentsRepo->getComment($commentId);
if ($comment !== false
&& (string) $comment['module'] === static::CANVAS_NAME.'canvasitem'
&& (int) $comment['moduleId'] === (int) $canvasItem['id']) {
$this->commentsRepo->deleteComment($commentId);
$this->tpl->setNotification($this->language->__('notifications.comment_deleted'), 'success', strtoupper(static::CANVAS_NAME).'canvascomment_deleted');
}
}
$comments = $this->commentsRepo->getComments(static::CANVAS_NAME.'canvasitem', $canvasItem['id']);
$this->tpl->assign('numComments', $this->commentsRepo->countComments(static::CANVAS_NAME.'canvasitem', $canvasItem['id']));
} else {
if (isset($params['type'])) {
$type = strip_tags($params['type']);
} else {
$type = array_key_first($canvasTypes);
}
$canvasItem = [
'id' => '',
'box' => $type,
'description' => '',
'status' => array_key_first($this->canvasRepo->getStatusLabels()),
'relates' => array_key_first($this->canvasRepo->getRelatesLabels()),
'assumptions' => '',
'data' => '',
'conclusion' => '',
'milestoneHeadline' => '',
'milestoneId' => '',
];
$comments = [];
}
$this->tpl->assign('comments', $comments);
$this->tpl->assign('canvasTypes', $canvasTypes);
$this->tpl->assign('canvasItem', $canvasItem);
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.canvasComment');
}
/**
* post - handle post requests
*/
#[RequiresPermission(BlueprintsPermissions::EDIT, entityScoped: true)]
public function post($params)
{
$canvasType = static::CANVAS_NAME.'canvas';
if (isset($params['changeItem'])) {
if (isset($params['itemId']) && $params['itemId'] != '') {
if (isset($params['description']) && ! empty($params['description'])) {
$currentCanvasId = (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas');
$canvasItem = [
'box' => $params['box'],
'author' => session('userdata.id'),
'description' => $params['description'],
'status' => $params['status'],
'relates' => $params['relates'],
'assumptions' => $params['assumptions'],
'data' => $params['data'],
'conclusion' => $params['conclusion'],
'itemId' => $params['itemId'],
'id' => $params['itemId'],
'canvasId' => $currentCanvasId,
'milestoneId' => $params['milestoneId'],
'dependentMilstone' => '',
];
// Resolves the item's real project from itemId and authorizes EDIT there.
$this->blueprintsService->updateCanvasItem($canvasItem, $canvasType);
$comments = $this->commentsRepo->getComments(static::CANVAS_NAME.'canvasitem', $params['itemId']);
$this->tpl->assign('numComments', $this->commentsRepo->countComments(
static::CANVAS_NAME.'canvasitem',
$params['itemId']
));
$this->tpl->assign('comments', $comments);
$this->tpl->setNotification($this->language->__('notifications.canvas_item_updates'), 'success', strtoupper(static::CANVAS_NAME).'canvasitem_updated');
$notification = app()->make(NotificationModel::class);
$notification->url = [
'url' => BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.(int) $params['itemId'],
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
];
$notification->entity = $canvasItem;
$notification->module = static::CANVAS_NAME.'canvas';
$notification->action = 'updated';
$notification->projectId = session('currentProject');
$notification->subject = $this->language->__('email_notifications.canvas_board_edited');
$notification->authorId = session('userdata.id');
$notification->message = sprintf(
$this->language->__('email_notifications.canvas_item_update_message'),
session('userdata.name'),
$canvasItem['description']
);
$this->projectService->notifyProjectUsers($notification);
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.$params['itemId']);
} else {
$this->tpl->setNotification($this->language->__('notification.please_enter_element_title'), 'error');
}
} else {
if (isset($_POST['description']) && ! empty($_POST['description'])) {
$currentCanvasId = (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas');
$canvasItem = [
'box' => $params['box'],
'author' => session('userdata.id'),
'description' => $params['description'],
'status' => $params['status'],
'relates' => $params['relates'],
'assumptions' => $params['assumptions'],
'data' => $params['data'],
'conclusion' => $params['conclusion'],
'canvasId' => $currentCanvasId,
];
// Resolves the target board's real project from canvasId and authorizes CREATE.
$id = $this->blueprintsService->createCanvasItem($canvasItem, $canvasType);
$canvasItem['id'] = $id;
$canvasTypes = $this->canvasRepo->getCanvasTypes();
$this->tpl->setNotification($canvasTypes[$params['box']].' successfully created', 'success', strtoupper(static::CANVAS_NAME).'canvasitem_created');
$notification = app()->make(NotificationModel::class);
$notification->url = [
'url' => BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.(int) ($params['itemId'] ?? $id),
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
];
$notification->entity = $canvasItem;
$notification->module = static::CANVAS_NAME.'canvas';
$notification->action = 'created';
$notification->projectId = session('currentProject');
$notification->subject = $this->language->__('email_notifications.canvas_board_item_created');
$notification->authorId = session('userdata.id');
$notification->message = sprintf(
$this->language->__('email_notifications.canvas_item_created_message'),
session('userdata.name'),
$canvasItem['description']
);
$this->projectService->notifyProjectUsers($notification);
$this->tpl->setNotification($this->language->__('notification.element_created'), 'success', strtoupper(static::CANVAS_NAME).'canvasitem_created');
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.$id);
} else {
$this->tpl->setNotification($this->language->__('notification.please_enter_element_title'), 'error');
}
}
}
if (isset($params['comment']) === true) {
$itemId = (int) ($_GET['id'] ?? 0);
// Only allow commenting on an item the user can view in their project.
if (! $this->blueprintsService->getCanvasItem($itemId, $canvasType)) {
return $this->tpl->displayPartial('errors.error404');
}
$values = [
'text' => $params['text'],
'date' => date('Y-m-d H:i:s'),
'userId' => (session('userdata.id')),
'moduleId' => $itemId,
'commentParent' => ($params['father']),
];
$message = $this->commentsRepo->addComment($values, static::CANVAS_NAME.'canvasitem');
$this->tpl->setNotification($this->language->__('notifications.comment_create_success'), 'success', strtoupper(static::CANVAS_NAME).'canvasitemcomment_created');
$notification = app()->make(NotificationModel::class);
$notification->url = [
'url' => BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.$itemId,
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
];
$notification->entity = $values;
$notification->module = static::CANVAS_NAME.'canvas';
$notification->action = 'commented';
$notification->projectId = session('currentProject');
$notification->subject = $this->language->__('email_notifications.canvas_board_comment_created');
$notification->authorId = session('userdata.id');
$notification->message = sprintf(
$this->language->__('email_notifications.canvas_item__comment_created_message'),
session('userdata.name')
);
$this->projectService->notifyProjectUsers($notification);
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasComment/'.$itemId);
}
// Fallback re-display: VIEW-authorize the item; false = missing/foreign/unauthorized -> 404.
$itemId = (int) ($_GET['id'] ?? 0);
$canvasItem = $this->blueprintsService->getCanvasItem($itemId, $canvasType);
if ($itemId > 0 && ! $canvasItem) {
return $this->tpl->displayPartial('errors.error404');
}
$this->tpl->assign('id', $itemId);
$this->tpl->assign('canvasTypes', $this->canvasRepo->getCanvasTypes());
$this->tpl->assign('canvasItem', $canvasItem ?: []);
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas.canvasComment');
}
/**
* put - handle put requests
*/
public function put($params) {}
/**
* delete - handle delete requests
*/
public function delete($params) {}
}

View File

@@ -0,0 +1,387 @@
<?php
namespace Leantime\Domain\Canvas\Controllers;
use Illuminate\Support\Str;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Core\Http\IncomingRequest;
use Leantime\Core\Language;
use Leantime\Core\UI\Template;
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
use Leantime\Domain\Comments\Repositories\Comments as CommentRepository;
use Leantime\Domain\Notifications\Models\Notification as NotificationModel;
use Leantime\Domain\Projects\Services\Projects as ProjectService;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
/**
* editCanvasItem class - Generic canvas controller / Edit Canvas Item.
*
* By-id canvas item access routes through the Blueprints service (scoped to this canvas type,
* static::CANVAS_NAME.'canvas') so it is authorized against the item's real project; the
* per-variant repo is used only for label/config reads. The base canvas type ('??canvas') has
* no data — these controllers are reached via subclasses (e.g. Logicmodelcanvas).
*/
class EditCanvasItem extends Controller
{
/**
* Constant that must be redefined
*
* @var string
*/
protected const CANVAS_NAME = '??';
private TicketService $ticketService;
private ProjectService $projectService;
private CommentRepository $commentsRepo;
private BlueprintsService $blueprintsService;
private object $canvasRepo;
/**
* __construct - constructor
*/
public function __construct(
IncomingRequest $incomingRequest,
Template $tpl,
Language $language
) {
$this->ticketService = app()->make(TicketService::class);
$this->projectService = app()->make(ProjectService::class);
$this->commentsRepo = app()->make(CommentRepository::class);
$this->blueprintsService = app()->make(BlueprintsService::class);
$canvasName = Str::studly(static::CANVAS_NAME).'canvas';
$repoName = app()->getNamespace()."Domain\\$canvasName\\Repositories\\$canvasName";
$this->canvasRepo = app()->make($repoName);
parent::__construct($incomingRequest, $tpl, $language);
}
/**
* get - handle get requests
*/
#[RequiresPermission(BlueprintsPermissions::VIEW, entityScoped: true)]
public function get($params)
{
$canvasType = static::CANVAS_NAME.'canvas';
$commentModule = static::CANVAS_NAME.'canvas'.'item';
if (isset($params['id'])) {
// Resolve + VIEW-authorize the item against its real project BEFORE any mutation.
$canvasItem = $this->blueprintsService->getCanvasItem((int) $params['id'], $canvasType);
if (! $canvasItem) {
return $this->tpl->displayPartial('errors.error404');
}
// Delete comment — only when it belongs to THIS gated item (module + moduleId);
// deleteComment() filters on the comment id alone, so the bind prevents deleting a
// foreign item's / project's comment.
if (isset($params['delComment'])) {
$commentId = (int) ($params['delComment']);
$comment = $this->commentsRepo->getComment($commentId);
if ($comment !== false
&& (string) $comment['module'] === $commentModule
&& (int) $comment['moduleId'] === (int) $canvasItem['id']) {
$this->commentsRepo->deleteComment($commentId);
$this->tpl->setNotification($this->language->__('notifications.comment_deleted'), 'success');
}
}
// Delete milestone relationship — an EDIT, authorized by the service.
if (isset($params['removeMilestone'])) {
$this->blueprintsService->patchCanvasItem((int) $params['id'], ['milestoneId' => ''], $canvasType);
$canvasItem = $this->blueprintsService->getCanvasItem((int) $params['id'], $canvasType);
$this->tpl->setNotification($this->language->__('notifications.milestone_detached'), 'success');
}
$comments = $this->commentsRepo->getComments($commentModule, $canvasItem['id']);
$this->tpl->assign(
'numComments',
$this->commentsRepo->countComments($commentModule, $canvasItem['id'])
);
} else {
if (isset($params['type'])) {
$type = strip_tags($params['type']);
} else {
$type = array_key_first($this->canvasRepo->elementLabels);
}
$canvasItem = [
'id' => '',
'box' => $type,
'description' => '',
'status' => array_key_first($this->canvasRepo->getStatusLabels()),
'relates' => array_key_first($this->canvasRepo->getRelatesLabels()),
'assumptions' => '',
'data' => '',
'conclusion' => '',
'milestoneHeadline' => '',
'milestoneId' => '',
];
$comments = [];
}
$this->tpl->assign('comments', $comments);
$allProjectMilestones = $this->ticketService->getAllMilestones(['sprint' => '', 'type' => 'milestone', 'currentProject' => session('currentProject')]);
$this->tpl->assign('milestones', $allProjectMilestones);
$this->tpl->assign('canvasItem', $canvasItem);
$this->tpl->assign('canvasIcon', $this->canvasRepo->getIcon());
$this->tpl->assign('relatesLabels', $this->canvasRepo->getRelatesLabels());
$this->tpl->assign('canvasTypes', $this->canvasRepo->getCanvasTypes());
$this->tpl->assign('statusLabels', $this->canvasRepo->getStatusLabels());
$this->tpl->assign('dataLabels', $this->canvasRepo->getDataLabels());
$this->tpl->assign('currentCanvas', (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas'));
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas'.'.canvasDialog');
}
/**
* post - handle post requests
*/
#[RequiresPermission(BlueprintsPermissions::EDIT, entityScoped: true)]
public function post($params)
{
$canvasType = static::CANVAS_NAME.'canvas';
if (isset($params['changeItem'])) {
if (isset($params['itemId']) && ! empty($params['itemId'])) {
if (isset($params['description']) && ! empty($params['description'])) {
$currentCanvasId = (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas');
$canvasItem = [
'box' => $params['box'],
'author' => session('userdata.id'),
'description' => $params['description'],
'status' => $params['status'],
'relates' => $params['relates'],
'assumptions' => $params['assumptions'],
'data' => $params['data'],
'conclusion' => $params['conclusion'],
'itemId' => $params['itemId'],
'canvasId' => $currentCanvasId,
'milestoneId' => $params['milestoneId'],
'dependentMilstone' => '',
'id' => $params['itemId'],
];
if (isset($params['newMilestone']) && $params['newMilestone'] != '') {
$params['headline'] = $params['newMilestone'];
$params['tags'] = '#ccc';
$params['editFrom'] = dtHelper()->userNow()->formatDateForUser();
$params['editTo'] = dtHelper()->userNow()->addDays(7)->formatDateForUser();
$params['dependentMilestone'] = '';
$id = $this->ticketService->quickAddMilestone($params);
if ($id !== false) {
$canvasItem['milestoneId'] = $id;
}
}
if (isset($params['existingMilestone']) && $params['existingMilestone'] != '') {
$canvasItem['milestoneId'] = $params['existingMilestone'];
}
// Resolves the item's real project from itemId and authorizes EDIT there.
$this->blueprintsService->updateCanvasItem($canvasItem, $canvasType);
$comments = $this->commentsRepo->getComments(static::CANVAS_NAME.'canvas'.'item', $params['itemId']);
$this->tpl->assign('numComments', $this->commentsRepo->countComments(
static::CANVAS_NAME.'canvas'.'item',
$params['itemId']
));
$this->tpl->assign('comments', $comments);
$this->tpl->setNotification($this->language->__('notifications.canvas_item_updates'), 'success');
$subject = $this->language->__('email_notifications.canvas_board_edited');
$actual_link = BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'#/editCanvasItem/'.(int) $params['itemId'];
$message = sprintf(
$this->language->__('email_notifications.canvas_item_update_message'),
session('userdata.name'),
strip_tags($canvasItem['description'])
);
$notification = app()->make(NotificationModel::class);
$notification->url = [
'url' => $actual_link,
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
];
$notification->entity = $canvasItem;
$notification->module = static::CANVAS_NAME.'canvas';
$notification->action = 'updated';
$notification->projectId = session('currentProject');
$notification->subject = $subject;
$notification->authorId = session('userdata.id');
$notification->message = $message;
$this->projectService->notifyProjectUsers($notification);
$closeModal = '';
if (isset($_POST['submitAction']) && $_POST['submitAction'] == 'closeModal') {
$closeModal = '?closeModal=true';
}
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasItem/'.$params['itemId'].$closeModal);
} else {
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
}
} else {
if (isset($_POST['description']) && ! empty($_POST['description'])) {
$currentCanvasId = (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas');
$canvasItem = [
'box' => $params['box'],
'author' => session('userdata.id'),
'description' => $params['description'],
'status' => $params['status'],
'relates' => $params['relates'],
'assumptions' => $params['assumptions'],
'data' => $params['data'],
'conclusion' => $params['conclusion'],
'canvasId' => $currentCanvasId,
];
// Resolves the target board's real project from canvasId and authorizes CREATE.
$id = $this->blueprintsService->createCanvasItem($canvasItem, $canvasType);
$canvasTypes = $this->canvasRepo->getCanvasTypes();
$this->tpl->setNotification($canvasTypes[$params['box']]['title'].' successfully created', 'success', ''.$params['box'].'_item_created');
$subject = $this->language->__('email_notifications.canvas_board_item_created');
$actual_link = BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'#/editCanvasItem/'.(int) ($params['itemId'] ?? $id);
$message = sprintf(
$this->language->__('email_notifications.canvas_item_created_message'),
session('userdata.name'),
strip_tags($canvasItem['description'])
);
$notification = app()->make(NotificationModel::class);
$notification->url = [
'url' => $actual_link,
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
];
$notification->entity = $canvasItem;
$notification->module = static::CANVAS_NAME.'canvas';
$notification->action = 'created';
$notification->projectId = session('currentProject');
$notification->subject = $subject;
$notification->authorId = session('userdata.id');
$notification->message = $message;
$this->projectService->notifyProjectUsers($notification);
$this->tpl->setNotification($this->language->__('notification.element_created'), 'success');
$closeModal = '';
if (isset($_POST['submitAction']) && $_POST['submitAction'] == 'closeModal') {
$closeModal = '?closeModal=true';
}
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasItem/'.$id.$closeModal);
} else {
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
}
}
}
if (isset($params['comment']) && isset($params['id'])) {
$itemId = (int) $params['id'];
// Only allow commenting on an item the user can view in their project.
if (! $this->blueprintsService->getCanvasItem($itemId, $canvasType)) {
return $this->tpl->displayPartial('errors.error404');
}
$values = [
'text' => $params['text'],
'date' => date('Y-m-d H:i:s'),
'userId' => (session('userdata.id')),
'moduleId' => $itemId,
'commentParent' => ($params['father']),
];
$commentId = $this->commentsRepo->addComment($values, static::CANVAS_NAME.'canvas'.'item');
$this->tpl->setNotification($this->language->__('notifications.comment_create_success'), 'success');
$values['id'] = $commentId;
$subject = $this->language->__('email_notifications.canvas_board_comment_created');
$actual_link = BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'#/editCanvasItem/'.$itemId;
$message = sprintf(
$this->language->__('email_notifications.canvas_item__comment_created_message'),
session('userdata.name')
);
$notification = app()->make(NotificationModel::class);
$notification->url = [
'url' => $actual_link,
'text' => $this->language->__('email_notifications.canvas_item_update_cta'),
];
$notification->entity = $values;
$notification->module = static::CANVAS_NAME.'canvas';
$notification->action = 'commented';
$notification->projectId = session('currentProject');
$notification->subject = $subject;
$notification->authorId = session('userdata.id');
$notification->message = $message;
$this->projectService->notifyProjectUsers($notification);
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas'.'/editCanvasItem/'.$itemId);
}
$allProjectMilestones = $this->ticketService->getAllMilestones(['sprint' => '', 'type' => 'milestone', 'currentProject' => session('currentProject')]);
$this->tpl->assign('milestones', $allProjectMilestones);
$this->tpl->assign('canvasTypes', $this->canvasRepo->getCanvasTypes());
$this->tpl->assign('statusLabels', $this->canvasRepo->getStatusLabels());
$this->tpl->assign('relatesLabels', $this->canvasRepo->getRelatesLabels());
$this->tpl->assign('dataLabels', $this->canvasRepo->getDataLabels());
if (isset($params['id'])) {
$canvasItemId = (int) $params['id'];
// VIEW-authorize before re-displaying; false = missing/foreign/unauthorized -> 404.
$canvasItem = $this->blueprintsService->getCanvasItem($canvasItemId, $canvasType);
if (! $canvasItem) {
return $this->tpl->displayPartial('errors.error404');
}
$comments = $this->commentsRepo->getComments(static::CANVAS_NAME.'canvas'.'item', $canvasItemId);
$this->tpl->assign('canvasItem', $canvasItem);
} else {
$value = [
'id' => '',
'box' => $params['box'],
'author' => session('userdata.id'),
'description' => '',
'status' => array_key_first($this->canvasRepo->getStatusLabels()),
'relates' => array_key_first($this->canvasRepo->getRelatesLabels()),
'assumptions' => '',
'data' => '',
'conclusion' => '',
'milestoneHeadline' => '',
'milestoneId' => '',
];
$comments = [];
$this->tpl->assign('canvasItem', $value);
}
$this->tpl->assign('comments', $comments);
$this->tpl->assign('currentCanvas', (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas'));
return $this->tpl->displayPartial(static::CANVAS_NAME.'canvas'.'.canvasDialog');
}
/**
* put - handle put requests
*/
public function put($params) {}
/**
* delete - handle delete requests
*/
public function delete($params) {}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace Leantime\Domain\Canvas\Controllers;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Str;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Configuration\Environment as EnvironmentCore;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Language as LanguageCore;
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
use Leantime\Domain\Projects\Services\Projects as ProjectService;
use Symfony\Component\HttpFoundation\Response;
/**
* Exports a canvas board as an XML file.
*/
class Export extends Controller
{
/**
* Constant that must be redefined by subclasses.
*/
protected const CANVAS_NAME = '??';
protected const CANVAS_TYPE = 'canvas';
protected EnvironmentCore $config;
protected LanguageCore $language;
protected mixed $canvasRepo;
protected BlueprintsService $blueprintsService;
protected array $canvasTypes;
protected array $statusLabels;
protected array $relatesLabels;
protected array $dataLabels;
/**
* Initializes dependencies.
*/
public function init(
EnvironmentCore $config,
LanguageCore $language,
): void {
$this->config = $config;
$this->language = $language;
$this->blueprintsService = app()->make(BlueprintsService::class);
$canvasName = Str::studly(static::CANVAS_NAME).static::CANVAS_TYPE;
$repoName = app()->getNamespace()."Domain\\$canvasName\\Repositories\\$canvasName";
$this->canvasRepo = app()->make($repoName);
$this->canvasTypes = $this->canvasRepo->getCanvasTypes();
$this->statusLabels = $this->canvasRepo->getStatusLabels();
$this->relatesLabels = $this->canvasRepo->getRelatesLabels();
$this->dataLabels = $this->canvasRepo->getDataLabels();
}
/**
* Generates and serves an XML export of the canvas.
*
* @param array $params Request parameters
*/
#[RequiresPermission(BlueprintsPermissions::VIEW, entityScoped: true)]
public function get(array $params): Response
{
$canvasId = (int) ($params['id'] ?? $_GET['id'] ?? 0);
if ($canvasId <= 0 && session()->exists('current'.strtoupper(static::CANVAS_NAME).'Canvas')) {
$canvasId = (int) session('current'.strtoupper(static::CANVAS_NAME).'Canvas');
}
if ($canvasId <= 0) {
return new Response;
}
$exportData = $this->export($canvasId);
clearstatcache();
$response = new Response($exportData);
$response->headers->set('Content-type', 'application/xml');
$response->headers->set('Content-Disposition', 'attachment; filename="'.static::CANVAS_NAME.static::CANVAS_TYPE.'-'.$canvasId.'.xml"');
$response->headers->set('Cache-Control', 'no-cache');
return $response;
}
/**
* Generates XML data for the given canvas.
*
* @param int $id Canvas identifier
* @return string XML data
*
* @throws BindingResolutionException
* @throws Exception
*/
protected function export(int $id): string
{
// getBoard authorizes VIEW against the board's real project and returns false for a
// missing/foreign/unauthorized board (export is reachable by arbitrary board id).
$canvasAry = $this->blueprintsService->getBoard($id, static::CANVAS_NAME.static::CANVAS_TYPE);
! empty($canvasAry) || throw new Exception("Cannot find canvas with id '$id'");
$projectId = $canvasAry[0]['projectId'];
$recordsAry = $this->blueprintsService->getBoardItems($id, static::CANVAS_NAME.static::CANVAS_TYPE, static::CANVAS_NAME.'canvasitem');
$projectService = app()->make(ProjectService::class);
$projectAry = $projectService->getProject($projectId);
! empty($projectAry) || throw new Exception("Cannot retrieve project id '$projectId'");
$xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>'.PHP_EOL.PHP_EOL;
$xml .= $this->xmlExport(static::CANVAS_NAME.static::CANVAS_TYPE, $canvasAry[0]['title'], $recordsAry);
return $xml;
}
/**
* Generates XML markup for canvas data.
*
* @param string $canvasKey Encoded canvas name
* @param string $canvasTitle Canvas title
* @param array $recordsAry Array of canvas entry records
* @param int $indent Indent level to use
* @return string XML data
*/
protected function xmlExport(string $canvasKey, string $canvasTitle, array $recordsAry, int $indent = 0): string
{
$is = str_repeat(' ', 4 * $indent);
$tab = str_repeat(' ', 4);
$xml = $is.'<canvas key="'.$canvasKey.'">'.PHP_EOL;
$xml .= $is.$tab.'<title>'.$canvasTitle.'</title>'.PHP_EOL;
$xml .= $is.$tab.'<content>'.PHP_EOL;
foreach ($this->canvasTypes as $key => $data) {
$xml .= $is.$tab.$tab.'<element key="'.$key.'">'.PHP_EOL;
foreach ($recordsAry as $record) {
if ($record['box'] === $key) {
$xml .= $is.$tab.$tab.$tab.'<item>'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.$tab.'<created>'.($record['created'] ?? '').'</created>'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.$tab.'<modified>'.($record['modified'] ?? '').'</modified>'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.$tab.'<author id="'.$record['author'].'" firstname="'.($record['authorFirstname'] ?? '').'" '.
'lastname="'.($record['authorLastname'] ?? '').'"/>'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.$tab.'<description>'.($record['description'] ?? '').'</description>'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.$tab.'<status key="'.($record['status'] ?? '').'" />'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.$tab.'<relates key="'.($record['relates'] ?? '').'" />'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.$tab.'<assumptions>'.($record['assumptions'] ?? '').'</assumptions>'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.$tab.'<data>'.($record['data'] ?? '').'</data>'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.$tab.'<conclusion>'.($record['conclusion'] ?? '').'</conclusion>'.PHP_EOL;
$xml .= $is.$tab.$tab.$tab.'</item>'.PHP_EOL;
}
}
$xml .= $is.$tab.$tab.'</element>'.PHP_EOL;
}
$xml .= $is.$tab.'</content>'.PHP_EOL;
$xml .= $is.'</canvas>'.PHP_EOL;
return $xml;
}
}

View File

@@ -0,0 +1,398 @@
<?php
namespace Leantime\Domain\Canvas\Controllers;
use Illuminate\Support\Str;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Core\Mailer as MailerCore;
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
use Leantime\Domain\Canvas\Services\Canvas as CanvaService;
use Leantime\Domain\Projects\Services\Projects as ProjectService;
use Leantime\Domain\Queue\Repositories\Queue as QueueRepository;
use Symfony\Component\HttpFoundation\Response;
class ShowCanvas extends Controller
{
/**
* Constant that must be redefined by subclasses.
*/
protected const CANVAS_NAME = '??';
private ProjectService $projectService;
private BlueprintsService $blueprintsService;
private object $canvasRepo;
/**
* Initializes dependencies.
*/
public function init(ProjectService $projectService): void
{
$this->projectService = $projectService;
$this->blueprintsService = app()->make(BlueprintsService::class);
$canvasName = Str::studly(static::CANVAS_NAME).'canvas';
$repoName = app()->getNamespace()."Domain\\$canvasName\\Repositories\\$canvasName";
$this->canvasRepo = app()->make($repoName);
}
/**
* Displays the canvas board view.
*
* @param array $params Request parameters
*/
#[RequiresPermission(BlueprintsPermissions::VIEW)]
public function get(array $params): Response
{
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
if (isset($_REQUEST['searchCanvas'])) {
$currentCanvasId = (int) $_REQUEST['searchCanvas'];
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
}
$this->assignTemplateVars($currentCanvasId, $allCanvas);
if (! isset($_GET['raw'])) {
return $this->tpl->display(static::CANVAS_NAME.'canvas.showCanvas');
}
return new Response;
}
/**
* Handles canvas mutations (create, edit, clone, merge, import).
*
* @param array $params Request parameters
*/
#[RequiresPermission(BlueprintsPermissions::EDIT)]
public function post(array $params): Response
{
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
$currentCanvasId = $this->resolveCurrentCanvasId($allCanvas, $params);
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;
}
}
if (isset($_POST['cloneCanvas']) && $currentCanvasId > 0) {
$result = $this->handleCloneCanvas($currentCanvasId, $allCanvas);
if ($result !== null) {
return $result;
}
}
if (isset($_POST['mergeCanvas']) && $currentCanvasId > 0) {
$result = $this->handleMergeCanvas($currentCanvasId);
if ($result !== null) {
return $result;
}
}
if (isset($_POST['importCanvas'])) {
$result = $this->handleImportCanvas($currentCanvasId, $allCanvas);
if ($result !== null) {
return $result;
}
}
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
$this->assignTemplateVars($currentCanvasId, $allCanvas);
if (! isset($_GET['raw'])) {
return $this->tpl->display(static::CANVAS_NAME.'canvas.showCanvas');
}
return new Response;
}
/**
* Resolves the current canvas ID from session, GET params, or creates a default.
*/
private function resolveCurrentCanvasId(array &$allCanvas, array $params): int
{
$sessionKey = 'current'.strtoupper(static::CANVAS_NAME).'Canvas';
if (! $allCanvas) {
$values = [
'title' => $this->language->__('label.board'),
'author' => session('userdata.id'),
'projectId' => session('currentProject'),
];
$currentCanvasId = $this->canvasRepo->addCanvas($values);
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
return $currentCanvasId;
}
$currentCanvasId = -1;
if (session()->exists($sessionKey)) {
$currentCanvasId = session($sessionKey);
$found = false;
foreach ($allCanvas as $row) {
if ($currentCanvasId == $row['id']) {
$found = true;
break;
}
}
if (! $found) {
$currentCanvasId = -1;
session([$sessionKey => '']);
}
} else {
session([$sessionKey => '']);
}
if (session($sessionKey) == '') {
$currentCanvasId = $allCanvas[0]['id'];
session([$sessionKey => $currentCanvasId]);
}
if (isset($params['id'])) {
// Only honor an explicit board id that belongs to the CURRENT project's boards
// ($allCanvas is project-scoped). A foreign/unknown id must not become the active
// board — otherwise assignTemplateVars would read another project's items.
$requestedId = (int) $params['id'];
$projectBoardIds = array_map(static fn ($row) => (int) $row['id'], $allCanvas);
if (in_array($requestedId, $projectBoardIds, true)) {
$currentCanvasId = $requestedId;
session([$sessionKey => $currentCanvasId]);
}
}
return $currentCanvasId;
}
/**
* Handles creating a new canvas 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;
}
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
return null;
}
$values = [
'title' => $_POST['canvastitle'],
'author' => session('userdata.id'),
'projectId' => session('currentProject'),
];
// createBoard authorizes CREATE against the target (current) project.
$currentCanvasId = $this->blueprintsService->createBoard($values, static::CANVAS_NAME.'canvas');
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
$this->notifyBoardCreated($values['title'], 'notification.board_created', 'email_notifications.canvas_created_message');
$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.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
}
/**
* Handles editing a canvas 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;
}
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
return null;
}
// renameBoard authorizes EDIT against the board's real project.
$currentCanvasId = $this->blueprintsService->renameBoard($currentCanvasId, $_POST['canvastitle'], static::CANVAS_NAME.'canvas');
$this->tpl->setNotification($this->language->__('notification.board_edited'), 'success');
return $this->tpl->displayPartial('canvas.boardDialog');
}
/**
* Handles cloning a canvas board.
*/
private function handleCloneCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
{
if (! isset($_POST['canvastitle']) || empty($_POST['canvastitle'])) {
$this->tpl->setNotification($this->language->__('notification.please_enter_title'), 'error');
return null;
}
if ($this->canvasRepo->existCanvas(session('currentProject'), $_POST['canvastitle'])) {
$this->tpl->setNotification($this->language->__('notification.board_exists'), 'error');
return null;
}
// copyBoard authorizes VIEW on the source board's project and CREATE on the target.
$currentCanvasId = $this->blueprintsService->copyBoard(
$currentCanvasId,
(int) session('currentProject'),
(int) session('userdata.id'),
$_POST['canvastitle'],
static::CANVAS_NAME.'canvas'
);
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
$this->tpl->setNotification($this->language->__('notification.board_copied'), 'success');
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
}
/**
* Handles merging two canvas boards.
*/
private function handleMergeCanvas(int $currentCanvasId): ?Response
{
if (! isset($_POST['canvasid']) || $_POST['canvasid'] <= 0) {
$this->tpl->setNotification($this->language->__('notification.internal_error'), 'error');
return null;
}
// mergeBoard authorizes EDIT on the target board's project and VIEW on the source's.
$status = $this->blueprintsService->mergeBoard($currentCanvasId, (int) $_POST['canvasid'], static::CANVAS_NAME.'canvas');
if ($status) {
$this->tpl->setNotification($this->language->__('notification.board_merged'), 'success');
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
}
$this->tpl->setNotification($this->language->__('notification.merge_error'), 'error');
return null;
}
/**
* Handles importing a canvas from an XML file.
*/
private function handleImportCanvas(int &$currentCanvasId, array &$allCanvas): ?Response
{
if (! isset($_FILES['canvasfile']) || $_FILES['canvasfile']['error'] !== 0) {
return null;
}
$uploadfile = tempnam(sys_get_temp_dir(), 'leantime.').'.xml';
if (! move_uploaded_file($_FILES['canvasfile']['tmp_name'], $uploadfile)) {
$this->tpl->setNotification($this->language->__('notification.board_import_failed'), 'error');
return null;
}
$services = app()->make(CanvaService::class);
$importCanvasId = $services->import(
$uploadfile,
static::CANVAS_NAME.'canvas',
projectId: session('currentProject'),
authorId: session('userdata.id')
);
unlink($uploadfile);
if ($importCanvasId === false) {
$this->tpl->setNotification($this->language->__('notification.board_import_failed'), 'error');
return null;
}
$currentCanvasId = $importCanvasId;
$allCanvas = $this->canvasRepo->getAllCanvas(session('currentProject'));
session(['current'.strtoupper(static::CANVAS_NAME).'Canvas' => $currentCanvasId]);
$canvas = $this->blueprintsService->getBoard($currentCanvasId, static::CANVAS_NAME.'canvas');
$this->notifyBoardCreated(
strip_tags($canvas !== false ? ($canvas[0]['title'] ?? '') : ''),
'notification.board_imported',
'email_notifications.canvas_imported_message'
);
$this->tpl->setNotification($this->language->__('notification.board_imported'), 'success');
return Frontcontroller::redirect(BASE_URL.'/'.static::CANVAS_NAME.'canvas/showCanvas/');
}
/**
* Sends board creation/import notifications to project users.
*/
private function notifyBoardCreated(string $title, string $subjectKey, string $messageKey): void
{
$mailer = app()->make(MailerCore::class);
$users = $this->projectService->getUsersToNotify(session('currentProject'));
$mailer->setSubject($this->language->__($subjectKey));
$message = sprintf(
$this->language->__($messageKey),
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->__($subjectKey),
session('currentProject')
);
}
/**
* Assigns common template variables.
*/
private function assignTemplateVars(int $currentCanvasId, array $allCanvas): void
{
$filter['status'] = $_GET['filter_status'] ?? (session('filter_status') ?? 'all');
session(['filter_status' => $filter['status']]);
$filter['relates'] = $_GET['filter_relates'] ?? (session('filter_relates') ?? 'all');
session(['filter_relates' => $filter['relates']]);
$this->tpl->assign('filter', $filter);
$this->tpl->assign('currentCanvas', $currentCanvasId);
$this->tpl->assign('canvasIcon', $this->canvasRepo->getIcon());
$this->tpl->assign('canvasTypes', $this->canvasRepo->getCanvasTypes());
$this->tpl->assign('statusLabels', $this->canvasRepo->getStatusLabels());
$this->tpl->assign('relatesLabels', $this->canvasRepo->getRelatesLabels());
$this->tpl->assign('dataLabels', $this->canvasRepo->getDataLabels());
$this->tpl->assign('disclaimer', $this->canvasRepo->getDisclaimer());
$this->tpl->assign('allCanvas', $allCanvas);
// getBoardItems authorizes VIEW against the board's real project; [] for foreign/unknown.
$this->tpl->assign('canvasItems', $this->blueprintsService->getBoardItems($currentCanvasId, static::CANVAS_NAME.'canvas', static::CANVAS_NAME.'canvasitem'));
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject(session('currentProject')));
}
}

View File

@@ -0,0 +1,242 @@
leantime.canvasController = (function () {
var canvasName = '';
var setCanvasName = function (name) {
canvasName = name;
};
var initFilterBar = function () {
jQuery(window).bind("load", function () {
jQuery(".loading").fadeOut();
jQuery(".filterBar .row-fluid").css("opacity", "1");
});
};
var initCanvasLinks = function () {
jQuery(".addCanvasLink").nyroModal();
jQuery(".editCanvasLink").click(function () {
jQuery('#editCanvas').modal('show');
});
jQuery(".cloneCanvasLink").click(function () {
jQuery('#cloneCanvas').modal('show');
});
jQuery(".mergeCanvasLink").click(function () {
jQuery('#mergeCanvas').modal('show');
});
jQuery(".importCanvasLink").click(function () {
jQuery('#importCanvas').modal('show');
});
};
var closeModal = false;
//Variables
var canvasoptions = {
sizes: {
minW: 700,
minH: 1000,
},
resizable: true,
autoSizable: true,
callbacks: {
beforeShowCont: function () {
jQuery(".showDialogOnLoad").show();
if (closeModal == true) {
closeModal = false;
location.reload();
}
},
afterShowCont: function () {
window.htmx.process('.nyroModalCont');
jQuery("." + canvasName + "CanvasModal, #commentForm, #commentForm .deleteComment, ." + canvasName + "CanvasMilestone .deleteMilestone").nyroModal(canvasoptions);
},
beforeClose: function () {
location.reload();
}
},
titleFromIframe: true
};
//Functions
var _initModals = function () {
jQuery("." + canvasName + "CanvasModal, #commentForm, #commentForm .deleteComment, ." + canvasName + "CanvasMilestone .deleteMilestone").nyroModal(canvasoptions);
};
var openModalManually = function (url) {
jQuery.nmManual(url, canvasoptions);
};
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];
jQuery.ajax(
{
type: 'PATCH',
url: leantime.appUrl + '/api/' + canvasName + 'canvas',
data:
{
id : canvasId,
author:userId
}
}
).done(
function () {
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"), style: "success"});
}
);
}
}
);
};
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 == 2) {
var canvasItemId = dataValue[0];
var status = dataValue[1];
var statusClass = jQuery(this).attr('class');
jQuery.ajax(
{
type: 'PATCH',
url: leantime.appUrl + '/api/' + canvasName + 'canvas',
data:
{
id : canvasItemId,
status: status
}
}
).done(
function () {
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")});
}
);
}
}
);
};
var initRelatesDropdown = function () {
jQuery("body").on(
"click",
".relatesDropdown .dropdown-menu a",
function () {
var dataValue = jQuery(this).attr("data-value").split("/");
var dataLabel = jQuery(this).attr('data-label');
if (dataValue.length == 2) {
var canvasItemId = dataValue[0];
var relates = dataValue[1];
var relatesClass = jQuery(this).attr('class');
jQuery.ajax(
{
type: 'PATCH',
url: leantime.appUrl + '/api/' + canvasName + 'canvas',
data:
{
id : canvasItemId,
relates: relates
}
}
).done(
function () {
jQuery("#relatesDropdownMenuLink" + canvasItemId + " span.text").text(dataLabel);
jQuery("#relatesDropdownMenuLink" + canvasItemId).removeClass().addClass(relatesClass + " dropdown-toggle f-left relates ");
jQuery.growl({message: leantime.i18n.__("short_notifications.relates_updated")});
}
);
}
}
);
};
// Make public what you want to have public, everything else is private
return {
setCanvasName:setCanvasName,
initFilterBar:initFilterBar,
initCanvasLinks:initCanvasLinks,
initUserDropdown:initUserDropdown,
initStatusDropdown:initStatusDropdown,
initRelatesDropdown:initRelatesDropdown,
setCloseModal:setCloseModal,
toggleMilestoneSelectors:toggleMilestoneSelectors,
openModalManually:openModalManually
};
})();

View File

@@ -0,0 +1,463 @@
<?php
/**
* canvas class - DEPRECATED backwards-compatibility adapter.
*/
namespace Leantime\Domain\Canvas\Repositories;
use Illuminate\Database\ConnectionInterface;
use Leantime\Core\Db\DatabaseHelper;
use Leantime\Core\Db\Db as DbCore;
use Leantime\Core\Language as LanguageCore;
use Leantime\Domain\Blueprints\Repositories\Blueprints as BlueprintsRepository;
use Leantime\Domain\Tickets\Repositories\Tickets;
/**
* Thin backwards-compatibility shim over the Blueprints repository.
*
* The canvas system was consolidated into Leantime\Domain\Blueprints. This class
* is kept ONLY so plugins (and any code) that still extend or instantiate the old
* canvas repository keep working: its data methods now delegate to the Blueprints
* repository, deriving the canvas type from the subclass CANVAS_NAME constant.
*
* @deprecated Use Leantime\Domain\Blueprints\Repositories\Blueprints instead.
* Do not build new features on this class.
*/
class Canvas extends BlueprintsRepository
{
/**
* Constant that must be redefined
*/
protected const CANVAS_NAME = '??';
/***
* icon - Icon associated with canvas (must be extended)
*
* @access protected
* @var string Fontawesome icone
*/
protected string $icon = 'fa-x';
/***
* disclaimer - Disclaimer (may be extended)
*
* @access protected
* @var string Disclaimer (including href)
*/
protected string $disclaimer = '';
/**
* canvasTypes - Canvas elements / boxes (must be extended)
*
* @acces protected
*/
protected array $canvasTypes = [
// '??_' => [ 'icon' => 'fa-????', 'title' => 'box.??.????' ],
];
/**
* statusLabels - Status labels (may be extended)
*
* @acces protected
*/
protected array $statusLabels = [
'status_draft' => ['icon' => 'fa-circle-question', 'color' => 'blue', 'title' => 'status.draft', 'dropdown' => 'info', 'active' => true],
'status_review' => ['icon' => 'fa-circle-exclamation', 'color' => 'orange', 'title' => 'status.review', 'dropdown' => 'warning', 'active' => true],
'status_valid' => ['icon' => 'fa-circle-check', 'color' => 'green', 'title' => 'status.valid', 'dropdown' => 'success', 'active' => true],
'status_hold' => ['icon' => 'fa-circle-h', 'color' => 'red', 'title' => 'status.hold', 'dropdown' => 'danger', 'active' => true],
'status_invalid' => ['icon' => 'fa-circle-xmark', 'color' => 'red', 'title' => 'status.invalid', 'dropdown' => 'danger', 'active' => true],
];
/**
* relatesLabels - Relates to label (same structure as `statusLabels`)
*
* @acces public
*/
protected array $relatesLabels = [
'relates_none' => ['icon' => 'fa-border-none', 'color' => 'grey', 'title' => 'relates.none', 'dropdown' => 'default', 'active' => true],
'relates_customers' => ['icon' => 'fa-users', 'color' => 'green', 'title' => 'relates.customers', 'dropdown' => 'success', 'active' => true],
'relates_offerings' => ['icon' => 'fa-barcode', 'color' => 'red', 'title' => 'relates.offerings', 'dropdown' => 'danger', 'active' => true],
'relates_capabilities' => ['icon' => 'fa-pen-ruler', 'color' => 'blue', 'title' => 'relates.capabilities', 'dropdown' => 'info', 'active' => true],
'relates_financials' => ['icon' => 'fa-money-bill', 'color' => 'yellow', 'title' => 'relates.financials', 'dropdown' => 'warning', 'active' => true],
'relates_markets' => ['icon' => 'fa-shop', 'color' => 'brown', 'title' => 'relates.markets', 'dropdown' => 'default', 'active' => true],
'relates_environment' => ['icon' => 'fa-tree', 'color' => 'darkgreen', 'title' => 'relates.environment', 'dropdown' => 'default', 'active' => true],
'relates_firm' => ['icon' => 'fa-building', 'color' => 'darkblue', 'title' => 'relates.firm', 'dropdown' => 'info', 'active' => true],
];
/**
* dataLabels - Data labels (may be extended)
*
* @acces protected
*/
protected array $dataLabels = [
1 => ['title' => 'label.assumptions', 'field' => 'assumptions', 'active' => true],
2 => ['title' => 'label.data', 'field' => 'data', 'active' => true],
3 => ['title' => 'label.conclusion', 'field' => 'conclusion', 'active' => true],
];
public ?object $result = null;
public ?object $tickets = null;
protected ?DbCore $db = null;
protected ConnectionInterface $connection;
protected DatabaseHelper $dbHelper;
private LanguageCore $language;
/**
* __construct - get db connection and initialise the underlying Blueprints repository
*/
public function __construct(
DbCore $db,
LanguageCore $language,
Tickets $ticketRepo,
DatabaseHelper $dbHelper
) {
// Initialise the Blueprints parent so the delegated methods have a connection.
parent::__construct($db, $ticketRepo, $dbHelper);
// Keep local copies for the config accessors and goal-specific queries below.
$this->db = $db;
$this->connection = $db->getConnection();
$this->language = $language;
$this->dbHelper = $dbHelper;
}
/**
* getIcon() - Retrieve canvas icon
*
* @return string Canvas icon
*/
public function getIcon(): string
{
return $this->icon;
}
/**
* getDisclaimer() - Retrieve disclaimer
*
* @return string Canvas disclaimer
*/
public function getDisclaimer(): string
{
if (empty($this->disclaimer)) {
return '';
}
return $this->language->__($this->disclaimer);
}
/**
* getCanvasTypes() - Retrieve translated canvaas items
*
* @return array Array of data
*/
public function getCanvasTypes(): array
{
$canvasTypes = $this->canvasTypes;
foreach ($canvasTypes as $key => $data) {
if (isset($data['title'])) {
$canvasTypes[$key]['title'] = $this->language->__($data['title']);
}
}
return $canvasTypes;
}
/**
* getStatusLabels() - Retrieve translated status labels
*
* @return array Array of data
*/
public function getStatusLabels(): array
{
$statusLabels = $this->statusLabels;
foreach ($statusLabels as $key => $data) {
if (isset($data['title'])) {
$statusLabels[$key]['title'] = $this->language->__($data['title']);
}
}
return $statusLabels;
}
/**
* getRelatesLabels() - Retrieve translated relates labels
*
* @return array Array of data
*/
public function getRelatesLabels(): array
{
$relatesLabels = $this->relatesLabels;
foreach ($relatesLabels as $key => $data) {
if (isset($data['title'])) {
$relatesLabels[$key]['title'] = $this->language->__($data['title']);
}
}
return $relatesLabels;
}
/**
* getDataLabels() - Retrieve translated data labels
*
* @return array Array of data
*/
public function getDataLabels(): array
{
$dataLabels = $this->dataLabels;
foreach ($dataLabels as $key => $data) {
if (isset($data['title'])) {
$dataLabels[$key]['title'] = $this->language->__($data['title']);
}
}
return $dataLabels;
}
/**
* @deprecated delegate to Blueprints repository
*/
public function getAllCanvas($projectId, $type = null): false|array
{
$canvasType = ($type === null || $type === '') ? static::CANVAS_NAME.'canvas' : $type;
return parent::getAllCanvas((int) $projectId, $canvasType);
}
/**
* @deprecated delegate to Blueprints repository
*/
public function getSingleCanvas($canvasId, $canvasType = null): false|array
{
return parent::getSingleCanvas((int) $canvasId, $canvasType ?: static::CANVAS_NAME.'canvas');
}
/**
* @deprecated delegate to Blueprints repository
*/
public function addCanvas($values, $type = null): false|string
{
$canvasType = ($type === null || $type === '') ? static::CANVAS_NAME.'canvas' : $type;
return parent::addCanvas($values, $canvasType);
}
/**
* @deprecated delegate to Blueprints repository
*/
public function getCanvasItemsById($id, $commentModule = null): false|array
{
return parent::getCanvasItemsById((int) $id, $commentModule ?: static::CANVAS_NAME.'canvasitem');
}
/**
* @deprecated delegate to Blueprints repository
*/
public function getNumberOfCanvasItems($projectId = null, $canvasType = null): mixed
{
return parent::getNumberOfCanvasItems($projectId !== null ? (int) $projectId : null, $canvasType ?: static::CANVAS_NAME.'canvas');
}
/**
* @deprecated delegate to Blueprints repository
*/
public function getNumberOfBoards($projectId = null, $canvasType = null): mixed
{
return parent::getNumberOfBoards($projectId !== null ? (int) $projectId : null, $canvasType ?: static::CANVAS_NAME.'canvas');
}
/**
* existCanvas - return if a canvas exists with a given title in the specified project
*
* @param int $projectId Project identifier
* @param string $canvasTitle Canvas title
* @return bool True if canvas exists
*
* @deprecated delegate to Blueprints repository
*/
public function existCanvas(int $projectId, string $canvasTitle, $canvasType = null): bool
{
return parent::existCanvas($projectId, $canvasTitle, $canvasType ?: static::CANVAS_NAME.'canvas');
}
/***
* copyCanvas - create a copy of an existing canvas
*
* @deprecated delegate to Blueprints repository
*/
public function copyCanvas(int $projectId, int $canvasId, int $authorId, string $canvasTitle, $canvasType = null): int
{
return parent::copyCanvas($projectId, $canvasId, $authorId, $canvasTitle, $canvasType ?: static::CANVAS_NAME.'canvas');
}
/**
* getCanvasItemsByKPI - goal-specific KPI hierarchy query (not part of Blueprints).
*/
public function getCanvasItemsByKPI($id): false|array
{
$results = $this->connection->table('zp_canvas_items')
->select([
'zp_canvas_items.id',
'zp_canvas_items.title',
'zp_canvas_items.kpi',
'zp_canvas_items.startDate',
'zp_canvas_items.endDate',
'zp_canvas_items.setting',
'zp_canvas_items.metricType',
'zp_canvas_items.startValue',
'zp_canvas_items.currentValue',
'zp_canvas_items.endValue',
'zp_canvas_items.canvasId',
'zp_canvas.title as boardTitle',
'zp_canvas.projectId as projectId',
'zp_projects.name as projectName',
'childrenLvl1.id as childId',
'childrenLvl1.title as childTitle',
'childrenLvl1.kpi as childKpi',
'childrenLvl1.startDate as childStartDate',
'childrenLvl1.endDate as childEndDate',
'childrenLvl1.setting as childSetting',
'childrenLvl1.metricType as childMetricType',
'childrenLvl1.startValue as childStartValue',
'childrenLvl1.currentValue as childCurrentValue',
'childrenLvl1.endValue as childEndValue',
'childrenLvl1.canvasId as childCanvasId',
'childrenLvl1Board.title as childBoardTitle',
'childrenLvl1Project.name as childProjectName',
])
->leftJoin('zp_canvas', 'zp_canvas_items.canvasId', '=', 'zp_canvas.id')
->leftJoin('zp_projects', 'zp_canvas.projectId', '=', 'zp_projects.id')
->leftJoin('zp_canvas_items as childrenLvl1', 'childrenLvl1.kpi', '=', 'zp_canvas_items.id')
->leftJoin('zp_canvas as childrenLvl1Board', 'childrenLvl1.canvasId', '=', 'childrenLvl1Board.id')
->leftJoin('zp_projects as childrenLvl1Project', 'childrenLvl1Board.projectId', '=', 'childrenLvl1Project.id')
->where('zp_canvas_items.box', 'goal')
->where('zp_canvas_items.kpi', $id)
->get();
return array_map(fn ($item) => (array) $item, $results->toArray());
}
/**
* getAllAvailableParents - goal-specific parent lookup (not part of Blueprints).
*/
public function getAllAvailableParents($projectId): false|array
{
$results = $this->connection->table('zp_canvas_items')
->select([
'zp_canvas_items.id',
'zp_canvas_items.description',
'zp_canvas_items.title',
'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.relates',
'zp_canvas_items.milestoneId',
'zp_canvas_items.kpi',
'zp_canvas_items.data1',
'zp_canvas_items.data2',
'zp_canvas_items.data3',
'zp_canvas_items.data4',
'zp_canvas_items.data5',
'zp_canvas_items.startDate',
'zp_canvas_items.endDate',
'zp_canvas_items.setting',
'zp_canvas_items.metricType',
'zp_canvas_items.startValue',
'zp_canvas_items.currentValue',
'zp_canvas_items.endValue',
'zp_canvas_items.impact',
'zp_canvas_items.effort',
'zp_canvas_items.probability',
'zp_canvas_items.action',
'zp_canvas_items.assignedTo',
'zp_canvas_items.parent',
'zp_canvas_items.tags',
'board.title as boardTitle',
'parentKPI.description as parentKPIDescription',
'parentGoal.description as parentGoalDescription',
't1.firstname as authorFirstname',
't1.lastname as authorLastname',
'milestone.headline as milestoneHeadline',
'milestone.editTo as milestoneEditTo',
])
->leftJoin('zp_canvas as board', 'board.id', '=', 'zp_canvas_items.canvasId')
->leftJoin('zp_canvas_items as parentKPI', 'zp_canvas_items.kpi', '=', 'parentKPI.id')
->leftJoin('zp_canvas_items as parentGoal', 'zp_canvas_items.parent', '=', 'parentGoal.id')
->leftJoin('zp_tickets as milestone', function ($join) {
$join->on('zp_canvas_items.milestoneId', '=', $this->connection->raw($this->dbHelper->castAs($this->dbHelper->wrapColumn('milestone.id'), 'text')));
})
->leftJoin('zp_user as t1', 'zp_canvas_items.author', '=', 't1.id')
->where('board.projectId', $projectId)
->groupBy(['id', 'board.id', 'board.title', 'parentKPI.description', 'parentGoal.description', 't1.firstname', 't1.lastname', 'milestone.headline', 'milestone.editTo'])
->orderBy('board.id')
->get();
return array_map(fn ($item) => (array) $item, $results->toArray());
}
/**
* getAllAvailableKPIs - goal-specific KPI lookup across parent projects (not part of Blueprints).
*/
public function getAllAvailableKPIs($projectId): false|array
{
// First, get parent project IDs (cross-database compatible approach)
$parentProjectIds = [];
$parentProject = $this->connection->table('zp_projects')
->select('parent')
->where('id', $projectId)
->first();
if ($parentProject && $parentProject->parent) {
$parentProjectIds[] = $parentProject->parent;
// Check for grandparent
$grandparent = $this->connection->table('zp_projects')
->select('parent')
->where('id', $parentProject->parent)
->first();
if ($grandparent && $grandparent->parent) {
$parentProjectIds[] = $grandparent->parent;
}
}
// If no parent projects found, return empty array
if (empty($parentProjectIds)) {
return [];
}
// Now query canvas items from parent projects
$results = $this->connection->table('zp_canvas_items')
->select([
'zp_canvas_items.id',
'zp_canvas_items.description',
'project.name as projectName',
'zp_canvas.title as boardTitle',
])
->leftJoin('zp_canvas', 'zp_canvas.id', '=', 'zp_canvas_items.canvasId')
->leftJoin('zp_projects as project', 'zp_canvas.projectId', '=', 'project.id')
->whereIn('zp_canvas.projectId', $parentProjectIds)
->where(function ($query) {
$query->where('zp_canvas_items.setting', 'linkAndReport')
->orWhere('zp_canvas_items.setting', 'linkonly');
})
->orderBy('zp_canvas.id')
->get();
return array_map(fn ($item) => (array) $item, $results->toArray());
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Leantime\Domain\Canvas\Services;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Domain\Blueprints\Permissions\BlueprintsPermissions;
use Leantime\Domain\Blueprints\Services\Blueprints as BlueprintsService;
/**
* Thin backwards-compatibility shim over the Blueprints service.
*
* The canvas system was consolidated into Leantime\Domain\Blueprints. This class
* is kept ONLY so plugins (and any code) that still call the old canvas service
* keep working: every method delegates to the Blueprints service.
*
* @deprecated Use Leantime\Domain\Blueprints\Services\Blueprints instead.
* Do not build new features on this class.
*
* @api
*/
class Canvas
{
/**
* import - Import canvas from XML file (delegates to Blueprints).
*
* @param string $filename File to import
* @param string $canvasName Legacy canvas type (e.g. "swotcanvas") or slug
* @param int $projectId Project identifier
* @param int $authorId Author identifier
* @return bool|int False if import failed, otherwise the new canvas id
*
* @deprecated use Blueprints service
*
* @api
*/
#[RequiresPermission(BlueprintsPermissions::CREATE, projectIdParam: 'projectId')]
public function import(string $filename, string $canvasName, int $projectId, int $authorId): bool|int
{
// Old callers pass the full type ("swotcanvas"); Blueprints works on the slug ("swot").
$canvasSlug = str_ends_with($canvasName, 'canvas')
? substr($canvasName, 0, -strlen('canvas'))
: $canvasName;
return app(BlueprintsService::class)->import($filename, $canvasSlug, $projectId, $authorId);
}
/**
* getBoardProgress - completion percentage per canvas type (delegates to Blueprints).
*
* @param string $projectId projectId (optional)
* @param array $boards Array of project board types
* @return array List of boards with a progress percentage
*
* @deprecated use Blueprints service
*
* @api
*/
#[RequiresPermission(BlueprintsPermissions::VIEW, projectIdParam: 'projectId')]
public function getBoardProgress(string $projectId = '', array $boards = []): array
{
return app(BlueprintsService::class)->getBoardProgress($projectId, $boards);
}
/**
* getLastUpdatedCanvas - canvas boards ordered by last updated item (delegates to Blueprints).
*
* @param int|null $projectId projectId (optional)
* @param array $boards Array of project board types
* @return array List of boards
*
* @deprecated use Blueprints service
*
* @api
*/
#[RequiresPermission(BlueprintsPermissions::VIEW, projectIdParam: 'projectId')]
public function getLastUpdatedCanvas(?int $projectId = null, array $boards = []): array
{
return app(BlueprintsService::class)->getLastUpdatedCanvas($projectId, $boards);
}
}

View File

@@ -0,0 +1,25 @@
@php
$canvasTitle = $canvasTitle ?? '';
$canvasName = $canvasName ?? '';
@endphp
<form action="{{ BASE_URL }}/{{ $canvasName }}canvas/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> {!! __('subtitles.create_new_board') !!}</h4>
</div>
<div class="modal-body">
<label>{!! __('label.title_new') !!}</label><br />
<x-global::forms.text-input name="canvastitle" value="{{ $canvasTitle }}" placeholder="{{ __('input.placeholders.enter_title_for_board') }}"
style="width: 100%" />
</div>
<div class="modal-footer">
@if(isset($_GET['id']))
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save_board')" name="newCanvas" />
<input type="hidden" name="editCanvas" value="{{ (int) $_GET['id'] }}">
@else
<input type="hidden" name="newCanvas" value="true">
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.create_board')" name="newCanvas" />
@endif
<x-global::forms.button inputType="button" contentRole="tertiary" onclick="jQuery.nmTop().close();">{!! __('buttons.close') !!}</x-global::forms.button>
</div>
</form>

View File

@@ -0,0 +1,54 @@
@php
$canvasName = $canvasName ?? '';
$canvasItem = $canvasItem ?? ['id' => '', 'box' => '', 'description' => ''];
$canvasTypes = $canvasTypes ?? [];
$id = '';
if (isset($canvasItem['id']) && $canvasItem['id'] != '') {
$id = $canvasItem['id'];
}
@endphp
<script type="text/javascript">
window.onload = function() {
if (!window.jQuery) {
//It's not a modal
location.href="{{ BASE_URL }}/{{ $canvasName }}canvas/showCanvas?showModal={{ $canvasItem['id'] }}";
}
}
</script>
<div class="showDialogOnLoad" style="display:none;">
<h4 class="widgettitle title-light" style="padding-bottom: 0"><i class="fas {{ $canvasTypes[$canvasItem['box']]['icon'] ?? '' }}"></i> {{ $canvasTypes[$canvasItem['box']]['title'] ?? '' }}</h4>
<hr style="margin-top: 5px; margin-bottom: 15px;">
{!! $tpl->displayNotification() !!}
<h5 style="padding-left: 40px"><strong>{{ $canvasItem['description'] }}</strong></h5>
@if($id !== '')
<br />
<input type="hidden" name="comment" value="1" />
<h4 class="widgettitle title-light"><span class="fa fa-comments"></span>{!! __('subtitles.discussion') !!}</h4>
@include('comments::submodules.generalComment', ['formUrl' => '/' . $canvasName . 'canvas/editCanvasComment/' . $id])
@endif
</div>
<script type="text/javascript">
jQuery(document).ready(function(){
if (window.leantime && window.leantime.tiptapController) {
leantime.tiptapController.initSimpleEditor();
}
@if(! $login::userIsAtLeast($roles::$editor))
leantime.authController.makeInputReadonly(".nyroModalCont");
@endif
@if($login::userHasRole([$roles::$commenter]))
leantime.commentsController.enableCommenterForms();
@endif
})
</script>

View File

@@ -0,0 +1,227 @@
@php
$canvasName = $canvasName ?? '';
$currentCanvas = $currentCanvas ?? '';
$canvasItem = $canvasItem ?? ['id' => '', 'box' => '', 'description' => '', 'status' => '', 'relates' => '', 'milestoneId' => '', 'milestoneHeadline' => ''];
$canvasTypes = $canvasTypes ?? [];
$hiddenStatusLabels = $statusLabels ?? [];
$statusLabels = $statusLabels ?? [];
$hiddenRelatesLabels = $relatesLabels ?? [];
$relatesLabels = $relatesLabels ?? [];
$dataLabels = $dataLabels ?? [1 => ['active' => false, 'field' => '', 'title' => ''], 2 => ['active' => false, 'field' => '', 'title' => ''], 3 => ['active' => false, 'field' => '', 'title' => '']];
$milestones = $milestones ?? [];
$users = $users ?? [];
$searchCriteria = $searchCriteria ?? [];
$id = '';
if (isset($canvasItem['id']) && $canvasItem['id'] != '') {
$id = $canvasItem['id'];
}
@endphp
<script type="text/javascript">
window.onload = function() {
if (!window.jQuery) {
//It's not a modal
location.href="{{ BASE_URL }}/{{ $canvasName }}canvas/showCanvas?showModal={{ $canvasItem['id'] }}";
}
}
</script>
<div class="" style="width:900px;">
<h4 class="widgettitle title-light" style="padding-bottom: 0"><i class="fas {{ $canvasTypes[$canvasItem['box']]['icon'] }}"></i> {{ $canvasTypes[$canvasItem['box']]['title'] }}</h4>
<hr style="margin-top: 5px; margin-bottom: 15px;">
{!! $tpl->displayNotification() !!}
<form class="formModal" method="post" action="{{ BASE_URL }}/{{ $canvasName }}canvas/editCanvasItem/{{ $id }}">
<input type="hidden" value="{{ $currentCanvas }}" name="canvasId" />
<input type="hidden" value="{{ $canvasItem['box'] }}" name="box" id="box"/>
<input type="hidden" value="{{ $id }}" name="itemId" id="itemId"/>
<label>{!! __('label.description') !!}</label>
<x-global::forms.text-input name="description" value="{{ $canvasItem['description'] }}" style="width:100%" /><br />
@if(! empty($statusLabels))
<label>{!! __('label.status') !!}</label>
<select name="status" style="width: 50%" id="statusCanvas">
</select><br /><br />
@else
<input type="hidden" name="status" value="{{ $canvasItem['status'] ?? array_key_first($hiddenStatusLabels) }}" />
@endif
@if(! empty($relatesLabels))
<label>{!! __('label.relates') !!}</label>
<select name="relates" style="width: 50%" id="relatesCanvas">
</select><br />
@else
<input type="hidden" name="relates" value="{{ $canvasItem['relates'] ?? array_key_first($hiddenRelatesLabels) }}" />
@endif
@if($dataLabels[1]['active'])
<label>{!! __($dataLabels[1]['title']) !!}</label>
@if(isset($dataLabels[1]['type']) && $dataLabels[1]['type'] == 'int')
<x-global::forms.text-input type="number" name="{{ $dataLabels[1]['field'] }}" value="{{ $canvasItem[$dataLabels[1]['field']] }}" /><br />
@elseif(isset($dataLabels[1]['type']) && $dataLabels[1]['type'] == 'string')
<x-global::forms.text-input name="{{ $dataLabels[1]['field'] }}" value="{{ $canvasItem[$dataLabels[1]['field']] }}" style="width:100%" /><br />
@else
<textarea style="width:100%" rows="3" cols="10" name="{{ $dataLabels[1]['field'] }}" class="modalTextArea tiptapSimple">{{ $canvasItem[$dataLabels[1]['field']] }}</textarea><br />
@endif
@else
<input type="hidden" name="{{ $dataLabels[1]['field'] }}" value="" />
@endif
@if($dataLabels[2]['active'])
<label>{!! __($dataLabels[2]['title']) !!}</label>
@if(isset($dataLabels[2]['type']) && $dataLabels[2]['type'] == 'int')
<x-global::forms.text-input type="number" name="{{ $dataLabels[2]['field'] }}" value="{{ $canvasItem[$dataLabels[2]['field']] }}" /><br />
@elseif(isset($dataLabels[2]['type']) && $dataLabels[2]['type'] == 'string')
<x-global::forms.text-input name="{{ $dataLabels[2]['field'] }}" value="{{ $canvasItem[$dataLabels[2]['field']] }}" style="width:100%" /><br />
@else
<textarea style="width:100%" rows="3" cols="10" name="{{ $dataLabels[2]['field'] }}" class="modalTextArea tiptapSimple">{{ $canvasItem[$dataLabels[2]['field']] }}</textarea><br />
@endif
@else
<input type="hidden" name="{{ $dataLabels[2]['field'] }}" value="" />
@endif
@if($dataLabels[3]['active'])
<label>{!! __($dataLabels[3]['title']) !!}</label>
@if(isset($dataLabels[3]['type']) && $dataLabels[3]['type'] == 'int')
<x-global::forms.text-input type="number" name="{{ $dataLabels[3]['field'] }}" value="{{ $canvasItem[$dataLabels[3]['field']] }}" /><br />
@elseif(isset($dataLabels[3]['type']) && $dataLabels[3]['type'] == 'string')
<x-global::forms.text-input name="{{ $dataLabels[3]['field'] }}" value="{{ $canvasItem[$dataLabels[3]['field']] }}" /><br />
@else
<textarea style="width:100%" rows="3" cols="10" name="{{ $dataLabels[3]['field'] }}" class="modalTextArea tiptapSimple">{{ $canvasItem[$dataLabels[3]['field']] }}</textarea><br />
@endif
@else
<input type="hidden" name="{{ $dataLabels[3]['field'] }}" value="" />
@endif
<input type="hidden" name="milestoneId" value="{{ $canvasItem['milestoneId'] }}" />
<input type="hidden" name="changeItem" value="1" />
@if($id != '')
<x-global::forms.button tag="a" link="{{ BASE_URL }}/{{ $canvasName }}canvas/delCanvasItem/{{ $id }}" class="{{ $canvasName }}CanvasModal delete right" state="danger" variant="outline"><i class='fa fa-trash-can'></i> {!! __('links.delete') !!}</x-global::forms.button>
@endif
@if($login::userIsAtLeast($roles::$editor))
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" id="primaryCanvasSubmitButton" />
<x-global::forms.button inputType="submit" contentRole="secondary" value="closeModal" id="saveAndClose" onclick="leantime.{{ $canvasName }}CanvasController.setCloseModal();">{!! __('buttons.save_and_close') !!}</x-global::forms.button>
@endif
@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>
@if($canvasItem['milestoneId'] == '')
<center>
<h4>{!! __('headlines.no_milestone_link') !!}</h4>
<div class="row" id="milestoneSelectors">
@if($login::userIsAtLeast($roles::$editor))
<div class="col-md-12">
<a href="javascript:void(0);" onclick="leantime.{{ $canvasName }}CanvasController.toggleMilestoneSelectors('new');">{!! __('links.create_link_milestone') !!}</a>
@if(count($milestones) > 0)
| <a href="javascript:void(0);" onclick="leantime.{{ $canvasName }}CanvasController.toggleMilestoneSelectors('existing');">{!! __('links.link_existing_milestone') !!}</a>
@endif
</div>
@endif
</div>
<div class="row" id="newMilestone" style="display:none;">
<div class="col-md-12">
<x-global::forms.text-input width="50%" name="newMilestone" /><br />
<input type="hidden" name="type" value="milestone" />
<input type="hidden" name="{{ $canvasName }}canvasitemid" value="{{ $id }} " />
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.save')" onclick="jQuery('#primaryCanvasSubmitButton').click()" contentRole="primary" />
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.cancel')" onclick="leantime.{{ $canvasName }}CanvasController.toggleMilestoneSelectors('hide')" contentRole="tertiary" />
</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=""></option>
@foreach($milestones as $milestoneRow)
<option value="{{ $milestoneRow->id }}"
@if(isset($searchCriteria['milestone']) && $searchCriteria['milestone'] == $milestoneRow->id) selected='selected' @endif
>{{ $milestoneRow->headline }}</option>
@endforeach
</select>
<input type="hidden" name="type" value="milestone" />
<input type="hidden" name="{{ $canvasName }}canvasitemid" value="{{ $id }} " />
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.save')" onclick="jQuery('#primaryCanvasSubmitButton').click()" contentRole="primary" />
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.cancel')" onclick="leantime.{{ $canvasName }}CanvasController.toggleMilestoneSelectors('hide')" contentRole="tertiary" />
</div>
</div>
</center>
@else
<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>
<x-global::forms.button tag="a" link="{{ CURRENT_URL }}?removeMilestone={{ $canvasItem['milestoneId'] }}" class="{{ $canvasName }}CanvasModal delete formModal" state="danger" variant="outline"><i class="fa fa-close"></i> {!! __('links.remove') !!}</x-global::forms.button>
@endif
@endif
</form>
@if($id !== '')
<br />
<input type="hidden" name="comment" value="1" />
<h4 class="widgettitle title-light"><span class="fa fa-comments"></span>{!! __('subtitles.discussion') !!}</h4>
@include('comments::submodules.generalComment', ['formUrl' => '/' . $canvasName . 'canvas/editCanvasItem/' . $id])
@endif
</div>
<script type="text/javascript">
jQuery(document).ready(function(){
@if(! empty($statusLabels))
new SlimSelect({
select: '#statusCanvas',
showSearch: false,
valuesUseText: false,
data: [
@foreach($statusLabels as $key => $data)
@if($data['active'])
{ innerHTML: '<i class="fas fa-fw {{ $data['icon'] }}"></i>&nbsp;{{ $data['title'] }}',
text: "{{ $data['title'] }}", value: "{{ $key }}", selected: {{ $canvasItem['status'] == $key ? 'true' : 'false' }}},
@endif
@endforeach
]
});
@endif
@if(! empty($relatesLabels))
new SlimSelect({
select: '#relatesCanvas',
showSearch: false,
valuesUseText: false,
data: [
@foreach($relatesLabels as $key => $data)
@if($data['active'])
{ innerHTML: '<i class="fas fa-fw {{ $data['icon'] }}"></i>&nbsp;{{ $data['title'] }}',
text: "{{ $data['title'] }}", value: "{{ $key }}", selected: {{ $canvasItem['relates'] == $key ? 'true' : 'false' }}},
@endif
@endforeach
]
});
@endif
if (window.leantime && window.leantime.tiptapController) {
leantime.tiptapController.initSimpleEditor();
}
@if(! $login::userIsAtLeast($roles::$editor))
leantime.authController.makeInputReadonly(".nyroModalCont");
@endif
@if($login::userHasRole([$roles::$commenter]))
leantime.commentsController.enableCommenterForms();
@endif
})
</script>

View File

@@ -0,0 +1,12 @@
@php
$id = filter_var($_GET['id'], FILTER_SANITIZE_NUMBER_INT);
@endphp
<h4 class="widgettitle title-light">{!! __('subtitles.delete') !!}</h4>
<form method="post" action="{{ BASE_URL }}/{{ $canvasName }}canvas/delCanvas/{{ $id }}">
<p>{!! __('text.confirm_board_deletion') !!}</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 }}/{{ $canvasName }}canvas/showCanvas">{!! __('buttons.back') !!}</x-global::forms.button>
</form>

View File

@@ -0,0 +1,12 @@
@php
$id = filter_var($_GET['id'], FILTER_SANITIZE_NUMBER_INT);
@endphp
<h4 class="widgettitle title-light">{!! __('subtitles.delete') !!}</h4>
<hr style="margin-top: 5px; margin-bottom: 15px;">
<form method="post" action="{{ BASE_URL }}/{{ $canvasName }}canvas/delCanvasItem/{{ $id }}">
<p>{!! __('text.confirm_board_item_deletion') !!}</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 }}/{{ $canvasName }}canvas/showCanvas">{!! __('buttons.back') !!}</x-global::forms.button>
</form>

View File

@@ -0,0 +1,150 @@
@php
use Leantime\Domain\Comments\Repositories\Comments;
@endphp
<h4 class="widgettitle title-primary">
@if(isset($canvasTypes[$elementName]['icon']))
<i class="fas {{ $canvasTypes[$elementName]['icon'] }}"></i>
@endif
{{ $canvasTypes[$elementName]['title'] }}
</h4>
<div class="contentInner even status_{{ $elementName }}"
{!! isset($canvasTypes[$elementName]['color']) ? 'style="background: ' . $canvasTypes[$elementName]['color'] . ';"' : '' !!}>
@foreach($canvasItems as $row)
@php
$filterStatus = $filter['status'] ?? 'all';
$filterRelates = $filter['relates'] ?? 'all';
@endphp
@if($row['box'] === $elementName && ($filterStatus == 'all' || $filterStatus == $row['status']) && ($filterRelates == 'all' || $filterRelates == $row['relates']))
@php
$comments = app()->make(Comments::class);
$nbcomments = $comments->countComments(moduleId: $row['id']);
@endphp
<div class="ticketBox" id="item_{{ $row['id'] }}">
<div class="row">
<div class="col-md-12">
<div class="inlineDropDownContainer" style="float:right;">
@if($login::userIsAtLeast($roles::$editor))
<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))
&nbsp;&nbsp;&nbsp;
<ul class="dropdown-menu">
<li class="nav-header">{!! __('subtitles.edit') !!}</li>
<li><a href="#/{{ $canvasName }}canvas/editCanvasItem/{{ $row['id'] }}"
data="item_{{ $row['id'] }}"> {!! __('links.edit_canvas_item') !!}</a></li>
<li><a href="#/{{ $canvasName }}canvas/delCanvasItem/{{ $row['id'] }}"
class="delete"
data="item_{{ $row['id'] }}"> {!! __('links.delete_canvas_item') !!}</a></li>
</ul>
@endif
</div>
<h4><a href="#/{{ $canvasName }}canvas/editCanvasItem/{{ $row['id'] }}"
data="item_{{ $row['id'] }}">{{ $row['description'] }}</a></h4>
@if($row['conclusion'] != '')
<small>{!! $tpl->escapeMinimal($row['conclusion']) !!}</small>
@endif
<div class="clearfix" style="padding-bottom: 8px;"></div>
@if(! empty($statusLabels))
<div class="dropdown ticketDropdown statusDropdown colorized show firstDropdown">
<a class="dropdown-toggle f-left status label-{{ $statusLabels[$row['status']]['dropdown'] }}"
href="javascript:void(0);" role="button"
id="statusDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="text">{{ $statusLabels[$row['status']]['title'] }}</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($statusLabels as $key => $data)
@if($data['active'] || true)
<li class='dropdown-item'>
<a href="javascript:void(0);" class="label-{{ $data['dropdown'] }}"
data-label='{{ $data['title'] }}' data-value="{{ $row['id'] . '/' . $key }}"
id="ticketStatusChange{{ $row['id'] }}{{ $key }}">{{ $data['title'] }}</a>
</li>
@endif
@endforeach
</ul>
</div>
@endif
@if(! empty($relatesLabels))
<div class="dropdown ticketDropdown relatesDropdown colorized show firstDropdown">
<a class="dropdown-toggle f-left relates label-{{ $relatesLabels[$row['relates']]['dropdown'] }}"
href="javascript:void(0);" role="button"
id="relatesDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<span class="text">{{ $relatesLabels[$row['relates']]['title'] }}</span> <i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="relatesDropdownMenuLink{{ $row['id'] }}">
<li class="nav-header border">{!! __('dropdown.choose_relates') !!}</li>
@foreach($relatesLabels as $key => $data)
@if($data['active'] || true)
<li class='dropdown-item'>
<a href="javascript:void(0);" class="label-{{ $data['dropdown'] }}"
data-label='{{ $data['title'] }}'
data-value="{{ $row['id'] . '/' . $key }}"
id="ticketRelatesChange{{ $row['id'] }}{{ $key }}">{{ $data['title'] }}</a>
</li>
@endif
@endforeach
</ul>
</div>
@endif
<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'), e($user['firstname']), e($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'] }}&v={{ $user['modified'] }}' width='25' style='vertical-align: middle; margin-right:5px;'/>{{ sprintf(__('text.full_name'), e($user['firstname']), e($user['lastname'])) }}</a>
</li>
@endforeach
</ul>
</div>
<div class="pull-right" style="margin-right:10px;">
<span class="fas fa-comments"></span> <small>{{ $nbcomments }}</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
<br />
@if($login::userIsAtLeast($roles::$editor))
<a href="#/{{ $canvasName }}canvas/editCanvasItem?type={{ $elementName }}"
class="" id="{{ $elementName }}"
style="padding-bottom: 10px;">{!! __('links.add_new_canvas_item') !!}</a>
@endif
</div>

View File

@@ -0,0 +1,22 @@
<div class="center padding-lg">
<div class="row">
<div class="col-md-12">
<div style='width:300px' class='svgContainer'>
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_design_data_khdb.svg') !!}
</div>
<br />
<h1>{!! __("headlines.$canvasName.welcome_to_board") !!}</h1><br />
{!! __("text.$canvasName.helper_content") !!}
<br /><br />
</div>
</div>
<div class="row">
<div class="col-md-12">
<p></p>
<x-global::forms.button tag="a" link="javascript:void(0);" onclick="jQuery.nmTop().close()" contentRole="tertiary">{!! __('links.close') !!}</x-global::forms.button><br />
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
{{-- modals.blade.php - Generic template for canvas modals --}}
{{-- Note: The modal content was previously commented out in the legacy template. --}}
{{-- Kept as an empty partial for compatibility. The actual board dialog is boardDialog.blade.php --}}

View File

@@ -0,0 +1,70 @@
@if(count($allCanvas) > 0)
@else
<br /><br />
<div class='center'>
<div class='svgContainer'>
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_design_data_khdb.svg') !!}
</div>
<h3>{!! __("headlines.$canvasName.analysis") !!}</h3>
<br />{!! __("text.$canvasName.helper_content") !!}
@if($login::userIsAtLeast($roles::$editor))
<br /><br />
<a href='javascript:void(0)' class='addCanvasLink btn btn-primary'>
{!! __('links.icon.create_new_board') !!}</a>.
@endif
</div>
@endif
@if(! empty($disclaimer) && count($allCanvas) > 0)
<small class="align-center">{{ $disclaimer }}</small>
@endif
@include('canvas::modals')
</div>
</div>
@once @push('scripts')
<script type="text/javascript">
jQuery(document).ready(function() {
if(jQuery('#searchCanvas').length > 0) {
new SlimSelect({ select: '#searchCanvas' });
}
@if(isset($_GET['closeModal']))
jQuery.nmTop().close();
@endif
leantime.{{ $canvasName }}CanvasController.setRowHeights();
leantime.canvasController.setCanvasName('{{ $canvasName }}');
leantime.canvasController.initFilterBar();
@if($login::userIsAtLeast($roles::$editor))
leantime.canvasController.initCanvasLinks();
leantime.canvasController.initUserDropdown();
leantime.canvasController.initStatusDropdown();
leantime.canvasController.initRelatesDropdown();
@else
leantime.authController.makeInputReadonly(".maincontentinner");
@endif
@if(isset($_GET['showModal']))
@php
if ($_GET['showModal'] == '') {
$modalUrl = '&type=' . array_key_first($canvasTypes);
} else {
$modalUrl = '/' . (int) $_GET['showModal'];
}
@endphp
leantime.canvasController.openModalManually("{{ BASE_URL }}/{{ $canvasName }}canvas/editCanvasItem{{ $modalUrl }}");
window.history.pushState({},document.title, '{{ BASE_URL }}/{{ $canvasName }}canvas/showCanvas/');
@endif
});
</script>
@endpush @endonce

View File

@@ -0,0 +1,131 @@
@php
$canvasTitle = '';
$allCanvas = $allCanvas ?? [];
$canvasIcon = $canvasIcon ?? '';
$canvasTypes = $canvasTypes ?? [];
$statusLabels = $statusLabels ?? [];
$relatesLabels = $relatesLabels ?? [];
$dataLabels = $dataLabels ?? [];
$disclaimer = $disclaimer ?? '';
$canvasItems = $canvasItems ?? [];
$filter['status'] = $_GET['filter_status'] ?? (session('filter_status') ?? 'all');
session(['filter_status' => $filter['status']]);
$filter['relates'] = $_GET['filter_relates'] ?? (session('filter_relates') ?? 'all');
session(['filter_relates' => $filter['relates']]);
// get canvas title
foreach ($allCanvas as $canvasRow) {
if ($canvasRow['id'] == ($currentCanvas ?? '')) {
$canvasTitle = $canvasRow['title'];
break;
}
}
@endphp
<style>
.canvas-row { margin-left: 0px; margin-right: 0px;}
.canvas-title-only { border-radius: var(--box-radius-small); }
h4.canvas-element-title-empty { background: white !important; border-color: white !important; }
div.canvas-element-center-middle { text-align: center; }
</style>
<div class="pageheader">
<div class="pageicon"><span class='fa {{ $canvasIcon }}'></span></div>
<div class="pagetitle">
@if(count($allCanvas) > 0)
<x-global::subjectSwitcher
:parent="__('headline.' . $canvasName . '.board')"
:current="$canvasTitle">
@if($login::userIsAtLeast($roles::$editor))
<li><a href="#/{{ $canvasName }}canvas/boardDialog">{!! __('links.icon.create_new_board') !!}</a></li>
@endif
<li class="border"></li>
@foreach($allCanvas as $canvasRow)
<li><a href="{{ BASE_URL }}/{{ $canvasName }}canvas/showCanvas/{{ $canvasRow['id'] }}">{{ e($canvasRow['title']) }}</a></li>
@endforeach
</x-global::subjectSwitcher>
@else
<h1>{!! __("headline.$canvasName.board") !!}</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="#/{{ $canvasName }}canvas/boardDialog/{{ $currentCanvas }}" class="editCanvasLink ">{!! __('links.icon.edit') !!}</a></li>
@endif
<li><a href="{{ BASE_URL }}/{{ $canvasName }}canvas/export/{{ $currentCanvas }}">{!! __('links.icon.export') !!}</a></li>
<li><a href="javascript:window.print();">{!! __('links.icon.print') !!}</a></li>
@if($login::userIsAtLeast($roles::$editor))
<li><a href="#/{{ $canvasName }}canvas/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-3">
@if($login::userIsAtLeast($roles::$editor) && count($canvasTypes) == 1 && count($allCanvas) > 0)
<x-global::forms.button tag="a" link="#/{{ $canvasName }}canvas/editCanvasItem?type={{ $elementName }}"
contentRole="primary" id="{{ $elementName }}">{!! __('links.add_new_canvas_item' . $canvasName) !!}</x-global::forms.button>
@endif
</div>
<div class="col-md-6 center">
</div>
<div class="col-md-3">
<div class="pull-right">
<div class="btn-group viewDropDown">
@if(count($allCanvas) > 0 && ! empty($statusLabels))
@if($filter['status'] == 'all')
<button class="btn dropdown-toggle" data-toggle="dropdown"><i class="fas fa-filter"></i> {!! __('status.all') !!} {!! __('links.view') !!}</button>
@else
<button class="btn dropdown-toggle" data-toggle="dropdown"><i class="fas fa-fw {!! __($statusLabels[$filter['status']]['icon']) !!}"></i> {{ $statusLabels[$filter['status']]['title'] }} {!! __('links.view') !!}</button>
@endif
<ul class="dropdown-menu">
<li><a href="{{ BASE_URL }}/{{ $canvasName }}canvas/showCanvas?filter_status=all" @if($filter['status'] == 'all') class="active" @endif><i class="fas fa-globe"></i> {!! __('status.all') !!}</a></li>
@foreach($statusLabels as $key => $data)
<li><a href="{{ BASE_URL }}/{{ $canvasName }}canvas/showCanvas?filter_status={{ $key }}" @if($filter['status'] == $key) class="active" @endif><i class="fas fa-fw {{ $data['icon'] }}"></i> {{ $data['title'] }}</a></li>
@endforeach
</ul>
@endif
</div>
<div class="btn-group viewDropDown">
@if(count($allCanvas) > 0 && ! empty($relatesLabels))
@if($filter['relates'] == 'all')
<button class="btn dropdown-toggle" data-toggle="dropdown"><i class="fas fa-fw fa-globe"></i> {!! __('relates.all') !!} {!! __('links.view') !!}</button>
@else
<button class="btn dropdown-toggle" data-toggle="dropdown"><i class="fas fa-fw {!! __($relatesLabels[$filter['relates']]['icon']) !!}"></i> {{ $relatesLabels[$filter['relates']]['title'] }} {!! __('links.view') !!}</button>
@endif
<ul class="dropdown-menu">
<li><a href="{{ BASE_URL }}/{{ $canvasName }}canvas/showCanvas?filter_relates=all" @if($filter['relates'] == 'all') class="active" @endif><i class="fas fa-globe"></i> {!! __('relates.all') !!}</a></li>
@foreach($relatesLabels as $key => $data)
<li><a href="{{ BASE_URL }}/{{ $canvasName }}canvas/showCanvas?filter_relates={{ $key }}" @if($filter['relates'] == $key) class="active" @endif><i class="fas fa-fw {{ $data['icon'] }}"></i> {{ $data['title'] }}</a></li>
@endforeach
</ul>
@endif
</div>
</div>
</div>
</div>
<div class="clearfix"></div>