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 @@ @@ -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 @@ {{ props.text }} - + @@ -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);