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;
class PageRevisionController extends Controller
{
public function __construct(
- protected PageRepo $pageRepo
+ protected PageRepo $pageRepo,
+ protected RevisionRepo $revisionRepo,
) {
}
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);
+ }
}
'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)',
'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',
// 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
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
// 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));
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)}`);
}, 2000);
}
- async discardDraft() {
+ async discardDraft(notify = true) {
let response;
try {
response = await window.$http.get(`/ajax/page/${this.pageId}`);
}
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,
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() {
this.changelogDisplay.innerText = summary;
}
- toggleDiscardDraftVisibility(show) {
- this.discardDraftWrap.classList.toggle('hidden', !show);
- }
-
async changeEditor(event) {
event.preventDefault();
*/
#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();
}
</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">
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--}}
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>
<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>
\ No newline at end of file
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']);
]);
}
+ 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();