Skip to content

Commit b01bbf9

Browse files
committed
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.
1 parent f39938c commit b01bbf9

File tree

9 files changed

+101
-14
lines changed

9 files changed

+101
-14
lines changed

Diff for: app/Entities/Controllers/PageRevisionController.php

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use BookStack\Activity\ActivityType;
66
use BookStack\Entities\Models\PageRevision;
77
use BookStack\Entities\Repos\PageRepo;
8+
use BookStack\Entities\Repos\RevisionRepo;
89
use BookStack\Entities\Tools\PageContent;
910
use BookStack\Exceptions\NotFoundException;
1011
use BookStack\Facades\Activity;
@@ -16,7 +17,8 @@
1617
class PageRevisionController extends Controller
1718
{
1819
public function __construct(
19-
protected PageRepo $pageRepo
20+
protected PageRepo $pageRepo,
21+
protected RevisionRepo $revisionRepo,
2022
) {
2123
}
2224

@@ -154,4 +156,15 @@ public function destroy(string $bookSlug, string $pageSlug, int $revId)
154156

155157
return redirect($page->getUrl('/revisions'));
156158
}
159+
160+
/**
161+
* Destroys existing drafts, belonging to the current user, for the given page.
162+
*/
163+
public function destroyUserDraft(string $pageId)
164+
{
165+
$page = $this->pageRepo->getById($pageId);
166+
$this->revisionRepo->deleteDraftsForCurrentUser($page);
167+
168+
return response('', 200);
169+
}
157170
}

Diff for: lang/en/entities.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@
213213
'pages_editing_page' => 'Editing Page',
214214
'pages_edit_draft_save_at' => 'Draft saved at ',
215215
'pages_edit_delete_draft' => 'Delete Draft',
216+
'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.',
216217
'pages_edit_discard_draft' => 'Discard Draft',
217218
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
218219
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
@@ -285,7 +286,8 @@
285286
'time_b' => 'in the last :minCount minutes',
286287
'message' => ':start :time. Take care not to overwrite each other\'s updates!',
287288
],
288-
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
289+
'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',
290+
'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',
289291
'pages_specific' => 'Specific Page',
290292
'pages_is_template' => 'Page Template',
291293

Diff for: lang/en/errors.php

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858

5959
// Pages
6060
'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
61+
'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',
6162
'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',
6263

6364
// Entities

Diff for: resources/js/components/page-editor.js

+34-8
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,23 @@ export class PageEditor extends Component {
1919
this.saveDraftButton = this.$refs.saveDraft;
2020
this.discardDraftButton = this.$refs.discardDraft;
2121
this.discardDraftWrap = this.$refs.discardDraftWrap;
22+
this.deleteDraftButton = this.$refs.deleteDraft;
23+
this.deleteDraftWrap = this.$refs.deleteDraftWrap;
2224
this.draftDisplay = this.$refs.draftDisplay;
2325
this.draftDisplayIcon = this.$refs.draftDisplayIcon;
2426
this.changelogInput = this.$refs.changelogInput;
2527
this.changelogDisplay = this.$refs.changelogDisplay;
2628
this.changeEditorButtons = this.$manyRefs.changeEditor || [];
2729
this.switchDialogContainer = this.$refs.switchDialog;
30+
this.deleteDraftDialogContainer = this.$refs.deleteDraftDialog;
2831

2932
// Translations
3033
this.draftText = this.$opts.draftText;
3134
this.autosaveFailText = this.$opts.autosaveFailText;
3235
this.editingPageText = this.$opts.editingPageText;
3336
this.draftDiscardedText = this.$opts.draftDiscardedText;
37+
this.draftDeleteText = this.$opts.draftDeleteText;
38+
this.draftDeleteFailText = this.$opts.draftDeleteFailText;
3439
this.setChangelogText = this.$opts.setChangelogText;
3540

3641
// State data
@@ -75,6 +80,7 @@ export class PageEditor extends Component {
7580
// Draft Controls
7681
onSelect(this.saveDraftButton, this.saveDraft.bind(this));
7782
onSelect(this.discardDraftButton, this.discardDraft.bind(this));
83+
onSelect(this.deleteDraftButton, this.deleteDraft.bind(this));
7884

7985
// Change editor controls
8086
onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
@@ -119,7 +125,8 @@ export class PageEditor extends Component {
119125
try {
120126
const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
121127
if (!this.isNewDraft) {
122-
this.toggleDiscardDraftVisibility(true);
128+
this.discardDraftWrap.toggleAttribute('hidden', false);
129+
this.deleteDraftWrap.toggleAttribute('hidden', false);
123130
}
124131

125132
this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
@@ -154,7 +161,7 @@ export class PageEditor extends Component {
154161
}, 2000);
155162
}
156163

157-
async discardDraft() {
164+
async discardDraft(notify = true) {
158165
let response;
159166
try {
160167
response = await window.$http.get(`/ajax/page/${this.pageId}`);
@@ -168,7 +175,7 @@ export class PageEditor extends Component {
168175
}
169176

170177
this.draftDisplay.innerText = this.editingPageText;
171-
this.toggleDiscardDraftVisibility(false);
178+
this.discardDraftWrap.toggleAttribute('hidden', true);
172179
window.$events.emit('editor::replace', {
173180
html: response.data.html,
174181
markdown: response.data.markdown,
@@ -178,7 +185,30 @@ export class PageEditor extends Component {
178185
window.setTimeout(() => {
179186
this.startAutoSave();
180187
}, 1000);
181-
window.$events.emit('success', this.draftDiscardedText);
188+
189+
if (notify) {
190+
window.$events.success(this.draftDiscardedText);
191+
}
192+
}
193+
194+
async deleteDraft() {
195+
/** @var {ConfirmDialog} * */
196+
const dialog = window.$components.firstOnElement(this.deleteDraftDialogContainer, 'confirm-dialog');
197+
const confirmed = await dialog.show();
198+
if (!confirmed) {
199+
return;
200+
}
201+
202+
try {
203+
const discard = this.discardDraft(false);
204+
const draftDelete = window.$http.delete(`/page-revisions/user-drafts/${this.pageId}`);
205+
await Promise.all([discard, draftDelete]);
206+
window.$events.success(this.draftDeleteText);
207+
this.deleteDraftWrap.toggleAttribute('hidden', true);
208+
} catch (err) {
209+
console.error(err);
210+
window.$events.error(this.draftDeleteFailText);
211+
}
182212
}
183213

184214
updateChangelogDisplay() {
@@ -191,10 +221,6 @@ export class PageEditor extends Component {
191221
this.changelogDisplay.innerText = summary;
192222
}
193223

194-
toggleDiscardDraftVisibility(show) {
195-
this.discardDraftWrap.classList.toggle('hidden', !show);
196-
}
197-
198224
async changeEditor(event) {
199225
event.preventDefault();
200226

Diff for: resources/js/markdown/actions.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,9 @@ export class Actions {
433433
*/
434434
#setText(text, selectionRange = null) {
435435
selectionRange = selectionRange || this.#getSelectionRange();
436-
this.#dispatchChange(0, this.editor.cm.state.doc.length, text, selectionRange.from);
436+
const newDoc = this.editor.cm.state.toText(text);
437+
const newSelectFrom = Math.min(selectionRange.from, newDoc.length);
438+
this.#dispatchChange(0, this.editor.cm.state.doc.length, text, newSelectFrom);
437439
this.focus();
438440
}
439441

Diff for: resources/views/pages/parts/editor-toolbar.blade.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,22 @@ class="dropdown-container draft-display text {{ $draftsEnabled ? '' : 'hidden' }
2727
</a>
2828
</li>
2929
@endif
30-
<li refs="page-editor@discardDraftWrap" class="{{ $isDraftRevision ? '' : 'hidden' }}">
31-
<button refs="page-editor@discardDraft" type="button" class="text-neg icon-item">
30+
<li refs="page-editor@discard-draft-wrap" {{ $isDraftRevision ? '' : 'hidden' }}>
31+
<button refs="page-editor@discard-draft" type="button" class="text-warn icon-item">
3232
@icon('cancel')
3333
<div>{{ trans('entities.pages_edit_discard_draft') }}</div>
3434
</button>
3535
</li>
36+
<li refs="page-editor@delete-draft-wrap" {{ $isDraftRevision ? '' : 'hidden' }}>
37+
<button refs="page-editor@delete-draft" type="button" class="text-neg icon-item">
38+
@icon('delete')
39+
<div>{{ trans('entities.pages_edit_delete_draft') }}</div>
40+
</button>
41+
</li>
3642
@if(userCan('editor-change'))
43+
<li>
44+
<hr>
45+
</li>
3746
<li>
3847
@if($editor === 'wysiwyg')
3948
<a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-clean" refs="page-editor@changeEditor" class="icon-item">

Diff for: resources/views/pages/parts/form.blade.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
option:page-editor:autosave-fail-text="{{ trans('errors.page_draft_autosave_fail') }}"
1414
option:page-editor:editing-page-text="{{ trans('entities.pages_editing_page') }}"
1515
option:page-editor:draft-discarded-text="{{ trans('entities.pages_draft_discarded') }}"
16+
option:page-editor:draft-delete-text="{{ trans('entities.pages_draft_deleted') }}"
17+
option:page-editor:draft-delete-fail-text="{{ trans('errors.page_draft_delete_fail') }}"
1618
option:page-editor:set-changelog-text="{{ trans('entities.pages_edit_set_changelog') }}">
1719

1820
{{--Header Toolbar--}}
@@ -47,7 +49,7 @@
4749
class="text-link text-button hide-over-m page-save-mobile-button">@icon('save')</button>
4850

4951
{{--Editor Change Dialog--}}
50-
@component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switchDialog'])
52+
@component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switch-dialog'])
5153
<p>
5254
{{ trans('entities.pages_editor_switch_are_you_sure') }}
5355
<br>
@@ -60,4 +62,11 @@ class="text-link text-button hide-over-m page-save-mobile-button">@icon('save')<
6062
<li>{{ trans('entities.pages_editor_switch_consideration_c') }}</li>
6163
</ul>
6264
@endcomponent
65+
66+
{{--Delete Draft Dialog--}}
67+
@component('common.confirm-dialog', ['title' => trans('entities.pages_edit_delete_draft'), 'ref' => 'page-editor@delete-draft-dialog'])
68+
<p>
69+
{{ trans('entities.pages_edit_delete_draft_confirm') }}
70+
</p>
71+
@endcomponent
6372
</div>

Diff for: routes/web.php

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
Route::get('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', [EntityControllers\PageRevisionController::class, 'changes']);
107107
Route::put('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', [EntityControllers\PageRevisionController::class, 'restore']);
108108
Route::delete('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', [EntityControllers\PageRevisionController::class, 'destroy']);
109+
Route::delete('/page-revisions/user-drafts/{pageId}', [EntityControllers\PageRevisionController::class, 'destroyUserDraft']);
109110

110111
// Chapters
111112
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/create-page', [EntityControllers\PageController::class, 'create']);

Diff for: tests/Entity/PageDraftTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,30 @@ public function test_page_html_in_ajax_fetch_response()
166166
]);
167167
}
168168

169+
public function test_user_draft_removed_on_user_drafts_delete_call()
170+
{
171+
$editor = $this->users->editor();
172+
$page = $this->entities->page();
173+
174+
$this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
175+
'name' => $page->name,
176+
'html' => '<p>updated draft again</p>',
177+
]);
178+
179+
$revisionData = [
180+
'type' => 'update_draft',
181+
'created_by' => $editor->id,
182+
'page_id' => $page->id,
183+
];
184+
185+
$this->assertDatabaseHas('page_revisions', $revisionData);
186+
187+
$resp = $this->delete("/page-revisions/user-drafts/{$page->id}");
188+
189+
$resp->assertOk();
190+
$this->assertDatabaseMissing('page_revisions', $revisionData);
191+
}
192+
169193
public function test_updating_page_draft_with_markdown_retains_markdown_content()
170194
{
171195
$book = $this->entities->book();

0 commit comments

Comments
 (0)