0
0
Fork 0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-04-09 23:07:50 +00:00

Merge pull request from BookStackApp/editor_switching

Page editor switching
This commit is contained in:
Dan Brown 2022-04-24 14:03:03 +01:00 committed by GitHub
commit 5a7fb20116
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 743 additions and 209 deletions

View file

@ -22,6 +22,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
* @property bool $template
* @property bool $draft
* @property int $revision_count
* @property string $editor
* @property Chapter $chapter
* @property Collection $attachments
* @property Collection $revisions

View file

@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
*
* @property mixed $id
* @property int $page_id
* @property string $name
* @property string $slug
* @property string $book_slug
* @property int $created_by
@ -21,13 +22,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $summary
* @property string $markdown
* @property string $html
* @property string $text
* @property int $revision_number
* @property Page $page
* @property-read ?User $createdBy
*/
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
protected $fillable = ['name', 'text', 'summary'];
protected $hidden = ['html', 'markdown', 'restricted', 'text'];
/**

View file

@ -10,6 +10,7 @@ use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditorData;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
@ -217,11 +218,25 @@ class PageRepo
}
$pageContent = new PageContent($page);
if (!empty($input['markdown'] ?? '')) {
$currentEditor = $page->editor ?: PageEditorData::getSystemDefaultEditor();
$newEditor = $currentEditor;
$haveInput = isset($input['markdown']) || isset($input['html']);
$inputEmpty = empty($input['markdown']) && empty($input['html']);
if ($haveInput && $inputEmpty) {
$pageContent->setNewHTML('');
} elseif (!empty($input['markdown']) && is_string($input['markdown'])) {
$newEditor = 'markdown';
$pageContent->setNewMarkdown($input['markdown']);
} elseif (isset($input['html'])) {
$newEditor = 'wysiwyg';
$pageContent->setNewHTML($input['html']);
}
if ($newEditor !== $currentEditor && userCan('editor-change')) {
$page->editor = $newEditor;
}
}
/**
@ -229,8 +244,12 @@ class PageRepo
*/
protected function savePageRevision(Page $page, string $summary = null): PageRevision
{
$revision = new PageRevision($page->getAttributes());
$revision = new PageRevision();
$revision->name = $page->name;
$revision->html = $page->html;
$revision->markdown = $page->markdown;
$revision->text = $page->text;
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
@ -260,10 +279,15 @@ class PageRepo
return $page;
}
// Otherwise save the data to a revision
// Otherwise, save the data to a revision
$draft = $this->getPageRevisionToUpdate($page);
$draft->fill($input);
if (setting('app-editor') !== 'markdown') {
if (!empty($input['markdown'])) {
$draft->markdown = $input['markdown'];
$draft->html = '';
} else {
$draft->html = $input['html'];
$draft->markdown = '';
}

View file

@ -21,7 +21,7 @@ use League\HTMLToMarkdown\HtmlConverter;
class HtmlToMarkdown
{
protected $html;
protected string $html;
public function __construct(string $html)
{

View file

@ -0,0 +1,37 @@
<?php
namespace BookStack\Entities\Tools\Markdown;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
class MarkdownToHtml
{
protected string $markdown;
public function __construct(string $markdown)
{
$this->markdown = $markdown;
}
public function convert(): string
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CustomStrikeThroughExtension());
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
$converter = new CommonMarkConverter([], $environment);
$environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
return $converter->convertToHtml($this->markdown);
}
}

View file

@ -3,11 +3,8 @@
namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\CustomListItemRenderer;
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageService;
use BookStack\Util\HtmlContentFilter;
@ -17,15 +14,10 @@ use DOMNode;
use DOMNodeList;
use DOMXPath;
use Illuminate\Support\Str;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
class PageContent
{
protected $page;
protected Page $page;
/**
* PageContent constructor.
@ -53,28 +45,11 @@ class PageContent
{
$markdown = $this->extractBase64ImagesFromMarkdown($markdown);
$this->page->markdown = $markdown;
$html = $this->markdownToHtml($markdown);
$html = (new MarkdownToHtml($markdown))->convert();
$this->page->html = $this->formatHtml($html);
$this->page->text = $this->toPlainText();
}
/**
* Convert the given Markdown content to a HTML string.
*/
protected function markdownToHtml(string $markdown): string
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CustomStrikeThroughExtension());
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
$converter = new CommonMarkConverter([], $environment);
$environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
return $converter->convertToHtml($markdown);
}
/**
* Convert all base64 image data to saved images.
*/

View file

@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Builder;
class PageEditActivity
{
protected $page;
protected Page $page;
/**
* PageEditActivity constructor.

View file

@ -0,0 +1,116 @@
<?php
namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
class PageEditorData
{
protected Page $page;
protected PageRepo $pageRepo;
protected string $requestedEditor;
protected array $viewData;
protected array $warnings;
public function __construct(Page $page, PageRepo $pageRepo, string $requestedEditor)
{
$this->page = $page;
$this->pageRepo = $pageRepo;
$this->requestedEditor = $requestedEditor;
$this->viewData = $this->build();
}
public function getViewData(): array
{
return $this->viewData;
}
public function getWarnings(): array
{
return $this->warnings;
}
protected function build(): array
{
$page = clone $this->page;
$isDraft = boolval($this->page->draft);
$templates = $this->pageRepo->getTemplates(10);
$draftsEnabled = auth()->check();
$isDraftRevision = false;
$this->warnings = [];
$editActivity = new PageEditActivity($page);
if ($editActivity->hasActiveEditing()) {
$this->warnings[] = $editActivity->activeEditingMessage();
}
// Check for a current draft version for this user
$userDraft = $this->pageRepo->getUserDraft($page);
if ($userDraft !== null) {
$page->forceFill($userDraft->only(['name', 'html', 'markdown']));
$isDraftRevision = true;
$this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
}
$editorType = $this->getEditorType($page);
$this->updateContentForEditor($page, $editorType);
return [
'page' => $page,
'book' => $page->book,
'isDraft' => $isDraft,
'isDraftRevision' => $isDraftRevision,
'draftsEnabled' => $draftsEnabled,
'templates' => $templates,
'editor' => $editorType,
];
}
protected function updateContentForEditor(Page $page, string $editorType): void
{
$isHtml = !empty($page->html) && empty($page->markdown);
// HTML to markdown-clean conversion
if ($editorType === 'markdown' && $isHtml && $this->requestedEditor === 'markdown-clean') {
$page->markdown = (new HtmlToMarkdown($page->html))->convert();
}
// Markdown to HTML conversion if we don't have HTML
if ($editorType === 'wysiwyg' && !$isHtml) {
$page->html = (new MarkdownToHtml($page->markdown))->convert();
}
}
/**
* Get the type of editor to show for editing the given page.
* Defaults based upon the current content of the page otherwise will fall back
* to system default but will take a requested type (if provided) if permissions allow.
*/
protected function getEditorType(Page $page): string
{
$editorType = $page->editor ?: self::getSystemDefaultEditor();
// Use requested editor if valid and if we have permission
$requestedType = explode('-', $this->requestedEditor)[0];
if (($requestedType === 'markdown' || $requestedType === 'wysiwyg') && userCan('editor-change')) {
$editorType = $requestedType;
}
return $editorType;
}
/**
* Get the configured system default editor.
*/
public static function getSystemDefaultEditor(): string
{
return setting('app-editor') === 'markdown' ? 'markdown' : 'wysiwyg';
}
}

View file

@ -10,6 +10,7 @@ use BookStack\Entities\Tools\Cloner;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
use BookStack\Entities\Tools\PageEditorData;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
@ -21,7 +22,7 @@ use Throwable;
class PageController extends Controller
{
protected $pageRepo;
protected PageRepo $pageRepo;
/**
* PageController constructor.
@ -82,22 +83,15 @@ class PageController extends Controller
*
* @throws NotFoundException
*/
public function editDraft(string $bookSlug, int $pageId)
public function editDraft(Request $request, string $bookSlug, int $pageId)
{
$draft = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-create', $draft->getParent());
$editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->isSignedIn();
$templates = $this->pageRepo->getTemplates(10);
return view('pages.edit', [
'page' => $draft,
'book' => $draft->book,
'isDraft' => true,
'draftsEnabled' => $draftsEnabled,
'templates' => $templates,
]);
return view('pages.edit', $editorData->getViewData());
}
/**
@ -188,43 +182,19 @@ class PageController extends Controller
*
* @throws NotFoundException
*/
public function edit(string $bookSlug, string $pageSlug)
public function edit(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$page->isDraft = false;
$editActivity = new PageEditActivity($page);
// Check for active editing
$warnings = [];
if ($editActivity->hasActiveEditing()) {
$warnings[] = $editActivity->activeEditingMessage();
$editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
if ($editorData->getWarnings()) {
$this->showWarningNotification(implode("\n", $editorData->getWarnings()));
}
// Check for a current draft version for this user
$userDraft = $this->pageRepo->getUserDraft($page);
if ($userDraft !== null) {
$page->forceFill($userDraft->only(['name', 'html', 'markdown']));
$page->isDraft = true;
$warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
}
if (count($warnings) > 0) {
$this->showWarningNotification(implode("\n", $warnings));
}
$templates = $this->pageRepo->getTemplates(10);
$draftsEnabled = $this->isSignedIn();
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
return view('pages.edit', [
'page' => $page,
'book' => $page->book,
'current' => $page,
'draftsEnabled' => $draftsEnabled,
'templates' => $templates,
]);
return view('pages.edit', $editorData->getViewData());
}
/**

View file

@ -0,0 +1,62 @@
<?php
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class AddEditorChangeFieldAndPermission extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Add the new 'editor' column to the pages table
Schema::table('pages', function(Blueprint $table) {
$table->string('editor', 50)->default('');
});
// Populate the new 'editor' column
// We set it to 'markdown' for pages currently with markdown content
DB::table('pages')->where('markdown', '!=', '')->update(['editor' => 'markdown']);
// We set it to 'wysiwyg' where we have HTML but no markdown
DB::table('pages')->where('markdown', '=', '')
->where('html', '!=', '')
->update(['editor' => 'wysiwyg']);
// Give the admin user permission to change the editor
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
$permissionId = DB::table('role_permissions')->insertGetId([
'name' => 'editor-change',
'display_name' => 'Change page editor',
'created_at' => Carbon::now()->toDateTimeString(),
'updated_at' => Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId,
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Drop the new column from the pages table
Schema::table('pages', function(Blueprint $table) {
$table->dropColumn('editor');
});
// Remove traces of the role permission
DB::table('role_permissions')->where('name', '=', 'editor-change')->delete();
}
}

View file

@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.99 16H14v-2H6.99v-3L3 15l3.99 4ZM21 9l-3.99-4v3H10v2h7.01v3z"/></svg>

After

(image error) Size: 141 B

View file

@ -131,7 +131,7 @@ class AutoSuggest {
return this.hideSuggestions();
}
this.list.innerHTML = suggestions.map(value => `<li><button type="button">${escapeHtml(value)}</button></li>`).join('');
this.list.innerHTML = suggestions.map(value => `<li><button type="button" class="text-item">${escapeHtml(value)}</button></li>`).join('');
this.list.style.display = 'block';
for (const button of this.list.querySelectorAll('button')) {
button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));

View file

@ -96,7 +96,7 @@ class CodeEditor {
this.historyDropDown.classList.toggle('hidden', historyKeys.length === 0);
this.historyList.innerHTML = historyKeys.map(key => {
const localTime = (new Date(parseInt(key))).toLocaleTimeString();
return `<li><button type="button" data-time="${key}">${localTime}</button></li>`;
return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
}).join('');
}

View file

@ -0,0 +1,52 @@
import {onSelect} from "../services/dom";
/**
* Custom equivalent of window.confirm() using our popup component.
* Is promise based so can be used like so:
* `const result = await dialog.show()`
* @extends {Component}
*/
class ConfirmDialog {
setup() {
this.container = this.$el;
this.confirmButton = this.$refs.confirm;
this.res = null;
onSelect(this.confirmButton, () => {
this.sendResult(true);
this.getPopup().hide();
});
}
show() {
this.getPopup().show(null, () => {
this.sendResult(false);
});
return new Promise((res, rej) => {
this.res = res;
});
}
/**
* @returns {Popup}
*/
getPopup() {
return this.container.components.popup;
}
/**
* @param {Boolean} result
*/
sendResult(result) {
if (this.res) {
this.res(result)
this.res = null;
}
}
}
export default ConfirmDialog;

View file

@ -10,6 +10,7 @@ import chapterToggle from "./chapter-toggle.js"
import codeEditor from "./code-editor.js"
import codeHighlighter from "./code-highlighter.js"
import collapsible from "./collapsible.js"
import confirmDialog from "./confirm-dialog"
import customCheckbox from "./custom-checkbox.js"
import detailsHighlighter from "./details-highlighter.js"
import dropdown from "./dropdown.js"
@ -26,7 +27,6 @@ import headerMobileToggle from "./header-mobile-toggle.js"
import homepageControl from "./homepage-control.js"
import imageManager from "./image-manager.js"
import imagePicker from "./image-picker.js"
import index from "./index.js"
import listSortControl from "./list-sort-control.js"
import markdownEditor from "./markdown-editor.js"
import newUserPassword from "./new-user-password.js"
@ -66,6 +66,7 @@ const componentMapping = {
"code-editor": codeEditor,
"code-highlighter": codeHighlighter,
"collapsible": collapsible,
"confirm-dialog": confirmDialog,
"custom-checkbox": customCheckbox,
"details-highlighter": detailsHighlighter,
"dropdown": dropdown,
@ -82,7 +83,6 @@ const componentMapping = {
"homepage-control": homepageControl,
"image-manager": imageManager,
"image-picker": imagePicker,
"index": index,
"list-sort-control": listSortControl,
"markdown-editor": markdownEditor,
"new-user-password": newUserPassword,

View file

@ -24,6 +24,8 @@ class PageEditor {
this.draftDisplayIcon = this.$refs.draftDisplayIcon;
this.changelogInput = this.$refs.changelogInput;
this.changelogDisplay = this.$refs.changelogDisplay;
this.changeEditorButtons = this.$manyRefs.changeEditor;
this.switchDialogContainer = this.$refs.switchDialog;
// Translations
this.draftText = this.$opts.draftText;
@ -72,6 +74,9 @@ class PageEditor {
// Draft Controls
onSelect(this.saveDraftButton, this.saveDraft.bind(this));
onSelect(this.discardDraftButton, this.discardDraft.bind(this));
// Change editor controls
onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
}
setInitialFocus() {
@ -113,17 +118,21 @@ class PageEditor {
data.markdown = this.editorMarkdown;
}
let didSave = false;
try {
const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
if (!this.isNewDraft) {
this.toggleDiscardDraftVisibility(true);
}
this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
this.autoSave.last = Date.now();
if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
window.$events.emit('warning', resp.data.warning);
this.shownWarningsCache.add(resp.data.warning);
}
didSave = true;
} catch (err) {
// Save the editor content in LocalStorage as a last resort, just in case.
try {
@ -134,6 +143,7 @@ class PageEditor {
window.$events.emit('error', this.autosaveFailText);
}
return didSave;
}
draftNotifyChange(text) {
@ -185,6 +195,18 @@ class PageEditor {
this.discardDraftWrap.classList.toggle('hidden', !show);
}
async changeEditor(event) {
event.preventDefault();
const link = event.target.closest('a').href;
const dialog = this.switchDialogContainer.components['confirm-dialog'];
const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
if (saved && confirmed) {
window.location = link;
}
}
}
export default PageEditor;

View file

@ -34,7 +34,7 @@ class Popup {
}
hide(onComplete = null) {
fadeOut(this.container, 240, onComplete);
fadeOut(this.container, 120, onComplete);
if (this.onkeyup) {
window.removeEventListener('keyup', this.onkeyup);
this.onkeyup = null;
@ -45,7 +45,7 @@ class Popup {
}
show(onComplete = null, onHide = null) {
fadeIn(this.container, 240, onComplete);
fadeIn(this.container, 120, onComplete);
this.onkeyup = (event) => {
if (event.key === 'Escape') {

View file

@ -196,9 +196,19 @@ return [
'pages_edit_draft_save_at' => 'Draft saved at ',
'pages_edit_delete_draft' => 'Delete Draft',
'pages_edit_discard_draft' => 'Discard Draft',
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'pages_edit_set_changelog' => 'Set Changelog',
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
'pages_edit_enter_changelog' => 'Enter Changelog',
'pages_editor_switch_title' => 'Switch Editor',
'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
'pages_save' => 'Save Page',
'pages_title' => 'Page Title',
'pages_name' => 'Page Name',
@ -225,6 +235,7 @@ return [
'pages_revisions_number' => '#',
'pages_revisions_numbered' => 'Revision #:id',
'pages_revisions_numbered_changes' => 'Revision #:id Changes',
'pages_revisions_editor' => 'Editor Type',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Current Version',

View file

@ -27,8 +27,8 @@ return [
'app_secure_images' => 'Higher Security Image Uploads',
'app_secure_images_toggle' => 'Enable higher security image uploads',
'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',
'app_editor' => 'Page Editor',
'app_editor_desc' => 'Select which editor will be used by all users to edit pages.',
'app_default_editor' => 'Default Page Editor',
'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
'app_custom_html' => 'Custom HTML Head Content',
'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',
@ -152,6 +152,7 @@ return [
'role_access_api' => 'Access system API',
'role_manage_settings' => 'Manage app settings',
'role_export_content' => 'Export content',
'role_editor_change' => 'Change page editor',
'role_asset' => 'Asset Permissions',
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',

View file

@ -120,6 +120,11 @@
width: 800px;
max-width: 90%;
}
&.very-small {
margin: 2% auto;
width: 600px;
max-width: 90%;
}
&:before {
display: flex;
align-self: flex-start;

View file

@ -593,13 +593,22 @@ ul.pagination {
li.active a {
font-weight: 600;
}
a, button {
display: block;
padding: $-xs $-m;
button {
width: 100%;
text-align: start;
}
li.border-bottom {
border-bottom: 1px solid #DDD;
}
li hr {
margin: $-xs 0;
}
.icon-item, .text-item, .label-item {
padding: 8px $-m;
@include lightDark(color, #555, #eee);
fill: currentColor;
white-space: nowrap;
line-height: 1.6;
line-height: 1.4;
cursor: pointer;
&:hover, &:focus {
text-decoration: none;
@ -616,15 +625,30 @@ ul.pagination {
width: 16px;
}
}
button {
width: 100%;
text-align: start;
.text-item {
display: block;
}
li.border-bottom {
border-bottom: 1px solid #DDD;
.label-item {
display: grid;
align-items: center;
grid-template-columns: auto min-content;
gap: $-m;
}
li hr {
margin: $-xs 0;
.label-item > *:nth-child(2) {
opacity: 0.7;
&:hover {
opacity: 1;
}
}
.icon-item {
display: grid;
align-items: start;
grid-template-columns: 16px auto;
gap: $-m;
svg {
margin-inline-end: 0;
margin-block-start: 1px;
}
}
}

View file

@ -163,7 +163,6 @@ em, i, .italic {
small, p.small, span.small, .text-small {
font-size: 0.75rem;
@include lightDark(color, #5e5e5e, #999);
}
sup, .superscript {

View file

@ -28,7 +28,7 @@
class="drag-card-action text-center text-neg">@icon('close')</button>
<div refs="dropdown@menu" class="dropdown-menu">
<p class="text-neg small px-m mb-xs">{{ trans('entities.attachments_delete') }}</p>
<button refs="ajax-delete-row@delete" type="button" class="text-primary small delete">{{ trans('common.confirm') }}</button>
<button refs="ajax-delete-row@delete" type="button" class="text-primary small delete text-item">{{ trans('common.confirm') }}</button>
</div>
</div>
</div>

View file

@ -31,7 +31,12 @@
<button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button>
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
<li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li>
<li><button action="delete" type="button" class="text-button text-neg" >@icon('delete'){{ trans('common.delete') }}</button></li>
<li>
<button action="delete" type="button" class="text-button text-neg icon-item">
@icon('delete')
<div>{{ trans('common.delete') }}</div>
</button>
</li>
</ul>
</div>
@endif

View file

@ -0,0 +1,21 @@
<div components="popup confirm-dialog"
refs="confirm-dialog@popup {{ $ref }}"
class="popup-background">
<div class="popup-body very-small" tabindex="-1">
<div class="popup-header primary-background">
<div class="popup-title">{{ $title }}</div>
<button refs="popup@hide" type="button" class="popup-header-close">x</button>
</div>
<div class="px-m py-m">
{{ $slot }}
<div class="text-right">
<button type="button" class="button outline" refs="popup@hide">{{ trans('common.cancel') }}</button>
<button type="button" class="button" refs="confirm-dialog@confirm">{{ trans('common.continue') }}</button>
</div>
</div>
</div>
</div>

View file

@ -62,26 +62,36 @@
</span>
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
<li>
<a href="{{ url('/favourites') }}">@icon('star'){{ trans('entities.my_favourites') }}</a>
<a href="{{ url('/favourites') }}" class="icon-item">
@icon('star')
<div>{{ trans('entities.my_favourites') }}</div>
</a>
</li>
<li>
<a href="{{ $currentUser->getProfileUrl() }}">@icon('user'){{ trans('common.view_profile') }}</a>
<a href="{{ $currentUser->getProfileUrl() }}" class="icon-item">
@icon('user')
<div>{{ trans('common.view_profile') }}</div>
</a>
</li>
<li>
<a href="{{ $currentUser->getEditUrl() }}">@icon('edit'){{ trans('common.edit_profile') }}</a>
<a href="{{ $currentUser->getEditUrl() }}" class="icon-item">
@icon('edit')
<div>{{ trans('common.edit_profile') }}</div>
</a>
</li>
<li>
<form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
method="post">
{{ csrf_field() }}
<button class="text-muted icon-list-item text-primary">
@icon('logout'){{ trans('auth.logout') }}
<button class="icon-item">
@icon('logout')
<div>{{ trans('auth.logout') }}</div>
</button>
</form>
</li>
<li><hr></li>
<li>
@include('common.dark-mode-toggle')
@include('common.dark-mode-toggle', ['classes' => 'icon-item'])
</li>
</ul>
</div>

View file

@ -5,9 +5,9 @@
<span>{{ trans('entities.export') }}</span>
</div>
<ul refs="dropdown@menu" class="wide dropdown-menu" role="menu">
<li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank" rel="noopener">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
<li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank" rel="noopener">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
<li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank" rel="noopener">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
<li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank" rel="noopener">{{ trans('entities.export_md') }} <span class="text-muted float right">.md</span></a></li>
<li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_html') }}</span><span>.html</span></a></li>
<li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_pdf') }}</span><span>.pdf</span></a></li>
<li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_text') }}</span><span>.txt</span></a></li>
<li><a href="{{ $entity->getUrl('/export/markdown') }}" target="_blank" class="label-item"><span>{{ trans('entities.export_md') }}</span><span>.md</span></a></li>
</ul>
</div>

View file

@ -16,7 +16,7 @@
<div refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" tabindex="0">{{ $options[$selectedSort] }}</div>
<ul refs="dropdown@menu" class="dropdown-menu">
@foreach($options as $key => $label)
<li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}">{{ $label }}</a></li>
<li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}" class="text-item">{{ $label }}</a></li>
@endforeach
</ul>
</div>

View file

@ -17,6 +17,13 @@
</div>
</div>
<script nonce="{{ $cspNonce }}">
setTimeout(async () => {
const result = await window.components["confirm-dialog"][0].show();
console.log({result});
}, 1000);
</script>
<div class="container" id="home-default">
<div class="grid third gap-xxl no-row-gap" >
<div>

View file

@ -19,7 +19,7 @@
<form action="{{ url('/mfa/' . $method . '/remove') }}" method="post">
{{ csrf_field() }}
{{ method_field('delete') }}
<button class="text-primary small delete">{{ trans('common.confirm') }}</button>
<button class="text-primary small text-item">{{ trans('common.confirm') }}</button>
</form>
</div>
</div>

View file

@ -1,9 +1,5 @@
@extends('layouts.base')
@section('head')
<script src="{{ url('/libs/tinymce/tinymce.min.js?ver=5.10.2') }}" nonce="{{ $cspNonce }}"></script>
@stop
@section('body-class', 'flexbox')
@section('content')
@ -12,9 +8,7 @@
<form action="{{ $page->getUrl() }}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
{{ csrf_field() }}
@if(!isset($isDraft))
<input type="hidden" name="_method" value="PUT">
@endif
@if(!$isDraft) {{ method_field('PUT') }} @endif
@include('pages.parts.form', ['model' => $page])
@include('pages.parts.editor-toolbox')
</form>

View file

@ -0,0 +1,86 @@
<div class="primary-background-light toolbar page-edit-toolbar">
<div class="grid third no-break v-center">
<div class="action-buttons text-left px-m py-xs">
<a href="{{ $isDraft ? $page->getParent()->getUrl() : $page->getUrl() }}"
class="text-button text-primary">@icon('back')<span class="hide-under-l">{{ trans('common.back') }}</span></a>
</div>
<div class="text-center px-m">
<div component="dropdown"
option:dropdown:move-menu="true"
class="dropdown-container draft-display text {{ $draftsEnabled ? '' : 'hidden' }}">
<button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" title="{{ trans('entities.pages_edit_draft_options') }}" class="text-primary text-button py-s px-m"><span refs="page-editor@draftDisplay" class="faded-text"></span>&nbsp; @icon('more')</button>
@icon('check-circle', ['class' => 'text-pos draft-notification svg-icon', 'refs' => 'page-editor@draftDisplayIcon'])
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
<li>
<button refs="page-editor@saveDraft" type="button" class="text-pos icon-item">
@icon('save')
<div>{{ trans('entities.pages_edit_save_draft') }}</div>
</button>
</li>
@if($isDraft)
<li>
<a href="{{ $model->getUrl('/delete') }}" class="text-neg icon-item">
@icon('delete')
{{ trans('entities.pages_edit_delete_draft') }}
</a>
</li>
@endif
<li refs="page-editor@discardDraftWrap" class="{{ $isDraft ? '' : 'hidden' }}">
<button refs="page-editor@discardDraft" type="button" class="text-neg icon-item">
@icon('cancel')
<div>{{ trans('entities.pages_edit_discard_draft') }}</div>
</button>
</li>
@if(userCan('editor-change'))
<li>
@if($editor === 'wysiwyg')
<a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-clean" refs="page-editor@changeEditor" class="icon-item">
@icon('swap-horizontal')
<div>
{{ trans('entities.pages_edit_switch_to_markdown') }}
<br>
<small>{{ trans('entities.pages_edit_switch_to_markdown_clean') }}</small>
</div>
</a>
<a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-stable" refs="page-editor@changeEditor" class="icon-item">
@icon('swap-horizontal')
<div>
{{ trans('entities.pages_edit_switch_to_markdown') }}
<br>
<small>{{ trans('entities.pages_edit_switch_to_markdown_stable') }}</small>
</div>
</a>
@else
<a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=wysiwyg" refs="page-editor@changeEditor" class="icon-item">
@icon('swap-horizontal')
<div>{{ trans('entities.pages_edit_switch_to_wysiwyg') }}</div>
</a>
@endif
</li>
@endif
</ul>
</div>
</div>
<div class="action-buttons px-m py-xs">
<div component="dropdown" dropdown-move-menu class="dropdown-container">
<button refs="dropdown@toggle" type="button" aria-haspopup="true" aria-expanded="false" class="text-primary text-button">@icon('edit') <span refs="page-editor@changelogDisplay">{{ trans('entities.pages_edit_set_changelog') }}</span></button>
<ul refs="dropdown@menu" class="wide dropdown-menu">
<li class="px-l py-m">
<p class="text-muted pb-s">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
<input refs="page-editor@changelogInput"
name="summary"
id="summary-input"
type="text"
placeholder="{{ trans('entities.pages_edit_enter_changelog') }}" />
</li>
</ul>
<span>{{-- Prevents button jumping on menu show --}}</span>
</div>
<button type="submit" id="save-button" class="float-left text-primary text-button text-pos-hover hide-under-m">@icon('save')<span>{{ trans('entities.pages_save') }}</span></button>
</div>
</div>
</div>

View file

@ -6,66 +6,17 @@
@if($model->name === trans('entities.pages_initial_name'))
option:page-editor:has-default-title="true"
@endif
option:page-editor:editor-type="{{ setting('app-editor') }}"
option:page-editor:editor-type="{{ $editor }}"
option:page-editor:page-id="{{ $model->id ?? '0' }}"
option:page-editor:page-new-draft="{{ ($model->draft ?? false) ? 'true' : 'false' }}"
option:page-editor:draft-text="{{ ($model->draft || $model->isDraft) ? trans('entities.pages_editing_draft') : trans('entities.pages_editing_page') }}"
option:page-editor:page-new-draft="{{ $isDraft ? 'true' : 'false' }}"
option:page-editor:draft-text="{{ ($isDraft || $isDraftRevision) ? trans('entities.pages_editing_draft') : trans('entities.pages_editing_page') }}"
option:page-editor:autosave-fail-text="{{ trans('errors.page_draft_autosave_fail') }}"
option:page-editor:editing-page-text="{{ trans('entities.pages_editing_page') }}"
option:page-editor:draft-discarded-text="{{ trans('entities.pages_draft_discarded') }}"
option:page-editor:set-changelog-text="{{ trans('entities.pages_edit_set_changelog') }}">
{{--Header Bar--}}
<div class="primary-background-light toolbar page-edit-toolbar">
<div class="grid third no-break v-center">
<div class="action-buttons text-left px-m py-xs">
<a href="{{ $page->draft ? $page->getParent()->getUrl() : $page->getUrl() }}"
class="text-button text-primary">@icon('back')<span class="hide-under-l">{{ trans('common.back') }}</span></a>
</div>
<div class="text-center px-m py-xs">
<div component="dropdown"
option:dropdown:move-menu="true"
class="dropdown-container draft-display text {{ $draftsEnabled ? '' : 'hidden' }}">
<button type="button" refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" title="{{ trans('entities.pages_edit_draft_options') }}" class="text-primary text-button"><span refs="page-editor@draftDisplay" class="faded-text"></span>&nbsp; @icon('more')</button>
@icon('check-circle', ['class' => 'text-pos draft-notification svg-icon', 'refs' => 'page-editor@draftDisplayIcon'])
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
<li>
<button refs="page-editor@saveDraft" type="button" class="text-pos">@icon('save'){{ trans('entities.pages_edit_save_draft') }}</button>
</li>
@if ($model->draft)
<li>
<a href="{{ $model->getUrl('/delete') }}" class="text-neg">@icon('delete'){{ trans('entities.pages_edit_delete_draft') }}</a>
</li>
@endif
<li refs="page-editor@discardDraftWrap" class="{{ ($model->isDraft ?? false) ? '' : 'hidden' }}">
<button refs="page-editor@discardDraft" type="button" class="text-neg">@icon('cancel'){{ trans('entities.pages_edit_discard_draft') }}</button>
</li>
</ul>
</div>
</div>
<div class="action-buttons px-m py-xs">
<div component="dropdown" dropdown-move-menu class="dropdown-container">
<button refs="dropdown@toggle" type="button" aria-haspopup="true" aria-expanded="false" class="text-primary text-button">@icon('edit') <span refs="page-editor@changelogDisplay">{{ trans('entities.pages_edit_set_changelog') }}</span></button>
<ul refs="dropdown@menu" class="wide dropdown-menu">
<li class="px-l py-m">
<p class="text-muted pb-s">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
<input refs="page-editor@changelogInput"
name="summary"
id="summary-input"
type="text"
placeholder="{{ trans('entities.pages_edit_enter_changelog') }}" />
</li>
</ul>
<span>{{-- Prevents button jumping on menu show --}}</span>
</div>
<button type="submit" id="save-button" class="float-left text-primary text-button text-pos-hover hide-under-m">@icon('save')<span>{{ trans('entities.pages_save') }}</span></button>
</div>
</div>
</div>
{{--Header Toolbar--}}
@include('pages.parts.editor-toolbar', ['model' => $model, 'editor' => $editor, 'isDraft' => $isDraft, 'draftsEnabled' => $draftsEnabled])
{{--Title input--}}
<div class="title-input page-title clearfix">
@ -78,19 +29,35 @@
<div class="edit-area flex-fill flex">
{{--WYSIWYG Editor--}}
@if(setting('app-editor') === 'wysiwyg')
@if($editor === 'wysiwyg')
@include('pages.parts.wysiwyg-editor', ['model' => $model])
@endif
{{--Markdown Editor--}}
@if(setting('app-editor') === 'markdown')
@if($editor === 'markdown')
@include('pages.parts.markdown-editor', ['model' => $model])
@endif
</div>
{{--Mobile Save Button--}}
<button type="submit"
id="save-button-mobile"
title="{{ trans('entities.pages_save') }}"
class="text-primary text-button hide-over-m page-save-mobile-button">@icon('save')</button>
{{--Editor Change Dialog--}}
@component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switchDialog'])
<p>
{{ trans('entities.pages_editor_switch_are_you_sure') }}
<br>
{{ trans('entities.pages_editor_switch_consider_following') }}
</p>
<ul>
<li>{{ trans('entities.pages_editor_switch_consideration_a') }}</li>
<li>{{ trans('entities.pages_editor_switch_consideration_b') }}</li>
<li>{{ trans('entities.pages_editor_switch_consideration_c') }}</li>
</ul>
@endcomponent
</div>

View file

@ -1,3 +1,7 @@
@push('head')
<script src="{{ url('/libs/tinymce/tinymce.min.js?ver=5.10.2') }}" nonce="{{ $cspNonce }}"></script>
@endpush
<div component="wysiwyg-editor"
option:wysiwyg-editor:language="{{ config('app.lang') }}"
option:wysiwyg-editor:page-id="{{ $model->id ?? 0 }}"

View file

@ -21,26 +21,39 @@
<table class="table">
<tr>
<th width="3%">{{ trans('entities.pages_revisions_number') }}</th>
<th width="23%">{{ trans('entities.pages_name') }}</th>
<th colspan="2" width="8%">{{ trans('entities.pages_revisions_created_by') }}</th>
<th width="15%">{{ trans('entities.pages_revisions_date') }}</th>
<th width="25%">{{ trans('entities.pages_revisions_changelog') }}</th>
<th width="20%">{{ trans('common.actions') }}</th>
<th width="40">{{ trans('entities.pages_revisions_number') }}</th>
<th>
{{ trans('entities.pages_name') }} / {{ trans('entities.pages_revisions_editor') }}
</th>
<th colspan="2">{{ trans('entities.pages_revisions_created_by') }} / {{ trans('entities.pages_revisions_date') }}</th>
<th>{{ trans('entities.pages_revisions_changelog') }}</th>
<th class="text-right">{{ trans('common.actions') }}</th>
</tr>
@foreach($page->revisions as $index => $revision)
<tr>
<td>{{ $revision->revision_number == 0 ? '' : $revision->revision_number }}</td>
<td>{{ $revision->name }}</td>
<td style="line-height: 0;">
<td>
{{ $revision->name }}
<br>
<small class="text-muted">({{ $revision->markdown ? 'Markdown' : 'WYSIWYG' }})</small>
</td>
<td style="line-height: 0;" width="30">
@if($revision->createdBy)
<img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
@endif
</td>
<td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif</td>
<td><small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
<td>{{ $revision->summary }}</td>
<td class="actions">
<td width="260">
@if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif
<br>
<div class="text-muted">
<small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }}</small>
<small>({{ $revision->created_at->diffForHumans() }})</small>
</div>
</td>
<td>
{{ $revision->summary }}
</td>
<td class="actions text-small text-right">
<a href="{{ $revision->getUrl('changes') }}" target="_blank" rel="noopener">{{ trans('entities.pages_revisions_changes') }}</a>
<span class="text-muted">&nbsp;|&nbsp;</span>
@ -58,7 +71,10 @@
<form action="{{ $revision->getUrl('/restore') }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
<button type="submit" class="text-button text-primary">@icon('history'){{ trans('entities.pages_revisions_restore') }}</button>
<button type="submit" class="text-primary icon-item">
@icon('history')
<div>{{ trans('entities.pages_revisions_restore') }}</div>
</button>
</form>
</li>
</ul>
@ -72,7 +88,10 @@
<form action="{{ $revision->getUrl('/delete/') }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="text-button text-neg">@icon('delete'){{ trans('common.delete') }}</button>
<button type="submit" class="text-neg icon-item">
@icon('delete')
<div>{{ trans('common.delete') }}</div>
</button>
</form>
</li>
</ul>

View file

@ -14,9 +14,9 @@
<label for="">{{ trans('settings.audit_event_filter') }}</label>
<button refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" class="input-base text-left">{{ $listDetails['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>
<ul refs="dropdown@menu" class="dropdown-menu">
<li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
<li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}" class="text-item">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
@foreach($activityTypes as $type)
<li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}">{{ $type }}</a></li>
<li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}" class="text-item">{{ $type }}</a></li>
@endforeach
</ul>
</div>

View file

@ -23,12 +23,12 @@
</div>
</div>
<div class="grid half gap-xl">
<div class="grid half gap-xl items-center">
<div>
<label class="setting-list-label">{{ trans('settings.app_editor') }}</label>
<p class="small">{{ trans('settings.app_editor_desc') }}</p>
<label class="setting-list-label" for="setting-app-editor">{{ trans('settings.app_default_editor') }}</label>
<p class="small">{{ trans('settings.app_default_editor_desc') }}</p>
</div>
<div class="pt-xs">
<div>
<select name="setting-app-editor" id="setting-app-editor">
<option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
<option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
@ -90,12 +90,12 @@
</div>
</div>
<div homepage-control id="homepage-control" class="grid half gap-xl">
<div homepage-control id="homepage-control" class="grid half gap-xl items-center">
<div>
<label for="setting-app-homepage" class="setting-list-label">{{ trans('settings.app_homepage') }}</label>
<label for="setting-app-homepage-type" class="setting-list-label">{{ trans('settings.app_homepage') }}</label>
<p class="small">{{ trans('settings.app_homepage_desc') }}</p>
</div>
<div class="pt-xs">
<div>
<select name="setting-app-homepage-type" id="setting-app-homepage-type">
<option @if(setting('app-homepage-type') === 'default') selected @endif value="default">{{ trans('common.default') }}</option>
<option @if(setting('app-homepage-type') === 'books') selected @endif value="books">{{ trans('entities.books') }}</option>

View file

@ -22,7 +22,7 @@
<form action="{{ url('/settings/recycle-bin/empty') }}" method="POST">
{!! csrf_field() !!}
<button type="submit" class="text-primary small delete">{{ trans('common.confirm') }}</button>
<button type="submit" class="text-primary small delete text-item">{{ trans('common.confirm') }}</button>
</form>
</div>
</div>
@ -93,8 +93,8 @@
<div component="dropdown" class="dropdown-container">
<button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
<ul refs="dropdown@menu" class="dropdown-menu">
<li><a class="block" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
<li><a class="block" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
<li><a class="text-item" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
<li><a class="text-item" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
</ul>
</div>
</td>

View file

@ -37,6 +37,7 @@
<div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'editor-change', 'label' => trans('settings.role_editor_change')])</div>
</div>
<div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>

View file

@ -252,7 +252,9 @@ class PagesApiTest extends TestCase
'tags' => [['name' => 'Category', 'value' => 'Testing']]
];
$this->putJson($this->baseEndpoint . "/{$page->id}", $details);
$resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
$resp->assertOk();
$page->refresh();
$this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());
}

View file

@ -18,36 +18,39 @@ class PageEditorTest extends TestCase
$this->page = Page::query()->first();
}
public function test_default_editor_is_wysiwyg()
public function test_default_editor_is_wysiwyg_for_new_pages()
{
$this->assertEquals('wysiwyg', setting('app-editor'));
$this->asAdmin()->get($this->page->getUrl() . '/edit')
->assertElementExists('#html-editor');
$resp = $this->asAdmin()->get($this->page->book->getUrl('/create-page'));
$this->followRedirects($resp)->assertElementExists('#html-editor');
}
public function test_markdown_setting_shows_markdown_editor()
public function test_markdown_setting_shows_markdown_editor_for_new_pages()
{
$this->setSettings(['app-editor' => 'markdown']);
$this->asAdmin()->get($this->page->getUrl() . '/edit')
$resp = $this->asAdmin()->get($this->page->book->getUrl('/create-page'));
$this->followRedirects($resp)
->assertElementNotExists('#html-editor')
->assertElementExists('#markdown-editor');
}
public function test_markdown_content_given_to_editor()
{
$this->setSettings(['app-editor' => 'markdown']);
$mdContent = '# hello. This is a test';
$this->page->markdown = $mdContent;
$this->page->editor = 'markdown';
$this->page->save();
$this->asAdmin()->get($this->page->getUrl() . '/edit')
$this->asAdmin()->get($this->page->getUrl('/edit'))
->assertElementContains('[name="markdown"]', $mdContent);
}
public function test_html_content_given_to_editor_if_no_markdown()
{
$this->setSettings(['app-editor' => 'markdown']);
$this->page->editor = 'markdown';
$this->page->save();
$this->asAdmin()->get($this->page->getUrl() . '/edit')
->assertElementContains('[name="markdown"]', $this->page->html);
}
@ -102,4 +105,102 @@ class PageEditorTest extends TestCase
$resp = $this->get($draft->getUrl('/edit'));
$resp->assertElementContains('a[href="' . $draft->getUrl() . '"]', 'Back');
}
public function test_switching_from_html_to_clean_markdown_works()
{
/** @var Page $page */
$page = Page::query()->first();
$page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
$page->save();
$resp = $this->asAdmin()->get($page->getUrl('/edit?editor=markdown-clean'));
$resp->assertStatus(200);
$resp->assertSee("## A Header\n\nSome **bold** content.");
$resp->assertElementExists('#markdown-editor');
}
public function test_switching_from_html_to_stable_markdown_works()
{
/** @var Page $page */
$page = Page::query()->first();
$page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
$page->save();
$resp = $this->asAdmin()->get($page->getUrl('/edit?editor=markdown-stable'));
$resp->assertStatus(200);
$resp->assertSee("<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>", true);
$resp->assertElementExists('[component="markdown-editor"]');
}
public function test_switching_from_markdown_to_wysiwyg_works()
{
/** @var Page $page */
$page = Page::query()->first();
$page->html = '';
$page->markdown = "## A Header\n\nSome content with **bold** text!";
$page->save();
$resp = $this->asAdmin()->get($page->getUrl('/edit?editor=wysiwyg'));
$resp->assertStatus(200);
$resp->assertElementExists('[component="wysiwyg-editor"]');
$resp->assertSee("<h2>A Header</h2>\n<p>Some content with <strong>bold</strong> text!</p>", true);
}
public function test_page_editor_changes_with_editor_property()
{
$resp = $this->asAdmin()->get($this->page->getUrl('/edit'));
$resp->assertElementExists('[component="wysiwyg-editor"]');
$this->page->markdown = "## A Header\n\nSome content with **bold** text!";
$this->page->editor = 'markdown';
$this->page->save();
$resp = $this->asAdmin()->get($this->page->getUrl('/edit'));
$resp->assertElementExists('[component="markdown-editor"]');
}
public function test_editor_type_switch_options_show()
{
$resp = $this->asAdmin()->get($this->page->getUrl('/edit'));
$editLink = $this->page->getUrl('/edit') . '?editor=';
$resp->assertElementContains("a[href=\"${editLink}markdown-clean\"]", '(Clean Content)');
$resp->assertElementContains("a[href=\"${editLink}markdown-stable\"]", '(Stable Content)');
$resp = $this->asAdmin()->get($this->page->getUrl('/edit?editor=markdown-stable'));
$editLink = $this->page->getUrl('/edit') . '?editor=';
$resp->assertElementContains("a[href=\"${editLink}wysiwyg\"]", 'Switch to WYSIWYG Editor');
}
public function test_editor_type_switch_options_dont_show_if_without_change_editor_permissions()
{
$resp = $this->asEditor()->get($this->page->getUrl('/edit'));
$editLink = $this->page->getUrl('/edit') . '?editor=';
$resp->assertElementNotExists("a[href*=\"${editLink}\"]");
}
public function test_page_editor_type_switch_does_not_work_without_change_editor_permissions()
{
/** @var Page $page */
$page = Page::query()->first();
$page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
$page->save();
$resp = $this->asEditor()->get($page->getUrl('/edit?editor=markdown-stable'));
$resp->assertStatus(200);
$resp->assertElementExists('[component="wysiwyg-editor"]');
$resp->assertElementNotExists('[component="markdown-editor"]');
}
public function test_page_save_does_not_change_active_editor_without_change_editor_permissions()
{
/** @var Page $page */
$page = Page::query()->first();
$page->html = '<h2>A Header</h2><p>Some <strong>bold</strong> content.</p>';
$page->editor = 'wysiwyg';
$page->save();
$this->asEditor()->put($page->getUrl(), ['name' => $page->name, 'markdown' => '## Updated content abc']);
$this->assertEquals('wysiwyg', $page->refresh()->editor);
}
}

View file

@ -203,4 +203,19 @@ class PageRevisionTest extends TestCase
$revisionCount = $page->revisions()->count();
$this->assertEquals(12, $revisionCount);
}
public function test_revision_list_shows_editor_type()
{
/** @var Page $page */
$page = Page::first();
$this->asAdmin()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html']);
$resp = $this->get($page->refresh()->getUrl('/revisions'));
$resp->assertElementContains('td', '(WYSIWYG)');
$resp->assertElementNotContains('td', '(Markdown)');
$this->asAdmin()->put($page->getUrl(), ['name' => 'Updated page', 'markdown' => '# Some markdown content']);
$resp = $this->get($page->refresh()->getUrl('/revisions'));
$resp->assertElementContains('td', '(Markdown)');
}
}