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 && (
+
+ )}
)
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.') }}
+
+
+
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',