Просмотр исходного кода

Merge branch 'folder' into dev

moell 1 год назад
Родитель
Сommit
2396e1b8b3

+ 201 - 0
app/Http/Controllers/API/FolderController.php

@@ -0,0 +1,201 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Http\Controllers\Controller;
+use App\Http\Requests\API\Folder\CreateRequest;
+use App\Http\Requests\API\Folder\UpdateRequest;
+use App\Http\Resources\API\FolderDetailResource;
+use App\Models\Folder;
+use App\Models\Library;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
+
+class FolderController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function library($libraryId)
+    {
+        $folders = Folder::query()
+            ->where("library_id", $libraryId)
+            ->when(request("parent_id", 0) > 0, function ($query) {
+                return $query->where("path", "like", "%," . \request("parent_id") . ",%")->where("id", "!=", \request("parent_id"));
+            })
+            ->orderByDesc("sequence")
+            ->get([
+                'id',
+                'name',
+                'parent_id'
+            ]);
+
+        return $this->success([
+            'data' => make_tree($folders->toArray(), \request("parent_id", 0)),
+        ]);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     */
+    public function store(CreateRequest $request)
+    {
+        $library = Library::query()->findOrFail($request->library_id);
+
+        $parentFolder = $request->parent_id > 0
+            ? Folder::query()->where("library_id", $library->id)->findOrFail($request->parent_id)
+            : null;
+
+        $names = [];
+        $updateFolders = [];
+
+        foreach ($request->items as $item) {
+            if (! isset($item['name']) || !$item['name']) {
+                return $this->badRequest("Folder name cannot be empty");
+            }
+
+            if (in_array($item['name'], $names)) {
+                return $this->badRequest("Folder names must be unique");
+            }
+            $names[] = $item['name'];
+
+            $isUpdate = isset($item['id']) && $item['id'];
+
+            $count = Folder::query()
+                ->where("library_id", $library->id)
+                ->where("parent_id", $request->parent_id)
+                ->when($isUpdate, function ($query) use ($item) {
+                    return $query->where("id", "!=", $item['id']);
+                })
+                ->where("name", $item['name'])
+                ->count();
+            if ($count > 0) {
+                return $this->badRequest("Folder names must be unique");
+            }
+
+            if ($isUpdate) {
+                $folder = Folder::query()
+                    ->where("library_id", $library->id)
+                    ->where("parent_id", $request->parent_id)
+                    ->find($item['id']);
+                if (! $folder) {
+                    return $this->badRequest("Illegal parameters or the file relationship that needs to be updated has changed.");
+                }
+
+                $updateFolders[$item['id']] = $folder;
+            }
+        }
+
+        foreach ($request->items as $item) {
+            $isUpdate = isset($item['id']) && $item['id'];
+            $data = [
+                'name' => $item['name'],
+                'sequence' => data_get($item, "sequence", 0),
+            ];
+
+            if ($isUpdate) {
+                $folder = $updateFolders[$item['id']];
+                $folder->fill($data);
+                $folder->save();
+            } else {
+                $folder = Folder::query()->create([
+                    'company_id' => Auth::user()->company_id,
+                    'library_id' => $library->id,
+                    'parent_id' => $request->parent_id,
+                    ...$data
+                ]);
+
+                $folder->path = $parentFolder ? $parentFolder?->path . $folder->id . "," : sprintf(",%s,", $folder->id);
+                $folder->save();
+            }
+        }
+
+        return $this->created();
+    }
+
+    /**
+     * Display the specified resource.
+     */
+    public function show(string $id)
+    {
+        $folder = Folder::query()->findOrFail($id);
+
+
+        return new FolderDetailResource($folder);
+    }
+
+    /**
+     * Update the specified resource in storage.
+     */
+    public function update(UpdateRequest $request, string $id)
+    {
+        $folder = Folder::query()->findOrFail($id);
+
+        $library = Library::query()->findOrFail($request->library_id);
+
+        $parentFolder = $request->parent_id > 0
+            ? Folder::query()->where("library_id", $library->id)->findOrFail($request->parent_id)
+            : null;
+
+        $count = Folder::query()
+            ->where("library_id", $request->id)
+            ->where("parent_id", $request->parent_id)
+            ->where("name", $request->name)
+            ->where("id", "!=", $folder->id)
+            ->count();
+        if ($count > 0) {
+            return $this->badRequest("Folder names must be unique");
+        }
+
+        $fields = ['name', 'sequence'];
+        if ($parentFolder?->id == $folder->parent_id) {
+            $folder->fill($request->only($fields));
+            $folder->save();
+        } else {
+            $path = $parentFolder->path . $folder->id . ",";
+            $folderData = [
+                ...$request->only($fields),
+                'path' => $path,
+                'parent_id' => $request->parent_id,
+                'library_id' => $library->id,
+            ];
+
+            $children = Folder::query()
+                ->where("library_id", $library->id)
+                ->where("id", "!=", $folder->id)
+                ->where("path", "like", "%," . $folder->id . ",%")
+                ->get();
+
+            foreach ($children as $child) {
+                $child->fill([
+                    'path' => str_replace($folder->path, $path, $child->path),
+                    'library_id' => $library->id,
+                ]);
+                $child->save();
+            }
+
+            $folder->fill($folderData);
+            $folder->save();
+        }
+
+        return $this->noContent();
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     */
+    public function destroy(string $id)
+    {
+        $folder = Folder::query()->findOrFail($id);
+
+        $children = Folder::query()->where("parent_id", $folder->id)->count();
+        if ($children > 0) {
+            return $this->badRequest("Subordinate folders or containers exist and are not allowed to be deleted.");
+        }
+
+        $folder->delete();
+
+        return $this->noContent();
+    }
+}

+ 30 - 0
app/Http/Requests/API/Folder/CreateRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Requests\API\Folder;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class CreateRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
+     */
+    public function rules(): array
+    {
+        return [
+            "library_id" => "required",
+            "parent_id" => "required|numeric",
+            "items" => "required",
+        ];
+    }
+}

+ 31 - 0
app/Http/Requests/API/Folder/UpdateRequest.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Http\Requests\API\Folder;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class UpdateRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
+     */
+    public function rules(): array
+    {
+        return [
+            "library_id" => "required",
+            "parent_id" => "required|numeric",
+            "name" => "required",
+            "sequence" => "numeric",
+        ];
+    }
+}

+ 24 - 0
app/Http/Resources/API/FolderDetailResource.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class FolderDetailResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'library_id' => $this->library_id,
+            'name' => $this->name,
+            'parent_id' => $this->parent_id,
+            'sequence' => $this->sequence,
+        ];
+    }
+}

+ 20 - 0
app/Models/Folder.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Models;
+
+use App\Models\Scopes\CompanyScope;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class Folder extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $guarded = ['id'];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope(new CompanyScope);
+    }
+}

+ 30 - 0
app/helpers.php

@@ -0,0 +1,30 @@
+<?php
+
+if (!function_exists('make_tree')) {
+    /**
+     * @param array $list
+     * @param int $parentId
+     * @return array
+     */
+    function make_tree(array $list, $parentId = 0) {
+        $tree = [];
+        if (empty($list)) {
+            return $tree;
+        }
+
+        $newList = [];
+        foreach ($list as $k => $v) {
+            $newList[$v['id']] = $v;
+        }
+
+        foreach ($newList as $value) {
+            if ($parentId == $value['parent_id']) {
+                $tree[] = &$newList[$value['id']];
+            } elseif (isset($newList[$value['parent_id']])) {
+                $newList[$value['parent_id']]['children'][] = &$newList[$value['id']];
+            }
+        }
+
+        return $tree;
+    }
+}

+ 4 - 1
composer.json

@@ -30,7 +30,10 @@
             "App\\": "app/",
             "Database\\Factories\\": "database/factories/",
             "Database\\Seeders\\": "database/seeders/"
-        }
+        },
+        "files": [
+            "app/helpers.php"
+        ]
     },
     "autoload-dev": {
         "psr-4": {

+ 29 - 0
database/factories/FolderFactory.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\Library;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Facades\Auth;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Folder>
+ */
+class FolderFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            "library_id" => Library::factory()->create(),
+            "company_id" => Auth::user()->company_id,
+            "name" => fake()->name(),
+            "parent_id" => 0,
+            "sequence" => 0,
+        ];
+    }
+}

+ 35 - 0
database/migrations/2024_02_28_124423_create_folders_table.php

@@ -0,0 +1,35 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('folders', function (Blueprint $table) {
+            $table->id();
+            $table->integer("company_id");
+            $table->integer("library_id");
+            $table->string("name", 60);
+            $table->integer("parent_id")->default(0);
+            $table->string("path")->nullable();
+            $table->integer("sequence")->default(0);
+            $table->softDeletes();
+            $table->timestamps();
+            $table->index(['company_id', 'library_id']);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('folders');
+    }
+};

+ 6 - 0
routes/api.php

@@ -73,5 +73,11 @@ Route::middleware(['auth:sanctum'])->group(function () {
 
         Route::get("team-member/{project}", [API\TeamMemberController::class, "members"])->name("team-member.list");
         Route::delete("team-member/{team_member}", [API\TeamMemberController::class, "destroy"])->name("team-member.destroy");
+
+        Route::get("library/{library}/folder", [API\FolderController::class, "library"])->name("folder.library");
+        Route::post("folder", [API\FolderController::class, "store"])->name("folder.store");
+        Route::get("folder/{folder}", [API\FolderController::class, "show"])->name("folder.show");
+        Route::patch("folder/{folder}", [API\FolderController::class, "update"])->name("folder.update");
+        Route::delete("folder/{folder}", [API\FolderController::class, "destroy"])->name("folder.destroy");
     });
 });

+ 90 - 0
tests/Feature/API/FolderTest.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace Tests\Feature\API;
+
+use App\Models\Folder;
+use App\Models\Library;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Foundation\Testing\WithFaker;
+use Tests\Feature\TestCase;
+
+class FolderTest extends TestCase
+{
+    public function test_create_folder(): void
+    {
+        $response = $this->postJson(route('folder.store'), [
+            'library_id' => Library::factory()->create()->id,
+            'parent_id' => 0,
+            'items' => [
+                ['name' => fake()->name(), 'sequence' => 0],
+                ['name' => fake()->name(), 'sequence' => 0],
+            ]
+        ]);
+
+        $response->assertStatus(201);
+    }
+
+    protected function test_folder_list()
+    {
+        Folder::factory(30)->create();
+
+        $response = $this->get(route('folder.index'));
+
+        $response->assertStatus(200)
+            ->assertJsonStructure([
+                'data' => [
+                    '*' => [
+                        'id',
+                        'name',
+                        'global',
+                        'status',
+                        'company'
+                    ]
+                ]
+            ]);
+    }
+
+
+    public function test_folder_show(): void
+    {
+        $folder = Folder::factory()->create();
+
+        $response = $this->getJson(route('folder.show', ['folder' => $folder->id]));
+
+        $response->assertStatus(200)
+            ->assertJsonStructure([
+                'data' => [
+                    'library_id',
+                    'name',
+                    'parent_id',
+                    'sequence',
+                ]
+            ]);
+    }
+
+    public function test_folder_update(): void
+    {
+        $folder = Folder::factory()->create();
+
+        $form = Folder::factory()->make();
+
+        $response = $this->patchJson(route('folder.update', ['folder' => $folder->id]), $form->toArray());
+
+        $response->assertStatus(204);
+
+        $newAsset = Folder::find($folder->id);
+
+        $this->assertEquals($form->name, $newAsset->name);
+    }
+
+    public function test_folder_delete(): void
+    {
+        $folder = Folder::factory()->create();
+
+        $response = $this->deleteJson(route('folder.destroy', ['folder' => $folder->id]));
+
+        $response->assertStatus(204);
+
+        $this->assertNull(Folder::find($folder->id));
+    }
+}