diff --git a/app/Http/Controllers/Settings/CategoryController.php b/app/Http/Controllers/Settings/CategoryController.php index 8b92ea00..aa96248a 100644 --- a/app/Http/Controllers/Settings/CategoryController.php +++ b/app/Http/Controllers/Settings/CategoryController.php @@ -107,7 +107,8 @@ class CategoryController extends Controller { if (! str_contains($exception->getMessage(), 'categories_user_id_name_unique') && ! str_contains($exception->getMessage(), 'categories_user_id_name_active_unique') - && ! str_contains($exception->getMessage(), 'categories_user_id_parent_name_active_unique')) { + && ! str_contains($exception->getMessage(), 'categories_user_id_parent_name_active_unique') + && ! str_contains($exception->getMessage(), 'categories_space_id_parent_name_active_unique')) { throw $exception; } diff --git a/database/migrations/2026_07_06_130000_rescope_unique_indexes_to_space.php b/database/migrations/2026_07_06_130000_rescope_unique_indexes_to_space.php new file mode 100644 index 00000000..81f72a1a --- /dev/null +++ b/database/migrations/2026_07_06_130000_rescope_unique_indexes_to_space.php @@ -0,0 +1,67 @@ +index('user_id', 'categories_user_id_index'); + $table->dropUnique('categories_user_id_parent_name_active_unique'); + $table->unique( + ['space_id', 'parent_unique_marker', 'name', 'active_unique_marker'], + 'categories_space_id_parent_name_active_unique', + ); + }); + + Schema::table('labels', function (Blueprint $table) { + $table->index('user_id', 'labels_user_id_index'); + $table->dropUnique('labels_user_id_name_deleted_at_unique'); + $table->unique(['space_id', 'name', 'deleted_at'], 'labels_space_id_name_deleted_at_unique'); + }); + + Schema::table('saved_filters', function (Blueprint $table) { + $table->index('user_id', 'saved_filters_user_id_index'); + $table->dropUnique('saved_filters_user_id_name_unique'); + $table->unique(['space_id', 'name'], 'saved_filters_space_id_name_unique'); + }); + } + + public function down(): void + { + Schema::table('categories', function (Blueprint $table) { + $table->dropUnique('categories_space_id_parent_name_active_unique'); + $table->unique( + ['user_id', 'parent_unique_marker', 'name', 'active_unique_marker'], + 'categories_user_id_parent_name_active_unique', + ); + $table->dropIndex('categories_user_id_index'); + }); + + Schema::table('labels', function (Blueprint $table) { + $table->dropUnique('labels_space_id_name_deleted_at_unique'); + $table->unique(['user_id', 'name', 'deleted_at'], 'labels_user_id_name_deleted_at_unique'); + $table->dropIndex('labels_user_id_index'); + }); + + Schema::table('saved_filters', function (Blueprint $table) { + $table->dropUnique('saved_filters_space_id_name_unique'); + $table->unique(['user_id', 'name'], 'saved_filters_user_id_name_unique'); + $table->dropIndex('saved_filters_user_id_index'); + }); + } +}; diff --git a/tests/Feature/Spaces/SpaceManagementTest.php b/tests/Feature/Spaces/SpaceManagementTest.php index b114d63f..b9749fd3 100644 --- a/tests/Feature/Spaces/SpaceManagementTest.php +++ b/tests/Feature/Spaces/SpaceManagementTest.php @@ -1,5 +1,6 @@ onboarded()->create(); Feature::for($user)->activate(Spaces::class); + // The personal space already carries the default categories (as it would + // after registration), so seeding the same names into a second space must + // NOT collide — category name uniqueness is per space, not per user. + app(CreateDefaultCategories::class)->handle($user, $user->personalSpace); + $this->actingAs($user)->post('/settings/spaces', ['name' => 'Acme']) ->assertRedirect(); @@ -18,7 +24,8 @@ it('creates a space, seeds its categories and switches to it', function () { expect($space)->not->toBeNull() ->and($user->fresh()->current_space_id)->toBe($space->id) - ->and(Category::where('space_id', $space->id)->count())->toBeGreaterThan(0); + ->and(Category::where('space_id', $space->id)->count())->toBeGreaterThan(0) + ->and(Category::where('space_id', $user->personalSpace->id)->count())->toBeGreaterThan(0); }); it('forbids creating a space without the spaces feature', function () {