Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e24d08595d |
@@ -440,14 +440,7 @@ class CompositionManager {
|
||||
];
|
||||
}
|
||||
|
||||
// Reuse the synced remote draft (if any) so the provider can natively clean it up
|
||||
$remoteEntity = $this->remoteEntityIdentifier($composed['remote']['entity'] ?? null);
|
||||
$source = ($remoteEntity !== null
|
||||
&& $remoteEntity->provider() === $service->provider()
|
||||
&& (string)$remoteEntity->service() === (string)$service->identifier())
|
||||
? $remoteEntity
|
||||
: null;
|
||||
|
||||
$source = null;
|
||||
$properties = $this->buildMessageProperties(
|
||||
service: $service,
|
||||
sender: $senderObject,
|
||||
@@ -472,8 +465,6 @@ class CompositionManager {
|
||||
];
|
||||
}
|
||||
|
||||
$this->compositionStore->compositionDiscard($tenantId, $userId, $identifier);
|
||||
|
||||
return [
|
||||
'identifier' => $identifier,
|
||||
'disposition' => 'sent'
|
||||
@@ -591,7 +582,6 @@ class CompositionManager {
|
||||
$deviceAttachmentsChanged = $uploads !== [];
|
||||
$documentAttachmentsChanged = count($failed) < count($documents);
|
||||
if ($deviceAttachmentsChanged || $documentAttachmentsChanged) {
|
||||
$composed['revision'] = (int)($composed['revision'] ?? 0) + 1;
|
||||
$composed = $this->markRemoteDirty($composed);
|
||||
$compositionChanged = true;
|
||||
}
|
||||
@@ -626,7 +616,6 @@ class CompositionManager {
|
||||
];
|
||||
}
|
||||
if ($compositionChanged) {
|
||||
$result['revision'] = (int)($composed['revision'] ?? 0);
|
||||
$event = new CompositionSavedEvent($tenantId, $userId, $composition, (int)($composed['revision'] ?? 0));
|
||||
$this->events->dispatch($event);
|
||||
}
|
||||
@@ -785,10 +774,8 @@ class CompositionManager {
|
||||
}
|
||||
|
||||
unset($composed['attachments'][$identifier]);
|
||||
$composed['revision'] = (int)($composed['revision'] ?? 0) + 1;
|
||||
$composed = $this->markRemoteDirty($composed);
|
||||
$result['disposition'] = 'removed';
|
||||
$result['revision'] = $composed['revision'];
|
||||
return $composed;
|
||||
};
|
||||
|
||||
|
||||
@@ -80,8 +80,8 @@ final class CompositionController extends ControllerAbstract {
|
||||
if (!isset($data['action']) || !is_string($data['action'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: action must be a string');
|
||||
}
|
||||
if (!in_array($data['action'], ['fresh', 'reply', 'replyAll', 'forward'], true)) {
|
||||
throw new InvalidArgumentException('Invalid parameter: action must be one of fresh, reply, replyAll, forward');
|
||||
if (!in_array($data['action'], ['fresh', 'reply', 'forward'], true)) {
|
||||
throw new InvalidArgumentException('Invalid parameter: action must be one of fresh, reply, forward');
|
||||
}
|
||||
if (!isset($data['sender']) || !is_array($data['sender'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: sender must be an array');
|
||||
@@ -89,8 +89,8 @@ final class CompositionController extends ControllerAbstract {
|
||||
if (!isset($data['message']) || !is_array($data['message'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: message must be an array');
|
||||
}
|
||||
if (($data['action'] === 'reply' || $data['action'] === 'replyAll' || $data['action'] === 'forward') && (!isset($data['source']) || !is_string($data['source']) || $data['source'] === '')) {
|
||||
throw new InvalidArgumentException('Invalid parameter: source must be a non-empty string for reply, replyAll, or forward action');
|
||||
if (($data['action'] === 'reply' || $data['action'] === 'forward') && (!isset($data['source']) || !is_string($data['source']) || $data['source'] === '')) {
|
||||
throw new InvalidArgumentException('Invalid parameter: source must be a non-empty string for reply or forward action');
|
||||
}
|
||||
|
||||
return $this->compositionManager->stage($tenantId, $userId, $data['identifier'], $data['action'], $data['sender'], $data['message'], $data['source'] ?? null);
|
||||
|
||||
+12
-7
@@ -4,14 +4,13 @@ namespace KTXM\Mail;
|
||||
|
||||
use KTXF\Event\DeliveryMode;
|
||||
use KTXF\Event\EventListenerRegistrarInterface;
|
||||
use KTXF\Module\Configuration\BrowserModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ModuleContextInterface;
|
||||
use KTXF\Module\ModuleBrowserInterface;
|
||||
use KTXF\Module\ModuleInstanceAbstract;
|
||||
|
||||
/**
|
||||
* Mail Module - Email Client
|
||||
*/
|
||||
class Module extends ModuleInstanceAbstract
|
||||
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
@@ -80,10 +79,16 @@ class Module extends ModuleInstanceAbstract
|
||||
];
|
||||
}
|
||||
|
||||
public function configure(ModuleContextInterface $context): void
|
||||
public function registerBI(): array
|
||||
{
|
||||
if ($context instanceof BrowserModuleContextInterface) {
|
||||
$context->registerModule($this, 'Mail', 'static/module.mjs');
|
||||
}
|
||||
return [
|
||||
'handle' => $this->handle(),
|
||||
'namespace' => 'Mail',
|
||||
'version' => $this->version(),
|
||||
'label' => $this->label(),
|
||||
'author' => $this->author(),
|
||||
'description' => $this->description(),
|
||||
'boot' => 'static/module.mjs',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+157
-157
@@ -795,49 +795,49 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.31.3.tgz",
|
||||
"integrity": "sha512-Cz50pvciQrxdSxgTkHOVz0uD0Yl/8Xt0QatGD6ILm47jW8EzyHR9RkUGs/D5IqzXKuVPntfw1ttaT926vXfiRg==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.30.2.tgz",
|
||||
"integrity": "sha512-QbZC/s1OOqcoUdkhIY16TjR/gCtR0qAk9e4bJwUqOJqZuv5ozqCL5hzWm22jjTPp6c6Ei2tPd6t30VwfIKW4lQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/pm": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-blockquote": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.31.3.tgz",
|
||||
"integrity": "sha512-fyY2XMbyDDDfOTQ1Qdrnqa1qwC9DWE4n7AfE0EKQI0G8MfLV8RaDlLDcOZDJ9JbMPY7/Gx7EjyK4NKxSW9n2hQ==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.30.2.tgz",
|
||||
"integrity": "sha512-BOkwhZenek7vzXBOgKppSrlx4YryBdAYu1p1MXKn0R9A9eNmE2HVhmm0gG49+E8BhsE/TG8wKVclwET42JJiIg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bold": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.31.3.tgz",
|
||||
"integrity": "sha512-dIuYhKk8TitKU/FeDpoTeWZhU42YgDN5npgWNjAmMmRktPdoxnH3/wGSiwXlqZgJWjehN7kPWDePWpJeAImGpQ==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.30.2.tgz",
|
||||
"integrity": "sha512-MsvJhPgYejY2D9MhwYJv8AmscozvLBI8qtJ7YLdYZBWkMR4bgmxHq5+xqEfBsao9bOMMwBon9p3+P+/Tq5ReWA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bubble-menu": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.31.3.tgz",
|
||||
"integrity": "sha512-EV6ZnwKc++2OM/OcD54n8s1B7C9LP7GKAtdEPwu0t3BYJf3sG8Ueoinf7SgGPO9oMPg96GneOkDNm1urMV167g==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.30.2.tgz",
|
||||
"integrity": "sha512-oS0WiWNXHKpiPYMkcnHm1j7iEvufTGGtLFtcJJd3olb5OS6V2acoXVDL0nNJDmRQ32K3QXut3fbRcMseMxT+lw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -848,80 +848,80 @@
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bullet-list": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.31.3.tgz",
|
||||
"integrity": "sha512-qEyyoPapPef4LO8XKaN83bxtaNzkJ4kFn/IxLnEKd4BJ3Mvi4MH2yJYlyDqwFnMoev4pMA1zBHRAt/C0THRmZw==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.30.2.tgz",
|
||||
"integrity": "sha512-+awIL/TUz4aB3rL68igU1rWfaaoBIAkPcakkktkRq8gYf0bd9eSb48P6kHkpx/3q+JyK7g9vsnltLNHNh6twnA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.31.3"
|
||||
"@tiptap/extension-list": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.31.3.tgz",
|
||||
"integrity": "sha512-SzxOqchrD2AcN3uT67PjKmRFEMOU3vNNiwNaamZJUbZrI6Hmy+bvdHJrc3jrIddyyCrAtsnAfRI0cmorW1jGfg==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.30.2.tgz",
|
||||
"integrity": "sha512-r8EZk3R9yGpF6v5xxafAU1HwrD/e+RpbfnmVi2TeB/ZHAsVO62fW96E32G0t6IdaCtOFtAd85hkDAaOfCT4yGg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.31.3.tgz",
|
||||
"integrity": "sha512-nvknt4FhyJQjYcvxptmeUlFsIAc8ibua3E5BN4Pim374/9RWepH4cdE9X0/qUTtguHElLx0iKtSIY3rW2qGTFA==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.30.2.tgz",
|
||||
"integrity": "sha512-9otGKaQZmePHrLXFtCtz+BYDn5z4sSumTkUqQIQHz0gVxwPoTi7g51RedwxvViTb/zu2XV5ROXYLHIxKxypMPg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.31.3.tgz",
|
||||
"integrity": "sha512-EexgmqnyDNyGlISxo7SMrp5MygpJYmqD+0cY5jB6L1U6L4CpKKRWUt8OO1sWzEHDE1+TTvwt+WIFoIWAziOtEA==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.30.2.tgz",
|
||||
"integrity": "sha512-+xIv67V+/2L1uvz98FAT5W7kWEfHwfNV3MD7b4UsKPU0lhcCWuVOXy0JB8yYmdNExqpI7xT9g3MWzREoBvBQSg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-dropcursor": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.31.3.tgz",
|
||||
"integrity": "sha512-NWomSfu5CSC7VacnMSDzKT8qm66SzMfZwVPEtwY5bPpRTJgTiT1rNK0neDrrzfMN27MfylGyKWWf7Q5Qf8w/fg==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.30.2.tgz",
|
||||
"integrity": "sha512-nyRKUmItATnKI9AiRChmjhcBbCEsNxRu+AaCz+cx8EvnAcNHsVRdNYL5PmBs3WlNA/Et4Eb2DG0hVQDnNF61eg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "3.31.3"
|
||||
"@tiptap/extensions": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-floating-menu": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.31.3.tgz",
|
||||
"integrity": "sha512-rd4VJ9PGSP9Eop8ZTEwaLZcMzMXLuJKe3hUNf58rq+zANpwM+9fI+vB7g9MAc3eXwUNDxNDVACFIL2oeBqpQ3A==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.30.2.tgz",
|
||||
"integrity": "sha512-A8PLvvh8W6PUMrqh+EpBerxm+Ucr0irGxJvwAnzYQmNGNIJ9U4OVgw4OcEU+9JH0gMmEzDcHzMPP2s/s1lIcyw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
@@ -930,80 +930,80 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-gapcursor": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.31.3.tgz",
|
||||
"integrity": "sha512-EBXKb1FrVStsNYCcRGtd9jmzveCvR+eqgg1rVqoONrqFK6U7bga6LN+1dMKroP1kliDVgveYkP3vRYxqw+rFqg==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.30.2.tgz",
|
||||
"integrity": "sha512-7Xk0ut6FM+RAsvKxDN3bAtk7zvYZ6Aa8pawJ6s7dLAmLR9JwrZevlcL4FSrj4bR7rqKOj92RYCFxzgTEbWimag==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "3.31.3"
|
||||
"@tiptap/extensions": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-hard-break": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.31.3.tgz",
|
||||
"integrity": "sha512-QAdCvNO4+yW9ATwsrej11NTkDYFqPLIEQr3ARNrKOK1qaiS7A0fia2SEukb/hrkP3A6mbozhoQt2r2RGUf/DpQ==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.30.2.tgz",
|
||||
"integrity": "sha512-IxSNgmG3d4OZdUTeebrOI7SxdIWXXJqlcGiSNDabWqxipUitfy3mZ3gDDE6G01koKxZRbhz4KIplAZlpxnTFSg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-heading": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.31.3.tgz",
|
||||
"integrity": "sha512-rk5VHMAeQcg06SLauN6EGdD2jc0O2qY8QkZYPd0LxNvLblw2BxBx+lxUQSYwLAT9Ie5914gKIK2YbRyO2Ts3ig==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.30.2.tgz",
|
||||
"integrity": "sha512-PblDvgSJ05p1t6hzyPi02xeiBjB0M2abReoGEImqSWCy79UqnAGacgsZo4EeEawtJV1NEP8chhvmX+nRtzdT1A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.31.3.tgz",
|
||||
"integrity": "sha512-YnHGy2KShRwvCseAmmxl9VP7R0qaj8QMp3DA6DJWZqp7r5gLGvDkAqhedxqqefqsE4Y43hjkBdjtB9Ce78LkIw==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.30.2.tgz",
|
||||
"integrity": "sha512-j8aswLTsuEdJKC62DF+kw0EgvIRL7QMUyAVp2fdjR0qgM0ZVlEwCC4qIEq3kK9tFVU4kRtQ5BSj/jn6QwrlbCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-italic": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.31.3.tgz",
|
||||
"integrity": "sha512-ibGvdvAPyfxBMUVNRI43eb9h2/Jka1MRG5GtnGqbcCX/2+Y/y0EOfFrPQPkuYphiWQYyu9+FzPKMB/pjuaLuKQ==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.30.2.tgz",
|
||||
"integrity": "sha512-pp8uaiuXsUbLm5rYzR1jlWbwm1mAahRajdHwAKBtthFRB2rDvC7ZWhKaCSoKhZvfIDRmu9/B67+uAHoutL0dCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-link": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.31.3.tgz",
|
||||
"integrity": "sha512-986wOQzTL9Zr5lf84LCLpm+YOms8A0K39/8DVoqRfebqcOe0/eq4bnztmAlfabOd+kJY92g3AgZERFUx/w+dcw==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.30.2.tgz",
|
||||
"integrity": "sha512-jwdcymKcrbFpj5hRAuGVLCq8FieVkGFnENyroYmvkad+XAt8ZLy/MTFYRN6SK3ukH6PZMY7H4iObGtciQaC5nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"linkifyjs": "^4.3.3"
|
||||
@@ -1013,159 +1013,159 @@
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.31.3.tgz",
|
||||
"integrity": "sha512-LoveGnC0FVdCV4jUNBaG1ZA+KWE07+adzV3kGy6uUYFcJEjbVUHTnPDrBOob3IwOSO3sCwIkvQb6EVYeXn/4yg==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.30.2.tgz",
|
||||
"integrity": "sha512-MIUpo1Bd9Rf1Qg+TNYNwDZ4xsfFeQahjU9Xhy6UcaszKQzbAM7KCzn5BObNytK1NdcqNHsC8Wj5vFvMKEzrXdw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-item": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.31.3.tgz",
|
||||
"integrity": "sha512-4QlKOriJJMvg95QJTEsy9BUYPBQ6UvJyb8WURRwdUtQUkkqb8h32lg/eyQUv2FzW9IT1AdUyNZfVaxH64kRfQA==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.30.2.tgz",
|
||||
"integrity": "sha512-HWgRCRlGxulE+hN1VUcnWD6P2NE08VBgGtcaxOfdXVqaI93BCK6AhRQZGpLsfKgajLk+5DXTBraaitwnBqzCxg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.31.3"
|
||||
"@tiptap/extension-list": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-keymap": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.31.3.tgz",
|
||||
"integrity": "sha512-If8UOEdDZbPJU6iYTvLtH6DOp2KBy6BKxg9UELL1AevVetGHEF/7lW8hP50Gn1tHMVpPRqmhSzVrJRpEJJgb/Q==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.30.2.tgz",
|
||||
"integrity": "sha512-TTve3WOlQaYu1ahMqsQ/T0wzaxfgZvcOl3/OuPyInOi8QtxXhqGhFjmYe5jOr56G9W2QDuFWVsZecVwfDte9zg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.31.3"
|
||||
"@tiptap/extension-list": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-ordered-list": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.31.3.tgz",
|
||||
"integrity": "sha512-mp3g11NgA/PYu8rj7J7Ez3l4qBy6WfTSmHIG4PZvEGG5w2oUAIkgb9DU7nPPzjmeme27oazFYZw+AtZA0+u4tw==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.30.2.tgz",
|
||||
"integrity": "sha512-Z7OO1HcF0idda1n6vodXeQ3h2ylN9JR4IfIGUYkar5Xl9JusK8PDETTBQZQn//96p49I2d+GoWsD2LXPtjHXXg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.31.3"
|
||||
"@tiptap/extension-list": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-paragraph": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.31.3.tgz",
|
||||
"integrity": "sha512-+iPku7wJfy5hbNDNLX8dveFtYVsZMmh7vztjuq8hT3mipSC4IByDehDbU8fzUCjfXiEzmI7mQn7c8LGmHULuzA==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.30.2.tgz",
|
||||
"integrity": "sha512-ulEu3LNt+kPVAWEnrhoz13Fs8Q/v/8NUxQbAeteuBchQ8joxJXuWExhpy1fUfZir5+b+W5z7/NesgPjZQfv47w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-placeholder": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.31.3.tgz",
|
||||
"integrity": "sha512-9jYtR8ELEw7GVaruyrm4oFkPcjig9Q+crc+dpmarhBNXUmxagCdlhVzNwCJ2WJRzvBAtx59sEYqNTU38Wx8S3A==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.30.2.tgz",
|
||||
"integrity": "sha512-Bj1seUvPCoRrD/LpzMoKD+jQIjxuc+oq931GpPq4wobSUUbD4pF/0NMwpCLpiHO19QnTQz8+9p2dqPdlc44LHA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "3.31.3"
|
||||
"@tiptap/extensions": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-strike": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.31.3.tgz",
|
||||
"integrity": "sha512-G29bhKttYwcKHT+BI6emWVFol3RO/gUXxQVcmr/iT8LXXy7j8J6HFUnsKM+Kg5YlP1rxMRgsa65dbrQVahZi0A==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.30.2.tgz",
|
||||
"integrity": "sha512-fBLxMXz6hYIURzLOD+/L6aVATztsKham00ANWmGi13vN0hx2lQMYZffN+gR+QqiCDfMQxBXzrf5a7tJuDiQHLQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.31.3.tgz",
|
||||
"integrity": "sha512-gdsWtF+taeaCu6V+5Ct10fGo0ACUy1GnYtbb+mcathBt8OqbT+Ws60p/yEmKesBDz2Hn+B5IcWy6+2BBZl5ZTg==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.30.2.tgz",
|
||||
"integrity": "sha512-n/iZnirgRmXet6f97kolAnP3j8DsgLSiTbz/KLWc8eBYiFmkjRzkuisOm5xuGdfGIxwpB4x3tlSF4ef4DLnbRg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text-align": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.31.3.tgz",
|
||||
"integrity": "sha512-26BJ7c28EyvHzJovNpuenKiIgfRI4KsT/OjBB00y58MyZ0kRXc6dwZVclw8y66clupKTLTWYCDUsy0P/xHj+cA==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.30.2.tgz",
|
||||
"integrity": "sha512-zexiz0uJlX2KzeAyYI/uEoBsl0Zw3Ua0CTV/kRKTnk7K9vCOnwvJXtXdkHA4WC12ACqUsPnyc4yMTwDR22fKFw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-underline": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.31.3.tgz",
|
||||
"integrity": "sha512-HghdJaOwRqYzsAxqSyNyb+IWyOMcdCl8IoiBETA9BZCJAqdXzFLcuWp7CqCqJPam9dgkWosSoHqTCAO4nTBfpw==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.30.2.tgz",
|
||||
"integrity": "sha512-SZiTMnvqXcnrtJX+X25ZbYsuDO83haGOVMBD/O+mAWYNYXhaSc5Rkph5czzItxrd+Yyp/vs4PiwD7XTNbfqmpA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
"@tiptap/core": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extensions": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.31.3.tgz",
|
||||
"integrity": "sha512-8sJNPGGUe8f3aDojcOW5cfVL7I5NrBbE0UWxG08qoi9Tea6qWbvQJsCR9tsrOapr/DaLr3kpbGZ9s1gEEfcNcA==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.30.2.tgz",
|
||||
"integrity": "sha512-2LqAHXk26QDsryW+beECxYeBzv5Ylk4GuB3cOmfghS7/G37R2W+Te3TkUK7BT0EWoDryvBT57/5q0DEFIhfZZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.31.3.tgz",
|
||||
"integrity": "sha512-sZime0SWsz/k62W2WvHx5Ig7G2h7kVhrrmnqy+wEgIHfDwEfOlelRjaWCiBCFlF7dxGUntJusCh9FxlLhni0Ag==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.30.2.tgz",
|
||||
"integrity": "sha512-BJN8tUx4ppFN3R3cV/FJfrJbJkvo1lj4uciq+nwpjwzdRvFzqIuglWf+HLcJ6CwlYpLOHp7ArgkBg4Q5e60Gog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.4.1",
|
||||
@@ -1180,7 +1180,7 @@
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-tables": "^1.8.5",
|
||||
"prosemirror-transform": "^1.12.0",
|
||||
"prosemirror-view": "^1.42.3"
|
||||
"prosemirror-view": "^1.41.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -1188,35 +1188,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.31.3.tgz",
|
||||
"integrity": "sha512-WKof9RewdmGHvWJ1wn0/HVNG2mV+HOgVRyJkKekuM9fgr6BZAAH/xZsWE1eon+94JnQ+KtK2ThydXQM/qc6b2A==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.30.2.tgz",
|
||||
"integrity": "sha512-fJSrhW1CyD4sjYA20evSP4Cp13B/HhbxCdM974K0xpHOVqvCtNU9w2s9hfq9mg2yGoU7MSNHKNYMkJjIi2/Xyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/extension-blockquote": "3.31.3",
|
||||
"@tiptap/extension-bold": "3.31.3",
|
||||
"@tiptap/extension-bullet-list": "3.31.3",
|
||||
"@tiptap/extension-code": "3.31.3",
|
||||
"@tiptap/extension-code-block": "3.31.3",
|
||||
"@tiptap/extension-document": "3.31.3",
|
||||
"@tiptap/extension-dropcursor": "3.31.3",
|
||||
"@tiptap/extension-gapcursor": "3.31.3",
|
||||
"@tiptap/extension-hard-break": "3.31.3",
|
||||
"@tiptap/extension-heading": "3.31.3",
|
||||
"@tiptap/extension-horizontal-rule": "3.31.3",
|
||||
"@tiptap/extension-italic": "3.31.3",
|
||||
"@tiptap/extension-link": "3.31.3",
|
||||
"@tiptap/extension-list": "3.31.3",
|
||||
"@tiptap/extension-list-item": "3.31.3",
|
||||
"@tiptap/extension-list-keymap": "3.31.3",
|
||||
"@tiptap/extension-ordered-list": "3.31.3",
|
||||
"@tiptap/extension-paragraph": "3.31.3",
|
||||
"@tiptap/extension-strike": "3.31.3",
|
||||
"@tiptap/extension-text": "3.31.3",
|
||||
"@tiptap/extension-underline": "3.31.3",
|
||||
"@tiptap/extensions": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/extension-blockquote": "3.30.2",
|
||||
"@tiptap/extension-bold": "3.30.2",
|
||||
"@tiptap/extension-bullet-list": "3.30.2",
|
||||
"@tiptap/extension-code": "3.30.2",
|
||||
"@tiptap/extension-code-block": "3.30.2",
|
||||
"@tiptap/extension-document": "3.30.2",
|
||||
"@tiptap/extension-dropcursor": "3.30.2",
|
||||
"@tiptap/extension-gapcursor": "3.30.2",
|
||||
"@tiptap/extension-hard-break": "3.30.2",
|
||||
"@tiptap/extension-heading": "3.30.2",
|
||||
"@tiptap/extension-horizontal-rule": "3.30.2",
|
||||
"@tiptap/extension-italic": "3.30.2",
|
||||
"@tiptap/extension-link": "3.30.2",
|
||||
"@tiptap/extension-list": "3.30.2",
|
||||
"@tiptap/extension-list-item": "3.30.2",
|
||||
"@tiptap/extension-list-keymap": "3.30.2",
|
||||
"@tiptap/extension-ordered-list": "3.30.2",
|
||||
"@tiptap/extension-paragraph": "3.30.2",
|
||||
"@tiptap/extension-strike": "3.30.2",
|
||||
"@tiptap/extension-text": "3.30.2",
|
||||
"@tiptap/extension-underline": "3.30.2",
|
||||
"@tiptap/extensions": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -1224,22 +1224,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/vue-3": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.31.3.tgz",
|
||||
"integrity": "sha512-PNt0+rm7BXzhe/U+xlNnXBeJ6t9x2mE3902VXeIeEBxu5CrYLCJ0a5yHrof1jU3KwJrLOe/BtlGWjbq4qnTTfQ==",
|
||||
"version": "3.30.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.30.2.tgz",
|
||||
"integrity": "sha512-7TCahsK19ChY60FvgseTuzaz67yR1tkzTbKW6s3iXx9NTiU411C6WEQAS/EtPt4dsGDv/esFjSc9EMKilRhY5g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tiptap/extension-bubble-menu": "^3.31.3",
|
||||
"@tiptap/extension-floating-menu": "^3.31.3"
|
||||
"@tiptap/extension-bubble-menu": "^3.30.2",
|
||||
"@tiptap/extension-floating-menu": "^3.30.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3",
|
||||
"@tiptap/core": "3.30.2",
|
||||
"@tiptap/pm": "3.30.2",
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
@@ -3155,9 +3155,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.42.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.3.tgz",
|
||||
"integrity": "sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==",
|
||||
"version": "1.42.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.1.tgz",
|
||||
"integrity": "sha512-rRqzZnRgkyh69XoOMrfFJHwauHscLBmHbq772kwbic1ymQAM8gXjzEbJse5j1ep2UO2HRIAQL0bY3kZ/RoqjVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.25.8",
|
||||
|
||||
@@ -12,7 +12,6 @@ import type { CollectionObject, MessageAddressObject } from '@MailManager/models
|
||||
import { useMailOperationsStore } from '@/stores/mailOperationsStore'
|
||||
import { useMailCompositionStore } from '@/stores/mailCompositionStore'
|
||||
import { ComposerMode } from '@/types/composer'
|
||||
import type { ComposerFormat } from '@/types/composer'
|
||||
import { documentsSelectorComponent } from '@/services/documentsSelector'
|
||||
import ComposerToolbar from '@/components/composer/ComposerToolbar.vue'
|
||||
import ComposerSender from '@/components/composer/ComposerSender.vue'
|
||||
@@ -49,8 +48,6 @@ const {
|
||||
// State
|
||||
const showCc = ref(false)
|
||||
const showBcc = ref(false)
|
||||
const format = ref<ComposerFormat>('rich')
|
||||
const textEdited = ref(false)
|
||||
const applyingDraftToEditor = ref(false)
|
||||
const showDocumentSelector = ref(false)
|
||||
const showCloseDialog = ref(false)
|
||||
@@ -88,14 +85,6 @@ const subject = computed({
|
||||
|
||||
const attachments = computed(() => activeDraft.value?.attachments ?? {})
|
||||
|
||||
const text = computed({
|
||||
get: () => activeDraft.value?.message.body.text ?? '',
|
||||
set: value => {
|
||||
textEdited.value = true
|
||||
compositionStore.updateBody({ text: value, html: textToHtml(value) })
|
||||
},
|
||||
})
|
||||
|
||||
// Initialize Tiptap editor
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
@@ -129,8 +118,6 @@ watch(
|
||||
compositionStore.openDraft(props.mode, props.source)
|
||||
showCc.value = (activeDraft.value?.message.cc.length ?? 0) > 0
|
||||
showBcc.value = (activeDraft.value?.message.bcc.length ?? 0) > 0
|
||||
format.value = 'rich'
|
||||
textEdited.value = false
|
||||
|
||||
applyingDraftToEditor.value = true
|
||||
currentEditor.commands.setContent(activeDraft.value?.message.body.html || '')
|
||||
@@ -262,42 +249,6 @@ const toggleLink = () => {
|
||||
const isActive = (name: string, attrs?: any) => {
|
||||
return editor.value?.isActive(name, attrs) || false
|
||||
}
|
||||
|
||||
const toggleFormat = () => {
|
||||
if (format.value === 'rich') {
|
||||
if (editor.value) {
|
||||
compositionStore.updateBody({
|
||||
html: activeDraft.value?.message.body.html ?? '',
|
||||
text: editor.value.getText(),
|
||||
})
|
||||
}
|
||||
textEdited.value = false
|
||||
format.value = 'plain'
|
||||
return
|
||||
}
|
||||
|
||||
format.value = 'rich'
|
||||
if (textEdited.value && editor.value) {
|
||||
applyingDraftToEditor.value = true
|
||||
editor.value.commands.setContent(activeDraft.value?.message.body.html || '')
|
||||
applyingDraftToEditor.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function textToHtml(text: string): string {
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map(line => `<p>${escapeHtml(line) || '<br>'}</p>`)
|
||||
.join('')
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -346,8 +297,6 @@ function escapeHtml(value: string): string {
|
||||
|
||||
<ComposerEditor
|
||||
:editor="editor"
|
||||
:format="format"
|
||||
:text="text"
|
||||
:is-bold-active="isActive('bold')"
|
||||
:is-italic-active="isActive('italic')"
|
||||
:is-underline-active="isActive('underline')"
|
||||
@@ -360,8 +309,6 @@ function escapeHtml(value: string): string {
|
||||
@bullet-list="toggleBulletList"
|
||||
@ordered-list="toggleOrderedList"
|
||||
@link="toggleLink"
|
||||
@toggle-format="toggleFormat"
|
||||
@update:text="text = $event"
|
||||
@attach-from-device="handleAttachFromDevice"
|
||||
@attach-from-documents="handleAttachFromDocuments"
|
||||
/>
|
||||
|
||||
@@ -16,14 +16,12 @@ interface Props {
|
||||
selectionMode?: boolean
|
||||
selectedCollection?: CollectionObject | null
|
||||
loading?: boolean
|
||||
searchActive?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
selectionList: () => [],
|
||||
selectionMode: false,
|
||||
searchActive: false,
|
||||
})
|
||||
|
||||
// Emits
|
||||
@@ -374,10 +372,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="messages.length === 0" class="pa-8 text-center">
|
||||
<v-icon size="64" color="grey-lighten-1">{{ searchActive ? 'mdi-email-search-outline' : 'mdi-email-outline' }}</v-icon>
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-email-outline</v-icon>
|
||||
<div class="text-h6 mt-4 text-medium-emphasis">No messages</div>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
{{ searchActive ? 'No messages match your search' : 'This folder is empty' }}
|
||||
This folder is empty
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { usePreviewStore, useUser } from '@KTXC'
|
||||
import { useUser } from '@KTXC'
|
||||
import { useMailUiStore } from '@/stores/mailUiStore'
|
||||
import type { EntityObject, MessageObject } from '@MailManager/models'
|
||||
import { SecurityLevel } from '@/utile/emailSanitizer'
|
||||
@@ -19,7 +19,6 @@ const props = defineProps<{
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
reply: [entity: EntityObject]
|
||||
replyAll: [entity: EntityObject]
|
||||
forward: [entity: EntityObject]
|
||||
move: [entity: EntityObject]
|
||||
delete: [entity: EntityObject]
|
||||
@@ -29,7 +28,6 @@ const emit = defineEmits<{
|
||||
|
||||
// User settings
|
||||
const { getSetting } = useUser()
|
||||
const previewStore = usePreviewStore()
|
||||
const mailUiStore = useMailUiStore()
|
||||
|
||||
// Security overrides
|
||||
@@ -66,7 +64,6 @@ const effectiveSecurityLevel = computed(() => {
|
||||
// Preview state
|
||||
const previewDialog = computed(() => mediaViewerComponent('dialog'))
|
||||
const previewItems = computed<MediaViewerItem[]>(() => {
|
||||
if (!previewStore.availability.storage) return []
|
||||
const attachments = message.value?.attachments ?? []
|
||||
const items: MediaViewerItem[] = []
|
||||
attachments.forEach((attachment, index) => {
|
||||
@@ -110,12 +107,6 @@ const handleReply = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleReplyAll = () => {
|
||||
if (props.entity) {
|
||||
emit('replyAll', props.entity)
|
||||
}
|
||||
}
|
||||
|
||||
const handleForward = () => {
|
||||
if (props.entity) {
|
||||
emit('forward', props.entity)
|
||||
@@ -161,7 +152,7 @@ const handlePreview = (index: number) => {
|
||||
|
||||
const handleViewerRetrieve = (item: MediaViewerItem): Promise<Blob> => {
|
||||
if (!props.entity) return Promise.reject(new Error('No message selected'))
|
||||
return mailUiStore.attachmentPreviewBlob(props.entity, item.meta.attachment, 'fullscreen')
|
||||
return mailUiStore.attachmentBlob(props.entity, item.meta.attachment)
|
||||
}
|
||||
|
||||
const handleViewerDownload = (item: MediaViewerItem) => {
|
||||
@@ -185,7 +176,6 @@ const handleViewerDownload = (item: MediaViewerItem) => {
|
||||
:security-level="effectiveSecurityLevel"
|
||||
:is-security-overridden="overrideSecurityLevel !== null"
|
||||
@reply="handleReply"
|
||||
@reply-all="handleReplyAll"
|
||||
@forward="handleForward"
|
||||
@move="handleMove"
|
||||
@delete="handleDelete"
|
||||
|
||||
@@ -9,14 +9,6 @@ defineProps({
|
||||
type: Object as PropType<Editor | null>,
|
||||
default: null,
|
||||
},
|
||||
format: {
|
||||
type: String as PropType<'rich' | 'plain'>,
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isBoldActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
@@ -50,8 +42,6 @@ const emit = defineEmits<{
|
||||
bulletList: []
|
||||
orderedList: []
|
||||
link: []
|
||||
toggleFormat: []
|
||||
'update:text': [value: string]
|
||||
attachFromDevice: [files: File[]]
|
||||
attachFromDocuments: []
|
||||
}>()
|
||||
@@ -76,84 +66,68 @@ function handleFileChange(event: Event) {
|
||||
|
||||
<template>
|
||||
<v-toolbar density="compact" elevation="0" class="editor-toolbar">
|
||||
<template v-if="format === 'rich'">
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isBoldActive }"
|
||||
@click="$emit('bold')"
|
||||
>
|
||||
<v-icon>mdi-format-bold</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Bold</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isItalicActive }"
|
||||
@click="$emit('italic')"
|
||||
>
|
||||
<v-icon>mdi-format-italic</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Italic</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isUnderlineActive }"
|
||||
@click="$emit('underline')"
|
||||
>
|
||||
<v-icon>mdi-format-underline</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Underline</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-divider vertical class="mx-2" />
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isBulletListActive }"
|
||||
@click="$emit('bulletList')"
|
||||
>
|
||||
<v-icon>mdi-format-list-bulleted</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Bullet List</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isOrderedListActive }"
|
||||
@click="$emit('orderedList')"
|
||||
>
|
||||
<v-icon>mdi-format-list-numbered</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Numbered List</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-divider vertical class="mx-2" />
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isLinkActive }"
|
||||
@click="$emit('link')"
|
||||
>
|
||||
<v-icon>mdi-link</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Link</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-divider vertical class="mx-2" />
|
||||
</template>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isBoldActive }"
|
||||
@click="$emit('bold')"
|
||||
>
|
||||
<v-icon>mdi-format-bold</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Bold</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': format === 'plain' }"
|
||||
@click="$emit('toggleFormat')"
|
||||
:class="{ 'v-btn--active': isItalicActive }"
|
||||
@click="$emit('italic')"
|
||||
>
|
||||
<v-icon>{{ format === 'plain' ? 'mdi-format-text' : 'mdi-code-tags' }}</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">
|
||||
{{ format === 'plain' ? 'Rich Text' : 'Plain Text' }}
|
||||
</v-tooltip>
|
||||
<v-icon>mdi-format-italic</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Italic</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isUnderlineActive }"
|
||||
@click="$emit('underline')"
|
||||
>
|
||||
<v-icon>mdi-format-underline</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Underline</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-divider vertical class="mx-2" />
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isBulletListActive }"
|
||||
@click="$emit('bulletList')"
|
||||
>
|
||||
<v-icon>mdi-format-list-bulleted</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Bullet List</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isOrderedListActive }"
|
||||
@click="$emit('orderedList')"
|
||||
>
|
||||
<v-icon>mdi-format-list-numbered</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Numbered List</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-divider vertical class="mx-2" />
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isLinkActive }"
|
||||
@click="$emit('link')"
|
||||
>
|
||||
<v-icon>mdi-link</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Link</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-spacer />
|
||||
@@ -180,17 +154,7 @@ function handleFileChange(event: Event) {
|
||||
<v-divider />
|
||||
|
||||
<div class="editor-container">
|
||||
<EditorContent v-if="format === 'rich'" :editor="editor ?? undefined" />
|
||||
<v-textarea
|
||||
v-else
|
||||
:model-value="text"
|
||||
variant="plain"
|
||||
auto-grow
|
||||
hide-details
|
||||
placeholder="Write your message..."
|
||||
class="text-editor pa-4"
|
||||
@update:model-value="$emit('update:text', $event)"
|
||||
/>
|
||||
<EditorContent :editor="editor ?? undefined" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -215,11 +179,6 @@ function handleFileChange(event: Event) {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.text-editor :deep(textarea) {
|
||||
min-height: 300px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
:deep(.tiptap-editor) {
|
||||
outline: none;
|
||||
min-height: 300px;
|
||||
|
||||
@@ -30,7 +30,7 @@ defineEmits<{
|
||||
</v-btn>
|
||||
|
||||
<v-toolbar-title>
|
||||
{{ mode === ComposerMode.Reply ? 'Reply' : mode === ComposerMode.ReplyAll ? 'Reply All' : mode === ComposerMode.Forward ? 'Forward' : 'New Message' }}
|
||||
{{ mode === ComposerMode.Reply ? 'Reply' : mode === ComposerMode.Forward ? 'Forward' : 'New Message' }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { usePreviewStore } from '@KTXC'
|
||||
import RecipientDetails from '@/components/common/RecipientDetails.vue'
|
||||
import { formatFileSize } from '@/utile/format'
|
||||
import { mediaViewerCanDisplay, mediaViewerComponent } from '@/services/mediaViewer'
|
||||
@@ -28,9 +27,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const mailUiStore = useMailUiStore()
|
||||
const previewStore = usePreviewStore()
|
||||
const previewsAvailable = computed(() => previewStore.availability.storage)
|
||||
const previewPopover = computed(() => previewsAvailable.value ? mediaViewerComponent('popover') : null)
|
||||
const previewPopover = computed(() => mediaViewerComponent('popover'))
|
||||
|
||||
const message = computed(() => {
|
||||
return props.entity?.properties ?? null
|
||||
@@ -51,7 +48,7 @@ const attachmentItems = computed<AttachmentItem[]>(() =>
|
||||
)
|
||||
|
||||
const isPreviewable = (attachment: MessagePartInterface): boolean => {
|
||||
return previewsAvailable.value && mediaViewerCanDisplay(attachment.type)
|
||||
return mediaViewerCanDisplay(attachment.type)
|
||||
}
|
||||
|
||||
const handleAttachmentClick = (attachment: MessagePartInterface, index: number): void => {
|
||||
@@ -64,7 +61,7 @@ const handleAttachmentClick = (attachment: MessagePartInterface, index: number):
|
||||
|
||||
const handleViewerRetrieve = (item: AttachmentItem): Promise<Blob> => {
|
||||
if (!props.entity) return Promise.reject(new Error('No message selected'))
|
||||
return mailUiStore.attachmentPreviewBlob(props.entity, item.meta.attachment, 'inline')
|
||||
return mailUiStore.attachmentBlob(props.entity, item.meta.attachment)
|
||||
}
|
||||
|
||||
// Format date for display
|
||||
|
||||
@@ -13,7 +13,6 @@ const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
reply: []
|
||||
replyAll: []
|
||||
forward: []
|
||||
move: []
|
||||
delete: []
|
||||
@@ -48,7 +47,7 @@ const currentSecurityLevel = computed(() => {
|
||||
<v-btn
|
||||
icon="mdi-reply-all"
|
||||
variant="text"
|
||||
@click="emit('replyAll')"
|
||||
@click="emit('reply')"
|
||||
>
|
||||
<v-icon>mdi-reply-all</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Reply All</v-tooltip>
|
||||
|
||||
+11
-16
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, unref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, unref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useModuleStore } from '@KTXC'
|
||||
@@ -41,8 +41,6 @@ const {
|
||||
loading,
|
||||
selectedMessage,
|
||||
currentMessages,
|
||||
searchResults,
|
||||
searching,
|
||||
} = storeToRefs(mailOperationsStore)
|
||||
|
||||
const {
|
||||
@@ -76,19 +74,21 @@ const {
|
||||
// Complex store/composable objects accessed directly (not simple refs)
|
||||
const { mailSync, entitiesStore } = mailOperationsStore
|
||||
|
||||
// Message search — debounced server-side search of the selected folder
|
||||
// Message search (filters the current message list by subject/sender)
|
||||
const searchQuery = ref('')
|
||||
|
||||
watch(searchQuery, query => mailOperationsStore.searchMessages(query))
|
||||
const filteredMessages = computed(() => {
|
||||
const query = (searchQuery.value ?? '').trim().toLowerCase()
|
||||
if (!query) return currentMessages.value
|
||||
|
||||
// Reset the search box when the folder changes so it doesn't show a stale
|
||||
// query for a folder whose results are no longer being filtered.
|
||||
watch(selectedFolder, () => {
|
||||
searchQuery.value = ''
|
||||
return currentMessages.value.filter((message: EntityObject) => {
|
||||
const subject = (message.properties.subject || '').toLowerCase()
|
||||
const fromLabel = (message.properties.from?.label || '').toLowerCase()
|
||||
const fromAddress = (message.properties.from?.address || '').toLowerCase()
|
||||
return subject.includes(query) || fromLabel.includes(query) || fromAddress.includes(query)
|
||||
})
|
||||
})
|
||||
|
||||
const filteredMessages = computed(() => searchResults.value ?? currentMessages.value)
|
||||
|
||||
const lastSyncLabel = computed(() => {
|
||||
const lastSync = unref(unref(mailSync.lastSync))
|
||||
|
||||
@@ -167,8 +167,6 @@ const handleMessageComposeFresh = () => mailUiStore.openComposer()
|
||||
|
||||
const handleMessageComposeReply = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.Reply)
|
||||
|
||||
const handleMessageComposeReplyAll = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.ReplyAll)
|
||||
|
||||
const handleMessageComposeForward = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.Forward)
|
||||
|
||||
const handleMessageComposeClose = () => mailUiStore.closeComposer()
|
||||
@@ -252,7 +250,6 @@ const handleMessageSelectionDelete = () => mailUiStore.deleteSelectedMessages()
|
||||
variant="outlined"
|
||||
placeholder="Search messages..."
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:loading="searching"
|
||||
hide-details
|
||||
single-line
|
||||
clearable
|
||||
@@ -311,7 +308,6 @@ const handleMessageSelectionDelete = () => mailUiStore.deleteSelectedMessages()
|
||||
:selection-list="selectionList"
|
||||
:selection-mode="selectionMode"
|
||||
:loading="loading"
|
||||
:search-active="searchResults !== null"
|
||||
@open="handleMessageOpen"
|
||||
@reply="handleMessageComposeReply"
|
||||
@forward="handleMessageComposeForward"
|
||||
@@ -343,7 +339,6 @@ const handleMessageSelectionDelete = () => mailUiStore.deleteSelectedMessages()
|
||||
:entity="selectedMessage"
|
||||
@compose="handleMessageComposeFresh"
|
||||
@reply="handleMessageComposeReply"
|
||||
@reply-all="handleMessageComposeReplyAll"
|
||||
@forward="handleMessageComposeForward"
|
||||
@move="handleMessageMove"
|
||||
@delete="handleMessageDelete"
|
||||
|
||||
@@ -118,8 +118,7 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
||||
return
|
||||
}
|
||||
|
||||
const ownAddresses = senderIdentities.value.map(identity => identity.address)
|
||||
activeDraft.value = buildDraft(mode, sender, source, ownAddresses)
|
||||
activeDraft.value = buildDraft(mode, sender, source)
|
||||
activeDraft.value.action = mode
|
||||
|
||||
stageDraft()
|
||||
@@ -381,10 +380,6 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
||||
source: attachment.source,
|
||||
}
|
||||
})
|
||||
|
||||
if (response.revision !== undefined) {
|
||||
activeDraft.value.revision = response.revision
|
||||
}
|
||||
}
|
||||
|
||||
async function addAttachmentsFromDocuments(items: { identifier: string; name: string; mimeType?: string; size?: number }[]) {
|
||||
@@ -437,10 +432,6 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
||||
}
|
||||
})
|
||||
|
||||
if (response.revision !== undefined) {
|
||||
activeDraft.value.revision = response.revision
|
||||
}
|
||||
|
||||
if (response.failed?.length) {
|
||||
mailUiStore.notify(
|
||||
response.failed.length === 1
|
||||
@@ -471,11 +462,8 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
||||
if (response.disposition === 'error') {
|
||||
console.error('[Mail][Composer] Failed to remove attachment:', response.error)
|
||||
}
|
||||
|
||||
|
||||
delete activeDraft.value.attachments[attachment.identifier]
|
||||
if (response.revision !== undefined) {
|
||||
activeDraft.value.revision = response.revision
|
||||
}
|
||||
}
|
||||
|
||||
function queueSave(immediate: boolean) {
|
||||
@@ -559,9 +547,9 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
||||
}
|
||||
})
|
||||
|
||||
function buildDraft(mode: ComposerMode, sender: ComposerSenderIdentity, source: EntityObject | MessageAddressObject | null | undefined, ownAddresses: string[]): ComposerDraft {
|
||||
function buildDraft(mode: ComposerMode, sender: ComposerSenderIdentity, source?: EntityObject | MessageAddressObject | null): ComposerDraft {
|
||||
const composition = createIdentifier()
|
||||
const freshMessage = buildMessage(mode, source, ownAddresses)
|
||||
const freshMessage = buildMessage(mode, source)
|
||||
const freshAttachments = buildAttachments(composition, mode, source)
|
||||
const sourceIdentifier = source instanceof EntityObject ? source.identifier : null
|
||||
return {
|
||||
@@ -577,7 +565,7 @@ function buildDraft(mode: ComposerMode, sender: ComposerSenderIdentity, source:
|
||||
}
|
||||
}
|
||||
|
||||
function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressObject | null | undefined, ownAddresses: string[] = []): ComposerDraftMessage {
|
||||
function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressObject | null | undefined): ComposerDraftMessage {
|
||||
|
||||
if (!source) {
|
||||
return emptyMessage()
|
||||
@@ -604,28 +592,12 @@ function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressO
|
||||
const sentAt = sourceMessage.sent || source.created || ''
|
||||
const sentLabel = sentAt ? new Date(sentAt).toLocaleString() : 'an unknown time'
|
||||
|
||||
if (mode === ComposerMode.Reply || mode === ComposerMode.ReplyAll) {
|
||||
if (mode === ComposerMode.Reply) {
|
||||
const replyToAddr = sourceMessage.replyTo?.[0] ?? sourceMessage.from ?? null
|
||||
const replyToAddress = replyToAddr?.address || ''
|
||||
const to: MessageAddressInterface[] = replyToAddress ? [{ address: replyToAddress, label: replyToAddr?.label }] : []
|
||||
|
||||
let cc: MessageAddressInterface[] = []
|
||||
if (mode === ComposerMode.ReplyAll) {
|
||||
const excluded = new Set([replyToAddress.toLowerCase(), ...ownAddresses.map(addr => addr.toLowerCase())])
|
||||
const seen = new Set<string>()
|
||||
cc = [...(sourceMessage.to || []), ...(sourceMessage.cc || [])].filter(recipient => {
|
||||
const address = recipient.address?.toLowerCase() || ''
|
||||
if (!address || excluded.has(address) || seen.has(address)) {
|
||||
return false
|
||||
}
|
||||
seen.add(address)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
to,
|
||||
cc,
|
||||
to: replyToAddress ? [{ address: replyToAddress, label: replyToAddr?.label }] : [],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
subject: /^Re:/i.test(originalSubject) ? originalSubject : `Re: ${originalSubject}`,
|
||||
body: {
|
||||
@@ -648,7 +620,7 @@ function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressO
|
||||
}
|
||||
|
||||
function buildAttachments(composition: string, mode: ComposerMode, source?: EntityObject | MessageAddressObject | null): Record<string, ComposerDraftAttachment> {
|
||||
if (mode === ComposerMode.Fresh || mode === ComposerMode.Reply || mode === ComposerMode.ReplyAll || !(source instanceof EntityObject)) {
|
||||
if (mode === ComposerMode.Fresh || mode === ComposerMode.Reply || !(source instanceof EntityObject)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
|
||||
import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
|
||||
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
||||
import { useMailSync } from '@MailManager/composables/useMailSync'
|
||||
import type { ServiceIdentifier, CollectionIdentifier, EntityIdentifier, ListFilter } from '@MailManager/types/common'
|
||||
import { ListFilterComparisonOperator, ListFilterConjunctionOperator } from '@MailManager/types/common'
|
||||
import type { ServiceIdentifier, CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
||||
import type { EntityTransmitRequest, EntityBlobSelector } from '@MailManager/types/entity'
|
||||
import type { MessageAddressInterface, MessageInterface, MessagePartInterface } from '@MailManager/types/message'
|
||||
import { ServiceObject, type CollectionObject, type EntityObject } from '@MailManager/models'
|
||||
@@ -51,8 +50,6 @@ export const useMailOperationsStore = defineStore('mailOperationsStore', () => {
|
||||
|
||||
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
||||
const selectedMessage = shallowRef<EntityObject | null>(null)
|
||||
const searchResults = shallowRef<EntityObject[] | null>(null)
|
||||
const searching = ref(false)
|
||||
const composerSaving = ref(false)
|
||||
const composerSending = ref(false)
|
||||
const composerLastSaved = ref<Date | null>(null)
|
||||
@@ -296,7 +293,6 @@ export const useMailOperationsStore = defineStore('mailOperationsStore', () => {
|
||||
}
|
||||
|
||||
async function selectFolder(folder: CollectionObject | null) {
|
||||
clearSearch()
|
||||
selectedFolder.value = folder
|
||||
selectedMessage.value = null
|
||||
|
||||
@@ -311,67 +307,6 @@ export const useMailOperationsStore = defineStore('mailOperationsStore', () => {
|
||||
_updateSyncSources()
|
||||
}
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 300
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
// Monotonic token so out-of-order/stale search responses are discarded.
|
||||
let searchToken = 0
|
||||
|
||||
/**
|
||||
* Debounced server-side search of the selected folder. Clearing the query
|
||||
* (or the folder) reverts the message list back to `currentMessages`.
|
||||
*/
|
||||
function searchMessages(query: string) {
|
||||
const trimmed = (query ?? '').trim()
|
||||
|
||||
if (searchDebounceTimer) {
|
||||
clearTimeout(searchDebounceTimer)
|
||||
searchDebounceTimer = null
|
||||
}
|
||||
|
||||
if (!trimmed || !selectedFolder.value) {
|
||||
searchToken++
|
||||
searchResults.value = null
|
||||
searching.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const folder = selectedFolder.value
|
||||
// 'from'/'to'/'subject'/'body' are the attributes both IMAP and JMAP
|
||||
// backends whitelist for entity search (see their entityListFilter
|
||||
// capability); OR them together for a single-box search.
|
||||
const filter: ListFilter = {
|
||||
from: [trimmed, ListFilterComparisonOperator.LIKE],
|
||||
to: [trimmed, ListFilterComparisonOperator.LIKE, ListFilterConjunctionOperator.OR],
|
||||
subject: [trimmed, ListFilterComparisonOperator.LIKE, ListFilterConjunctionOperator.OR],
|
||||
body: [trimmed, ListFilterComparisonOperator.LIKE, ListFilterConjunctionOperator.OR],
|
||||
}
|
||||
searching.value = true
|
||||
const token = ++searchToken
|
||||
|
||||
searchDebounceTimer = setTimeout(async () => {
|
||||
try {
|
||||
const results = await entitiesStore.list([folder.identifier], filter)
|
||||
if (token !== searchToken) return
|
||||
searchResults.value = Object.values(results)
|
||||
} catch (error) {
|
||||
if (token === searchToken) searchResults.value = []
|
||||
console.error('[Mail][Operations] Failed to search messages:', error)
|
||||
} finally {
|
||||
if (token === searchToken) searching.value = false
|
||||
}
|
||||
}, SEARCH_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
if (searchDebounceTimer) {
|
||||
clearTimeout(searchDebounceTimer)
|
||||
searchDebounceTimer = null
|
||||
}
|
||||
searchToken++
|
||||
searchResults.value = null
|
||||
searching.value = false
|
||||
}
|
||||
|
||||
function selectMessage(entity: EntityObject | null) {
|
||||
selectedMessage.value = entity
|
||||
}
|
||||
@@ -640,24 +575,6 @@ export const useMailOperationsStore = defineStore('mailOperationsStore', () => {
|
||||
return blob
|
||||
}
|
||||
|
||||
async function attachmentPreviewBlob(
|
||||
entity: EntityObject,
|
||||
part: MessagePartInterface,
|
||||
variant: 'inline' | 'fullscreen' = 'inline',
|
||||
): Promise<Blob> {
|
||||
return entitiesStore.preview({
|
||||
target: entity.identifier,
|
||||
part: {
|
||||
blobId: part.blobId ?? undefined,
|
||||
partId: part.partId ?? undefined,
|
||||
cid: part.cid ?? undefined,
|
||||
type: part.type || 'application/octet-stream',
|
||||
size: part.size ?? null,
|
||||
},
|
||||
variant,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Exports ───────────────────────────────────────────────────────────────
|
||||
|
||||
return {
|
||||
@@ -670,8 +587,6 @@ export const useMailOperationsStore = defineStore('mailOperationsStore', () => {
|
||||
// State
|
||||
loading,
|
||||
selectedMessage,
|
||||
searchResults,
|
||||
searching,
|
||||
composerSaving,
|
||||
composerSending,
|
||||
composerLastSaved,
|
||||
@@ -687,8 +602,6 @@ export const useMailOperationsStore = defineStore('mailOperationsStore', () => {
|
||||
retrieveService,
|
||||
selectFolder,
|
||||
selectMessage,
|
||||
searchMessages,
|
||||
clearSearch,
|
||||
createFolder,
|
||||
saveComposerDraft,
|
||||
sendComposerMessage,
|
||||
@@ -699,7 +612,6 @@ export const useMailOperationsStore = defineStore('mailOperationsStore', () => {
|
||||
moveMessages,
|
||||
downloadMessage,
|
||||
attachmentBlob,
|
||||
attachmentPreviewBlob,
|
||||
moveFolder,
|
||||
renameFolder,
|
||||
isServiceFolderLoading,
|
||||
|
||||
@@ -714,14 +714,6 @@ export const useMailUiStore = defineStore('mailUiStore', () => {
|
||||
return mailOperationsStore.attachmentBlob(entity, part)
|
||||
}
|
||||
|
||||
function attachmentPreviewBlob(
|
||||
entity: EntityObject,
|
||||
part: Parameters<typeof mailOperationsStore.attachmentPreviewBlob>[1],
|
||||
variant: 'inline' | 'fullscreen' = 'inline',
|
||||
) {
|
||||
return mailOperationsStore.attachmentPreviewBlob(entity, part, variant)
|
||||
}
|
||||
|
||||
return {
|
||||
sidebarVisible,
|
||||
settingsDialogVisible,
|
||||
@@ -771,7 +763,6 @@ export const useMailUiStore = defineStore('mailUiStore', () => {
|
||||
moveMessages,
|
||||
downloadMessage,
|
||||
attachmentBlob,
|
||||
attachmentPreviewBlob,
|
||||
messageSelectionModeActivate,
|
||||
messageSelectionModeDeactivate,
|
||||
messageSelectionToggleOne,
|
||||
@@ -797,4 +788,4 @@ export const useMailUiStore = defineStore('mailUiStore', () => {
|
||||
confirmDeleteFolder,
|
||||
flagSelectedMessages,
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -5,13 +5,9 @@ import type { MessageAddressInterface } from "@MailManager/types/message"
|
||||
export enum ComposerMode {
|
||||
Fresh = 'fresh',
|
||||
Reply = 'reply',
|
||||
ReplyAll = 'replyAll',
|
||||
Forward = 'forward',
|
||||
}
|
||||
|
||||
/** Body editing format: rich (Tiptap/HTML) or plain text. */
|
||||
export type ComposerFormat = 'rich' | 'plain'
|
||||
|
||||
export interface ComposerDraftAttachment {
|
||||
identifier: string
|
||||
composition: string
|
||||
|
||||
@@ -111,7 +111,6 @@ export interface CompositionAttachmentAddResponse {
|
||||
message: string
|
||||
}
|
||||
composition: string
|
||||
revision?: number
|
||||
attachments: Record<string, CompositionAttachmentInterface>
|
||||
failed?: {
|
||||
identifier: string
|
||||
@@ -132,5 +131,4 @@ export interface CompositionAttachmentRemoveResponse {
|
||||
}
|
||||
composition: string
|
||||
identifier: string
|
||||
revision?: number
|
||||
}
|
||||
@@ -187,7 +187,6 @@ final class CompositionManagerTest extends TestCase {
|
||||
$this->assertSame('added', $result['disposition']);
|
||||
$this->assertCount(1, $result['attachments']);
|
||||
$this->assertSame([], $result['failed']);
|
||||
$this->assertSame(1, $result['revision']);
|
||||
|
||||
$attachment = $result['attachments']['attach-1'];
|
||||
$this->assertSame('documents', $attachment['origin']);
|
||||
@@ -197,58 +196,11 @@ final class CompositionManagerTest extends TestCase {
|
||||
$this->assertSame(strlen('hello world'), $attachment['size']);
|
||||
|
||||
$snapshot = $this->compositionStore->compositionFetch(self::TENANT_ID, self::USER_ID, $compositionId);
|
||||
$this->assertSame(1, $snapshot['revision']);
|
||||
$this->assertSame('dirty', $snapshot['remote']['status']);
|
||||
$this->assertNull($snapshot['remote']['entity']);
|
||||
$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 {
|
||||
$compositionId = $this->stageEmptyComposition();
|
||||
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
||||
@@ -333,45 +285,6 @@ final class CompositionManagerTest extends TestCase {
|
||||
$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(
|
||||
Stub $documentsManager,
|
||||
bool $documentsModuleEnabled = true,
|
||||
|
||||
@@ -12,8 +12,6 @@ use KTXF\Mail\Entity\EntityMutableInterface;
|
||||
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceBaseInterface;
|
||||
use KTXF\Mail\Service\ServiceEntityMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceEntitySubmitInterface;
|
||||
use KTXF\Mail\Submission\EntitySubmitResult;
|
||||
use KTXF\Resource\Filter\IFilter;
|
||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||
@@ -259,133 +257,6 @@ final class CompositionSynchronizationTest extends TestCase {
|
||||
));
|
||||
}
|
||||
|
||||
public function testSendPurgesLocalCompositionOnSuccess(): void {
|
||||
$this->stageComposition();
|
||||
$properties = $this->createStub(MessagePropertiesMutableInterface::class);
|
||||
$service = $this->submitService($properties);
|
||||
$service->method('entitySubmit')->willReturn(new EntitySubmitResult(EntitySubmitResult::DISPOSITION_SENT));
|
||||
|
||||
$mailManager = $this->createStub(\KTXM\MailManager\Manager::class);
|
||||
$mailManager->method('serviceFindByAddress')->willReturn($service);
|
||||
|
||||
$result = $this->compositionManager($mailManager)->send(
|
||||
self::TENANT_ID,
|
||||
self::USER_ID,
|
||||
self::COMPOSITION_ID,
|
||||
['address' => 'sender@example.com'],
|
||||
['subject' => 'Sent draft'],
|
||||
[],
|
||||
);
|
||||
|
||||
$this->assertSame('sent', $result['disposition']);
|
||||
$this->assertNull($this->compositionStore->compositionFetch(
|
||||
self::TENANT_ID,
|
||||
self::USER_ID,
|
||||
self::COMPOSITION_ID,
|
||||
));
|
||||
}
|
||||
|
||||
public function testSendReusesSyncedRemoteDraftAsSubmissionSource(): void {
|
||||
$this->stageComposition('provider:service:drafts:remote-1');
|
||||
$properties = $this->createStub(MessagePropertiesMutableInterface::class);
|
||||
$service = $this->createMockForIntersectionOfInterfaces([
|
||||
ServiceBaseInterface::class,
|
||||
ServiceEntitySubmitInterface::class,
|
||||
]);
|
||||
$entity = $this->createStub(EntityMutableInterface::class);
|
||||
$entity->method('getProperties')->willReturn($properties);
|
||||
$service->method('provider')->willReturn('provider');
|
||||
$service->method('identifier')->willReturn('service');
|
||||
$service->method('getEnabled')->willReturn(true);
|
||||
$service->method('entityFresh')->willReturn($entity);
|
||||
$service->expects($this->once())
|
||||
->method('entitySubmit')
|
||||
->with(
|
||||
$this->anything(),
|
||||
$this->callback(static fn(?EntityIdentifier $source): bool => $source !== null
|
||||
&& (string)$source === 'provider:service:drafts:remote-1'),
|
||||
$properties,
|
||||
)
|
||||
->willReturn(new EntitySubmitResult(EntitySubmitResult::DISPOSITION_SENT));
|
||||
|
||||
$mailManager = $this->createStub(\KTXM\MailManager\Manager::class);
|
||||
$mailManager->method('serviceFindByAddress')->willReturn($service);
|
||||
|
||||
$result = $this->compositionManager($mailManager)->send(
|
||||
self::TENANT_ID,
|
||||
self::USER_ID,
|
||||
self::COMPOSITION_ID,
|
||||
['address' => 'sender@example.com'],
|
||||
['subject' => 'Sent draft'],
|
||||
[],
|
||||
);
|
||||
|
||||
$this->assertSame('sent', $result['disposition']);
|
||||
}
|
||||
|
||||
public function testSendIgnoresRemoteDraftBelongingToAnotherService(): void {
|
||||
$this->stageComposition('provider:other-service:drafts:remote-1');
|
||||
$properties = $this->createStub(MessagePropertiesMutableInterface::class);
|
||||
$service = $this->createMockForIntersectionOfInterfaces([
|
||||
ServiceBaseInterface::class,
|
||||
ServiceEntitySubmitInterface::class,
|
||||
]);
|
||||
$entity = $this->createStub(EntityMutableInterface::class);
|
||||
$entity->method('getProperties')->willReturn($properties);
|
||||
$service->method('provider')->willReturn('provider');
|
||||
$service->method('identifier')->willReturn('service');
|
||||
$service->method('getEnabled')->willReturn(true);
|
||||
$service->method('entityFresh')->willReturn($entity);
|
||||
$service->expects($this->once())
|
||||
->method('entitySubmit')
|
||||
->with($this->anything(), $this->isNull(), $properties)
|
||||
->willReturn(new EntitySubmitResult(EntitySubmitResult::DISPOSITION_SENT));
|
||||
|
||||
$mailManager = $this->createStub(\KTXM\MailManager\Manager::class);
|
||||
$mailManager->method('serviceFindByAddress')->willReturn($service);
|
||||
|
||||
$result = $this->compositionManager($mailManager)->send(
|
||||
self::TENANT_ID,
|
||||
self::USER_ID,
|
||||
self::COMPOSITION_ID,
|
||||
['address' => 'sender@example.com'],
|
||||
['subject' => 'Sent draft'],
|
||||
[],
|
||||
);
|
||||
|
||||
$this->assertSame('sent', $result['disposition']);
|
||||
}
|
||||
|
||||
public function testSendFailureKeepsLocalComposition(): void {
|
||||
$this->stageComposition();
|
||||
$properties = $this->createStub(MessagePropertiesMutableInterface::class);
|
||||
$service = $this->submitService($properties);
|
||||
$service->method('entitySubmit')->willReturn(new EntitySubmitResult(
|
||||
EntitySubmitResult::DISPOSITION_ERROR,
|
||||
errorMessage: 'Transport unavailable',
|
||||
));
|
||||
|
||||
$mailManager = $this->createStub(\KTXM\MailManager\Manager::class);
|
||||
$mailManager->method('serviceFindByAddress')->willReturn($service);
|
||||
|
||||
$result = $this->compositionManager($mailManager)->send(
|
||||
self::TENANT_ID,
|
||||
self::USER_ID,
|
||||
self::COMPOSITION_ID,
|
||||
['address' => 'sender@example.com'],
|
||||
['subject' => 'Keep this draft'],
|
||||
[],
|
||||
);
|
||||
|
||||
$this->assertSame('error', $result['disposition']);
|
||||
$this->assertSame('Transport unavailable', $result['error']['message']);
|
||||
$this->assertNotNull($this->compositionStore->compositionFetch(
|
||||
self::TENANT_ID,
|
||||
self::USER_ID,
|
||||
self::COMPOSITION_ID,
|
||||
));
|
||||
}
|
||||
|
||||
private function stageComposition(?string $remoteEntity = null): void {
|
||||
$saveComposition = static fn(): array => [
|
||||
'identifier' => self::COMPOSITION_ID,
|
||||
@@ -421,20 +292,6 @@ final class CompositionSynchronizationTest extends TestCase {
|
||||
return $service;
|
||||
}
|
||||
|
||||
private function submitService(MessagePropertiesMutableInterface $properties): ServiceBaseInterface&ServiceEntitySubmitInterface&Stub {
|
||||
$service = $this->createStubForIntersectionOfInterfaces([
|
||||
ServiceBaseInterface::class,
|
||||
ServiceEntitySubmitInterface::class,
|
||||
]);
|
||||
$entity = $this->createStub(EntityMutableInterface::class);
|
||||
$entity->method('getProperties')->willReturn($properties);
|
||||
$service->method('provider')->willReturn('provider');
|
||||
$service->method('identifier')->willReturn('service');
|
||||
$service->method('getEnabled')->willReturn(true);
|
||||
$service->method('entityFresh')->willReturn($entity);
|
||||
return $service;
|
||||
}
|
||||
|
||||
private function compositionManager(\KTXM\MailManager\Manager $mailManager): CompositionManager {
|
||||
return new CompositionManager(
|
||||
container: $this->createStub(ContainerInterface::class),
|
||||
|
||||
Reference in New Issue
Block a user