Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 953a98afdc | |||
| ce246669d9 | |||
| 6b040ba19e | |||
| 33a06c6509 | |||
| f80e3679b6 |
@@ -50,11 +50,7 @@ class TransferController extends ControllerAbstract
|
||||
*
|
||||
* GET /download/entity/{provider}/{service}/{collection}/{identifier}
|
||||
*/
|
||||
#[AuthenticatedRoute(
|
||||
'/download/entity/{provider}/{service}/{collection}/{identifier}',
|
||||
name: 'document_manager.download.entity',
|
||||
methods: ['GET']
|
||||
)]
|
||||
#[AuthenticatedRoute('/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 {
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
$userId = $this->identityContext->identifier();
|
||||
@@ -124,16 +120,44 @@ 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 JsonResponse([
|
||||
'status' => 'error',
|
||||
'error' => ['code' => 404, 'message' => 'Preview unavailable'],
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$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
|
||||
*
|
||||
* GET /download/archive?provider=...&service=...&ids[]=...&ids[]=...
|
||||
*/
|
||||
#[AuthenticatedRoute(
|
||||
'/download/archive',
|
||||
name: 'manager.download.archive',
|
||||
methods: ['GET']
|
||||
)]
|
||||
#[AuthenticatedRoute('/download/archive', name: 'manager.download.archive', methods: ['GET'])]
|
||||
public function downloadArchive(string $provider, string $service, array $ids = [], ?string $collection = null, string $name = 'download'): Response {
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
$userId = $this->identityContext->identifier();
|
||||
@@ -215,11 +239,7 @@ class TransferController extends ControllerAbstract
|
||||
*
|
||||
* GET /download/collection/{provider}/{service}/{identifier}
|
||||
*/
|
||||
#[AuthenticatedRoute(
|
||||
'/download/collection/{provider}/{service}/{identifier}',
|
||||
name: 'manager.download.collection',
|
||||
methods: ['GET']
|
||||
)]
|
||||
#[AuthenticatedRoute('/download/collection/{provider}/{service}/{identifier}', name: 'manager.download.collection', methods: ['GET'])]
|
||||
public function downloadCollection(string $provider, string $service, string $identifier): Response {
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
$userId = $this->identityContext->identifier();
|
||||
|
||||
@@ -5,6 +5,9 @@ declare(strict_types=1);
|
||||
namespace KTXM\DocumentsManager;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use KTXC\Preview\Preview;
|
||||
use KTXC\Preview\PreviewManager;
|
||||
use KTXC\Resource\ProviderManager;
|
||||
use KTXF\Documents\Collection\CollectionBaseInterface;
|
||||
use KTXF\Documents\Collection\CollectionPropertiesMutableInterface;
|
||||
@@ -18,6 +21,8 @@ use KTXF\Documents\Service\ServiceCollectionMutableInterface;
|
||||
use KTXF\Documents\Service\ServiceEntityMutableInterface;
|
||||
use KTXF\Documents\Service\ServiceMutableInterface;
|
||||
use KTXF\Resource\Filter\IFilter;
|
||||
use KTXF\Preview\PreviewSource;
|
||||
use KTXF\Resource\BinaryResource;
|
||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
||||
@@ -37,6 +42,7 @@ class Manager {
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private ProviderManager $providerManager,
|
||||
private PreviewManager $previewManager,
|
||||
) { }
|
||||
|
||||
/**
|
||||
@@ -1342,4 +1348,62 @@ class Manager {
|
||||
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[$target->entity()] ?? reset($entities);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Generated
+253
-113
@@ -9,52 +9,73 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"pinia": "^4.0.0",
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitest/coverage-v8": "^4.1.6",
|
||||
"@vue/tsconfig": "^0.9.0",
|
||||
"jsdom": "^30.0.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"typescript": "~6.0.0",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.1.6"
|
||||
"vitest": "^4.1.6",
|
||||
"vue-tsc": "^3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz",
|
||||
"integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==",
|
||||
"version": "5.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
||||
"integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^3.2.1",
|
||||
"@csstools/css-color-parser": "^4.1.9",
|
||||
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||
"@csstools/css-calc": "^3.2.0",
|
||||
"@csstools/css-color-parser": "^4.1.0",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0",
|
||||
"lru-cache": "^11.5.2"
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.0.tgz",
|
||||
"integrity": "sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
|
||||
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.2.1",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2"
|
||||
"is-potential-custom-element-name": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/generational-cache": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
|
||||
"integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
@@ -74,12 +95,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
|
||||
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
"@babel/types": "^7.29.8"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -89,9 +110,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
|
||||
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
@@ -696,6 +717,23 @@
|
||||
"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": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
|
||||
@@ -850,40 +888,69 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz",
|
||||
"integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==",
|
||||
"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": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@vue/shared": "3.5.40",
|
||||
"@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": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz",
|
||||
"integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@vue/shared": "3.5.42",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz",
|
||||
"integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==",
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz",
|
||||
"integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
"@vue/compiler-core": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz",
|
||||
"integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==",
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz",
|
||||
"integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@vue/compiler-core": "3.5.40",
|
||||
"@vue/compiler-dom": "3.5.40",
|
||||
"@vue/compiler-ssr": "3.5.40",
|
||||
"@vue/shared": "3.5.40",
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@vue/compiler-core": "3.5.42",
|
||||
"@vue/compiler-dom": "3.5.42",
|
||||
"@vue/compiler-ssr": "3.5.42",
|
||||
"@vue/shared": "3.5.42",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.19",
|
||||
@@ -891,13 +958,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz",
|
||||
"integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==",
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz",
|
||||
"integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
"@vue/compiler-dom": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
@@ -930,52 +997,68 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz",
|
||||
"integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==",
|
||||
"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": {
|
||||
"@vue/shared": "3.5.40"
|
||||
"@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": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz",
|
||||
"integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.42"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz",
|
||||
"integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==",
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz",
|
||||
"integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
"@vue/reactivity": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz",
|
||||
"integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==",
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz",
|
||||
"integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.40",
|
||||
"@vue/runtime-core": "3.5.40",
|
||||
"@vue/shared": "3.5.40",
|
||||
"@vue/reactivity": "3.5.42",
|
||||
"@vue/runtime-core": "3.5.42",
|
||||
"@vue/shared": "3.5.42",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz",
|
||||
"integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==",
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz",
|
||||
"integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.40",
|
||||
"@vue/runtime-dom": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
"@vue/compiler-ssr": "3.5.42",
|
||||
"@vue/runtime-dom": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz",
|
||||
"integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==",
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz",
|
||||
"integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/tsconfig": {
|
||||
@@ -997,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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
@@ -1276,39 +1366,39 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "30.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.0.tgz",
|
||||
"integrity": "sha512-JQHfRGmmKmaZoUAvIgff5jjG/0SzTQlGz8c7t72KzBzo8ZULEjAjnYE0sNwBOUA4QtWwYE2xoYitg8NFsmiYxA==",
|
||||
"version": "29.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
||||
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^6.0.5",
|
||||
"@asamuzakjp/dom-selector": "^8.2.5",
|
||||
"@asamuzakjp/css-color": "^5.1.11",
|
||||
"@asamuzakjp/dom-selector": "^7.1.1",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.6",
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.3",
|
||||
"@exodus/bytes": "^1.15.0",
|
||||
"css-tree": "^3.2.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^6.0.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2",
|
||||
"lru-cache": "^11.3.5",
|
||||
"parse5": "^8.0.1",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.2",
|
||||
"undici": "^8.7.0",
|
||||
"tough-cookie": "^6.0.1",
|
||||
"undici": "^7.25.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^17.1.0",
|
||||
"whatwg-url": "^16.0.1",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.2.3"
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
@@ -1316,21 +1406,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/whatwg-url": {
|
||||
"version": "17.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz",
|
||||
"integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.14.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
@@ -1646,6 +1721,13 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
@@ -1710,6 +1792,13 @@
|
||||
"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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
@@ -2039,13 +2128,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
|
||||
"integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
|
||||
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
@@ -2223,17 +2312,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz",
|
||||
"integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==",
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
|
||||
"integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.40",
|
||||
"@vue/compiler-sfc": "3.5.40",
|
||||
"@vue/runtime-dom": "3.5.40",
|
||||
"@vue/server-renderer": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
"@vue/compiler-dom": "3.5.42",
|
||||
"@vue/compiler-sfc": "3.5.42",
|
||||
"@vue/runtime-dom": "3.5.42",
|
||||
"@vue/server-renderer": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -2244,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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
||||
+5
-2
@@ -15,16 +15,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^4.0.0",
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.9.0",
|
||||
"typescript": "~6.0.0",
|
||||
"vite": "^8.0.0",
|
||||
"vue-tsc": "^3.0.5",
|
||||
"@vitest/coverage-v8": "^4.1.6",
|
||||
"jsdom": "^30.0.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"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,305 @@
|
||||
/**
|
||||
* 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 } 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'
|
||||
|
||||
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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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 refresh()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Refresh current location
|
||||
const refresh = async (
|
||||
filter?: ListFilter,
|
||||
sort?: ListSort,
|
||||
range?: ListRange
|
||||
) => {
|
||||
await nodesStore.fetchNodes(
|
||||
currentProviderId(),
|
||||
currentServiceId(),
|
||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||
filter,
|
||||
sort,
|
||||
range
|
||||
)
|
||||
}
|
||||
|
||||
// Create a new folder
|
||||
const createFolder = async (label: string): Promise<CollectionObject> => {
|
||||
return await nodesStore.createCollection(
|
||||
currentProviderId(),
|
||||
currentServiceId(),
|
||||
currentLocation.value === ROOT_ID ? ROOT_ID : 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 === ROOT_ID ? ROOT_ID : 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(currentProviderId(), currentServiceId(), 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(currentProviderId(), currentServiceId(), node.collection, 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(currentProviderId(), currentServiceId(), nodeId)
|
||||
} else {
|
||||
return await nodesStore.deleteEntity(currentProviderId(), currentServiceId(), node.collection, 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(currentProviderId(), currentServiceId(), 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(currentProviderId(), currentServiceId(), node.collection, 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 refresh()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
currentLocation,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// Provider/Service
|
||||
provider,
|
||||
service,
|
||||
rootId,
|
||||
|
||||
// Current view
|
||||
currentChildren,
|
||||
currentCollections,
|
||||
currentEntities,
|
||||
breadcrumbs,
|
||||
isAtRoot,
|
||||
|
||||
// Navigation
|
||||
navigateTo,
|
||||
navigateUp,
|
||||
navigateToRoot,
|
||||
refresh,
|
||||
|
||||
// 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
|
||||
*/
|
||||
|
||||
import integrations from '@/integrations'
|
||||
|
||||
console.log('[Documents Manager] Booting module...')
|
||||
|
||||
console.log('[Documents Manager] Module booted successfully...')
|
||||
|
||||
// CSS will be injected by build process
|
||||
//export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||
// CSS filename is injected by the vite plugin at build time
|
||||
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||
|
||||
export { integrations }
|
||||
|
||||
// Export services, stores and models for external use
|
||||
export * from '@/services'
|
||||
export * from '@/stores'
|
||||
export * from '@/models'
|
||||
export * from '@/composables'
|
||||
export * from '@/utils/fileHelpers'
|
||||
|
||||
@@ -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/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user