Compare commits

...

3 Commits

Author SHA1 Message Date
KodeStar ad9baffa62 Only offer pinned tags as the default tag
The default_tag dropdown was populated from every tag (type=1), but the
dashboard taglist only renders pinned tags. Selecting an unpinned tag as the
default therefore triggered a click on a taglist entry that does not exist,
silently doing nothing. Filter the option queries in both Setting accessors to
pinned tags so only selectable tags are offered, and assert an unpinned tag is
excluded.

Also drop the unused $data['default_tag'] assignment in ItemController: the
taglist partial reads the setting directly via Setting::fetch(), so the view
variable was never consumed.
2026-07-08 18:26:19 +01:00
KodeStar c0c202c5ff Add a configurable default tag for the dashboard
In tags mode the dashboard always opened showing every link. This adds a
"Default tag" setting (Advanced) that pre-selects one tag group on load, so
the dashboard opens filtered to it - the built-in equivalent of the custom
JavaScript workaround people have been sharing.

The setting is a select populated from the user's own tags, following the
same dynamic-option pattern already used for the search provider. When a tag
is chosen its slug is exposed on the tag list and the matching tab is
activated on load; when the setting is empty, behaviour is unchanged and all
links are shown.

Resolves #1556
2026-07-08 17:10:19 +01:00
KodeStar e2215fe42b Merge pull request #1571 from linuxserver/fix/ci-node-select2
Fix CI: pin Node 24 and install frontend deps with npm ci
2026-07-08 16:08:59 +01:00
8 changed files with 129 additions and 1 deletions
+12
View File
@@ -123,6 +123,12 @@ class Setting extends Model
$options = (array) json_decode($this->options);
if ($this->key === 'search_provider') {
$options = Search::providers()->pluck('name', 'id')->toArray();
} elseif ($this->key === 'default_tag') {
$options = [];
$tags = Item::where('type', 1)->where('id', '>', 0)->pinned()->orderBy('title', 'asc')->get();
foreach ($tags as $tag) {
$options[$tag->tag_url] = $tag->title;
}
}
$value = (array_key_exists($this->value, $options))
? __($options[$this->value])
@@ -190,6 +196,12 @@ class Setting extends Model
$options = json_decode($this->options);
if ($this->key === 'search_provider') {
$options = Search::providers()->pluck('name', 'id');
} elseif ($this->key === 'default_tag') {
$options = ['' => 'app.options.none'];
$tags = Item::where('type', 1)->where('id', '>', 0)->pinned()->orderBy('title', 'asc')->get();
foreach ($tags as $tag) {
$options[$tag->tag_url] = $tag->title;
}
}
$value = '<select name="value" class="form-control">';
foreach ($options as $key => $opt) {
+15
View File
@@ -349,5 +349,20 @@ class SettingsSeeder extends Seeder
$setting->label = 'app.settings.treat_tags_as';
$setting->save();
}
if (! $setting = Setting::find(15)) {
$setting = new Setting;
$setting->id = 15;
$setting->group_id = 4;
$setting->key = 'default_tag';
$setting->type = 'select';
$setting->label = 'app.settings.default_tag';
$setting->value = '';
$setting->save();
} else {
$setting->group_id = 4;
$setting->label = 'app.settings.default_tag';
$setting->save();
}
}
}
+1
View File
@@ -29,6 +29,7 @@ return array (
'settings.custom_css' => 'Custom CSS',
'settings.custom_js' => 'Custom JavaScript',
'settings.treat_tags_as' => 'Treat Tags As:',
'settings.default_tag' => 'Default tag',
'settings.folders' => 'Folders',
'settings.tags' => 'Tags',
'settings.categories' => 'Categories',
+8
View File
@@ -4287,6 +4287,14 @@ $.when($.ready).then(function () {
alert("Something went wrong: ".concat(responseData.responseText.substring(0, 100)));
});
});
// Auto-select the configured default tag on load (tags mode only)
var taglist = document.getElementById("taglist");
if (taglist !== null) {
var defaultTag = taglist.getAttribute("data-default-tag");
if (typeof defaultTag === "string" && defaultTag !== "") {
$("#taglist .tag[data-tag=\"tag-".concat(defaultTag, "\"]")).trigger("click");
}
}
$("#pinlist").on("click", "a", function (e) {
e.preventDefault();
var current = $(this);
+9
View File
@@ -334,6 +334,15 @@ $.when($.ready).then(() => {
);
});
});
// Auto-select the configured default tag on load (tags mode only)
const taglist = document.getElementById("taglist");
if (taglist !== null) {
const defaultTag = taglist.getAttribute("data-default-tag");
if (typeof defaultTag === "string" && defaultTag !== "") {
$(`#taglist .tag[data-tag="tag-${defaultTag}"]`).trigger("click");
}
}
$("#pinlist").on("click", "a", function (e) {
e.preventDefault();
const current = $(this);
+1 -1
View File
@@ -3,7 +3,7 @@ $treat_tags_as = \App\Setting::fetch('treat_tags_as');
?>
@if( $treat_tags_as == 'tags')
@if($taglist->first())
<div id="taglist" class="taglist">
<div id="taglist" class="taglist" data-default-tag="{{ \App\Setting::fetch('default_tag') }}">
<div class="tag white current" data-tag="all">All</div>
@foreach($taglist as $tag)
<div class="tag link{{ title_color($tag->colour) }}" style="background-color: {{ $tag->colour }}" data-tag="tag-{{ $tag->tag_url }}">{{ $tag->title }}</div>
+22
View File
@@ -4,6 +4,7 @@ namespace Tests\Feature;
use App\Item;
use App\ItemTag;
use App\Setting;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@@ -80,4 +81,25 @@ class DashTest extends TestCase
$response->assertSee('Tag 1');
$response->assertSee('Tag 2');
}
public function test_dash_exposes_the_configured_default_tag(): void
{
$this->seed();
Setting::where('key', 'treat_tags_as')->update(['value' => 'tags']);
Setting::where('key', 'default_tag')->update(['value' => 'home-dashboard']);
Item::factory()->create([
'title' => 'Home',
'url' => 'home-dashboard',
'type' => 1,
'pinned' => 1,
'user_id' => 0,
]);
$response = $this->get('/');
$response->assertStatus(200);
$response->assertSee('data-default-tag="home-dashboard"', false);
}
}
@@ -2,11 +2,16 @@
namespace Tests\Unit\database\seeders;
use App\Item;
use App\Setting;
use Database\Seeders\SettingsSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SettingsSeederTest extends TestCase
{
use RefreshDatabase;
/**
* All language keys are defined in all languages based on the en language file.
*/
@@ -18,4 +23,60 @@ class SettingsSeederTest extends TestCase
$this->assertTrue(count($languageMap) === count($languageDirectories));
}
public function test_seeds_the_default_tag_setting(): void
{
$this->seed();
$setting = Setting::where('key', 'default_tag')->first();
$this->assertNotNull($setting);
$this->assertSame('select', $setting->type);
$this->assertSame(4, (int) $setting->group_id);
}
public function test_default_tag_edit_value_lists_all_tags_and_a_none_option(): void
{
$this->seed();
Item::factory()->create([
'title' => 'Home',
'url' => 'home-dashboard',
'type' => 1,
'pinned' => 1,
'user_id' => 0,
]);
Item::factory()->create([
'title' => 'Media',
'url' => 'media',
'type' => 1,
'pinned' => 1,
'user_id' => 0,
]);
// An unpinned tag is not rendered in the dashboard taglist, so it must
// not be offered as a default (selecting it would silently do nothing).
Item::factory()->create([
'title' => 'Archive',
'url' => 'archive',
'type' => 1,
'pinned' => 0,
'user_id' => 0,
]);
$setting = Setting::where('key', 'default_tag')->first();
$editValue = $setting->edit_value;
// A "none" option with an empty value, using the shared translation key.
$this->assertStringContainsString('<option value="" ', $editValue);
$this->assertStringContainsString(__('app.options.none'), $editValue);
// One option per pinned tag: the slug as the value, the raw title as the label.
$this->assertStringContainsString('value="home-dashboard"', $editValue);
$this->assertStringContainsString('>Home</option>', $editValue);
$this->assertStringContainsString('value="media"', $editValue);
$this->assertStringContainsString('>Media</option>', $editValue);
// The unpinned tag is excluded.
$this->assertStringNotContainsString('value="archive"', $editValue);
}
}