Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 788807becc | |||
| 4b37be3e7a | |||
| 26ed0c8aad | |||
| 41de70da75 | |||
| e802c8fc0f | |||
| 7d18bcab1a | |||
| 93aa1b0f24 | |||
| cbcf2e5acb | |||
| ce246669d9 | |||
| 6b040ba19e | |||
| 33a06c6509 | |||
| f80e3679b6 |
File diff suppressed because it is too large
Load Diff
@@ -50,11 +50,7 @@ class TransferController extends ControllerAbstract
|
|||||||
*
|
*
|
||||||
* GET /download/entity/{provider}/{service}/{collection}/{identifier}
|
* GET /download/entity/{provider}/{service}/{collection}/{identifier}
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute(
|
#[AuthenticatedRoute('/download/entity/{provider}/{service}/{collection}/{identifier}', name: 'document_manager.download.entity', methods: ['GET'])]
|
||||||
'/download/entity/{provider}/{service}/{collection}/{identifier}',
|
|
||||||
name: 'document_manager.download.entity',
|
|
||||||
methods: ['GET']
|
|
||||||
)]
|
|
||||||
public function downloadEntity(string $provider, string $service, string $collection, string $identifier): Response {
|
public function downloadEntity(string $provider, string $service, string $collection, string $identifier): Response {
|
||||||
$tenantId = $this->tenantContext->identifier();
|
$tenantId = $this->tenantContext->identifier();
|
||||||
$userId = $this->identityContext->identifier();
|
$userId = $this->identityContext->identifier();
|
||||||
@@ -66,14 +62,14 @@ class TransferController extends ControllerAbstract
|
|||||||
/** @var EntityBaseInterface[] $entities */
|
/** @var EntityBaseInterface[] $entities */
|
||||||
$entities = $this->manager->entityFetchBulk($tenantId, $userId, $target);
|
$entities = $this->manager->entityFetchBulk($tenantId, $userId, $target);
|
||||||
|
|
||||||
if (empty($entities) || !isset($entities[$identifier])) {
|
if (!isset($entities[(string) $target])) {
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'error' => ['code' => 404, 'message' => 'File not found']
|
'error' => ['code' => 404, 'message' => 'File not found']
|
||||||
], Response::HTTP_NOT_FOUND);
|
], Response::HTTP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
$entity = $entities[$identifier];
|
$entity = $entities[(string) $target];
|
||||||
|
|
||||||
// Get the stream
|
// Get the stream
|
||||||
$stream = $this->manager->entityReadStream($tenantId, $userId, $target);
|
$stream = $this->manager->entityReadStream($tenantId, $userId, $target);
|
||||||
@@ -124,16 +120,41 @@ class TransferController extends ControllerAbstract
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[AuthenticatedRoute('/preview/entity/{provider}/{service}/{collection}/{identifier}', name: 'document_manager.preview.entity', methods: ['GET'])]
|
||||||
|
public function previewEntity(string $provider, string $service, string $collection, string $identifier, string $variant = 'inline'): Response {
|
||||||
|
try {
|
||||||
|
$preview = $this->manager->entityPreview(
|
||||||
|
$this->tenantContext->identifier(),
|
||||||
|
$this->identityContext->identifier(),
|
||||||
|
new EntityIdentifier($provider, $service, $collection, $identifier),
|
||||||
|
$variant,
|
||||||
|
);
|
||||||
|
if ($preview === null) {
|
||||||
|
return new Response('', Response::HTTP_NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new StreamedResponse($preview->resource->stream());
|
||||||
|
$response->headers->set('Content-Type', $preview->resource->mimeType());
|
||||||
|
$response->headers->set('Content-Length', (string) $preview->size);
|
||||||
|
$response->headers->set('ETag', '"' . trim($preview->etag, '"') . '"');
|
||||||
|
$response->headers->set('Cache-Control', 'private, max-age=3600');
|
||||||
|
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||||
|
return $response;
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
$this->logger->error('Document preview failed', ['exception' => $throwable]);
|
||||||
|
return new JsonResponse([
|
||||||
|
'status' => 'error',
|
||||||
|
'error' => ['code' => 500, 'message' => 'Preview unavailable'],
|
||||||
|
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download multiple files as a ZIP archive
|
* Download multiple files as a ZIP archive
|
||||||
*
|
*
|
||||||
* GET /download/archive?provider=...&service=...&ids[]=...&ids[]=...
|
* GET /download/archive?provider=...&service=...&ids[]=...&ids[]=...
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute(
|
#[AuthenticatedRoute('/download/archive', name: 'manager.download.archive', methods: ['GET'])]
|
||||||
'/download/archive',
|
|
||||||
name: 'manager.download.archive',
|
|
||||||
methods: ['GET']
|
|
||||||
)]
|
|
||||||
public function downloadArchive(string $provider, string $service, array $ids = [], ?string $collection = null, string $name = 'download'): Response {
|
public function downloadArchive(string $provider, string $service, array $ids = [], ?string $collection = null, string $name = 'download'): Response {
|
||||||
$tenantId = $this->tenantContext->identifier();
|
$tenantId = $this->tenantContext->identifier();
|
||||||
$userId = $this->identityContext->identifier();
|
$userId = $this->identityContext->identifier();
|
||||||
@@ -215,11 +236,7 @@ class TransferController extends ControllerAbstract
|
|||||||
*
|
*
|
||||||
* GET /download/collection/{provider}/{service}/{identifier}
|
* GET /download/collection/{provider}/{service}/{identifier}
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute(
|
#[AuthenticatedRoute('/download/collection/{provider}/{service}/{identifier}', name: 'manager.download.collection', methods: ['GET'])]
|
||||||
'/download/collection/{provider}/{service}/{identifier}',
|
|
||||||
name: 'manager.download.collection',
|
|
||||||
methods: ['GET']
|
|
||||||
)]
|
|
||||||
public function downloadCollection(string $provider, string $service, string $identifier): Response {
|
public function downloadCollection(string $provider, string $service, string $identifier): Response {
|
||||||
$tenantId = $this->tenantContext->identifier();
|
$tenantId = $this->tenantContext->identifier();
|
||||||
$userId = $this->identityContext->identifier();
|
$userId = $this->identityContext->identifier();
|
||||||
@@ -310,15 +327,16 @@ class TransferController extends ControllerAbstract
|
|||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
// Try as entity first
|
// Try as entity first
|
||||||
if ($collection !== null) {
|
if ($collection !== null) {
|
||||||
|
$target = new EntityIdentifier($provider, $service, $collection, (string) $id);
|
||||||
/** @var EntityBaseInterface[] $entities */
|
/** @var EntityBaseInterface[] $entities */
|
||||||
$entities = $this->manager->entityFetchBulk(
|
$entities = $this->manager->entityFetchBulk(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
new EntityIdentifier($provider, $service, $collection, (string)$id)
|
$target
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!empty($entities) && isset($entities[$id])) {
|
if (isset($entities[(string) $target])) {
|
||||||
$entity = $entities[$id];
|
$entity = $entities[(string) $target];
|
||||||
$files[] = [
|
$files[] = [
|
||||||
'type' => 'file',
|
'type' => 'file',
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
|
|||||||
+161
-11
@@ -5,6 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace KTXM\DocumentsManager;
|
namespace KTXM\DocumentsManager;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use KTXC\Preview\Preview;
|
||||||
|
use KTXC\Preview\PreviewManager;
|
||||||
use KTXC\Resource\ProviderManager;
|
use KTXC\Resource\ProviderManager;
|
||||||
use KTXF\Documents\Collection\CollectionBaseInterface;
|
use KTXF\Documents\Collection\CollectionBaseInterface;
|
||||||
use KTXF\Documents\Collection\CollectionPropertiesMutableInterface;
|
use KTXF\Documents\Collection\CollectionPropertiesMutableInterface;
|
||||||
@@ -17,8 +20,12 @@ use KTXF\Documents\Service\ServiceBaseInterface;
|
|||||||
use KTXF\Documents\Service\ServiceCollectionMutableInterface;
|
use KTXF\Documents\Service\ServiceCollectionMutableInterface;
|
||||||
use KTXF\Documents\Service\ServiceEntityMutableInterface;
|
use KTXF\Documents\Service\ServiceEntityMutableInterface;
|
||||||
use KTXF\Documents\Service\ServiceMutableInterface;
|
use KTXF\Documents\Service\ServiceMutableInterface;
|
||||||
|
use KTXF\Documents\Service\ServiceNodeListInterface;
|
||||||
use KTXF\Resource\Filter\IFilter;
|
use KTXF\Resource\Filter\IFilter;
|
||||||
|
use KTXF\Preview\PreviewSource;
|
||||||
|
use KTXF\Resource\BinaryResource;
|
||||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||||
|
use KTXF\Resource\Identifier\ServiceIdentifier;
|
||||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||||
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
||||||
use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
|
use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
|
||||||
@@ -37,6 +44,7 @@ class Manager {
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private ProviderManager $providerManager,
|
private ProviderManager $providerManager,
|
||||||
|
private PreviewManager $previewManager,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -500,18 +508,16 @@ class Manager {
|
|||||||
*
|
*
|
||||||
* @param string $tenantId tenant identifier
|
* @param string $tenantId tenant identifier
|
||||||
* @param string $userId user identifier
|
* @param string $userId user identifier
|
||||||
* @param string $provider provider identifier
|
* @param ServiceIdentifier|CollectionIdentifier $target service root or parent collection
|
||||||
* @param string|int $service service identifier
|
|
||||||
* @param CollectionIdentifier|null $target target parent collection identifier (null for root)
|
|
||||||
* @param CollectionPropertiesMutableInterface|array $properties properties for the new collection
|
* @param CollectionPropertiesMutableInterface|array $properties properties for the new collection
|
||||||
* @param array $options additional options for creation
|
* @param array $options additional options for creation
|
||||||
*
|
*
|
||||||
* @return CollectionBaseInterface
|
* @return CollectionBaseInterface
|
||||||
* @throws InvalidArgumentException
|
* @throws InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
public function collectionCreate(string $tenantId, string $userId, string $provider, string|int $service, CollectionIdentifier|null $target, CollectionPropertiesMutableInterface|array $properties, array $options = []): CollectionBaseInterface {
|
public function collectionCreate(string $tenantId, string $userId, ServiceIdentifier|CollectionIdentifier $target, CollectionPropertiesMutableInterface|array $properties, array $options = []): CollectionBaseInterface {
|
||||||
// retrieve service
|
// retrieve service
|
||||||
$service = $this->serviceFetch($tenantId, $userId, $provider, $service);
|
$service = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
|
||||||
// Check if service supports collection creation
|
// Check if service supports collection creation
|
||||||
if ($service->getEnabled() === false) {
|
if ($service->getEnabled() === false) {
|
||||||
throw new InvalidArgumentException("Service '{$service->identifier()}' not found or is disabled");
|
throw new InvalidArgumentException("Service '{$service->identifier()}' not found or is disabled");
|
||||||
@@ -527,7 +533,7 @@ class Manager {
|
|||||||
$properties = $service->collectionFresh()->getProperties()->jsonDeserialize($properties);
|
$properties = $service->collectionFresh()->getProperties()->jsonDeserialize($properties);
|
||||||
}
|
}
|
||||||
// Create collection
|
// Create collection
|
||||||
return $service->collectionCreate($target, $properties, $options);
|
return $service->collectionCreate($target instanceof CollectionIdentifier ? $target : null, $properties, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -805,21 +811,28 @@ class Manager {
|
|||||||
* @return array<string,EntityBaseInterface>
|
* @return array<string,EntityBaseInterface>
|
||||||
*/
|
*/
|
||||||
public function entityFetchBulk(string $tenantId, ?string $userId, EntityIdentifier ...$identifiers): array {
|
public function entityFetchBulk(string $tenantId, ?string $userId, EntityIdentifier ...$identifiers): array {
|
||||||
// group identifiers by provider/service
|
// Keep locations separate: providers may return maps keyed by local ID.
|
||||||
$groupedIdentifiers = [];
|
$groupedIdentifiers = [];
|
||||||
foreach ($identifiers as $identifier) {
|
foreach ($identifiers as $identifier) {
|
||||||
$groupedIdentifiers[$identifier->provider()][$identifier->service()][] = $identifier;
|
$groupedIdentifiers[$identifier->provider()][$identifier->service()][$identifier->collection()][] = $identifier;
|
||||||
}
|
}
|
||||||
// retrieve each service and fetch entities
|
// retrieve each service and fetch entities
|
||||||
$list = [];
|
$list = [];
|
||||||
foreach ($groupedIdentifiers as $providerId => $services) {
|
foreach ($groupedIdentifiers as $providerId => $services) {
|
||||||
foreach ($services as $serviceId => $entities) {
|
foreach ($services as $serviceId => $collections) {
|
||||||
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId);
|
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId);
|
||||||
if ($service->getEnabled() === false) {
|
if ($service->getEnabled() === false) {
|
||||||
throw new InvalidArgumentException("Service '{$providerId}:{$serviceId}' not found or is disabled");
|
throw new InvalidArgumentException("Service '{$providerId}:{$serviceId}' not found or is disabled");
|
||||||
}
|
}
|
||||||
// retrieve entities and merge into list
|
foreach ($collections as $entities) {
|
||||||
$list = array_merge($list, $service->entityFetchBulk(...$entities));
|
$fetched = $service->entityFetchBulk(...$entities);
|
||||||
|
foreach ($entities as $identifier) {
|
||||||
|
$entity = $fetched[(string) $identifier] ?? $fetched[$identifier->entity()] ?? null;
|
||||||
|
if ($entity !== null) {
|
||||||
|
$list[(string) $identifier] = $entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $list;
|
return $list;
|
||||||
@@ -1342,4 +1355,141 @@ class Manager {
|
|||||||
return $this->serviceForWrite($tenantId, $userId, $target)->entityWriteChunk($target, $offset, $data);
|
return $this->serviceForWrite($tenantId, $userId, $target)->entityWriteChunk($target, $offset, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an authorized entity and lazily generate or retrieve its preview.
|
||||||
|
*/
|
||||||
|
public function entityPreview(string $tenantId, string $userId, EntityIdentifier $target, string $variant = 'inline'): ?Preview {
|
||||||
|
$entities = $this->entityFetchBulk($tenantId, $userId, $target);
|
||||||
|
$entity = $entities[(string) $target] ?? null;
|
||||||
|
if (!$entity instanceof EntityBaseInterface) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$properties = $entity->getProperties();
|
||||||
|
$mimeType = $properties->getMime();
|
||||||
|
$size = $properties->size();
|
||||||
|
$signature = $entity->signature() ?? hash('sha256', implode('|', [
|
||||||
|
(string) $target,
|
||||||
|
$entity->modified()?->format('U.u') ?? '',
|
||||||
|
(string) $size,
|
||||||
|
$mimeType,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$source = new PreviewSource(
|
||||||
|
sourceType: 'document',
|
||||||
|
identity: (string) $target,
|
||||||
|
signature: $signature,
|
||||||
|
mimeType: $mimeType,
|
||||||
|
size: $size,
|
||||||
|
resourceFactory: function () use ($tenantId, $userId, $target, $properties, $mimeType): BinaryResource {
|
||||||
|
$stream = $this->entityReadStream($tenantId, $userId, $target);
|
||||||
|
if (!is_resource($stream)) {
|
||||||
|
throw new RuntimeException('Document content is unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BinaryResource(
|
||||||
|
$properties->getLabel(),
|
||||||
|
$mimeType,
|
||||||
|
(static function () use ($stream): \Generator {
|
||||||
|
try {
|
||||||
|
while (!feof($stream)) {
|
||||||
|
$chunk = fread($stream, 65536);
|
||||||
|
if ($chunk === false) {
|
||||||
|
throw new RuntimeException('Failed to read document content');
|
||||||
|
}
|
||||||
|
if ($chunk !== '') {
|
||||||
|
yield $chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
fclose($stream);
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->previewManager->fetch($tenantId, $source, $variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List collections and entities within a location as one unified, paginated set
|
||||||
|
*
|
||||||
|
* Folders are ordered before files within the returned range. Unlike
|
||||||
|
* {@see collectionList} and {@see entityListBulk}, this requires the target
|
||||||
|
* service to natively implement {@see ServiceNodeListInterface} — there is
|
||||||
|
* intentionally no fallback for services that only support the separate
|
||||||
|
* collection/entity listing operations, since every current caller targets a
|
||||||
|
* single provider/service/location per request.
|
||||||
|
*
|
||||||
|
* @since 2026.09.01
|
||||||
|
*
|
||||||
|
* @param string $tenantId Tenant identifier
|
||||||
|
* @param string $userId User identifier
|
||||||
|
* @param ResourceIdentifiers|null $targets Node sources with collection identifiers
|
||||||
|
* @param array|null $filter Node filter
|
||||||
|
* @param array|null $sort Node sort
|
||||||
|
* @param array|null $range Node range/pagination
|
||||||
|
*
|
||||||
|
* @return \Generator<int,CollectionBaseInterface|EntityBaseInterface> Nodes in provider order
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException If the resolved service does not support unified node listing
|
||||||
|
*/
|
||||||
|
public function nodeList(string $tenantId, string $userId, ?ResourceIdentifiers $targets = null, array|null $filter = null, array|null $sort = null, array|null $range = null): \Generator {
|
||||||
|
// confirm that sources are provided
|
||||||
|
if ($targets === null) {
|
||||||
|
$targets = new ResourceIdentifiers([]);
|
||||||
|
}
|
||||||
|
// retrieve services for each provider
|
||||||
|
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
|
||||||
|
// retrieve nodes for each service
|
||||||
|
foreach ($aggregateServices as $services) {
|
||||||
|
/** @var ServiceBaseInterface $service */
|
||||||
|
foreach ($services as $service) {
|
||||||
|
// omit disabled services
|
||||||
|
if ($service->getEnabled() === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($service instanceof ServiceNodeListInterface === false) {
|
||||||
|
throw new InvalidArgumentException("Service '{$service->identifier()}' does not support paginated node listing");
|
||||||
|
}
|
||||||
|
// retrieve collections for each service
|
||||||
|
$collectionSelected = $targets->byProvider($service->provider())->byService($service->identifier())->collections();
|
||||||
|
if ($collectionSelected === []) {
|
||||||
|
// documents are hierarchical: service level selection lists the root collection
|
||||||
|
$collectionSelected = [''];
|
||||||
|
}
|
||||||
|
// construct filter for nodes
|
||||||
|
$nodeFilter = null;
|
||||||
|
if ($filter !== null && $filter !== []) {
|
||||||
|
$nodeFilter = $service->nodeListFilter();
|
||||||
|
foreach ($filter as $attribute => $value) {
|
||||||
|
$nodeFilter->condition($attribute, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// construct sort for nodes
|
||||||
|
$nodeSort = null;
|
||||||
|
if ($sort !== null && $sort !== []) {
|
||||||
|
$nodeSort = $service->nodeListSort();
|
||||||
|
foreach ($sort as $attribute => $direction) {
|
||||||
|
$nodeSort->condition($attribute, $direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// construct range for nodes
|
||||||
|
$nodeRange = null;
|
||||||
|
if ($range !== null && $range !== [] && isset($range['type'])) {
|
||||||
|
$nodeRange = $service->nodeListRange(RangeType::from($range['type']))->jsonDeserialize($range);
|
||||||
|
}
|
||||||
|
// Preserve provider order without grouping or buffering nodes.
|
||||||
|
foreach ($collectionSelected as $collectionId) {
|
||||||
|
$nodes = $service->nodeList($collectionId, $nodeFilter, $nodeSort, $nodeRange);
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
yield $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-12
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
namespace KTXM\DocumentsManager;
|
namespace KTXM\DocumentsManager;
|
||||||
|
|
||||||
use KTXF\Module\ModuleBrowserInterface;
|
use KTXF\Module\Configuration\BrowserModuleContextInterface;
|
||||||
|
use KTXF\Module\Configuration\ModuleContextInterface;
|
||||||
use KTXF\Module\ModuleInstanceAbstract;
|
use KTXF\Module\ModuleInstanceAbstract;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File Manager Module
|
* File Manager Module
|
||||||
*/
|
*/
|
||||||
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
class Module extends ModuleInstanceAbstract
|
||||||
{
|
{
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
@@ -50,16 +51,11 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function registerBI(): array {
|
public function configure(ModuleContextInterface $context): void
|
||||||
return [
|
{
|
||||||
'handle' => $this->handle(),
|
if ($context instanceof BrowserModuleContextInterface) {
|
||||||
'namespace' => 'DocumentsManager',
|
$context->registerModule($this, 'DocumentsManager', 'static/module.mjs');
|
||||||
'version' => $this->version(),
|
}
|
||||||
'label' => $this->label(),
|
|
||||||
'author' => $this->author(),
|
|
||||||
'description' => $this->description(),
|
|
||||||
'boot' => 'static/module.mjs',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+358
-129
@@ -9,17 +9,20 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^4.0.0",
|
"pinia": "^4.0.0",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13",
|
||||||
|
"vuetify": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node24": "^24.0.0",
|
"@tsconfig/node24": "^24.0.0",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.1.6",
|
"@vitest/coverage-v8": "^4.1.6",
|
||||||
"@vue/tsconfig": "^0.9.0",
|
"@vue/tsconfig": "^0.9.0",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"typescript": "~6.0.0",
|
"typescript": "~6.0.0",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"vitest": "^4.1.6"
|
"vitest": "^4.1.6",
|
||||||
|
"vue-tsc": "^3.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@asamuzakjp/css-color": {
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
@@ -282,6 +285,40 @@
|
|||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@exodus/bytes": {
|
"node_modules/@exodus/bytes": {
|
||||||
"version": "1.15.1",
|
"version": "1.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
||||||
@@ -327,10 +364,29 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@tybys/wasm-util": "^0.10.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.143.0",
|
"version": "0.139.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
||||||
"integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==",
|
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -338,9 +394,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
||||||
"integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==",
|
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -355,9 +411,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
||||||
"integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==",
|
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -372,9 +428,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
||||||
"integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==",
|
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -389,9 +445,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
||||||
"integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==",
|
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -406,9 +462,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
||||||
"integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==",
|
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -423,9 +479,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==",
|
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -440,9 +496,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
||||||
"integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==",
|
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -457,9 +513,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==",
|
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -474,9 +530,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==",
|
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -491,9 +547,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
||||||
"integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==",
|
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -508,9 +564,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
||||||
"integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==",
|
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -525,9 +581,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
||||||
"integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==",
|
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -541,10 +597,29 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "1.11.1",
|
||||||
|
"@emnapi/runtime": "1.11.1",
|
||||||
|
"@napi-rs/wasm-runtime": "^1.1.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
||||||
"integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==",
|
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -559,9 +634,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
||||||
"integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==",
|
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -596,6 +671,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||||
|
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
@@ -631,6 +717,23 @@
|
|||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
|
"version": "6.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
|
||||||
|
"integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rolldown/pluginutils": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
|
"vue": "^3.2.25"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/coverage-v8": {
|
"node_modules/@vitest/coverage-v8": {
|
||||||
"version": "4.1.10",
|
"version": "4.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
|
||||||
@@ -785,6 +888,35 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@volar/language-core": {
|
||||||
|
"version": "2.4.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
|
||||||
|
"integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/source-map": "2.4.28"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@volar/source-map": {
|
||||||
|
"version": "2.4.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
|
||||||
|
"integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@volar/typescript": {
|
||||||
|
"version": "2.4.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
|
||||||
|
"integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/language-core": "2.4.28",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"vscode-uri": "^3.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.40",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz",
|
||||||
@@ -865,6 +997,22 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/language-core": {
|
||||||
|
"version": "3.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.9.tgz",
|
||||||
|
"integrity": "sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/language-core": "2.4.28",
|
||||||
|
"@vue/compiler-dom": "^3.5.0",
|
||||||
|
"@vue/shared": "^3.5.0",
|
||||||
|
"alien-signals": "^3.2.1",
|
||||||
|
"muggle-string": "^0.4.1",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"picomatch": "^4.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.40",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz",
|
||||||
@@ -932,6 +1080,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/alien-signals": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@@ -1252,9 +1407,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1268,23 +1423,23 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"lightningcss-android-arm64": "1.33.0",
|
"lightningcss-android-arm64": "1.32.0",
|
||||||
"lightningcss-darwin-arm64": "1.33.0",
|
"lightningcss-darwin-arm64": "1.32.0",
|
||||||
"lightningcss-darwin-x64": "1.33.0",
|
"lightningcss-darwin-x64": "1.32.0",
|
||||||
"lightningcss-freebsd-x64": "1.33.0",
|
"lightningcss-freebsd-x64": "1.32.0",
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.33.0",
|
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||||
"lightningcss-linux-arm64-gnu": "1.33.0",
|
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||||
"lightningcss-linux-arm64-musl": "1.33.0",
|
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||||
"lightningcss-linux-x64-gnu": "1.33.0",
|
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||||
"lightningcss-linux-x64-musl": "1.33.0",
|
"lightningcss-linux-x64-musl": "1.32.0",
|
||||||
"lightningcss-win32-arm64-msvc": "1.33.0",
|
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||||
"lightningcss-win32-x64-msvc": "1.33.0"
|
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-android-arm64": {
|
"node_modules/lightningcss-android-arm64": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||||
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
|
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1303,9 +1458,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||||
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
|
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1324,9 +1479,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||||
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
|
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1345,9 +1500,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-freebsd-x64": {
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||||
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
|
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1366,9 +1521,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||||
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
|
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1387,9 +1542,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||||
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
|
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1408,9 +1563,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||||
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
|
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1429,9 +1584,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||||
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
|
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1450,9 +1605,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||||
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
|
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1471,9 +1626,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||||
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
|
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1492,9 +1647,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
"version": "1.33.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||||
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
|
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1566,10 +1721,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/muggle-string": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.18",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1630,6 +1792,13 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-browserify": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
@@ -1664,9 +1833,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pinia": {
|
"node_modules/pinia": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.3.tgz",
|
||||||
"integrity": "sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==",
|
"integrity": "sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nostics": "^1.1.4"
|
"nostics": "^1.1.4"
|
||||||
@@ -1689,9 +1858,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.26",
|
"version": "8.5.19",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
||||||
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
"integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1708,7 +1877,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.17",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -1737,13 +1906,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.2.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||||
"integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==",
|
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.143.0",
|
"@oxc-project/types": "=0.139.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1753,20 +1922,21 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.2.3",
|
"@rolldown/binding-android-arm64": "1.1.5",
|
||||||
"@rolldown/binding-darwin-arm64": "1.2.3",
|
"@rolldown/binding-darwin-arm64": "1.1.5",
|
||||||
"@rolldown/binding-darwin-x64": "1.2.3",
|
"@rolldown/binding-darwin-x64": "1.1.5",
|
||||||
"@rolldown/binding-freebsd-x64": "1.2.3",
|
"@rolldown/binding-freebsd-x64": "1.1.5",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.3",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.2.3",
|
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.2.3",
|
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.3",
|
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.2.3",
|
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.2.3",
|
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.2.3",
|
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.2.3",
|
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.2.3",
|
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.2.3"
|
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
||||||
|
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/saxes": {
|
"node_modules/saxes": {
|
||||||
@@ -1935,6 +2105,14 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
@@ -1967,16 +2145,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.2.1",
|
"version": "8.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
||||||
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
|
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.33.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.5",
|
"picomatch": "^4.0.5",
|
||||||
"postcss": "^8.5.25",
|
"postcss": "^8.5.17",
|
||||||
"rolldown": "~1.2.1",
|
"rolldown": "~1.1.5",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1993,7 +2171,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
"@vitejs/devtools": "^0.4.0",
|
"@vitejs/devtools": "^0.3.0",
|
||||||
"esbuild": "^0.27.0 || ^0.28.0",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
@@ -2134,6 +2312,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vscode-uri": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.40",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz",
|
||||||
@@ -2155,6 +2340,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-tsc": {
|
||||||
|
"version": "3.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.9.tgz",
|
||||||
|
"integrity": "sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/typescript": "2.4.28",
|
||||||
|
"@vue/language-core": "3.3.9"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vue-tsc": "bin/vue-tsc.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vuetify": {
|
||||||
|
"version": "4.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.9.tgz",
|
||||||
|
"integrity": "sha512-2NlpUiSwkUAGxKmkKwoRCst3IJF6EPP7msNJd+VxFjcAN9xCG+RFODzsfXfO2ypl9aSj3T/PJ65BBjQeTGgFKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/johnleider"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.7",
|
||||||
|
"vite-plugin-vuetify": ">=2.1.0",
|
||||||
|
"vue": "^3.5.0 || ^3.6.0-0",
|
||||||
|
"webpack-plugin-vuetify": ">=3.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/w3c-xmlserializer": {
|
"node_modules/w3c-xmlserializer": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
|||||||
+4
-1
@@ -15,14 +15,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^4.0.0",
|
"pinia": "^4.0.0",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13",
|
||||||
|
"vuetify": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node24": "^24.0.0",
|
"@tsconfig/node24": "^24.0.0",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/tsconfig": "^0.9.0",
|
"@vue/tsconfig": "^0.9.0",
|
||||||
"typescript": "~6.0.0",
|
"typescript": "~6.0.0",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
|
"vue-tsc": "^3.0.5",
|
||||||
"@vitest/coverage-v8": "^4.1.6",
|
"@vitest/coverage-v8": "^4.1.6",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"vitest": "^4.1.6"
|
"vitest": "^4.1.6"
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useDocumentManager, useDocumentSelection } from '@/composables'
|
||||||
|
import { CollectionObject } from '@/models/collection'
|
||||||
|
import { EntityObject } from '@/models/entity'
|
||||||
|
import { formatSize, getFileIcon } from '@/utils/fileHelpers'
|
||||||
|
|
||||||
|
export interface DocumentSelection {
|
||||||
|
kind: 'entity' | 'collection'
|
||||||
|
identifier: string
|
||||||
|
name: string
|
||||||
|
mimeType?: string
|
||||||
|
size?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROVIDER_ID = 'default'
|
||||||
|
const SERVICE_ID = 'personal'
|
||||||
|
const LARGE_FILE_WARNING_BYTES = 25 * 1024 * 1024
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
selectable?: ('entity' | 'collection')[]
|
||||||
|
}>(), {
|
||||||
|
selectable: () => ['entity'],
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
select: [items: DocumentSelection[]]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const allowEntities = computed(() => props.selectable.includes('entity'))
|
||||||
|
const allowCollections = computed(() => props.selectable.includes('collection'))
|
||||||
|
|
||||||
|
const documentManager = useDocumentManager({
|
||||||
|
providerId: PROVIDER_ID,
|
||||||
|
serviceId: SERVICE_ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
const selection = useDocumentSelection({
|
||||||
|
multiple: true,
|
||||||
|
allowFolders: allowCollections.value,
|
||||||
|
allowFiles: allowEntities.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialized = ref(false)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, async (isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.clear()
|
||||||
|
|
||||||
|
if (!initialized.value) {
|
||||||
|
await documentManager.initialize()
|
||||||
|
initialized.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
await documentManager.navigateToRoot()
|
||||||
|
})
|
||||||
|
|
||||||
|
function nodeId(node: CollectionObject | EntityObject): string {
|
||||||
|
return String(node.identifier ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeLabel(node: CollectionObject | EntityObject): string {
|
||||||
|
return node.properties.label || nodeId(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => [
|
||||||
|
{ id: documentManager.ROOT_ID, label: 'Home' },
|
||||||
|
...documentManager.breadcrumbs.value.map(node => ({ id: nodeId(node), label: nodeLabel(node) })),
|
||||||
|
])
|
||||||
|
|
||||||
|
async function navigateToBreadcrumb(id: string) {
|
||||||
|
selection.clear()
|
||||||
|
await documentManager.navigateTo(id === documentManager.ROOT_ID ? null : id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openFolder(folder: CollectionObject) {
|
||||||
|
selection.clear()
|
||||||
|
await documentManager.navigateTo(nodeId(folder))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFolderSelection(folder: CollectionObject, event: Event) {
|
||||||
|
event.stopPropagation()
|
||||||
|
selection.toggle(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEntity(entity: EntityObject) {
|
||||||
|
selection.toggle(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLargeSelection = computed(() =>
|
||||||
|
selection.selectedEntities.value.some(entity => entity.properties.size > LARGE_FILE_WARNING_BYTES),
|
||||||
|
)
|
||||||
|
|
||||||
|
function entityIdentifier(entity: EntityObject): string {
|
||||||
|
return `${entity.provider}:${entity.service}:${entity.collection}:${entity.identifier}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectionIdentifier(collection: CollectionObject): string {
|
||||||
|
return `${collection.provider}:${collection.service}:${collection.identifier}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSelection(node: CollectionObject | EntityObject): DocumentSelection {
|
||||||
|
if (node instanceof EntityObject) {
|
||||||
|
return {
|
||||||
|
kind: 'entity',
|
||||||
|
identifier: entityIdentifier(node),
|
||||||
|
name: nodeLabel(node),
|
||||||
|
mimeType: node.properties.mime || 'application/octet-stream',
|
||||||
|
size: node.properties.size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'collection',
|
||||||
|
identifier: collectionIdentifier(node),
|
||||||
|
name: nodeLabel(node),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
const items = selection.selectedNodeArray.value.map(toSelection)
|
||||||
|
if (items.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('select', items)
|
||||||
|
selection.clear()
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" max-width="560">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2">mdi-folder-search-outline</v-icon>
|
||||||
|
Select Documents
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-breadcrumbs :items="breadcrumbs" density="compact" class="px-0 mb-2">
|
||||||
|
<template #item="{ item }">
|
||||||
|
<v-breadcrumbs-item :disabled="item.id === breadcrumbs[breadcrumbs.length - 1]?.id" @click="navigateToBreadcrumb(item.id)">
|
||||||
|
{{ item.label }}
|
||||||
|
</v-breadcrumbs-item>
|
||||||
|
</template>
|
||||||
|
</v-breadcrumbs>
|
||||||
|
|
||||||
|
<v-progress-linear v-if="documentManager.isLoading.value" indeterminate color="primary" class="mb-2" />
|
||||||
|
|
||||||
|
<div class="selector-list">
|
||||||
|
<v-list v-if="documentManager.currentCollections.value.length > 0 || documentManager.currentEntities.value.length > 0" density="compact">
|
||||||
|
<v-list-item
|
||||||
|
v-for="folder in documentManager.currentCollections.value"
|
||||||
|
:key="nodeId(folder)"
|
||||||
|
:title="nodeLabel(folder)"
|
||||||
|
@click="openFolder(folder)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-checkbox-btn
|
||||||
|
v-if="allowCollections"
|
||||||
|
:model-value="selection.isSelected(nodeId(folder))"
|
||||||
|
class="mr-2"
|
||||||
|
@click="toggleFolderSelection(folder, $event)"
|
||||||
|
/>
|
||||||
|
<v-icon>mdi-folder</v-icon>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<v-icon size="small">mdi-chevron-right</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-for="entity in documentManager.currentEntities.value"
|
||||||
|
:key="nodeId(entity)"
|
||||||
|
:title="nodeLabel(entity)"
|
||||||
|
:subtitle="formatSize(entity.properties.size)"
|
||||||
|
:disabled="!allowEntities"
|
||||||
|
@click="allowEntities && toggleEntity(entity)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-checkbox-btn v-if="allowEntities" :model-value="selection.isSelected(nodeId(entity))" class="mr-2" />
|
||||||
|
<v-icon>{{ getFileIcon(entity) }}</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<div v-else-if="!documentManager.isLoading.value" class="text-center py-8 text-medium-emphasis">
|
||||||
|
<v-icon size="40" class="mb-2">mdi-folder-open-outline</v-icon>
|
||||||
|
<div>This folder is empty</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-alert v-if="hasLargeSelection" type="warning" variant="tonal" density="compact" class="mt-3">
|
||||||
|
One or more selected files are larger than 25 MB and may take a moment to process.
|
||||||
|
</v-alert>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<span class="text-caption text-medium-emphasis ml-2">
|
||||||
|
{{ selection.count.value }} selected
|
||||||
|
</span>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="handleClose">Cancel</v-btn>
|
||||||
|
<v-btn color="primary" variant="elevated" :disabled="!selection.hasSelection.value" @click="handleConfirm">
|
||||||
|
Select
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.selector-list {
|
||||||
|
max-height: 360px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid rgb(var(--v-border-color));
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Central export point for Documents Manager composables
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { useDocumentManager } from './useDocumentManager'
|
||||||
|
export { useDocumentSelection } from './useDocumentSelection'
|
||||||
|
|
||||||
|
export type { UseDocumentManagerOptions } from './useDocumentManager'
|
||||||
|
export type { UseDocumentSelectionOptions } from './useDocumentSelection'
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
/**
|
||||||
|
* File Manager composable for convenient file/folder operations
|
||||||
|
* Provides reactive access to file manager state and actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, ref, unref } from 'vue'
|
||||||
|
import type { Ref, ComputedRef } from 'vue'
|
||||||
|
import { useProvidersStore } from '@/stores/providersStore'
|
||||||
|
import { useServicesStore } from '@/stores/servicesStore'
|
||||||
|
import { useNodesStore, ROOT_ID } from '@/stores/nodesStore'
|
||||||
|
import type { ListFilter, ListSort, ListRange, ServiceIdentifier, CollectionIdentifier } from '@/types/common'
|
||||||
|
import type { DocumentInterface } from '@/types/document'
|
||||||
|
import { CollectionObject } from '@/models/collection'
|
||||||
|
import { EntityObject } from '@/models/entity'
|
||||||
|
|
||||||
|
// Base URL for file manager transfer endpoints
|
||||||
|
const TRANSFER_BASE_URL = '/m/documents_manager'
|
||||||
|
|
||||||
|
// Number of collections+entities requested per page
|
||||||
|
const DEFAULT_PAGE_TALLY = 64
|
||||||
|
|
||||||
|
export interface UseDocumentManagerOptions {
|
||||||
|
providerId: string | Ref<string> | ComputedRef<string>
|
||||||
|
serviceId: string | Ref<string> | ComputedRef<string>
|
||||||
|
autoFetch?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||||
|
const providersStore = useProvidersStore()
|
||||||
|
const servicesStore = useServicesStore()
|
||||||
|
const nodesStore = useNodesStore()
|
||||||
|
|
||||||
|
const { providerId, serviceId, autoFetch = false } = options
|
||||||
|
|
||||||
|
const currentProviderId = () => unref(providerId)
|
||||||
|
const currentServiceId = () => unref(serviceId)
|
||||||
|
|
||||||
|
const currentSources = (): (ServiceIdentifier | CollectionIdentifier)[] => [
|
||||||
|
currentLocation.value === ROOT_ID
|
||||||
|
? `${currentProviderId()}:${currentServiceId()}`
|
||||||
|
: `${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
||||||
|
]
|
||||||
|
|
||||||
|
// Current location (folder being viewed)
|
||||||
|
const currentLocation: Ref<string> = ref(ROOT_ID)
|
||||||
|
|
||||||
|
// Loading/error state
|
||||||
|
const isLoading = computed(() => nodesStore.transceiving)
|
||||||
|
const error = computed(() => nodesStore.error)
|
||||||
|
|
||||||
|
// Pagination state for the current location + filter + sort
|
||||||
|
const currentFilter: Ref<ListFilter | undefined> = ref(undefined)
|
||||||
|
const currentSort: Ref<ListSort | undefined> = ref(undefined)
|
||||||
|
const loadedCount = ref(0)
|
||||||
|
const hasMore = ref(false)
|
||||||
|
const isLoadingMore = ref(false)
|
||||||
|
|
||||||
|
// Provider and service
|
||||||
|
const provider = computed(() => providersStore.provider(currentProviderId()))
|
||||||
|
const service = computed(() => servicesStore.service(currentProviderId(), currentServiceId()))
|
||||||
|
const rootId = computed(() => ROOT_ID)
|
||||||
|
|
||||||
|
// Current children
|
||||||
|
const currentChildren = computed(() =>
|
||||||
|
nodesStore.getChildren(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentCollections: ComputedRef<CollectionObject[]> = computed(() =>
|
||||||
|
nodesStore.getChildCollections(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentEntities: ComputedRef<EntityObject[]> = computed(() =>
|
||||||
|
nodesStore.getChildEntities(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Breadcrumb path
|
||||||
|
const breadcrumbs = computed(() => {
|
||||||
|
if (currentLocation.value === ROOT_ID) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return nodesStore.getPath(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Is at root?
|
||||||
|
const isAtRoot = computed(() => currentLocation.value === ROOT_ID)
|
||||||
|
|
||||||
|
// Navigate to a folder
|
||||||
|
const navigateTo = async (collectionId: string | null) => {
|
||||||
|
currentLocation.value = collectionId || ROOT_ID
|
||||||
|
await resetAndLoad(currentFilter.value, currentSort.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate up one level
|
||||||
|
const navigateUp = async () => {
|
||||||
|
if (currentLocation.value === ROOT_ID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentNode = nodesStore.getNode(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
if (currentNode) {
|
||||||
|
await navigateTo(currentNode.collection ? String(currentNode.collection) : ROOT_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to root
|
||||||
|
const navigateToRoot = async () => {
|
||||||
|
await navigateTo(ROOT_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the current location's loaded items and (re)fetch the first page,
|
||||||
|
// optionally with a new filter/sort (e.g. search query or sort field change)
|
||||||
|
const resetAndLoad = async (filter?: ListFilter, sort?: ListSort) => {
|
||||||
|
currentFilter.value = filter
|
||||||
|
currentSort.value = sort
|
||||||
|
loadedCount.value = 0
|
||||||
|
hasMore.value = false
|
||||||
|
|
||||||
|
const range: ListRange = { type: 'tally', anchor: 'absolute', position: 0, tally: DEFAULT_PAGE_TALLY }
|
||||||
|
const result = await nodesStore.fetchNodes(
|
||||||
|
currentSources(),
|
||||||
|
filter,
|
||||||
|
sort,
|
||||||
|
range,
|
||||||
|
'reset'
|
||||||
|
)
|
||||||
|
|
||||||
|
loadedCount.value = result.collections.length + result.entities.length
|
||||||
|
hasMore.value = !result.isLastPage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the next page of the current location (infinite scroll "load more")
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (!hasMore.value || isLoadingMore.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingMore.value = true
|
||||||
|
try {
|
||||||
|
const range: ListRange = { type: 'tally', anchor: 'absolute', position: loadedCount.value, tally: DEFAULT_PAGE_TALLY }
|
||||||
|
const result = await nodesStore.fetchNodes(
|
||||||
|
currentSources(),
|
||||||
|
currentFilter.value,
|
||||||
|
currentSort.value,
|
||||||
|
range,
|
||||||
|
'append'
|
||||||
|
)
|
||||||
|
|
||||||
|
loadedCount.value += result.collections.length + result.entities.length
|
||||||
|
hasMore.value = !result.isLastPage
|
||||||
|
} finally {
|
||||||
|
isLoadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new folder
|
||||||
|
const createFolder = async (label: string): Promise<CollectionObject> => {
|
||||||
|
return await nodesStore.createCollection(
|
||||||
|
`${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
||||||
|
{ label, owner: '' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new file
|
||||||
|
const createFile = async (
|
||||||
|
label: string,
|
||||||
|
mime: string = 'application/octet-stream'
|
||||||
|
): Promise<EntityObject> => {
|
||||||
|
const properties: DocumentInterface = {
|
||||||
|
'@type': 'documents.properties',
|
||||||
|
urid: null,
|
||||||
|
size: 0,
|
||||||
|
label,
|
||||||
|
mime,
|
||||||
|
format: null,
|
||||||
|
encoding: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return await nodesStore.createEntity(
|
||||||
|
`${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
||||||
|
properties
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename a node
|
||||||
|
const renameNode = async (nodeId: string, newLabel: string) => {
|
||||||
|
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), nodeId)
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Node not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof CollectionObject) {
|
||||||
|
return await nodesStore.updateCollection(`${node.provider}:${node.service}:${nodeId}`, {
|
||||||
|
label: newLabel,
|
||||||
|
owner: node.properties.owner,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const properties: DocumentInterface = {
|
||||||
|
'@type': 'documents.properties',
|
||||||
|
urid: node.properties.urid,
|
||||||
|
size: node.properties.size,
|
||||||
|
label: newLabel,
|
||||||
|
mime: node.properties.mime,
|
||||||
|
format: node.properties.format,
|
||||||
|
encoding: node.properties.encoding,
|
||||||
|
}
|
||||||
|
return await nodesStore.updateEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${nodeId}`, properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a node
|
||||||
|
const deleteNode = async (nodeId: string): Promise<boolean> => {
|
||||||
|
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), nodeId)
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Node not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof CollectionObject) {
|
||||||
|
return await nodesStore.deleteCollection(`${node.provider}:${node.service}:${nodeId}`)
|
||||||
|
} else {
|
||||||
|
return await nodesStore.deleteEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${nodeId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
const readFile = async (entityId: string): Promise<string | null> => {
|
||||||
|
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), entityId)
|
||||||
|
if (!node || !(node instanceof EntityObject)) {
|
||||||
|
throw new Error('Entity not found')
|
||||||
|
}
|
||||||
|
return await nodesStore.readEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${entityId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file content
|
||||||
|
const writeFile = async (entityId: string, content: string): Promise<number> => {
|
||||||
|
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), entityId)
|
||||||
|
if (!node || !(node instanceof EntityObject)) {
|
||||||
|
throw new Error('Entity not found')
|
||||||
|
}
|
||||||
|
return await nodesStore.writeEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${entityId}`, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a URL suitable for inline viewing (img src / video src)
|
||||||
|
const getEntityUrl = (entityId: string, collectionId?: string | null): string => {
|
||||||
|
const collection = collectionId ?? currentLocation.value
|
||||||
|
return `${TRANSFER_BASE_URL}/download/entity/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEntityPreviewUrl = (
|
||||||
|
entityId: string,
|
||||||
|
collectionId?: string | null,
|
||||||
|
variant: 'thumbnail' | 'inline' | 'fullscreen' = 'inline',
|
||||||
|
): string => {
|
||||||
|
const collection = collectionId ?? currentLocation.value
|
||||||
|
return `${TRANSFER_BASE_URL}/preview/entity/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}?variant=${variant}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download a single file
|
||||||
|
const downloadEntity = (entityId: string, collectionId?: string | null): void => {
|
||||||
|
const collection = collectionId ?? currentLocation.value
|
||||||
|
// Use path parameters: /download/entity/{provider}/{service}/{collection}/{identifier}
|
||||||
|
const url = `${TRANSFER_BASE_URL}/download/entity/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}`
|
||||||
|
|
||||||
|
// Trigger download by opening URL (browser handles it)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadCollection = (collectionId: string): void => {
|
||||||
|
// Use path parameters: /download/collection/{provider}/{service}/{identifier}
|
||||||
|
const url = `${TRANSFER_BASE_URL}/download/collection/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collectionId)}`
|
||||||
|
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadArchive = (ids: string[], name: string = 'download', collectionId?: string | null): void => {
|
||||||
|
const collection = collectionId ?? currentLocation.value
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
provider: currentProviderId(),
|
||||||
|
service: currentServiceId(),
|
||||||
|
})
|
||||||
|
ids.forEach(id => params.append('ids[]', id))
|
||||||
|
if (name) {
|
||||||
|
params.append('name', name)
|
||||||
|
}
|
||||||
|
if (collection && collection !== ROOT_ID) {
|
||||||
|
params.append('collection', collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${TRANSFER_BASE_URL}/download/archive?${params.toString()}`
|
||||||
|
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize - fetch providers, services, and initial nodes if autoFetch
|
||||||
|
const initialize = async () => {
|
||||||
|
await providersStore.list([currentProviderId()])
|
||||||
|
await servicesStore.list()
|
||||||
|
if (autoFetch) {
|
||||||
|
await resetAndLoad()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
currentLocation,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
hasMore,
|
||||||
|
isLoadingMore,
|
||||||
|
|
||||||
|
// Provider/Service
|
||||||
|
provider,
|
||||||
|
service,
|
||||||
|
rootId,
|
||||||
|
|
||||||
|
// Current view
|
||||||
|
currentChildren,
|
||||||
|
currentCollections,
|
||||||
|
currentEntities,
|
||||||
|
breadcrumbs,
|
||||||
|
isAtRoot,
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
navigateTo,
|
||||||
|
navigateUp,
|
||||||
|
navigateToRoot,
|
||||||
|
resetAndLoad,
|
||||||
|
loadMore,
|
||||||
|
|
||||||
|
// Operations
|
||||||
|
createFolder,
|
||||||
|
createFile,
|
||||||
|
renameNode,
|
||||||
|
deleteNode,
|
||||||
|
readFile,
|
||||||
|
writeFile,
|
||||||
|
getEntityUrl,
|
||||||
|
getEntityPreviewUrl,
|
||||||
|
downloadEntity,
|
||||||
|
downloadCollection,
|
||||||
|
downloadArchive,
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
initialize,
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
ROOT_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDocumentManager
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* File selection composable
|
||||||
|
* Provides reactive selection management for file manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { Ref, ComputedRef } from 'vue'
|
||||||
|
import { CollectionObject } from '@/models/collection'
|
||||||
|
import { EntityObject } from '@/models/entity'
|
||||||
|
|
||||||
|
type NodeRecord = CollectionObject | EntityObject
|
||||||
|
|
||||||
|
export interface UseDocumentSelectionOptions {
|
||||||
|
multiple?: boolean
|
||||||
|
allowFolders?: boolean
|
||||||
|
allowFiles?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDocumentSelection(options: UseDocumentSelectionOptions = {}) {
|
||||||
|
const {
|
||||||
|
multiple = true,
|
||||||
|
allowFolders = true,
|
||||||
|
allowFiles = true,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const selectedIds: Ref<Set<string>> = ref(new Set())
|
||||||
|
const selectedNodes: Ref<Map<string, NodeRecord>> = ref(new Map())
|
||||||
|
|
||||||
|
const count: ComputedRef<number> = computed(() => selectedIds.value.size)
|
||||||
|
const hasSelection: ComputedRef<boolean> = computed(() => selectedIds.value.size > 0)
|
||||||
|
|
||||||
|
const selectedIdArray: ComputedRef<string[]> = computed(() => Array.from(selectedIds.value))
|
||||||
|
const selectedNodeArray: ComputedRef<NodeRecord[]> = computed(() => Array.from(selectedNodes.value.values()))
|
||||||
|
|
||||||
|
const selectedCollections: ComputedRef<CollectionObject[]> = computed(() =>
|
||||||
|
selectedNodeArray.value.filter((node): node is CollectionObject => node instanceof CollectionObject),
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedEntities: ComputedRef<EntityObject[]> = computed(() =>
|
||||||
|
selectedNodeArray.value.filter((node): node is EntityObject => node instanceof EntityObject),
|
||||||
|
)
|
||||||
|
|
||||||
|
const isSelected = (nodeId: string): boolean => selectedIds.value.has(nodeId)
|
||||||
|
|
||||||
|
const isTypeAllowed = (node: NodeRecord): boolean => {
|
||||||
|
if (node instanceof CollectionObject && !allowFolders) return false
|
||||||
|
if (node instanceof EntityObject && !allowFiles) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = (node: NodeRecord) => {
|
||||||
|
if (!isTypeAllowed(node)) return
|
||||||
|
|
||||||
|
if (!multiple) {
|
||||||
|
selectedIds.value.clear()
|
||||||
|
selectedNodes.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeId = String(node.identifier)
|
||||||
|
selectedIds.value.add(nodeId)
|
||||||
|
selectedNodes.value.set(nodeId, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deselect = (nodeId: string) => {
|
||||||
|
selectedIds.value.delete(nodeId)
|
||||||
|
selectedNodes.value.delete(nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = (node: NodeRecord) => {
|
||||||
|
const nodeId = String(node.identifier)
|
||||||
|
if (isSelected(nodeId)) {
|
||||||
|
deselect(nodeId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectMultiple = (nodes: NodeRecord[]) => {
|
||||||
|
if (!multiple) {
|
||||||
|
const lastNode = nodes[nodes.length - 1]
|
||||||
|
if (lastNode && isTypeAllowed(lastNode)) {
|
||||||
|
selectedIds.value.clear()
|
||||||
|
selectedNodes.value.clear()
|
||||||
|
const nodeId = String(lastNode.identifier)
|
||||||
|
selectedIds.value.add(nodeId)
|
||||||
|
selectedNodes.value.set(nodeId, lastNode)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (isTypeAllowed(node)) {
|
||||||
|
const nodeId = String(node.identifier)
|
||||||
|
selectedIds.value.add(nodeId)
|
||||||
|
selectedNodes.value.set(nodeId, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAll = (nodes: NodeRecord[]) => {
|
||||||
|
if (!multiple) return
|
||||||
|
selectMultiple(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
selectedIds.value.clear()
|
||||||
|
selectedNodes.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSelection = (nodes: NodeRecord[]) => {
|
||||||
|
clear()
|
||||||
|
selectMultiple(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedIds,
|
||||||
|
selectedNodes,
|
||||||
|
count,
|
||||||
|
hasSelection,
|
||||||
|
selectedIdArray,
|
||||||
|
selectedNodeArray,
|
||||||
|
selectedCollections,
|
||||||
|
selectedEntities,
|
||||||
|
isSelected,
|
||||||
|
select,
|
||||||
|
deselect,
|
||||||
|
toggle,
|
||||||
|
selectMultiple,
|
||||||
|
selectAll,
|
||||||
|
clear,
|
||||||
|
setSelection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDocumentSelection
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ModuleIntegrations } from "@KTXC/types/moduleTypes";
|
||||||
|
|
||||||
|
const integrations: ModuleIntegrations = {
|
||||||
|
documents_selector: [
|
||||||
|
{
|
||||||
|
id: 'dialog',
|
||||||
|
label: 'Document Selector Dialog',
|
||||||
|
priority: 10,
|
||||||
|
component: () => import('./components/DocumentSelectorDialog.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default integrations;
|
||||||
+8
-2
@@ -2,14 +2,20 @@
|
|||||||
* Documents Manager Module Boot
|
* Documents Manager Module Boot
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import integrations from '@/integrations'
|
||||||
|
|
||||||
console.log('[Documents Manager] Booting module...')
|
console.log('[Documents Manager] Booting module...')
|
||||||
|
|
||||||
console.log('[Documents Manager] Module booted successfully...')
|
console.log('[Documents Manager] Module booted successfully...')
|
||||||
|
|
||||||
// CSS will be injected by build process
|
// CSS filename is injected by the vite plugin at build time
|
||||||
//export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||||
|
|
||||||
|
export { integrations }
|
||||||
|
|
||||||
// Export services, stores and models for external use
|
// Export services, stores and models for external use
|
||||||
export * from '@/services'
|
export * from '@/services'
|
||||||
export * from '@/stores'
|
export * from '@/stores'
|
||||||
export * from '@/models'
|
export * from '@/models'
|
||||||
|
export * from '@/composables'
|
||||||
|
export * from '@/utils/fileHelpers'
|
||||||
|
|||||||
@@ -25,23 +25,11 @@ import type {
|
|||||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||||
import { CollectionObject, CollectionPropertiesObject } from '../models/collection';
|
import { CollectionObject, CollectionPropertiesObject } from '../models/collection';
|
||||||
|
|
||||||
function isCollectionPayload(value: unknown): value is CollectionInterface {
|
|
||||||
if (!value || typeof value !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidate = value as Record<string, unknown>;
|
|
||||||
return (
|
|
||||||
('identifier' in candidate || 'provider' in candidate || 'service' in candidate)
|
|
||||||
&& 'properties' in candidate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to create the right collection model class based on provider identifier
|
* Helper to create the right collection model class based on provider identifier
|
||||||
* Uses provider-specific factory if available, otherwise returns base CollectionObject
|
* Uses provider-specific factory if available, otherwise returns base CollectionObject
|
||||||
*/
|
*/
|
||||||
function createCollectionObject(data: CollectionInterface): CollectionObject {
|
export function createCollectionObject(data: CollectionInterface): CollectionObject {
|
||||||
const integrationStore = useIntegrationStore();
|
const integrationStore = useIntegrationStore();
|
||||||
const factoryItem = integrationStore.getItemById('documents_collection_factory', data.provider) as any;
|
const factoryItem = integrationStore.getItemById('documents_collection_factory', data.provider) as any;
|
||||||
const factory = factoryItem?.factory;
|
const factory = factoryItem?.factory;
|
||||||
@@ -68,19 +56,8 @@ export const collectionService = {
|
|||||||
const serviceList: Record<string, Record<string, CollectionObject>> = {};
|
const serviceList: Record<string, Record<string, CollectionObject>> = {};
|
||||||
Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => {
|
Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => {
|
||||||
const collectionList: Record<string, CollectionObject> = {};
|
const collectionList: Record<string, CollectionObject> = {};
|
||||||
Object.entries(serviceCollections as Record<string, unknown>).forEach(([collectionId, collectionData]) => {
|
Object.entries(serviceCollections).forEach(([collectionId, collectionData]) => {
|
||||||
if (isCollectionPayload(collectionData)) {
|
|
||||||
collectionList[collectionId] = createCollectionObject(collectionData);
|
collectionList[collectionId] = createCollectionObject(collectionData);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionData && typeof collectionData === 'object') {
|
|
||||||
Object.entries(collectionData as Record<string, unknown>).forEach(([nestedCollectionId, nestedCollectionData]) => {
|
|
||||||
if (isCollectionPayload(nestedCollectionData)) {
|
|
||||||
collectionList[nestedCollectionId] = createCollectionObject(nestedCollectionData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
serviceList[serviceId] = collectionList;
|
serviceList[serviceId] = collectionList;
|
||||||
});
|
});
|
||||||
@@ -91,15 +68,15 @@ export const collectionService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a specific collection by provider and identifier
|
* Retrieve collections by resource identifier
|
||||||
*
|
*
|
||||||
* @param request - fetch request parameters
|
* @param request - fetch request parameters
|
||||||
*
|
*
|
||||||
* @returns Promise with collection object
|
* @returns Promise with collections keyed by resource identifier
|
||||||
*/
|
*/
|
||||||
async fetch(request: CollectionFetchRequest): Promise<CollectionObject> {
|
async fetch(request: CollectionFetchRequest): Promise<Record<string, CollectionObject>> {
|
||||||
const response = await transceivePost<CollectionFetchRequest, CollectionFetchResponse>('collection.fetch', request);
|
const response = await transceivePost<CollectionFetchRequest, CollectionFetchResponse>('collection.fetch', request);
|
||||||
return createCollectionObject(response);
|
return Object.fromEntries(Object.entries(response).map(([target, data]) => [target, createCollectionObject(data)]));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { EntityObject } from '../models';
|
|||||||
* Helper to create the right entity model class based on provider identifier
|
* Helper to create the right entity model class based on provider identifier
|
||||||
* Uses provider-specific factory if available, otherwise returns base EntityObject
|
* Uses provider-specific factory if available, otherwise returns base EntityObject
|
||||||
*/
|
*/
|
||||||
function createEntityObject(data: EntityInterface): EntityObject {
|
export function createEntityObject(data: EntityInterface): EntityObject {
|
||||||
const integrationStore = useIntegrationStore();
|
const integrationStore = useIntegrationStore();
|
||||||
const factoryItem = integrationStore.getItemById('documents_entity_factory', data.provider) as any;
|
const factoryItem = integrationStore.getItemById('documents_entity_factory', data.provider) as any;
|
||||||
const factory = factoryItem?.factory;
|
const factory = factoryItem?.factory;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { transceiveStream } from './transceive';
|
||||||
|
import type { NodeListRequest, NodeInterface } from '../types/node';
|
||||||
|
import { createCollectionObject } from './collectionService';
|
||||||
|
import { createEntityObject } from './entityService';
|
||||||
|
import type { CollectionObject } from '../models/collection';
|
||||||
|
import type { EntityObject } from '../models/entity';
|
||||||
|
|
||||||
|
export type NodeListResult = (CollectionObject | EntityObject)[];
|
||||||
|
|
||||||
|
export const nodeService = {
|
||||||
|
/** Hydrate a mixed page in the exact order returned by the provider. */
|
||||||
|
async list(request: NodeListRequest = {}): Promise<NodeListResult> {
|
||||||
|
const nodes: NodeListResult = [];
|
||||||
|
const { total } = await transceiveStream<NodeListRequest, NodeInterface>('node.list', request, node => {
|
||||||
|
switch (node['@type']) {
|
||||||
|
case 'document:collection':
|
||||||
|
nodes.push(createCollectionObject(node));
|
||||||
|
break;
|
||||||
|
case 'document:entity':
|
||||||
|
nodes.push(createEntityObject(node));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported node type in node.list response');
|
||||||
|
}
|
||||||
|
}, { requireEnd: true });
|
||||||
|
if (total !== nodes.length) {
|
||||||
|
throw new Error('Incomplete node.list stream');
|
||||||
|
}
|
||||||
|
// Publish only complete pages so a retry uses the same offset after failure.
|
||||||
|
return nodes;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nodeService;
|
||||||
@@ -62,7 +62,7 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
operation: string,
|
operation: string,
|
||||||
data: TRequest,
|
data: TRequest,
|
||||||
onData: (data: TData) => void,
|
onData: (data: TData) => void,
|
||||||
options?: { user?: string; onStart?: (expected?: number) => void }
|
options?: { user?: string; onStart?: (expected?: number) => void; requireEnd?: boolean }
|
||||||
): Promise<{ total: number }> {
|
): Promise<{ total: number }> {
|
||||||
const request: ApiRequest<TRequest> = {
|
const request: ApiRequest<TRequest> = {
|
||||||
version: API_VERSION,
|
version: API_VERSION,
|
||||||
@@ -73,6 +73,7 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
let ended = false;
|
||||||
|
|
||||||
const dispatch = (line: string): void => {
|
const dispatch = (line: string): void => {
|
||||||
const message = JSON.parse(line) as ApiStreamResponse<TData>;
|
const message = JSON.parse(line) as ApiStreamResponse<TData>;
|
||||||
@@ -82,6 +83,7 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
options?.onStart?.(message.total);
|
options?.onStart?.(message.total);
|
||||||
} else if (message.status === 'end') {
|
} else if (message.status === 'end') {
|
||||||
total = message.total;
|
total = message.total;
|
||||||
|
ended = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -118,6 +120,7 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer += decoder.decode();
|
||||||
if (buffer.trim()) dispatch(buffer);
|
if (buffer.trim()) dispatch(buffer);
|
||||||
} finally {
|
} finally {
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
@@ -125,5 +128,8 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options?.requireEnd && !ended) {
|
||||||
|
throw new Error(`[${operation}] Stream ended without a completion frame`);
|
||||||
|
}
|
||||||
return { total };
|
return { total };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,25 +40,25 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
return `${provider}:${service ?? ''}:${identifier ?? ''}`
|
return `${provider}:${service ?? ''}:${identifier ?? ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function collection(provider: string, service: string | number, identifier: string | number, retrieve: boolean = false): CollectionObject | null {
|
function collection(target: CollectionIdentifier, retrieve: boolean = false): CollectionObject | null {
|
||||||
const key = identifierKey(provider, service, identifier)
|
const key = target
|
||||||
if (retrieve === true && !_collections.value[key]) {
|
if (retrieve === true && !_collections.value[key]) {
|
||||||
console.debug(`[Documents Manager][Store] - Force fetching collection "${key}"`)
|
console.debug(`[Documents Manager][Store] - Force fetching collection "${key}"`)
|
||||||
fetch(provider, service, identifier)
|
void fetch([target])
|
||||||
}
|
}
|
||||||
|
|
||||||
return _collections.value[key] || null
|
return _collections.value[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectionsForService(provider: string, service: string | number): CollectionObject[] {
|
function collectionsForService(target: ServiceIdentifier): CollectionObject[] {
|
||||||
const serviceKeyPrefix = `${provider}:${service}:`
|
const serviceKeyPrefix = `${target}:`
|
||||||
return Object.entries(_collections.value)
|
return Object.entries(_collections.value)
|
||||||
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
||||||
.map(([_, collectionObj]) => collectionObj)
|
.map(([_, collectionObj]) => collectionObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearService(provider: string, service: string | number): void {
|
function clearService(target: ServiceIdentifier): void {
|
||||||
const serviceKeyPrefix = `${provider}:${service}:`
|
const serviceKeyPrefix = `${target}:`
|
||||||
Object.keys(_collections.value)
|
Object.keys(_collections.value)
|
||||||
.filter((key) => key.startsWith(serviceKeyPrefix))
|
.filter((key) => key.startsWith(serviceKeyPrefix))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
@@ -70,6 +70,27 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
_collections.value = {}
|
_collections.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge externally-hydrated collections into the store (used by paginated node fetches)
|
||||||
|
*/
|
||||||
|
function mergeMany(collections: CollectionObject[]): void {
|
||||||
|
const hydrated: Record<string, CollectionObject> = {}
|
||||||
|
collections.forEach((collectionObj) => {
|
||||||
|
const key = identifierKey(collectionObj.provider, collectionObj.service, collectionObj.identifier)
|
||||||
|
hydrated[key] = collectionObj
|
||||||
|
})
|
||||||
|
_collections.value = { ..._collections.value, ...hydrated }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove specific collections by their own identifiers (used to evict a stale page before a reset)
|
||||||
|
*/
|
||||||
|
function removeMany(targets: CollectionIdentifier[]): void {
|
||||||
|
targets.forEach((target) => {
|
||||||
|
delete _collections.value[target]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort): Promise<Record<string, CollectionObject>> {
|
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort): Promise<Record<string, CollectionObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
@@ -98,14 +119,11 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetch(provider: string, service: string | number, identifier: string | number): Promise<CollectionObject> {
|
async function fetch(targets: CollectionIdentifier[]): Promise<Record<string, CollectionObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.fetch({ targets: [`${provider}:${service}:${identifier}`] })
|
const response = await collectionService.fetch({ targets })
|
||||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
_collections.value = { ..._collections.value, ...response }
|
||||||
_collections.value[key] = response
|
|
||||||
|
|
||||||
console.debug('[Documents Manager][Store] - Successfully fetched collection:', key)
|
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to fetch collection:', error)
|
console.error('[Documents Manager][Store] - Failed to fetch collection:', error)
|
||||||
@@ -130,17 +148,12 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function create(
|
async function create(
|
||||||
provider: string,
|
target: ServiceIdentifier | CollectionIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number | null,
|
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const target: CollectionIdentifier | undefined = collection !== null && collection !== undefined && collection !== ''
|
const response = await collectionService.create({ target, properties })
|
||||||
? `${provider}:${service}:${collection}`
|
|
||||||
: undefined
|
|
||||||
const response = await collectionService.create({ provider, service: String(service), target, properties })
|
|
||||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||||
_collections.value[key] = response
|
_collections.value[key] = response
|
||||||
|
|
||||||
@@ -155,14 +168,12 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function update(
|
async function update(
|
||||||
provider: string,
|
target: CollectionIdentifier,
|
||||||
service: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.update({ target: `${provider}:${service}:${identifier}`, properties })
|
const response = await collectionService.update({ target, properties })
|
||||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||||
_collections.value[key] = response
|
_collections.value[key] = response
|
||||||
|
|
||||||
@@ -176,17 +187,16 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(provider: string, service: string | number, identifier: string | number): Promise<{ success: boolean }> {
|
async function remove(target: CollectionIdentifier): Promise<{ success: boolean }> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.delete({ target: `${provider}:${service}:${identifier}` })
|
const response = await collectionService.delete({ target })
|
||||||
const success = response.disposition === 'deleted' || response.disposition === 'moved'
|
const success = response.disposition === 'deleted' || response.disposition === 'moved'
|
||||||
if (success) {
|
if (success) {
|
||||||
const key = identifierKey(provider, service, identifier)
|
delete _collections.value[target]
|
||||||
delete _collections.value[key]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('[Documents Manager][Store] - Successfully deleted collection:', `${provider}:${service}:${identifier}`)
|
console.debug('[Documents Manager][Store] - Successfully deleted collection:', target)
|
||||||
return { success }
|
return { success }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to delete collection:', error)
|
console.error('[Documents Manager][Store] - Failed to delete collection:', error)
|
||||||
@@ -206,6 +216,8 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
collectionsForService,
|
collectionsForService,
|
||||||
clearService,
|
clearService,
|
||||||
clearAll,
|
clearAll,
|
||||||
|
mergeMany,
|
||||||
|
removeMany,
|
||||||
list,
|
list,
|
||||||
fetch,
|
fetch,
|
||||||
extant,
|
extant,
|
||||||
|
|||||||
+48
-62
@@ -49,37 +49,34 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function entity(
|
function entity(
|
||||||
provider: string,
|
target: EntityIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
retrieve: boolean = false,
|
retrieve: boolean = false,
|
||||||
): EntityObject | null {
|
): EntityObject | null {
|
||||||
const key = identifierKey(provider, service, collection, identifier)
|
const key = target
|
||||||
if (retrieve === true && !_entities.value[key]) {
|
if (retrieve === true && !_entities.value[key]) {
|
||||||
console.debug(`[Documents Manager][Store] - Force fetching entity "${key}"`)
|
console.debug(`[Documents Manager][Store] - Force fetching entity "${key}"`)
|
||||||
fetch(provider, service, collection, [identifier])
|
void fetch([target])
|
||||||
}
|
}
|
||||||
|
|
||||||
return _entities.value[key] || null
|
return _entities.value[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function entitiesForService(provider: string, service: string | number): EntityObject[] {
|
function entitiesForService(target: ServiceIdentifier): EntityObject[] {
|
||||||
const serviceKeyPrefix = `${provider}:${service}:`
|
const serviceKeyPrefix = `${target}:`
|
||||||
return Object.entries(_entities.value)
|
return Object.entries(_entities.value)
|
||||||
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
||||||
.map(([_, entityObj]) => entityObj)
|
.map(([_, entityObj]) => entityObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function entitiesForCollection(provider: string, service: string | number, collection: string | number): EntityObject[] {
|
function entitiesForCollection(target: CollectionIdentifier): EntityObject[] {
|
||||||
const collectionKeyPrefix = `${provider}:${service}:${collection}:`
|
const collectionKeyPrefix = `${target}:`
|
||||||
return Object.entries(_entities.value)
|
return Object.entries(_entities.value)
|
||||||
.filter(([key]) => key.startsWith(collectionKeyPrefix))
|
.filter(([key]) => key.startsWith(collectionKeyPrefix))
|
||||||
.map(([_, entityObj]) => entityObj)
|
.map(([_, entityObj]) => entityObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearService(provider: string, service: string | number): void {
|
function clearService(target: ServiceIdentifier): void {
|
||||||
const serviceKeyPrefix = `${provider}:${service}:`
|
const serviceKeyPrefix = `${target}:`
|
||||||
Object.keys(_entities.value)
|
Object.keys(_entities.value)
|
||||||
.filter((key) => key.startsWith(serviceKeyPrefix))
|
.filter((key) => key.startsWith(serviceKeyPrefix))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
@@ -87,8 +84,8 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearCollection(provider: string, service: string | number, collection: string | number): void {
|
function clearCollection(target: CollectionIdentifier): void {
|
||||||
const collectionKeyPrefix = `${provider}:${service}:${collection}:`
|
const collectionKeyPrefix = `${target}:`
|
||||||
Object.keys(_entities.value)
|
Object.keys(_entities.value)
|
||||||
.filter((key) => key.startsWith(collectionKeyPrefix))
|
.filter((key) => key.startsWith(collectionKeyPrefix))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
@@ -100,6 +97,18 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
_entities.value = {}
|
_entities.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge externally-hydrated entities into the store (used by paginated node fetches)
|
||||||
|
*/
|
||||||
|
function mergeMany(entities: EntityObject[]): void {
|
||||||
|
const hydrated: Record<string, EntityObject> = {}
|
||||||
|
entities.forEach((entityObj) => {
|
||||||
|
const key = identifierKey(entityObj.provider, entityObj.service, entityObj.collection, entityObj.identifier)
|
||||||
|
hydrated[key] = entityObj
|
||||||
|
})
|
||||||
|
_entities.value = { ..._entities.value, ...hydrated }
|
||||||
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise<Record<string, EntityObject>> {
|
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise<Record<string, EntityObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
@@ -131,25 +140,13 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetch(
|
async function fetch(
|
||||||
provider: string,
|
targets: EntityIdentifier[],
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifiers: (string | number)[],
|
|
||||||
): Promise<Record<string, EntityObject>> {
|
): Promise<Record<string, EntityObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const targets: EntityIdentifier[] = identifiers.map((identifier) => `${provider}:${service}:${collection}:${identifier}` as EntityIdentifier)
|
|
||||||
const response = await entityService.fetch({ targets })
|
const response = await entityService.fetch({ targets })
|
||||||
|
_entities.value = { ..._entities.value, ...response }
|
||||||
const hydrated: Record<string, EntityObject> = {}
|
return response
|
||||||
Object.entries(response).forEach(([identifier, entityObj]) => {
|
|
||||||
const key = identifierKey(provider, service, collection, identifier)
|
|
||||||
hydrated[key] = entityObj
|
|
||||||
_entities.value[key] = entityObj
|
|
||||||
})
|
|
||||||
|
|
||||||
console.debug('[Documents Manager][Store] - Successfully fetched', Object.keys(hydrated).length, 'entities')
|
|
||||||
return hydrated
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to fetch entities:', error)
|
console.error('[Documents Manager][Store] - Failed to fetch entities:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -173,15 +170,13 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function create(
|
async function create(
|
||||||
provider: string,
|
target: CollectionIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
options?: Record<string, unknown>,
|
options?: Record<string, unknown>,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.create({ target: `${provider}:${service}:${collection}`, properties, options })
|
const response = await entityService.create({ target, properties, options })
|
||||||
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
||||||
_entities.value[key] = response
|
_entities.value[key] = response
|
||||||
|
|
||||||
@@ -196,15 +191,12 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function update(
|
async function update(
|
||||||
provider: string,
|
target: EntityIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.update({ target: `${provider}:${service}:${collection}:${identifier}`, properties })
|
const response = await entityService.update({ target, properties })
|
||||||
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
||||||
_entities.value[key] = response
|
_entities.value[key] = response
|
||||||
|
|
||||||
@@ -219,23 +211,22 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(
|
async function remove(
|
||||||
provider: string,
|
targets: EntityIdentifier[],
|
||||||
service: string | number,
|
): Promise<{ successes: EntityIdentifier[]; failures: EntityIdentifier[] }> {
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<{ success: boolean }> {
|
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const target: EntityIdentifier = `${provider}:${service}:${collection}:${identifier}`
|
const response = await entityService.delete({ targets })
|
||||||
const response = await entityService.delete({ targets: [target] })
|
const successes: EntityIdentifier[] = []
|
||||||
const success = response[target]?.disposition === 'deleted'
|
const failures: EntityIdentifier[] = []
|
||||||
if (success) {
|
for (const target of targets) {
|
||||||
const key = identifierKey(provider, service, collection, identifier)
|
if (response[target]?.disposition === 'deleted') {
|
||||||
delete _entities.value[key]
|
delete _entities.value[target]
|
||||||
|
successes.push(target)
|
||||||
|
} else {
|
||||||
|
failures.push(target)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
console.debug('[Documents Manager][Store] - Successfully deleted entity:', target)
|
return { successes, failures }
|
||||||
return { success }
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to delete entity:', error)
|
console.error('[Documents Manager][Store] - Failed to delete entity:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -277,14 +268,11 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function read(
|
async function read(
|
||||||
provider: string,
|
target: EntityIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.read({ target: `${provider}:${service}:${collection}:${identifier}` })
|
const response = await entityService.read({ target })
|
||||||
return response.content
|
return response.content
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to read entity:', error)
|
console.error('[Documents Manager][Store] - Failed to read entity:', error)
|
||||||
@@ -295,15 +283,12 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function write(
|
async function write(
|
||||||
provider: string,
|
target: EntityIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
content: string,
|
content: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.write({ target: `${provider}:${service}:${collection}:${identifier}`, content, encoding: 'base64' })
|
const response = await entityService.write({ target, content, encoding: 'base64' })
|
||||||
return response.bytesWritten
|
return response.bytesWritten
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to write entity:', error)
|
console.error('[Documents Manager][Store] - Failed to write entity:', error)
|
||||||
@@ -325,6 +310,7 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
clearService,
|
clearService,
|
||||||
clearCollection,
|
clearCollection,
|
||||||
clearAll,
|
clearAll,
|
||||||
|
mergeMany,
|
||||||
list,
|
list,
|
||||||
fetch,
|
fetch,
|
||||||
extant,
|
extant,
|
||||||
|
|||||||
+76
-68
@@ -6,6 +6,7 @@ import { computed, ref, readonly } from 'vue'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type {
|
import type {
|
||||||
CollectionIdentifier,
|
CollectionIdentifier,
|
||||||
|
EntityIdentifier,
|
||||||
ServiceIdentifier,
|
ServiceIdentifier,
|
||||||
ListFilter,
|
ListFilter,
|
||||||
ListSort,
|
ListSort,
|
||||||
@@ -16,6 +17,7 @@ import type {
|
|||||||
import { CollectionObject, EntityObject } from '../models'
|
import { CollectionObject, EntityObject } from '../models'
|
||||||
import { useCollectionsStore } from './collectionsStore'
|
import { useCollectionsStore } from './collectionsStore'
|
||||||
import { useEntitiesStore } from './entitiesStore'
|
import { useEntitiesStore } from './entitiesStore'
|
||||||
|
import { nodeService } from '../services/nodeService'
|
||||||
|
|
||||||
export const ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
export const ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
||||||
|
|
||||||
@@ -46,8 +48,8 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
|
|
||||||
function getServiceNodes(providerId: string, serviceId: string | number): NodeRecord[] {
|
function getServiceNodes(providerId: string, serviceId: string | number): NodeRecord[] {
|
||||||
return [
|
return [
|
||||||
...collectionsStore.collectionsForService(providerId, serviceId),
|
...collectionsStore.collectionsForService(`${providerId}:${serviceId}`),
|
||||||
...entitiesStore.entitiesForService(providerId, serviceId),
|
...entitiesStore.entitiesForService(`${providerId}:${serviceId}`),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,20 +107,13 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCollections(
|
async function fetchCollections(
|
||||||
providerId: string,
|
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number | null,
|
|
||||||
filter?: ListFilter,
|
filter?: ListFilter,
|
||||||
sort?: ListSort,
|
sort?: ListSort,
|
||||||
): Promise<CollectionObject[]> {
|
): Promise<CollectionObject[]> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const sources: (ServiceIdentifier | CollectionIdentifier)[] = collectionId === null
|
return Object.values(await collectionsStore.list(sources, filter, sort))
|
||||||
? [`${providerId}:${serviceId}`]
|
|
||||||
: [`${providerId}:${serviceId}:${collectionId}`]
|
|
||||||
|
|
||||||
await collectionsStore.list(sources, filter, sort)
|
|
||||||
return collectionsStore.collectionsForService(providerId, serviceId)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to fetch collections'
|
error.value = e instanceof Error ? e.message : 'Failed to fetch collections'
|
||||||
throw e
|
throw e
|
||||||
@@ -126,43 +121,76 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchEntities(
|
async function fetchEntities(
|
||||||
providerId: string,
|
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number | null,
|
|
||||||
filter?: ListFilter,
|
filter?: ListFilter,
|
||||||
sort?: ListSort,
|
sort?: ListSort,
|
||||||
range?: ListRange,
|
range?: ListRange,
|
||||||
): Promise<EntityObject[]> {
|
): Promise<EntityObject[]> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const sources: (ServiceIdentifier | CollectionIdentifier)[] = collectionId === null
|
return Object.values(await entitiesStore.list(sources, filter, sort, range))
|
||||||
? [`${providerId}:${serviceId}`]
|
|
||||||
: [`${providerId}:${serviceId}:${collectionId}`]
|
|
||||||
|
|
||||||
await entitiesStore.list(sources, filter, sort, range)
|
|
||||||
return entitiesStore.entitiesForCollection(providerId, serviceId, collectionId)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to fetch entities'
|
error.value = e instanceof Error ? e.message : 'Failed to fetch entities'
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a unified node listing, optionally limited to a page by range.
|
||||||
|
*/
|
||||||
async function fetchNodes(
|
async function fetchNodes(
|
||||||
providerId: string,
|
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
||||||
serviceId: string | number,
|
|
||||||
parentId: string | number | null = ROOT_ID,
|
|
||||||
filter?: ListFilter,
|
filter?: ListFilter,
|
||||||
sort?: ListSort,
|
sort?: ListSort,
|
||||||
range?: ListRange,
|
range?: ListRange,
|
||||||
): Promise<NodeRecord[]> {
|
mode: 'reset' | 'append' = 'reset',
|
||||||
|
): Promise<{ collections: CollectionObject[]; entities: EntityObject[]; isLastPage: boolean }> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
const result = await nodeService.list({ sources, filter, sort, range })
|
||||||
fetchCollections(providerId, serviceId, parentId, filter, sort),
|
|
||||||
fetchEntities(providerId, serviceId, parentId, filter, sort, range),
|
|
||||||
])
|
|
||||||
|
|
||||||
return getChildren(providerId, serviceId, parentId)
|
// Service selectors list the root; collection selectors list that folder.
|
||||||
|
// Omitted sources list roots across services, not every cached descendant.
|
||||||
|
const locations = new Map<string, { provider: string; service: string; parent: string }>()
|
||||||
|
if (sources?.length) {
|
||||||
|
for (const source of sources) {
|
||||||
|
const [provider, service, parent] = source.split(':')
|
||||||
|
const location = { provider: provider!, service: service!, parent: isRoot(parent) ? ROOT_ID : parent! }
|
||||||
|
locations.set(`${location.provider}:${location.service}:${location.parent}`, location)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const node of [...nodeList.value, ...result]) {
|
||||||
|
const location = { provider: node.provider, service: String(node.service), parent: ROOT_ID }
|
||||||
|
locations.set(`${location.provider}:${location.service}:${ROOT_ID}`, location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace only the selected locations, and only after the stream succeeds.
|
||||||
|
if (mode === 'reset') {
|
||||||
|
for (const { provider, service, parent } of locations.values()) {
|
||||||
|
const staleIds = getChildCollections(provider, service, parent)
|
||||||
|
.map((node): CollectionIdentifier => `${node.provider}:${node.service}:${node.identifier}`)
|
||||||
|
collectionsStore.removeMany(staleIds)
|
||||||
|
entitiesStore.clearCollection(`${provider}:${service}:${parent}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageCollections = result.filter((node): node is CollectionObject => node instanceof CollectionObject)
|
||||||
|
const pageEntities = result.filter((node): node is EntityObject => node instanceof EntityObject)
|
||||||
|
|
||||||
|
collectionsStore.mergeMany(pageCollections)
|
||||||
|
entitiesStore.mergeMany(pageEntities)
|
||||||
|
|
||||||
|
// The backend applies the range independently to each selected location.
|
||||||
|
const counts = new Map<string, number>()
|
||||||
|
for (const node of result) {
|
||||||
|
const parent = isRoot(node.collection) ? ROOT_ID : String(node.collection)
|
||||||
|
const key = `${node.provider}:${node.service}:${parent}`
|
||||||
|
counts.set(key, (counts.get(key) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
const isLastPage = range === undefined || [...counts.values()].every(count => count < range.tally)
|
||||||
|
|
||||||
|
return { collections: pageCollections, entities: pageEntities, isLastPage }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to fetch nodes'
|
error.value = e instanceof Error ? e.message : 'Failed to fetch nodes'
|
||||||
throw e
|
throw e
|
||||||
@@ -170,14 +198,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createCollection(
|
async function createCollection(
|
||||||
providerId: string,
|
target: ServiceIdentifier | CollectionIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
parentCollectionId: string | number | null,
|
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await collectionsStore.create(providerId, serviceId, parentCollectionId, properties)
|
return await collectionsStore.create(target, properties)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to create collection'
|
error.value = e instanceof Error ? e.message : 'Failed to create collection'
|
||||||
throw e
|
throw e
|
||||||
@@ -185,14 +211,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateCollection(
|
async function updateCollection(
|
||||||
providerId: string,
|
target: CollectionIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await collectionsStore.update(providerId, serviceId, identifier, properties)
|
return await collectionsStore.update(target, properties)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to update collection'
|
error.value = e instanceof Error ? e.message : 'Failed to update collection'
|
||||||
throw e
|
throw e
|
||||||
@@ -200,13 +224,11 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCollection(
|
async function deleteCollection(
|
||||||
providerId: string,
|
target: CollectionIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const response = await collectionsStore.delete(providerId, serviceId, identifier)
|
const response = await collectionsStore.delete(target)
|
||||||
return response.success
|
return response.success
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to delete collection'
|
error.value = e instanceof Error ? e.message : 'Failed to delete collection'
|
||||||
@@ -215,15 +237,13 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createEntity(
|
async function createEntity(
|
||||||
providerId: string,
|
target: CollectionIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
options?: Record<string, unknown>,
|
options?: Record<string, unknown>,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.create(providerId, serviceId, collectionId, properties, options)
|
return await entitiesStore.create(target, properties, options)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to create entity'
|
error.value = e instanceof Error ? e.message : 'Failed to create entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -231,15 +251,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateEntity(
|
async function updateEntity(
|
||||||
providerId: string,
|
target: EntityIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.update(providerId, serviceId, collectionId, identifier, properties)
|
return await entitiesStore.update(target, properties)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to update entity'
|
error.value = e instanceof Error ? e.message : 'Failed to update entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -247,15 +264,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteEntity(
|
async function deleteEntity(
|
||||||
providerId: string,
|
target: EntityIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const response = await entitiesStore.delete(providerId, serviceId, collectionId, identifier)
|
const response = await entitiesStore.delete([target])
|
||||||
return response.success
|
return response.successes.includes(target)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to delete entity'
|
error.value = e instanceof Error ? e.message : 'Failed to delete entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -263,14 +277,11 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function readEntity(
|
async function readEntity(
|
||||||
providerId: string,
|
target: EntityIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.read(providerId, serviceId, collectionId, identifier)
|
return await entitiesStore.read(target)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to read entity'
|
error.value = e instanceof Error ? e.message : 'Failed to read entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -278,15 +289,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function writeEntity(
|
async function writeEntity(
|
||||||
providerId: string,
|
target: EntityIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
content: string,
|
content: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.write(providerId, serviceId, collectionId, identifier, content)
|
return await entitiesStore.write(target, content)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to write entity'
|
error.value = e instanceof Error ? e.message : 'Failed to write entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -294,8 +302,8 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearServiceNodes(providerId: string, serviceId: string | number): void {
|
function clearServiceNodes(providerId: string, serviceId: string | number): void {
|
||||||
collectionsStore.clearService(providerId, serviceId)
|
collectionsStore.clearService(`${providerId}:${serviceId}`)
|
||||||
entitiesStore.clearService(providerId, serviceId)
|
entitiesStore.clearService(`${providerId}:${serviceId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearNodes(): void {
|
function clearNodes(): void {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export interface CollectionFetchRequest {
|
|||||||
targets: CollectionIdentifier[];
|
targets: CollectionIdentifier[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionFetchResponse extends CollectionInterface {}
|
export type CollectionFetchResponse = Record<CollectionIdentifier, CollectionInterface>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection extant
|
* Collection extant
|
||||||
@@ -87,9 +87,7 @@ export interface CollectionExtantResponse {
|
|||||||
* Collection create
|
* Collection create
|
||||||
*/
|
*/
|
||||||
export interface CollectionCreateRequest {
|
export interface CollectionCreateRequest {
|
||||||
provider: string;
|
target: ServiceIdentifier | CollectionIdentifier; // Service root or parent collection
|
||||||
service: string | number;
|
|
||||||
target?: CollectionIdentifier | null; // Parent collection identifier (absent for root)
|
|
||||||
properties: CollectionMutableProperties;
|
properties: CollectionMutableProperties;
|
||||||
options?: Record<string, unknown>;
|
options?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export type * from './provider';
|
|||||||
export type * from './service';
|
export type * from './service';
|
||||||
export type * from './collection';
|
export type * from './collection';
|
||||||
export type * from './entity';
|
export type * from './entity';
|
||||||
|
export type * from './node';
|
||||||
export type * from './document';
|
export type * from './document';
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Node (unified collections + entities) type definitions
|
||||||
|
*/
|
||||||
|
import type { CollectionIdentifier, ListFilter, ListRange, ListSort, ServiceIdentifier } from './common';
|
||||||
|
import type { CollectionInterface } from './collection';
|
||||||
|
import type { EntityInterface } from './entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node list
|
||||||
|
*/
|
||||||
|
export interface NodeListRequest {
|
||||||
|
sources?: (ServiceIdentifier | CollectionIdentifier)[];
|
||||||
|
filter?: ListFilter;
|
||||||
|
sort?: ListSort;
|
||||||
|
range?: ListRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeInterface =
|
||||||
|
| (Omit<CollectionInterface, '@type'> & { '@type': 'document:collection' })
|
||||||
|
| (Omit<EntityInterface, '@type'> & { '@type': 'document:entity' });
|
||||||
|
|
||||||
|
/** Payload of each data frame in the node.list NDJSON stream. */
|
||||||
|
export type NodeListResponse = NodeInterface;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { EntityObject } from '@/models/entity'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the appropriate icon for a file based on its MIME type
|
||||||
|
*/
|
||||||
|
export function getFileIcon(entity: EntityObject): string {
|
||||||
|
const mime = entity.properties.mime || ''
|
||||||
|
if (!mime) return 'mdi-file'
|
||||||
|
if (mime.startsWith('image/')) return 'mdi-file-image'
|
||||||
|
if (mime.startsWith('video/')) return 'mdi-file-video'
|
||||||
|
if (mime.startsWith('audio/')) return 'mdi-file-music'
|
||||||
|
if (mime.startsWith('text/')) return 'mdi-file-document'
|
||||||
|
if (mime === 'application/pdf') return 'mdi-file-pdf-box'
|
||||||
|
if (mime.includes('zip') || mime.includes('tar') || mime.includes('compressed')) return 'mdi-folder-zip'
|
||||||
|
if (mime.includes('word') || mime.includes('document')) return 'mdi-file-word'
|
||||||
|
if (mime.includes('excel') || mime.includes('spreadsheet')) return 'mdi-file-excel'
|
||||||
|
if (mime.includes('powerpoint') || mime.includes('presentation')) return 'mdi-file-powerpoint'
|
||||||
|
return 'mdi-file'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format file size in human readable format
|
||||||
|
*/
|
||||||
|
export function formatSize(bytes: number): string {
|
||||||
|
const safeBytes = Number.isFinite(bytes) ? bytes : 0
|
||||||
|
if (safeBytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(safeBytes) / Math.log(k))
|
||||||
|
return parseFloat((safeBytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date string to localized format
|
||||||
|
*/
|
||||||
|
export function formatDate(value: Date | string | null | undefined): string {
|
||||||
|
if (!value) return '—'
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return '—'
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upload status icon
|
||||||
|
*/
|
||||||
|
export function getUploadStatusIcon(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending': return 'mdi-clock-outline'
|
||||||
|
case 'uploading': return 'mdi-loading mdi-spin'
|
||||||
|
case 'completed': return 'mdi-check-circle'
|
||||||
|
case 'error': return 'mdi-alert-circle'
|
||||||
|
default: return 'mdi-file'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upload status color
|
||||||
|
*/
|
||||||
|
export function getUploadStatusColor(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending': return 'grey'
|
||||||
|
case 'uploading': return 'primary'
|
||||||
|
case 'completed': return 'success'
|
||||||
|
case 'error': return 'error'
|
||||||
|
default: return 'grey'
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -15,5 +15,5 @@
|
|||||||
"@KTXC/*": ["../../core/src/*"]
|
"@KTXC/*": ["../../core/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user