mirror of
https://github.com/linuxserver/Heimdall.git
synced 2026-08-07 07:16:13 +00:00
Merge pull request #1567 from linuxserver/fix/export-import-tags
Include tags in item export and restore them on import
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Item;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -21,6 +22,7 @@ class ItemRestController extends Controller
|
||||
public function index(): Collection
|
||||
{
|
||||
$columns = [
|
||||
'id',
|
||||
'title',
|
||||
'colour',
|
||||
'url',
|
||||
@@ -29,11 +31,27 @@ class ItemRestController extends Controller
|
||||
'appdescription',
|
||||
];
|
||||
|
||||
return Item::select($columns)
|
||||
return Item::with('parents')
|
||||
->select($columns)
|
||||
->where('deleted_at', null)
|
||||
->where('type', '0')
|
||||
->orderBy('order', 'asc')
|
||||
->get();
|
||||
->get()
|
||||
->map(function (Item $item) {
|
||||
return [
|
||||
'title' => $item->title,
|
||||
'colour' => $item->colour,
|
||||
'url' => $item->url,
|
||||
'description' => $item->description,
|
||||
'appid' => $item->appid,
|
||||
'appdescription' => $item->appdescription,
|
||||
'tags' => $item->parents
|
||||
->where('id', '!=', 0)
|
||||
->pluck('title')
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +68,16 @@ class ItemRestController extends Controller
|
||||
*/
|
||||
public function store(Request $request): object
|
||||
{
|
||||
// Imports pass tags as an array of tag titles so they can round-trip
|
||||
// across instances. Resolve those titles into local tag ids (creating
|
||||
// any that don't yet exist) before handing off to the shared store
|
||||
// logic. When no tags are supplied we keep the previous behaviour.
|
||||
if ($request->has('tags')) {
|
||||
$request->merge([
|
||||
'tags' => $this->resolveTags($request->input('tags')),
|
||||
]);
|
||||
}
|
||||
|
||||
$item = ItemController::storelogic($request);
|
||||
|
||||
if ($item) {
|
||||
@@ -59,6 +87,61 @@ class ItemRestController extends Controller
|
||||
return (object) ['status' => 'FAILED'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an incoming list of tags into tag ids.
|
||||
*
|
||||
* Numeric 0 (or "0") maps to the root/default dashboard. Every other entry
|
||||
* is treated as a tag title: an existing tag with that title is reused, and
|
||||
* a missing one is created. The lookup keeps the operation idempotent so
|
||||
* importing many items that share a tag title only ever creates one tag.
|
||||
*
|
||||
* @param mixed $tags
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function resolveTags($tags): array
|
||||
{
|
||||
if (! is_array($tags)) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag === 0 || $tag === '0') {
|
||||
$ids[] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
$title = is_string($tag) ? trim($tag) : $tag;
|
||||
|
||||
if ($title === '' || $title === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = Item::where('type', '1')
|
||||
->where('title', $title)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$ids[] = (int) $existing->id;
|
||||
continue;
|
||||
}
|
||||
|
||||
$created = Item::create([
|
||||
'title' => $title,
|
||||
'type' => '1',
|
||||
'url' => str_slug($title, '-', 'en_US'),
|
||||
'user_id' => User::currentUser()->getId(),
|
||||
]);
|
||||
|
||||
$ids[] = (int) $created->id;
|
||||
}
|
||||
|
||||
$ids = array_values(array_unique($ids));
|
||||
|
||||
return empty($ids) ? [0] : $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
|
||||
Vendored
+1
-1
@@ -4446,7 +4446,7 @@ var getCSRFToken = function getCSRFToken() {
|
||||
var mergeItemWithAppDetails = function mergeItemWithAppDetails(item, appDetails) {
|
||||
return {
|
||||
pinned: 1,
|
||||
tags: [0],
|
||||
tags: Array.isArray(item.tags) && item.tags.length ? item.tags : [0],
|
||||
appid: item.appid,
|
||||
title: item.title,
|
||||
colour: item.colour,
|
||||
|
||||
Vendored
+1
-1
@@ -60,7 +60,7 @@ const getCSRFToken = () => {
|
||||
*/
|
||||
const mergeItemWithAppDetails = (item, appDetails) => ({
|
||||
pinned: 1,
|
||||
tags: [0],
|
||||
tags: Array.isArray(item.tags) && item.tags.length ? item.tags : [0],
|
||||
|
||||
appid: item.appid,
|
||||
title: item.title,
|
||||
|
||||
@@ -34,7 +34,28 @@ class ItemExportTest extends TestCase
|
||||
|
||||
$response = $this->get('api/item');
|
||||
|
||||
$response->assertExactJson([(object)$exampleItem]);
|
||||
$response->assertExactJson([$exampleItem + ["tags" => []]]);
|
||||
}
|
||||
|
||||
public function test_exports_assigned_tag_titles_excluding_the_root_tag(): void
|
||||
{
|
||||
$item = Item::factory()
|
||||
->create([
|
||||
'title' => 'Tagged Item',
|
||||
]);
|
||||
$tag = Item::factory()
|
||||
->create([
|
||||
'type' => 1,
|
||||
'title' => 'Media',
|
||||
]);
|
||||
|
||||
// Assign both the root/default dashboard (id 0) and the Media tag.
|
||||
$item->parents()->sync([0, $tag->id]);
|
||||
|
||||
$response = $this->get('api/item');
|
||||
|
||||
$response->assertJsonCount(1);
|
||||
$response->assertJsonPath('0.tags', ['Media']);
|
||||
}
|
||||
|
||||
public function test_returns_all_items(): void
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Item;
|
||||
use App\ItemTag;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ItemImportTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function importPayload(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'pinned' => 1,
|
||||
'appid' => 'null',
|
||||
'website' => null,
|
||||
'title' => 'Item A',
|
||||
'colour' => '#00f',
|
||||
'url' => 'http://10.0.1.1',
|
||||
'tags' => [0],
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
public function test_import_creates_and_assigns_a_tag_from_its_title(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->postJson('api/item', $this->importPayload([
|
||||
'title' => 'Item A',
|
||||
'tags' => ['Media'],
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson(['status' => 'OK']);
|
||||
|
||||
$tag = Item::where('type', 1)->where('title', 'Media')->first();
|
||||
$this->assertNotNull($tag);
|
||||
$this->assertSame(1, (int) $tag->type);
|
||||
|
||||
$item = Item::where('type', 0)->where('title', 'Item A')->first();
|
||||
$this->assertNotNull($item);
|
||||
|
||||
$this->assertTrue(
|
||||
ItemTag::where('item_id', $item->id)->where('tag_id', $tag->id)->exists()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_import_reuses_an_existing_tag_for_the_same_title(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->postJson('api/item', $this->importPayload([
|
||||
'title' => 'Item A',
|
||||
'tags' => ['Media'],
|
||||
]))->assertStatus(200);
|
||||
|
||||
$this->postJson('api/item', $this->importPayload([
|
||||
'title' => 'Item B',
|
||||
'tags' => ['Media'],
|
||||
]))->assertStatus(200);
|
||||
|
||||
$this->assertSame(
|
||||
1,
|
||||
Item::where('type', 1)->where('title', 'Media')->count()
|
||||
);
|
||||
|
||||
$tag = Item::where('type', 1)->where('title', 'Media')->first();
|
||||
$itemA = Item::where('type', 0)->where('title', 'Item A')->first();
|
||||
$itemB = Item::where('type', 0)->where('title', 'Item B')->first();
|
||||
|
||||
$this->assertTrue(
|
||||
ItemTag::where('item_id', $itemA->id)->where('tag_id', $tag->id)->exists()
|
||||
);
|
||||
$this->assertTrue(
|
||||
ItemTag::where('item_id', $itemB->id)->where('tag_id', $tag->id)->exists()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_import_with_root_tag_only_creates_no_tags(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->postJson('api/item', $this->importPayload([
|
||||
'title' => 'Item A',
|
||||
'tags' => [0],
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
// No stray tag items should have been created beyond the seeded
|
||||
// root/default dashboard tag (id 0).
|
||||
$this->assertSame(0, Item::where('type', 1)->where('id', '>', 0)->count());
|
||||
|
||||
// The item should be assigned to the root/default dashboard (tag id 0).
|
||||
$item = Item::where('type', 0)->where('title', 'Item A')->first();
|
||||
$this->assertNotNull($item);
|
||||
$this->assertTrue(
|
||||
ItemTag::where('item_id', $item->id)->where('tag_id', 0)->exists()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user