From a831e58c572dc04c8bdbf177021a8df35053a43f Mon Sep 17 00:00:00 2001 From: Mateo Date: Tue, 23 Jun 2026 22:54:16 +0200 Subject: [PATCH] feat: add attachments to tasks --- .../Controllers/Api/LinkPreviewController.php | 8 +- .../Api/TaskAttachmentController.php | 49 +++ app/Http/Controllers/TaskController.php | 1 + app/Http/Requests/TaskCreateRequest.php | 2 + app/Http/Requests/TaskUpdateRequest.php | 4 + app/Http/Resources/TaskResource.php | 11 + app/Models/Task.php | 8 + app/Models/TaskAttachment.php | 38 ++ app/Services/TaskService.php | 52 ++- ...1_195105_create_task_attachments_table.php | 34 ++ package-lock.json | 314 ++++++++-------- package.json | 12 +- .../dashboard/DashboardAttentionCard.vue | 2 +- .../dashboard/DashboardRecentActivity.vue | 2 +- .../components/tasks/TaskAttachmentsPanel.vue | 342 ++++++++++++++++++ .../js/components/tasks/TaskCreateDialog.vue | 11 +- .../js/components/tasks/TaskEditDialog.vue | 16 +- .../js/components/tasks/TaskEditFormPanel.vue | 19 +- .../components/tasks/TaskRichTextEditor.vue | 106 +++++- resources/js/pages/Tasks.vue | 6 +- resources/js/types/index.d.ts | 12 + routes/tasks.php | 4 + 22 files changed, 860 insertions(+), 193 deletions(-) create mode 100644 app/Http/Controllers/Api/TaskAttachmentController.php create mode 100644 app/Models/TaskAttachment.php create mode 100644 database/migrations/2026_06_21_195105_create_task_attachments_table.php create mode 100644 resources/js/components/tasks/TaskAttachmentsPanel.vue diff --git a/app/Http/Controllers/Api/LinkPreviewController.php b/app/Http/Controllers/Api/LinkPreviewController.php index ec75888..0df6f08 100644 --- a/app/Http/Controllers/Api/LinkPreviewController.php +++ b/app/Http/Controllers/Api/LinkPreviewController.php @@ -31,7 +31,7 @@ public function show(Request $request) 'title' => $host ?: 'Local Link', 'description' => 'Link to local page', 'image' => null, - 'favicon' => asset('favicon.ico'), + 'favicon' => null, ]); } @@ -49,7 +49,7 @@ public function show(Request $request) 'title' => parse_url($url, PHP_URL_HOST), 'description' => 'Failed to fetch link preview', 'image' => null, - 'favicon' => asset('favicon.ico'), + 'favicon' => null, ]; } }); @@ -77,7 +77,7 @@ private function fetchPreviewData(string $url): array 'title' => basename($url) ?: parse_url($url, PHP_URL_HOST), 'description' => 'Link to file: ' . $contentType, 'image' => null, - 'favicon' => asset('favicon.ico'), + 'favicon' => null, ]; } @@ -129,7 +129,7 @@ private function fetchPreviewData(string $url): array if ($favicon) { $favicon = $this->resolveUrl($favicon, $url); } else { - $favicon = asset('favicon.ico'); + $favicon = null; } return [ diff --git a/app/Http/Controllers/Api/TaskAttachmentController.php b/app/Http/Controllers/Api/TaskAttachmentController.php new file mode 100644 index 0000000..c334ab3 --- /dev/null +++ b/app/Http/Controllers/Api/TaskAttachmentController.php @@ -0,0 +1,49 @@ +team_id !== $request->user()->team_id) { + abort(403); + } + + if (!Storage::disk('local')->exists($attachment->path)) { + abort(404); + } + + return response()->file(Storage::disk('local')->path($attachment->path)); + + // if we want the browser not to cache the file, we can use the following code instead of response()->file(): + // example if we sxitch teams we should not see the previous teams attachments in the browser cache + // ->path($attachment->path), [ + // 'Cache-Control' => 'private, no-cache, no-store, must-revalidate', + // 'Pragma' => 'no-cache', + // 'Expires' => '0', + // ]); + } + + public function destroy(Request $request, string $id) + { + $attachment = TaskAttachment::findOrFail($id); + + if ($attachment->team_id !== $request->user()->team_id) { + abort(403); + } + + Storage::disk('local')->delete($attachment->path); + $attachment->delete(); + + return response()->noContent(); + } +} diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index 0839c78..e602eeb 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -76,6 +76,7 @@ public function show(Request $request, Task $task): JsonResponse 'creator:id,name', 'assignee:id,name', 'tags:id,name,color', + 'taskAttachments', 'comments' => fn($query) => $query ->whereNull('parent_id') ->with(['user:id,name', 'replies']), diff --git a/app/Http/Requests/TaskCreateRequest.php b/app/Http/Requests/TaskCreateRequest.php index 1344ade..dfff19b 100644 --- a/app/Http/Requests/TaskCreateRequest.php +++ b/app/Http/Requests/TaskCreateRequest.php @@ -37,6 +37,8 @@ public function rules(): array 'nullable', Rule::exists('columns', 'id')->where(fn($query) => $query->where('team_id', $this->user()?->team_id)), ], + 'attachments' => ['sometimes', 'array'], + 'attachments.*' => ['file', 'max:20480'], // 20MB max per file ]; } diff --git a/app/Http/Requests/TaskUpdateRequest.php b/app/Http/Requests/TaskUpdateRequest.php index c5c6c32..a37749d 100644 --- a/app/Http/Requests/TaskUpdateRequest.php +++ b/app/Http/Requests/TaskUpdateRequest.php @@ -32,6 +32,10 @@ public function rules(): array fn($query) => $query->where('team_id', $this->user()?->team_id) ), ], + 'attachments' => ['sometimes', 'array'], + 'attachments.*' => ['file', 'max:20480'], // 20MB max per file + 'removed_attachment_ids' => ['sometimes', 'array'], + 'removed_attachment_ids.*' => ['uuid'], ]; } } diff --git a/app/Http/Resources/TaskResource.php b/app/Http/Resources/TaskResource.php index 22b2827..710638f 100644 --- a/app/Http/Resources/TaskResource.php +++ b/app/Http/Resources/TaskResource.php @@ -189,6 +189,17 @@ public function toArray(Request $request): array 'color' => $tag->color, ])->values() ), + 'attachments' => $this->whenLoaded( + 'taskAttachments', + fn() => $this->taskAttachments + ->map(fn($a) => [ + 'id' => $a->id, + 'filename' => $a->filename, + 'mime_type' => $a->mime_type, + 'size' => $a->size, + 'url' => route('attachments.show', $a->id), + ])->values() + ), ]; } } diff --git a/app/Models/Task.php b/app/Models/Task.php index bd4eda2..268eef2 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -100,6 +100,14 @@ public function tags(): BelongsToMany return $this->belongsToMany(Tag::class, 'task_tag'); } + /** + * Get the file attachments for the task. + */ + public function taskAttachments(): HasMany + { + return $this->hasMany(TaskAttachment::class)->orderBy('created_at'); + } + /** * Scope a query to filter tasks based on array parameters. * diff --git a/app/Models/TaskAttachment.php b/app/Models/TaskAttachment.php new file mode 100644 index 0000000..2ede11b --- /dev/null +++ b/app/Models/TaskAttachment.php @@ -0,0 +1,38 @@ +belongsTo(Team::class); + } + + public function task(): BelongsTo + { + return $this->belongsTo(Task::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Services/TaskService.php b/app/Services/TaskService.php index 8038ff7..9fe6a59 100644 --- a/app/Services/TaskService.php +++ b/app/Services/TaskService.php @@ -4,8 +4,10 @@ use App\Models\Task; use App\Models\Column; +use App\Models\TaskAttachment; use App\Models\User; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; class TaskService { @@ -24,11 +26,12 @@ public function createTask(array $data, User $user): Task Task::where('column_id', $columnId)->increment('order'); $order = 0; - unset($data['column_id'], $data['tag_ids']); + $newFiles = $data['attachments'] ?? []; + unset($data['column_id'], $data['tag_ids'], $data['attachments']); $columnName = Column::query()->where('id', $columnId)->value('name'); - return DB::transaction(function () use ($data, $user, $columnId, $order, $columnName, $tagIds) { + return DB::transaction(function () use ($data, $user, $columnId, $order, $columnName, $tagIds, $newFiles) { $task = Task::create(array_merge($data, [ 'team_id' => $user->team_id, 'created_by' => $user->id, @@ -65,6 +68,20 @@ public function createTask(array $data, User $user): Task ]); } + foreach ($newFiles as $file) { + $path = $file->store('attachments', 'local'); + + TaskAttachment::create([ + 'team_id' => $user->team_id, + 'task_id' => $task->id, + 'user_id' => $user->id, + 'path' => $path, + 'filename' => $file->getClientOriginalName(), + 'mime_type' => $file->getMimeType(), + 'size' => $file->getSize(), + ]); + } + return $task; }); } @@ -75,10 +92,10 @@ public function createTask(array $data, User $user): Task public function updateTask(Task $task, array $data, User $actor): bool { $oldAssignedTo = $task->assigned_to; - - // Extract tag_ids if provided $tagIds = $data['tag_ids'] ?? null; - unset($data['tag_ids']); + $newFiles = $data['attachments'] ?? []; + $removedIds = $data['removed_attachment_ids'] ?? []; + unset($data['tag_ids'], $data['attachments'], $data['removed_attachment_ids']); $updated = $task->update($data); @@ -108,6 +125,31 @@ public function updateTask(Task $task, array $data, User $actor): bool ]); } + if (!empty($removedIds)) { + $toRemove = TaskAttachment::whereIn('id', $removedIds) + ->where('team_id', $actor->team_id) + ->get(); + + foreach ($toRemove as $attachment) { + Storage::disk('local')->delete($attachment->path); + $attachment->delete(); + } + } + + foreach ($newFiles as $file) { + $path = $file->store('attachments', 'local'); + + TaskAttachment::create([ + 'team_id' => $actor->team_id, + 'task_id' => $task->id, + 'user_id' => $actor->id, + 'path' => $path, + 'filename' => $file->getClientOriginalName(), + 'mime_type' => $file->getMimeType(), + 'size' => $file->getSize(), + ]); + } + return true; } diff --git a/database/migrations/2026_06_21_195105_create_task_attachments_table.php b/database/migrations/2026_06_21_195105_create_task_attachments_table.php new file mode 100644 index 0000000..a79d341 --- /dev/null +++ b/database/migrations/2026_06_21_195105_create_task_attachments_table.php @@ -0,0 +1,34 @@ +uuid('id')->primary(); + $table->foreignId('team_id')->constrained()->cascadeOnDelete(); + $table->foreignId('task_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('path'); + $table->string('filename'); + $table->string('mime_type'); + $table->unsignedBigInteger('size'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('task_attachments'); + } +}; diff --git a/package-lock.json b/package-lock.json index fe53655..a32e92e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,12 @@ "dependencies": { "@inertiajs/vite": "^3.0.0", "@inertiajs/vue3": "^3.0.0", - "@tiptap/core": "^3.22.3", - "@tiptap/extension-link": "^3.22.3", - "@tiptap/extension-paragraph": "^3.22.3", - "@tiptap/extension-placeholder": "^3.22.3", - "@tiptap/starter-kit": "^3.22.3", - "@tiptap/vue-3": "^3.22.3", + "@tiptap/core": "^3.27.1", + "@tiptap/extension-link": "^3.27.1", + "@tiptap/extension-paragraph": "^3.27.1", + "@tiptap/extension-placeholder": "^3.27.1", + "@tiptap/starter-kit": "^3.27.1", + "@tiptap/vue-3": "^3.27.1", "@vueuse/core": "^12.8.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -2052,9 +2052,9 @@ } }, "node_modules/@tiptap/core": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.3.tgz", - "integrity": "sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.27.1.tgz", + "integrity": "sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ==", "license": "MIT", "peer": true, "funding": { @@ -2062,39 +2062,39 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^3.22.3" + "@tiptap/pm": "3.27.1" } }, "node_modules/@tiptap/extension-blockquote": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.22.3.tgz", - "integrity": "sha512-IaUx3zh7yLHXzIXKL+fw/jzFhsIImdhJyw0lMhe8FfYrefFqXJFYW/sey6+L/e8B3AWvTksPA6VBwefzbH77JA==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.27.1.tgz", + "integrity": "sha512-VMF7xJx6qEGiX6DTKNiL31NLqypOcd/4sNjFSe8rb41PwejBJh/nOqVIbBvWkiT6NMGFLxMhj7zJ8/zPo1hXeg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-bold": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.22.3.tgz", - "integrity": "sha512-tysipHla2zCWr8XNIWRaW9O+7i7/SoEqnRqSRUUi2ailcJjlia+RBy3RykhkgyThrQDStu5KGBS/UvrXwA+O1A==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.27.1.tgz", + "integrity": "sha512-TlC5bsS+pqETTrlz4CZz9RO/cKBYtELGIxwtKeivUn3eNfnOxQbbu4WDsiwIfzRFyd0OMnKl6BPM2KnYEehoEQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.22.3.tgz", - "integrity": "sha512-Y6zQjh0ypDg32HWgICEvmPSKjGLr39k3aDxxt/H0uQEZSfw4smT0hxUyyyjVjx68C6t6MTnwdfz0hPI5lL68vQ==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.27.1.tgz", + "integrity": "sha512-j/j8Qp9Z5nViade2m7zjrO/CYH/Ca80Qj7aqo0eUaei6FZQ5izlF9o4XQU5EFMAutV6mwynsPUp8FVo5sCuYfw==", "license": "MIT", "optional": true, "dependencies": { @@ -2105,80 +2105,80 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3", - "@tiptap/pm": "^3.22.3" + "@tiptap/core": "3.27.1", + "@tiptap/pm": "3.27.1" } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.22.3.tgz", - "integrity": "sha512-xOmW/b1hgECIE6r3IeZvKn4VVlG3+dfTjCWE6lnnyLaqdNkNhKS1CwUmDZdYNLUS2ryIUtgz5ID1W/8A3PhbiA==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.27.1.tgz", + "integrity": "sha512-faCUHnRP47o9Zh9VZZX6EX/569udw9Vopm2PgEKPWuKLE2qaS5WBuUVU0iItdJmKUqaWiOZkpoW4jvnDmj0dfg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.22.3" + "@tiptap/extension-list": "3.27.1" } }, "node_modules/@tiptap/extension-code": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.22.3.tgz", - "integrity": "sha512-wafWTDQOuMKtXpZEuk1PFQmzopabBciNLryL90MB9S03MNLaQQZYLnmYkDBlzAaLAbgF5QiC+2XZQEBQuTVjFQ==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.27.1.tgz", + "integrity": "sha512-epOUpFfEmBzjvnqvjv2qHX7NAuLo5dlOGV690lWu+sAYMjibuJBeVvAiKPyFCfRCCTUxdbDB3jbaOA1yEcEJ7w==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-code-block": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.22.3.tgz", - "integrity": "sha512-RiQtEjDAPrHpdo6sw6b7fOw/PijqgFIsozKKkGcSeBgWHQuFg7q9OxJTj+l0e60rVwSu/5gmKEEobzM9bX+t2Q==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.27.1.tgz", + "integrity": "sha512-pHlzmZx2OlHfyQ0yRlT5UL4mGokz947DthZuYefN1OleVqOkHpWBG+2JQwqoNq6bmzMne92zbH32rhcJUEYSjA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3", - "@tiptap/pm": "^3.22.3" + "@tiptap/core": "3.27.1", + "@tiptap/pm": "3.27.1" } }, "node_modules/@tiptap/extension-document": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.3.tgz", - "integrity": "sha512-MCSr1PFPtTd++lA3H1RNgqAczAE59XXJ5wUFIQf2F+/0DPY5q2SU4g5QsNJVxPPft5mrNT4C6ty8xBPrALFEdA==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.27.1.tgz", + "integrity": "sha512-8FbBTkfnRP4iVaoj+2h3iWa+H0eGDD3yTyVCwrmue/sQTkqUNUoSuAZa3GDG4Sd41xdPwTJxl9nUWGgM1qDCnw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.22.3.tgz", - "integrity": "sha512-taXq9Tl5aybdFbptJtFRHX9LFJzbXphAbPp4/vutFyTrBu5meXDxuS+B9pEmE+Or0XcolTlW2nDZB0Tqnr18JQ==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.27.1.tgz", + "integrity": "sha512-blFf9x9RG0Qr7P3FoAH/033ffa+mMLZn34trVs8Vi0Ppk6FmJAg5HpYFOtmYoeREdNDJ5rHJKV7SoACbOHgskQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.22.3" + "@tiptap/extensions": "3.27.1" } }, "node_modules/@tiptap/extension-floating-menu": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.22.3.tgz", - "integrity": "sha512-0f8b4KZ3XKai8GXWseIYJGdOfQr3evtFbBo3U08zy2aYzMMXWG0zEF7qe5/oiYp2aZ95edjjITnEceviTsZkIg==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.27.1.tgz", + "integrity": "sha512-BmJF1VqB7dSJkgAalrpVFj88WLhxKjcWPuWHOqf2ITrUU2832BhKLXKmxjWUy1gqV8PfNNVWtGfIERy7I0y0+Q==", "license": "MIT", "optional": true, "funding": { @@ -2187,97 +2187,97 @@ }, "peerDependencies": { "@floating-ui/dom": "^1.0.0", - "@tiptap/core": "^3.22.3", - "@tiptap/pm": "^3.22.3" + "@tiptap/core": "3.27.1", + "@tiptap/pm": "3.27.1" } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.22.3.tgz", - "integrity": "sha512-L/Px4UeQEVG/D9WIlcAOIej+4wyIBCMUSYicSR+hW68UsObe4rxVbUas1QgidQKm6DOhoT7U7D4KQHA/Gdg/7A==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.27.1.tgz", + "integrity": "sha512-QoezN0wdvXIwLQ4ee2ccWDaX3RG0lzgQpIMpMz55oPDhpUVax1+19ApsS53LkcktpS4EbnPL4xO4DaJk0Vp7PQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.22.3" + "@tiptap/extensions": "3.27.1" } }, "node_modules/@tiptap/extension-hard-break": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.22.3.tgz", - "integrity": "sha512-J0v8I99y9tbvVmgKYKzKP/JYNsWaZYS7avn4rzLft2OhnyTfwt3OoY8DtpHmmi6apSUaCtoWHWta/TmoEfK1nQ==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.27.1.tgz", + "integrity": "sha512-iv/m9hzl6jfSj9Q8UEjAxONvCoUDaP7M9SRCPx3PaLNxA230TTD6RE0Ye4zFJ8ze7ZVoJJMAqg9Qpq1iYg2JOQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-heading": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.22.3.tgz", - "integrity": "sha512-XBHuhiEV2EEhZHpOLcplLqAmBIhJciU3I6AtwmqeEqDC0P114uMEfAO7JGlbBZdCYotNer26PKnu44TBTeNtkw==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.27.1.tgz", + "integrity": "sha512-SrC4l1kEIyv9ZXFaI/8LQqU2MyMmjczw7XXsWUQOTN4YXv0JyVgMNR3cI/wz0d2xsTfBdZ1N85Tdng+Ga1t0Sg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.3.tgz", - "integrity": "sha512-wI2bFzScs+KgWeBH/BtypcVKeYelCyqV0RG8nxsZMWtPrBhqixzNd0Oi3gEKtjSjKUqMQ/kjJAIRuESr5UzlHA==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.27.1.tgz", + "integrity": "sha512-QlKE7qn5qMnIGVGhXQlvYedvLtNJ9z0dmit5w8vPb8tKzW4Spk6M7N2kruprrDA8GBwHfeR5wmF+njfUm34qxg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3", - "@tiptap/pm": "^3.22.3" + "@tiptap/core": "3.27.1", + "@tiptap/pm": "3.27.1" } }, "node_modules/@tiptap/extension-italic": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.22.3.tgz", - "integrity": "sha512-LteA4cb4EGCiUtrK2JHvDF/Zg0/YqV4DUyHhAAho+oGEQDupZlsS6m0ia5wQcclkiTLzsoPrwcSNu6RDGQ16wQ==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.27.1.tgz", + "integrity": "sha512-jGGeyn9uRUnNjSTHpbqhiGsp6KaYTSbV09jDXPJI9cDwfV9hpugLvpaCZd0BMBbhU1B1W6kOfX0BE15qX/HQfA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-link": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.22.3.tgz", - "integrity": "sha512-S8/P2o9pv6B3kqLjH2TRWwSAximGbciNc6R8/QcN6HWLYxp0N0JoqN3rZHl9VWIBAGRWc4zkt80dhqrl2xmgfQ==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.27.1.tgz", + "integrity": "sha512-/2jBfsxBZUDGJmpZifqRQPz7f1E5qpS1BckTZ39TADzUJX+feKy7RJ3DtQ02+8y6SSMzvP9loGVjrk6zEMTk4g==", "license": "MIT", "dependencies": { - "linkifyjs": "^4.3.2" + "linkifyjs": "^4.3.3" }, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3", - "@tiptap/pm": "^3.22.3" + "@tiptap/core": "3.27.1", + "@tiptap/pm": "3.27.1" } }, "node_modules/@tiptap/extension-list": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.3.tgz", - "integrity": "sha512-rqvv/dtqwbX+8KnPv0eMYp6PnBcuhPMol5cv1GlS8Nq/Cxt68EWGUHBuTFesw+hdnRQLmKwzoO1DlRn7PhxYRQ==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.27.1.tgz", + "integrity": "sha512-c2Upru7lj0/ZV/Ibww6cNz6sUS8m6Dp/9uygFhYcZOd3X8M0xBIEk42c6m6SQehkPziVA8QOgNJz7sMqsbz1OQ==", "license": "MIT", "peer": true, "funding": { @@ -2285,118 +2285,118 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3", - "@tiptap/pm": "^3.22.3" + "@tiptap/core": "3.27.1", + "@tiptap/pm": "3.27.1" } }, "node_modules/@tiptap/extension-list-item": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.22.3.tgz", - "integrity": "sha512-80CNf4oO5y8+LdckT4CyMe1t01EyhpRrQC9H45JW20P7559Nrchp5my3vvMtIAJbpTPPZtcB7LwdzWGKsG5drg==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.27.1.tgz", + "integrity": "sha512-zwRl01ETfCkWUvtvK5fw9bXtAajMPkvlkE3Cq6JvH3LF7XXJwDtNj5Tj7exacMpCaSZmlNc43vFb2rAYnrnwMA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.22.3" + "@tiptap/extension-list": "3.27.1" } }, "node_modules/@tiptap/extension-list-keymap": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.22.3.tgz", - "integrity": "sha512-pKuyj5llu35zd/s2u/H9aydKZjmPRAIK5P1q/YXULhhCNln2RnmuRfQ5NklAqTD3yGciQ2lxDwwf7J6iw3ergA==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.27.1.tgz", + "integrity": "sha512-OIMZNlzPSO8WRd4ic73Fxckzl4N1tesjjLL2XApaNA/uMpO0LoF6WSRPAWv+Z24Wp92ARRJAnRP7iZoI5+Jxig==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.22.3" + "@tiptap/extension-list": "3.27.1" } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.22.3.tgz", - "integrity": "sha512-orAghtmd+K4Euu4BgI1hG+iZDXBYOyl5YTwiLBc2mQn+pqtZ9LqaH2us4ETwEwNP3/IWXGSAimUZ19nuL+eM2w==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.27.1.tgz", + "integrity": "sha512-GYrKqD//9nHJ2r80uXqbDMzRnFpGzbaEQRTSGaO/SH7DvXWFMow8evkOdjQ7PCQO07jNjJo75+A85Jwu3Ov3AA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.22.3" + "@tiptap/extension-list": "3.27.1" } }, "node_modules/@tiptap/extension-paragraph": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.22.3.tgz", - "integrity": "sha512-oO7rhfyhEuwm+50s9K3GZPjYyEEEvFAvm1wXopvZnhbkBLydIWImBfrZoC5IQh4/sRDlTIjosV2C+ji5y0tUSg==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.27.1.tgz", + "integrity": "sha512-7K7eo1gruOgAsnbK+GCV23AUVUI0cL1bTig8HaPneoFMVbig7vddk8jNLKBWO8TXVbG7TuHdnDN4F98vdtwh5Q==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-placeholder": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.22.3.tgz", - "integrity": "sha512-7vbtlDVO00odqCnsMSmA4b6wjL5PFdfExFsdsDO0K0VemqHZ/doIRx/tosNUD1VYSOyKQd8U7efUjkFyVoIPlg==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.27.1.tgz", + "integrity": "sha512-lhcNDcczQ75yJOSywCHb58Hmtg1aPL/2TdYmeLPxVrP048D7rRs133sfONcgyyw0AvhnfmPOkHLTv3QtKSowhw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.22.3" + "@tiptap/extensions": "3.27.1" } }, "node_modules/@tiptap/extension-strike": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.22.3.tgz", - "integrity": "sha512-jY2InoUlKkuk5KHoIDGdML1OCA2n6PRHAtxwHNkAmiYh0Khf0zaVPGFpx4dgQrN7W5Q1WE6oBZnjrvy6qb7w0g==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.27.1.tgz", + "integrity": "sha512-Y3DW1jlSlCNCyMGHP3+3qBNNPS83wuFz4RTYGjZtvRRTCRh7apZme9XRWMq1rN5mJ2Cr7fKocA2/5Bs13KgN6Q==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-text": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.3.tgz", - "integrity": "sha512-Q9R7JsTdomP5uUjtPjNKxHT1xoh/i9OJZnmgJLe7FcgZEaPOQ3bWxmKZoLZQfDfZjyB8BtH+Hc7nUvhCMOePxw==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.27.1.tgz", + "integrity": "sha512-6ZwaZwSrDh+KFFv6V1J79oO37yPs7y1bFxvk1/9Ih2rn3Xr5AWz+eMS+n8RpH3djBVVAQpdIAeYQgcn+VCSsTg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extension-underline": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.3.tgz", - "integrity": "sha512-Ch6CBWRa5w90yYSPUW6x9Py9JdrXMqk3pZ9OIlMYD8A7BqyZGfiHerX7XDMYDS09KjyK3U9XH60/zxYOzXdDLA==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.27.1.tgz", + "integrity": "sha512-N889J4nXN/TPfVt8uF9N1A0SY82E90zwc1y26lqOcw6KWNLmQrlhMh/9OD4ikLDbekmFpOBq/UicpHf/6S8hbQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3" + "@tiptap/core": "3.27.1" } }, "node_modules/@tiptap/extensions": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.3.tgz", - "integrity": "sha512-s5eiMq0m5N6N+W7dU6rd60KgZyyCD7FvtPNNswISfPr12EQwJBfbjWwTqd0UKNzA4fNrhQEERXnzORkykttPeA==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.27.1.tgz", + "integrity": "sha512-1Tdx9faw8k0/83V6X+xCDVhV8yElGt95JxeW3YMkKQJI56QdlPz0xOdJPlMiSGJKinPyVier+x9LJD/YZUZIaw==", "license": "MIT", "peer": true, "funding": { @@ -2404,14 +2404,14 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.22.3", - "@tiptap/pm": "^3.22.3" + "@tiptap/core": "3.27.1", + "@tiptap/pm": "3.27.1" } }, "node_modules/@tiptap/pm": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.26.1.tgz", - "integrity": "sha512-48cJQRbvr9Ux0+IgM1BR5vOLU5hkC+n+uerdQy2JjrIRKpYE/huU8fQFm6PoRppoKYfilklzb29elsQ+n2TA+g==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.27.1.tgz", + "integrity": "sha512-Ffjx+vimmBU7zH/KrpXzJid3+pziCe/VL2aexSTP63cyQwKQ65LkFkCKaIsSpFdQQuakVZBGWjCA5RoBV852pw==", "license": "MIT", "peer": true, "dependencies": { @@ -2435,35 +2435,35 @@ } }, "node_modules/@tiptap/starter-kit": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.22.3.tgz", - "integrity": "sha512-vdW/Oo1fdwTL1VOQ5YYbTov00ANeHLquBVEZyL/EkV7Xv5io9rXQsCysJfTSHhiQlyr2MtWFB4+CPGuwXjQWOQ==", - "license": "MIT", - "dependencies": { - "@tiptap/core": "^3.22.3", - "@tiptap/extension-blockquote": "^3.22.3", - "@tiptap/extension-bold": "^3.22.3", - "@tiptap/extension-bullet-list": "^3.22.3", - "@tiptap/extension-code": "^3.22.3", - "@tiptap/extension-code-block": "^3.22.3", - "@tiptap/extension-document": "^3.22.3", - "@tiptap/extension-dropcursor": "^3.22.3", - "@tiptap/extension-gapcursor": "^3.22.3", - "@tiptap/extension-hard-break": "^3.22.3", - "@tiptap/extension-heading": "^3.22.3", - "@tiptap/extension-horizontal-rule": "^3.22.3", - "@tiptap/extension-italic": "^3.22.3", - "@tiptap/extension-link": "^3.22.3", - "@tiptap/extension-list": "^3.22.3", - "@tiptap/extension-list-item": "^3.22.3", - "@tiptap/extension-list-keymap": "^3.22.3", - "@tiptap/extension-ordered-list": "^3.22.3", - "@tiptap/extension-paragraph": "^3.22.3", - "@tiptap/extension-strike": "^3.22.3", - "@tiptap/extension-text": "^3.22.3", - "@tiptap/extension-underline": "^3.22.3", - "@tiptap/extensions": "^3.22.3", - "@tiptap/pm": "^3.22.3" + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.27.1.tgz", + "integrity": "sha512-vfxRsqW8rCc0k4pzo0ilU3wobVi2wqVj88VZI2SlgZlNnUAkrDGDIAph7CTa9k9fshV+O1ivpEgPC5yC046jow==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.27.1", + "@tiptap/extension-blockquote": "^3.27.1", + "@tiptap/extension-bold": "^3.27.1", + "@tiptap/extension-bullet-list": "^3.27.1", + "@tiptap/extension-code": "^3.27.1", + "@tiptap/extension-code-block": "^3.27.1", + "@tiptap/extension-document": "^3.27.1", + "@tiptap/extension-dropcursor": "^3.27.1", + "@tiptap/extension-gapcursor": "^3.27.1", + "@tiptap/extension-hard-break": "^3.27.1", + "@tiptap/extension-heading": "^3.27.1", + "@tiptap/extension-horizontal-rule": "^3.27.1", + "@tiptap/extension-italic": "^3.27.1", + "@tiptap/extension-link": "^3.27.1", + "@tiptap/extension-list": "^3.27.1", + "@tiptap/extension-list-item": "^3.27.1", + "@tiptap/extension-list-keymap": "^3.27.1", + "@tiptap/extension-ordered-list": "^3.27.1", + "@tiptap/extension-paragraph": "^3.27.1", + "@tiptap/extension-strike": "^3.27.1", + "@tiptap/extension-text": "^3.27.1", + "@tiptap/extension-underline": "^3.27.1", + "@tiptap/extensions": "^3.27.1", + "@tiptap/pm": "^3.27.1" }, "funding": { "type": "github", @@ -2471,22 +2471,22 @@ } }, "node_modules/@tiptap/vue-3": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.22.3.tgz", - "integrity": "sha512-wOGZiBwIJYCZXts5VWWGgseGCRMc8tO46tgaOXNyMLVl2h2cO6/CNEcPO2pgtuoLIHoV4KYvfpw6n+XqR9cqbA==", + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.27.1.tgz", + "integrity": "sha512-o5GB6hfUnyf9sCB306rHWmaIYRL+02ROX657EkuY8tEWKHMTuMjHWl2AqHMP47wz0W9DaMOJLvcPpYdAEKq3Mw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "optionalDependencies": { - "@tiptap/extension-bubble-menu": "^3.22.3", - "@tiptap/extension-floating-menu": "^3.22.3" + "@tiptap/extension-bubble-menu": "^3.27.1", + "@tiptap/extension-floating-menu": "^3.27.1" }, "peerDependencies": { "@floating-ui/dom": "^1.0.0", - "@tiptap/core": "^3.22.3", - "@tiptap/pm": "^3.22.3", + "@tiptap/core": "3.27.1", + "@tiptap/pm": "3.27.1", "vue": "^3.0.0" } }, diff --git a/package.json b/package.json index 932a1c5..fb776a1 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,12 @@ "dependencies": { "@inertiajs/vite": "^3.0.0", "@inertiajs/vue3": "^3.0.0", - "@tiptap/core": "^3.22.3", - "@tiptap/extension-link": "^3.22.3", - "@tiptap/extension-paragraph": "^3.22.3", - "@tiptap/extension-placeholder": "^3.22.3", - "@tiptap/starter-kit": "^3.22.3", - "@tiptap/vue-3": "^3.22.3", + "@tiptap/core": "^3.27.1", + "@tiptap/extension-link": "^3.27.1", + "@tiptap/extension-paragraph": "^3.27.1", + "@tiptap/extension-placeholder": "^3.27.1", + "@tiptap/starter-kit": "^3.27.1", + "@tiptap/vue-3": "^3.27.1", "@vueuse/core": "^12.8.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/resources/js/components/dashboard/DashboardAttentionCard.vue b/resources/js/components/dashboard/DashboardAttentionCard.vue index d13d08d..858e874 100644 --- a/resources/js/components/dashboard/DashboardAttentionCard.vue +++ b/resources/js/components/dashboard/DashboardAttentionCard.vue @@ -63,7 +63,7 @@ onBeforeUnmount(() => {
diff --git a/resources/js/components/dashboard/DashboardRecentActivity.vue b/resources/js/components/dashboard/DashboardRecentActivity.vue index 5ddafbf..5311c09 100644 --- a/resources/js/components/dashboard/DashboardRecentActivity.vue +++ b/resources/js/components/dashboard/DashboardRecentActivity.vue @@ -19,7 +19,7 @@ defineProps<{
diff --git a/resources/js/components/tasks/TaskAttachmentsPanel.vue b/resources/js/components/tasks/TaskAttachmentsPanel.vue new file mode 100644 index 0000000..c0b4fa2 --- /dev/null +++ b/resources/js/components/tasks/TaskAttachmentsPanel.vue @@ -0,0 +1,342 @@ + + + diff --git a/resources/js/components/tasks/TaskCreateDialog.vue b/resources/js/components/tasks/TaskCreateDialog.vue index 95e6545..c732f28 100644 --- a/resources/js/components/tasks/TaskCreateDialog.vue +++ b/resources/js/components/tasks/TaskCreateDialog.vue @@ -38,6 +38,7 @@ const form = useForm({ due_date: '', assigned_to: currentUserId.value, tag_ids: [], + attachments: [], }); const selectedTags = ref([]); @@ -48,7 +49,7 @@ const onTagsUpdated = (tags: Tag[]) => { }; const submit = () => { - if (form.isDirty) { + if (form.isDirty || form.attachments.length > 0) { form.submit(store(), { onSuccess: () => { form.reset(); @@ -103,9 +104,13 @@ const submit = () => {
@@ -146,7 +151,7 @@ const submit = () => {