fix: increment composition revision on attachment add and remove
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
@@ -582,6 +582,7 @@ class CompositionManager {
|
|||||||
$deviceAttachmentsChanged = $uploads !== [];
|
$deviceAttachmentsChanged = $uploads !== [];
|
||||||
$documentAttachmentsChanged = count($failed) < count($documents);
|
$documentAttachmentsChanged = count($failed) < count($documents);
|
||||||
if ($deviceAttachmentsChanged || $documentAttachmentsChanged) {
|
if ($deviceAttachmentsChanged || $documentAttachmentsChanged) {
|
||||||
|
$composed['revision'] = (int)($composed['revision'] ?? 0) + 1;
|
||||||
$composed = $this->markRemoteDirty($composed);
|
$composed = $this->markRemoteDirty($composed);
|
||||||
$compositionChanged = true;
|
$compositionChanged = true;
|
||||||
}
|
}
|
||||||
@@ -616,6 +617,7 @@ class CompositionManager {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
if ($compositionChanged) {
|
if ($compositionChanged) {
|
||||||
|
$result['revision'] = (int)($composed['revision'] ?? 0);
|
||||||
$event = new CompositionSavedEvent($tenantId, $userId, $composition, (int)($composed['revision'] ?? 0));
|
$event = new CompositionSavedEvent($tenantId, $userId, $composition, (int)($composed['revision'] ?? 0));
|
||||||
$this->events->dispatch($event);
|
$this->events->dispatch($event);
|
||||||
}
|
}
|
||||||
@@ -774,8 +776,10 @@ class CompositionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unset($composed['attachments'][$identifier]);
|
unset($composed['attachments'][$identifier]);
|
||||||
|
$composed['revision'] = (int)($composed['revision'] ?? 0) + 1;
|
||||||
$composed = $this->markRemoteDirty($composed);
|
$composed = $this->markRemoteDirty($composed);
|
||||||
$result['disposition'] = 'removed';
|
$result['disposition'] = 'removed';
|
||||||
|
$result['revision'] = $composed['revision'];
|
||||||
return $composed;
|
return $composed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -380,6 +380,10 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
|||||||
source: attachment.source,
|
source: attachment.source,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (response.revision !== undefined) {
|
||||||
|
activeDraft.value.revision = response.revision
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addAttachmentsFromDocuments(items: { identifier: string; name: string; mimeType?: string; size?: number }[]) {
|
async function addAttachmentsFromDocuments(items: { identifier: string; name: string; mimeType?: string; size?: number }[]) {
|
||||||
@@ -432,6 +436,10 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (response.revision !== undefined) {
|
||||||
|
activeDraft.value.revision = response.revision
|
||||||
|
}
|
||||||
|
|
||||||
if (response.failed?.length) {
|
if (response.failed?.length) {
|
||||||
mailUiStore.notify(
|
mailUiStore.notify(
|
||||||
response.failed.length === 1
|
response.failed.length === 1
|
||||||
@@ -462,8 +470,11 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
|||||||
if (response.disposition === 'error') {
|
if (response.disposition === 'error') {
|
||||||
console.error('[Mail][Composer] Failed to remove attachment:', response.error)
|
console.error('[Mail][Composer] Failed to remove attachment:', response.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete activeDraft.value.attachments[attachment.identifier]
|
delete activeDraft.value.attachments[attachment.identifier]
|
||||||
|
if (response.revision !== undefined) {
|
||||||
|
activeDraft.value.revision = response.revision
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueSave(immediate: boolean) {
|
function queueSave(immediate: boolean) {
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export interface CompositionAttachmentAddResponse {
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
composition: string
|
composition: string
|
||||||
|
revision?: number
|
||||||
attachments: Record<string, CompositionAttachmentInterface>
|
attachments: Record<string, CompositionAttachmentInterface>
|
||||||
failed?: {
|
failed?: {
|
||||||
identifier: string
|
identifier: string
|
||||||
@@ -131,4 +132,5 @@ export interface CompositionAttachmentRemoveResponse {
|
|||||||
}
|
}
|
||||||
composition: string
|
composition: string
|
||||||
identifier: string
|
identifier: string
|
||||||
|
revision?: number
|
||||||
}
|
}
|
||||||
@@ -187,6 +187,7 @@ final class CompositionManagerTest extends TestCase {
|
|||||||
$this->assertSame('added', $result['disposition']);
|
$this->assertSame('added', $result['disposition']);
|
||||||
$this->assertCount(1, $result['attachments']);
|
$this->assertCount(1, $result['attachments']);
|
||||||
$this->assertSame([], $result['failed']);
|
$this->assertSame([], $result['failed']);
|
||||||
|
$this->assertSame(1, $result['revision']);
|
||||||
|
|
||||||
$attachment = $result['attachments']['attach-1'];
|
$attachment = $result['attachments']['attach-1'];
|
||||||
$this->assertSame('documents', $attachment['origin']);
|
$this->assertSame('documents', $attachment['origin']);
|
||||||
@@ -196,11 +197,58 @@ final class CompositionManagerTest extends TestCase {
|
|||||||
$this->assertSame(strlen('hello world'), $attachment['size']);
|
$this->assertSame(strlen('hello world'), $attachment['size']);
|
||||||
|
|
||||||
$snapshot = $this->compositionStore->compositionFetch(self::TENANT_ID, self::USER_ID, $compositionId);
|
$snapshot = $this->compositionStore->compositionFetch(self::TENANT_ID, self::USER_ID, $compositionId);
|
||||||
|
$this->assertSame(1, $snapshot['revision']);
|
||||||
$this->assertSame('dirty', $snapshot['remote']['status']);
|
$this->assertSame('dirty', $snapshot['remote']['status']);
|
||||||
$this->assertNull($snapshot['remote']['entity']);
|
$this->assertNull($snapshot['remote']['entity']);
|
||||||
$this->assertNull($snapshot['remote']['error']);
|
$this->assertNull($snapshot['remote']['error']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAttachmentAddIncrementsRevisionOnSubsequentAdd(): void {
|
||||||
|
$compositionId = $this->stageEmptyComposition();
|
||||||
|
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
||||||
|
$documentsManager->method('entityFetchBulk')->willReturn([
|
||||||
|
$this->makeEntity('default:personal:root:doc1', 'hello.txt', 'text/plain'),
|
||||||
|
$this->makeEntity('default:personal:root:doc2', 'world.txt', 'text/plain'),
|
||||||
|
]);
|
||||||
|
$documentsManager->method('entityReadStream')->willReturnCallback(
|
||||||
|
fn(): mixed => $this->makeStream('hello world'),
|
||||||
|
);
|
||||||
|
$manager = $this->makeManager($documentsManager);
|
||||||
|
|
||||||
|
$manager->attachmentAdd(self::TENANT_ID, self::USER_ID, $compositionId, [
|
||||||
|
'identifier' => 'attach-1',
|
||||||
|
'origin' => 'documents',
|
||||||
|
'source' => 'default:personal:root:doc1',
|
||||||
|
]);
|
||||||
|
$result = $manager->attachmentAdd(self::TENANT_ID, self::USER_ID, $compositionId, [
|
||||||
|
'identifier' => 'attach-2',
|
||||||
|
'origin' => 'documents',
|
||||||
|
'source' => 'default:personal:root:doc2',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(2, $result['revision']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAttachmentAddDoesNotChangeRevisionWhenNothingIsAdded(): void {
|
||||||
|
$compositionId = $this->stageEmptyComposition();
|
||||||
|
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
||||||
|
// documents_manager unavailable means the sourced attachment fails
|
||||||
|
// entirely, so no attachment is actually added to the composition.
|
||||||
|
$manager = $this->makeManager($documentsManager, documentsModuleEnabled: false);
|
||||||
|
|
||||||
|
$result = $manager->attachmentAdd(self::TENANT_ID, self::USER_ID, $compositionId, [
|
||||||
|
'identifier' => 'attach-1',
|
||||||
|
'origin' => 'documents',
|
||||||
|
'source' => 'default:personal:root:doc1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('error', $result['disposition']);
|
||||||
|
$this->assertArrayNotHasKey('revision', $result);
|
||||||
|
|
||||||
|
$snapshot = $this->compositionStore->compositionFetch(self::TENANT_ID, self::USER_ID, $compositionId);
|
||||||
|
$this->assertArrayNotHasKey('revision', $snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
public function testAttachmentAddFromDocumentsPartialFailure(): void {
|
public function testAttachmentAddFromDocumentsPartialFailure(): void {
|
||||||
$compositionId = $this->stageEmptyComposition();
|
$compositionId = $this->stageEmptyComposition();
|
||||||
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
||||||
@@ -285,6 +333,45 @@ final class CompositionManagerTest extends TestCase {
|
|||||||
$this->assertStringContainsString('not installed or enabled', $result['failed'][0]['message']);
|
$this->assertStringContainsString('not installed or enabled', $result['failed'][0]['message']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAttachmentRemoveIncrementsRevision(): void {
|
||||||
|
$compositionId = $this->stageEmptyComposition();
|
||||||
|
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
||||||
|
$documentsManager->method('entityFetchBulk')->willReturn([
|
||||||
|
$this->makeEntity('default:personal:root:doc1', 'hello.txt', 'text/plain'),
|
||||||
|
]);
|
||||||
|
$documentsManager->method('entityReadStream')->willReturn($this->makeStream('hello world'));
|
||||||
|
$manager = $this->makeManager($documentsManager);
|
||||||
|
|
||||||
|
$added = $manager->attachmentAdd(self::TENANT_ID, self::USER_ID, $compositionId, [
|
||||||
|
'identifier' => 'attach-1',
|
||||||
|
'origin' => 'documents',
|
||||||
|
'source' => 'default:personal:root:doc1',
|
||||||
|
]);
|
||||||
|
$this->assertSame(1, $added['revision']);
|
||||||
|
|
||||||
|
$result = $manager->attachmentRemove(self::TENANT_ID, self::USER_ID, $compositionId, 'attach-1');
|
||||||
|
|
||||||
|
$this->assertSame('removed', $result['disposition']);
|
||||||
|
$this->assertSame(2, $result['revision']);
|
||||||
|
|
||||||
|
$snapshot = $this->compositionStore->compositionFetch(self::TENANT_ID, self::USER_ID, $compositionId);
|
||||||
|
$this->assertSame(2, $snapshot['revision']);
|
||||||
|
$this->assertArrayNotHasKey('attach-1', $snapshot['attachments']);
|
||||||
|
$this->assertSame('dirty', $snapshot['remote']['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAttachmentRemoveNotFoundDoesNotChangeRevision(): void {
|
||||||
|
$compositionId = $this->stageEmptyComposition();
|
||||||
|
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
||||||
|
$manager = $this->makeManager($documentsManager);
|
||||||
|
|
||||||
|
$result = $manager->attachmentRemove(self::TENANT_ID, self::USER_ID, $compositionId, 'missing-attachment');
|
||||||
|
|
||||||
|
$this->assertSame('error', $result['disposition']);
|
||||||
|
$this->assertSame('attachment_not_found', $result['error']['type']);
|
||||||
|
$this->assertArrayNotHasKey('revision', $result);
|
||||||
|
}
|
||||||
|
|
||||||
private function makeManager(
|
private function makeManager(
|
||||||
Stub $documentsManager,
|
Stub $documentsManager,
|
||||||
bool $documentsModuleEnabled = true,
|
bool $documentsModuleEnabled = true,
|
||||||
|
|||||||
Reference in New Issue
Block a user