diff --git a/app/Helpers/AssetHelpers.php b/app/Helpers/AssetHelpers.php index 31de8cda15..895213b2f9 100644 --- a/app/Helpers/AssetHelpers.php +++ b/app/Helpers/AssetHelpers.php @@ -699,3 +699,95 @@ function getRewardLootData($showData, $recipient = 'User', $useCustomSelectize = return $rewardLootData; } + +/********************************************************************************************** + + ATTACHMENTS + Similar functionality to the above rewards but agnostic of recipient and other filters. + +**********************************************************************************************/ + +/** + * Gets the valid attachment types. + * + * @return array + */ +function getAttachmentTypes() { + return [ + 'Item' => 'Item', + 'Currency' => 'Currency', + 'Character' => 'Character', + 'Prompt' => 'Prompt', + 'News' => 'News', + 'Sales' => 'Sales', + 'LootTable' => 'Loot Table', + 'Raffle' => 'Raffle Ticket', + 'Shop' => 'Shop', + ]; +} + +/** + * Gets the attachment data needed for selection blades. + * + * @return array + */ +function getAttachmentData() { + $attachmentTypes = getAttachmentTypes(); + + $attachmentLootData = []; + + // Iterate through each valid key in $attachmentTypes and get the data associated with it + foreach ($attachmentTypes as $attachmentKey => $attachmentType) { + $query = null; + + switch ($attachmentKey) { + case 'Item': + $query = App\Models\Item\Item::orderBy('name'); + break; + case 'Currency': + $query = App\Models\Currency\Currency::query()->orderBy('sort_character', 'DESC'); + break; + case 'LootTable': + $query = App\Models\Loot\LootTable::orderBy('name'); + break; + case 'Raffle': + $query = App\Models\Raffle\Raffle::where('rolled_at', null)->where('is_active', 1)->orderBy('name'); + break; + case 'Character': + $query = App\Models\Character\Character::orderBy('slug'); + break; + case 'Prompt': + $query = App\Models\Prompt\Prompt::orderBy('name'); + break; + case 'News': + $query = App\Models\News::orderBy('title'); + break; + case 'Sales': + $query = App\Models\Sales\Sales::orderBy('title'); + break; + case 'Shop': + $query = App\Models\Shop\Shop::orderBy('name'); + break; + // Add the query builder for your other assets here, set with the matching key in getAttachmentTypes + // If your asset type does not have a model, you may need to add special handling here. + // + // case 'Example': + // $query = \App\Models\Example::orderby('name'); + // break; + } + + $data = $query->get()->mapWithKeys(function ($item) { + return [ + $item->id => json_encode([ + 'name' => $item->title ?? ($item->fullName ?? $item->name), + 'image_url' => $item->imageUrl ?? null, + ]), + ]; + }); + + // Finally, add the data to the array. + $attachmentLootData[$attachmentKey] = $data; + } + + return $attachmentLootData; +} diff --git a/app/Helpers/Helpers.php b/app/Helpers/Helpers.php index 40be9cb59b..97a68a298a 100644 --- a/app/Helpers/Helpers.php +++ b/app/Helpers/Helpers.php @@ -529,3 +529,23 @@ function getRewards($object) { function hasRewards($object) { return App\Models\Reward\Reward::where('object_model', get_class($object))->where('object_id', $object->id)->exists(); } + +/** + * Returns the given objects attachments, if any. + * + * @param mixed $object + * + * @return bool + */ +function getAttachments($object) { + return App\Models\Attachment::where('parent_model', get_class($object))->where('parent_id', $object->id)->get(); +} + +/** + * checks if a certain object has any attachments. + * + * @param mixed $object + */ +function hasAttachments($object) { + return App\Models\Attachment::where('parent_model', get_class($object))->where('parent_id', $object->id)->exists(); +} diff --git a/app/Http/Controllers/Admin/AttachmentController.php b/app/Http/Controllers/Admin/AttachmentController.php new file mode 100644 index 0000000000..a51bea889b --- /dev/null +++ b/app/Http/Controllers/Admin/AttachmentController.php @@ -0,0 +1,31 @@ +only([ + 'parent_model', 'parent_id', 'attachment_type', 'attachment_id', 'data', + ]); + if ($service->editAttachments($data['parent_model'], $data['parent_id'], $data)) { + flash('Attachments updated successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } +} diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php new file mode 100644 index 0000000000..bcb0681169 --- /dev/null +++ b/app/Models/Attachment.php @@ -0,0 +1,101 @@ + 'array', + ]; + + /********************************************************************************************** + + RELATIONS + + **********************************************************************************************/ + + /** + * Get the parent object this is attached to. + */ + public function parent() { + return $this->belongsTo($this->parent_model, 'parent_id'); + } + + /** + * Get the attached object. + */ + public function attachment() { + $model = getAssetModelString(strtolower($this->attachment_type)); + + if (!class_exists($model)) { + // Laravel requires a relationship instance to be returned (cannot return null), so returning one that doesn't exist here. + return $this->belongsTo(self::class, 'id', 'attachment_id')->whereNull('attachment_id'); + } + + return $this->belongsTo($model, 'attachment_id'); + } + + /********************************************************************************************** + + ATTRIBUTES + + **********************************************************************************************/ + + /** + * Returns the description attribute or null if not set. + */ + public function getDescriptionAttribute() { + return $this->data['description'] ?? null; + } + + /** + * Returns the parsed description attribute or null if not set. + */ + public function getParsedDescriptionAttribute() { + return $this->data['parsed_description'] ?? null; + } + + /********************************************************************************************** + + OTHER FUNCTIONS + + **********************************************************************************************/ + + /** + * Checks if a certain object has any attachments. + * + * @param mixed $object + */ + public static function hasAttachments($object) { + return self::where('parent_model', get_class($object))->where('parent_id', $object->id)->exists(); + } + + /** + * Get the attachments of a certain object. + * + * @param mixed $object + */ + public static function getAttachments($object) { + return self::where('parent_model', get_class($object))->where('parent_id', $object->id)->get(); + } +} diff --git a/app/Services/AttachmentService.php b/app/Services/AttachmentService.php new file mode 100644 index 0000000000..523a3f1c5b --- /dev/null +++ b/app/Services/AttachmentService.php @@ -0,0 +1,108 @@ + 0) { + $attachments->each(function ($attachment) { + $attachment->delete(); + }); + } + + if (count($attachments) > 0) { + flash('Deleted '.count($attachments).' old attachments.')->success(); + } + + if (isset($data['attachment_type'])) { + foreach ($data['attachment_type'] as $key => $type) { + $attributes = [ + 'parent_model' => $parent_model, + 'parent_id' => $parent_id, + 'attachment_type' => $data['attachment_type'][$key], + 'attachment_id' => $data['attachment_id'][$key], + 'data' => null, + ]; + + if (isset($data['data'][$key])) { + // description is special we also store parsed_description + if (isset($data['data'][$key]['description'])) { + $attributes['data']['description'] = $data['data'][$key]['description']; + $attributes['data']['parsed_description'] = parse($data['data'][$key]['description']); + + unset($data['data'][$key]['description']); + } + // additional "special" fields can be mapped here in the future. + + // any other fields are stored as-is + foreach ($data['data'][$key] as $field_key => $field_value) { + $attributes['data'][$field_key] = $field_value; + } + } + + $attachment = new Attachment([ + 'parent_model' => $parent_model, + 'parent_id' => $parent_id, + 'attachment_type' => $data['attachment_type'][$key], + 'attachment_id' => $data['attachment_id'][$key], + 'data' => $attributes['data'] ?? null, + ]); + + if (!$attachment->save()) { + throw new \Exception('Failed to save attachment.'); + } + } + } + + // Log the action + if ($log && !$this->logAdminAction(Auth::user(), 'Edited Attachments', 'Edited '.$parent->displayName.' attachments')) { + throw new \Exception('Failed to log admin action.'); + } + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } +} diff --git a/database/migrations/2026_01_11_172249_create_attachments_table.php b/database/migrations/2026_01_11_172249_create_attachments_table.php new file mode 100644 index 0000000000..16b69c2363 --- /dev/null +++ b/database/migrations/2026_01_11_172249_create_attachments_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('parent_model'); + $table->unsignedBigInteger('parent_id'); + $table->string('attachment_type'); + $table->unsignedBigInteger('attachment_id'); + + $table->json('data')->nullable()->default(null); // for extensible data storage / extra functionality + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + Schema::dropIfExists('attachments'); + } +}; diff --git a/resources/views/admin/items/create_edit_item.blade.php b/resources/views/admin/items/create_edit_item.blade.php index 8f74b35010..cf3436aaad 100644 --- a/resources/views/admin/items/create_edit_item.blade.php +++ b/resources/views/admin/items/create_edit_item.blade.php @@ -161,6 +161,10 @@ Add a Tag + @include('widgets._add_attachments', [ + 'object' => $item, + ]) +
Submissions to this prompt are hidden.
@endif + @if (hasAttachments($prompt)) +
+ You can add attachments to this object by clicking "Add Attachment" and entering the attachment model and type.
+
Each attachment can include additional data fields (e.g., description), and you can add custom fields as needed.
+
' . $info . '
' : '' !!} + + {!! Form::open(['url' => 'admin/attachments']) !!} + {!! Form::hidden('parent_model', get_class($object)) !!} + {!! Form::hidden('parent_id', $object->id) !!} + +widgets/attachments/_custom-style.blade.php file. Make sure to create a sub-view file that matches the style name you provide.
+
+By default, if no style is provided, the widget will use the `_card` style.
\ No newline at end of file
diff --git a/resources/views/widgets/attachments/_card.blade.php b/resources/views/widgets/attachments/_card.blade.php
new file mode 100644
index 0000000000..a9a03adc10
--- /dev/null
+++ b/resources/views/widgets/attachments/_card.blade.php
@@ -0,0 +1,14 @@
+