Skip to content

Commit

Permalink
Merge pull request #6835 from getkirby/v5/lock-response
Browse files Browse the repository at this point in the history
New LockException and Lock Dialog
  • Loading branch information
bastianallgeier authored Dec 7, 2024
2 parents 32a02e2 + 334f130 commit 33d345f
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 38 deletions.
6 changes: 6 additions & 0 deletions i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@

"error.cache.type.invalid": "Invalid cache type \"{type}\"",

"error.content.lock.delete": "The version is locked and cannot be deleted",
"error.content.lock.move": "The source version is locked and cannot be moved",
"error.content.lock.publish": "This version is already published",
"error.content.lock.replace": "The version is locked and cannot be replaced",
"error.content.lock.update": "The version is locked and cannot be updated",

"error.email.preset.notFound": "The email preset \"{name}\" cannot be found",

"error.field.converter.invalid": "Invalid converter \"{converter}\"",
Expand Down
62 changes: 62 additions & 0 deletions panel/src/components/Dialogs/LockAlertDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<template>
<k-dialog
ref="dialog"
v-bind="$props"
:cancel-button="false"
:submit-button="{ theme: 'passive' }"
class="k-lock-alert-dialog"
@cancel="$emit('cancel')"
@submit="$emit('submit')"
>
<k-dialog-text :text="$t('form.locked')" />

<dl>
<div>
<dt><k-icon type="user" /></dt>
<dd>{{ lock.user.email }}</dd>
</div>
<div>
<dt><k-icon type="clock" /></dt>
<dd>
{{ $library.dayjs(lock.modified).format("YYYY-MM-DD HH:mm:ss") }}
</dd>
</div>
</dl>
</k-dialog>
</template>

<script>
import Dialog from "@/mixins/dialog.js";
export const props = {
mixins: [Dialog],
props: {
cancelButton: null,
submitButton: null,
lock: Object,
preview: String
}
};
export default {
mixins: [props]
};
</script>

<style>
.k-lock-alert-dialog dl {
margin: var(--spacing-6) 0 var(--spacing-2) 0;
}
.k-lock-alert-dialog dl div {
padding: var(--spacing-1) 0;
line-height: var(--leading-normal);
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--color-gray-500);
}
.k-lock-alert-dialog .k-dialog-buttons {
grid-template-columns: 1fr;
}
</style>
2 changes: 2 additions & 0 deletions panel/src/components/Dialogs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import FilesDialog from "./FilesDialog.vue";
import FormDialog from "./FormDialog.vue";
import LanguageDialog from "./LanguageDialog.vue";
import LicenseDialog from "./LicenseDialog.vue";
import LockAlertDialog from "./LockAlertDialog.vue";
import LinkDialog from "./LinkDialog.vue";
import ModelsDialog from "./ModelsDialog.vue";
import PageCreateDialog from "./PageCreateDialog.vue";
Expand All @@ -38,6 +39,7 @@ export default {
app.component("k-form-dialog", FormDialog);
app.component("k-license-dialog", LicenseDialog);
app.component("k-link-dialog", LinkDialog);
app.component("k-lock-alert-dialog", LockAlertDialog);
app.component("k-language-dialog", LanguageDialog);
app.component("k-models-dialog", ModelsDialog);
app.component("k-page-create-dialog", PageCreateDialog);
Expand Down
63 changes: 49 additions & 14 deletions panel/src/panel/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export default (panel) => {
}

this.emit("discard", {}, env);
} catch (error) {
// handle locked states
if (error.key.startsWith("error.content.lock")) {
return this.lockDialog(error.details);
}

// let our regular error handler take over
throw error;
} finally {
this.isProcessing = false;
}
Expand Down Expand Up @@ -124,6 +132,26 @@ export default (panel) => {
return panel.view.props.lock;
},

/**
* Opens the lock dialog to inform the current editor
* about edits from another user
*/
lockDialog(lock) {
this.dialog = panel.dialog;
this.dialog.open({
component: "k-lock-alert-dialog",
props: {
lock: lock
},
on: {
close: () => {
this.dialog = null;
panel.view.reload();
}
}
});
},

/**
* Merge new content changes with the
* original values and update the view props
Expand Down Expand Up @@ -153,12 +181,6 @@ export default (panel) => {
return;
}

// In the current view, we can use the existing
// lock state to determine if changes can be published
if (this.isCurrent(env) === true && this.isLocked(env) === true) {
throw new Error("Cannot publish locked changes");
}

this.isProcessing = true;

// Send updated values to API
Expand All @@ -175,6 +197,11 @@ export default (panel) => {

this.emit("publish", { values }, env);
} catch (error) {
// handle locked states
if (error.key.startsWith("error.content.lock")) {
return this.lockDialog(error.details);
}

this.retry("publish", error, [values, env]);
} finally {
this.isProcessing = false;
Expand Down Expand Up @@ -248,10 +275,6 @@ export default (panel) => {
* Saves any changes
*/
async save(values = {}, env = {}) {
if (this.isCurrent(env) === true && this.isLocked(env) === true) {
throw new Error("Cannot save locked changes");
}

this.isProcessing = true;

// ensure to abort unfinished previous save request
Expand All @@ -274,11 +297,23 @@ export default (panel) => {

this.emit("save", { values }, env);
} catch (error) {
// silent aborted requests, but throw all other errors
if (error.name !== "AbortError") {
this.isProcessing = false;
this.retry("save", error, [values, env]);
// handle aborted requests silently
if (error.name === "AbortError") {
return;
}

// processing must not be interrupted for aborted
// requests because the follow-up request is already
// in progress and setting the state to false here
// would be wrong
this.isProcessing = false;

// handle locked states
if (error.key.startsWith("error.content.lock")) {
return this.lockDialog(error.details);
}

this.retry("save", error, [values, env]);
}
},

Expand Down
31 changes: 31 additions & 0 deletions src/Content/LockedContentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Kirby\Content;

use Kirby\Exception\LogicException;

/**
* @package Kirby Content
* @author Bastian Allgeier <[email protected]>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class LockedContentException extends LogicException
{
protected static string $defaultKey = 'content.lock';
protected static string $defaultFallback = 'The version is locked';
protected static int $defaultHttpCode = 423;

public function __construct(
Lock $lock,
string|null $key = null,
string|null $message = null,
) {
parent::__construct(
message: $message,
key: $key,
details: $lock->toArray()
);
}
}
30 changes: 18 additions & 12 deletions src/Content/VersionRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ public static function delete(
Language $language
): void {
if ($version->isLocked('*') === true) {
throw new LogicException(
message: 'The version is locked and cannot be deleted'
throw new LockedContentException(
lock: $version->lock('*'),
key: 'content.lock.delete'
);
}
}
Expand All @@ -85,15 +86,17 @@ public static function move(

// check if the source version is locked in any language
if ($fromVersion->isLocked('*') === true) {
throw new LogicException(
message: 'The source version is locked and cannot be moved'
throw new LockedContentException(
lock: $fromVersion->lock('*'),
key: 'content.lock.move'
);
}

// check if the target version is locked in any language
if ($toVersion->isLocked('*') === true) {
throw new LogicException(
message: 'The target version is locked and cannot be overwritten'
throw new LockedContentException(
lock: $toVersion->lock('*'),
key: 'content.lock.update'
);
}
}
Expand All @@ -114,8 +117,9 @@ public static function publish(

// check if the version is locked in any language
if ($version->isLocked('*') === true) {
throw new LogicException(
message: 'The version is locked and cannot be published'
throw new LockedContentException(
lock: $version->lock('*'),
key: 'content.lock.publish'
);
}
}
Expand All @@ -137,8 +141,9 @@ public static function replace(

// check if the version is locked in any language
if ($version->isLocked('*') === true) {
throw new LogicException(
message: 'The version is locked and cannot be replaced'
throw new LockedContentException(
lock: $version->lock('*'),
key: 'content.lock.replace'
);
}
}
Expand All @@ -158,8 +163,9 @@ public static function update(
static::ensure($version, $language);

if ($version->isLocked('*') === true) {
throw new LogicException(
message: 'The version is locked and cannot be updated'
throw new LockedContentException(
lock: $version->lock('*'),
key: 'content.lock.update'
);
}
}
Expand Down
43 changes: 43 additions & 0 deletions tests/Content/LockedContentExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Kirby\Content;

use Kirby\Cms\User;

/**
* @coversDefaultClass \Kirby\Content\LockedContentException
*/
class LockedContentExceptionTest extends TestCase
{
public function testException()
{
$lock = new Lock(
user: new User(['username' => 'test']),
modified: $time = time()
);

$exception = new LockedContentException(
lock: $lock
);

$this->assertSame('The version is locked', $exception->getMessage());
$this->assertSame($lock->toArray(), $exception->getDetails());
$this->assertSame(423, $exception->getHttpCode());
$this->assertSame('error.content.lock', $exception->getKey());
}

public function testCustomMessage()
{
$lock = new Lock(
user: new User(['username' => 'test']),
modified: $time = time()
);

$exception = new LockedContentException(
lock: $lock,
message: $message = 'The version is locked and cannot be deleted'
);

$this->assertSame($message, $exception->getMessage());
}
}
Loading

0 comments on commit 33d345f

Please sign in to comment.