mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-29 23:22:34 +01:00
Page Drafts: Added new "Delete Draft" action to draft menu
Provides a way for users to actually delte their user drafts where required. For #3927 Added test to cover new endpoint. Makes update to MD editor #setText so that new selection is within new range, otherwise it errors and fails operation.
This commit is contained in:
parent
f39938c4e3
commit
b01bbf9c89
@ -5,6 +5,7 @@ namespace BookStack\Entities\Controllers;
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Entities\Models\PageRevision;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use BookStack\Entities\Repos\RevisionRepo;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Facades\Activity;
|
||||
@ -16,7 +17,8 @@ use Ssddanbrown\HtmlDiff\Diff;
|
||||
class PageRevisionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected PageRepo $pageRepo
|
||||
protected PageRepo $pageRepo,
|
||||
protected RevisionRepo $revisionRepo,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -154,4 +156,15 @@ class PageRevisionController extends Controller
|
||||
|
||||
return redirect($page->getUrl('/revisions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys existing drafts, belonging to the current user, for the given page.
|
||||
*/
|
||||
public function destroyUserDraft(string $pageId)
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
$this->revisionRepo->deleteDraftsForCurrentUser($page);
|
||||
|
||||
return response('', 200);
|
||||
}
|
||||
}
|
||||
|
@ -213,6 +213,7 @@ return [
|
||||
'pages_editing_page' => 'Editing Page',
|
||||
'pages_edit_draft_save_at' => 'Draft saved at ',
|
||||
'pages_edit_delete_draft' => 'Delete Draft',
|
||||
'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',
|
||||
'pages_edit_discard_draft' => 'Discard Draft',
|
||||
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
|
||||
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
|
||||
@ -285,7 +286,8 @@ return [
|
||||
'time_b' => 'in the last :minCount minutes',
|
||||
'message' => ':start :time. Take care not to overwrite each other\'s updates!',
|
||||
],
|
||||
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
|
||||
'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',
|
||||
'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',
|
||||
'pages_specific' => 'Specific Page',
|
||||
'pages_is_template' => 'Page Template',
|
||||
|
||||
|
@ -58,6 +58,7 @@ return [
|
||||
|
||||
// Pages
|
||||
'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
|
||||
'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',
|
||||
'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',
|
||||
|
||||
// Entities
|
||||
|
@ -19,18 +19,23 @@ export class PageEditor extends Component {
|
||||
this.saveDraftButton = this.$refs.saveDraft;
|
||||
this.discardDraftButton = this.$refs.discardDraft;
|
||||
this.discardDraftWrap = this.$refs.discardDraftWrap;
|
||||
this.deleteDraftButton = this.$refs.deleteDraft;
|
||||
this.deleteDraftWrap = this.$refs.deleteDraftWrap;
|
||||
this.draftDisplay = this.$refs.draftDisplay;
|
||||
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;
|
||||
this.deleteDraftDialogContainer = this.$refs.deleteDraftDialog;
|
||||
|
||||
// Translations
|
||||
this.draftText = this.$opts.draftText;
|
||||
this.autosaveFailText = this.$opts.autosaveFailText;
|
||||
this.editingPageText = this.$opts.editingPageText;
|
||||
this.draftDiscardedText = this.$opts.draftDiscardedText;
|
||||
this.draftDeleteText = this.$opts.draftDeleteText;
|
||||
this.draftDeleteFailText = this.$opts.draftDeleteFailText;
|
||||
this.setChangelogText = this.$opts.setChangelogText;
|
||||
|
||||
// State data
|
||||
@ -75,6 +80,7 @@ export class PageEditor extends Component {
|
||||
// Draft Controls
|
||||
onSelect(this.saveDraftButton, this.saveDraft.bind(this));
|
||||
onSelect(this.discardDraftButton, this.discardDraft.bind(this));
|
||||
onSelect(this.deleteDraftButton, this.deleteDraft.bind(this));
|
||||
|
||||
// Change editor controls
|
||||
onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
|
||||
@ -119,7 +125,8 @@ export class PageEditor extends Component {
|
||||
try {
|
||||
const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
|
||||
if (!this.isNewDraft) {
|
||||
this.toggleDiscardDraftVisibility(true);
|
||||
this.discardDraftWrap.toggleAttribute('hidden', false);
|
||||
this.deleteDraftWrap.toggleAttribute('hidden', false);
|
||||
}
|
||||
|
||||
this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
|
||||
@ -154,7 +161,7 @@ export class PageEditor extends Component {
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async discardDraft() {
|
||||
async discardDraft(notify = true) {
|
||||
let response;
|
||||
try {
|
||||
response = await window.$http.get(`/ajax/page/${this.pageId}`);
|
||||
@ -168,7 +175,7 @@ export class PageEditor extends Component {
|
||||
}
|
||||
|
||||
this.draftDisplay.innerText = this.editingPageText;
|
||||
this.toggleDiscardDraftVisibility(false);
|
||||
this.discardDraftWrap.toggleAttribute('hidden', true);
|
||||
window.$events.emit('editor::replace', {
|
||||
html: response.data.html,
|
||||
markdown: response.data.markdown,
|
||||
@ -178,7 +185,30 @@ export class PageEditor extends Component {
|
||||
window.setTimeout(() => {
|
||||
this.startAutoSave();
|
||||
}, 1000);
|
||||
window.$events.emit('success', this.draftDiscardedText);
|
||||
|
||||
if (notify) {
|
||||
window.$events.success(this.draftDiscardedText);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDraft() {
|
||||
/** @var {ConfirmDialog} * */
|
||||
const dialog = window.$components.firstOnElement(this.deleteDraftDialogContainer, 'confirm-dialog');
|
||||
const confirmed = await dialog.show();
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const discard = this.discardDraft(false);
|
||||
const draftDelete = window.$http.delete(`/page-revisions/user-drafts/${this.pageId}`);
|
||||
await Promise.all([discard, draftDelete]);
|
||||
window.$events.success(this.draftDeleteText);
|
||||
this.deleteDraftWrap.toggleAttribute('hidden', true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
window.$events.error(this.draftDeleteFailText);
|
||||
}
|
||||
}
|
||||
|
||||
updateChangelogDisplay() {
|
||||
@ -191,10 +221,6 @@ export class PageEditor extends Component {
|
||||
this.changelogDisplay.innerText = summary;
|
||||
}
|
||||
|
||||
toggleDiscardDraftVisibility(show) {
|
||||
this.discardDraftWrap.classList.toggle('hidden', !show);
|
||||
}
|
||||
|
||||
async changeEditor(event) {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -433,7 +433,9 @@ export class Actions {
|
||||
*/
|
||||
#setText(text, selectionRange = null) {
|
||||
selectionRange = selectionRange || this.#getSelectionRange();
|
||||
this.#dispatchChange(0, this.editor.cm.state.doc.length, text, selectionRange.from);
|
||||
const newDoc = this.editor.cm.state.toText(text);
|
||||
const newSelectFrom = Math.min(selectionRange.from, newDoc.length);
|
||||
this.#dispatchChange(0, this.editor.cm.state.doc.length, text, newSelectFrom);
|
||||
this.focus();
|
||||
}
|
||||
|
||||
|
@ -27,13 +27,22 @@
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<li refs="page-editor@discardDraftWrap" class="{{ $isDraftRevision ? '' : 'hidden' }}">
|
||||
<button refs="page-editor@discardDraft" type="button" class="text-neg icon-item">
|
||||
<li refs="page-editor@discard-draft-wrap" {{ $isDraftRevision ? '' : 'hidden' }}>
|
||||
<button refs="page-editor@discard-draft" type="button" class="text-warn icon-item">
|
||||
@icon('cancel')
|
||||
<div>{{ trans('entities.pages_edit_discard_draft') }}</div>
|
||||
</button>
|
||||
</li>
|
||||
<li refs="page-editor@delete-draft-wrap" {{ $isDraftRevision ? '' : 'hidden' }}>
|
||||
<button refs="page-editor@delete-draft" type="button" class="text-neg icon-item">
|
||||
@icon('delete')
|
||||
<div>{{ trans('entities.pages_edit_delete_draft') }}</div>
|
||||
</button>
|
||||
</li>
|
||||
@if(userCan('editor-change'))
|
||||
<li>
|
||||
<hr>
|
||||
</li>
|
||||
<li>
|
||||
@if($editor === 'wysiwyg')
|
||||
<a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-clean" refs="page-editor@changeEditor" class="icon-item">
|
||||
|
@ -13,6 +13,8 @@
|
||||
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:draft-delete-text="{{ trans('entities.pages_draft_deleted') }}"
|
||||
option:page-editor:draft-delete-fail-text="{{ trans('errors.page_draft_delete_fail') }}"
|
||||
option:page-editor:set-changelog-text="{{ trans('entities.pages_edit_set_changelog') }}">
|
||||
|
||||
{{--Header Toolbar--}}
|
||||
@ -47,7 +49,7 @@
|
||||
class="text-link 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'])
|
||||
@component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switch-dialog'])
|
||||
<p>
|
||||
{{ trans('entities.pages_editor_switch_are_you_sure') }}
|
||||
<br>
|
||||
@ -60,4 +62,11 @@
|
||||
<li>{{ trans('entities.pages_editor_switch_consideration_c') }}</li>
|
||||
</ul>
|
||||
@endcomponent
|
||||
|
||||
{{--Delete Draft Dialog--}}
|
||||
@component('common.confirm-dialog', ['title' => trans('entities.pages_edit_delete_draft'), 'ref' => 'page-editor@delete-draft-dialog'])
|
||||
<p>
|
||||
{{ trans('entities.pages_edit_delete_draft_confirm') }}
|
||||
</p>
|
||||
@endcomponent
|
||||
</div>
|
@ -106,6 +106,7 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', [EntityControllers\PageRevisionController::class, 'changes']);
|
||||
Route::put('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', [EntityControllers\PageRevisionController::class, 'restore']);
|
||||
Route::delete('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', [EntityControllers\PageRevisionController::class, 'destroy']);
|
||||
Route::delete('/page-revisions/user-drafts/{pageId}', [EntityControllers\PageRevisionController::class, 'destroyUserDraft']);
|
||||
|
||||
// Chapters
|
||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/create-page', [EntityControllers\PageController::class, 'create']);
|
||||
|
@ -166,6 +166,30 @@ class PageDraftTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_user_draft_removed_on_user_drafts_delete_call()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$page = $this->entities->page();
|
||||
|
||||
$this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
|
||||
'name' => $page->name,
|
||||
'html' => '<p>updated draft again</p>',
|
||||
]);
|
||||
|
||||
$revisionData = [
|
||||
'type' => 'update_draft',
|
||||
'created_by' => $editor->id,
|
||||
'page_id' => $page->id,
|
||||
];
|
||||
|
||||
$this->assertDatabaseHas('page_revisions', $revisionData);
|
||||
|
||||
$resp = $this->delete("/page-revisions/user-drafts/{$page->id}");
|
||||
|
||||
$resp->assertOk();
|
||||
$this->assertDatabaseMissing('page_revisions', $revisionData);
|
||||
}
|
||||
|
||||
public function test_updating_page_draft_with_markdown_retains_markdown_content()
|
||||
{
|
||||
$book = $this->entities->book();
|
||||
|
Loading…
Reference in New Issue
Block a user