diff --git a/appinfo/routes.php b/appinfo/routes.php index 3e8e0cb3..9dae8c34 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -23,6 +23,8 @@ ['name' => 'Whiteboard#getLib', 'url' => 'library', 'verb' => 'GET'], /** @see WhiteboardController::updateLib() */ ['name' => 'Whiteboard#updateLib', 'url' => 'library', 'verb' => 'PUT'], + /** @see WhiteboardController::saveLibTemplate() */ + ['name' => 'Whiteboard#saveLibTemplate', 'url' => 'library/template', 'verb' => 'POST'], /** @see WhiteboardController::update() */ ['name' => 'Whiteboard#update', 'url' => '{fileId}', 'verb' => 'PUT'], /** @see WhiteboardController::show() */ @@ -33,6 +35,12 @@ ['name' => 'Recording#upload', 'url' => 'recording/{fileId}/upload', 'verb' => 'POST'], /** @see SettingsController::update() */ ['name' => 'Settings#update', 'url' => 'settings', 'verb' => 'POST'], + /** @see SettingsController::listGlobalLibraryTemplates() */ + ['name' => 'Settings#listGlobalLibraryTemplates', 'url' => 'settings/global-library', 'verb' => 'GET'], + /** @see SettingsController::uploadGlobalLibraryTemplate() */ + ['name' => 'Settings#uploadGlobalLibraryTemplate', 'url' => 'settings/global-library', 'verb' => 'POST'], + /** @see SettingsController::deleteGlobalLibraryTemplate() */ + ['name' => 'Settings#deleteGlobalLibraryTemplate', 'url' => 'settings/global-library/{templateName}', 'verb' => 'DELETE'], /** @see SettingsController::updatePersonal() */ ['name' => 'Settings#updatePersonal', 'url' => 'settings/personal', 'verb' => 'POST'], ] diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 3ca22b1b..c369a022 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -14,14 +14,17 @@ use OCA\Viewer\Event\LoadViewer; use OCA\Whiteboard\Listener\AddContentSecurityPolicyListener; use OCA\Whiteboard\Listener\BeforeTemplateRenderedListener; +use OCA\Whiteboard\Listener\FileCreatedFromTemplateListener; use OCA\Whiteboard\Listener\LoadTextEditorListener; use OCA\Whiteboard\Listener\LoadViewerListener; use OCA\Whiteboard\Listener\RegisterTemplateCreatorListener; use OCA\Whiteboard\Settings\SetupCheck; +use OCA\Whiteboard\Template\GlobalLibraryTemplateProvider; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\Files\Template\FileCreatedFromTemplateEvent; use OCP\Files\Template\ITemplateManager; use OCP\Files\Template\RegisterTemplateCreatorEvent; use OCP\IL10N; @@ -47,7 +50,9 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(LoadViewer::class, LoadViewerListener::class); $context->registerEventListener(LoadViewer::class, LoadTextEditorListener::class); $context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class); + $context->registerEventListener(FileCreatedFromTemplateEvent::class, FileCreatedFromTemplateListener::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); + $context->registerTemplateProvider(GlobalLibraryTemplateProvider::class); $context->registerSetupCheck(SetupCheck::class); } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 3b46cc88..94386b50 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -13,8 +13,10 @@ use OCA\Whiteboard\Service\ConfigService; use OCA\Whiteboard\Service\ExceptionService; use OCA\Whiteboard\Service\JWTService; +use OCA\Whiteboard\Service\WhiteboardLibraryService; use OCA\Whiteboard\Settings\SetupCheck; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; use OCP\IUserSession; @@ -31,6 +33,7 @@ public function __construct( private ConfigService $configService, private SetupCheck $setupCheck, private IUserSession $userSession, + private WhiteboardLibraryService $libraryService, ) { parent::__construct('whiteboard', $request); } @@ -91,4 +94,44 @@ public function updatePersonal(): DataResponse { return $this->exceptionService->handleException($e); } } + + public function listGlobalLibraryTemplates(): DataResponse { + try { + return new DataResponse($this->libraryService->getGlobalTemplateMetadata()); + } catch (Exception $e) { + return $this->exceptionService->handleException($e); + } + } + + public function uploadGlobalLibraryTemplate(): DataResponse { + try { + $uploadedFile = $this->request->getUploadedFile('file'); + if (!is_array($uploadedFile) || !isset($uploadedFile['tmp_name'], $uploadedFile['name'])) { + throw new Exception('No library template uploaded', Http::STATUS_BAD_REQUEST); + } + if (($uploadedFile['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) { + throw new Exception('Library template upload failed', Http::STATUS_BAD_REQUEST); + } + + $content = file_get_contents($uploadedFile['tmp_name']); + if ($content === false) { + throw new Exception('Failed to read uploaded library template', Http::STATUS_BAD_REQUEST); + } + + return new DataResponse([ + 'template' => $this->libraryService->saveGlobalTemplateFromUpload($uploadedFile['name'], $content), + ], Http::STATUS_CREATED); + } catch (Exception $e) { + return $this->exceptionService->handleException($e); + } + } + + public function deleteGlobalLibraryTemplate(string $templateName): DataResponse { + try { + $this->libraryService->deleteGlobalTemplate($templateName); + return new DataResponse(['status' => 'success']); + } catch (Exception $e) { + return $this->exceptionService->handleException($e); + } + } } diff --git a/lib/Controller/WhiteboardController.php b/lib/Controller/WhiteboardController.php index 9f720c09..bb7ee3c0 100644 --- a/lib/Controller/WhiteboardController.php +++ b/lib/Controller/WhiteboardController.php @@ -10,6 +10,7 @@ namespace OCA\Whiteboard\Controller; use Exception; +use InvalidArgumentException; use OCA\Whiteboard\Exception\InvalidUserException; use OCA\Whiteboard\Exception\UnauthorizedException; use OCA\Whiteboard\Service\Authentication\GetUserFromIdServiceFactory; @@ -110,8 +111,8 @@ public function update(int $fileId, array $data): DataResponse { public function getLib(): DataResponse { try { $jwt = $this->getJwtFromRequest(); - $this->jwtService->getUserIdFromJWT($jwt); - $data = $this->libraryService->getUserLib(); + $userId = $this->jwtService->getUserIdFromJWT($jwt); + $data = $this->libraryService->getTemplates($userId); return new DataResponse(['data' => $data]); } catch (Exception $e) { @@ -126,8 +127,11 @@ public function updateLib(): DataResponse { try { $jwt = $this->getJwtFromRequest(); $userId = $this->jwtService->getUserIdFromJWT($jwt); - $items = $this->request->getParam('items', []); - $this->libraryService->updateUserLib($userId, $items); + $templates = $this->request->getParam('templates'); + if (!is_array($templates)) { + throw new InvalidArgumentException('Library templates payload is required', 400); + } + $this->libraryService->updateUserTemplates($userId, $templates); return new DataResponse(['status' => 'success']); } catch (Exception $e) { @@ -135,6 +139,27 @@ public function updateLib(): DataResponse { } } + #[NoAdminRequired] + #[NoCSRFRequired] + #[PublicPage] + public function saveLibTemplate(): DataResponse { + try { + $jwt = $this->getJwtFromRequest(); + $userId = $this->jwtService->getUserIdFromJWT($jwt); + $templateName = $this->request->getParam('templateName', ''); + $items = $this->request->getParam('items', []); + if (!is_string($templateName) || !is_array($items)) { + throw new InvalidArgumentException('Invalid library template payload', 400); + } + + $template = $this->libraryService->saveUserTemplate($userId, $templateName, $items); + + return new DataResponse(['status' => 'success', 'template' => $template]); + } catch (Exception $e) { + return $this->exceptionService->handleException($e); + } + } + private function getJwtFromRequest(): string { $authHeader = $this->request->getHeader('Authorization'); if (sscanf($authHeader, 'Bearer %s', $jwt) !== 1) { diff --git a/lib/Listener/FileCreatedFromTemplateListener.php b/lib/Listener/FileCreatedFromTemplateListener.php new file mode 100644 index 00000000..975674f7 --- /dev/null +++ b/lib/Listener/FileCreatedFromTemplateListener.php @@ -0,0 +1,182 @@ + */ +/** + * @psalm-suppress MissingTemplateParam + */ +final class FileCreatedFromTemplateListener implements IEventListener { + private const LIB_EXTENSION = '.excalidrawlib'; + private const WHITEBOARD_EXTENSION = '.whiteboard'; + private const VOLATILE_ELEMENT_KEYS = [ + 'id' => true, + 'seed' => true, + 'version' => true, + 'versionNonce' => true, + 'updated' => true, + 'index' => true, + 'groupIds' => true, + 'frameId' => true, + 'boundElements' => true, + 'containerId' => true, + ]; + + public function __construct( + private LoggerInterface $logger, + ) { + } + + #[\Override] + public function handle(Event $event): void { + if (!($event instanceof FileCreatedFromTemplateEvent)) { + return; + } + + $template = $event->getTemplate(); + $target = $event->getTarget(); + if (!($template instanceof File)) { + return; + } + + if (!$this->isLibraryTemplate($template) || !$this->isWhiteboardTarget($target)) { + return; + } + + $libraryItems = $this->parseLibraryItems($template); + if ($libraryItems === []) { + return; + } + + try { + $target->putContent(json_encode([ + 'elements' => [], + 'files' => [], + 'libraryItems' => $libraryItems, + 'scrollToContent' => true, + ], JSON_THROW_ON_ERROR)); + } catch (\Throwable $e) { + $this->logger->warning('Failed to normalize whiteboard created from library template', [ + 'app' => 'whiteboard', + 'template' => $template->getPath(), + 'target' => $target->getPath(), + 'exception' => $e, + ]); + } + } + + private function isLibraryTemplate(File $file): bool { + return str_ends_with(strtolower($file->getName()), self::LIB_EXTENSION); + } + + private function isWhiteboardTarget(File $file): bool { + return str_ends_with(strtolower($file->getName()), self::WHITEBOARD_EXTENSION); + } + + private function parseLibraryItems(File $file): array { + try { + $data = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR); + } catch (\Throwable) { + return []; + } + + if (!is_array($data)) { + return []; + } + + if (isset($data['libraryItems']) && is_array($data['libraryItems'])) { + return $this->normalizeLibraryItems($data['libraryItems']); + } + + if (isset($data['library']) && is_array($data['library'])) { + $items = []; + foreach ($data['library'] as $elements) { + if (!is_array($elements) || count($elements) === 0) { + continue; + } + $items[] = [ + 'id' => $this->createLibraryItemId($elements), + 'elements' => array_values($elements), + 'status' => 'published', + ]; + } + return $this->normalizeLibraryItems($items); + } + + return []; + } + + private function normalizeLibraryItems(array $items): array { + $normalized = []; + $seen = []; + + foreach ($items as $item) { + if (!is_array($item) || !isset($item['elements']) || !is_array($item['elements']) || count($item['elements']) === 0) { + continue; + } + + unset($item['templateName'], $item['scope'], $item['filename'], $item['basename']); + $item['elements'] = array_values($item['elements']); + $key = $this->createLibraryItemId($item['elements']); + if (isset($seen[$key])) { + continue; + } + $seen[$key] = true; + + $item['id'] = isset($item['id']) && is_string($item['id']) && $item['id'] !== '' + ? $item['id'] + : $key; + $item['created'] = isset($item['created']) && is_numeric($item['created']) + ? (int)$item['created'] + : $this->nowMs(); + $item['status'] = isset($item['status']) && is_string($item['status']) + ? $item['status'] + : 'unpublished'; + + $normalized[] = $item; + } + + return $normalized; + } + + private function createLibraryItemId(array $elements): string { + $canonicalElements = $this->canonicalizeLibraryValue($elements); + $encoded = json_encode($canonicalElements); + return substr(hash('sha256', $encoded !== false ? $encoded : serialize($canonicalElements)), 0, 20); + } + + private function canonicalizeLibraryValue(mixed $value): mixed { + if (!is_array($value)) { + return $value; + } + + if (array_is_list($value)) { + return array_map(fn ($item) => $this->canonicalizeLibraryValue($item), $value); + } + + ksort($value); + $normalized = []; + foreach ($value as $key => $item) { + if (is_string($key) && isset(self::VOLATILE_ELEMENT_KEYS[$key])) { + continue; + } + $normalized[$key] = $this->canonicalizeLibraryValue($item); + } + return $normalized; + } + + private function nowMs(): int { + return (int)floor(microtime(true) * 1000); + } +} diff --git a/lib/Service/WhiteboardContentService.php b/lib/Service/WhiteboardContentService.php index 5ba88d87..b89f8402 100644 --- a/lib/Service/WhiteboardContentService.php +++ b/lib/Service/WhiteboardContentService.php @@ -17,6 +17,19 @@ use Psr\Log\LoggerInterface; final class WhiteboardContentService { + private const VOLATILE_ELEMENT_KEYS = [ + 'id' => true, + 'seed' => true, + 'version' => true, + 'versionNonce' => true, + 'updated' => true, + 'index' => true, + 'groupIds' => true, + 'frameId' => true, + 'boundElements' => true, + 'containerId' => true, + ]; + public function __construct( private LoggerInterface $logger, ) { @@ -168,6 +181,10 @@ private function normalizeIncomingData(array $incoming): array { : []; } + if (array_key_exists('libraryItems', $incoming) && is_array($incoming['libraryItems'])) { + $normalized['libraryItems'] = $this->sanitizeLibraryItems($incoming['libraryItems']); + } + if (array_key_exists('appState', $incoming) && is_array($incoming['appState'])) { $normalized['appState'] = $this->sanitizeAppState($incoming['appState']); } @@ -199,6 +216,14 @@ private function isEffectivelyEmptyPayload(array $payload): bool { return false; } + $hasLibraryItems = array_key_exists('libraryItems', $payload) + && is_array($payload['libraryItems']) + && !empty($payload['libraryItems']); + + if ($hasLibraryItems) { + return false; + } + if (array_key_exists('scrollToContent', $payload) && $payload['scrollToContent'] !== true) { return false; } @@ -212,7 +237,7 @@ private function isEffectivelyEmptyPayload(array $payload): bool { } foreach ($payload as $key => $_value) { - if (!in_array($key, ['elements', 'files', 'appState', 'scrollToContent'], true)) { + if (!in_array($key, ['elements', 'files', 'libraryItems', 'appState', 'scrollToContent'], true)) { return false; } } @@ -244,6 +269,10 @@ private function normalizeStoredData(array $stored): array { $normalized['files'] = $this->sanitizeFiles($stored['files']); } + if (array_key_exists('libraryItems', $stored) && is_array($stored['libraryItems'])) { + $normalized['libraryItems'] = $this->sanitizeLibraryItems($stored['libraryItems']); + } + if (array_key_exists('appState', $stored) && is_array($stored['appState'])) { $normalized['appState'] = $this->sanitizeAppState($stored['appState']); } elseif (array_key_exists('appState', $stored) && $stored['appState'] === null) { @@ -274,6 +303,10 @@ private function mergeData(array $current, array $incoming): array { $merged['files'] = $incoming['files']; } + if (array_key_exists('libraryItems', $incoming)) { + $merged['libraryItems'] = $incoming['libraryItems']; + } + if (array_key_exists('appState', $incoming)) { if ($incoming['appState'] === null) { unset($merged['appState']); @@ -331,6 +364,50 @@ private function sanitizeFiles(array $files): array { return $sanitized; } + private function sanitizeLibraryItems(array $items): array { + $sanitized = []; + $seen = []; + + foreach ($items as $item) { + if (!is_array($item) || !isset($item['elements']) || !is_array($item['elements']) || count($item['elements']) === 0) { + continue; + } + + $item['elements'] = array_values($item['elements']); + $canonicalElements = $this->canonicalizeLibraryValue($item['elements']); + $encoded = json_encode($canonicalElements); + $key = hash('sha256', $encoded !== false ? $encoded : serialize($canonicalElements)); + if (isset($seen[$key])) { + continue; + } + $seen[$key] = true; + unset($item['templateName'], $item['scope'], $item['filename'], $item['basename']); + $sanitized[] = $item; + } + + return $sanitized; + } + + private function canonicalizeLibraryValue(mixed $value): mixed { + if (!is_array($value)) { + return $value; + } + + if ($this->isList($value)) { + return array_map(fn ($item) => $this->canonicalizeLibraryValue($item), $value); + } + + ksort($value); + $normalized = []; + foreach ($value as $key => $item) { + if (is_string($key) && isset(self::VOLATILE_ELEMENT_KEYS[$key])) { + continue; + } + $normalized[$key] = $this->canonicalizeLibraryValue($item); + } + return $normalized; + } + /** * @param array $appState * diff --git a/lib/Service/WhiteboardLibraryService.php b/lib/Service/WhiteboardLibraryService.php index de27f938..bde405ca 100644 --- a/lib/Service/WhiteboardLibraryService.php +++ b/lib/Service/WhiteboardLibraryService.php @@ -9,16 +9,22 @@ namespace OCA\Whiteboard\Service; +use Exception; +use InvalidArgumentException; use JsonException; -use OCA\Whiteboard\AppInfo\Application; +use OCP\AppFramework\Http; use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\GenericFileException; +use OCP\Files\IFilenameValidator; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\Template\ITemplateManager; +use OCP\IConfig; use OCP\Lock\LockedException; +use Psr\Log\LoggerInterface; +use RuntimeException; /** * @psalm-suppress UndefinedDocblockClass @@ -26,50 +32,145 @@ * @psalm-suppress MissingDependency */ final class WhiteboardLibraryService { + private const LIB_EXTENSION = '.excalidrawlib'; + private const PERSONAL_TEMPLATE = 'personal'; + private const BOARD_TEMPLATE = 'board'; + private const DEFAULT_TEMPLATE_DIR = 'Templates/'; + private const GLOBAL_TEMPLATE_DIR = 'global-libraries'; + private const GLOBAL_SCOPE = 'global'; + private const USER_SCOPE = 'user'; + private const MAX_FILENAME_BYTES = 250; + private const VOLATILE_ELEMENT_KEYS = [ + 'id' => true, + 'seed' => true, + 'version' => true, + 'versionNonce' => true, + 'updated' => true, + 'index' => true, + 'groupIds' => true, + 'frameId' => true, + 'boundElements' => true, + 'containerId' => true, + ]; + public function __construct( private ITemplateManager $templateManager, private IRootFolder $rootFolder, + private IFilenameValidator $filenameValidator, + private IConfig $config, + private LoggerInterface $logger, ) { } + /** + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + */ + public function getTemplates(string $uid): array { + return [ + 'templates' => $this->listUserTemplates($uid)['templates'], + ]; + } + + /** + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + */ + public function getGlobalTemplateMetadata(): array { + return [ + 'templates' => array_map(static fn (array $template): array => [ + 'templateName' => $template['templateName'], + 'scope' => $template['scope'], + 'itemCount' => count($template['items']), + ], $this->listGlobalTemplates()['templates']), + ]; + } + + /** + * @return array + * + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + */ + public function getGlobalTemplateFiles(): array { + return $this->listGlobalTemplates()['files']; + } + + /** + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + * @throws NotFoundException + */ + public function getGlobalTemplateFile(string $templateName): File { + $normalizedName = $this->normalizeTemplateName($templateName); + $files = $this->getGlobalTemplateFiles(); + $file = $files[$this->toCaseKey($normalizedName)] ?? null; + if (!$file instanceof File) { + throw new NotFoundException('Organization library template not found'); + } + return $file; + } + /** * @throws NotPermittedException * @throws GenericFileException * @throws LockedException * @throws JsonException */ - public function getUserLib(): array { - // Get the .excalidrawlib files from the /Templates directory - $availableFileCreators = $this->templateManager->listTemplates(); - $templates = []; - $libs = []; + public function saveGlobalTemplateFromUpload(string $fileName, string $content): array { + if (!$this->isLibraryFileName($fileName)) { + throw new InvalidArgumentException('Upload an .excalidrawlib file', Http::STATUS_BAD_REQUEST); + } - foreach ($availableFileCreators as $fileCreator) { - if ($fileCreator['app'] !== Application::APP_ID) { - continue; - } - $templates = $fileCreator['templates']; - break; + $templateName = $this->normalizeTemplateName($fileName); + $this->assertGlobalTemplateNameAllowed($templateName); + $caseKey = $this->toCaseKey($templateName); + $current = $this->listGlobalTemplates(); + + if (isset($current['loadedFiles'][$caseKey])) { + throw new RuntimeException('A library template with this name already exists. Rename the file and upload it again.', Http::STATUS_CONFLICT); } - foreach ($templates as $template) { - $templateDetails = $template->jsonSerialize(); + $items = $this->parseLibraryContent($content); + if ($items === null) { + throw new InvalidArgumentException('This is not a valid Excalidraw library file.', Http::STATUS_BAD_REQUEST); + } + if ($items === []) { + throw new InvalidArgumentException('This library has no reusable items. Upload a library with at least one item.', Http::STATUS_BAD_REQUEST); + } - if (str_ends_with($templateDetails['basename'], '.excalidrawlib')) { - $fileId = $templateDetails['fileid']; - $file = $this->rootFolder->getFirstNodeById($fileId); + $this->writeLibraryTemplate($this->getGlobalTemplateFolder(), $templateName, $items); - if (!$file instanceof File) { - continue; - } + return [ + 'templateName' => $templateName, + 'scope' => self::GLOBAL_SCOPE, + 'itemCount' => count($items), + ]; + } - $lib = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR); - $lib['basename'] = $templateDetails['basename']; - $libs[] = $lib; - } + /** + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + * @throws NotFoundException + */ + public function deleteGlobalTemplate(string $templateName): void { + $normalizedName = $this->normalizeTemplateName($templateName); + $current = $this->listGlobalTemplates(); + $fileName = $current['loadedFiles'][$this->toCaseKey($normalizedName)] ?? null; + if (!is_string($fileName)) { + throw new NotFoundException('Organization library template not found'); } - return $libs; + $node = $this->getGlobalTemplateFolder()->get($fileName); + if (!$node instanceof File) { + throw new NotFoundException('Organization library template not found'); + } + $node->delete(); } /** @@ -79,57 +180,523 @@ public function getUserLib(): array { * @throws LockedException * @throws JsonException */ - public function updateUserLib(string $uid, array $items): void { - // Check if the user has a Templates folder, if not create one - if (!$this->templateManager->hasTemplateDirectory()) { - $this->templateManager->initializeTemplateDirectory(null, $uid, false); + public function updateUserTemplates(string $uid, array $templates): void { + $templateFolder = $this->getUserTemplateFolder($uid); + $current = $this->listUserTemplates($uid); + $loadedFiles = $current['loadedFiles']; + $payload = $this->normalizePayloadTemplates($templates, $loadedFiles); + + foreach ($payload as $templateName => $items) { + $this->writeUserTemplate($templateFolder, $templateName, $items); } + } - // Update the .excalidrawlib files in the Templates directory + /** + * @throws NotPermittedException + * @throws NotFoundException + * @throws GenericFileException + * @throws LockedException + * @throws JsonException + */ + public function saveUserTemplate(string $uid, string $templateName, array $items): array { + $templateFolder = $this->getUserTemplateFolder($uid); + $normalizedName = $this->normalizeTemplateName($templateName); + $caseKey = $this->toCaseKey($normalizedName); + $current = $this->listUserTemplates($uid); + $userNames = $current['loadedFiles']; + $normalizedItems = $this->normalizeLibraryItems($items); + + if (isset($userNames[$caseKey])) { + throw new RuntimeException('Library template already exists', Http::STATUS_CONFLICT); + } + + if ($normalizedItems === []) { + throw new InvalidArgumentException('Library template must contain at least one item', Http::STATUS_BAD_REQUEST); + } + + $contentKey = $this->createLibraryTemplateContentKey($normalizedItems); + foreach ($current['templates'] as $template) { + if ($this->createLibraryTemplateContentKey($template['items']) === $contentKey) { + throw new RuntimeException('Library template with same items already exists', Http::STATUS_CONFLICT); + } + } + + $this->writeUserTemplate($templateFolder, $normalizedName, $normalizedItems); + + return [ + 'templateName' => $normalizedName, + 'scope' => self::USER_SCOPE, + 'items' => $normalizedItems, + ]; + } + + /** + * @return array{templates: array, loadedFiles: array} + * + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + */ + private function listUserTemplates(string $uid): array { + $templateFolder = $this->getUserTemplateFolder($uid); + $templates = []; + $loadedFiles = []; + + foreach ($templateFolder->getDirectoryListing() as $node) { + if (!$node instanceof File || !$this->isLibraryFileName($node->getName())) { + continue; + } + + $templateName = $this->stripLibraryExtension($node->getName()); + $loadedFiles[$this->toCaseKey($templateName)] = $node->getName(); + $items = $this->parseLibraryContent($node->getContent()); + if ($items === null) { + $this->logger->warning('Skipping malformed whiteboard library template', [ + 'uid' => $uid, + 'file' => $node->getName(), + ]); + continue; + } + + $templates[] = [ + 'templateName' => $templateName, + 'scope' => self::USER_SCOPE, + 'items' => $items, + ]; + } + + usort($templates, $this->sortTemplates(...)); + + return [ + 'templates' => $templates, + 'loadedFiles' => $loadedFiles, + ]; + } + + /** + * @return array{templates: array, loadedFiles: array, files: array} + * + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + */ + private function listGlobalTemplates(): array { + $templateFolder = $this->getGlobalTemplateFolder(); + $templates = []; + $loadedFiles = []; + $files = []; + + foreach ($templateFolder->getDirectoryListing() as $node) { + if (!$node instanceof File || !$this->isLibraryFileName($node->getName())) { + continue; + } + + $templateName = $this->stripLibraryExtension($node->getName()); + $caseKey = $this->toCaseKey($templateName); + $loadedFiles[$caseKey] = $node->getName(); + $items = $this->parseLibraryContent($node->getContent()); + if ($items === null) { + $this->logger->warning('Skipping malformed organization whiteboard library template', [ + 'file' => $node->getName(), + ]); + continue; + } + + $templates[] = [ + 'templateName' => $templateName, + 'scope' => self::GLOBAL_SCOPE, + 'items' => $items, + ]; + $files[$caseKey] = $node; + } + + usort($templates, $this->sortTemplates(...)); + + return [ + 'templates' => $templates, + 'loadedFiles' => $loadedFiles, + 'files' => $files, + ]; + } + + /** + * @throws NotPermittedException + */ + private function getUserTemplateFolder(string $uid): Folder { $userFolder = $this->rootFolder->getUserFolder($uid); - $templatesPath = $this->templateManager->getTemplatePath(); - $templatesFolder = $userFolder->get($templatesPath); + $configuredPath = $this->normalizeTemplatePath( + $this->config->getUserValue($uid, 'core', 'templateDirectory', '') + ); + $templateFolder = $configuredPath !== null + ? $this->getExistingFolder($userFolder, $configuredPath) + : null; + + if (!$templateFolder instanceof Folder) { + $templatePath = $this->templateManager->initializeTemplateDirectory(self::DEFAULT_TEMPLATE_DIR, $uid, false); + $templateFolder = $this->ensureFolder($userFolder, $this->normalizeTemplatePath($templatePath) ?? self::DEFAULT_TEMPLATE_DIR); + } + + $this->migrateRootPersonalTemplate($userFolder, $templateFolder); + + return $templateFolder; + } + + private function getExistingFolder(Folder $userFolder, string $path): ?Folder { + try { + if (!$userFolder->nodeExists($path)) { + return null; + } + $node = $userFolder->get($path); + return $node instanceof Folder ? $node : null; + } catch (Exception) { + return null; + } + } + + /** + * @throws NotPermittedException + */ + private function getGlobalTemplateFolder(): Folder { + $instanceId = $this->config->getSystemValueString('instanceid', ''); + if ($instanceId === '') { + throw new RuntimeException('No instance id configured'); + } + + $appDataRoot = $this->ensureChildFolder($this->rootFolder, 'appdata_' . $instanceId); + $appFolder = $this->ensureChildFolder($appDataRoot, 'whiteboard'); + return $this->ensureChildFolder($appFolder, self::GLOBAL_TEMPLATE_DIR); + } + + /** + * @throws NotPermittedException + */ + private function ensureChildFolder(Folder $folder, string $name): Folder { + if (!$folder->nodeExists($name)) { + $folder->newFolder($name); + } + + $node = $folder->get($name); + if (!$node instanceof Folder) { + throw new RuntimeException('Expected folder at ' . $name); + } + return $node; + } + + /** + * @throws NotPermittedException + */ + private function ensureFolder(Folder $userFolder, string $path): Folder { + $normalizedPath = $this->normalizeTemplatePath($path) ?? trim(self::DEFAULT_TEMPLATE_DIR, '/'); + if (!$userFolder->nodeExists($normalizedPath)) { + $userFolder->newFolder($normalizedPath); + } + $folder = $userFolder->get($normalizedPath); + if (!$folder instanceof Folder) { + throw new RuntimeException('Template path is not a folder'); + } + return $folder; + } + + private function migrateRootPersonalTemplate(Folder $userFolder, Folder $templateFolder): void { + $fileName = $this->toLibraryFileName(self::PERSONAL_TEMPLATE); + try { + if (!$userFolder->nodeExists($fileName)) { + return; + } + if ($templateFolder->nodeExists($fileName)) { + $this->logger->info('Leaving root personal whiteboard library in place because target exists'); + return; + } + + $source = $userFolder->get($fileName); + if (!$source instanceof File) { + return; + } + if ($this->parseLibraryContent($source->getContent()) === null) { + $this->logger->warning('Leaving malformed root personal whiteboard library in place'); + return; + } + + $targetPath = $templateFolder->getPath() . '/' . $fileName; + try { + $source->move($targetPath); + return; + } catch (Exception $e) { + $this->logger->warning('Failed to move root personal whiteboard library, trying copy/delete', [ + 'exception' => $e->getMessage(), + ]); + } - if (!$templatesFolder instanceof Folder) { - throw new NotFoundException('Templates folder not found for user: ' . $uid); + try { + $source->copy($targetPath); + $source->delete(); + } catch (Exception $e) { + $this->logger->warning('Failed to migrate root personal whiteboard library', [ + 'exception' => $e->getMessage(), + ]); + $this->deleteTargetCopyAfterFailedMigration($templateFolder, $fileName); + } + } catch (Exception $e) { + $this->logger->warning('Failed to inspect root personal whiteboard library', [ + 'exception' => $e->getMessage(), + ]); } + } - $files = [ - 'personal.excalidrawlib' => [ - 'type' => 'excalidrawlib', - 'version' => 2, - 'libraryItems' => [], - ], + private function deleteTargetCopyAfterFailedMigration(Folder $templateFolder, string $fileName): void { + try { + if ($templateFolder->nodeExists($fileName)) { + $node = $templateFolder->get($fileName); + if ($node instanceof File) { + $node->delete(); + } + } + } catch (Exception) { + } + } + + /** + * @throws JsonException + */ + private function writeUserTemplate(Folder $templateFolder, string $templateName, array $items): void { + $this->writeLibraryTemplate($templateFolder, $templateName, $items); + } + + /** + * @throws JsonException + */ + private function writeLibraryTemplate(Folder $templateFolder, string $templateName, array $items): void { + $fileName = $this->toLibraryFileName($templateName); + $encoded = $this->encodeLibraryFile($items); + $file = $templateFolder->nodeExists($fileName) + ? $templateFolder->get($fileName) + : $templateFolder->newFile($fileName); + + if (!$file instanceof File) { + throw new GenericFileException('Failed to create or get file: ' . $fileName); + } + + $file->putContent($encoded); + } + + /** + * @throws JsonException + */ + private function encodeLibraryFile(array $items): string { + $fileData = [ + 'type' => 'excalidrawlib', + 'version' => 2, + 'libraryItems' => $this->normalizeLibraryItems($items), ]; + $encoded = json_encode($fileData, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); + if ($this->parseLibraryContent($encoded) === null) { + throw new InvalidArgumentException('Generated library file is invalid', Http::STATUS_BAD_REQUEST); + } + return $encoded; + } + + private function parseLibraryContent(string $content): ?array { + try { + $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException) { + return null; + } + + if (!is_array($data)) { + return null; + } + + if (array_key_exists('libraryItems', $data)) { + return is_array($data['libraryItems']) ? $this->normalizeLibraryItems($data['libraryItems']) : null; + } + + if (array_key_exists('library', $data)) { + return is_array($data['library']) ? $this->normalizeLegacyLibraryItems($data['library']) : null; + } + + if (array_is_list($data)) { + return $this->normalizeLibraryItems($data); + } + return null; + } + + private function normalizeLegacyLibraryItems(array $libraries): array { + $items = []; + foreach ($libraries as $elements) { + if (!is_array($elements) || count($elements) === 0) { + continue; + } + $items[] = [ + 'id' => $this->createLibraryItemId($elements), + 'created' => $this->nowMs(), + 'status' => 'published', + 'elements' => array_values($elements), + ]; + } + return $this->normalizeLibraryItems($items); + } + + private function normalizeLibraryItems(array $items): array { + $normalized = []; + $seen = []; foreach ($items as $item) { - if (!isset($item['filename'])) { - $files['personal.excalidrawlib']['libraryItems'][] = $item; - } else { - if (isset($files[$item['filename']])) { - $files[$item['filename']]['libraryItems'][] = $item; - } else { - $files[$item['filename']] = [ - 'type' => 'excalidrawlib', - 'version' => 2, - 'libraryItems' => [$item], - ]; - } + if (!is_array($item) || !isset($item['elements']) || !is_array($item['elements']) || count($item['elements']) === 0) { + continue; + } + unset($item['templateName'], $item['scope'], $item['filename'], $item['basename']); + $item['elements'] = array_values($item['elements']); + $contentKey = $this->createLibraryItemId($item['elements']); + if (isset($seen[$contentKey])) { + continue; } + $seen[$contentKey] = true; + $item['id'] = isset($item['id']) && is_string($item['id']) && $item['id'] !== '' + ? $item['id'] + : $contentKey; + $item['created'] = isset($item['created']) && is_numeric($item['created']) + ? (int)$item['created'] + : $this->nowMs(); + $item['status'] = isset($item['status']) && is_string($item['status']) + ? $item['status'] + : 'unpublished'; + $normalized[] = $item; } + return $normalized; + } - foreach ($files as $filename => $fileData) { - if ($templatesFolder->nodeExists($filename)) { - $file = $templatesFolder->get($filename); - } else { - $file = $templatesFolder->newFile($filename); + private function normalizePayloadTemplates(array $templates, array $loadedFiles): array { + $normalized = []; + $seen = []; + + foreach ($templates as $template) { + if (!is_array($template)) { + throw new InvalidArgumentException('Invalid library template payload', Http::STATUS_BAD_REQUEST); + } + if (isset($template['scope']) && $template['scope'] !== 'user') { + throw new InvalidArgumentException('Only user library templates can be updated here', Http::STATUS_BAD_REQUEST); + } + if (!isset($template['templateName']) || !is_string($template['templateName'])) { + throw new InvalidArgumentException('Library templateName is required', Http::STATUS_BAD_REQUEST); } - if (!$file instanceof File) { - throw new GenericFileException('Failed to create or get file: ' . $filename); + $templateName = $this->normalizeTemplateName($template['templateName']); + $caseKey = $this->toCaseKey($templateName); + if ($caseKey !== self::PERSONAL_TEMPLATE) { + throw new InvalidArgumentException('Only the personal library template can be updated here', Http::STATUS_BAD_REQUEST); + } + if (isset($seen[$caseKey])) { + throw new RuntimeException('Duplicate library template name', Http::STATUS_CONFLICT); + } + $seen[$caseKey] = true; + $normalizedName = isset($loadedFiles[$caseKey]) + ? $this->stripLibraryExtension($loadedFiles[$caseKey]) + : $templateName; + $templateItems = $template['items'] ?? []; + $normalized[$normalizedName] = $this->normalizeLibraryItems(is_array($templateItems) ? $templateItems : []); + } + + return $normalized; + } + + private function normalizeTemplateName(string $templateName): string { + $name = trim($templateName); + if ($this->isLibraryFileName($name)) { + $name = trim($this->stripLibraryExtension($name)); + } + + if ($name === '' || $name === '.' || $name === '..') { + throw new InvalidArgumentException('Invalid library template name', Http::STATUS_BAD_REQUEST); + } + if (str_contains($name, '/') || str_contains($name, '\\') || preg_match('/[\x00-\x1F\x7F]/', $name) === 1) { + throw new InvalidArgumentException('Invalid library template name', Http::STATUS_BAD_REQUEST); + } + + $fileName = $this->toLibraryFileName($name); + if (strlen($fileName) > self::MAX_FILENAME_BYTES) { + throw new InvalidArgumentException('Library template name is too long', Http::STATUS_BAD_REQUEST); + } + + try { + $this->filenameValidator->validateFilename($fileName); + } catch (Exception $e) { + throw new InvalidArgumentException('Invalid library template name: ' . $e->getMessage(), Http::STATUS_BAD_REQUEST, $e); + } + + return $name; + } + + private function assertGlobalTemplateNameAllowed(string $templateName): void { + $caseKey = $this->toCaseKey($templateName); + if ($caseKey === self::PERSONAL_TEMPLATE || $caseKey === self::BOARD_TEMPLATE) { + throw new InvalidArgumentException('Reserved library template name', Http::STATUS_BAD_REQUEST); + } + } + + private function normalizeTemplatePath(string $path): ?string { + $normalized = trim($path, '/'); + return $normalized === '' ? null : $normalized; + } + + private function toLibraryFileName(string $templateName): string { + return $templateName . self::LIB_EXTENSION; + } + + private function isLibraryFileName(string $fileName): bool { + return str_ends_with(strtolower($fileName), self::LIB_EXTENSION); + } + + private function stripLibraryExtension(string $fileName): string { + return substr($fileName, 0, -strlen(self::LIB_EXTENSION)); + } + + private function toCaseKey(string $value): string { + return strtolower($value); + } + + private function createLibraryItemId(array $elements): string { + $canonicalElements = $this->canonicalizeLibraryValue($elements); + $encoded = json_encode($canonicalElements); + return substr(hash('sha256', $encoded !== false ? $encoded : serialize($canonicalElements)), 0, 20); + } + + private function createLibraryTemplateContentKey(array $items): string { + $itemKeys = []; + foreach ($items as $item) { + if (!is_array($item) || !isset($item['elements']) || !is_array($item['elements'])) { + continue; } + $itemKeys[] = $this->createLibraryItemId($item['elements']); + } + sort($itemKeys); + return hash('sha256', implode("\n", $itemKeys)); + } + + private function canonicalizeLibraryValue(mixed $value): mixed { + if (!is_array($value)) { + return $value; + } - $file->putContent(json_encode($fileData, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); + if (array_is_list($value)) { + return array_map(fn ($item) => $this->canonicalizeLibraryValue($item), $value); } + + ksort($value); + $normalized = []; + foreach ($value as $key => $item) { + if (is_string($key) && isset(self::VOLATILE_ELEMENT_KEYS[$key])) { + continue; + } + $normalized[$key] = $this->canonicalizeLibraryValue($item); + } + return $normalized; + } + + private function nowMs(): int { + return (int)floor((float)microtime(true) * 1000.0); + } + + private function sortTemplates(array $left, array $right): int { + return strcasecmp($left['templateName'], $right['templateName']); } } diff --git a/lib/Template/GlobalLibraryTemplateProvider.php b/lib/Template/GlobalLibraryTemplateProvider.php new file mode 100644 index 00000000..98f08fbd --- /dev/null +++ b/lib/Template/GlobalLibraryTemplateProvider.php @@ -0,0 +1,71 @@ + new Template(self::class, substr($file->getName(), 0, -strlen('.excalidrawlib')), $file), + array_values($this->libraryService->getGlobalTemplateFiles()) + ); + } catch (Exception $e) { + $this->logger->warning('Failed to list organization whiteboard library templates', [ + 'exception' => $e, + ]); + return []; + } + } + + /** + * @throws NotFoundException + */ + #[\Override] + public function getCustomTemplate(string $template): File { + try { + return $this->libraryService->getGlobalTemplateFile($template); + } catch (NotFoundException $e) { + throw $e; + } catch (Exception $e) { + $this->logger->warning('Failed to load organization whiteboard library template', [ + 'template' => $template, + 'exception' => $e, + ]); + throw new NotFoundException('Organization library template not found'); + } + } +} diff --git a/package-lock.json b/package-lock.json index 3f337772..d17d3765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,10 @@ "@nextcloud/capabilities": "^1.2.1", "@nextcloud/dialogs": "^6.4.1", "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/excalidraw": "^0.18.0-bdcb957", + "@nextcloud/excalidraw": "file:../../../../../nextcloud-deps-excalidraw/packages/excalidraw", + "@nextcloud/excalidraw-common": "file:../../../../../nextcloud-deps-excalidraw/packages/common", + "@nextcloud/excalidraw-element": "file:../../../../../nextcloud-deps-excalidraw/packages/element", + "@nextcloud/excalidraw-math": "file:../../../../../nextcloud-deps-excalidraw/packages/math", "@nextcloud/files": "^4.0.0", "@nextcloud/initial-state": "^3.0.0", "@nextcloud/l10n": "^3.4.1", @@ -77,6 +80,104 @@ "npm": "^11.3.0" } }, + "../../../../../nextcloud-deps-excalidraw/packages/common": { + "name": "@nextcloud/excalidraw-common", + "version": "0.18.0", + "license": "MIT", + "dependencies": { + "tinycolor2": "1.6.0" + }, + "devDependencies": { + "@types/tinycolor2": "1.4.6" + } + }, + "../../../../../nextcloud-deps-excalidraw/packages/element": { + "name": "@nextcloud/excalidraw-element", + "version": "0.18.0", + "license": "MIT", + "dependencies": { + "@nextcloud/excalidraw-common": "0.18.0", + "@nextcloud/excalidraw-math": "0.18.0" + } + }, + "../../../../../nextcloud-deps-excalidraw/packages/excalidraw": { + "name": "@nextcloud/excalidraw", + "version": "0.18.0", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "6.0.2", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@excalidraw/laser-pointer": "1.3.1", + "@excalidraw/mermaid-to-excalidraw": "2.2.2", + "@excalidraw/random-username": "1.1.0", + "@lezer/highlight": "^1.0.0", + "@nextcloud/excalidraw-common": "0.18.0", + "@nextcloud/excalidraw-element": "0.18.0", + "@nextcloud/excalidraw-math": "0.18.0", + "browser-fs-access": "0.38.0", + "canvas-roundrect-polyfill": "0.0.1", + "clsx": "1.1.1", + "cross-env": "7.0.3", + "es6-promise-pool": "2.5.0", + "fractional-indexing": "3.2.0", + "fuzzy": "0.1.3", + "image-blob-reduce": "3.0.1", + "jotai": "2.11.0", + "jotai-scope": "0.7.2", + "lodash.debounce": "4.0.8", + "lodash.throttle": "4.1.1", + "nanoid": "3.3.3", + "pako": "2.0.3", + "perfect-freehand": "1.2.0", + "pica": "7.1.1", + "png-chunk-text": "1.0.0", + "png-chunks-encode": "1.0.0", + "png-chunks-extract": "1.0.0", + "points-on-curve": "1.0.1", + "pwacompat": "2.0.17", + "radix-ui": "1.4.3", + "roughjs": "4.6.4", + "sass": "1.51.0", + "tunnel-rat": "0.1.2" + }, + "devDependencies": { + "@size-limit/preset-big-lib": "9.0.0", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.6.3", + "@testing-library/react": "16.2.0", + "@types/lodash.debounce": "4.0.8", + "@types/pako": "2.0.3", + "@types/pica": "5.1.3", + "@types/resize-observer-browser": "0.1.7", + "ansicolor": "2.0.3", + "autoprefixer": "10.4.7", + "cross-env": "7.0.3", + "dotenv": "16.0.1", + "esbuild": "0.19.10", + "esbuild-sass-plugin": "2.16.0", + "eslint-plugin-react": "7.32.2", + "fake-indexeddb": "3.1.7", + "fonteditor-core": "2.4.1", + "harfbuzzjs": "0.3.6", + "jest-diff": "29.7.0", + "typescript": "5.9.3" + }, + "peerDependencies": { + "react": "^17.0.2 || ^18.2.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0" + } + }, + "../../../../../nextcloud-deps-excalidraw/packages/math": { + "name": "@nextcloud/excalidraw-math", + "version": "0.18.0", + "license": "MIT", + "dependencies": { + "@nextcloud/excalidraw-common": "0.18.0" + } + }, "node_modules/@adobe/css-tools": { "version": "4.3.3", "dev": true, @@ -413,10 +514,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/@braintree/sanitize-url": { - "version": "6.0.2", - "license": "MIT" - }, "node_modules/@buttercup/fetch": { "version": "0.2.1", "license": "MIT", @@ -1123,10 +1220,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@excalidraw/laser-pointer": { - "version": "1.3.1", - "license": "MIT" - }, "node_modules/@excalidraw/markdown-to-text": { "version": "0.1.2", "license": "MIT" @@ -1159,13 +1252,6 @@ "node": "^14 || ^16 || >=18" } }, - "node_modules/@excalidraw/random-username": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/@file-type/xml": { "version": "0.4.3", "license": "MIT", @@ -1189,17 +1275,6 @@ "@floating-ui/utils": "^0.2.10" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.4", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.2" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@floating-ui/utils": { "version": "0.2.10", "license": "MIT" @@ -2070,2125 +2145,871 @@ } }, "node_modules/@nextcloud/excalidraw": { - "version": "0.18.0-bdcb957", - "resolved": "https://registry.npmjs.org/@nextcloud/excalidraw/-/excalidraw-0.18.0-bdcb957.tgz", - "integrity": "sha512-5RCLHdd9G5gmUavL7hv6Q2YEvo0T1qfCZNFtsZB++3kzJ9vaUPaG1ILrvcHt1yl39kcM9vHz4LXOTRuByE3ASQ==", - "license": "MIT", + "resolved": "../../../../../nextcloud-deps-excalidraw/packages/excalidraw", + "link": true + }, + "node_modules/@nextcloud/excalidraw-common": { + "resolved": "../../../../../nextcloud-deps-excalidraw/packages/common", + "link": true + }, + "node_modules/@nextcloud/excalidraw-element": { + "resolved": "../../../../../nextcloud-deps-excalidraw/packages/element", + "link": true + }, + "node_modules/@nextcloud/excalidraw-math": { + "resolved": "../../../../../nextcloud-deps-excalidraw/packages/math", + "link": true + }, + "node_modules/@nextcloud/files": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.0.0.tgz", + "integrity": "sha512-TmecnZIS+PGWGtRh7RpGEboCT4K6iTbHULUcfR6hs3eEzjDVsCc1Ldf8popGY/70lbpdlfYle8xbXnPIo3qaXA==", + "license": "AGPL-3.0-or-later", "dependencies": { - "@braintree/sanitize-url": "6.0.2", - "@excalidraw/laser-pointer": "1.3.1", - "@excalidraw/mermaid-to-excalidraw": "1.1.2", - "@excalidraw/random-username": "1.1.0", - "@radix-ui/react-popover": "1.1.6", - "@radix-ui/react-tabs": "1.0.2", - "browser-fs-access": "0.29.1", - "canvas-roundrect-polyfill": "0.0.1", - "clsx": "1.1.1", - "cross-env": "7.0.3", - "es6-promise-pool": "2.5.0", - "fractional-indexing": "3.2.0", - "fuzzy": "0.1.3", - "image-blob-reduce": "3.0.1", - "jotai": "2.11.0", - "jotai-scope": "0.7.2", - "lodash.debounce": "4.0.8", - "lodash.throttle": "4.1.1", - "nanoid": "3.3.3", - "open-color": "1.9.1", - "pako": "2.0.3", - "perfect-freehand": "1.2.0", - "pica": "7.1.1", - "png-chunk-text": "1.0.0", - "png-chunks-encode": "1.0.0", - "png-chunks-extract": "1.0.0", - "points-on-curve": "1.0.1", - "pwacompat": "2.0.17", - "roughjs": "4.6.4", - "sass": "1.51.0", - "tunnel-rat": "0.1.2" + "@nextcloud/auth": "^2.5.3", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/paths": "^3.0.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.3.0", + "is-svg": "^6.1.0", + "typescript-event-target": "^1.1.2", + "webdav": "^5.9.0" }, - "peerDependencies": { - "react": "^17.0.2 || ^18.2.0 || ^19.0.0", - "react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0" + "engines": { + "node": "^24.0.0" } }, - "node_modules/@nextcloud/excalidraw/node_modules/@excalidraw/mermaid-to-excalidraw": { - "version": "1.1.2", + "node_modules/@nextcloud/files/node_modules/@nextcloud/files": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", + "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", + "license": "AGPL-3.0-or-later", + "optional": true, "dependencies": { - "@excalidraw/markdown-to-text": "0.1.2", - "mermaid": "10.9.3", - "nanoid": "4.0.2" + "@nextcloud/auth": "^2.5.3", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/paths": "^3.0.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.3.0", + "cancelable-promise": "^4.3.1", + "is-svg": "^6.1.0", + "typescript-event-target": "^1.1.1", + "webdav": "^5.8.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, - "node_modules/@nextcloud/excalidraw/node_modules/@excalidraw/mermaid-to-excalidraw/node_modules/nanoid": { - "version": "4.0.2", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, + "node_modules/@nextcloud/files/node_modules/@nextcloud/paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-3.0.0.tgz", + "integrity": "sha512-+sTfTkIbVUa2Ue3bkz3R7F1mhddvHPOWUxkSNg7Q5dAsimVFBaTRgiBAJmsAag3JPsxyuS8kUgeb0zdEssRdTA==", + "license": "GPL-3.0-or-later", "engines": { - "node": "^14 || ^16 || >=18" + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, - "node_modules/@nextcloud/excalidraw/node_modules/@types/mdast": { - "version": "3.0.15", - "license": "MIT", + "node_modules/@nextcloud/files/node_modules/@nextcloud/sharing": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", + "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", + "license": "GPL-3.0-or-later", "dependencies": { - "@types/unist": "^2" + "@nextcloud/initial-state": "^3.0.0", + "is-svg": "^6.1.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + }, + "optionalDependencies": { + "@nextcloud/files": "^3.12.0" } }, - "node_modules/@nextcloud/excalidraw/node_modules/@types/unist": { - "version": "2.0.11", - "license": "MIT" - }, - "node_modules/@nextcloud/excalidraw/node_modules/dompurify": { - "version": "3.1.6", - "license": "(MPL-2.0 OR Apache-2.0)" - }, - "node_modules/@nextcloud/excalidraw/node_modules/immutable": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", - "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", - "license": "MIT" + "node_modules/@nextcloud/initial-state": { + "version": "3.0.0", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } }, - "node_modules/@nextcloud/excalidraw/node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "license": "MIT", + "node_modules/@nextcloud/l10n": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-3.4.1.tgz", + "integrity": "sha512-aTFinTcKiK2gEXwLgutXekpZZ8/v/4QiC8C3QCLH5m0o+WtxsBC+fqV142ebC/rfDnzCLhY4ZtswSu8bFbZocg==", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" + "@nextcloud/router": "^3.0.1", + "@nextcloud/typings": "^1.9.1", + "@types/escape-html": "^1.0.4", + "dompurify": "^3.2.6", + "escape-html": "^1.0.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^20 || ^22 || ^24" } }, - "node_modules/@nextcloud/excalidraw/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "license": "MIT", + "node_modules/@nextcloud/logger": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nextcloud/logger/-/logger-3.0.3.tgz", + "integrity": "sha512-TcbVRL4/O5ffI1RXFmQAFD3gwwT15AAdr1770x+RNqVvfBdoGVyhzOwCIyA5Vfc3fA1iJXFa+rE6buJZSoqlcw==", + "license": "GPL-3.0-or-later", "dependencies": { - "@types/mdast": "^3.0.0" + "@nextcloud/auth": "^2.5.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, - "node_modules/@nextcloud/excalidraw/node_modules/mermaid": { - "version": "10.9.3", - "license": "MIT", - "dependencies": { - "@braintree/sanitize-url": "^6.0.1", - "@types/d3-scale": "^4.0.3", - "@types/d3-scale-chromatic": "^3.0.0", - "cytoscape": "^3.28.1", - "cytoscape-cose-bilkent": "^4.1.0", - "d3": "^7.4.0", - "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.10", - "dayjs": "^1.11.7", - "dompurify": "^3.0.5 <3.1.7", - "elkjs": "^0.9.0", - "katex": "^0.16.9", - "khroma": "^2.0.0", - "lodash-es": "^4.17.21", - "mdast-util-from-markdown": "^1.3.0", - "non-layered-tidy-tree-layout": "^2.0.2", - "stylis": "^4.1.3", - "ts-dedent": "^2.2.0", - "uuid": "^9.0.0", - "web-worker": "^1.2.0" + "node_modules/@nextcloud/paths": { + "version": "2.2.1", + "dev": true, + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" } }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark": { - "version": "3.2.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", + "node_modules/@nextcloud/router": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.1.0.tgz", + "integrity": "sha512-e4dkIaxRSwdZJlZFpn9x03QgBn/Sa2hN1hp/BA7+AbzykmSAlKuWfdmX8j/8ewrLpQwYmZR23IZO9XwpJXq2Uw==", + "license": "GPL-3.0-or-later", "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-factory-destination": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-factory-label": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-factory-space": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-factory-title": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-character": { - "version": "1.2.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-chunked": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-encode": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/@nextcloud/excalidraw/node_modules/micromark-util-types": { - "version": "1.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/@nextcloud/excalidraw/node_modules/nanoid": { - "version": "3.3.3", - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/pako": { - "version": "2.0.3", - "license": "(MIT AND Zlib)" - }, - "node_modules/@nextcloud/excalidraw/node_modules/sass": { - "version": "1.51.0", - "license": "MIT", - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@nextcloud/excalidraw/node_modules/uuid": { - "version": "9.0.1", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@nextcloud/files": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.0.0.tgz", - "integrity": "sha512-TmecnZIS+PGWGtRh7RpGEboCT4K6iTbHULUcfR6hs3eEzjDVsCc1Ldf8popGY/70lbpdlfYle8xbXnPIo3qaXA==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.5.3", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.0.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "is-svg": "^6.1.0", - "typescript-event-target": "^1.1.2", - "webdav": "^5.9.0" - }, - "engines": { - "node": "^24.0.0" - } - }, - "node_modules/@nextcloud/files/node_modules/@nextcloud/files": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", - "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", - "license": "AGPL-3.0-or-later", - "optional": true, - "dependencies": { - "@nextcloud/auth": "^2.5.3", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.0.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "cancelable-promise": "^4.3.1", - "is-svg": "^6.1.0", - "typescript-event-target": "^1.1.1", - "webdav": "^5.8.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/files/node_modules/@nextcloud/paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-3.0.0.tgz", - "integrity": "sha512-+sTfTkIbVUa2Ue3bkz3R7F1mhddvHPOWUxkSNg7Q5dAsimVFBaTRgiBAJmsAag3JPsxyuS8kUgeb0zdEssRdTA==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/files/node_modules/@nextcloud/sharing": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", - "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.0" - } - }, - "node_modules/@nextcloud/initial-state": { - "version": "3.0.0", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/l10n": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-3.4.1.tgz", - "integrity": "sha512-aTFinTcKiK2gEXwLgutXekpZZ8/v/4QiC8C3QCLH5m0o+WtxsBC+fqV142ebC/rfDnzCLhY4ZtswSu8bFbZocg==", - "dependencies": { - "@nextcloud/router": "^3.0.1", - "@nextcloud/typings": "^1.9.1", - "@types/escape-html": "^1.0.4", - "dompurify": "^3.2.6", - "escape-html": "^1.0.3" - }, - "engines": { - "node": "^20 || ^22 || ^24" - } - }, - "node_modules/@nextcloud/logger": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@nextcloud/logger/-/logger-3.0.3.tgz", - "integrity": "sha512-TcbVRL4/O5ffI1RXFmQAFD3gwwT15AAdr1770x+RNqVvfBdoGVyhzOwCIyA5Vfc3fA1iJXFa+rE6buJZSoqlcw==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.5.3" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/paths": { - "version": "2.2.1", - "dev": true, - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - } - }, - "node_modules/@nextcloud/router": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.1.0.tgz", - "integrity": "sha512-e4dkIaxRSwdZJlZFpn9x03QgBn/Sa2hN1hp/BA7+AbzykmSAlKuWfdmX8j/8ewrLpQwYmZR23IZO9XwpJXq2Uw==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/typings": "^1.10.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/sharing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", - "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.2 || ^4.0.0" - } - }, - "node_modules/@nextcloud/stylelint-config": { - "version": "3.1.1", - "dev": true, - "license": "AGPL-3.0-or-later", - "dependencies": { - "stylelint-use-logical": "^2.1.2" - }, - "engines": { - "node": "^20 || ^22 || ^24" - }, - "peerDependencies": { - "stylelint": "^16.13.2", - "stylelint-config-recommended-scss": "^15.0.1", - "stylelint-config-recommended-vue": "^1.5.0" - } - }, - "node_modules/@nextcloud/timezones": { - "version": "0.2.0", - "license": "AGPL-3.0-or-later", - "dependencies": { - "ical.js": "^2.1.0" - }, - "engines": { - "node": "^20 || ^22" - } - }, - "node_modules/@nextcloud/typings": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.10.0.tgz", - "integrity": "sha512-SMC42rDjOH3SspPTLMZRv76ZliHpj2JJkF8pGLP8l1QrVTZxE47Qz5qeKmbj2VL+dRv2e/NgixlAFmzVnxkhqg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@types/jquery": "3.5.16" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/vite-config": { - "version": "1.6.0", - "dev": true, - "license": "AGPL-3.0-or-later", - "dependencies": { - "@rollup/plugin-replace": "^6.0.2", - "@vitejs/plugin-vue2": "^2.3.3", - "browserslist-to-esbuild": "^2.1.1", - "magic-string": "^0.30.17", - "rollup-plugin-corejs": "^1.0.1", - "rollup-plugin-esbuild-minify": "^1.3.0", - "rollup-plugin-license": "^3.6.0", - "rollup-plugin-node-externals": "^8.0.1", - "spdx-expression-parse": "^4.0.0", - "vite-plugin-css-injected-by-js": "^3.5.2", - "vite-plugin-dts": "^4.5.4", - "vite-plugin-node-polyfills": "^0.24.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0", - "npm": "^10.0.0" - }, - "peerDependencies": { - "browserslist": ">=4.0", - "sass": ">=1.60", - "vite": "^5 || ^6 || ^7" - } - }, - "node_modules/@nextcloud/vue": { - "version": "8.32.0", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@floating-ui/dom": "^1.7.4", - "@linusborg/vue-simple-portal": "^0.1.5", - "@nextcloud/auth": "^2.5.2", - "@nextcloud/axios": "^2.5.2", - "@nextcloud/browser-storage": "^0.4.0", - "@nextcloud/capabilities": "^1.2.0", - "@nextcloud/event-bus": "^3.3.2", - "@nextcloud/initial-state": "^2.2.0", - "@nextcloud/l10n": "^3.4.0", - "@nextcloud/logger": "^3.0.2", - "@nextcloud/router": "^3.0.1", - "@nextcloud/sharing": "^0.3.0", - "@nextcloud/timezones": "^0.2.0", - "@nextcloud/vue-select": "^3.26.0", - "@vueuse/components": "^11.0.0", - "@vueuse/core": "^11.0.0", - "blurhash": "^2.0.5", - "clone": "^2.1.2", - "debounce": "^2.2.0", - "dompurify": "^3.2.7", - "emoji-mart-vue-fast": "^15.0.5", - "escape-html": "^1.0.3", - "floating-vue": "^1.0.0-beta.19", - "focus-trap": "^7.4.3", - "linkify-string": "^4.3.2", - "md5": "^2.3.0", - "p-queue": "^8.1.1", - "rehype-external-links": "^3.0.0", - "rehype-highlight": "^7.0.2", - "rehype-react": "^7.1.2", - "remark-breaks": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "remark-unlink-protocols": "^1.0.0", - "splitpanes": "^2.4.1", - "string-length": "^5.0.1", - "striptags": "^3.2.0", - "tabbable": "^6.2.0", - "tributejs": "^5.1.3", - "unified": "^11.0.1", - "unist-builder": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vue": "^2.7.16", - "vue-color": "^2.8.1", - "vue-frag": "^1.4.3", - "vue-router": "^3.6.5", - "vue2-datepicker": "^3.11.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/vue-select": { - "version": "3.26.0", - "license": "MIT", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "peerDependencies": { - "vue": "2.x" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/files": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", - "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", - "license": "AGPL-3.0-or-later", - "optional": true, - "dependencies": { - "@nextcloud/auth": "^2.5.3", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.0.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "cancelable-promise": "^4.3.1", - "is-svg": "^6.1.0", - "typescript-event-target": "^1.1.1", - "webdav": "^5.8.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/initial-state": { - "version": "2.2.0", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-3.0.0.tgz", - "integrity": "sha512-+sTfTkIbVUa2Ue3bkz3R7F1mhddvHPOWUxkSNg7Q5dAsimVFBaTRgiBAJmsAag3JPsxyuS8kUgeb0zdEssRdTA==", - "license": "GPL-3.0-or-later", - "optional": true, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", - "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing/node_modules/@nextcloud/initial-state": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", - "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "eslint-scope": "5.1.1" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@playwright/test": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", - "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.59.1" - }, - "bin": { - "playwright": "cli.js" + "@nextcloud/typings": "^1.10.0" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@puppeteer/browsers": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", - "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", - "license": "Apache-2.0", + "node_modules/@nextcloud/sharing": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", + "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", + "license": "GPL-3.0-or-later", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, - "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "@nextcloud/initial-state": "^3.0.0", + "is-svg": "^6.1.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" }, "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-primitive": "1.0.1", - "@radix-ui/react-slot": "1.0.1" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "@nextcloud/files": "^3.12.2 || ^4.0.0" } }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { - "version": "1.0.0", - "license": "MIT", + "node_modules/@nextcloud/stylelint-config": { + "version": "3.1.1", + "dev": true, + "license": "AGPL-3.0-or-later", "dependencies": { - "@babel/runtime": "^7.13.10" + "stylelint-use-logical": "^2.1.2" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.1" + "engines": { + "node": "^20 || ^22 || ^24" }, "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "stylelint": "^16.13.2", + "stylelint-config-recommended-scss": "^15.0.1", + "stylelint-config-recommended-vue": "^1.5.0" } }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.0.1", - "license": "MIT", + "node_modules/@nextcloud/timezones": { + "version": "0.2.0", + "license": "AGPL-3.0-or-later", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "ical.js": "^2.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": "^20 || ^22" } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.0.0", - "license": "MIT", + "node_modules/@nextcloud/typings": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.10.0.tgz", + "integrity": "sha512-SMC42rDjOH3SspPTLMZRv76ZliHpj2JJkF8pGLP8l1QrVTZxE47Qz5qeKmbj2VL+dRv2e/NgixlAFmzVnxkhqg==", + "license": "GPL-3.0-or-later", "dependencies": { - "@babel/runtime": "^7.13.10" + "@types/jquery": "3.5.16" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.5", - "license": "MIT", + "node_modules/@nextcloud/vite-config": { + "version": "1.6.0", + "dev": true, + "license": "AGPL-3.0-or-later", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@rollup/plugin-replace": "^6.0.2", + "@vitejs/plugin-vue2": "^2.3.3", + "browserslist-to-esbuild": "^2.1.1", + "magic-string": "^0.30.17", + "rollup-plugin-corejs": "^1.0.1", + "rollup-plugin-esbuild-minify": "^1.3.0", + "rollup-plugin-license": "^3.6.0", + "rollup-plugin-node-externals": "^8.0.1", + "spdx-expression-parse": "^4.0.0", + "vite-plugin-css-injected-by-js": "^3.5.2", + "vite-plugin-dts": "^4.5.4", + "vite-plugin-node-polyfills": "^0.24.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0", + "npm": "^10.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "browserslist": ">=4.0", + "sass": ">=1.60", + "vite": "^5 || ^6 || ^7" } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "license": "MIT", + "node_modules/@nextcloud/vue": { + "version": "8.32.0", + "license": "AGPL-3.0-or-later", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@floating-ui/dom": "^1.7.4", + "@linusborg/vue-simple-portal": "^0.1.5", + "@nextcloud/auth": "^2.5.2", + "@nextcloud/axios": "^2.5.2", + "@nextcloud/browser-storage": "^0.4.0", + "@nextcloud/capabilities": "^1.2.0", + "@nextcloud/event-bus": "^3.3.2", + "@nextcloud/initial-state": "^2.2.0", + "@nextcloud/l10n": "^3.4.0", + "@nextcloud/logger": "^3.0.2", + "@nextcloud/router": "^3.0.1", + "@nextcloud/sharing": "^0.3.0", + "@nextcloud/timezones": "^0.2.0", + "@nextcloud/vue-select": "^3.26.0", + "@vueuse/components": "^11.0.0", + "@vueuse/core": "^11.0.0", + "blurhash": "^2.0.5", + "clone": "^2.1.2", + "debounce": "^2.2.0", + "dompurify": "^3.2.7", + "emoji-mart-vue-fast": "^15.0.5", + "escape-html": "^1.0.3", + "floating-vue": "^1.0.0-beta.19", + "focus-trap": "^7.4.3", + "linkify-string": "^4.3.2", + "md5": "^2.3.0", + "p-queue": "^8.1.1", + "rehype-external-links": "^3.0.0", + "rehype-highlight": "^7.0.2", + "rehype-react": "^7.1.2", + "remark-breaks": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "remark-unlink-protocols": "^1.0.0", + "splitpanes": "^2.4.1", + "string-length": "^5.0.1", + "striptags": "^3.2.0", + "tabbable": "^6.2.0", + "tributejs": "^5.1.3", + "unified": "^11.0.1", + "unist-builder": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vue": "^2.7.16", + "vue-color": "^2.8.1", + "vue-frag": "^1.4.3", + "vue-router": "^3.6.5", + "vue2-datepicker": "^3.11.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.0", + "node_modules/@nextcloud/vue-select": { + "version": "3.26.0", "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "vue": "2.x" } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.6", - "license": "MIT", + "node_modules/@nextcloud/vue/node_modules/@nextcloud/files": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", + "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", + "license": "AGPL-3.0-or-later", + "optional": true, "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@nextcloud/auth": "^2.5.3", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/paths": "^3.0.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.3.0", + "cancelable-promise": "^4.3.1", + "is-svg": "^6.1.0", + "typescript-event-target": "^1.1.1", + "webdav": "^5.8.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.2", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@nextcloud/vue/node_modules/@nextcloud/initial-state": { + "version": "2.2.0", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@nextcloud/vue/node_modules/@nextcloud/paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-3.0.0.tgz", + "integrity": "sha512-+sTfTkIbVUa2Ue3bkz3R7F1mhddvHPOWUxkSNg7Q5dAsimVFBaTRgiBAJmsAag3JPsxyuS8kUgeb0zdEssRdTA==", + "license": "GPL-3.0-or-later", + "optional": true, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.2", - "license": "MIT", + "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", + "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", + "license": "GPL-3.0-or-later", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.0" + "@nextcloud/initial-state": "^3.0.0", + "is-svg": "^6.1.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@nextcloud/files": "^3.12.0" } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing/node_modules/@nextcloud/initial-state": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", + "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.2", + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-collection": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-direction": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-primitive": "1.0.1", - "@radix-ui/react-use-callback-ref": "1.0.0", - "@radix-ui/react-use-controllable-state": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "eslint-scope": "5.1.1" } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": { - "version": "1.0.0", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.0", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "peer": true, + "engines": { + "node": ">= 8" } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { - "version": "1.0.0", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">= 8" } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-id": { - "version": "1.0.0", + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "peer": true, + "engines": { + "node": ">=12.4.0" } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { - "version": "1.0.1", + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.1" + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { - "version": "1.0.1", + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.0", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.0", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.0", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.1.2", + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.0.2", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-direction": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-presence": "1.0.0", - "@radix-ui/react-primitive": "1.0.1", - "@radix-ui/react-roving-focus": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { - "version": "1.0.0", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.0", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { - "version": "1.0.0", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": { - "version": "1.0.0", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { - "version": "1.0.0", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-use-layout-effect": "1.0.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { - "version": "1.0.1", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.1" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { - "version": "1.0.1", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.0", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "optional": true, + "engines": { + "node": ">=14" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.0", - "license": "MIT", + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.0" + "playwright": "1.59.1" }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.0", - "license": "MIT", + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" } }, - "node_modules/@radix-ui/react-use-callback-ref": { + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { "version": "1.1.0", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "dev": true, + "license": "BSD-3-Clause" }, - "node_modules/@radix-ui/react-use-controllable-state": { + "node_modules/@protobufjs/pool": { "version": "1.1.0", - "license": "MIT", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "bin": { + "browsers": "lib/cjs/main-cli.js" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18" } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, + "node_modules/@puppeteer/browsers/node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react-native-b4a": "*" }, "peerDependenciesMeta": { - "@types/react": { + "react-native-b4a": { "optional": true } } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=10" } }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.0", + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.0", + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, - "node_modules/@radix-ui/rect": { - "version": "1.1.0", - "license": "MIT" - }, "node_modules/@redis/bloom": { "version": "1.2.0", "license": "MIT", @@ -5399,7 +4220,7 @@ }, "node_modules/@types/react-dom": { "version": "18.3.7", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -6437,6 +5258,7 @@ }, "node_modules/anymatch": { "version": "3.1.3", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -6450,6 +5272,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -6473,16 +5296,6 @@ "license": "Python-2.0", "peer": true }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "dev": true, @@ -6899,6 +5712,7 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6999,6 +5813,7 @@ }, "node_modules/braces": { "version": "3.0.3", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -7012,10 +5827,6 @@ "dev": true, "license": "MIT" }, - "node_modules/browser-fs-access": { - "version": "0.29.1", - "license": "Apache-2.0" - }, "node_modules/browser-resolve": { "version": "2.0.0", "dev": true, @@ -7397,10 +6208,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas-roundrect-polyfill": { - "version": "0.0.1", - "license": "MIT" - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -7477,6 +6284,7 @@ }, "node_modules/chokidar": { "version": "3.6.0", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -7499,6 +6307,7 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7560,13 +6369,6 @@ "node": ">=0.8" } }, - "node_modules/clsx": { - "version": "1.1.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/cluster-key-slot": { "version": "1.1.2", "license": "Apache-2.0", @@ -7794,13 +6596,6 @@ "node": ">=10.0.0" } }, - "node_modules/crc-32": { - "version": "0.3.0", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/create-ecdh": { "version": "4.0.4", "dev": true, @@ -7845,24 +6640,9 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-env": { - "version": "7.0.3", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8354,14 +7134,6 @@ "node": ">=12" } }, - "node_modules/dagre-d3-es": { - "version": "7.0.10", - "license": "MIT", - "dependencies": { - "d3": "^7.8.2", - "lodash-es": "^4.17.21" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "license": "MIT", @@ -8583,10 +7355,6 @@ "node": ">=0.10" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "license": "MIT", @@ -8610,15 +7378,6 @@ "integrity": "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==", "license": "Apache-2.0" }, - "node_modules/diff": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", - "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diffie-hellman": { "version": "5.0.3", "dev": true, @@ -8832,10 +7591,6 @@ "dev": true, "license": "ISC" }, - "node_modules/elkjs": { - "version": "0.9.3", - "license": "EPL-2.0" - }, "node_modules/elliptic": { "version": "6.6.1", "dev": true, @@ -9187,13 +7942,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-promise-pool": { - "version": "2.5.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/esbuild": { "version": "0.25.5", "dev": true, @@ -10283,6 +9031,7 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -10583,13 +9332,6 @@ "node": ">= 0.6" } }, - "node_modules/fractional-indexing": { - "version": "3.2.0", - "license": "CC0-1.0", - "engines": { - "node": "^14.13.1 || >=16.0.0" - } - }, "node_modules/fresh": { "version": "2.0.0", "license": "MIT", @@ -10624,6 +9366,7 @@ }, "node_modules/fsevents": { "version": "2.3.3", + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10669,12 +9412,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fuzzy": { - "version": "0.1.3", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/generic-pool": { "version": "2.0.4", "engines": { @@ -10718,13 +9455,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/get-port": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", @@ -10951,10 +9681,6 @@ "license": "MIT", "peer": true }, - "node_modules/glur": { - "version": "1.1.2", - "license": "MIT" - }, "node_modules/gopd": { "version": "1.2.0", "license": "MIT", @@ -11317,13 +10043,6 @@ "dev": true, "license": "ISC" }, - "node_modules/image-blob-reduce": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "pica": "^7.1.0" - } - }, "node_modules/image-size": { "version": "0.5.5", "dev": true, @@ -11522,6 +10241,7 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -11646,6 +10366,7 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11692,6 +10413,7 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -11741,6 +10463,7 @@ }, "node_modules/is-number": { "version": "7.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -11952,6 +10675,7 @@ }, "node_modules/isexe": { "version": "2.0.0", + "dev": true, "license": "ISC" }, "node_modules/isomorphic-timers-promises": { @@ -12002,33 +10726,6 @@ "node": ">= 20" } }, - "node_modules/jotai": { - "version": "2.11.0", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=17.0.0", - "react": ">=17.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/jotai-scope": { - "version": "0.7.2", - "license": "MIT", - "peerDependencies": { - "jotai": ">=2.9.2", - "react": ">=17.0.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -12212,13 +10909,6 @@ "node": ">=0.10.0" } }, - "node_modules/kleur": { - "version": "4.1.5", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/known-css-properties": { "version": "0.36.0", "dev": true, @@ -12386,10 +11076,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "license": "MIT" @@ -13354,13 +12040,6 @@ "node": "*" } }, - "node_modules/mri": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" @@ -13370,14 +12049,6 @@ "dev": true, "license": "MIT" }, - "node_modules/multimath": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "glur": "^1.1.2", - "object-assign": "^4.1.1" - } - }, "node_modules/nan": { "version": "2.22.2", "dev": true, @@ -13620,12 +12291,9 @@ "node": ">=4" } }, - "node_modules/non-layered-tidy-tree-layout": { - "version": "2.0.2", - "license": "MIT" - }, "node_modules/normalize-path": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13780,10 +12448,6 @@ "wrappy": "1" } }, - "node_modules/open-color": { - "version": "1.9.1", - "license": "MIT" - }, "node_modules/optimist": { "version": "0.3.7", "license": "MIT/X11", @@ -14076,6 +12740,7 @@ }, "node_modules/path-key": { "version": "3.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14187,10 +12852,6 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, - "node_modules/perfect-freehand": { - "version": "1.2.0", - "license": "MIT" - }, "node_modules/persist": { "version": "0.2.7", "license": "MIT", @@ -14204,17 +12865,6 @@ "node": ">=0.6.0" } }, - "node_modules/pica": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "glur": "^1.1.2", - "inherits": "^2.0.3", - "multimath": "^2.0.0", - "object-assign": "^4.1.1", - "webworkify": "^1.5.0" - } - }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -14309,29 +12959,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/png-chunk-text": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/png-chunks-encode": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "crc-32": "^0.3.0", - "sliced": "^1.0.1" - } - }, - "node_modules/png-chunks-extract": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "crc-32": "^0.3.0" - } - }, - "node_modules/points-on-curve": { - "version": "1.0.1", - "license": "MIT" - }, "node_modules/points-on-path": { "version": "0.2.1", "license": "MIT", @@ -14799,10 +13426,6 @@ } } }, - "node_modules/pwacompat": { - "version": "2.0.17", - "license": "Apache-2.0" - }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -14952,69 +13575,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-remove-scroll": { - "version": "2.7.1", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/readable-stream": { "version": "3.6.2", "dev": true, @@ -15030,6 +13590,7 @@ }, "node_modules/readdirp": { "version": "3.6.0", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -15042,6 +13603,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -15616,20 +14178,6 @@ "rollup": "^4.0.0" } }, - "node_modules/roughjs": { - "version": "4.6.4", - "license": "MIT", - "dependencies": { - "hachure-fill": "^0.5.2", - "path-data-parser": "^0.1.0", - "points-on-curve": "^0.2.0", - "points-on-path": "^0.2.1" - } - }, - "node_modules/roughjs/node_modules/points-on-curve": { - "version": "0.2.0", - "license": "MIT" - }, "node_modules/router": { "version": "2.2.0", "license": "MIT", @@ -15679,16 +14227,6 @@ "tslib": "^2.1.0" } }, - "node_modules/sade": { - "version": "1.8.1", - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/safari-14-idb-fix": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-1.0.6.tgz", @@ -15940,6 +14478,7 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -15950,6 +14489,7 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16083,10 +14623,6 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/sliced": { - "version": "1.0.1", - "license": "MIT" - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -17367,6 +15903,7 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -17466,39 +16003,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tunnel-rat": { - "version": "0.1.2", - "license": "MIT", - "dependencies": { - "zustand": "^4.3.2" - } - }, - "node_modules/tunnel-rat/node_modules/zustand": { - "version": "4.5.7", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, "node_modules/tweetnacl": { "version": "0.14.5", "dev": true, @@ -17941,52 +16445,6 @@ "dev": true, "license": "MIT" }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util": { "version": "0.12.5", "dev": true, @@ -18017,22 +16475,6 @@ "uuid": "dist-node/bin/uuid" } }, - "node_modules/uvu": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/vary": { "version": "1.1.2", "license": "MIT", @@ -18511,10 +16953,6 @@ "node": ">= 8" } }, - "node_modules/web-worker": { - "version": "1.5.0", - "license": "Apache-2.0" - }, "node_modules/webdav": { "version": "5.9.0", "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.9.0.tgz", @@ -18590,12 +17028,9 @@ "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", "license": "Apache-2.0" }, - "node_modules/webworkify": { - "version": "1.5.0", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/package.json b/package.json index a096fccf..e0d3aaab 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,10 @@ "@nextcloud/capabilities": "^1.2.1", "@nextcloud/dialogs": "^6.4.1", "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/excalidraw": "^0.18.0-bdcb957", + "@nextcloud/excalidraw": "file:../../../../../nextcloud-deps-excalidraw/packages/excalidraw", + "@nextcloud/excalidraw-common": "file:../../../../../nextcloud-deps-excalidraw/packages/common", + "@nextcloud/excalidraw-element": "file:../../../../../nextcloud-deps-excalidraw/packages/element", + "@nextcloud/excalidraw-math": "file:../../../../../nextcloud-deps-excalidraw/packages/math", "@nextcloud/files": "^4.0.0", "@nextcloud/initial-state": "^3.0.0", "@nextcloud/l10n": "^3.4.1", @@ -126,4 +129,4 @@ "node": "^24.0.0", "npm": "^11.3.0" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index daa742c9..811568fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,13 +5,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react' +import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import type { FormEvent } from 'react' import { getCurrentUser } from '@nextcloud/auth' -import { translate as t } from '@nextcloud/l10n' +import { translate as t, translatePlural as n } from '@nextcloud/l10n' import { loadState } from '@nextcloud/initial-state' import { Excalidraw as ExcalidrawComponent, useHandleLibrary, Sidebar, isElementLink } from '@nextcloud/excalidraw' -import '@excalidraw/excalidraw/index.css' -import type { LibraryItems } from '@nextcloud/excalidraw/dist/types/excalidraw/types' +import '@nextcloud/excalidraw/index.css' +import type { ExcalidrawImperativeAPI, LibraryItems } from '@nextcloud/excalidraw/dist/types/excalidraw/types' import { useExcalidrawStore } from './stores/useExcalidrawStore' import { useWhiteboardConfigStore } from './stores/useWhiteboardConfigStore' import { useThemeHandling } from './hooks/useThemeHandling' @@ -54,6 +55,7 @@ import { VotingSidebar } from './components/VotingSidebar' import { useVoting } from './hooks/useVoting' import { useContextMenuFilter } from './hooks/useContextMenuFilter' import { useDisableExternalLibraries } from './hooks/useDisableExternalLibraries' +import { showError, showSuccess } from '@nextcloud/dialogs' const Excalidraw = memo(ExcalidrawComponent) @@ -61,6 +63,14 @@ const MemoizedNetworkStatusIndicator = memo(NetworkStatusIndicator) const MemoizedAuthErrorNotification = memo(AuthErrorNotification) const MemoizedExcalidrawMenu = memo(ExcalidrawMenu) +type LoadLibraryForApi = (api: ExcalidrawImperativeAPI) => void +type LibraryTemplateDialogSource = 'library' | 'selection' +const LIBRARY_TEMPLATE_LOADED_STORAGE_KEY = 'whiteboard.libraryTemplateLoaded' + +function formatLibraryItemCount(count: number): string { + return n('whiteboard', '%n library item', '%n library items', count) +} + export interface WhiteboardAppProps { fileId: number fileName: string @@ -98,6 +108,18 @@ export default function App({ setExcalidrawAPI: state.setExcalidrawAPI, resetExcalidrawAPI: state.resetExcalidrawAPI, }))) + const excalidrawAPIRef = useRef(null) + const loadLibraryForApiRef = useRef(() => {}) + const handleExcalidrawAPI = useCallback((api: ExcalidrawImperativeAPI | null) => { + if (api) { + excalidrawAPIRef.current = api + setExcalidrawAPI(api) + loadLibraryForApiRef.current(api) + return + } + excalidrawAPIRef.current = null + resetExcalidrawAPI() + }, [resetExcalidrawAPI, setExcalidrawAPI]) const { setConfig, @@ -130,7 +152,21 @@ export default function App({ const { renderAssistant } = useAssistant() const { renderEmojiPicker } = useEmojiPicker() const { onChange: onChangeSync, onPointerUpdate } = useSync() - const { fetchLibraryItems, updateLibraryItems, isLibraryLoaded, setIsLibraryLoaded } = useLibrary() + const { + fetchLibraryItems, + mergeInitialLibraryItems, + updateLibraryItems, + saveLibraryTemplate, + isLibraryLoaded, + setIsLibraryLoaded, + } = useLibrary() + const [libraryTemplateDialogItems, setLibraryTemplateDialogItems] = useState(null) + const [libraryTemplateDialogSource, setLibraryTemplateDialogSource] = useState('library') + const [libraryTemplateName, setLibraryTemplateName] = useState('') + const [libraryTemplateError, setLibraryTemplateError] = useState(null) + const [isSavingLibraryTemplate, setIsSavingLibraryTemplate] = useState(false) + const libraryTemplateNameInputRef = useRef(null) + const notifiedLibraryTemplateFileIdRef = useRef(null) useCollaboration() const { isReadOnly, refreshReadOnlyState } = useReadOnlyState() @@ -285,10 +321,67 @@ export default function App({ }, [handleExternalRestore, normalizedFileId]) // Use the board data manager hook - const { saveOnUnmount, isLoading } = useBoardDataManager() + const { saveOnUnmount, isLoading, getInitialLibraryItems, getInitialLibraryItemsPresent } = useBoardDataManager() + + loadLibraryForApiRef.current = (api: ExcalidrawImperativeAPI) => { + if (isLoading) { + return + } + + window.name = fileName + const loadLibraryItems = async () => { + try { + const initialLibraryItems = getInitialLibraryItems() + const hasInitialLibraryItems = getInitialLibraryItemsPresent() && initialLibraryItems.length > 0 + const libraryItems = await fetchLibraryItems() + const mergedLibraryItems = mergeInitialLibraryItems( + libraryItems || [], + initialLibraryItems, + hasInitialLibraryItems, + ) + await api.updateLibrary({ + libraryItems: mergedLibraryItems, + merge: false, + }) + if (hasInitialLibraryItems && !isVersionPreview) { + const notificationKey = `${LIBRARY_TEMPLATE_LOADED_STORAGE_KEY}.${normalizedFileId}` + let alreadyNotified = notifiedLibraryTemplateFileIdRef.current === normalizedFileId + try { + alreadyNotified = alreadyNotified || window.localStorage.getItem(notificationKey) === '1' + } catch { + // Ignore blocked storage. The notification is best-effort UI polish. + } + if (!alreadyNotified) { + api.toggleSidebar({ name: 'default', tab: 'library', force: true }) + showSuccess(t('whiteboard', 'Library template loaded. {items} were added to the Library sidebar.', { + items: formatLibraryItemCount(initialLibraryItems.length), + })) + notifiedLibraryTemplateFileIdRef.current = normalizedFileId + try { + window.localStorage.setItem(notificationKey, '1') + } catch { + // Ignore blocked storage. The in-memory guard still prevents duplicate toasts this load. + } + } + } + setIsLibraryLoaded(true) + } catch (error) { + logger.error('[App] Error updating library items:', error) + } + } + loadLibraryItems() + } + + useEffect(() => { + if (!excalidrawAPI || isLoading || isLibraryLoaded) { + return + } + loadLibraryForApiRef.current(excalidrawAPI) + }, [excalidrawAPI, isLoading, isLibraryLoaded]) // Effect to handle fileId changes - cleanup previous board data useEffect(() => { + setIsLibraryLoaded(false) // Clear any existing Excalidraw data when fileId changes if (excalidrawAPI) { excalidrawAPI.resetScene() @@ -306,41 +399,18 @@ export default function App({ }, [normalizedFileId, excalidrawAPI, resetInitialDataPromise, saveOnUnmount]) useEffect(() => { - resetInitialDataPromise() - - // Fetch library items from the API - window.name = fileName - const fetchLibInterval = setInterval(async () => { - const api = useExcalidrawStore.getState().excalidrawAPI - if (!api) { - logger.warn('[App] Excalidraw API not available, cannot update library') - return - } - clearInterval(fetchLibInterval) - try { - const libraryItems = await fetchLibraryItems() - await api.updateLibrary({ - libraryItems: libraryItems || [], - }) - setIsLibraryLoaded(true) - } catch (error) { - logger.error('[App] Error updating library items:', error) - } - }, 1000) - - // On unmount: Clean up all stores to prevent stale state return () => { - // Save any pending changes before resetting stores saveOnUnmount() - - // Reset all stores resetStore() resetExcalidrawAPI() - - // Terminate the worker terminateWorker() } - }, [resetInitialDataPromise, resetStore, resetExcalidrawAPI, terminateWorker, saveOnUnmount]) + }, [ + resetStore, + resetExcalidrawAPI, + terminateWorker, + saveOnUnmount, + ]) const [activeCommentThreadId, setActiveCommentThreadId] = useState(null) const [commentSidebarDocked, setCommentSidebarDocked] = useState(false) @@ -397,11 +467,98 @@ export default function App({ return } try { - await updateLibraryItems(items) + await updateLibraryItems(items, normalizedFileId, getInitialLibraryItemsPresent()) } catch (error) { logger.error('[App] Error syncing library items:', error) } - }, [isLibraryLoaded]) + }, [getInitialLibraryItemsPresent, isLibraryLoaded, normalizedFileId, updateLibraryItems]) + + useEffect(() => { + if (!libraryTemplateDialogItems) { + return + } + requestAnimationFrame(() => libraryTemplateNameInputRef.current?.focus()) + }, [libraryTemplateDialogItems]) + + const onLibrarySaveAsTemplate = useCallback((items: LibraryItems, context?: { source?: LibraryTemplateDialogSource }) => { + if (items.length === 0) { + showError(t('whiteboard', 'Add items to your library before saving a library template')) + return + } + + setLibraryTemplateDialogSource(context?.source === 'selection' ? 'selection' : 'library') + setLibraryTemplateName('') + setLibraryTemplateError(null) + setLibraryTemplateDialogItems(items) + }, []) + + const closeLibraryTemplateDialog = useCallback(() => { + if (isSavingLibraryTemplate) { + return + } + setLibraryTemplateDialogItems(null) + setLibraryTemplateDialogSource('library') + setLibraryTemplateName('') + setLibraryTemplateError(null) + }, [isSavingLibraryTemplate]) + + const submitLibraryTemplateDialog = useCallback(async (event?: FormEvent) => { + event?.preventDefault() + if (!libraryTemplateDialogItems || isSavingLibraryTemplate) { + return + } + + const templateName = libraryTemplateName.trim() + if (!templateName) { + setLibraryTemplateError(t('whiteboard', 'Library template name is required')) + return + } + + setIsSavingLibraryTemplate(true) + setLibraryTemplateError(null) + try { + await saveLibraryTemplate(templateName, libraryTemplateDialogItems) + const libraryItems = await fetchLibraryItems() + await (excalidrawAPI ?? excalidrawAPIRef.current)?.updateLibrary({ + libraryItems: mergeInitialLibraryItems( + libraryItems || [], + getInitialLibraryItems(), + getInitialLibraryItemsPresent(), + ), + merge: false, + }) + setLibraryTemplateDialogItems(null) + setLibraryTemplateDialogSource('library') + setLibraryTemplateName('') + showSuccess(t('whiteboard', 'Saved "{name}" as a library template with {items}.', { + name: templateName, + items: formatLibraryItemCount(libraryTemplateDialogItems.length), + })) + } catch (error: any) { + if (error?.status === 409) { + const conflictMessage = t('whiteboard', 'A library template with this name or the same items already exists') + setLibraryTemplateError(conflictMessage) + showError(conflictMessage) + return + } + logger.error('[App] Error saving library template:', error) + const errorMessage = t('whiteboard', 'Could not save library template') + setLibraryTemplateError(errorMessage) + showError(errorMessage) + } finally { + setIsSavingLibraryTemplate(false) + } + }, [ + excalidrawAPI, + fetchLibraryItems, + getInitialLibraryItems, + getInitialLibraryItemsPresent, + isSavingLibraryTemplate, + libraryTemplateDialogItems, + libraryTemplateName, + mergeInitialLibraryItems, + saveLibraryTemplate, + ]) const libraryReturnUrl = encodeURIComponent(window.location.href) @@ -524,7 +681,7 @@ export default function App({ validateEmbeddable={() => true} renderEmbeddable={Embeddable} beforeElementCreated={beforeElementCreated} - excalidrawAPI={setExcalidrawAPI} + onExcalidrawAPI={handleExcalidrawAPI} initialData={initialDataPromise} generateIdForFile={generateIdForFile} onPointerUpdate={onPointerUpdate} @@ -539,6 +696,7 @@ export default function App({ }} onLinkOpen={onLinkOpen} onLibraryChange={onLibraryChange} + onLibrarySaveAsTemplate={onLibrarySaveAsTemplate} langCode={lang} libraryReturnUrl={libraryReturnUrl} > @@ -616,6 +774,68 @@ export default function App({ settings={creatorDisplaySettings} /> )} + {libraryTemplateDialogItems && ( +
+
{ + if (event.key === 'Escape') { + event.stopPropagation() + closeLibraryTemplateDialog() + } + }} + > +

+ {libraryTemplateDialogSource === 'selection' + ? t('whiteboard', 'Save selected items as library template') + : t('whiteboard', 'Save library as template')} +

+

+ {t('whiteboard', 'Creates a template for future whiteboards. The canvas is not included.')} +

+

+ {formatLibraryItemCount(libraryTemplateDialogItems.length)} +

+ + setLibraryTemplateName(event.target.value)} + /> + {libraryTemplateError && ( +

+ {libraryTemplateError} +

+ )} +
+ + +
+
+
+ )} ) diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 9828fc68..3404687a 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -49,6 +49,46 @@

+ +

+ {{ t('whiteboard', 'Upload .excalidrawlib files to make reusable library items available when users create a new whiteboard. New boards start with an empty canvas and copied library items. Changes affect only future whiteboards.') }} +

+ + + {{ uploadingGlobalLibraryTemplate ? t('whiteboard', 'Uploading…') : t('whiteboard', 'Upload library template') }} + + + + + {{ t('whiteboard', 'Loading organization library templates…') }} + +

+ {{ t('whiteboard', 'No organization library templates yet. Upload an .excalidrawlib file to let users start new whiteboards with reusable library items.') }} +

+
    +
  • +
    + {{ template.templateName }} + {{ formatLibraryItemCount(template.itemCount) }} +
    + + {{ t('whiteboard', 'Delete') }} + +
  • +
+

diff --git a/src/hooks/useBoardDataManager.ts b/src/hooks/useBoardDataManager.ts index 699c11b6..52f8de25 100644 --- a/src/hooks/useBoardDataManager.ts +++ b/src/hooks/useBoardDataManager.ts @@ -18,10 +18,78 @@ import logger from '../utils/logger' import { computeElementVersionHash, mergeSceneElements } from '../utils/syncSceneData' import { sanitizeAppStateForSync } from '../utils/sanitizeAppState' +const VOLATILE_ELEMENT_KEYS = new Set([ + 'id', + 'seed', + 'version', + 'versionNonce', + 'updated', + 'index', + 'groupIds', + 'frameId', + 'boundElements', + 'containerId', +]) + +function canonicalizeLibraryValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(canonicalizeLibraryValue) + } + if (value && typeof value === 'object') { + const normalized: Record = {} + for (const key of Object.keys(value as Record).sort()) { + if (VOLATILE_ELEMENT_KEYS.has(key)) { + continue + } + normalized[key] = canonicalizeLibraryValue((value as Record)[key]) + } + return normalized + } + return value +} + +function sanitizeLibraryItems(items: unknown): any[] { + if (!Array.isArray(items)) { + return [] + } + + const sanitized: any[] = [] + const seen = new Set() + + for (const item of items) { + if (!item || typeof item !== 'object' || !Array.isArray((item as any).elements) || (item as any).elements.length === 0) { + continue + } + + const cleanItem = { ...(item as any) } + delete cleanItem.templateName + delete cleanItem.scope + delete cleanItem.filename + delete cleanItem.basename + cleanItem.elements = [...cleanItem.elements] + + let key = '' + try { + key = JSON.stringify(canonicalizeLibraryValue(cleanItem.elements)) + } catch { + key = cleanItem.id || '' + } + if (!key || seen.has(key)) { + continue + } + seen.add(key) + sanitized.push(cleanItem) + } + + return sanitized +} + export function useBoardDataManager() { const [isLoading, setIsLoading] = useState(true) const loadingTimeoutsRef = useRef>(new Set()) const currentFileIdRef = useRef(null) + const initialLibraryItemsRef = useRef([]) + const initialLibraryItemsPresentRef = useRef(false) const { fileId, @@ -82,6 +150,8 @@ export function useBoardDataManager() { }, []) const loadBoard = useCallback(async () => { + initialLibraryItemsRef.current = [] + initialLibraryItemsPresentRef.current = false if (isVersionPreview) { try { if (!versionSource) { @@ -139,6 +209,11 @@ export function useBoardDataManager() { ...sanitizedAppState, } + const libraryItemsPresent = Array.isArray(parsedContent.libraryItems) + const libraryItems = sanitizeLibraryItems(parsedContent.libraryItems) + initialLibraryItemsRef.current = libraryItems + initialLibraryItemsPresentRef.current = libraryItemsPresent + resolveInitialData({ elements: parsedContent.elements, files: parsedContent.files || {}, @@ -210,6 +285,7 @@ export function useBoardDataManager() { dataToUse = { elements: reconciledElements, files: mergedFiles, + libraryItems: sanitizeLibraryItems(serverData.libraryItems), appState: mergedAppState, scrollToContent: serverScrollToContent, } @@ -266,6 +342,11 @@ export function useBoardDataManager() { const sanitizedAppState = sanitizeAppStateForSync(dataToUse.appState) const finalAppState = { ...defaultSettings, ...sanitizedAppState } const files = dataToUse.files || {} + const libraryItemsPresent = Object.prototype.hasOwnProperty.call(dataToUse, 'libraryItems') + && Array.isArray(dataToUse.libraryItems) + const libraryItems = sanitizeLibraryItems(dataToUse.libraryItems) + initialLibraryItemsRef.current = libraryItems + initialLibraryItemsPresentRef.current = libraryItemsPresent // Force a small delay to ensure the component is ready to receive the data const timeout = setTimeout(() => { @@ -283,6 +364,8 @@ export function useBoardDataManager() { }, 50) loadingTimeoutsRef.current.add(timeout) } else { + initialLibraryItemsRef.current = [] + initialLibraryItemsPresentRef.current = false // No valid data from either source, use defaults // Force a small delay to ensure the component is ready to receive the data const timeout = setTimeout(() => { @@ -301,6 +384,8 @@ export function useBoardDataManager() { const timeout = setTimeout(() => { // Validate one more time before resolving if (currentFileIdRef.current === fileId) { + initialLibraryItemsRef.current = [] + initialLibraryItemsPresentRef.current = false resolveInitialData(initialDataState) setIsLoading(false) } @@ -396,9 +481,14 @@ export function useBoardDataManager() { } }, [cancelPendingTimeouts]) + const getInitialLibraryItems = useCallback(() => initialLibraryItemsRef.current, []) + const getInitialLibraryItemsPresent = useCallback(() => initialLibraryItemsPresentRef.current, []) + return { isLoading, loadBoard, saveOnUnmount, + getInitialLibraryItems, + getInitialLibraryItemsPresent, } } diff --git a/src/hooks/useLibrary.ts b/src/hooks/useLibrary.ts index 7721c7ab..c3b97990 100644 --- a/src/hooks/useLibrary.ts +++ b/src/hooks/useLibrary.ts @@ -3,15 +3,126 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { useCallback, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { useJWTStore } from '../stores/useJwtStore' import { useShallow } from 'zustand/react/shallow' import { generateUrl } from '@nextcloud/router' -import type { LibraryItem, LibraryItems } from '@excalidraw/excalidraw/types/types' +import type { LibraryItem, LibraryItems } from '@nextcloud/excalidraw/dist/types/excalidraw/types' import logger from '../utils/logger' -type LibraryItemExtended = LibraryItem & { - filename?: string; +type LibraryTemplate = { + templateName: string + items: LibraryItem[] +} + +type LibraryItemContext = { + templateName: string +} + +type LibrarySaveError = Error & { + status?: number +} + +const PERSONAL_TEMPLATE = 'personal' +const BOARD_TEMPLATE = '__board_template__' +const VOLATILE_ELEMENT_KEYS = new Set([ + 'id', + 'seed', + 'version', + 'versionNonce', + 'updated', + 'index', + 'groupIds', + 'frameId', + 'boundElements', + 'containerId', +]) + +function cleanLibraryItem(item: LibraryItem): LibraryItem { + const cleanItem = { ...item } as LibraryItem & Record + delete cleanItem.templateName + delete cleanItem.scope + delete cleanItem.filename + delete cleanItem.basename + return cleanItem +} + +function canonicalizeLibraryValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(canonicalizeLibraryValue) + } + if (value && typeof value === 'object') { + const normalized: Record = {} + for (const key of Object.keys(value as Record).sort()) { + if (VOLATILE_ELEMENT_KEYS.has(key)) { + continue + } + normalized[key] = canonicalizeLibraryValue((value as Record)[key]) + } + return normalized + } + return value +} + +function getLibraryItemContentKey(item: LibraryItem): string { + try { + return JSON.stringify(canonicalizeLibraryValue(item.elements ?? [])) + } catch { + return item.id || '' + } +} + +function dedupeLibraryItems(items: LibraryItems): LibraryItem[] { + const deduped: LibraryItem[] = [] + const seen = new Set() + + for (const item of items) { + const key = getLibraryItemContentKey(item) + if (!key || seen.has(key)) { + continue + } + seen.add(key) + deduped.push(cleanLibraryItem(item)) + } + + return deduped +} + +function normalizeTemplates(responseData: unknown): LibraryTemplate[] { + const templates = (responseData as { data?: { templates?: unknown } })?.data?.templates + if (!Array.isArray(templates)) { + return [] + } + + return templates + .filter((template): template is { templateName: string; scope?: string; items: LibraryItem[] } => { + const candidate = template as { templateName?: unknown; items?: unknown } + return typeof candidate.templateName === 'string' && Array.isArray(candidate.items) + }) + .map(template => ({ + templateName: template.templateName, + items: template.items, + })) +} + +function updateItemContextMap(templates: LibraryTemplate[], itemContexts: Map) { + for (const template of templates) { + for (const item of template.items) { + if (!item.id) { + continue + } + const existing = itemContexts.get(item.id) + if (existing?.templateName === PERSONAL_TEMPLATE && template.templateName !== PERSONAL_TEMPLATE) { + continue + } + if (existing && existing.templateName !== PERSONAL_TEMPLATE && template.templateName !== PERSONAL_TEMPLATE) { + continue + } + itemContexts.set(item.id, { + templateName: template.templateName, + }) + } + } } export function useLibrary() { @@ -22,6 +133,7 @@ export function useLibrary() { ) const [isLibraryLoaded, setIsLibraryLoaded] = useState(false) + const itemContextsRef = useRef>(new Map()) const fetchLibraryItems = useCallback(async (): Promise => { try { @@ -45,60 +157,128 @@ export function useLibrary() { } const data = await response.json() - const libraryItems: LibraryItems = [] + const templates = normalizeTemplates(data) + const personalTemplates = templates.filter(template => template.templateName.toLowerCase() === PERSONAL_TEMPLATE) + const personalItems = dedupeLibraryItems(personalTemplates.flatMap(template => template.items)) + const itemContexts = new Map() + updateItemContextMap([{ + templateName: PERSONAL_TEMPLATE, + items: personalItems, + }], itemContexts) + itemContextsRef.current = itemContexts + + return personalItems + } catch (error) { + logger.error('[Library] Error fetching library:', error) + return null + } + }, [getJWT]) - for (const file of data.data) { - if (!file.library && !file.libraryItems) { + const mergeInitialLibraryItems = useCallback((personalItems: LibraryItems, currentItems: LibraryItems, useBoardLibrary = false): LibraryItems => { + const merged: LibraryItem[] = [] + const seen = new Set() + const nextContexts = new Map() + + if (useBoardLibrary) { + for (const item of dedupeLibraryItems(currentItems)) { + const key = getLibraryItemContentKey(item) + if (!key || seen.has(key)) { continue } - - const date = new Date() - - // Handle for version 1 (legacy library files from https://excalidraw.com) - if (file.library) { - for (const elements of file.library) { - const item: LibraryItemExtended = { - id: '', - created: date.getTime(), - status: 'published', - elements, - filename: file.filename, - } - libraryItems.push(item) - } + seen.add(key) + merged.push(item) + if (item.id) { + nextContexts.set(item.id, { templateName: BOARD_TEMPLATE }) } + } + itemContextsRef.current = nextContexts + return merged + } - // Handle for version 2 - if (file.libraryItems) { - for (const item of file.libraryItems) { - if (!item.elements || item.elements.length === 0) { - continue - } - const libraryItem: LibraryItemExtended = { - id: item.id, - created: item.created || date.getTime(), - status: item.status || 'unpublished', - elements: item.elements, - filename: file.filename, - } - libraryItems.push(libraryItem) - } - } + for (const item of dedupeLibraryItems(personalItems)) { + const key = getLibraryItemContentKey(item) + seen.add(key) + merged.push(item) + if (item.id) { + nextContexts.set(item.id, { templateName: PERSONAL_TEMPLATE }) } - return libraryItems - } catch (error) { - logger.error('[Library] Error fetching library:', error) - return null } - }) - const updateLibraryItems = useCallback(async (items: LibraryItems): Promise => { + for (const item of dedupeLibraryItems(currentItems)) { + const key = getLibraryItemContentKey(item) + if (!key || seen.has(key)) { + continue + } + seen.add(key) + merged.push(item) + if (item.id) { + nextContexts.set(item.id, { templateName: BOARD_TEMPLATE }) + } + } + + itemContextsRef.current = nextContexts + return merged + }, []) + + const updateLibraryItems = useCallback(async (items: LibraryItems, boardFileId?: number | null, useBoardLibrary = false): Promise => { try { const jwt = await getJWT() if (!jwt) { logger.warn('[Library] No JWT found, cannot update library') return } + + if (useBoardLibrary && boardFileId) { + const boardItems = dedupeLibraryItems(items) + const url = generateUrl(`apps/whiteboard/${boardFileId}`) + const response = await globalThis.fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify({ + data: { + libraryItems: boardItems, + }, + }), + }) + + if (!response.ok) { + throw new Error(`Failed to update board library: ${response.statusText}`) + } + + const nextContexts = new Map() + updateItemContextMap([{ + templateName: BOARD_TEMPLATE, + items: boardItems, + }], nextContexts) + itemContextsRef.current = nextContexts + return + } + + const personalItems: LibraryItem[] = [] + const seen = new Set() + for (const item of items) { + const context = item.id ? itemContextsRef.current.get(item.id) : undefined + if (context && context.templateName !== PERSONAL_TEMPLATE) { + continue + } + const cleanItem = cleanLibraryItem(item) + const key = getLibraryItemContentKey(cleanItem) + if (!key || seen.has(key)) { + continue + } + seen.add(key) + personalItems.push(cleanItem) + } + + const templates = [{ + templateName: PERSONAL_TEMPLATE, + items: personalItems, + }] + const url = generateUrl('apps/whiteboard/library') const response = await globalThis.fetch(url, { method: 'PUT', @@ -107,20 +287,59 @@ export function useLibrary() { 'X-Requested-With': 'XMLHttpRequest', Authorization: `Bearer ${jwt}`, }, - body: JSON.stringify({ items }), + body: JSON.stringify({ templates }), }) if (!response.ok) { throw new Error(`Failed to update library: ${response.statusText}`) } + + const nextContexts = new Map( + Array.from(itemContextsRef.current.entries()).filter(([, context]) => context.templateName !== PERSONAL_TEMPLATE), + ) + updateItemContextMap(templates.map(template => ({ + templateName: template.templateName, + items: template.items, + })), nextContexts) + itemContextsRef.current = nextContexts } catch (error) { logger.error('[Library] Error updating library:', error) } - }) + }, [getJWT]) + + const saveLibraryTemplate = useCallback(async (templateName: string, items: LibraryItems): Promise => { + const jwt = await getJWT() + if (!jwt) { + logger.warn('[Library] No JWT found, cannot save library template') + return + } + + const url = generateUrl('apps/whiteboard/library/template') + const response = await globalThis.fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify({ + templateName, + items: items.map(cleanLibraryItem), + }), + }) + + if (!response.ok) { + const error = new Error(`Failed to save library template: ${response.statusText}`) as LibrarySaveError + error.status = response.status + throw error + } + }, [getJWT]) return { fetchLibraryItems, + mergeInitialLibraryItems, updateLibraryItems, + saveLibraryTemplate, isLibraryLoaded, setIsLibraryLoaded, } diff --git a/src/styles/globals/_layout.scss b/src/styles/globals/_layout.scss index 7e305384..2f166706 100644 --- a/src/styles/globals/_layout.scss +++ b/src/styles/globals/_layout.scss @@ -237,6 +237,108 @@ } } +.library-template-dialog__backdrop { + position: absolute; + inset: 0; + z-index: 100021; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + background: rgba(0, 0, 0, 0.35); +} + +.library-template-dialog { + display: flex; + flex-direction: column; + gap: 12px; + width: min(420px, 100%); + padding: 20px; + color: var(--color-main-text); + background: var(--color-main-background); + border-radius: var(--border-radius-large); + box-shadow: 0 4px 18px var(--color-box-shadow); + + h2 { + margin: 0; + font-size: 20px; + font-weight: 700; + line-height: 1.3; + } + + p { + margin: 0; + } + + label { + font-weight: 600; + } + + input { + width: 100%; + min-height: 38px; + padding: 8px 10px; + color: var(--color-main-text); + background: var(--color-main-background); + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + + &:focus-visible { + @include tokens.focus-ring(); + } + } +} + +.library-template-dialog__hint, +.library-template-dialog__count { + color: var(--color-text-maxcontrast); + line-height: 1.4; +} + +.library-template-dialog__error { + margin: 0; + color: var(--color-error); +} + +.library-template-dialog__actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 8px; +} + +.library-template-dialog__button { + min-height: 34px; + padding: 6px 14px; + color: var(--color-main-text); + background: var(--color-background-hover); + border: none; + border-radius: var(--border-radius); + cursor: pointer; + + &:hover { + background: var(--color-background-dark); + } + + &:focus-visible { + @include tokens.focus-ring(); + } + + &:disabled { + cursor: default; + opacity: 0.6; + } +} + +.library-template-dialog__button--primary { + color: var(--color-primary-text); + background: var(--color-primary); + + &:hover { + background: var(--color-primary-element-hover); + } +} + .version-preview-banner { position: absolute; top: calc(var(--default-grid-baseline) * 2); diff --git a/src/utils/sanitizeAppState.ts b/src/utils/sanitizeAppState.ts index a05f8572..9e9252e8 100644 --- a/src/utils/sanitizeAppState.ts +++ b/src/utils/sanitizeAppState.ts @@ -12,6 +12,7 @@ const NON_TRANSFERRED_KEYS: Array = [ 'height', 'offsetTop', 'offsetLeft', + 'searchMatches', ] export function sanitizeAppStateForSync(state: Partial | AppState | null | undefined): Partial { diff --git a/tests/Unit/AppInfo/ApplicationTest.php b/tests/Unit/AppInfo/ApplicationTest.php index be02ba36..008b1f8f 100644 --- a/tests/Unit/AppInfo/ApplicationTest.php +++ b/tests/Unit/AppInfo/ApplicationTest.php @@ -10,10 +10,17 @@ namespace OCA\Whiteboard\AppInfo; +use OCA\Whiteboard\Template\GlobalLibraryTemplateProvider; +use OCP\AppFramework\Bootstrap\IRegistrationContext; + class ApplicationTest extends \Test\TestCase { public function testApp(): void { - $registrationContext = $this->createMock(\OCP\AppFramework\Bootstrap\IRegistrationContext::class); + $registrationContext = $this->createMock(IRegistrationContext::class); + $registrationContext->expects($this->once()) + ->method('registerTemplateProvider') + ->with(GlobalLibraryTemplateProvider::class); + $app = new Application(); $app->register($registrationContext); self::assertTrue(true); diff --git a/tests/Unit/Listener/FileCreatedFromTemplateListenerTest.php b/tests/Unit/Listener/FileCreatedFromTemplateListenerTest.php new file mode 100644 index 00000000..3865e48e --- /dev/null +++ b/tests/Unit/Listener/FileCreatedFromTemplateListenerTest.php @@ -0,0 +1,82 @@ +createMock(File::class); + $template->method('getName')->willReturn('Flowchart.excalidrawlib'); + $template->method('getPath')->willReturn('/appdata/whiteboard/global-libraries/Flowchart.excalidrawlib'); + $template->method('getContent')->willReturn(json_encode([ + 'type' => 'excalidrawlib', + 'version' => 2, + 'libraryItems' => [ + [ + 'id' => 'item-1', + 'status' => 'published', + 'elements' => [ + ['id' => 'element-1', 'type' => 'rectangle'], + ], + ], + ], + ], JSON_THROW_ON_ERROR)); + + $target = $this->createMock(File::class); + $target->method('getName')->willReturn('New whiteboard.whiteboard'); + $target->method('getPath')->willReturn('/admin/files/New whiteboard.whiteboard'); + $target->expects($this->once()) + ->method('putContent') + ->with($this->callback(static function (string $content): bool { + $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); + return $data['elements'] === [] + && $data['files'] === [] + && $data['scrollToContent'] === true + && count($data['libraryItems']) === 1 + && $data['libraryItems'][0]['id'] === 'item-1'; + })); + + $listener = new FileCreatedFromTemplateListener($this->createMock(LoggerInterface::class)); + $listener->handle(new FileCreatedFromTemplateEvent($template, $target, [])); + } + + public function testAcceptsLegacyLibraryFormat(): void { + $template = $this->createMock(File::class); + $template->method('getName')->willReturn('Legacy.excalidrawlib'); + $template->method('getPath')->willReturn('/appdata/whiteboard/global-libraries/Legacy.excalidrawlib'); + $template->method('getContent')->willReturn(json_encode([ + 'type' => 'excalidrawlib', + 'library' => [ + [ + ['id' => 'element-1', 'type' => 'diamond'], + ], + ], + ], JSON_THROW_ON_ERROR)); + + $target = $this->createMock(File::class); + $target->method('getName')->willReturn('New whiteboard.whiteboard'); + $target->method('getPath')->willReturn('/admin/files/New whiteboard.whiteboard'); + $target->expects($this->once()) + ->method('putContent') + ->with($this->callback(static function (string $content): bool { + $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); + return count($data['libraryItems']) === 1 + && $data['libraryItems'][0]['status'] === 'published' + && $data['libraryItems'][0]['elements'][0]['type'] === 'diamond'; + })); + + $listener = new FileCreatedFromTemplateListener($this->createMock(LoggerInterface::class)); + $listener->handle(new FileCreatedFromTemplateEvent($template, $target, [])); + } +} diff --git a/vite.config.ts b/vite.config.ts index 459a2af6..a487dfde 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,7 +16,20 @@ const AppConfig = createAppConfig({ }, { config: defineConfig({ resolve: { + dedupe: ['react', 'react-dom'], alias: [ + { + find: 'vite-plugin-node-polyfills/shims/buffer', + replacement: resolve('node_modules/vite-plugin-node-polyfills/shims/buffer/dist/index.js'), + }, + { + find: 'vite-plugin-node-polyfills/shims/global', + replacement: resolve('node_modules/vite-plugin-node-polyfills/shims/global/dist/index.js'), + }, + { + find: 'vite-plugin-node-polyfills/shims/process', + replacement: resolve('node_modules/vite-plugin-node-polyfills/shims/process/dist/index.js'), + }, { find: /^@excalidraw\/element(.*)$/, replacement: '@nextcloud/excalidraw-element$1',