Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3688bfcadf |
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mongo:
|
||||
image: mongo:7
|
||||
image: mongo:8
|
||||
options: >-
|
||||
--health-cmd "mongosh --quiet --eval \"db.adminCommand('ping')\""
|
||||
--health-interval 5s
|
||||
|
||||
@@ -22,7 +22,6 @@ coverage/
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
.ideas/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
+50
-212
@@ -5,29 +5,21 @@ declare(strict_types=1);
|
||||
namespace KTXM\Mail;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KTXC\Module\ModuleManager;
|
||||
use KTXF\Mail\Object\Address;
|
||||
use KTXF\Mail\Object\AddressInterface;
|
||||
use KTXF\Mail\Object\MessagePart;
|
||||
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceEntitySubmitInterface;
|
||||
use KTXF\Mail\Submission\EntitySubmitResult;
|
||||
use KTXF\Resource\BinaryResource;
|
||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||
use KTXF\Resource\Identifier\EntityIdentifierInterface;
|
||||
use KTXF\Resource\Identifier\ResourceIdentifier;
|
||||
use KTXF\Utile\UUID;
|
||||
use KTXM\Mail\Stores\CompositionStore;
|
||||
use KTXM\MailManager\Manager as MailManager;
|
||||
use KTXM\DocumentsManager\Manager as DocumentsManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class CompositionManager {
|
||||
|
||||
private const DOCUMENTS_MODULE_HANDLE = 'documents_manager';
|
||||
|
||||
public function __construct(
|
||||
private readonly ContainerInterface $container,
|
||||
private readonly ModuleManager $moduleManager,
|
||||
private readonly CompositionStore $compositionStore,
|
||||
private readonly MailManager $mailManager,
|
||||
) {}
|
||||
@@ -83,7 +75,7 @@ class CompositionManager {
|
||||
data: $data,
|
||||
);
|
||||
// supplement the attachment metadata with source information
|
||||
$meta['origin'] = 'message';
|
||||
$meta['origin'] = 'source';
|
||||
$meta['source'] = $source;
|
||||
$meta['partId'] = $attachment->getId();
|
||||
$meta['blobId'] = $attachment->getBlobId();
|
||||
@@ -300,13 +292,40 @@ class CompositionManager {
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array|string> $entries
|
||||
* @return array<int,Address>
|
||||
*/
|
||||
private function mapAddresses(array $entries): array {
|
||||
$addresses = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if (is_array($entry)) {
|
||||
$address = Address::fromArray($entry);
|
||||
if ($address->getAddress() !== '') {
|
||||
$addresses[] = $address;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($entry) && $entry !== '') {
|
||||
$address = Address::fromString($entry);
|
||||
if ($address->getAddress() !== '') {
|
||||
$addresses[] = $address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
public function attachmentAdd(string $tenantId, string $userId, string $composition, array ...$attachments): array {
|
||||
$result = [
|
||||
'composition' => $composition,
|
||||
'attachments' => [],
|
||||
];
|
||||
// fetch the composition to ensure it exists
|
||||
|
||||
$composed = $this->compositionStore->compositionFetch($tenantId, $userId, $composition);
|
||||
if ($composed === null) {
|
||||
$result['disposition'] = 'error';
|
||||
@@ -316,171 +335,34 @@ class CompositionManager {
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
// separate attachments by type
|
||||
$uploads = [];
|
||||
$documents = [];
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
if (isset($attachment['origin']) && $attachment['origin'] === 'documents') {
|
||||
$documents[] = $attachment;
|
||||
} elseif (isset($attachment['origin']) && $attachment['origin'] === 'device') {
|
||||
$uploads[] = $attachment;
|
||||
if (!isset($attachment['identifier']) || !is_string($attachment['identifier']) || $attachment['identifier'] === '') {
|
||||
$identifier = UUID::v4();
|
||||
} else {
|
||||
$identifier = $attachment['identifier'];
|
||||
}
|
||||
|
||||
$meta = $this->compositionStore->attachmentStageFromBase64(
|
||||
tenantId: $tenantId,
|
||||
userId: $userId,
|
||||
compositionId: $composition,
|
||||
attachmentId: $identifier,
|
||||
name: $attachment['name'] ?? 'unknown.bin',
|
||||
type: $attachment['type'] ?? 'application/octet-stream',
|
||||
data: $attachment['data'] ?? '',
|
||||
);
|
||||
$meta['origin'] = 'upload';
|
||||
$composed['attachments'][$meta['identifier']] = $meta;
|
||||
}
|
||||
// process uploaded attachments
|
||||
if ($uploads !== []) {
|
||||
$this->attachmentAddFromDevice($tenantId, $userId, $composition, $uploads, $composed);
|
||||
}
|
||||
// process attachments from the documents module
|
||||
if ($documents !== []) {
|
||||
$failed = $documents !== []
|
||||
? $this->attachmentAddFromDocuments($tenantId, $userId, $composition, $documents, $composed)
|
||||
: [];
|
||||
}
|
||||
// save the updated composition with new attachments
|
||||
|
||||
$this->compositionStore->compositionSave($tenantId, $userId, $composition, $composed);
|
||||
// construct the result
|
||||
|
||||
$result['disposition'] = 'added';
|
||||
$result['attachments'] = $composed['attachments'];
|
||||
$result['failed'] = $failed;
|
||||
$result['disposition'] = match (true) {
|
||||
$failed === [] => 'added',
|
||||
$result['attachments'] !== [] => 'partial',
|
||||
default => 'error',
|
||||
};
|
||||
if ($result['disposition'] === 'error') {
|
||||
$result['error'] = [
|
||||
'type' => 'attachment_add_failed',
|
||||
'message' => 'None of the requested attachments could be added',
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage device-uploaded (base64 payload) attachment entries
|
||||
*/
|
||||
private function attachmentAddFromDevice(string $tenantId, string $userId, string $composition, array $uploads, array &$composed): void {
|
||||
foreach ($uploads as $attachment) {
|
||||
$identifier = (isset($attachment['identifier']) && is_string($attachment['identifier']) && $attachment['identifier'] !== '')
|
||||
? $attachment['identifier']
|
||||
: UUID::v4();
|
||||
|
||||
$decoded = base64_decode((string) ($attachment['data'] ?? ''), true);
|
||||
if ($decoded === false) {
|
||||
throw new InvalidArgumentException('Attachment payload is not valid base64');
|
||||
}
|
||||
|
||||
$data = new BinaryResource(
|
||||
$attachment['name'] ?? 'unknown.bin',
|
||||
$attachment['type'] ?? 'application/octet-stream',
|
||||
$this->stringToGenerator($decoded),
|
||||
);
|
||||
|
||||
$meta = $this->compositionStore->attachmentStageFromStream(
|
||||
tenantId: $tenantId,
|
||||
userId: $userId,
|
||||
compositionId: $composition,
|
||||
attachmentId: $identifier,
|
||||
data: $data,
|
||||
);
|
||||
$meta['origin'] = 'device';
|
||||
$composed['attachments'][$meta['identifier']] = $meta;
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveDocumentsManager(): ?DocumentsManager {
|
||||
$module = $this->moduleManager->fetch(self::DOCUMENTS_MODULE_HANDLE);
|
||||
if ($module === null || !$module->enabled()) {
|
||||
return null;
|
||||
}
|
||||
if (!$this->container->has(DocumentsManager::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->container->get(DocumentsManager::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage documents-sourced attachment entries
|
||||
*/
|
||||
private function attachmentAddFromDocuments(string $tenantId, string $userId, string $composition, array $sourced, array &$composed): array {
|
||||
$failed = [];
|
||||
|
||||
// The documents module is an optional dependency, so we must resolve it
|
||||
// lazily and handle the case where it is not installed or enabled.
|
||||
$documentsManager = $this->resolveDocumentsManager();
|
||||
if ($documentsManager === null) {
|
||||
foreach ($sourced as $attachment) {
|
||||
$failed[] = [
|
||||
'identifier' => (string) ($attachment['source'] ?? ''),
|
||||
'message' => 'Documents module is not installed or enabled',
|
||||
];
|
||||
}
|
||||
return $failed;
|
||||
}
|
||||
|
||||
$requested = [];
|
||||
foreach ($sourced as $attachment) {
|
||||
$rawIdentifier = (string) $attachment['source'];
|
||||
try {
|
||||
$parsed = ResourceIdentifier::fromString($rawIdentifier);
|
||||
} catch (InvalidArgumentException) {
|
||||
$failed[] = ['identifier' => $rawIdentifier, 'message' => 'Invalid document identifier'];
|
||||
continue;
|
||||
}
|
||||
if (!$parsed instanceof EntityIdentifier) {
|
||||
$failed[] = ['identifier' => $rawIdentifier, 'message' => 'Invalid document identifier'];
|
||||
continue;
|
||||
}
|
||||
$requested[] = ['attachment' => $attachment, 'identifier' => $parsed];
|
||||
}
|
||||
|
||||
if ($requested === []) {
|
||||
return $failed;
|
||||
}
|
||||
|
||||
$entities = $documentsManager->entityFetchBulk($tenantId, $userId, ...array_column($requested, 'identifier'));
|
||||
$entitiesByUrn = [];
|
||||
foreach ($entities as $entity) {
|
||||
$entitiesByUrn[$entity->urn()] = $entity;
|
||||
}
|
||||
|
||||
foreach ($requested as ['attachment' => $attachment, 'identifier' => $entityIdentifier]) {
|
||||
$urn = (string) $entityIdentifier;
|
||||
$entity = $entitiesByUrn[$urn] ?? null;
|
||||
if ($entity === null) {
|
||||
$failed[] = ['identifier' => $urn, 'message' => 'Document not found or access denied'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$resource = $documentsManager->entityReadStream($tenantId, $userId, $entityIdentifier);
|
||||
if ($resource === null) {
|
||||
$failed[] = ['identifier' => $urn, 'message' => 'Document content could not be read'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$identifier = (isset($attachment['identifier']) && is_string($attachment['identifier']) && $attachment['identifier'] !== '')
|
||||
? $attachment['identifier']
|
||||
: UUID::v4();
|
||||
|
||||
$properties = $entity->getProperties();
|
||||
$data = new BinaryResource($properties->getLabel(), $properties->getMime(), $this->streamToGenerator($resource));
|
||||
|
||||
$meta = $this->compositionStore->attachmentStageFromStream(
|
||||
tenantId: $tenantId,
|
||||
userId: $userId,
|
||||
compositionId: $composition,
|
||||
attachmentId: $identifier,
|
||||
data: $data,
|
||||
);
|
||||
$meta['origin'] = 'documents';
|
||||
$meta['source'] = $urn;
|
||||
$composed['attachments'][$meta['identifier']] = $meta;
|
||||
}
|
||||
|
||||
return $failed;
|
||||
}
|
||||
|
||||
public function attachmentRemove(string $tenantId, string $userId, string $composition, string $identifier): array {
|
||||
$result = [
|
||||
'composition' => $composition,
|
||||
@@ -511,48 +393,4 @@ class CompositionManager {
|
||||
$result['disposition'] = 'removed';
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array|string> $entries
|
||||
* @return array<int,Address>
|
||||
*/
|
||||
private function mapAddresses(array $entries): array {
|
||||
$addresses = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if (is_array($entry)) {
|
||||
$address = Address::fromArray($entry);
|
||||
if ($address->getAddress() !== '') {
|
||||
$addresses[] = $address;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($entry) && $entry !== '') {
|
||||
$address = Address::fromString($entry);
|
||||
if ($address->getAddress() !== '') {
|
||||
$addresses[] = $address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an in-memory byte string in a single-chunk Generator.
|
||||
*/
|
||||
private function stringToGenerator(string $bytes): \Generator {
|
||||
yield $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a raw PHP stream resource in a Generator yielding chunks, closing it once exhausted.
|
||||
*/
|
||||
private function streamToGenerator($resource): \Generator {
|
||||
while (!feof($resource)) {
|
||||
yield fread($resource, 65536);
|
||||
}
|
||||
fclose($resource);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ namespace KTXM\Mail\Controllers;
|
||||
use InvalidArgumentException;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
use KTXM\Mail\CompositionManager;
|
||||
@@ -18,8 +18,8 @@ use Throwable;
|
||||
final class CompositionController extends ControllerAbstract {
|
||||
|
||||
public function __construct(
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly CompositionManager $compositionManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
@@ -31,8 +31,8 @@ final class CompositionController extends ControllerAbstract {
|
||||
string|null $operation = null,
|
||||
array|null $data = null,
|
||||
): Response {
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
$userId = $this->identityContext->identifier();
|
||||
$tenantId = $this->tenantIdentity->identifier();
|
||||
$userId = $this->userIdentity->identifier();
|
||||
|
||||
try {
|
||||
if ($operation === null) {
|
||||
|
||||
@@ -54,7 +54,13 @@ final class CompositionStore {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function attachmentStageFromStream(string $tenantId, string $userId, string $compositionId, string $attachmentId, BinaryResource $data): array {
|
||||
public function attachmentStageFromStream(
|
||||
string $tenantId,
|
||||
string $userId,
|
||||
string $compositionId,
|
||||
string $attachmentId,
|
||||
BinaryResource $data,
|
||||
): array {
|
||||
$attachmentDir = $this->attachmentDir($tenantId, $userId, $compositionId);
|
||||
if (!is_dir($attachmentDir)) {
|
||||
mkdir($attachmentDir, 0755, true);
|
||||
@@ -85,6 +91,40 @@ final class CompositionStore {
|
||||
return $meta;
|
||||
}
|
||||
|
||||
public function attachmentStageFromBase64(
|
||||
string $tenantId,
|
||||
string $userId,
|
||||
string $compositionId,
|
||||
string $attachmentId,
|
||||
string $name,
|
||||
string $type,
|
||||
string $data,
|
||||
): array {
|
||||
$decoded = base64_decode($data, true);
|
||||
if ($decoded === false) {
|
||||
throw new \InvalidArgumentException('Attachment payload is not valid base64');
|
||||
}
|
||||
|
||||
$attachmentDir = $this->attachmentDir($tenantId, $userId, $compositionId);
|
||||
if (!is_dir($attachmentDir)) {
|
||||
mkdir($attachmentDir, 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($attachmentDir . '/' . $attachmentId . '.blob', $decoded);
|
||||
|
||||
$metadata = [
|
||||
'identifier' => $attachmentId,
|
||||
'composition' => $compositionId,
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'size' => strlen($decoded),
|
||||
];
|
||||
|
||||
file_put_contents($attachmentDir . '/' . $attachmentId . '.meta', json_encode($metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
public function attachmentFetchData(string $tenantId, string $userId, string $compositionId, string $attachmentId): ?string {
|
||||
$path = $this->attachmentDir($tenantId, $userId, $compositionId) . '/' . $attachmentId . '.blob';
|
||||
if (!file_exists($path)) {
|
||||
|
||||
Generated
+198
-198
@@ -795,49 +795,49 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.30.1.tgz",
|
||||
"integrity": "sha512-HWEO6Qi8fI8Zt8X4atVV6GxthJx7Vrjr6L5YFq4OkjYJ7HG2/bFw6IKsDA3jOYgY4DM9lv8IadaiWUQ2JJRIBA==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.29.0.tgz",
|
||||
"integrity": "sha512-A/lrhKpOYtl0V5pmPS00Zps8pgBe1qDOoD9fzsumDSZ3HP8W398C959Jgru75PNFokAya9COPD3iaKP3NWF25g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "3.30.1"
|
||||
"@tiptap/pm": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-blockquote": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.30.1.tgz",
|
||||
"integrity": "sha512-nUBIPmf9fKfBzeMpQsexsHw3/ICzDfBK8DN9uLf5wO3I/gWycAByynvfNIqfHyl6Q77QgTIHrdjihEXUSYzeJg==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.29.0.tgz",
|
||||
"integrity": "sha512-k8NKHLEYOqre7guObZBeFM04CaTwmEceCmdIrIjd7H9KoHSbRODaBNved6j5G33x/FWMwIF/RQbE2BjWaFSgzA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1"
|
||||
"@tiptap/core": "3.29.0",
|
||||
"@tiptap/pm": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bold": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.30.1.tgz",
|
||||
"integrity": "sha512-xnEGv7evFQquT0r/byjBt5iqDYQi6bF/W7DPO2rAG0z33mShDEUHhE407jakMiOSYNUilFjuQj1naLvwpQC3Bw==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.29.0.tgz",
|
||||
"integrity": "sha512-BPUJvJ9sCsU3fxao5UJfDJrDuHhMn2hczZwZ9Qs4w1vkAeLGyOYbSejUTJdBYHLoexGiEwgnEeVeZN3C8GaI5g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bubble-menu": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.30.1.tgz",
|
||||
"integrity": "sha512-6asFH7ZW0gTh0aX7qrABYRVqiU/IOSjk4PKt6qdZM5YS455bjVLZDM0PcIccMDaYiomuCNjfuCSj5dxmPDoWiQ==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.29.0.tgz",
|
||||
"integrity": "sha512-aBNI8ebX76ObderRmu/PJ9Q2AGVEuhpg0TBvv2SNbHtmU5kNUMHQI8HxrxKFblJwETAmG6EJ3Ed/5jRxpJmrsg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -848,80 +848,67 @@
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bullet-list": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.30.1.tgz",
|
||||
"integrity": "sha512-9bf4VgIxaOe+c7W20WZGUU5d2SFNGSpVwDGU/7jGlnjobzR7ZhDK/THk9JQr8OpvZrVN8Nmf84ZD3axjt+h/hQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.30.1"
|
||||
"@tiptap/core": "3.29.0",
|
||||
"@tiptap/pm": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.30.1.tgz",
|
||||
"integrity": "sha512-oqLEOLZel3lrLhACP4vMhH5RNbV9yxFsJYiOmQjjEFEoCEWWxVfJuhZ+8NFS3mUCdgy77G62XimjEvqC3+7V7Q==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.29.0.tgz",
|
||||
"integrity": "sha512-Um0BlyunRJ8Fal288Jn7I2n15FRwQcTy3NnKBBrPz3ikwx+yZZ0n9xTozpLMSxXrTq6cRlxLkASp/9FFx9sX9A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.30.1.tgz",
|
||||
"integrity": "sha512-j19Ml9BpvHgTMSN4SfkJ26B5xSuHaxPXvMoXyAX1iOoSc4J92sCqShLGgpmtce42sIroYFRs4sBv0ndBRDcxtA==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.29.0.tgz",
|
||||
"integrity": "sha512-nKm1YSEiPVosjm4qeg6m+6SAKq82rKukAzE/g3M5eVs2OgEb4QIKQvQOmJ+zgGAnpb9Q/JIvcKsGGU1rn9Vnyw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1"
|
||||
"@tiptap/core": "3.29.0",
|
||||
"@tiptap/pm": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.30.1.tgz",
|
||||
"integrity": "sha512-aeMhO7EDoJyl5AGkF9hDQ9e52s18L8oYdbYjzcmXeo0YuXeaxbZCuUueVH4DnTfV7/vSkDRpDkk4TFF5LfKQpg==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.29.0.tgz",
|
||||
"integrity": "sha512-folTlrwmUL+WSnojvfjZ951Gb25Pw8BxjXwhvXFuZXJ92qMfOcezA480kwFuu5nG8pPc/76ezQlz9a86Y5uQJw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-dropcursor": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.30.1.tgz",
|
||||
"integrity": "sha512-N3R5rA01WX/brGm1Qcnf2KLu0xkyGbRPMsHzzl1YG2sOLQM9t+FQi1T8WvSJiOpfYBVaWQkILUoCymADSpgUAg==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.29.0.tgz",
|
||||
"integrity": "sha512-IAY21q7KSXyCXN3u13kfpq0E8D2i3r9nSbsNYlLzkEv0Bpn+pWShZi05A0ubuothDNjdaMa96xaRY0f1y9gDZw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "3.30.1"
|
||||
"@tiptap/extensions": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-floating-menu": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.30.1.tgz",
|
||||
"integrity": "sha512-/Rlqsn7OXThZNzD0Qq/Ru4rZCFj3YnxL8Vf01cez+pvcyxgbwf4b9Wir+1JWJmaEMYsmWprSgEBW0l9ctbq97w==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.29.0.tgz",
|
||||
"integrity": "sha512-/rFlN6T70HuT34kFvftKm56UX0IPff7vlII7Sqgs7DprKVYkItiU3lM7+AoeDRzCsWMg4z1yNHj70BPx7+EJ6Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
@@ -930,80 +917,80 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1"
|
||||
"@tiptap/core": "3.29.0",
|
||||
"@tiptap/pm": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-gapcursor": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.30.1.tgz",
|
||||
"integrity": "sha512-dp13WaPNj32SkDN9tun0OtwtfdO/y52BJWigwXPgpUFLEyr8hQ84TxXL3rDshGHE4Eyjdd+erdBHisjBEV9vwQ==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.29.0.tgz",
|
||||
"integrity": "sha512-Jz0zwriGxPMuRjKo6x7WhPa/7pZn0dovyJu/GLarN4T+mMseGNXWwEBsLEKkuumsJVPDEPPFjX+rzEeaFKInPQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "3.30.1"
|
||||
"@tiptap/extensions": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-hard-break": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.30.1.tgz",
|
||||
"integrity": "sha512-2emcEkSSj+Y3dXXEugKQZDNeWCum0LBuKFmun7SiJyLOdxNuRnxwm2l4/LLiEHGDJBchDti4Oo8jZGLTRt4uog==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.29.0.tgz",
|
||||
"integrity": "sha512-8qKj0oeuU7IYoG73Lj3W4EC/JS/dFjN5iHmc4yodqppP6qs0o2zsF/2ipceGN/REZq5megPtxRgXu/nxQZ1NYg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-heading": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.30.1.tgz",
|
||||
"integrity": "sha512-TGM1ZNeH/D8HQK59vFMZql2gesMoHy+6nLq3mFsYpiRT2TShMGoq+K7SC+ty6N5a/k4M82ZQrDq8t4zjiiS5xA==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.29.0.tgz",
|
||||
"integrity": "sha512-xWb3QEKo7cp9u6Vzi6oYKODra6jPwziHBv3fJAnxWpRldxtJqfyOVMikWJJFuDjmr+K0qyegzf206pMk+y0bDg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.30.1.tgz",
|
||||
"integrity": "sha512-fTaSDapNV3cRgsek94z/Z4+Fyr0UpkE8/9PdSvt9MHYo/2yx3mmuamEJwUhUCpe7gDfzGdrMMsRpDOcd0b1ZEQ==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.29.0.tgz",
|
||||
"integrity": "sha512-u1OgncXkokIuUJQIh4jVQfYHL/6I8VnrokUcCud4eErWvgeLnCbNmVdih4kxIdfmvEyAxXjDwcqf4AB79TxuNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1"
|
||||
"@tiptap/core": "3.29.0",
|
||||
"@tiptap/pm": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-italic": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.30.1.tgz",
|
||||
"integrity": "sha512-KldMtozKk8OBHexo6akCQpJCWMgJ5OwPYuEOJO+vFjgw6VS+9N16peIo5dY+vXAiVIZL+swEsBp4barwz7NiLw==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.29.0.tgz",
|
||||
"integrity": "sha512-c8gF+zM7yXB9iGxtVCioYecjQ+A2fLlNa1XYBYbTobNhbUo8a11akbTkGNcCIgttwnzwEoI4mB6/PK4yjaQvVg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-link": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.30.1.tgz",
|
||||
"integrity": "sha512-krq5yHDT4K+u2r7rS0CGk/yafyAKY1gbxexfbMmq+jyHtUUrr0UuCT1nOTa61bV3zX1nrObM1lKSpZ5FgbfdhQ==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.29.0.tgz",
|
||||
"integrity": "sha512-FuLDgE0k1dB4WCBTelw3re9BLmiurgXVSj3d3J4l3sunljyOs941RUU62b7pWP+ePNdGF7RgUMmMAB+58VZCIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"linkifyjs": "^4.3.3"
|
||||
@@ -1013,159 +1000,106 @@
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.30.1.tgz",
|
||||
"integrity": "sha512-LizBu3fWrjifsL7QxKt+NhnHJDVGW/WWL+5asfqyznMqCZTy6tvPJ2W39pUHH2dBaVtzRIkAlsgWjDcNX/ep/Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-item": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.30.1.tgz",
|
||||
"integrity": "sha512-8QqIKvEqXKlT4Cv3ZySHAq1jzKimT/UnFxNBCnkBg+aZBgImvPlRFdN5nFs0elxiSQ7Jtt6k29aOyrkjxgyeew==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.30.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-keymap": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.30.1.tgz",
|
||||
"integrity": "sha512-ju39AnbRyWr1vG4GnMM7BritV/9heGMeH103wccXgJU9hOEqzwmN9zpf5CZqeRnXOjzwQiojrEL+nwm5MnuboA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.30.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-ordered-list": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.30.1.tgz",
|
||||
"integrity": "sha512-kJ4+4I1n4b5jV8OpWH+dbKOlIWTWVPv/fxDQKZuAT/a0DRoowpRoAuGJ0Nk9/Azeqel6c8Ocb4n1J7xLR6Xl2Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.30.1"
|
||||
"@tiptap/core": "3.29.0",
|
||||
"@tiptap/pm": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-paragraph": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.30.1.tgz",
|
||||
"integrity": "sha512-1kAYoyIF88l2qPgHi9ZeS5D5N7TzdJg3FvVCOJvUplQtBIwrsNAx6zaH/16NKN7kRiW7zETMKXMKkhL6MIvnDg==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.29.0.tgz",
|
||||
"integrity": "sha512-OtGrkwzqlW+ehW+d2qrQ7VVcP9PbDeQmuT4Ec4yFjf3vR4QO8OVdVfeOcUq52PtkT3kYRxz9fzEiEvjqRm72yA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-placeholder": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.30.1.tgz",
|
||||
"integrity": "sha512-z0WAMWLF6Hh3C1kuhcYVA3srK0J7d7UNdrY8hw4LlrhmJv1xIoZSUIwH+c8WkMJ5c79idL2zpeWVCtGEyH8UHw==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.29.0.tgz",
|
||||
"integrity": "sha512-V2jb0kL/k3rygAtKmSPYxoyBMCQJtJikPYnxiCYuY7FI1ohmCrmhyhkVqJv0b0EPMNW8BMIqf6DgpKJ/ODmXgA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "3.30.1"
|
||||
"@tiptap/extensions": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-strike": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.30.1.tgz",
|
||||
"integrity": "sha512-vyF8TPTARaN5coGrFwqVREPXDONE4SXJMIxo9qMaBJWl7723m+Enpy7qmFscnwifW5XTpOA8jKl/QJ7qud/ljw==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.29.0.tgz",
|
||||
"integrity": "sha512-VzasZkckrEXzmFQT9z3O95rUWy1b0RLFHAC6GUSQHxKk4TpP+Ujfn5+rqmPukMn0eScJyw1Y36jwfJc0VuNR+Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.30.1.tgz",
|
||||
"integrity": "sha512-Zt3Ik95ZKJx5YNzC6TUXWTNBHUfRH/qVENmqfPjA7x7q4cqNdOEU+tX9DrlFjgxu/x0k48dlQS0Kaop24/vihA==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.29.0.tgz",
|
||||
"integrity": "sha512-AMsf2V7IiGvzbl+LqLLfyWzufazZxHuid1gOnEt3KwolQ9AO7p8WJgHld0PZLxNyEh4SaSqK7b3d3SM9sqrlTw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text-align": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.30.1.tgz",
|
||||
"integrity": "sha512-oYAYySpwcgcHSdZw8xgMqF+sCNUgM7v/2UQj0amsBOLbk5VMfiLdJp5Ku+z5aqCwBBUjb6sInh1Amzd3q57w4A==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.29.0.tgz",
|
||||
"integrity": "sha512-/1htV3teqRJ7i7trAuORDg62LV0CQ1qskbMcP3SB7grsw6H1G54zwdaJVXeiIXmQKDF7FoZgY7u/v/TJckthEw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-underline": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.30.1.tgz",
|
||||
"integrity": "sha512-Xro/EzEgWW+46ztrw5f4epDWQvcGpD+HUykzigj30bpIS8Vv4XDXFt+89dAcyDC7zRUMQULlWZV0UCyRLFEKBw==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.29.0.tgz",
|
||||
"integrity": "sha512-DSPTogdvxmoX/L0U5KgBC9jWffRvYkGiyh1tp28gn+QaARh4iJn79pAZpi1m1bEhal/0Mq5m4ukbiO7FWInwaA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1"
|
||||
"@tiptap/core": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extensions": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.30.1.tgz",
|
||||
"integrity": "sha512-prNrvF1oMj+tngCLmZgXzQfuUuvYMbCe2sEPXXKJPqJNNffHd2wAuUZ0h6UQS5aRILaxB2kN1/yfwpxnhRpwYA==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.29.0.tgz",
|
||||
"integrity": "sha512-ltlrm8dDHIgeNj3cOLEdLFMPPVy3TYvWA8ftrrJ44C/L01MBmgFB1f/vkPFYYnAasb2BYyVG6HxAGcTQHo5jHw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1"
|
||||
"@tiptap/core": "3.29.0",
|
||||
"@tiptap/pm": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.30.1.tgz",
|
||||
"integrity": "sha512-2bHY+ihexKpfBURbAD+ahY0+lWle0TueTK80GAGYqZ/G7ErAPOmzN5RVAGMyZJI4wDkcFycRNLi3k0/3VT9a1A==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.29.0.tgz",
|
||||
"integrity": "sha512-4rr3HiZ8kbSNINuWXqKQJLv9fFMypCBN5gOwxoU+D4lEYVblkoM60fA1SlrI2BZVlZQmw2U38XKl5XMtUr55XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.4.1",
|
||||
@@ -1188,58 +1122,124 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.30.1.tgz",
|
||||
"integrity": "sha512-6P2WEp2ZHsx8LV6hQ3K/MB3n39oiVhKESlzh0v+KhK8RR/iKSKGxvTFbi1D/1fo4j3fZf+43PGHjyKXskn9ZmQ==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.29.0.tgz",
|
||||
"integrity": "sha512-J3jTp3/WXnnL58TCtdMsfeQ2BeycFYrAib6Nbpjd9G0OLUBE7AILiH2HrobRQpvRbAUgD5SW+sBZYOonbWWqNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/extension-blockquote": "3.30.1",
|
||||
"@tiptap/extension-bold": "3.30.1",
|
||||
"@tiptap/extension-bullet-list": "3.30.1",
|
||||
"@tiptap/extension-code": "3.30.1",
|
||||
"@tiptap/extension-code-block": "3.30.1",
|
||||
"@tiptap/extension-document": "3.30.1",
|
||||
"@tiptap/extension-dropcursor": "3.30.1",
|
||||
"@tiptap/extension-gapcursor": "3.30.1",
|
||||
"@tiptap/extension-hard-break": "3.30.1",
|
||||
"@tiptap/extension-heading": "3.30.1",
|
||||
"@tiptap/extension-horizontal-rule": "3.30.1",
|
||||
"@tiptap/extension-italic": "3.30.1",
|
||||
"@tiptap/extension-link": "3.30.1",
|
||||
"@tiptap/extension-list": "3.30.1",
|
||||
"@tiptap/extension-list-item": "3.30.1",
|
||||
"@tiptap/extension-list-keymap": "3.30.1",
|
||||
"@tiptap/extension-ordered-list": "3.30.1",
|
||||
"@tiptap/extension-paragraph": "3.30.1",
|
||||
"@tiptap/extension-strike": "3.30.1",
|
||||
"@tiptap/extension-text": "3.30.1",
|
||||
"@tiptap/extension-underline": "3.30.1",
|
||||
"@tiptap/extensions": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1"
|
||||
"@tiptap/core": "^3.29.0",
|
||||
"@tiptap/extension-blockquote": "^3.29.0",
|
||||
"@tiptap/extension-bold": "^3.29.0",
|
||||
"@tiptap/extension-bullet-list": "^3.29.0",
|
||||
"@tiptap/extension-code": "^3.29.0",
|
||||
"@tiptap/extension-code-block": "^3.29.0",
|
||||
"@tiptap/extension-document": "^3.29.0",
|
||||
"@tiptap/extension-dropcursor": "^3.29.0",
|
||||
"@tiptap/extension-gapcursor": "^3.29.0",
|
||||
"@tiptap/extension-hard-break": "^3.29.0",
|
||||
"@tiptap/extension-heading": "^3.29.0",
|
||||
"@tiptap/extension-horizontal-rule": "^3.29.0",
|
||||
"@tiptap/extension-italic": "^3.29.0",
|
||||
"@tiptap/extension-link": "^3.29.0",
|
||||
"@tiptap/extension-list": "^3.29.0",
|
||||
"@tiptap/extension-list-item": "^3.29.0",
|
||||
"@tiptap/extension-list-keymap": "^3.29.0",
|
||||
"@tiptap/extension-ordered-list": "^3.29.0",
|
||||
"@tiptap/extension-paragraph": "^3.29.0",
|
||||
"@tiptap/extension-strike": "^3.29.0",
|
||||
"@tiptap/extension-text": "^3.29.0",
|
||||
"@tiptap/extension-underline": "^3.29.0",
|
||||
"@tiptap/extensions": "^3.29.0",
|
||||
"@tiptap/pm": "^3.29.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-bullet-list": {
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.29.0.tgz",
|
||||
"integrity": "sha512-1045sC5CRn7KD0wluxZeksBwpm2k3LKBUOM3kNwExHAPSV4in4gdarYJ2/WCmIKsmyj2FzUcF4rbpLLSB/dBSg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-list": {
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.29.0.tgz",
|
||||
"integrity": "sha512-k0B/+nIkn4VvHSQ0kP+AzzAmgeOVxKMAdqG4a6qwxp/lR12aJGHlOP92KCjXV2RNPtuwDksJ1RIXrgxdf9WmJg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.29.0",
|
||||
"@tiptap/pm": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-list-item": {
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.29.0.tgz",
|
||||
"integrity": "sha512-JSz27OIDHWDL7uw28E4W3eaN3H1u+NzJQKZkNXo5Qsvvsx8m/YIQWO9XlhiL3V4TZMjfvlc94lUp5tKW5KwETw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-list-keymap": {
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.29.0.tgz",
|
||||
"integrity": "sha512-g14QLZR9fmfJIL+X2R/cVTvQS0Mp7CoenA9PO+8adTjK7hCeB8hA3UAakMB4ekkElOXNOtb16e0wqulDcoqANg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-ordered-list": {
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.29.0.tgz",
|
||||
"integrity": "sha512-9TXUDFagkGeu6Bo8L0b2SAVAYrY2Xzd28MXwiDxYvDKMjEGWkkCUpMiEI5Qav3GnfFWLmS3jI8PgUjxIw9wLLQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.29.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/vue-3": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.30.1.tgz",
|
||||
"integrity": "sha512-Y7YUkC+LpHcnraN0GwJtySh7XZnveqde5SsHV5X7h1erflp+WT7rG52sE0WKyFOSc3t9ZKSJ/4IBUtB3+ZQHrA==",
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.29.0.tgz",
|
||||
"integrity": "sha512-OFESbEFlmDYPns5LrkyeKggis6tYICBvz+jrlVwxICCWJqWZJJvYb7y/uMtMCxFF1nAeGb8AFr8UhFrLaX5aiA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tiptap/extension-bubble-menu": "^3.30.1",
|
||||
"@tiptap/extension-floating-menu": "^3.30.1"
|
||||
"@tiptap/extension-bubble-menu": "^3.29.0",
|
||||
"@tiptap/extension-floating-menu": "^3.29.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"@tiptap/core": "3.30.1",
|
||||
"@tiptap/pm": "3.30.1",
|
||||
"@tiptap/core": "3.29.0",
|
||||
"@tiptap/pm": "3.29.0",
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 { documentsSelectorComponent } from '@/services/documentsSelector'
|
||||
import ComposerToolbar from '@/components/composer/ComposerToolbar.vue'
|
||||
import ComposerSender from '@/components/composer/ComposerSender.vue'
|
||||
import ComposerRecipients from '@/components/composer/ComposerRecipients.vue'
|
||||
@@ -49,8 +48,6 @@ const {
|
||||
const showCc = ref(false)
|
||||
const showBcc = ref(false)
|
||||
const applyingDraftToEditor = ref(false)
|
||||
const showDocumentSelector = ref(false)
|
||||
const documentSelector = computed(() => documentsSelectorComponent())
|
||||
|
||||
const sender = computed({
|
||||
get: () => activeDraft.value?.sender ?? null,
|
||||
@@ -169,23 +166,8 @@ const handleSend = async () => {
|
||||
await compositionStore.sendDraft()
|
||||
}
|
||||
|
||||
const handleAttachFromDevice = async (files: File[]) => {
|
||||
await compositionStore.addAttachmentsFromDevice(files)
|
||||
}
|
||||
|
||||
const handleAttachFromDocuments = () => {
|
||||
showDocumentSelector.value = true
|
||||
}
|
||||
|
||||
const handleDocumentsSelected = async (items: { kind: 'entity' | 'collection'; identifier: string; name: string; mimeType?: string; size?: number }[]) => {
|
||||
// The dialog defaults to entities-only (no `selectable` prop passed below), but
|
||||
// filter defensively since mail can't attach a folder as a single file blob.
|
||||
const files = items.filter(item => item.kind === 'entity')
|
||||
if (files.length === 0) {
|
||||
return
|
||||
}
|
||||
await compositionStore.addAttachmentsFromDocuments(files)
|
||||
showDocumentSelector.value = false
|
||||
const handleAttach = async (files: File[]) => {
|
||||
await compositionStore.addAttachments(files)
|
||||
}
|
||||
|
||||
const handleDetach = async (identifier: string) => {
|
||||
@@ -282,18 +264,9 @@ const isActive = (name: string, attrs?: any) => {
|
||||
@bullet-list="toggleBulletList"
|
||||
@ordered-list="toggleOrderedList"
|
||||
@link="toggleLink"
|
||||
@attach-from-device="handleAttachFromDevice"
|
||||
@attach-from-documents="handleAttachFromDocuments"
|
||||
@attach="handleAttach"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<component
|
||||
:is="documentSelector"
|
||||
v-if="documentSelector"
|
||||
v-model="showDocumentSelector"
|
||||
:selectable="['entity']"
|
||||
@select="handleDocumentsSelected"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { ref } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { EditorContent, type Editor } from '@tiptap/vue-3'
|
||||
import { documentsSelectorAvailable } from '@/services/documentsSelector'
|
||||
|
||||
defineProps({
|
||||
editor: {
|
||||
@@ -42,8 +41,7 @@ const emit = defineEmits<{
|
||||
bulletList: []
|
||||
orderedList: []
|
||||
link: []
|
||||
attachFromDevice: [files: File[]]
|
||||
attachFromDocuments: []
|
||||
attach: [files: File[]]
|
||||
}>()
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
@@ -56,7 +54,7 @@ function handleFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement | null
|
||||
const files = input?.files ? Array.from(input.files) : []
|
||||
if (files.length > 0) {
|
||||
emit('attachFromDevice', files)
|
||||
emit('attach', files)
|
||||
}
|
||||
if (input) {
|
||||
input.value = ''
|
||||
@@ -144,11 +142,6 @@ function handleFileChange(event: Event) {
|
||||
<v-icon>mdi-paperclip</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Attach Files</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="documentsSelectorAvailable()" icon size="small" @click="$emit('attachFromDocuments')">
|
||||
<v-icon>mdi-folder-search-outline</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Attach from Documents</v-tooltip>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-divider />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ComposerSenderIdentity } from '@/types/composer';
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: ComposerSenderIdentity | null
|
||||
@@ -27,6 +27,11 @@ const items = computed<SenderListItem[]>(() => {
|
||||
|
||||
const selectedValue = computed(() => props.modelValue?.address ?? null)
|
||||
|
||||
const singleOptionLabel = computed(() => {
|
||||
const onlyOption = props.options[0]
|
||||
return onlyOption ? formatSenderLabel(onlyOption.label, onlyOption.address) : ''
|
||||
})
|
||||
|
||||
const errorMessages = computed(() => {
|
||||
if (props.options.length === 0) {
|
||||
return ['No send-capable account is available.']
|
||||
@@ -35,18 +40,6 @@ const errorMessages = computed(() => {
|
||||
return []
|
||||
})
|
||||
|
||||
// Auto-select (and keep selected) the only send-capable account, since
|
||||
// there's no "from" UI shown for the user to pick it themselves.
|
||||
watch(
|
||||
() => props.options,
|
||||
options => {
|
||||
if (options.length === 1 && props.modelValue?.address !== options[0].address) {
|
||||
emit('update:modelValue', options[0])
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function handleUpdate(value: string | null) {
|
||||
const match = value ? props.options.find(o => o.address === value) ?? null : null
|
||||
emit('update:modelValue', match)
|
||||
@@ -58,8 +51,19 @@ function formatSenderLabel(label: string | null | undefined, address: string): s
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="options.length !== 1" class="composer-sender px-4 pt-4 pb-0">
|
||||
<div class="composer-sender px-4 pt-4 pb-0">
|
||||
<v-text-field
|
||||
v-if="options.length === 1"
|
||||
:model-value="singleOptionLabel"
|
||||
label="From"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
readonly
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-else
|
||||
:model-value="selectedValue"
|
||||
:items="items"
|
||||
item-title="title"
|
||||
|
||||
@@ -40,11 +40,10 @@ const handleAccountSaved = async () => {
|
||||
<v-list-item
|
||||
v-for="service in servicesStore.services"
|
||||
:key="`${service.provider}:${service.identifier}`"
|
||||
:class="{ 'opacity-60': !service.enabled }"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar :color="service.enabled ? 'primary' : 'grey'">
|
||||
<v-icon>{{ service.enabled ? 'mdi-email' : 'mdi-email-off' }}</v-icon>
|
||||
<v-avatar color="primary">
|
||||
<v-icon>mdi-email</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* documentsSelector — bridges the Mail composer to documents_manager's
|
||||
* document/collection picker dialog, resolved at runtime via the integration
|
||||
* store. documents_manager (not the end-user `documents` app) registers this,
|
||||
* since it's the module designed to be depended on by other feature modules;
|
||||
* this mirrors CompositionManager's own ModuleManager-based check on the
|
||||
* backend, which also targets documents_manager.
|
||||
*/
|
||||
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { useIntegrationStore } from '@KTXC'
|
||||
|
||||
const DOCUMENTS_SELECTOR_POINT = 'documents_selector'
|
||||
|
||||
/** True if a document selector dialog has been registered (i.e. documents_manager is enabled). */
|
||||
export function documentsSelectorAvailable(): boolean {
|
||||
return useIntegrationStore().getItems(DOCUMENTS_SELECTOR_POINT).length > 0
|
||||
}
|
||||
|
||||
export function documentsSelectorComponent() {
|
||||
// The integration store prefixes ids with the module handle
|
||||
// (e.g. 'documents_manager.dialog'), so match on the suffix.
|
||||
const entry = useIntegrationStore().getItems(DOCUMENTS_SELECTOR_POINT).find(i => i.id.endsWith('.dialog'))
|
||||
return entry?.component
|
||||
? defineAsyncComponent(entry.component as () => Promise<unknown>)
|
||||
: null
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
import { ComposerMode } from '@/types/composer'
|
||||
import type { ComposerDraft, ComposerDraftAttachment, ComposerDraftMessage, ComposerSenderIdentity } from '@/types/composer'
|
||||
import { EntityObject, MessageAddressObject, ServiceObject } from '@MailManager/models'
|
||||
import type { EntityIdentifier } from '@MailManager/types/common'
|
||||
import type { MessageAddressInterface } from '@MailManager/types/message'
|
||||
|
||||
export const useMailCompositionStore = defineStore('mailCompositionStore', () => {
|
||||
@@ -275,7 +274,7 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
||||
queueSave(false)
|
||||
}
|
||||
|
||||
async function addAttachmentsFromDevice(files: File[]) {
|
||||
async function addAttachments(files: File[]) {
|
||||
if (!activeDraft.value || files.length === 0) {
|
||||
return
|
||||
}
|
||||
@@ -289,7 +288,7 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
||||
return [identifier, {
|
||||
identifier,
|
||||
composition,
|
||||
origin: 'device',
|
||||
origin: 'upload',
|
||||
name: file.name,
|
||||
type: file.type || 'application/octet-stream',
|
||||
size: file.size,
|
||||
@@ -328,66 +327,6 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
||||
})
|
||||
}
|
||||
|
||||
async function addAttachmentsFromDocuments(items: { identifier: string; name: string; mimeType?: string; size?: number }[]) {
|
||||
if (!activeDraft.value || items.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const composition = activeDraft.value.identifier
|
||||
const attachments = Object.fromEntries(
|
||||
items.map(item => {
|
||||
const identifier = createIdentifier()
|
||||
return [identifier, {
|
||||
identifier,
|
||||
composition,
|
||||
origin: 'documents',
|
||||
name: item.name,
|
||||
type: item.mimeType || 'application/octet-stream',
|
||||
size: item.size ?? 0,
|
||||
source: item.identifier as EntityIdentifier,
|
||||
} satisfies CompositionAttachmentInterface]
|
||||
}),
|
||||
)
|
||||
|
||||
const request: CompositionAttachmentAddRequest = {
|
||||
composition,
|
||||
attachments,
|
||||
}
|
||||
|
||||
const response = await compositionService.attachmentAdd(request)
|
||||
|
||||
if (!activeDraft.value || response.composition !== activeDraft.value.identifier) {
|
||||
return
|
||||
}
|
||||
|
||||
if (response.disposition === 'error') {
|
||||
console.error('[Mail][Composer] Failed to add attachments from documents:', response.error)
|
||||
mailUiStore.notify(`Failed to attach documents: ${response.error?.message || 'Unknown error'}`, 'error')
|
||||
return
|
||||
}
|
||||
|
||||
Object.values(response.attachments).forEach(attachment => {
|
||||
activeDraft.value!.attachments[attachment.identifier] = {
|
||||
identifier: attachment.identifier,
|
||||
composition: attachment.composition,
|
||||
origin: attachment.origin,
|
||||
name: attachment.name,
|
||||
type: attachment.type,
|
||||
size: attachment.size,
|
||||
source: attachment.source,
|
||||
}
|
||||
})
|
||||
|
||||
if (response.failed?.length) {
|
||||
mailUiStore.notify(
|
||||
response.failed.length === 1
|
||||
? '1 document could not be attached'
|
||||
: `${response.failed.length} documents could not be attached`,
|
||||
'warning',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAttachment(identifier: string) {
|
||||
if (!activeDraft.value) {
|
||||
return
|
||||
@@ -485,14 +424,17 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
||||
updateRecipients,
|
||||
updateSubject,
|
||||
updateBody,
|
||||
addAttachmentsFromDevice,
|
||||
addAttachmentsFromDocuments,
|
||||
addAttachments,
|
||||
removeAttachment,
|
||||
flushSave,
|
||||
}
|
||||
})
|
||||
|
||||
function buildDraft(mode: ComposerMode, sender: ComposerSenderIdentity, source?: EntityObject | MessageAddressObject | null): ComposerDraft {
|
||||
function buildDraft(
|
||||
mode: ComposerMode,
|
||||
sender: ComposerSenderIdentity,
|
||||
source?: EntityObject | MessageAddressObject | null,
|
||||
): ComposerDraft {
|
||||
const composition = createIdentifier()
|
||||
const freshMessage = buildMessage(mode, source)
|
||||
const freshAttachments = buildAttachments(composition, mode, source)
|
||||
@@ -510,7 +452,10 @@ function buildDraft(mode: ComposerMode, sender: ComposerSenderIdentity, source?:
|
||||
}
|
||||
}
|
||||
|
||||
function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressObject | null | undefined): ComposerDraftMessage {
|
||||
function buildMessage(
|
||||
mode: ComposerMode,
|
||||
source: EntityObject | MessageAddressObject | null | undefined,
|
||||
): ComposerDraftMessage {
|
||||
|
||||
if (!source) {
|
||||
return emptyMessage()
|
||||
@@ -576,7 +521,7 @@ function buildAttachments(composition: string, mode: ComposerMode, source?: Enti
|
||||
attachments[id] = {
|
||||
identifier: id,
|
||||
composition: composition,
|
||||
origin: 'message',
|
||||
origin: 'source',
|
||||
name: attachment.name || '',
|
||||
type: attachment.type || 'application/octet-stream',
|
||||
size: attachment.size || 0,
|
||||
|
||||
@@ -11,7 +11,7 @@ export enum ComposerMode {
|
||||
export interface ComposerDraftAttachment {
|
||||
identifier: string
|
||||
composition: string
|
||||
origin: 'device' | 'message' | 'documents'
|
||||
origin: 'source' | 'upload'
|
||||
name: string
|
||||
type: string
|
||||
size: number
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface CompositionMessageInterface {
|
||||
export interface CompositionAttachmentInterface {
|
||||
identifier: string
|
||||
composition: string
|
||||
origin: 'device' | 'message' | 'documents'
|
||||
origin: 'source' | 'upload'
|
||||
name: string
|
||||
type: string
|
||||
size: number
|
||||
@@ -94,17 +94,13 @@ export interface CompositionAttachmentAddRequest {
|
||||
}
|
||||
|
||||
export interface CompositionAttachmentAddResponse {
|
||||
disposition: 'added' | 'partial' | 'error'
|
||||
disposition: 'added' | 'error'
|
||||
error?: {
|
||||
type: string
|
||||
message: string
|
||||
}
|
||||
composition: string
|
||||
attachments: Record<string, CompositionAttachmentInterface>
|
||||
failed?: {
|
||||
identifier: string
|
||||
message: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface CompositionAttachmentRemoveRequest {
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXT\Mail\Tests\Unit;
|
||||
|
||||
use KTXC\Module\ModuleManager;
|
||||
use KTXC\Module\ModuleObject;
|
||||
use KTXF\Documents\Entity\EntityBaseInterface;
|
||||
use KTXF\Documents\Entity\EntityPropertiesBaseInterface;
|
||||
use KTXM\Mail\CompositionManager;
|
||||
use KTXM\Mail\Stores\CompositionStore;
|
||||
use PHPUnit\Framework\MockObject\Stub;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
// KTXM\MailManager\Manager and KTXM\DocumentsManager\Manager live in sibling
|
||||
// modules. This module's isolated test bootstrap only autoloads its own PSR-4
|
||||
// map plus the server's KTXC/KTXF namespaces (see tests/php/bootstrap.php), so
|
||||
// sibling-module classes are not otherwise resolvable here. Require them
|
||||
// directly so PHPUnit can reflect on the real classes to build mocks; both
|
||||
// files only depend on already-autoloadable KTXC/KTXF types. Skipped
|
||||
// gracefully in setUp() if this module is ever checked out standalone.
|
||||
$mailManagerFile = dirname(__DIR__, 4) . '/mail_manager/lib/Manager.php';
|
||||
$documentsManagerFile = dirname(__DIR__, 4) . '/documents_manager/lib/Manager.php';
|
||||
if (is_file($mailManagerFile)) {
|
||||
require_once $mailManagerFile;
|
||||
}
|
||||
if (is_file($documentsManagerFile)) {
|
||||
require_once $documentsManagerFile;
|
||||
}
|
||||
|
||||
final class CompositionManagerTest extends TestCase {
|
||||
|
||||
private const TENANT_ID = 'tenant-1';
|
||||
private const USER_ID = 'user-1';
|
||||
|
||||
private string $rootDir;
|
||||
private CompositionStore $compositionStore;
|
||||
|
||||
protected function setUp(): void {
|
||||
if (!class_exists(\KTXM\MailManager\Manager::class) || !class_exists(\KTXM\DocumentsManager\Manager::class)) {
|
||||
$this->markTestSkipped('mail_manager and documents_manager modules are not available alongside this checkout');
|
||||
}
|
||||
|
||||
$this->rootDir = sys_get_temp_dir() . '/ktrix-mail-composition-test-' . uniqid();
|
||||
mkdir($this->rootDir, 0755, true);
|
||||
$this->compositionStore = new CompositionStore($this->rootDir);
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
$this->deleteDir($this->rootDir);
|
||||
}
|
||||
|
||||
public function testAttachmentAddFromDocumentsHappyPath(): 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);
|
||||
|
||||
$result = $manager->attachmentAdd(self::TENANT_ID, self::USER_ID, $compositionId, [
|
||||
'identifier' => 'attach-1',
|
||||
'origin' => 'documents',
|
||||
'source' => 'default:personal:root:doc1',
|
||||
]);
|
||||
|
||||
$this->assertSame('added', $result['disposition']);
|
||||
$this->assertCount(1, $result['attachments']);
|
||||
$this->assertSame([], $result['failed']);
|
||||
|
||||
$attachment = $result['attachments']['attach-1'];
|
||||
$this->assertSame('documents', $attachment['origin']);
|
||||
$this->assertSame('default:personal:root:doc1', $attachment['source']);
|
||||
$this->assertSame('hello.txt', $attachment['name']);
|
||||
$this->assertSame('text/plain', $attachment['type']);
|
||||
$this->assertSame(strlen('hello world'), $attachment['size']);
|
||||
}
|
||||
|
||||
public function testAttachmentAddFromDocumentsPartialFailure(): void {
|
||||
$compositionId = $this->stageEmptyComposition();
|
||||
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
||||
// only doc1 is returned even though doc1 and doc2 are both requested
|
||||
$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);
|
||||
|
||||
$result = $manager->attachmentAdd(
|
||||
self::TENANT_ID,
|
||||
self::USER_ID,
|
||||
$compositionId,
|
||||
['identifier' => 'attach-1', 'origin' => 'documents', 'source' => 'default:personal:root:doc1'],
|
||||
['identifier' => 'attach-2', 'origin' => 'documents', 'source' => 'default:personal:root:doc2'],
|
||||
);
|
||||
|
||||
$this->assertSame('partial', $result['disposition']);
|
||||
$this->assertCount(1, $result['attachments']);
|
||||
$this->assertCount(1, $result['failed']);
|
||||
$this->assertSame('default:personal:root:doc2', $result['failed'][0]['identifier']);
|
||||
}
|
||||
|
||||
public function testAttachmentAddMixedUploadAndDocuments(): 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);
|
||||
|
||||
$result = $manager->attachmentAdd(
|
||||
self::TENANT_ID,
|
||||
self::USER_ID,
|
||||
$compositionId,
|
||||
['identifier' => 'attach-upload', 'origin' => 'device', 'name' => 'upload.txt', 'type' => 'text/plain', 'data' => base64_encode('uploaded bytes')],
|
||||
['identifier' => 'attach-doc', 'origin' => 'documents', 'source' => 'default:personal:root:doc1'],
|
||||
);
|
||||
|
||||
$this->assertSame('added', $result['disposition']);
|
||||
$this->assertCount(2, $result['attachments']);
|
||||
$this->assertSame('device', $result['attachments']['attach-upload']['origin']);
|
||||
$this->assertSame('documents', $result['attachments']['attach-doc']['origin']);
|
||||
}
|
||||
|
||||
public function testAttachmentAddCompositionNotFound(): void {
|
||||
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
||||
$manager = $this->makeManager($documentsManager);
|
||||
|
||||
$result = $manager->attachmentAdd(self::TENANT_ID, self::USER_ID, 'unknown-composition', [
|
||||
'identifier' => 'attach-1',
|
||||
'origin' => 'documents',
|
||||
'source' => 'default:personal:root:doc1',
|
||||
]);
|
||||
|
||||
$this->assertSame('error', $result['disposition']);
|
||||
$this->assertSame('composition_not_found', $result['error']['type']);
|
||||
}
|
||||
|
||||
public function testAttachmentAddDocumentsModuleUnavailable(): void {
|
||||
$compositionId = $this->stageEmptyComposition();
|
||||
$documentsManager = $this->createStub(\KTXM\DocumentsManager\Manager::class);
|
||||
// documents_manager is not installed/enabled: attachmentAdd must still
|
||||
// work for the composition itself, just report the sourced entry as failed
|
||||
// rather than throwing (e.g. from a failed DI resolution).
|
||||
$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->assertCount(0, $result['attachments']);
|
||||
$this->assertCount(1, $result['failed']);
|
||||
$this->assertSame('default:personal:root:doc1', $result['failed'][0]['identifier']);
|
||||
$this->assertStringContainsString('not installed or enabled', $result['failed'][0]['message']);
|
||||
}
|
||||
|
||||
private function makeManager(Stub $documentsManager, bool $documentsModuleEnabled = true): CompositionManager {
|
||||
$mailManager = $this->createStub(\KTXM\MailManager\Manager::class);
|
||||
|
||||
$moduleManager = $this->createStub(ModuleManager::class);
|
||||
if ($documentsModuleEnabled) {
|
||||
$module = $this->createStub(ModuleObject::class);
|
||||
$module->method('enabled')->willReturn(true);
|
||||
$moduleManager->method('fetch')->willReturn($module);
|
||||
} else {
|
||||
$moduleManager->method('fetch')->willReturn(null);
|
||||
}
|
||||
|
||||
$container = $this->createStub(ContainerInterface::class);
|
||||
$container->method('has')->willReturn($documentsModuleEnabled);
|
||||
$container->method('get')->willReturn($documentsManager);
|
||||
|
||||
return new CompositionManager(
|
||||
container: $container,
|
||||
moduleManager: $moduleManager,
|
||||
compositionStore: $this->compositionStore,
|
||||
mailManager: $mailManager,
|
||||
);
|
||||
}
|
||||
|
||||
private function stageEmptyComposition(): string {
|
||||
$compositionId = 'draft-' . uniqid();
|
||||
$this->compositionStore->compositionSave(self::TENANT_ID, self::USER_ID, $compositionId, [
|
||||
'identifier' => $compositionId,
|
||||
'attachments' => [],
|
||||
]);
|
||||
return $compositionId;
|
||||
}
|
||||
|
||||
private function makeEntity(string $urn, string $label, string $mime): EntityBaseInterface&Stub {
|
||||
$properties = $this->createStub(EntityPropertiesBaseInterface::class);
|
||||
$properties->method('getLabel')->willReturn($label);
|
||||
$properties->method('getMime')->willReturn($mime);
|
||||
$properties->method('size')->willReturn(0);
|
||||
|
||||
$entity = $this->createStub(EntityBaseInterface::class);
|
||||
$entity->method('urn')->willReturn($urn);
|
||||
$entity->method('getProperties')->willReturn($properties);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/** @return resource */
|
||||
private function makeStream(string $contents) {
|
||||
$resource = fopen('php://memory', 'r+');
|
||||
fwrite($resource, $contents);
|
||||
rewind($resource);
|
||||
return $resource;
|
||||
}
|
||||
|
||||
private function deleteDir(string $path): void {
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (scandir($path) ?: [] as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$child = $path . '/' . $entry;
|
||||
if (is_dir($child)) {
|
||||
$this->deleteDir($child);
|
||||
continue;
|
||||
}
|
||||
|
||||
unlink($child);
|
||||
}
|
||||
|
||||
rmdir($path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user