From 737b7d1b96e9b849918bb43fa85b566ca3c2d785 Mon Sep 17 00:00:00 2001 From: Kusha Gharahi <3326002+kushagharahi@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:25:00 -0500 Subject: [PATCH 1/6] Add pinned & order to import/export --- app/Http/Controllers/ItemRestController.php | 4 ++++ public/js/app.js | 3 ++- resources/assets/js/itemImport.js | 3 ++- tests/Feature/ItemExportTest.php | 19 +++++++++++++++++++ tests/Feature/ItemImportTest.php | 16 ++++++++++++++++ 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/ItemRestController.php b/app/Http/Controllers/ItemRestController.php index ae68f5f1d..e3d91ab19 100644 --- a/app/Http/Controllers/ItemRestController.php +++ b/app/Http/Controllers/ItemRestController.php @@ -29,6 +29,8 @@ public function index(): Collection 'description', 'appid', 'appdescription', + 'pinned', + 'order', ]; return Item::with('parents') @@ -45,6 +47,8 @@ public function index(): Collection 'description' => $item->description, 'appid' => $item->appid, 'appdescription' => $item->appdescription, + 'pinned' => $item->pinned, + 'pinned_order' => $item->order, 'tags' => $item->parents ->where('id', '!=', 0) ->pluck('title') diff --git a/public/js/app.js b/public/js/app.js index 4d41224d4..3f869baf7 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -4458,7 +4458,8 @@ var getCSRFToken = function getCSRFToken() { */ var mergeItemWithAppDetails = function mergeItemWithAppDetails(item, appDetails) { return { - pinned: 1, + pinned: item.pinned !== undefined ? item.pinned : 1, + order: item.pinned_order !== undefined ? item.pinned_order : null, tags: Array.isArray(item.tags) && item.tags.length ? item.tags : [0], appid: item.appid, title: item.title, diff --git a/resources/assets/js/itemImport.js b/resources/assets/js/itemImport.js index 146660ce6..7ff8aaccd 100644 --- a/resources/assets/js/itemImport.js +++ b/resources/assets/js/itemImport.js @@ -59,7 +59,8 @@ const getCSRFToken = () => { * @returns {object} */ const mergeItemWithAppDetails = (item, appDetails) => ({ - pinned: 1, + pinned: item.pinned !== undefined ? item.pinned : 1, + order: item.pinned_order !== undefined ? item.pinned_order : null, tags: Array.isArray(item.tags) && item.tags.length ? item.tags : [0], appid: item.appid, diff --git a/tests/Feature/ItemExportTest.php b/tests/Feature/ItemExportTest.php index 58c251793..c92d3deb6 100644 --- a/tests/Feature/ItemExportTest.php +++ b/tests/Feature/ItemExportTest.php @@ -27,6 +27,7 @@ public function test_returns_exactly_the_defined_fields(): void "appid" => "123", "colour" => "#000", "description" => "Description", + "pinned" => 1, "title" => "Item Title", "url" => "http://gorczany.com/nihil-rerum-distinctio-voluptate-assumenda-accusantium-exercitationem" ]; @@ -38,6 +39,24 @@ public function test_returns_exactly_the_defined_fields(): void $response->assertExactJson([$exampleItem + ["tags" => []]]); } + public function test_exports_pinned_status_for_items(): void + { + $pinnedItem = Item::factory()->create([ + 'title' => 'Pinned App', + 'pinned' => 1, + ]); + $unpinnedItem = Item::factory()->create([ + 'title' => 'Unpinned App', + 'pinned' => 0, + ]); + + $response = $this->get('api/item'); + + $response->assertJsonCount(2); + $response->assertJsonPath('0.pinned', 1); + $response->assertJsonPath('1.pinned', 0); + } + public function test_exports_assigned_tag_titles_excluding_the_root_tag(): void { // Mirror the root/default dashboard row that production seeds (id 0), diff --git a/tests/Feature/ItemImportTest.php b/tests/Feature/ItemImportTest.php index e1370a320..faa8481e4 100644 --- a/tests/Feature/ItemImportTest.php +++ b/tests/Feature/ItemImportTest.php @@ -104,4 +104,20 @@ public function test_import_with_root_tag_only_creates_no_tags(): void ItemTag::where('item_id', $item->id)->where('tag_id', 0)->exists() ); } + + public function test_import_saves_unpinned_item(): void + { + $this->seed(); + + $response = $this->postJson('api/item', $this->importPayload([ + 'title' => 'Unpinned App', + 'pinned' => 0, + ])); + + $response->assertStatus(200); + + $item = Item::where('type', 0)->where('title', 'Unpinned App')->first(); + $this->assertNotNull($item); + $this->assertEquals(0, $item->pinned); + } } From 6aaf7aa19b1b043ccf052cc5e391a1a9e8da0abd Mon Sep 17 00:00:00 2001 From: Kusha Gharahi <3326002+kushagharahi@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:46:55 -0500 Subject: [PATCH 2/6] more tests --- tests/Feature/ItemExportTest.php | 17 ++++++++- tests/Feature/ItemImportTest.php | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/tests/Feature/ItemExportTest.php b/tests/Feature/ItemExportTest.php index c92d3deb6..44cf9dbb2 100644 --- a/tests/Feature/ItemExportTest.php +++ b/tests/Feature/ItemExportTest.php @@ -28,6 +28,7 @@ public function test_returns_exactly_the_defined_fields(): void "colour" => "#000", "description" => "Description", "pinned" => 1, + "pinned_order" => 0, "title" => "Item Title", "url" => "http://gorczany.com/nihil-rerum-distinctio-voluptate-assumenda-accusantium-exercitationem" ]; @@ -42,8 +43,8 @@ public function test_returns_exactly_the_defined_fields(): void public function test_exports_pinned_status_for_items(): void { $pinnedItem = Item::factory()->create([ - 'title' => 'Pinned App', - 'pinned' => 1, + 'title' => 'Pinned App', + 'pinned' => 1, ]); $unpinnedItem = Item::factory()->create([ 'title' => 'Unpinned App', @@ -57,6 +58,18 @@ public function test_exports_pinned_status_for_items(): void $response->assertJsonPath('1.pinned', 0); } + public function test_exports_pinned_order_for_items(): void + { + Item::factory()->create(['title' => 'First', 'order' => 1]); + Item::factory()->create(['title' => 'Second', 'order' => 2]); + + $response = $this->get('api/item'); + + $response->assertJsonCount(2); + $response->assertJsonPath('0.pinned_order', 1); + $response->assertJsonPath('1.pinned_order', 2); + } + public function test_exports_assigned_tag_titles_excluding_the_root_tag(): void { // Mirror the root/default dashboard row that production seeds (id 0), diff --git a/tests/Feature/ItemImportTest.php b/tests/Feature/ItemImportTest.php index faa8481e4..84244aae9 100644 --- a/tests/Feature/ItemImportTest.php +++ b/tests/Feature/ItemImportTest.php @@ -120,4 +120,68 @@ public function test_import_saves_unpinned_item(): void $this->assertNotNull($item); $this->assertEquals(0, $item->pinned); } + + public function test_import_saves_pinned_order(): void + { + $this->seed(); + + $response = $this->postJson('api/item', $this->importPayload([ + 'title' => 'Ordered App', + 'order' => 5, + ])); + + $response->assertStatus(200); + + $item = Item::where('type', 0)->where('title', 'Ordered App')->first(); + $this->assertNotNull($item); + $this->assertEquals(5, $item->order); + } + + public function test_import_defaults_order_to_zero_when_not_provided(): void + { + $this->seed(); + + $response = $this->postJson('api/item', $this->importPayload([ + 'title' => 'No Order App', + ])); + + $response->assertStatus(200); + + $item = Item::where('type', 0)->where('title', 'No Order App')->first(); + $this->assertNotNull($item); + $this->assertEquals(0, $item->order); + } + + public function test_export_import_round_trip_preserves_pinned_and_order(): void + { + $this->seed(); + + // Create items with specific pinned/order values + $this->postJson('api/item', $this->importPayload([ + 'title' => 'App One', + 'pinned' => 1, + 'order' => 3, + ]))->assertStatus(200); + + $this->postJson('api/item', $this->importPayload([ + 'title' => 'App Two', + 'pinned' => 0, + 'order' => 7, + ]))->assertStatus(200); + + // Export + $export = $this->get('api/item'); + $export->assertJsonCount(2); + + $exported = $export->json(); + + // Verify the exported JSON has the right keys/values + $appOne = collect($exported)->firstWhere('title', 'App One'); + $appTwo = collect($exported)->firstWhere('title', 'App Two'); + + $this->assertEquals(1, $appOne['pinned']); + $this->assertEquals(3, $appOne['pinned_order']); + $this->assertEquals(0, $appTwo['pinned']); + $this->assertEquals(7, $appTwo['pinned_order']); + } } From 036ba43c1feb292058620e83371dc273c0a63aa0 Mon Sep 17 00:00:00 2001 From: kushagharahi <3326002+kushagharahi@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:46:40 -0500 Subject: [PATCH 3/6] update test --- tests/Feature/ItemExportTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Feature/ItemExportTest.php b/tests/Feature/ItemExportTest.php index 44cf9dbb2..db9e2f4c0 100644 --- a/tests/Feature/ItemExportTest.php +++ b/tests/Feature/ItemExportTest.php @@ -28,16 +28,16 @@ public function test_returns_exactly_the_defined_fields(): void "colour" => "#000", "description" => "Description", "pinned" => 1, - "pinned_order" => 0, "title" => "Item Title", "url" => "http://gorczany.com/nihil-rerum-distinctio-voluptate-assumenda-accusantium-exercitationem" ]; + // The DB column is "order"; it is exported under the "pinned_order" key. Item::factory() - ->create($exampleItem); + ->create($exampleItem + ["order" => 0]); $response = $this->get('api/item'); - $response->assertExactJson([$exampleItem + ["tags" => []]]); + $response->assertExactJson([$exampleItem + ["pinned_order" => 0, "tags" => []]]); } public function test_exports_pinned_status_for_items(): void From 3fe20062b8588402920d59662297949a9d37d1b7 Mon Sep 17 00:00:00 2001 From: kushagharahi <3326002+kushagharahi@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:01:01 -0500 Subject: [PATCH 4/6] order is not nullable --- public/js/app.js | 2 +- resources/assets/js/itemImport.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 3f869baf7..146e98f65 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -4459,7 +4459,7 @@ var getCSRFToken = function getCSRFToken() { var mergeItemWithAppDetails = function mergeItemWithAppDetails(item, appDetails) { return { pinned: item.pinned !== undefined ? item.pinned : 1, - order: item.pinned_order !== undefined ? item.pinned_order : null, + order: item.pinned_order !== undefined ? item.pinned_order : 0, tags: Array.isArray(item.tags) && item.tags.length ? item.tags : [0], appid: item.appid, title: item.title, diff --git a/resources/assets/js/itemImport.js b/resources/assets/js/itemImport.js index 7ff8aaccd..8b1cbde0d 100644 --- a/resources/assets/js/itemImport.js +++ b/resources/assets/js/itemImport.js @@ -60,7 +60,7 @@ const getCSRFToken = () => { */ const mergeItemWithAppDetails = (item, appDetails) => ({ pinned: item.pinned !== undefined ? item.pinned : 1, - order: item.pinned_order !== undefined ? item.pinned_order : null, + order: item.pinned_order !== undefined ? item.pinned_order : 0, tags: Array.isArray(item.tags) && item.tags.length ? item.tags : [0], appid: item.appid, From 621d4758cf88058f36a33adfaa855b4759c46a6e Mon Sep 17 00:00:00 2001 From: kushagharahi <3326002+kushagharahi@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:01:10 -0500 Subject: [PATCH 5/6] order by title for determinism --- tests/Feature/ItemExportTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Feature/ItemExportTest.php b/tests/Feature/ItemExportTest.php index db9e2f4c0..5b48e109f 100644 --- a/tests/Feature/ItemExportTest.php +++ b/tests/Feature/ItemExportTest.php @@ -54,8 +54,9 @@ public function test_exports_pinned_status_for_items(): void $response = $this->get('api/item'); $response->assertJsonCount(2); - $response->assertJsonPath('0.pinned', 1); - $response->assertJsonPath('1.pinned', 0); + $exported = collect($response->json())->keyBy('title'); + $this->assertSame(1, $exported['Pinned App']['pinned']); + $this->assertSame(0, $exported['Unpinned App']['pinned']); } public function test_exports_pinned_order_for_items(): void From 64590e7faa060ab41be3103e90c0c30515d5c83d Mon Sep 17 00:00:00 2001 From: kushagharahi <3326002+kushagharahi@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:07:57 -0500 Subject: [PATCH 6/6] cleanup --- tests/Feature/ItemExportTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/ItemExportTest.php b/tests/Feature/ItemExportTest.php index 5b48e109f..0e417addf 100644 --- a/tests/Feature/ItemExportTest.php +++ b/tests/Feature/ItemExportTest.php @@ -42,11 +42,11 @@ public function test_returns_exactly_the_defined_fields(): void public function test_exports_pinned_status_for_items(): void { - $pinnedItem = Item::factory()->create([ + Item::factory()->create([ 'title' => 'Pinned App', 'pinned' => 1, ]); - $unpinnedItem = Item::factory()->create([ + Item::factory()->create([ 'title' => 'Unpinned App', 'pinned' => 0, ]);