From 1598efac3597bc87f9f294b89feebbcdda9d04b6 Mon Sep 17 00:00:00 2001
From: James Collins
Date: Wed, 19 Jul 2023 14:56:56 +1000
Subject: [PATCH] updated attachments
---
.../Controllers/Api/ArticleController.php | 141 ++----------------
app/Models/Article.php | 10 --
app/Models/Event.php | 12 +-
app/Traits/HasAttachments.php | 63 ++++++++
..._07_19_041445_update_attachments_table.php | 30 ++++
resources/js/components/SMAttachments.vue | 73 +++++++--
resources/js/components/SMHeader.vue | 50 ++++---
.../js/components/SMInputAttachments.vue | 1 -
resources/js/views/Article.vue | 4 +-
resources/js/views/dashboard/ArticleEdit.vue | 22 +--
routes/api.php | 2 +-
11 files changed, 212 insertions(+), 196 deletions(-)
create mode 100644 app/Traits/HasAttachments.php
create mode 100644 database/migrations/2023_07_19_041445_update_attachments_table.php
diff --git a/app/Http/Controllers/Api/ArticleController.php b/app/Http/Controllers/Api/ArticleController.php
index d334c2d..53fedcc 100644
--- a/app/Http/Controllers/Api/ArticleController.php
+++ b/app/Http/Controllers/Api/ArticleController.php
@@ -8,17 +8,14 @@ use App\Enum\HttpResponseCodes;
use App\Http\Requests\ArticleRequest;
use App\Models\Media;
use App\Models\Article;
-use App\Models\Gallery;
+use App\Traits\HasAttachments;
use App\Traits\HasGallery;
use Illuminate\Http\JsonResponse;
-use Carbon\Exceptions\InvalidFormatException;
-use Illuminate\Contracts\Container\BindingResolutionException;
-use Illuminate\Database\Eloquent\InvalidCastException;
-use Illuminate\Database\Eloquent\MassAssignmentException;
use Illuminate\Http\Request;
class ArticleController extends ApiController
{
+ use HasAttachments;
use HasGallery;
@@ -78,7 +75,11 @@ class ArticleController extends ApiController
public function store(ArticleRequest $request)
{
if (ArticleConductor::creatable() === true) {
- $article = Article::create($request->except('gallery'));
+ $article = Article::create($request->except(['attachments', 'gallery']));
+
+ if ($request->has('attachments') === true) {
+ $article->attachmentsAddMany($request->get('attachments'));
+ }
if ($request->has('gallery') === true) {
$article->galleryAddMany($request->get('gallery'));
@@ -103,12 +104,17 @@ class ArticleController extends ApiController
public function update(ArticleRequest $request, Article $article)
{
if (ArticleConductor::updatable($article) === true) {
+ if ($request->has('attachments') === true) {
+ $article->attachments()->delete();
+ $article->attachmentsAddMany($request->get('attachments'));
+ }
+
if ($request->has('gallery') === true) {
$article->gallery()->delete();
$article->galleryAddMany($request->get('gallery'));
}
- $article->update($request->except('gallery'));
+ $article->update($request->except(['attachments', 'gallery']));
return $this->respondAsResource(ArticleConductor::model($request, $article));
}
@@ -130,125 +136,4 @@ class ArticleController extends ApiController
return $this->respondForbidden();
}
}
-
- /**
- * Get a list of attachments related to this model.
- *
- * @param Request $request The user request.
- * @param Article $article The article model.
- * @return JsonResponse Returns the article attachments.
- */
- public function attachmentIndex(Request $request, Article $article): JsonResponse
- {
- if (ArticleConductor::viewable($article) === true) {
- $medium = $article->attachments->map(function ($attachment) {
- return $attachment->media;
- });
-
- return $this->respondAsResource(MediaConductor::collection($request, $medium), ['isCollection' => true, 'resourceName' => 'attachment']);
- }
-
- return $this->respondForbidden();
- }
-
- /**
- * Store an attachment related to this model.
- *
- * @param Request $request The user request.
- * @param Article $article The article model.
- * @return JsonResponse The response.
- */
- public function attachmentStore(Request $request, Article $article): JsonResponse
- {
- if (ArticleConductor::updatable($article) === true) {
- if ($request->has("medium") === true && Media::find($request->medium) !== null) {
- $article->attachments()->create(['media_id' => $request->medium]);
- return $this->respondCreated();
- }
-
- return $this->respondWithErrors(['media' => 'The media ID was not found']);
- }
-
- return $this->respondForbidden();
- }
-
- /**
- * Update/replace attachments related to this model.
- *
- * @param Request $request The user request.
- * @param Article $article The related model.
- * @return JsonResponse
- */
- public function attachmentUpdate(Request $request, Article $article): JsonResponse
- {
- if (ArticleConductor::updatable($article) === true) {
- $mediaIds = $request->attachments;
- if (is_array($mediaIds) === false) {
- $mediaIds = explode(',', $request->attachments);
- }
-
- $mediaIds = array_map('trim', $mediaIds); // trim each media ID
- $attachments = $article->attachments;
-
- // Delete attachments that are not in $mediaIds
- foreach ($attachments as $attachment) {
- if (in_array($attachment->media_id, $mediaIds) === false) {
- $attachment->delete();
- }
- }
-
- // Create new attachments for media IDs that are not already in $article->attachments()
- foreach ($mediaIds as $mediaId) {
- $found = false;
-
- foreach ($attachments as $attachment) {
- if ($attachment->media_id === $mediaId) {
- $found = true;
- break;
- }
- }
-
- if ($found === false) {
- $article->attachments()->create(['media_id' => $mediaId]);
- }
- }
-
- return $this->respondNoContent();
- }//end if
-
- return $this->respondForbidden();
- }
-
- /**
- * Delete a specific related attachment.
- * @param Request $request The user request.
- * @param Article $article The model.
- * @param Media $medium The attachment medium.
- * @return JsonResponse
- */
- public function attachmentDelete(Request $request, Article $article, Media $medium): JsonResponse
- {
- if (ArticleConductor::updatable($article) === true) {
- $attachments = $article->attachments;
- $deleted = false;
-
- foreach ($attachments as $attachment) {
- if ($attachment->media_id === $medium->id) {
- $attachment->delete();
- $deleted = true;
- break;
- }
- }
-
- if ($deleted === true) {
- // Attachment was deleted successfully
- return $this->respondNoContent();
- } else {
- // Attachment with matching media ID was not found
- return $this->respondNotFound();
- }
- }
-
- return $this->respondForbidden();
- }
}
diff --git a/app/Models/Article.php b/app/Models/Article.php
index 3dc2e87..94b2acd 100644
--- a/app/Models/Article.php
+++ b/app/Models/Article.php
@@ -39,14 +39,4 @@ class Article extends Model
{
return $this->belongsTo(User::class);
}
-
- /**
- * Get all of the article's attachments.
- *
- * @return Illuminate\Database\Eloquent\Relations\MorphMany
- */
- public function attachments(): MorphMany
- {
- return $this->morphMany(\App\Models\Attachment::class, 'attachable');
- }
}
diff --git a/app/Models/Event.php b/app/Models/Event.php
index 7f44314..54c36e7 100644
--- a/app/Models/Event.php
+++ b/app/Models/Event.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\HasAttachments;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
class Event extends Model
{
+ use HasAttachments;
use HasFactory;
use Uuids;
@@ -37,16 +39,10 @@ class Event extends Model
];
- /**
- * Get all of the article's attachments.
- */
- public function attachments(): MorphMany
- {
- return $this->morphMany(\App\Models\Attachment::class, 'attachable');
- }
-
/**
* Get all the associated users.
+ *
+ * @return BelongsToMany
*/
public function users(): BelongsToMany
{
diff --git a/app/Traits/HasAttachments.php b/app/Traits/HasAttachments.php
new file mode 100644
index 0000000..db88eb4
--- /dev/null
+++ b/app/Traits/HasAttachments.php
@@ -0,0 +1,63 @@
+attachments()->delete();
+ });
+ }
+
+ /**
+ * Add multiple attachments items to the model.
+ *
+ * @param array|string $ids The media ids to add.
+ * @param string $delimiter The split delimiter if $ids is a string.
+ * @param boolean $allowDuplicates Whether to allow duplicate media IDs or not.
+ * @return void
+ */
+ public function attachmentsAddMany(array|string $ids, string $delimiter = ',', bool $allowDuplicates = false): void
+ {
+ if (is_array($ids) === false) {
+ $ids = explode($delimiter, $ids);
+ }
+
+ $ids = array_map('trim', $ids);
+ $existingIds = $this->gallery()->pluck('media_id')->toArray();
+
+ $galleryItems = [];
+ foreach ($ids as $id) {
+ if ($allowDuplicates === false && in_array($id, $existingIds) === true) {
+ continue;
+ }
+
+ $media = Media::find($id);
+ if ($media !== null) {
+ $galleryItems[] = ['media_id' => $id];
+ }
+ }
+
+ $this->attachments()->createMany($galleryItems);
+ }
+
+ /**
+ * Get the article's attachments.
+ *
+ * @return Illuminate\Database\Eloquent\Relations\MorphMany The attachments items
+ */
+ public function attachments(): MorphMany
+ {
+ return $this->morphMany(\App\Models\Attachment::class, 'addendum');
+ }
+}
diff --git a/database/migrations/2023_07_19_041445_update_attachments_table.php b/database/migrations/2023_07_19_041445_update_attachments_table.php
new file mode 100644
index 0000000..7eb29de
--- /dev/null
+++ b/database/migrations/2023_07_19_041445_update_attachments_table.php
@@ -0,0 +1,30 @@
+renameColumn('attachable_type', 'addendum_type');
+ $table->renameColumn('attachable_id', 'addendum_id');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('attachments', function (Blueprint $table) {
+ $table->renameColumn('addendum_type', 'attachable_type');
+ $table->renameColumn('addendum_id', 'attachable_id');
+ });
+ }
+};
diff --git a/resources/js/components/SMAttachments.vue b/resources/js/components/SMAttachments.vue
index f981d79..05a016b 100644
--- a/resources/js/components/SMAttachments.vue
+++ b/resources/js/components/SMAttachments.vue
@@ -1,28 +1,32 @@
+
+ {{ modelValue.length }} file{{ modelValue.length != 1 ? "s" : "" }}
+
+
@@ -50,16 +62,53 @@
import { bytesReadable } from "../helpers/types";
import { getFileIconImagePath } from "../helpers/utils";
import SMHeader from "../components/SMHeader.vue";
+import { openDialog } from "../components/SMDialog";
+import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
+import { Media } from "../helpers/api.types";
+const emits = defineEmits(["update:modelValue"]);
const props = defineProps({
- attachments: {
- type: Object,
+ modelValue: {
+ type: Array,
+ default: () => [],
required: true,
},
+ showEditor: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
});
+
+/**
+ * Handle the user adding a new media item.
+ */
+const handleClickAdd = async () => {
+ let result = await openDialog(SMDialogMedia, {
+ mime: "",
+ accepts: "",
+ allowUpload: true,
+ multiple: true,
+ });
+
+ if (result) {
+ const mediaResult = result as Media[];
+ let newValue = props.modelValue;
+ let mediaIds = new Set(newValue.map((item) => item.id));
+
+ mediaResult.forEach((item) => {
+ if (!mediaIds.has(item.id)) {
+ newValue.push(item);
+ mediaIds.add(item.id);
+ }
+ });
+
+ emits("update:modelValue", newValue);
+ }
+};
-
+ -->
diff --git a/resources/js/components/SMHeader.vue b/resources/js/components/SMHeader.vue
index 80d7d91..95aecc3 100644
--- a/resources/js/components/SMHeader.vue
+++ b/resources/js/components/SMHeader.vue
@@ -2,10 +2,12 @@
@@ -28,6 +30,11 @@ const props = defineProps({
default: "",
required: false,
},
+ noCopy: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
});
const computedHeaderId = (text: string): string => {
@@ -35,29 +42,32 @@ const computedHeaderId = (text: string): string => {
};
const id = ref(
- props.id && props.id.length > 0 ? props.id : computedHeaderId(props.text)
+ props.id && props.id.length > 0 ? props.id : computedHeaderId(props.text),
);
const copyAnchor = () => {
- const currentUrl = window.location.href.replace(/#.*/, "");
- const newUrl = currentUrl + "#" + id.value;
+ if (props.noCopy === false) {
+ const currentUrl = window.location.href.replace(/#.*/, "");
+ const newUrl = currentUrl + "#" + id.value;
- navigator.clipboard
- .writeText(newUrl)
- .then(() => {
- useToastStore().addToast({
- title: "Copied to Clipboard",
- content: "The heading URL has been copied to the clipboard.",
- type: "success",
+ navigator.clipboard
+ .writeText(newUrl)
+ .then(() => {
+ useToastStore().addToast({
+ title: "Copied to Clipboard",
+ content:
+ "The heading URL has been copied to the clipboard.",
+ type: "success",
+ });
+ })
+ .catch(() => {
+ useToastStore().addToast({
+ title: "Copy to Clipboard",
+ content: "Failed to copy the heading URL to the clipboard.",
+ type: "danger",
+ });
});
- })
- .catch(() => {
- useToastStore().addToast({
- title: "Copy to Clipboard",
- content: "Failed to copy the heading URL to the clipboard.",
- type: "danger",
- });
- });
+ }
};
diff --git a/resources/js/components/SMInputAttachments.vue b/resources/js/components/SMInputAttachments.vue
index 6109d27..b08df28 100644
--- a/resources/js/components/SMInputAttachments.vue
+++ b/resources/js/components/SMInputAttachments.vue
@@ -1,6 +1,5 @@
@@ -136,6 +136,8 @@ const handleLoad = async () => {
"large",
);
applicationStore.setDynamicTitle(article.value.title);
+
+ console.log(article.value);
} else {
pageStatus.value = 404;
}
diff --git a/resources/js/views/dashboard/ArticleEdit.vue b/resources/js/views/dashboard/ArticleEdit.vue
index 17bdb57..d6d1b4b 100644
--- a/resources/js/views/dashboard/ArticleEdit.vue
+++ b/resources/js/views/dashboard/ArticleEdit.vue
@@ -56,7 +56,10 @@
-
+
{
form.controls.content.value = data.article.content;
form.controls.hero.value = data.article.hero;
- attachments.value = (data.article.attachments || []).map(
- function (attachment) {
- return attachment.id.toString();
- },
- );
-
+ attachments.value = data.article.attachments;
gallery.value = data.article.gallery;
} else {
pageError.value = 404;
@@ -211,6 +209,7 @@ const handleSubmit = async () => {
content: form.controls.content.value,
hero: form.controls.hero.value.id,
gallery: gallery.value.map((item) => item.id),
+ attachments: attachments.value.map((item) => item.id),
};
let article_id = "";
@@ -236,13 +235,6 @@ const handleSubmit = async () => {
}
}
- await api.put({
- url: `/articles/${article_id}/attachments`,
- body: {
- attachments: attachments.value,
- },
- });
-
useToastStore().addToast({
title: route.params.id ? "Article Updated" : "Article Created",
content: route.params.id
diff --git a/routes/api.php b/routes/api.php
index 81fd40c..5789e3d 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -44,7 +44,7 @@ Route::apiResource('media', MediaController::class);
Route::get('media/{medium}/download', [MediaController::class, 'download']);
Route::apiResource('articles', ArticleController::class);
-Route::apiAddendumResource('attachments', 'articles', ArticleController::class);
+// Route::apiAddendumResource('attachments', 'articles', ArticleController::class);
Route::apiResource('events', EventController::class);
Route::apiAddendumResource('attachments', 'events', EventController::class);