working
This commit is contained in:
parent
5eb85fe090
commit
e1192ee506
@ -5,7 +5,8 @@ declare(strict_types=1);
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return static function (ContainerConfigurator $containerConfigurator): void {
|
||||
$containerConfigurator->extension('framework', ['secret' => '%env(APP_SECRET)%',
|
||||
$containerConfigurator->extension('framework', [
|
||||
'secret' => '%env(APP_SECRET)%',
|
||||
'csrf_protection' => true,
|
||||
'session' => [
|
||||
'handler_id' => null,
|
||||
@ -13,6 +14,6 @@ return static function (ContainerConfigurator $containerConfigurator): void {
|
||||
'cookie_httponly' => true,
|
||||
'cookie_samesite' => 'lax',
|
||||
],
|
||||
'php_errors' => ['log' => true]
|
||||
'php_errors' => ['log' => true],
|
||||
]);
|
||||
};
|
||||
|
||||
79
src/new/src/PSC/Shop/MediaBundle/Api/All.php
Normal file
79
src/new/src/PSC/Shop/MediaBundle/Api/All.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace PSC\Shop\MediaBundle\Api;
|
||||
|
||||
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||
use MongoDB\BSON\ObjectId;
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use Nelmio\ApiDocBundle\Attribute\Security;
|
||||
use OpenApi\Attributes\JsonContent;
|
||||
use OpenApi\Attributes\Response;
|
||||
use OpenApi\Attributes\Tag;
|
||||
use PSC\Shop\MediaBundle\Document\Folder;
|
||||
use PSC\Shop\MediaBundle\Dto\Folder\All as PSCAll;
|
||||
use PSC\Shop\MediaBundle\Model\Folder as PSCFolder;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class All extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DocumentManager $dm,
|
||||
) {}
|
||||
|
||||
#[Response(
|
||||
response: 200,
|
||||
description: 'get all media',
|
||||
content: new JsonContent(ref: new Model(type: PSCAll::class)),
|
||||
)]
|
||||
#[Route(path: '/media/all{:uuid}', methods: ['GET'])]
|
||||
#[Tag('Media')]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Security(name: 'Bearer')]
|
||||
public function all()
|
||||
{
|
||||
$folders = $this->dm
|
||||
->getRepository(Folder::class)
|
||||
->createQueryBuilder('folder')
|
||||
->field('parent_id')
|
||||
->exists(false)
|
||||
->sort('title', 'ASC')
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$output = new PSCAll();
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$f = new PSCFolder();
|
||||
$f->setTitle($folder->getTitle());
|
||||
$f->setUuid($folder->getId());
|
||||
$this->getSubFolder($f);
|
||||
$output->data[] = $f;
|
||||
}
|
||||
return $this->json($output);
|
||||
}
|
||||
|
||||
private function getSubFolder(PSCFolder $f): void
|
||||
{
|
||||
$folders = $this->dm
|
||||
->getRepository(Folder::class)
|
||||
->createQueryBuilder('folder')
|
||||
->field('parent_id')
|
||||
->equals(new ObjectId($f->getUuid()))
|
||||
->sort('title', 'ASC')
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$tmp = [];
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$s = new PSCFolder();
|
||||
$s->setTitle($folder->getTitle());
|
||||
$s->setUuid($folder->getId());
|
||||
$tmp[] = $s;
|
||||
}
|
||||
|
||||
$f->setSubFolders($tmp);
|
||||
}
|
||||
}
|
||||
58
src/new/src/PSC/Shop/MediaBundle/Api/AllFolderPage.php
Normal file
58
src/new/src/PSC/Shop/MediaBundle/Api/AllFolderPage.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace PSC\Shop\MediaBundle\Api;
|
||||
|
||||
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use MongoDB\BSON\ObjectId;
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use Nelmio\ApiDocBundle\Attribute\Security;
|
||||
use OpenApi\Attributes\JsonContent;
|
||||
use OpenApi\Attributes\Response;
|
||||
use OpenApi\Attributes\Tag;
|
||||
use PSC\Shop\MediaBundle\Document\Media;
|
||||
use PSC\Shop\MediaBundle\Dto\Media\Folder;
|
||||
use PSC\Shop\MediaBundle\Model\Media as PSCMedia;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class AllFolderPage extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DocumentManager $dm,
|
||||
private readonly PaginatorInterface $paginator,
|
||||
) {}
|
||||
|
||||
#[Response(
|
||||
response: 200,
|
||||
description: 'get all media in folder',
|
||||
content: new JsonContent(ref: new Model(type: Folder::class)),
|
||||
)]
|
||||
#[Route(path: '/folder/{uuid}/page/{page}/{max}', methods: ['GET'])]
|
||||
#[Tag('Media')]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Security(name: 'Bearer')]
|
||||
public function all(string $uuid, int $page = 1, int $max = 12)
|
||||
{
|
||||
$qb = $this->dm
|
||||
->getRepository(Media::class)
|
||||
->createQueryBuilder('media')
|
||||
->field('folder.$id')
|
||||
->equals(new ObjectId($uuid))
|
||||
->sort('title', 'ASC');
|
||||
$pagination = $this->paginator->paginate($query = $qb->getQuery(), $page, $max);
|
||||
|
||||
$output = new Folder();
|
||||
$output->count = $pagination->getTotalItemCount();
|
||||
$output->currentPage = $pagination->getCurrentPageNumber();
|
||||
$output->lastPage = ceil($output->count / $max);
|
||||
foreach ($pagination->getItems() as $media) {
|
||||
$f = new PSCMedia();
|
||||
$f->setTitle($media->getTitle());
|
||||
$f->setUrl($media->getUrl());
|
||||
$output->data[] = $f;
|
||||
}
|
||||
return $this->json($output);
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,9 @@ class Add extends AbstractController
|
||||
$cat = new PSCFolder();
|
||||
$cat->setTitle($data->title);
|
||||
$cat->setIcon('fa-file');
|
||||
if ($data->parentUuid) {
|
||||
$cat->setParentId($data->parentUuid);
|
||||
}
|
||||
$this->dm->persist($cat);
|
||||
$this->dm->flush();
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace PSC\Shop\MediaBundle\Api\Folder;
|
||||
|
||||
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||
use MongoDB\BSON\ObjectId;
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use Nelmio\ApiDocBundle\Attribute\Security;
|
||||
use OpenApi\Attributes\JsonContent;
|
||||
@ -47,9 +48,33 @@ class All extends AbstractController
|
||||
$f = new PSCFolder();
|
||||
$f->setTitle($folder->getTitle());
|
||||
$f->setUuid($folder->getId());
|
||||
$this->getSubFolder($f);
|
||||
$output->data[] = $f;
|
||||
}
|
||||
|
||||
return $this->json($output);
|
||||
}
|
||||
|
||||
private function getSubFolder(PSCFolder $f): void
|
||||
{
|
||||
$folders = $this->dm
|
||||
->getRepository(Folder::class)
|
||||
->createQueryBuilder('folder')
|
||||
->field('parent_id')
|
||||
->equals(new ObjectId($f->getUuid()))
|
||||
->sort('title', 'ASC')
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$tmp = [];
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$s = new PSCFolder();
|
||||
$s->setTitle($folder->getTitle());
|
||||
$s->setUuid($folder->getId());
|
||||
$this->getSubFolder($s);
|
||||
$tmp[] = $s;
|
||||
}
|
||||
|
||||
$f->setSubFolders($tmp);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,66 +3,70 @@
|
||||
namespace PSC\Shop\MediaBundle\Api;
|
||||
|
||||
use Doctrine\ODM\MongoDB\DocumentManager;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Nelmio\ApiDocBundle\Attribute\Security;
|
||||
use OpenApi\Attributes\MediaType;
|
||||
use OpenApi\Attributes\Property;
|
||||
use OpenApi\Attributes\RequestBody;
|
||||
use OpenApi\Attributes\Response;
|
||||
use OpenApi\Attributes\Schema;
|
||||
use OpenApi\Attributes\Tag;
|
||||
use PSC\Shop\MediaBundle\Document\Media;
|
||||
use PSC\Shop\MediaBundle\Helper\MediaManager;
|
||||
use PSC\Shop\MediaBundle\Model\Media as MediaModel;
|
||||
use PSC\System\SettingsBundle\Service\Shop;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class Upload extends AbstractController
|
||||
{
|
||||
/**
|
||||
* create media
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="media",
|
||||
* @OA\JsonContent(ref=@Model(type=\PSC\Shop\MediaBundle\Model\Media::class))
|
||||
* )
|
||||
* @OA\RequestBody(
|
||||
* description="This is a request body",
|
||||
* @OA\MediaType(
|
||||
* mediaType="multipart/form-data",
|
||||
* @OA\Schema(
|
||||
* @OA\Property(
|
||||
* description="folder of file",
|
||||
* property="folder",
|
||||
* type="string"
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* description="Binary content of file",
|
||||
* property="file",
|
||||
* type="string",
|
||||
* format="binary",
|
||||
* ),
|
||||
* required={"file", "folder"}
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* @OA\Tag(name="Media")
|
||||
* @IsGranted("ROLE_USER")
|
||||
* @Security(name="ApiKeyAuth")
|
||||
* @Security(name="Bearer")
|
||||
*/
|
||||
#[RequestBody(description: 'file and folder', content: [
|
||||
new MediaType('multipart/form-data', new Schema(
|
||||
properties: [new Property(
|
||||
property: 'file',
|
||||
format: 'binary',
|
||||
type: 'file',
|
||||
description: 'media file',
|
||||
), new Property(
|
||||
property: 'folder',
|
||||
description: 'folder',
|
||||
)],
|
||||
required: ['file', 'folder'],
|
||||
)),
|
||||
])]
|
||||
#[Response(response: 200, description: 'add upload to folder', ref: MediaModel::class)]
|
||||
#[Response(response: 400, description: 'bad request no folder or folder not exists')]
|
||||
#[Route(path: '/create', methods: ['POST'])]
|
||||
#[Tag(name: 'Media')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
#[Security(name: 'Bearer')]
|
||||
#[Security(name: 'ApiKeyAuth')]
|
||||
public function create(
|
||||
MediaManager $mediaManager,
|
||||
Shop $shopService,
|
||||
DocumentManager $documentManager,
|
||||
Request $req,
|
||||
): JsonResponse {
|
||||
if (!$req->get('folder', false)) {
|
||||
return $this->json(
|
||||
data: ['error' => 'folder not provided'],
|
||||
status: JsonResponse::HTTP_BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
$selectedShop = $shopService->getShopByDomain();
|
||||
$selectedFolder = $documentManager
|
||||
->getRepository('PSC\Shop\MediaBundle\Document\Folder')
|
||||
->findOneBy(['id' => $req->get('folder')]);
|
||||
|
||||
if (!$selectedFolder) {
|
||||
return $this->json(
|
||||
data: ['error' => 'folder not found'],
|
||||
status: JsonResponse::HTTP_BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
$handler = $mediaManager->getHandlerForType('pdf');
|
||||
$media = new Media();
|
||||
$helper = $handler->getFormHelper($media);
|
||||
|
||||
@ -6,9 +6,11 @@ use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use OpenApi\Attributes\Items;
|
||||
use OpenApi\Attributes\Property;
|
||||
use PSC\Shop\MediaBundle\Model\Folder;
|
||||
use Symfony\Component\Serializer\Attribute\MaxDepth;
|
||||
|
||||
final class All
|
||||
{
|
||||
#[Property(type: 'array', items: new Items(ref: new Model(type: Folder::class)))]
|
||||
#[MaxDepth(4)]
|
||||
public array $data;
|
||||
}
|
||||
|
||||
@ -8,4 +8,12 @@ final class Input
|
||||
{
|
||||
#[Property(type: 'string')]
|
||||
public string $title;
|
||||
|
||||
#[Property(type: 'string')]
|
||||
public null|string $parentUuid;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->parentUuid = null;
|
||||
}
|
||||
}
|
||||
|
||||
23
src/new/src/PSC/Shop/MediaBundle/Dto/Media/Folder.php
Normal file
23
src/new/src/PSC/Shop/MediaBundle/Dto/Media/Folder.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace PSC\Shop\MediaBundle\Dto\Media;
|
||||
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use OpenApi\Attributes\Items;
|
||||
use OpenApi\Attributes\Property;
|
||||
use PSC\Shop\MediaBundle\Model\Media;
|
||||
use Symfony\Component\Serializer\Attribute\MaxDepth;
|
||||
|
||||
final class Folder
|
||||
{
|
||||
#[Property(type: 'array', items: new Items(ref: new Model(type: Media::class)))]
|
||||
#[MaxDepth(4)]
|
||||
public array $data;
|
||||
|
||||
#[Property(type: 'integer')]
|
||||
public int $count;
|
||||
#[Property(type: 'integer')]
|
||||
public int $lastPage;
|
||||
#[Property(type: 'integer')]
|
||||
public int $currentPage;
|
||||
}
|
||||
@ -4,6 +4,7 @@ namespace PSC\Shop\MediaBundle\Model;
|
||||
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Serializer\Attribute\MaxDepth;
|
||||
|
||||
class Folder
|
||||
{
|
||||
@ -13,7 +14,7 @@ class Folder
|
||||
#[OA\Property(type: 'string')]
|
||||
private string $uuid = '';
|
||||
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: new Model(type: Folder::class)))]
|
||||
#[MaxDepth(4), OA\Property(type: 'array', items: new OA\Items(ref: new Model(type: Folder::class)))]
|
||||
private array $subFolders = [];
|
||||
|
||||
public function getTitle(): string
|
||||
|
||||
83
src/new/tests/PSC/Shop/Media/Api/CompleteTest.php
Normal file
83
src/new/tests/PSC/Shop/Media/Api/CompleteTest.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\PSC\Shop\Media\Api;
|
||||
|
||||
use Faker\Factory;
|
||||
use Faker\Generator;
|
||||
use PSC\Shop\ContactBundle\Repository\ContactRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Tests\RefreshDatabaseTrait;
|
||||
|
||||
class CompleteTest extends WebTestCase
|
||||
{
|
||||
use RefreshDatabaseTrait;
|
||||
|
||||
private Generator $faker;
|
||||
private $client;
|
||||
private $subFolderUuid;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$_SERVER['SERVER_NAME'] = 'localhost';
|
||||
$this->faker = Factory::create(locale: 'de_DE');
|
||||
|
||||
$this->client = static::createClient();
|
||||
|
||||
$userRepository = static::getContainer()->get(ContactRepository::class);
|
||||
|
||||
$testUser = $userRepository->loadUserByUsername('admin@shop.de');
|
||||
|
||||
$this->client->loginUser($testUser, 'api');
|
||||
|
||||
$name = $this->faker->slug();
|
||||
$this->client->jsonRequest(
|
||||
'POST',
|
||||
'/api/media/folder/add',
|
||||
[
|
||||
'title' => $name,
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
$data = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$subName = $this->faker->slug();
|
||||
$this->client->jsonRequest(
|
||||
'POST',
|
||||
'/api/media/folder/add',
|
||||
[
|
||||
'title' => $subName,
|
||||
'parentUuid' => $data['uuid'],
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
$subData = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$this->subFolderUuid = $subData['uuid'];
|
||||
}
|
||||
|
||||
public function testUploadMediaToSubFolder(): void
|
||||
{
|
||||
$uploadedFile = new UploadedFile(__DIR__ . '/../../../../kenny.jpg', 'kenney.jpg');
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/media/create',
|
||||
['folder' => $this->subFolderUuid],
|
||||
[
|
||||
'file' => $uploadedFile,
|
||||
],
|
||||
);
|
||||
|
||||
$media = json_decode($this->client->getResponse()->getContent(), true);
|
||||
self::assertSame('kenney.jpg', $media['title']);
|
||||
|
||||
$this->client->request('GET', sprintf('/api/media/folder/%s/page/%s', $this->subFolderUuid, 1), [], []);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$media = json_decode($this->client->getResponse()->getContent(), true);
|
||||
self::assertCount(1, $media['data']);
|
||||
}
|
||||
}
|
||||
@ -45,6 +45,57 @@ class FolderTest extends WebTestCase
|
||||
self::assertSame($name, $data['title']);
|
||||
}
|
||||
|
||||
public function testCreateFolderWithSubFolders(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$userRepository = static::getContainer()->get(ContactRepository::class);
|
||||
|
||||
$testUser = $userRepository->loadUserByUsername('admin@shop.de');
|
||||
|
||||
$client->loginUser($testUser, 'api');
|
||||
|
||||
$name = $this->faker->slug();
|
||||
$client->jsonRequest(
|
||||
'POST',
|
||||
'/api/media/folder/add',
|
||||
[
|
||||
'title' => $name,
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$data = json_decode($client->getResponse()->getContent(), true);
|
||||
self::assertSame($name, $data['title']);
|
||||
|
||||
$subName = $this->faker->slug();
|
||||
$client->jsonRequest(
|
||||
'POST',
|
||||
'/api/media/folder/add',
|
||||
[
|
||||
'title' => $subName,
|
||||
'parentUuid' => $data['uuid'],
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$subData = json_decode($client->getResponse()->getContent(), true);
|
||||
self::assertSame($subName, $subData['title']);
|
||||
|
||||
$client->jsonRequest('GET', '/api/media/folder/all', [], []);
|
||||
self::assertCount(1, json_decode($client->getResponse()->getContent(), true)['data']);
|
||||
self::assertCount(1, json_decode($client->getResponse()->getContent(), true)['data'][0]['subFolders']);
|
||||
|
||||
self::assertSame($name, json_decode($client->getResponse()->getContent(), true)['data'][0]['title']);
|
||||
|
||||
self::assertSame(
|
||||
$subName,
|
||||
json_decode($client->getResponse()->getContent(), true)['data'][0]['subFolders'][0]['title'],
|
||||
);
|
||||
}
|
||||
|
||||
public function testCreateAndGetFolders(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
@ -22,11 +22,24 @@ class UploadTest extends WebTestCase
|
||||
|
||||
$client->loginUser($testUser, 'api');
|
||||
|
||||
$client->jsonRequest(
|
||||
'POST',
|
||||
'/api/media/folder/add',
|
||||
[
|
||||
'title' => 'testFolder',
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
$folder = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$uploadedFile = new UploadedFile(__DIR__ . '/../../../../kenny.jpg', 'kenney.jpg');
|
||||
$client->request(
|
||||
'POST',
|
||||
'/api/media/create',
|
||||
[],
|
||||
[
|
||||
'folder' => $folder['uuid'],
|
||||
],
|
||||
[
|
||||
'file' => $uploadedFile,
|
||||
],
|
||||
|
||||
@ -22,61 +22,80 @@ class UploadVariantTest extends WebTestCase
|
||||
|
||||
$client->loginUser($testUser, 'api');
|
||||
|
||||
$uploadedFile = new UploadedFile(
|
||||
__DIR__.'/../../../../kenny.jpg',
|
||||
'kenney.jpg'
|
||||
$client->jsonRequest(
|
||||
'POST',
|
||||
'/api/media/folder/add',
|
||||
[
|
||||
'title' => 'testFolder',
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
$folder = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$uploadedFile = new UploadedFile(__DIR__ . '/../../../../kenny.jpg', 'kenney.jpg');
|
||||
$client->request(
|
||||
'POST',
|
||||
'/api/media/create',
|
||||
['folder' => $folder['uuid']],
|
||||
[
|
||||
'file' => $uploadedFile,
|
||||
],
|
||||
);
|
||||
$client->request('POST', '/api/media/create', [], [
|
||||
'file' => $uploadedFile
|
||||
]);
|
||||
|
||||
$media = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
self::assertSame('kenney.jpg', $media['title']);
|
||||
self::assertNotEmpty($media['url']);
|
||||
|
||||
$uploadedFile = new UploadedFile(
|
||||
__DIR__.'/../../../../kenny_crop.jpg',
|
||||
'kenny_crop.jpg'
|
||||
);
|
||||
$client->request('POST', '/api/media/variant/create', [
|
||||
$uploadedFile = new UploadedFile(__DIR__ . '/../../../../kenny_crop.jpg', 'kenny_crop.jpg');
|
||||
$client->request(
|
||||
'POST',
|
||||
'/api/media/variant/create',
|
||||
[
|
||||
'uuid' => $media['uuid'],
|
||||
'settings' => '1/2'
|
||||
], [
|
||||
'file' => $uploadedFile
|
||||
]);
|
||||
'settings' => '1/2',
|
||||
],
|
||||
[
|
||||
'file' => $uploadedFile,
|
||||
],
|
||||
);
|
||||
|
||||
$mediaVariant = json_decode($client->getResponse()->getContent(), true);
|
||||
self::assertNotEmpty($mediaVariant['variants'][0]['url']);
|
||||
self::assertSame('kenny-crop.jpg', $mediaVariant['variants'][0]['title']);
|
||||
self::assertCount(1, $mediaVariant['variants']);
|
||||
|
||||
$uploadedFile = new UploadedFile(
|
||||
__DIR__.'/../../../../kenny_crop1.jpg',
|
||||
'kenny_crop1.jpg'
|
||||
);
|
||||
$client->request('POST', '/api/media/variant/create', [
|
||||
$uploadedFile = new UploadedFile(__DIR__ . '/../../../../kenny_crop1.jpg', 'kenny_crop1.jpg');
|
||||
$client->request(
|
||||
'POST',
|
||||
'/api/media/variant/create',
|
||||
[
|
||||
'uuid' => $media['uuid'],
|
||||
'settings' => '1/4'
|
||||
], [
|
||||
'file' => $uploadedFile
|
||||
]);
|
||||
'settings' => '1/4',
|
||||
],
|
||||
[
|
||||
'file' => $uploadedFile,
|
||||
],
|
||||
);
|
||||
|
||||
$mediaVariant = json_decode($client->getResponse()->getContent(), true);
|
||||
self::assertNotEmpty($mediaVariant['variants'][1]['url']);
|
||||
self::assertSame('kenny-crop1.jpg', $mediaVariant['variants'][1]['title']);
|
||||
self::assertCount(2, $mediaVariant['variants']);
|
||||
|
||||
$uploadedFile = new UploadedFile(
|
||||
__DIR__.'/../../../../kenny_crop_better.jpg',
|
||||
'kenny_crop_better.jpg'
|
||||
);
|
||||
$client->request('POST', '/api/media/variant/create', [
|
||||
$uploadedFile = new UploadedFile(__DIR__ . '/../../../../kenny_crop_better.jpg', 'kenny_crop_better.jpg');
|
||||
$client->request(
|
||||
'POST',
|
||||
'/api/media/variant/create',
|
||||
[
|
||||
'uuid' => $media['uuid'],
|
||||
'settings' => '1/2'
|
||||
], [
|
||||
'file' => $uploadedFile
|
||||
]);
|
||||
'settings' => '1/2',
|
||||
],
|
||||
[
|
||||
'file' => $uploadedFile,
|
||||
],
|
||||
);
|
||||
|
||||
$mediaVariant = json_decode($client->getResponse()->getContent(), true);
|
||||
self::assertNotEmpty($mediaVariant['variants'][1]['url']);
|
||||
|
||||
@ -8,13 +8,13 @@
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"ky": "^1.8.1",
|
||||
"lucide-vue-next": "^0.514.0",
|
||||
"pinia": "^3.0.3",
|
||||
"reka-ui": "^2.3.2",
|
||||
"reka-ui": "^2.4.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"ts-debounce": "^4.0.0",
|
||||
@ -364,11 +364,11 @@
|
||||
|
||||
"@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="],
|
||||
|
||||
"@vueuse/core": ["@vueuse/core@13.5.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.5.0", "@vueuse/shared": "13.5.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g=="],
|
||||
"@vueuse/core": ["@vueuse/core@13.6.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.6.0", "@vueuse/shared": "13.6.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A=="],
|
||||
|
||||
"@vueuse/metadata": ["@vueuse/metadata@13.5.0", "", {}, "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw=="],
|
||||
"@vueuse/metadata": ["@vueuse/metadata@13.6.0", "", {}, "sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ=="],
|
||||
|
||||
"@vueuse/shared": ["@vueuse/shared@13.5.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g=="],
|
||||
"@vueuse/shared": ["@vueuse/shared@13.6.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg=="],
|
||||
|
||||
"alien-signals": ["alien-signals@1.0.13", "", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="],
|
||||
|
||||
@ -560,7 +560,7 @@
|
||||
|
||||
"pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="],
|
||||
|
||||
"reka-ui": ["reka-ui@2.3.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^12.5.0", "@vueuse/shared": "^12.5.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-lCysSCILH2uqShEnt93/qzlXnB7ySvK7scR0Q5C+a2iXwFVzHhvZQsMaSnbQYueoCihx6yyUZTYECepnmKrbRA=="],
|
||||
"reka-ui": ["reka-ui@2.4.1", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^12.5.0", "@vueuse/shared": "^12.5.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-NB7DrCsODN8MH02BWtgiExygfFcuuZ5/PTn6fMgjppmFHqePvNhmSn1LEuF35nel6PFbA4v+gdj0IoGN1yZ+vw=="],
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
|
||||
@ -11,16 +11,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"ky": "^1.8.1",
|
||||
"lucide-vue-next": "^0.514.0",
|
||||
"pinia": "^3.0.3",
|
||||
"reka-ui": "^2.3.2",
|
||||
"reka-ui": "^2.4.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"ts-debounce": "^4.0.0",
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { Button } from '../../../components/ui/button';
|
||||
|
||||
interface Folder {
|
||||
uuid: string;
|
||||
title: string;
|
||||
subFolders?: Folder[];
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
folders: Folder[],
|
||||
selectedFolderId?: string | null
|
||||
}>();
|
||||
const emit = defineEmits(['select-folder']);
|
||||
|
||||
const selectFolder = (folderId: string) => {
|
||||
emit('select-folder', folderId);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="w-full">
|
||||
<li v-for="folder in folders" :key="folder.uuid">
|
||||
<Button
|
||||
:variant="folder.uuid === selectedFolderId ? 'secondary' : 'ghost'"
|
||||
@click="selectFolder(folder.uuid)"
|
||||
class="w-full justify-start"
|
||||
>
|
||||
{{ folder.title }}
|
||||
</Button>
|
||||
<div v-if="folder.subFolders && folder.subFolders.length > 0" class="ml-4">
|
||||
<FolderTree
|
||||
:folders="folder.subFolders"
|
||||
:selected-folder-id="selectedFolderId"
|
||||
@select-folder="selectFolder"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { fetchMediaFolders, fetchMediaByFolder } from '../../../lib/api';
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../../components/ui/resizable';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationEllipsis,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from '../../../components/ui/pagination'
|
||||
import { Button } from '../../../components/ui/button'
|
||||
import FolderTree from './FolderTree.vue';
|
||||
|
||||
interface Folder {
|
||||
uuid: string;
|
||||
title: string;
|
||||
subFolders?: Folder[];
|
||||
}
|
||||
|
||||
interface Media {
|
||||
uuid: string;
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const folders = ref<Folder[]>([]);
|
||||
const media = ref<Media[]>([]);
|
||||
const selectedFolder = ref<string | null>(null);
|
||||
const currentPage = ref(1);
|
||||
const totalPages = ref(1);
|
||||
|
||||
const loadFolders = async () => {
|
||||
try {
|
||||
const response: any = await fetchMediaFolders();
|
||||
folders.value = response.data;
|
||||
if (folders.value.length > 0) {
|
||||
selectFolder(folders.value[0].uuid);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch folders', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMedia = async (folderId: string, page: number = 1) => {
|
||||
try {
|
||||
const response: any = await fetchMediaByFolder(folderId, page);
|
||||
media.value = response.data;
|
||||
currentPage.value = response.currentPage;
|
||||
totalPages.value = response.lastPage;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch media for folder ${folderId}`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const selectFolder = (folderId: string) => {
|
||||
selectedFolder.value = folderId;
|
||||
loadMedia(folderId, 1);
|
||||
};
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
if (selectedFolder.value) {
|
||||
loadMedia(selectedFolder.value, page);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadFolders();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[70vh] flex flex-col">
|
||||
<h1 class="text-2xl font-bold mb-4">Media Browser</h1>
|
||||
<ResizablePanelGroup direction="horizontal" class="flex-grow rounded-lg border">
|
||||
<ResizablePanel :default-size="25">
|
||||
<div class="flex h-full items-start justify-center p-6">
|
||||
<FolderTree :folders="folders" :selected-folder-id="selectedFolder" @select-folder="selectFolder" />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel :default-size="75">
|
||||
<div class="flex flex-col h-full p-6">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 flex-grow">
|
||||
<div v-for="item in media" :key="item.uuid" class="aspect-square bg-gray-100 rounded-lg overflow-hidden">
|
||||
<img :src="item.url" :alt="item.name" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-center">
|
||||
<Pagination v-if="totalPages > 1" :total="totalPages" :sibling-count="1" show-edges :default-page="currentPage" @update:page="onPageChange">
|
||||
<PaginationContent v-slot="{ items }" class="flex items-center gap-1">
|
||||
<PaginationPrevious />
|
||||
<template v-for="(page, index) in items">
|
||||
<PaginationItem v-if="page.type === 'page'" :key="index" :value="page.value" as-child>
|
||||
<Button class="w-10 h-10 p-0" :variant="page.value === currentPage ? 'default' : 'outline'">
|
||||
{{ page.value }}
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
<PaginationEllipsis v-else :key="page.type" :index="index" />
|
||||
</template>
|
||||
<PaginationNext />
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</template>
|
||||
@ -3,8 +3,10 @@ import MediaElement from '../../../model/MediaElement';
|
||||
import { computed } from 'vue';
|
||||
import { Input } from '../../../components/ui/input'
|
||||
import { Button } from '../../../components/ui/button'
|
||||
import { fetchMediaDirectories, uploadFile } from '../../../lib/api';
|
||||
import { fetchMediaFolders, uploadFile } from '../../../lib/api';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { Dialog, DialogContent, DialogTrigger } from '../../../components/ui/dialog'
|
||||
import MediaBrowser from '../media/MediaBrowser.vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: MediaElement
|
||||
@ -52,7 +54,7 @@ const onFileChange = (event: Event) => {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
let response: any = await fetchMediaDirectories()
|
||||
let response: any = await fetchMediaFolders()
|
||||
directories.value = response.data
|
||||
if (response.data.length > 0) {
|
||||
selectedDirectory.value = directories.value[0].uuid;
|
||||
@ -87,7 +89,14 @@ const handleFile = async (file: File) => {
|
||||
<div>
|
||||
<label>{{ $t('id') }}</label>
|
||||
<Input v-model="theModel!.id" />
|
||||
<Dialog>
|
||||
<DialogTrigger as-child>
|
||||
<Button class="my-2 w-full">Mediabrowser</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="sm:max-w-5xl max-h-[80vh] overflow-y-auto">
|
||||
<MediaBrowser />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div
|
||||
class="flex items-center justify-center w-full"
|
||||
@dragover.prevent="onDragOver"
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PaginationRoot, type PaginationRootEmits, type PaginationRootProps, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<PaginationRootProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
const emits = defineEmits<PaginationRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="pagination"
|
||||
v-bind="forwarded"
|
||||
:class="cn('mx-auto flex w-full justify-center', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PaginationRoot>
|
||||
</template>
|
||||
@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PaginationList, type PaginationListProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<PaginationListProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationList
|
||||
v-slot="slotProps"
|
||||
data-slot="pagination-content"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('flex flex-row items-center gap-1', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PaginationList>
|
||||
</template>
|
||||
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { MoreHorizontal } from 'lucide-vue-next'
|
||||
import { PaginationEllipsis, type PaginationEllipsisProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<PaginationEllipsisProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationEllipsis
|
||||
data-slot="pagination-ellipsis"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('flex size-9 items-center justify-center', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<MoreHorizontal class="size-4" />
|
||||
<span class="sr-only">More pages</span>
|
||||
</slot>
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationFirstProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronLeftIcon } from 'lucide-vue-next'
|
||||
import { PaginationFirst, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants, type ButtonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationFirstProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}>(), {
|
||||
size: 'default',
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size')
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationFirst
|
||||
data-slot="pagination-first"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<ChevronLeftIcon />
|
||||
<span class="hidden sm:block">First</span>
|
||||
</slot>
|
||||
</PaginationFirst>
|
||||
</template>
|
||||
@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PaginationListItem, type PaginationListItemProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants, type ButtonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationListItemProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
isActive?: boolean
|
||||
}>(), {
|
||||
size: 'icon',
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size', 'isActive')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationListItem
|
||||
data-slot="pagination-item"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
props.class)"
|
||||
>
|
||||
<slot />
|
||||
</PaginationListItem>
|
||||
</template>
|
||||
@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationLastProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronRightIcon } from 'lucide-vue-next'
|
||||
import { PaginationLast, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants, type ButtonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationLastProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}>(), {
|
||||
size: 'default',
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size')
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationLast
|
||||
data-slot="pagination-last"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<span class="hidden sm:block">Last</span>
|
||||
<ChevronRightIcon />
|
||||
</slot>
|
||||
</PaginationLast>
|
||||
</template>
|
||||
@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationNextProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronRightIcon } from 'lucide-vue-next'
|
||||
import { PaginationNext, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants, type ButtonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationNextProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}>(), {
|
||||
size: 'default',
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size')
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationNext
|
||||
data-slot="pagination-next"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<span class="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</slot>
|
||||
</PaginationNext>
|
||||
</template>
|
||||
@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationPrevProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronLeftIcon } from 'lucide-vue-next'
|
||||
import { PaginationPrev, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants, type ButtonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationPrevProps & {
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}>(), {
|
||||
size: 'default',
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size')
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationPrev
|
||||
data-slot="pagination-previous"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<ChevronLeftIcon />
|
||||
<span class="hidden sm:block">Previous</span>
|
||||
</slot>
|
||||
</PaginationPrev>
|
||||
</template>
|
||||
@ -0,0 +1,8 @@
|
||||
export { default as Pagination } from './Pagination.vue'
|
||||
export { default as PaginationContent } from './PaginationContent.vue'
|
||||
export { default as PaginationEllipsis } from './PaginationEllipsis.vue'
|
||||
export { default as PaginationFirst } from './PaginationFirst.vue'
|
||||
export { default as PaginationItem } from './PaginationItem.vue'
|
||||
export { default as PaginationLast } from './PaginationLast.vue'
|
||||
export { default as PaginationNext } from './PaginationNext.vue'
|
||||
export { default as PaginationPrevious } from './PaginationPrevious.vue'
|
||||
@ -124,7 +124,7 @@ export const uploadFile = async (file: File, folder: string, onProgress: (progre
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMediaDirectories = async () => {
|
||||
export const fetchMediaFolders = async () => {
|
||||
try {
|
||||
const response = await api.get('api/media/folder/all');
|
||||
return await response.json();
|
||||
@ -133,4 +133,15 @@ export const fetchMediaDirectories = async () => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMediaByFolder = async (folderId: string, page: number = 1) => {
|
||||
try {
|
||||
const response = await api.get(`api/media/folder/${folderId}/page/${page}/12`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Error fetching media for folder ${folderId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@ -33,7 +33,7 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq) => {
|
||||
proxyReq.setHeader('Authorization', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NTM4ODEwMjUsImV4cCI6MTc1Mzg4NDYyNSwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfU0hPUF9PUEVSQVRPUiIsIlJPTEVfVVNFUiIsIlJPTEVfVVNFUiIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9FZGl0IiwiUk9MRV9QU0NfQ29sbGVjdF9Db250YWN0X0FkZCIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9EZWxldGUiLCJST0xFX1BTQ19Db2xsZWN0X0NvbnRhY3RfTG9jayIsIlJPTEVfUFNDX1IyX1NlbmRjbG91ZF9TaG93Il0sInVpZCI6MX0.T0JzZ8qoZOqOOGtHXtuGvY5xAwwWGSAHh9MXgGJlyngjsmA_RWUnL8wVW3Ah827sJQNL2W1JPEz_f9CuCvfCSVjoLml9T_n5N5xtB98wVdcksgh3PvtFrYs-NVUat9QlwJ54F0cUXIkyuimEc2op0Y2sSC9Pyw6d9m8WtYrPX657uXZ3U8KcX8FIqCMWpjzIsbLK2QemT-fgkJpVnSdvo8nFvComZ1yDPvj5Zl_m0NEF3CybYdxOOwz42egI297BM_qGp6_cZLeSrl2EwphzeFqPo8-q9wmuHj33HZozlwjlu_Uvp4vq0Jr98-WZkIUwK7706E1t_TdSc46nONyxVA');
|
||||
proxyReq.setHeader('Authorization', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NTM5NTU4MzMsImV4cCI6MTc1Mzk1OTQzMywicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfU0hPUF9PUEVSQVRPUiIsIlJPTEVfVVNFUiIsIlJPTEVfVVNFUiIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9FZGl0IiwiUk9MRV9QU0NfQ29sbGVjdF9Db250YWN0X0FkZCIsIlJPTEVfUFNDX0NvbGxlY3RfQ29udGFjdF9EZWxldGUiLCJST0xFX1BTQ19Db2xsZWN0X0NvbnRhY3RfTG9jayIsIlJPTEVfUFNDX1IyX1NlbmRjbG91ZF9TaG93Il0sInVpZCI6MX0.rUlkpFBa-RKQAxSa6e0UmggiqPlDj-Mz4I56T-kTOT5j5veI3JFYHOvptbeEjITPdmHM_Dm86CpEuIkezQC1UBsuzGkSYLQRy8Cb_YnNEEbaoCsg28q60Wu_dfBbDEFRjXnbEVSrwbfawRMq4FUTuky7r8qstORRhL1bPtfL3cN_lPEruffSreuvUReM7inkjOHU4utGuVhMm1D3oK1p398YmZACmQfiYLYOM6c07Isop5IZ0Mfq0LZ_BEVptQi7Thb4nAHiEUZfyBqhLrrWxmUFcpF2Mp7-_dZZeiA9w2ZYZBA-sGMAF0EwCDU1_WK-o0BDZDk-qKMj4zrFHe8TEg');
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user