OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
293
app/Domain/Wiki/Templates/articleDialog.blade.php
Normal file
293
app/Domain/Wiki/Templates/articleDialog.blade.php
Normal file
@@ -0,0 +1,293 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$currentArticle = $article ?? null;
|
||||
|
||||
$wikiHL = $wikiHeadlines ?? [];
|
||||
$wikiHeadlines = [];
|
||||
|
||||
function createTree($id, $parentId, &$wikiHeadlines, &$wikiHL, $indent)
|
||||
{
|
||||
$articles = array_filter($wikiHL, function ($v) use ($parentId) {
|
||||
return $v->parent == $parentId;
|
||||
});
|
||||
if (count($articles) > 0) {
|
||||
usort($articles, function ($a1, $a2) {
|
||||
return $a1->title > $a2->title;
|
||||
});
|
||||
if ($parentId != null) {
|
||||
$indent = $indent . '-';
|
||||
}
|
||||
foreach ($articles as $article) {
|
||||
if ($article->id != $id) {
|
||||
$art = $article;
|
||||
$art->title = $indent . $article->title;
|
||||
$wikiHeadlines[] = $art;
|
||||
createTree($id, $article->id, $wikiHeadlines, $wikiHL, $indent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($_GET['closeModal'])) {
|
||||
echo $tpl->displayNotification();
|
||||
}
|
||||
|
||||
$id = '';
|
||||
if (isset($currentArticle->id)) {
|
||||
$id = $currentArticle->id;
|
||||
}
|
||||
|
||||
// Populates the options tree
|
||||
createTree($id, null, $wikiHeadlines, $wikiHL, '');
|
||||
@endphp
|
||||
|
||||
<form class="formModal" method="post" action="{{ CURRENT_URL }}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<div class="row-fluid marginBottom">
|
||||
<h4 class="widgettitle title-light">
|
||||
<span class="fa fa-folder"></span>{!! __('subtitles.organization') !!}
|
||||
</h4>
|
||||
<label>Parent</label>
|
||||
<select name="parent" style="width:100%;">
|
||||
<option value="0">None</option>
|
||||
@foreach ($wikiHeadlines as $parent)
|
||||
@if ($id != $parent->id)
|
||||
<option value="{{ $parent->id }}"
|
||||
{{ ($parent->id == $currentArticle->parent) ? "selected='selected'" : '' }} >{{ $tpl->escape($parent->title) }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<label>{!! __('label.status') !!}</label>
|
||||
<select name="status" style="width:100%;">
|
||||
<option value="draft" {{ $currentArticle->status == 'draft' ? "selected='selected'" : '' }}>{!! __('label.draft') !!}</option>
|
||||
<option value="published" {{ $currentArticle->status == 'published' ? "selected='selected'" : '' }}>{!! __('label.published') !!}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if ($id !== '')
|
||||
<h4 class="widgettitle title-light"><span class="fa fa-link"></span> {!! __('headlines.linked_milestone') !!} <i class="fa fa-question-circle-o helperTooltip" data-tippy-content="{{ __('tooltip.link_milestones_tooltip') }}"></i></h4>
|
||||
|
||||
<ul class="sortableTicketList" style="width:99%">
|
||||
@if ($currentArticle->milestoneId == '')
|
||||
<li class="ui-state-default center" id="milestone_0">
|
||||
<h4>{!! __('headlines.no_milestone_link') !!}</h4>
|
||||
{!! __('text.use_milestone_to_track_leancanvas') !!}<br />
|
||||
<div class="row" id="milestoneSelectors">
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<div class="col-md-12">
|
||||
<a href="javascript:void(0);" onclick="leantime.leanCanvasController.toggleMilestoneSelectors('new');">{!! __('links.create_link_milestone') !!}</a>
|
||||
| <a href="javascript:void(0);" onclick="leantime.leanCanvasController.toggleMilestoneSelectors('existing');">{!! __('links.link_existing_milestone') !!}</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="row" id="newMilestone" style="display:none;">
|
||||
<div class="col-md-12">
|
||||
<x-global::forms.textarea name="newMilestone"></x-global::forms.textarea><br />
|
||||
<input type="hidden" name="type" value="milestone" />
|
||||
<input type="hidden" name="leancanvasitemid" value="{{ $id }} " />
|
||||
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.save')" onclick="jQuery('#primaryArticleSubmitButton').click()" contentRole="primary" />
|
||||
<x-global::forms.button tag="a" link="javascript:void(0);" onclick="leantime.leanCanvasController.toggleMilestoneSelectors('hide');" contentRole="tertiary">
|
||||
<i class="fas fa-times"></i> {!! __('links.cancel') !!}
|
||||
</x-global::forms.button>
|
||||
</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="">{!! __('label.all_milestones') !!}</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="articleId" value="{{ $id }} " />
|
||||
<x-global::forms.button tag="input" inputType="button" labelText="Save" onclick="jQuery('#primaryArticleSubmitButton').click()" contentRole="primary" />
|
||||
<x-global::forms.button tag="a" link="javascript:void(0);" onclick="leantime.leanCanvasController.toggleMilestoneSelectors('hide');" contentRole="tertiary">
|
||||
<i class="fas fa-times"></i> {!! __('links.cancel') !!}
|
||||
</x-global::forms.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
@else
|
||||
<li class="ui-state-default" id="milestone_{{ $currentArticle->milestoneId }}" class="leanCanvasMilestone" >
|
||||
|
||||
<div hx-trigger="load"
|
||||
hx-indicator=".htmx-indicator"
|
||||
hx-get="{{ BASE_URL }}/hx/tickets/milestones/showCard?milestoneId={{ $currentArticle->milestoneId }}">
|
||||
<div class="htmx-indicator">
|
||||
{!! __('label.loading_milestone') !!}
|
||||
</div>
|
||||
</div>
|
||||
<x-global::forms.button tag="a" link="{{ CURRENT_URL }}?removeMilestone={{ $currentArticle->milestoneId }}" class="formModal" state="danger" variant="outline"><i class="fa fa-close"></i> {!! __('links.remove') !!}</x-global::forms.button>
|
||||
|
||||
</li>
|
||||
@endif
|
||||
|
||||
</ul>
|
||||
|
||||
@endif
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="btn-group inlineDropDownContainerLeft">
|
||||
<button data-selected="graduation-cap" type="button"
|
||||
class="icp icp-dd btn btn-default dropdown-toggle iconpicker-container titleIconPicker"
|
||||
data-toggle="dropdown">
|
||||
<span class="iconPlaceholder">
|
||||
<i class="fa fa-file"></i>
|
||||
</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu"></div>
|
||||
</div>
|
||||
<input type="hidden" class="articleIcon" value="{{ $currentArticle->data }}" name="articleIcon"/>
|
||||
|
||||
<x-global::forms.text-input variant="headline" name="title" value="{{ $tpl->escape($currentArticle->title) }}" placeholder="{{ __('input.placeholders.wiki_title') }}" style="width:80%" />
|
||||
|
||||
<br />
|
||||
<input type="text" value="{{ $tpl->escape($currentArticle->tags) }}" name="tags" id="tags" />
|
||||
|
||||
<textarea class="tiptapComplex" rows="20" cols="80" id="wikiArticleContentEditor" name="description">{{ $currentArticle->description ?? '' }}</textarea>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10 padding-top-sm">
|
||||
<br />
|
||||
<input type="hidden" name="saveTicket" value="1" />
|
||||
<input type="hidden" id="saveAndCloseButton" name="saveAndCloseArticle" value="0" />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="saveArticle" id="primaryArticleSubmitButton" />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="secondary" name="saveAndCloseArticle" onclick="jQuery('#saveAndCloseButton').val('1');" :labelText="__('buttons.save_and_close')" />
|
||||
</div>
|
||||
<div class="col-md-2 align-right padding-top-sm">
|
||||
@if (isset($currentArticle->id) && $currentArticle->id != '' && $login::userIsAtLeast($roles::$editor))
|
||||
<br />
|
||||
<x-global::forms.button tag="a" link="#/wiki/delArticle/{{ $currentArticle->id }}" class="delete" state="danger" variant="outline"><i class="fa fa-trash"></i> {!! __('links.delete_article') !!}</x-global::forms.button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script>
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
// Initialize Tiptap complex editor
|
||||
if (window.leantime && window.leantime.tiptapController) {
|
||||
leantime.tiptapController.initComplexEditor();
|
||||
}
|
||||
|
||||
@if (isset($_GET['closeModal']))
|
||||
jQuery.nmTop().close();
|
||||
@endif
|
||||
|
||||
jQuery('.iconpicker-container').iconpicker({
|
||||
component:'.btn > .iconPlaceholder',
|
||||
input:'.articleIcon',
|
||||
inputSearch: true,
|
||||
defaultValue:"far fa-file-alt",
|
||||
selected: "{{ $currentArticle->data }}",
|
||||
showFooter: false,
|
||||
searchInFooter: false,
|
||||
icons: [
|
||||
{title: "far fa-file-alt", searchTerms:['icons']},
|
||||
{title: "fab fa-accessible-icon", searchTerms:['icons']},
|
||||
{title: "far fa-address-book", searchTerms:['icons']},
|
||||
{title: "fas fa-archive", searchTerms:['icons']},
|
||||
{title: "fas fa-asterisk", searchTerms:['icons']},
|
||||
{title: "fas fa-balance-scale", searchTerms:['icons']},
|
||||
{title: "fas fa-ban", searchTerms:['icons']},
|
||||
{title: "fas fa-bell", searchTerms:['icons']},
|
||||
{title: "fas fa-binoculars", searchTerms:['icons']},
|
||||
{title: "fas fa-birthday-cake", searchTerms:['icons']},
|
||||
{title: "fas fa-bolt", searchTerms:['icons']},
|
||||
{title: "fas fa-book", searchTerms:['icons']},
|
||||
{title: "fas fa-bookmark", searchTerms:['icons']},
|
||||
{title: "fas fa-briefcase", searchTerms:['icons']},
|
||||
{title: "fas fa-bug", searchTerms:['icons']},
|
||||
{title: "far fa-building", searchTerms:['icons']},
|
||||
{title: "fas fa-bullhorn", searchTerms:['icons']},
|
||||
{title: "far fa-calendar-alt", searchTerms:['icons']},
|
||||
{title: "fas fa-chart-bar", searchTerms:['icons']},
|
||||
{title: "fas fa-check-circle", searchTerms:['icons']},
|
||||
{title: "fas fa-chart-line", searchTerms:['icons']},
|
||||
{title: "fas fa-chess", searchTerms:['icons']},
|
||||
{title: "fas fa-cogs", searchTerms:['icons']},
|
||||
{title: "fas fa-comments", searchTerms:['icons']},
|
||||
{title: "fas fa-compass", searchTerms:['icons']},
|
||||
{title: "fas fa-database", searchTerms:['icons']},
|
||||
{title: "fas fa-envelope", searchTerms:['icons']},
|
||||
{title: "fas fa-exclamation-triangle", searchTerms:['icons']},
|
||||
{title: "fas fa-flask", searchTerms:['icons']},
|
||||
{title: "fas fa-globe", searchTerms:['icons']},
|
||||
{title: "fas fa-gem", searchTerms:['icons']},
|
||||
{title: "fas fa-graduation-cap", searchTerms:['icons']},
|
||||
{title: "fas fa-hand-spock", searchTerms:['icons']},
|
||||
{title: "fas fa-heart", searchTerms:['icons']},
|
||||
{title: "fas fa-home", searchTerms:['icons']},
|
||||
{title: "fas fa-image", searchTerms:['icons']},
|
||||
{title: "fas fa-info-circle", searchTerms:['icons']},
|
||||
{title: "fas fa-key", searchTerms:['icons']},
|
||||
{title: "fas fa-leaf", searchTerms:['icons']},
|
||||
{title: "fas fa-life-ring", searchTerms:['icons']},
|
||||
{title: "fas fa-lightbulb", searchTerms:['icons']},
|
||||
{title: "fas fa-link", searchTerms:['icons']},
|
||||
{title: "fas fa-location-arrow", searchTerms:['icons']},
|
||||
{title: "fas fa-lock", searchTerms:['icons']},
|
||||
{title: "fas fa-map", searchTerms:['icons']},
|
||||
{title: "fas fa-map-signs", searchTerms:['icons']},
|
||||
{title: "fas fa-money-bill-alt", searchTerms:['icons']},
|
||||
{title: "fas fa-paper-plane", searchTerms:['icons']},
|
||||
{title: "fas fa-paperclip", searchTerms:['icons']},
|
||||
{title: "fas fa-question-circle", searchTerms:['icons']},
|
||||
{title: "fas fa-quote-left", searchTerms:['icons']},
|
||||
{title: "fas fa-road", searchTerms:['icons']},
|
||||
{title: "fas fa-rocket", searchTerms:['icons']},
|
||||
{title: "fas fa-shopping-cart", searchTerms:['icons']},
|
||||
{title: "fas fa-sitemap", searchTerms:['icons']},
|
||||
{title: "fas fa-sliders-h", searchTerms:['icons']},
|
||||
{title: "fas fa-star", searchTerms:['icons']},
|
||||
{title: "fas fa-tachometer-alt", searchTerms:['icons']},
|
||||
{title: "fas fa-thermometer-half", searchTerms:['icons']},
|
||||
{title: "fas fa-thumbs-down", searchTerms:['icons']},
|
||||
{title: "fas fa-thumbs-up", searchTerms:['icons']},
|
||||
{title: "fas fa-trash-alt", searchTerms:['icons']},
|
||||
{title: "fas fa-trophy", searchTerms:['icons']},
|
||||
{title: "fas fa-user-circle", searchTerms:['icons']},
|
||||
{title: "fas fa-utensils", searchTerms:['icons']}
|
||||
]
|
||||
|
||||
});
|
||||
jQuery('.iconpicker-container').on('iconpickerSelected', function(event){
|
||||
jQuery(".articleIcon").val(event.iconpickerValue);
|
||||
});
|
||||
|
||||
leantime.ticketsController.initTagsInput();
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
17
app/Domain/Wiki/Templates/delArticle.blade.php
Normal file
17
app/Domain/Wiki/Templates/delArticle.blade.php
Normal file
@@ -0,0 +1,17 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$ticket = $ticket ?? null;
|
||||
@endphp
|
||||
|
||||
<h4 class="widgettitle title-light"><i class="fa fa-trash"></i> {!! __('buttons.delete') !!}</h4>
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/wiki/delArticle/{{ (int) $_GET['id'] }}">
|
||||
<p>{!! __('text.are_you_sure_delete_article') !!}</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 }}/wiki/show/">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
</form>
|
||||
|
||||
@endsection
|
||||
13
app/Domain/Wiki/Templates/delWiki.blade.php
Normal file
13
app/Domain/Wiki/Templates/delWiki.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<h4 class="widgettitle title-light"><i class="fa fa-trash"></i> {!! __('buttons.delete') !!}</h4>
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/wiki/delWiki/{{ $tpl->escape($_GET['id']) }}">
|
||||
<p>{!! __('text.are_you_sure_delete_wiki') !!}</p>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
|
||||
<x-global::forms.button tag="a" contentRole="tertiary" link="{{ BASE_URL }}/wiki/show">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
</form>
|
||||
|
||||
@endsection
|
||||
107
app/Domain/Wiki/Templates/partials/activityFeed.blade.php
Normal file
107
app/Domain/Wiki/Templates/partials/activityFeed.blade.php
Normal file
@@ -0,0 +1,107 @@
|
||||
@php
|
||||
/** @var array $activity */
|
||||
/** @var int $articleId */
|
||||
|
||||
$actionIcons = [
|
||||
'article.create' => 'fa fa-plus',
|
||||
'article.edit' => 'fa fa-edit',
|
||||
'article.title' => 'fa fa-heading',
|
||||
'article.status' => 'fa fa-circle-dot',
|
||||
'article.parent' => 'fa fa-folder-tree',
|
||||
'article.milestone' => 'fa fa-flag',
|
||||
'article.tags' => 'fa fa-tags',
|
||||
'article.icon' => 'fa fa-icons',
|
||||
];
|
||||
|
||||
$actionClasses = [
|
||||
'article.create' => '',
|
||||
'article.edit' => 'edit',
|
||||
'article.title' => 'edit',
|
||||
'article.status' => 'status',
|
||||
'article.parent' => '',
|
||||
'article.milestone' => '',
|
||||
'article.tags' => 'edit',
|
||||
'article.icon' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Build a natural-language label based on the action and its values.
|
||||
*/
|
||||
function getActivityLabel(string $action, array $values): string
|
||||
{
|
||||
switch ($action) {
|
||||
case 'article.create':
|
||||
return 'created the article';
|
||||
|
||||
case 'article.edit':
|
||||
return 'edited document text';
|
||||
|
||||
case 'article.title':
|
||||
$to = $values['to'] ?? '';
|
||||
return $to !== '' ? 'renamed the article to "' . e($to) . '"' : 'renamed the article';
|
||||
|
||||
case 'article.status':
|
||||
$to = $values['to'] ?? '';
|
||||
if ($to === 'published') {
|
||||
return 'published the article';
|
||||
} elseif ($to === 'draft') {
|
||||
return 'reverted to draft';
|
||||
}
|
||||
return 'changed the status';
|
||||
|
||||
case 'article.parent':
|
||||
$to = $values['to'] ?? '';
|
||||
if (empty($to) || $to === '0') {
|
||||
return 'removed the parent article';
|
||||
}
|
||||
return 'added a parent article';
|
||||
|
||||
case 'article.milestone':
|
||||
$to = $values['to'] ?? '';
|
||||
if (empty($to) || $to === '0') {
|
||||
return 'removed the milestone';
|
||||
}
|
||||
return 'added a milestone';
|
||||
|
||||
case 'article.tags':
|
||||
return 'updated tags';
|
||||
|
||||
case 'article.icon':
|
||||
return 'changed the icon';
|
||||
|
||||
default:
|
||||
return 'updated the article';
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="wiki-activity-feed" id="wikiActivityFeed">
|
||||
@forelse ($activity as $item)
|
||||
@php
|
||||
$action = $item['action'] ?? '';
|
||||
$icon = $actionIcons[$action] ?? 'fa fa-circle';
|
||||
$cssClass = $actionClasses[$action] ?? '';
|
||||
$name = trim(($item['firstname'] ?? '') . ' ' . ($item['lastname'] ?? ''));
|
||||
$date = $item['date'] ?? '';
|
||||
$values = $item['values'] ?? [];
|
||||
$label = getActivityLabel($action, $values);
|
||||
@endphp
|
||||
<div class="wiki-activity-item">
|
||||
<div class="wiki-activity-icon {{ $cssClass }}">
|
||||
<i class="{{ $icon }}"></i>
|
||||
</div>
|
||||
<div class="wiki-activity-content">
|
||||
<div class="wiki-activity-text">
|
||||
<strong>{{ $name ?: 'Someone' }}</strong> {{ $label }}
|
||||
</div>
|
||||
@if (!empty($date))
|
||||
<div class="wiki-activity-time">{{ format($date)->date() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="wiki-activity-empty">
|
||||
<span>No activity yet</span>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
883
app/Domain/Wiki/Templates/show.blade.php
Normal file
883
app/Domain/Wiki/Templates/show.blade.php
Normal file
@@ -0,0 +1,883 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$wikis = $wikis ?? [];
|
||||
$wikiHeadlines = $wikiHeadlines ?? [];
|
||||
$milestones = $milestones ?? [];
|
||||
$currentWiki = $currentWiki ?? false;
|
||||
$currentArticle = $currentArticle ?? null;
|
||||
|
||||
/**
|
||||
* Creates a modern tree view for wiki navigation
|
||||
*/
|
||||
function createModernTreeView($array, $currentParent, $currentArticleId, int $currLevel = 0, ?\Leantime\Core\UI\Template $tplObject = null): void
|
||||
{
|
||||
$hasChildren = false;
|
||||
|
||||
foreach ($array as $headline) {
|
||||
if ((int) $currentParent === (int) $headline->parent) {
|
||||
if (! $hasChildren) {
|
||||
echo '<ul class="wiki-tree">';
|
||||
$hasChildren = true;
|
||||
}
|
||||
|
||||
$isActive = ($currentArticleId == $headline->id) ? ' active' : '';
|
||||
$isDraft = ($headline->status == 'draft');
|
||||
|
||||
echo '<li class="wiki-tree-item">';
|
||||
echo '<a href="' . BASE_URL . '/wiki/show/' . $headline->id . '" class="wiki-tree-link' . $isActive . '">';
|
||||
echo '<i class="' . $tplObject->escape($headline->data) . '"></i>';
|
||||
echo '<span>' . $tplObject->escape($headline->title) . '</span>';
|
||||
if ($isDraft) {
|
||||
echo ' <span class="wiki-tree-draft">(' . $tplObject->__('label.draft') . ')</span>';
|
||||
}
|
||||
echo '</a>';
|
||||
|
||||
createModernTreeView($array, $headline->id, $currentArticleId, $currLevel + 1, $tplObject);
|
||||
|
||||
echo '</li>';
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasChildren) {
|
||||
echo '</ul>';
|
||||
}
|
||||
}
|
||||
|
||||
// Get author initials for avatar
|
||||
$authorInitials = '';
|
||||
if ($currentArticle && ! empty($currentArticle->firstname)) {
|
||||
$authorInitials .= strtoupper(substr($currentArticle->firstname, 0, 1));
|
||||
if (! empty($currentArticle->lastname)) {
|
||||
$authorInitials .= strtoupper(substr($currentArticle->lastname, 0, 1));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa fa-book"></span></div>
|
||||
<div class="pagetitle">
|
||||
@if(count($wikis) > 0)
|
||||
<x-global::subjectSwitcher
|
||||
:parent="__('headlines.documents')"
|
||||
:current="$currentWiki !== false ? $currentWiki->title : __('label.select_board')">
|
||||
<li><a class="inlineEdit" href="#/wiki/wikiModal/">{!! __('link.new_wiki') !!}</a></li>
|
||||
<li class='nav-header border'></li>
|
||||
@foreach($wikis as $wiki)
|
||||
<li>
|
||||
<a href="{{ BASE_URL . '/wiki/show?setWiki=' . $wiki->id }}">{{ $wiki->title }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</x-global::subjectSwitcher>
|
||||
@else
|
||||
<h1>{!! __('headlines.documents') !!}</h1>
|
||||
@endif
|
||||
</div>
|
||||
{{-- Header rule (2026-08-03): actions ⋮ sits in the right cluster,
|
||||
vertically centered — never floated inside the title block. --}}
|
||||
@if(count($wikis) > 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) && $currentWiki)
|
||||
<li><a class="inlineEdit" href="#/wiki/wikiModal/{{ $currentWiki->id }}">{!! __('link.edit_wiki') !!}</a></li>
|
||||
<li><a class="delete" href="#/wiki/delWiki/{{ $currentWiki->id }}"><i class="fa fa-trash"></i> {!! __('links.delete_wiki') !!}</a></li>
|
||||
@endif
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="maincontent">
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
@if((! $currentArticle || $currentArticle->id != null) && (! $wikis || count($wikis) == 0))
|
||||
<!-- No wikis exist - show empty state -->
|
||||
<div class="wiki-empty-state">
|
||||
<div class="wiki-empty-state-icon svgContainer">
|
||||
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_book_reading_re_fu2c.svg') !!}
|
||||
</div>
|
||||
<h3 class="wiki-empty-state-title">{!! __('headlines.no_articles_yet') !!}</h3>
|
||||
<p class="wiki-empty-state-text">{!! __('text.create_new_wiki') !!}</p>
|
||||
<x-global::forms.button tag="a" link="#/wiki/wikiModal/" class="inlineEdit" contentRole="primary">{!! __('links.icon.create_new_board') !!}</x-global::forms.button>
|
||||
</div>
|
||||
|
||||
@elseif($wikis && count($wikis) > 0)
|
||||
|
||||
@if($currentArticle && $currentArticle->id != null)
|
||||
<!-- Single Panel Layout: Contents | Document | Properties (all inside) -->
|
||||
<div class="wiki-layout">
|
||||
|
||||
<!-- Main Content Area (contains everything) -->
|
||||
<main class="wiki-content">
|
||||
|
||||
<!-- Three-panel layout inside -->
|
||||
<div class="wiki-content-layout">
|
||||
|
||||
<!-- Left: Contents Sidebar -->
|
||||
<div class="wiki-contents-panel" id="contentsPanel">
|
||||
<div class="wiki-panel-header">
|
||||
<h4 class="widgettitle title-light"><i class="fa fa-list"></i> Contents</h4>
|
||||
<button class="wiki-collapse-btn" id="toggleContents" title="Collapse">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav id="article-toc-wrapper">
|
||||
@php createModernTreeView($wikiHeadlines, 0, $currentArticle->id, 0, $tpl); @endphp
|
||||
</nav>
|
||||
|
||||
@if($login::userIsAtLeast($roles::$editor))
|
||||
<button class="wiki-create-btn"
|
||||
hx-post="{{ BASE_URL }}/hx/wiki/articleContent/create"
|
||||
hx-swap="none">
|
||||
<i class="fa fa-plus"></i>
|
||||
<span>{!! __('link.create_article') !!}</span>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Toggle for collapsed Contents -->
|
||||
<button class="wiki-panel-toggle left" id="showContentsBtn" title="Show Contents">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
|
||||
<div class="wiki-content-inner">
|
||||
|
||||
<!-- Toggle for collapsed Details -->
|
||||
<button class="wiki-panel-toggle right" id="showPropertiesBtn" title="Show Details">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
<!-- Document Header -->
|
||||
<header class="wiki-document-header">
|
||||
@if($login::userIsAtLeast($roles::$editor))
|
||||
<!-- Editable Title with Icon Picker -->
|
||||
<div class="form-group" id="wikiTitleWrapper">
|
||||
<div class="btn-group inlineDropDownContainerLeft">
|
||||
<button data-selected="graduation-cap" type="button"
|
||||
class="icp icp-dd btn btn-default dropdown-toggle iconpicker-container titleIconPicker"
|
||||
data-toggle="dropdown">
|
||||
<span class="iconPlaceholder"><i class="{{ e($currentArticle->data ?: 'fa fa-file-alt') }}"></i></span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu"></div>
|
||||
</div>
|
||||
<input type="hidden" id="wikiArticleIcon" class="articleIcon" value="{{ e($currentArticle->data) }}" />
|
||||
<x-global::forms.text-input
|
||||
id="wikiTitleEditable"
|
||||
variant="headline"
|
||||
value="{{ e($currentArticle->title) }}"
|
||||
data-original="{{ e($currentArticle->title) }}"
|
||||
placeholder="{{ __('input.placeholders.wiki_title') }}"
|
||||
style="width:80%" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<!-- Editable Tags -->
|
||||
<div class="wiki-tags-wrapper">
|
||||
<input type="text"
|
||||
id="wikiTagsInput"
|
||||
class="wiki-tags-input"
|
||||
data-role="tagsinput"
|
||||
value="{{ e($currentArticle->tags ?? '') }}"
|
||||
placeholder="Add tags..." />
|
||||
</div>
|
||||
@else
|
||||
<h1 class="wiki-document-title">
|
||||
<i class="article-icon {{ e($currentArticle->data) }}"></i>
|
||||
{{ $currentArticle->title }}
|
||||
</h1>
|
||||
|
||||
@php $tagsArray = array_filter(explode(',', $currentArticle->tags ?? '')); @endphp
|
||||
@if(count($tagsArray) > 0)
|
||||
<div class="wiki-document-tags">
|
||||
@foreach($tagsArray as $tag)
|
||||
@php $tag = trim($tag); @endphp
|
||||
@if(! empty($tag))
|
||||
<span class="wiki-document-tag">{{ $tag }}</span>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</header>
|
||||
|
||||
<!-- Document Body - Click to Edit -->
|
||||
<div class="wiki-document-wrapper" id="wikiDocumentWrapper" data-article-id="{{ $currentArticle->id }}">
|
||||
@if($login::userIsAtLeast($roles::$editor))
|
||||
<!-- Hidden textarea for Tiptap -->
|
||||
<textarea id="wikiArticleContent" class="wiki-editor-textarea" style="display:none;">{!! $tpl->escapeMinimal($currentArticle->description) !!}</textarea>
|
||||
<!-- Tiptap editor will be initialized here -->
|
||||
<div id="wikiTiptapEditor" class="wiki-document"></div>
|
||||
<!-- Edit mode indicator -->
|
||||
<div class="wiki-edit-indicator" id="wikiEditIndicator" style="display: none;">
|
||||
<i class="fa fa-circle"></i>
|
||||
<span>Editing</span>
|
||||
</div>
|
||||
@else
|
||||
<!-- Read-only view for non-editors -->
|
||||
<article class="wiki-document" id="wikiDocumentContent">
|
||||
{!! $tpl->escapeMinimal($currentArticle->description) !!}
|
||||
</article>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if(! empty($currentArticle->milestoneHeadline))
|
||||
<div class="wiki-milestone-card">
|
||||
<div hx-trigger="load"
|
||||
hx-indicator=".htmx-indicator"
|
||||
hx-get="{{ BASE_URL }}/hx/tickets/milestones/showCard?milestoneId={{ $currentArticle->milestoneId }}">
|
||||
<div class="htmx-indicator">
|
||||
{!! __('label.loading_milestone') !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Comments Section -->
|
||||
<section class="wiki-comments-section" id="comments">
|
||||
<h4 class="widgettitle title-light"><span class="fa-solid fa-comments"></span> {!! __('subtitles.discussion') !!}</h4>
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/wiki/show/{{ $currentArticle->id }}#comment">
|
||||
<input type="hidden" name="comment" value="1" />
|
||||
@include('comments::submodules.generalComment', ['formUrl' => BASE_URL . '/wiki/show/' . $currentArticle->id])
|
||||
</form>
|
||||
</section>
|
||||
|
||||
</div><!-- /.wiki-content-inner -->
|
||||
|
||||
<!-- Properties Panel (inside content area) -->
|
||||
<div class="wiki-properties-panel" id="propertiesPanel">
|
||||
<div class="wiki-panel-header">
|
||||
<h4 class="widgettitle title-light"><i class="fa fa-info-circle"></i> Details</h4>
|
||||
<button class="wiki-collapse-btn" id="collapseProperties" title="Collapse">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Properties Section -->
|
||||
<div class="wiki-properties-section">
|
||||
|
||||
<!-- Status Dropdown -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">{!! __('label.status') !!}</label>
|
||||
<div class="">
|
||||
@if($login::userIsAtLeast($roles::$editor))
|
||||
<select id="wikiStatusSelect" class="span11">
|
||||
<option value="draft" @selected($currentArticle->status === 'draft')>Draft</option>
|
||||
<option value="published" @selected($currentArticle->status !== 'draft')>Published</option>
|
||||
</select>
|
||||
@else
|
||||
{{ ucfirst($currentArticle->status) }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
// Find parent article name
|
||||
$parentName = 'None';
|
||||
if ($currentArticle->parent && $currentArticle->parent > 0) {
|
||||
foreach ($wikiHeadlines as $headline) {
|
||||
if ($headline->id == $currentArticle->parent) {
|
||||
$parentName = e($headline->title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<!-- Parent -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">Parent</label>
|
||||
<div class="">
|
||||
@if($login::userIsAtLeast($roles::$editor))
|
||||
@php
|
||||
$parentOptions = array_filter($wikiHeadlines, function ($h) use ($currentArticle) {
|
||||
return $h->id != $currentArticle->id;
|
||||
});
|
||||
@endphp
|
||||
<select id="wikiParentSelect" class="span11">
|
||||
<option value="0" @selected(! $currentArticle->parent || $currentArticle->parent == 0)>None</option>
|
||||
@foreach($parentOptions as $headline)
|
||||
<option value="{{ $headline->id }}" @selected($currentArticle->parent == $headline->id)>{{ $headline->title }}@if($headline->status === 'draft') ({!! __('label.draft') !!})@endif</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
@if($currentArticle->parent && $currentArticle->parent > 0)
|
||||
<a href="{{ BASE_URL }}/wiki/show/{{ $currentArticle->parent }}">
|
||||
{{ $parentName }}
|
||||
</a>
|
||||
@else
|
||||
<span>{{ $parentName }}</span>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Milestone -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">{!! __('label.milestone') !!}</label>
|
||||
<div class="">
|
||||
@if($login::userIsAtLeast($roles::$editor))
|
||||
<select id="wikiMilestoneSelect" class="span11">
|
||||
<option value="">{!! __('label.not_assigned_to_milestone') !!}</option>
|
||||
@foreach($milestones as $milestone)
|
||||
<option value="{{ $milestone->id }}" @selected($currentArticle->milestoneId == $milestone->id)>{{ $milestone->headline }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
@if(! empty($currentArticle->milestoneHeadline))
|
||||
<a href="{{ BASE_URL }}/tickets/roadmap#/tickets/editMilestone/{{ $currentArticle->milestoneId }}">
|
||||
{{ $currentArticle->milestoneHeadline }}
|
||||
</a>
|
||||
@else
|
||||
<span>{!! __('label.not_assigned_to_milestone') !!}</span>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Author -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">{!! __('label.author') !!}</label>
|
||||
<div class="">
|
||||
<div class="wiki-author">
|
||||
<span class="wiki-author-avatar">{{ $authorInitials }}</span>
|
||||
{{ $currentArticle->firstname }} {{ $currentArticle->lastname }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Saved -->
|
||||
<div class="form-group">
|
||||
<label class="control-label">{!! __('label.last_updated') !!}</label>
|
||||
<div class="" id="wikiLastSaved" data-timestamp="{{ $currentArticle->modified }}">
|
||||
{{ format($currentArticle->modified)->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Section -->
|
||||
<div class="wiki-properties-section wiki-activity-section">
|
||||
<h4 class="widgettitle title-light"><i class="fa fa-clock-rotate-left"></i> Activity</h4>
|
||||
|
||||
<div id="wikiActivityContainer"
|
||||
hx-get="{{ BASE_URL }}/hx/wiki/articleActivity?articleId={{ $currentArticle->id }}"
|
||||
hx-trigger="load, refreshActivity from:body"
|
||||
hx-swap="innerHTML">
|
||||
<div class="wiki-activity-loading">
|
||||
<i class="fa fa-circle-notch fa-spin"></i> Loading activity...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete (pinned to bottom) -->
|
||||
@if($login::userIsAtLeast($roles::$editor))
|
||||
<div class="wiki-properties-footer">
|
||||
<x-global::forms.button tag="a" link="#/wiki/delArticle/{{ $currentArticle->id }}" class="wiki-action-btn delete" state="danger" variant="outline">
|
||||
<i class="fa fa-trash"></i> Delete Article
|
||||
</x-global::forms.button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div><!-- /.wiki-properties-panel -->
|
||||
</div><!-- /.wiki-content-layout -->
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
@else
|
||||
<!-- Wiki exists but no articles yet -->
|
||||
<div class="wiki-empty-state">
|
||||
<div class="wiki-empty-state-icon svgContainer" style="width: 200px; margin: 0 auto;">
|
||||
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_book_reading_re_fu2c.svg') !!}
|
||||
</div>
|
||||
<h3 class="wiki-empty-state-title">{!! __('headlines.no_articles_yet') !!}</h3>
|
||||
<p class="wiki-empty-state-text">{!! __('text.create_new_content') !!}</p>
|
||||
<x-global::forms.button contentRole="primary"
|
||||
hx-post="{{ BASE_URL }}/hx/wiki/articleContent/create"
|
||||
hx-swap="none">
|
||||
<i class="fa fa-plus"></i> {!! __('link.create_article') !!}
|
||||
</x-global::forms.button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function() {
|
||||
|
||||
jQuery('#toggleContents').on('click', function() {
|
||||
var panel = jQuery('#contentsPanel');
|
||||
var showBtn = jQuery('#showContentsBtn');
|
||||
panel.addClass('collapsed');
|
||||
showBtn.addClass('visible');
|
||||
localStorage.setItem('wikiContentsCollapsed', 'true');
|
||||
});
|
||||
|
||||
jQuery('#showContentsBtn').on('click', function() {
|
||||
var panel = jQuery('#contentsPanel');
|
||||
var showBtn = jQuery('#showContentsBtn');
|
||||
panel.removeClass('collapsed');
|
||||
showBtn.removeClass('visible');
|
||||
localStorage.setItem('wikiContentsCollapsed', 'false');
|
||||
});
|
||||
|
||||
jQuery('#collapseProperties').on('click', function() {
|
||||
var panel = jQuery('#propertiesPanel');
|
||||
var showBtn = jQuery('#showPropertiesBtn');
|
||||
panel.addClass('collapsed');
|
||||
showBtn.addClass('visible');
|
||||
localStorage.setItem('wikiPropertiesCollapsed', 'true');
|
||||
});
|
||||
|
||||
jQuery('#showPropertiesBtn').on('click', function() {
|
||||
var panel = jQuery('#propertiesPanel');
|
||||
var showBtn = jQuery('#showPropertiesBtn');
|
||||
panel.removeClass('collapsed');
|
||||
showBtn.removeClass('visible');
|
||||
localStorage.setItem('wikiPropertiesCollapsed', 'false');
|
||||
});
|
||||
|
||||
var isSmallScreen = window.innerWidth <= 1280;
|
||||
|
||||
if (isSmallScreen || localStorage.getItem('wikiContentsCollapsed') === 'true') {
|
||||
jQuery('#contentsPanel').addClass('collapsed');
|
||||
jQuery('#showContentsBtn').addClass('visible');
|
||||
}
|
||||
|
||||
if (isSmallScreen || localStorage.getItem('wikiPropertiesCollapsed') === 'true') {
|
||||
jQuery('#propertiesPanel').addClass('collapsed');
|
||||
jQuery('#showPropertiesBtn').addClass('visible');
|
||||
}
|
||||
|
||||
var resizeTimeout;
|
||||
jQuery(window).on('resize', function() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(function() {
|
||||
var nowSmall = window.innerWidth <= 1280;
|
||||
if (nowSmall) {
|
||||
jQuery('#contentsPanel').addClass('collapsed');
|
||||
jQuery('#showContentsBtn').addClass('visible');
|
||||
jQuery('#propertiesPanel').addClass('collapsed');
|
||||
jQuery('#showPropertiesBtn').addClass('visible');
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
|
||||
@if($currentArticle && $login::userIsAtLeast($roles::$editor))
|
||||
(function() {
|
||||
var articleId = @json($currentArticle->id);
|
||||
var wrapper = document.getElementById('wikiDocumentWrapper');
|
||||
var editorEl = document.getElementById('wikiTiptapEditor');
|
||||
var textarea = document.getElementById('wikiArticleContent');
|
||||
var indicator = document.getElementById('wikiEditIndicator');
|
||||
|
||||
if (!editorEl || !textarea || !window.leantime || !window.leantime.tiptapController) {
|
||||
console.warn('[Wiki] Tiptap controller not available');
|
||||
return;
|
||||
}
|
||||
|
||||
var isEditing = false;
|
||||
var saveTimeout = null;
|
||||
var lastSavedContent = textarea.value;
|
||||
var toolbarClicking = false;
|
||||
|
||||
wrapper.addEventListener('mousedown', function(e) {
|
||||
if (isEditing && !editorEl.contains(e.target)) {
|
||||
toolbarClicking = true;
|
||||
}
|
||||
});
|
||||
|
||||
var tiptapInstance = leantime.tiptapController.initComplex(textarea, {
|
||||
placeholder: 'Click anywhere to start editing...',
|
||||
toolbar: false,
|
||||
autosave: false,
|
||||
onCreate: function(params) {
|
||||
params.editor.setEditable(false);
|
||||
},
|
||||
onUpdate: function(params) {
|
||||
if (isEditing) {
|
||||
clearTimeout(saveTimeout);
|
||||
showIndicator('saving');
|
||||
saveTimeout = setTimeout(function() {
|
||||
saveContent(params.editor.getHTML());
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
onBlur: function(params) {
|
||||
if (toolbarClicking) {
|
||||
toolbarClicking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
if (!isEditing) return;
|
||||
|
||||
var active = document.activeElement;
|
||||
|
||||
if (wrapper.contains(active)) return;
|
||||
|
||||
var openPopover = document.querySelector(
|
||||
'.tiptap-color-popover, .tiptap-font-popover, .tiptap-heading-popover, .tiptap-image-popover'
|
||||
);
|
||||
if (openPopover) return;
|
||||
|
||||
exitEditMode();
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
if (!tiptapInstance) {
|
||||
console.error('[Wiki] Failed to initialize Tiptap');
|
||||
return;
|
||||
}
|
||||
|
||||
var editor = tiptapInstance.editor;
|
||||
|
||||
function showToolbar() {
|
||||
if (window.leantime.tiptapToolbar) {
|
||||
var toolbar = window.leantime.tiptapToolbar.create(editor, 'complex');
|
||||
var tiptapEditorEl = wrapper.querySelector('.tiptap-editor');
|
||||
window.leantime.tiptapToolbar.attach({ element: tiptapEditorEl || editorEl }, toolbar);
|
||||
}
|
||||
}
|
||||
|
||||
function hideToolbar() {
|
||||
var toolbarEl = wrapper.querySelector('.tiptap-toolbar');
|
||||
if (toolbarEl) {
|
||||
toolbarEl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function enterEditMode() {
|
||||
if (isEditing) return;
|
||||
isEditing = true;
|
||||
editor.setEditable(true);
|
||||
wrapper.classList.add('editing');
|
||||
showToolbar();
|
||||
showIndicator('editing');
|
||||
editor.commands.focus('end');
|
||||
}
|
||||
|
||||
function exitEditMode() {
|
||||
if (!isEditing) return;
|
||||
|
||||
var currentContent = editor.getHTML();
|
||||
if (currentContent !== lastSavedContent) {
|
||||
saveContent(currentContent);
|
||||
}
|
||||
|
||||
isEditing = false;
|
||||
editor.setEditable(false);
|
||||
wrapper.classList.remove('editing');
|
||||
hideToolbar();
|
||||
hideIndicator();
|
||||
}
|
||||
|
||||
function saveContent(content) {
|
||||
showIndicator('saving');
|
||||
|
||||
fetch(leantime.appUrl + '/hx/wiki/articleContent/save?articleId=' + articleId, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: 'description=' + encodeURIComponent(content)
|
||||
})
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
lastSavedContent = content;
|
||||
showIndicator('saved');
|
||||
updateLastSaved();
|
||||
setTimeout(function() {
|
||||
if (!isEditing) hideIndicator();
|
||||
}, 2000);
|
||||
} else {
|
||||
showIndicator('error');
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('[Wiki] Save failed:', err);
|
||||
showIndicator('error');
|
||||
});
|
||||
}
|
||||
|
||||
function showIndicator(state) {
|
||||
if (!indicator) return;
|
||||
indicator.style.display = 'flex';
|
||||
indicator.className = 'wiki-edit-indicator ' + state;
|
||||
var icon = indicator.querySelector('i');
|
||||
var text = indicator.querySelector('span');
|
||||
switch (state) {
|
||||
case 'editing': icon.className = 'fa fa-edit'; text.textContent = 'Editing'; break;
|
||||
case 'saving': icon.className = 'fa fa-circle-notch fa-spin'; text.textContent = 'Saving...'; break;
|
||||
case 'saved': icon.className = 'fa fa-check'; text.textContent = 'Saved'; break;
|
||||
case 'error': icon.className = 'fa fa-exclamation-triangle'; text.textContent = 'Save failed'; break;
|
||||
}
|
||||
}
|
||||
|
||||
function hideIndicator() {
|
||||
if (indicator) indicator.style.display = 'none';
|
||||
}
|
||||
|
||||
wrapper.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A' || e.target.closest('a')) return;
|
||||
enterEditMode();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'e') {
|
||||
e.preventDefault();
|
||||
if (isEditing) exitEditMode(); else enterEditMode();
|
||||
}
|
||||
if (e.key === 'Escape' && isEditing) {
|
||||
e.preventDefault();
|
||||
exitEditMode();
|
||||
}
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 's' && isEditing) {
|
||||
e.preventDefault();
|
||||
saveContent(editor.getHTML());
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (isEditing) {
|
||||
var currentContent = editor.getHTML();
|
||||
if (currentContent !== lastSavedContent) {
|
||||
navigator.sendBeacon(
|
||||
leantime.appUrl + '/hx/wiki/articleContent/save?articleId=' + articleId,
|
||||
new URLSearchParams({ description: currentContent })
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
var titleEditable = document.getElementById('wikiTitleEditable');
|
||||
if (titleEditable) {
|
||||
var originalTitle = titleEditable.dataset.original;
|
||||
|
||||
titleEditable.addEventListener('blur', function() {
|
||||
var newTitle = titleEditable.value.trim();
|
||||
if (newTitle && newTitle !== originalTitle) {
|
||||
saveField('title', newTitle, function() {
|
||||
originalTitle = newTitle;
|
||||
titleEditable.dataset.original = newTitle;
|
||||
var treeLink = document.querySelector('.wiki-tree-link.active span');
|
||||
if (treeLink) treeLink.textContent = newTitle;
|
||||
updateLastSaved();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
titleEditable.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); titleEditable.blur(); }
|
||||
if (e.key === 'Escape') { titleEditable.value = originalTitle; titleEditable.blur(); }
|
||||
});
|
||||
}
|
||||
|
||||
var iconInput = document.getElementById('wikiArticleIcon');
|
||||
if (iconInput && jQuery.fn.iconpicker) {
|
||||
jQuery('.titleIconPicker').iconpicker({
|
||||
component: '.btn > .iconPlaceholder',
|
||||
input: '.articleIcon',
|
||||
inputSearch: true,
|
||||
defaultValue: 'far fa-file-alt',
|
||||
selected: iconInput.value || 'far fa-file-alt',
|
||||
showFooter: false,
|
||||
searchInFooter: false,
|
||||
icons: [
|
||||
{title: "far fa-file-alt", searchTerms:['icons']},
|
||||
{title: "fab fa-accessible-icon", searchTerms:['icons']},
|
||||
{title: "far fa-address-book", searchTerms:['icons']},
|
||||
{title: "fas fa-archive", searchTerms:['icons']},
|
||||
{title: "fas fa-asterisk", searchTerms:['icons']},
|
||||
{title: "fas fa-balance-scale", searchTerms:['icons']},
|
||||
{title: "fas fa-ban", searchTerms:['icons']},
|
||||
{title: "fas fa-bell", searchTerms:['icons']},
|
||||
{title: "fas fa-binoculars", searchTerms:['icons']},
|
||||
{title: "fas fa-birthday-cake", searchTerms:['icons']},
|
||||
{title: "fas fa-bolt", searchTerms:['icons']},
|
||||
{title: "fas fa-book", searchTerms:['icons']},
|
||||
{title: "fas fa-bookmark", searchTerms:['icons']},
|
||||
{title: "fas fa-briefcase", searchTerms:['icons']},
|
||||
{title: "fas fa-bug", searchTerms:['icons']},
|
||||
{title: "far fa-building", searchTerms:['icons']},
|
||||
{title: "fas fa-bullhorn", searchTerms:['icons']},
|
||||
{title: "far fa-calendar-alt", searchTerms:['icons']},
|
||||
{title: "fas fa-chart-bar", searchTerms:['icons']},
|
||||
{title: "fas fa-check-circle", searchTerms:['icons']},
|
||||
{title: "fas fa-chart-line", searchTerms:['icons']},
|
||||
{title: "fas fa-chess", searchTerms:['icons']},
|
||||
{title: "fas fa-cogs", searchTerms:['icons']},
|
||||
{title: "fas fa-comments", searchTerms:['icons']},
|
||||
{title: "fas fa-compass", searchTerms:['icons']},
|
||||
{title: "fas fa-database", searchTerms:['icons']},
|
||||
{title: "fas fa-envelope", searchTerms:['icons']},
|
||||
{title: "fas fa-exclamation-triangle", searchTerms:['icons']},
|
||||
{title: "fas fa-flask", searchTerms:['icons']},
|
||||
{title: "fas fa-globe", searchTerms:['icons']},
|
||||
{title: "fas fa-gem", searchTerms:['icons']},
|
||||
{title: "fas fa-graduation-cap", searchTerms:['icons']},
|
||||
{title: "fas fa-hand-spock", searchTerms:['icons']},
|
||||
{title: "fas fa-heart", searchTerms:['icons']},
|
||||
{title: "fas fa-home", searchTerms:['icons']},
|
||||
{title: "fas fa-image", searchTerms:['icons']},
|
||||
{title: "fas fa-info-circle", searchTerms:['icons']},
|
||||
{title: "fas fa-key", searchTerms:['icons']},
|
||||
{title: "fas fa-leaf", searchTerms:['icons']},
|
||||
{title: "fas fa-life-ring", searchTerms:['icons']},
|
||||
{title: "fas fa-lightbulb", searchTerms:['icons']},
|
||||
{title: "fas fa-link", searchTerms:['icons']},
|
||||
{title: "fas fa-location-arrow", searchTerms:['icons']},
|
||||
{title: "fas fa-lock", searchTerms:['icons']},
|
||||
{title: "fas fa-map", searchTerms:['icons']},
|
||||
{title: "fas fa-map-signs", searchTerms:['icons']},
|
||||
{title: "fas fa-money-bill-alt", searchTerms:['icons']},
|
||||
{title: "fas fa-paper-plane", searchTerms:['icons']},
|
||||
{title: "fas fa-paperclip", searchTerms:['icons']},
|
||||
{title: "fas fa-question-circle", searchTerms:['icons']},
|
||||
{title: "fas fa-quote-left", searchTerms:['icons']},
|
||||
{title: "fas fa-road", searchTerms:['icons']},
|
||||
{title: "fas fa-rocket", searchTerms:['icons']},
|
||||
{title: "fas fa-shopping-cart", searchTerms:['icons']},
|
||||
{title: "fas fa-sitemap", searchTerms:['icons']},
|
||||
{title: "fas fa-sliders-h", searchTerms:['icons']},
|
||||
{title: "fas fa-star", searchTerms:['icons']},
|
||||
{title: "fas fa-tachometer-alt", searchTerms:['icons']},
|
||||
{title: "fas fa-thermometer-half", searchTerms:['icons']},
|
||||
{title: "fas fa-thumbs-down", searchTerms:['icons']},
|
||||
{title: "fas fa-thumbs-up", searchTerms:['icons']},
|
||||
{title: "fas fa-trash-alt", searchTerms:['icons']},
|
||||
{title: "fas fa-trophy", searchTerms:['icons']},
|
||||
{title: "fas fa-user-circle", searchTerms:['icons']},
|
||||
{title: "fas fa-utensils", searchTerms:['icons']}
|
||||
]
|
||||
});
|
||||
|
||||
jQuery('.titleIconPicker').on('iconpickerSelected', function(event) {
|
||||
var newIcon = event.iconpickerValue;
|
||||
jQuery('.articleIcon').val(newIcon);
|
||||
jQuery('.titleIconPicker .iconPlaceholder > i').attr('class', newIcon);
|
||||
saveField('icon', newIcon, function() {
|
||||
var treeLink = document.querySelector('.wiki-tree-link.active i');
|
||||
if (treeLink) treeLink.className = newIcon;
|
||||
updateLastSaved();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var tagsInput = document.getElementById('wikiTagsInput');
|
||||
if (tagsInput && jQuery.fn.tagsInput) {
|
||||
jQuery('#wikiTagsInput').tagsInput({
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
defaultText: 'Add tag...',
|
||||
placeholderColor: 'var(--secondary-font-color)',
|
||||
onChange: function(elem, elem_tags) {
|
||||
// The tagsInput plugin passes only the single tag that changed as elem_tags, and
|
||||
// fires once with `undefined` during its initial import. Saving elem_tags directly
|
||||
// overwrote the column with just the last tag (and the literal "undefined" on load).
|
||||
// Ignore the init call and persist the full delimited value instead.
|
||||
if (typeof elem_tags === 'undefined') {
|
||||
return;
|
||||
}
|
||||
saveField('tags', jQuery('#wikiTagsInput').val(), function() { updateLastSaved(); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var statusSelect = document.getElementById('wikiStatusSelect');
|
||||
if (statusSelect) {
|
||||
statusSelect.addEventListener('change', function() {
|
||||
var newStatus = statusSelect.value;
|
||||
saveField('status', newStatus, function() {
|
||||
var activeLink = document.querySelector('.wiki-tree-link.active');
|
||||
if (activeLink) {
|
||||
var draftLabel = activeLink.querySelector('.wiki-tree-draft');
|
||||
if (newStatus === 'draft') {
|
||||
if (!draftLabel) {
|
||||
draftLabel = document.createElement('span');
|
||||
draftLabel.className = 'wiki-tree-draft';
|
||||
activeLink.appendChild(draftLabel);
|
||||
}
|
||||
draftLabel.textContent = '({{ __('label.draft') }})';
|
||||
} else if (draftLabel) {
|
||||
draftLabel.remove();
|
||||
}
|
||||
}
|
||||
updateLastSaved();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var milestoneSelect = document.getElementById('wikiMilestoneSelect');
|
||||
if (milestoneSelect) {
|
||||
milestoneSelect.addEventListener('change', function() {
|
||||
saveField('milestoneId', milestoneSelect.value, function() { window.location.reload(); });
|
||||
});
|
||||
}
|
||||
|
||||
var parentSelect = document.getElementById('wikiParentSelect');
|
||||
if (parentSelect) {
|
||||
parentSelect.addEventListener('change', function() {
|
||||
saveField('parent', parentSelect.value, function() { window.location.reload(); });
|
||||
});
|
||||
}
|
||||
|
||||
function updateLastSaved() {
|
||||
var lastSavedEl = document.getElementById('wikiLastSaved');
|
||||
if (lastSavedEl) {
|
||||
lastSavedEl.textContent = 'Just now';
|
||||
lastSavedEl.dataset.timestamp = new Date().toISOString();
|
||||
}
|
||||
htmx.trigger(document.body, 'refreshActivity');
|
||||
}
|
||||
|
||||
function saveField(field, value, onSuccess) {
|
||||
var articleId = @json($currentArticle->id);
|
||||
|
||||
fetch(leantime.appUrl + '/hx/wiki/articleContent/save?articleId=' + articleId, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: field + '=' + encodeURIComponent(value)
|
||||
})
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
if (onSuccess) onSuccess(data);
|
||||
} else {
|
||||
console.error('[Wiki] Failed to save ' + field);
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('[Wiki] Save failed:', err);
|
||||
});
|
||||
}
|
||||
@endif
|
||||
|
||||
@if($login::userHasRole([$roles::$commenter]))
|
||||
leantime.commentsController.enableCommenterForms();
|
||||
@endif
|
||||
|
||||
});
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
63
app/Domain/Wiki/Templates/templates.blade.php
Normal file
63
app/Domain/Wiki/Templates/templates.blade.php
Normal file
@@ -0,0 +1,63 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
use Leantime\Domain\Wiki\Models\Template;
|
||||
|
||||
$today = date(__('language.dateformat'));
|
||||
$author = session('userdata.name') . ' (' . session('userdata.mail') . ')';
|
||||
|
||||
// Document templates for the editor
|
||||
// All Templates require title, description, content
|
||||
|
||||
$templates = [];
|
||||
|
||||
// All built-in document templates live as YAML in
|
||||
// app/Domain/ContentTemplates/Library/wiki/ now. The registry block below
|
||||
// loads them at request time, alongside any plugin-registered wiki
|
||||
// templates. $today / $author are still in scope so plugins (via the
|
||||
// documentTemplates filter, dispatched below) can keep using them.
|
||||
|
||||
// ── ContentTemplates registry — appliesTo:"wiki" ──
|
||||
// Phase 3 of the content-templates rollout: plugins (and core) drop
|
||||
// YAML files into ContentTemplates/wiki/ and they appear here.
|
||||
//
|
||||
// Each YAML string may carry t:KEY translation references; the
|
||||
// TranslationResolver helper expands them at consume time so the
|
||||
// user sees their locale's wording.
|
||||
try {
|
||||
$registry = app(\Leantime\Domain\ContentTemplates\Services\ContentTemplateRegistry::class);
|
||||
foreach ($registry->forAppliesTo('wiki') as $contentTpl) {
|
||||
$tplObj = app()->make(Template::class);
|
||||
$tplObj->title = \Leantime\Domain\ContentTemplates\Support\TranslationResolver::resolve($contentTpl->title);
|
||||
$tplObj->description = \Leantime\Domain\ContentTemplates\Support\TranslationResolver::resolve($contentTpl->description);
|
||||
// Category — for wiki templates we reuse the existing `sector`
|
||||
// field (same concept, different domain vocabulary). Falls back to
|
||||
// "documents" when the YAML doesn't specify one, matching the
|
||||
// hardcoded templates' previous default.
|
||||
$tplObj->category = $contentTpl->sector !== null && $contentTpl->sector !== ''
|
||||
? \Leantime\Domain\ContentTemplates\Support\TranslationResolver::resolve($contentTpl->sector)
|
||||
: __('templates.documents');
|
||||
$articles = (array) ($contentTpl->payload['articles'] ?? []);
|
||||
// For single-article templates we mirror the legacy "one HTML blob"
|
||||
// shape the editor expects. Multi-article wiki templates are out of
|
||||
// scope for the editor's "insert template" flow (they'd map to wiki
|
||||
// page creation, not editor insertion).
|
||||
$tplObj->content = is_array($articles[0] ?? null)
|
||||
? \Leantime\Domain\ContentTemplates\Support\TranslationResolver::resolve((string) ($articles[0]['content'] ?? ''))
|
||||
: '';
|
||||
if ($tplObj->content !== '') {
|
||||
$templates[] = $tplObj;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Registry not available (boot ordering, install) — silently skip.
|
||||
}
|
||||
|
||||
$templates = $tpl->dispatch_filter('documentTemplates', $templates);
|
||||
|
||||
echo json_encode($templates);
|
||||
@endphp
|
||||
|
||||
@endsection
|
||||
68
app/Domain/Wiki/Templates/wikiDialog.blade.php
Normal file
68
app/Domain/Wiki/Templates/wikiDialog.blade.php
Normal file
@@ -0,0 +1,68 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$currentWiki = $wiki ?? null;
|
||||
@endphp
|
||||
|
||||
<h4 class="widgettitle title-light"><i class="fa fa-book"></i> {!! __('label.wiki') !!} {{ $tpl->escape($currentWiki->title) }}</h4>
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
@php
|
||||
$id = '';
|
||||
if (isset($currentWiki->id)) {
|
||||
$id = $currentWiki->id;
|
||||
}
|
||||
@endphp
|
||||
|
||||
<form class="formModal" method="post" action="{{ BASE_URL }}/wiki/wikiModal/{{ $id }}">
|
||||
|
||||
<label>{!! __('label.wiki_title') !!}</label>
|
||||
<x-global::forms.text-input name="title" id="wikiTitle" value="{{ $tpl->escape($currentWiki->title) }}" placeholder="{{ __('input.placeholders.wiki_title') }}" /><br />
|
||||
|
||||
<br />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" id="saveBtn" />
|
||||
</div>
|
||||
<div class="col-md-6 align-right padding-top-sm">
|
||||
@if (isset($currentWiki->id) && $currentWiki->id != '' && $login::userIsAtLeast($roles::$editor))
|
||||
<a href="{{ BASE_URL }}/wiki/delWiki/{{ $currentWiki->id }}" class="delete formModal"><i class="fa fa-trash"></i> {!! __('links.delete_wiki') !!}</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script>
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
@if (isset($_GET['closeModal']))
|
||||
jQuery.nmTop().close();
|
||||
@endif
|
||||
|
||||
if(jQuery("#wikiTitle").val().length >= 2) {
|
||||
jQuery("#saveBtn").removeAttr("disabled");
|
||||
}else{
|
||||
jQuery("#saveBtn").attr("disabled", "disabled");
|
||||
}
|
||||
|
||||
jQuery("#wikiTitle").keypress(function(){
|
||||
|
||||
if(jQuery("#wikiTitle").val().length >= 2) {
|
||||
jQuery("#saveBtn").removeAttr("disabled");
|
||||
}else{
|
||||
jQuery("#saveBtn").attr("disabled", "disabled");
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@endsection
|
||||
Reference in New Issue
Block a user