From 6b0615811525bc32e03bea98b9d85322f021d543 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 29 May 2026 13:26:26 +0200 Subject: [PATCH 1/7] fix: leftover naming Signed-off-by: Hamza --- lib/Settings/AdminSection.php | 4 ++-- lib/Settings/AdminSettings.php | 2 +- lib/Store/Local/ServicesTemplateStore.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Settings/AdminSection.php b/lib/Settings/AdminSection.php index 924dc14..f99aea5 100644 --- a/lib/Settings/AdminSection.php +++ b/lib/Settings/AdminSection.php @@ -32,7 +32,7 @@ public function __construct(IURLGenerator $urlGenerator, IL10N $l) { * @returns string */ public function getID(): string { - return 'integration-jmapc'; //or a generic id if feasible + return 'integration-davc'; } /** @@ -42,7 +42,7 @@ public function getID(): string { * @return string */ public function getName(): string { - return $this->l->t('JMAP Connector'); + return $this->l->t('DAV Connector'); } /** diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index e1d90ca..53de9f0 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -39,7 +39,7 @@ public function getForm(): TemplateResponse { } public function getSection(): string { - return 'integration-jmapc'; + return 'integration-davc'; } public function getPriority(): int { diff --git a/lib/Store/Local/ServicesTemplateStore.php b/lib/Store/Local/ServicesTemplateStore.php index 06c849c..9105758 100644 --- a/lib/Store/Local/ServicesTemplateStore.php +++ b/lib/Store/Local/ServicesTemplateStore.php @@ -14,7 +14,7 @@ class ServicesTemplateStore { protected IDBConnection $_Store; - protected string $_EntityTable = 'jmapc_service_templates'; + protected string $_EntityTable = 'davc_service_templates'; public function __construct(IDBConnection $store) { $this->_Store = $store; From 0fa5f06d8d789f8697e6b5304354758ba3b9db65 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 29 May 2026 13:37:34 +0200 Subject: [PATCH 2/7] fix: add error handling Signed-off-by: Hamza --- .../UserConfigurationController.php | 9 ++++--- lib/Service/CoreService.php | 24 +++++++++---------- src/views/UserSettings.vue | 6 ++--- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/lib/Controller/UserConfigurationController.php b/lib/Controller/UserConfigurationController.php index c9f8c57..afa3f84 100644 --- a/lib/Controller/UserConfigurationController.php +++ b/lib/Controller/UserConfigurationController.php @@ -29,7 +29,7 @@ public function __construct( private CoreService $CoreService, private HarmonizationService $HarmonizationService, private ServicesService $ServicesService, - private string $userId, + private ?string $userId, ) { parent::__construct($appName, $request); } @@ -74,8 +74,11 @@ public function Connect(array $service): DataResponse { } // execute command try { - $rs = $this->CoreService->connectAccount($this->userId, $service); - return new DataResponse('success'); + $entity = $this->CoreService->connectAccount($this->userId, $service); + if ($entity === null) { + return new DataResponse('Failed to connect account', Http::STATUS_BAD_REQUEST); + } + return new DataResponse($entity); } catch (\Throwable $th) { return new DataResponse($th->getMessage(), Http::STATUS_INTERNAL_SERVER_ERROR); } diff --git a/lib/Service/CoreService.php b/lib/Service/CoreService.php index 29e52fb..45e6890 100644 --- a/lib/Service/CoreService.php +++ b/lib/Service/CoreService.php @@ -111,40 +111,40 @@ public function locateAccount(array $configuration): ?array { * * @return bool */ - public function connectAccount(string $uid, array $configuration, array $options = []): bool { + public function connectAccount(string $uid, array $configuration, array $options = []): ?ServiceEntity { $forceAutoDiscovery = in_array('AUTO_DISCOVERY', $options, true); // validate service configuration if (!empty($configuration['location_host']) && !\OCA\DAVC\Utile\Validator::host($configuration['location_host'])) { - return false; + return null; } if ($configuration['auth'] === Constants::AUTHENTICATION_TYPE_BASIC) { // validate id //if (!\OCA\DAVC\Utile\Validator::username($configuration['bauth_id'])) { - // return false; + // return null; //} // validate secret if (empty($configuration['bauth_secret'])) { - return false; + return null; } } elseif ($configuration['auth'] === Constants::AUTHENTICATION_TYPE_TOKEN) { // validate id if (!\OCA\DAVC\Utile\Validator::username($configuration['oauth_id'])) { - return false; + return null; } // validate secret if (empty($configuration['oauth_access_token'])) { - return false; + return null; } } else { - return false; + return null; } // if host was not provided, or auto-discovery was explicitly requested, attempt to locate it if ($forceAutoDiscovery || empty($configuration['location_host'])) { $configuration = $this->locateAccount($configuration) ?? []; if (empty($configuration['location_host'])) { - return false; + return null; } } @@ -180,12 +180,12 @@ public function connectAccount(string $uid, array $configuration, array $options $info = $remoteStore->discover(); } catch (Throwable $e) { $this->logger->error('Connection failed:', ['app' => 'davc', 'exception' => $e]); - return false; + return null; } // determine if connection was established if ($info['connected'] === false) { - return false; + return null; } if ($info['principalUrl'] !== null) { @@ -201,13 +201,13 @@ public function connectAccount(string $uid, array $configuration, array $options $service->setEnabled(true); $service->setConnected(true); - $this->ServicesService->deposit($uid, $service); + $service = $this->ServicesService->deposit($uid, $service); // TODO: Should this be implemented? // register harmonization task //$this->TaskService->add(\OCA\DAVC\Tasks\HarmonizationLauncher::class, ['uid' => $uid, 'sid' => $service->getId()]); - return true; + return $service; } /** diff --git a/src/views/UserSettings.vue b/src/views/UserSettings.vue index ac5f420..0e1cc1b 100644 --- a/src/views/UserSettings.vue +++ b/src/views/UserSettings.vue @@ -129,11 +129,9 @@ async function connectService(): Promise { } try { const response = await axios.post(uri, data) - if (response.data === 'success') { + if (response.data && response.data.id) { showSuccess('Successfully connected to account') - if (selectedService.value) { - selectedService.value.connected = 1 - } + selectedService.value = response.data as Service serviceList() remoteCollectionsFetch() localCollectionsFetch() From f923df60dea08019ece2207ed91d779062b46980 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 29 May 2026 14:04:54 +0200 Subject: [PATCH 3/7] fix:easy wins code review Signed-off-by: Hamza --- appinfo/info.xml | 4 +-- eslint.config.mjs | 2 +- .../AdminConfigurationController.php | 7 ++--- .../UserConfigurationController.php | 2 +- src/views/UserSettings.vue | 29 +++++++++---------- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index af018a5..2e7029f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -1,11 +1,11 @@ - + integration_davc DAV Connector Connect Nextcloud to a DAV service 0.1.0-dev - agpl + AGPL-3.0-or-later Sebastian Krupinski DAVC diff --git a/eslint.config.mjs b/eslint.config.mjs index 30223ba..014c318 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,7 +15,7 @@ export default [ files: ['**/*.js', '**/*.vue', '**/*.ts'], rules: { // Relax some rules for now. Can be improved later on (baseline). - 'no-console': 'off', + 'no-console': 'warn', '@typescript-eslint/no-unused-vars': 'off', 'vue/multi-word-component-names': 'off', 'jsdoc/require-jsdoc': 'off', diff --git a/lib/Controller/AdminConfigurationController.php b/lib/Controller/AdminConfigurationController.php index a8cdb43..a9a1f1d 100644 --- a/lib/Controller/AdminConfigurationController.php +++ b/lib/Controller/AdminConfigurationController.php @@ -16,12 +16,9 @@ class AdminConfigurationController extends Controller { - /** - * @var ConfigurationService - */ - private $ConfigurationService; + private ConfigurationService $ConfigurationService; - public function __construct($appName, IRequest $request, ConfigurationService $ConfigurationService) { + public function __construct(string $appName, IRequest $request, ConfigurationService $ConfigurationService) { parent::__construct($appName, $request); diff --git a/lib/Controller/UserConfigurationController.php b/lib/Controller/UserConfigurationController.php index afa3f84..d4a198f 100644 --- a/lib/Controller/UserConfigurationController.php +++ b/lib/Controller/UserConfigurationController.php @@ -201,7 +201,7 @@ public function localCollectionsDeposit(int $sid, array $ContactCorrelations, ar } // execute command try { - $rs = $this->CoreService->localCollectionsDeposit($this->userId, $sid, $ContactCorrelations, $EventCorrelations); + $this->CoreService->localCollectionsDeposit($this->userId, $sid, $ContactCorrelations, $EventCorrelations); return $this->localCollectionsFetch($sid); } catch (\Throwable $th) { return new DataResponse($th->getMessage(), Http::STATUS_INTERNAL_SERVER_ERROR); diff --git a/src/views/UserSettings.vue b/src/views/UserSettings.vue index 0e1cc1b..ac68f7c 100644 --- a/src/views/UserSettings.vue +++ b/src/views/UserSettings.vue @@ -130,7 +130,7 @@ async function connectService(): Promise { try { const response = await axios.post(uri, data) if (response.data && response.data.id) { - showSuccess('Successfully connected to account') + showSuccess(t('integration_davc', 'Successfully connected to account')) selectedService.value = response.data as Service serviceList() remoteCollectionsFetch() @@ -149,7 +149,7 @@ async function disconnectService(): Promise { } try { await axios.post(uri, data) - showSuccess('Successfully disconnected from account') + showSuccess(t('integration_davc', 'Successfully disconnected from account')) // Reset state selectedService.value = null // contacts @@ -179,7 +179,7 @@ async function harmonizeService(): Promise { } try { await axios.post(uri, data) - showSuccess('Synchronization Successful') + showSuccess(t('integration_davc', 'Synchronization Successful')) } catch (error: unknown) { showError(t('integration_davc', 'Synchronization Failed') + ': ' + getErrorResponseText(error)) @@ -192,7 +192,7 @@ async function serviceList(): Promise { const response = await axios.get(uri) if (response.data) { configuredServices.value = Object.values(response.data) - showSuccess('Found ' + configuredServices.value.length + ' Configured Services') + showSuccess(t('integration_davc', 'Found {count} Configured Services', { count: configuredServices.value.length })) } } catch (error: unknown) { showError(t('integration_davc', 'Failed to load service list') @@ -215,16 +215,15 @@ async function remoteCollectionsFetch(): Promise { } try { const response = await axios.get(uri, { params }) - console.log('Remote collections response:', response) if (response.data.ContactsSupported) { contactsRemoteSupported.value = response.data.ContactsSupported contactsRemoteCollections.value = response.data.ContactsCollections - showSuccess('Found ' + contactsRemoteCollections.value.length + ' Remote Contacts Collections') + showSuccess(t('integration_davc', 'Found {count} Remote Contacts Collections', { count: contactsRemoteCollections.value.length })) } if (response.data.EventsSupported) { eventsRemoteSupported.value = response.data.EventsSupported eventsRemoteCollections.value = response.data.EventsCollections - showSuccess('Found ' + eventsRemoteCollections.value.length + ' Remote Events Collections') + showSuccess(t('integration_davc', 'Found {count} Remote Events Collections', { count: eventsRemoteCollections.value.length })) } } catch (error: unknown) { showError(t('integration_davc', 'Failed to load remote collections') @@ -241,11 +240,11 @@ async function localCollectionsFetch(): Promise { const response = await axios.get(uri, { params }) if (response.data.ContactCollections) { contactsLocalCollections.value = response.data.ContactCollections - showSuccess('Found ' + contactsLocalCollections.value.length + ' Local Contact Collections') + showSuccess(t('integration_davc', 'Found {count} Local Contact Collections', { count: contactsLocalCollections.value.length })) } if (response.data.EventCollections) { eventsLocalCollections.value = response.data.EventCollections - showSuccess('Found ' + eventsLocalCollections.value.length + ' Local Event Collections') + showSuccess(t('integration_davc', 'Found {count} Local Event Collections', { count: eventsLocalCollections.value.length })) } } catch (error: unknown) { showError(t('integration_davc', 'Failed to load remote collections') @@ -262,7 +261,7 @@ async function localCollectionsDeposit(): Promise { } try { await axios.post(uri, data) - showSuccess('Saved correlations') + showSuccess(t('integration_davc', 'Saved correlations')) localCollectionsFetch() } catch (error: unknown) { showError(t('integration_davc', 'Failed to save correlations') @@ -585,7 +584,7 @@ function establishedEventCorrelationHarmonized(ccid: string | null): number {
- {{ t('integration_ews', 'Secure Transport Verification (SSL Certificate Verification). Should always be ON, unless connecting to a service over a secure internal network') }} + {{ t('integration_davc', 'Secure Transport Verification (SSL Certificate Verification). Should always be ON, unless connecting to a service over a secure internal network') }}
@@ -677,8 +676,8 @@ function establishedEventCorrelationHarmonized(ccid: string | null): number { -
- {{ t('integration_davc', 'No contacts collections where found in the connected account') }} +
+ {{ t('integration_davc', 'No contacts collections were found in the connected account') }}
{{ t('integration_davc', 'Loading contacts collections from the connected account') }} @@ -720,8 +719,8 @@ function establishedEventCorrelationHarmonized(ccid: string | null): number { -
- {{ t('integration_davc', 'No events collections where found in the connected account') }} +
+ {{ t('integration_davc', 'No events collections were found in the connected account') }}
{{ t('integration_davc', 'Loading events collections from the connected account') }} From 4e5b25da91af06f25097a988ee3e4bf5883d8873 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 29 May 2026 14:12:57 +0200 Subject: [PATCH 4/7] fix: DAV provider crashes on contact/event move and PROPFIND MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - createFile/put: normalize stream resource → string before assigning to Entity::$data (?string), fixing TypeError on Sabre MOVE/COPY/chunked PUT - getChild/childExists/getMultipleChildren: strip .vcf/.ics from the URI segment before matching against the uuid column, fixing PROPFIND "Entity not found" after a contact/event is moved into a DAVC collection - Hybrid getACL: use ?: instead of ?? so an empty permissions array also falls back to {DAV:}all, fixing the "Access denied" PROPFIND on calendars/addressbooks whose permissions column was stored as [] Signed-off-by: Hamza --- .../DAV/Calendar/Cached/EventCollection.php | 5 +++++ lib/Providers/DAV/Calendar/Cached/EventEntity.php | 3 +++ .../DAV/Calendar/Hybrid/EventCollection.php | 10 +++++++++- lib/Providers/DAV/Calendar/Hybrid/EventEntity.php | 3 +++ .../DAV/Contacts/Cached/ContactCollection.php | 5 +++++ lib/Providers/DAV/Contacts/Cached/ContactEntity.php | 3 +++ .../DAV/Contacts/Hybrid/ContactCollection.php | 12 +++++++++++- lib/Providers/DAV/Contacts/Hybrid/ContactEntity.php | 3 +++ 8 files changed, 42 insertions(+), 2 deletions(-) diff --git a/lib/Providers/DAV/Calendar/Cached/EventCollection.php b/lib/Providers/DAV/Calendar/Cached/EventCollection.php index d39a03e..b01ed4f 100644 --- a/lib/Providers/DAV/Calendar/Cached/EventCollection.php +++ b/lib/Providers/DAV/Calendar/Cached/EventCollection.php @@ -312,6 +312,8 @@ public function childExists($id): bool { * @return array */ public function getMultipleChildren(array $ids): array { + // remove extension + $ids = array_map(fn ($id) => str_replace('.ics', '', $id), $ids); // construct filter $filter = $this->_store->entityListFilter(); $filter->condition('cid', $this->_collection->getId()); @@ -360,6 +362,9 @@ public function getChild($id): EventEntity|false { * @return string entity signature */ public function createFile($id, $data = null): string { + if (is_resource($data)) { + $data = stream_get_contents($data); + } // remove extension $id = str_replace('.ics', '', $id); // evaluate if data is in UTF8 format and convert if needed diff --git a/lib/Providers/DAV/Calendar/Cached/EventEntity.php b/lib/Providers/DAV/Calendar/Cached/EventEntity.php index d40ea86..0260e24 100644 --- a/lib/Providers/DAV/Calendar/Cached/EventEntity.php +++ b/lib/Providers/DAV/Calendar/Cached/EventEntity.php @@ -78,6 +78,9 @@ public function get() { * @inheritDoc */ public function put($data) { + if (is_resource($data)) { + $data = stream_get_contents($data); + } return $this->_collection->modifyFile($this->_entity, $data); } diff --git a/lib/Providers/DAV/Calendar/Hybrid/EventCollection.php b/lib/Providers/DAV/Calendar/Hybrid/EventCollection.php index 0d41e23..f00648d 100644 --- a/lib/Providers/DAV/Calendar/Hybrid/EventCollection.php +++ b/lib/Providers/DAV/Calendar/Hybrid/EventCollection.php @@ -111,7 +111,7 @@ public function getACL(): array { 'principal' => $this->getOwner(), 'protected' => true ]; - }, $this->collection->permissions ?? ['{DAV:}all']); + }, $this->collection->permissions ?: ['{DAV:}all']); } /** @@ -309,6 +309,8 @@ public function getChildren(): array { * @return bool */ public function childExists($id): bool { + // remove extension + $id = str_replace('.ics', '', $id); // construct filter $listFilter = $this->localService->entityListFilter(); $listFilter->condition('cid', $this->collection->localId); @@ -326,6 +328,8 @@ public function childExists($id): bool { * @return array */ public function getMultipleChildren(array $ids): array { + // remove extension + $ids = array_map(fn ($id) => str_replace('.ics', '', $id), $ids); // construct filter $listFilter = $this->localService->entityListFilter(); $listFilter->condition('cid', $this->collection->localId); @@ -375,6 +379,10 @@ public function getChild($id): EventEntity|false { */ public function createFile($id, $data = null): string { + if (is_resource($data)) { + $data = stream_get_contents($data); + } + $eo = new Entity(); $eo->localCollectionId = $this->collection->localId; $eo->remoteCollectionId = $this->collection->remoteId; diff --git a/lib/Providers/DAV/Calendar/Hybrid/EventEntity.php b/lib/Providers/DAV/Calendar/Hybrid/EventEntity.php index f987d67..10ab81b 100644 --- a/lib/Providers/DAV/Calendar/Hybrid/EventEntity.php +++ b/lib/Providers/DAV/Calendar/Hybrid/EventEntity.php @@ -71,6 +71,9 @@ public function get() { * @inheritDoc */ public function put($data) { + if (is_resource($data)) { + $data = stream_get_contents($data); + } return $this->collection->modifyFile($this->entity, $data); } diff --git a/lib/Providers/DAV/Contacts/Cached/ContactCollection.php b/lib/Providers/DAV/Contacts/Cached/ContactCollection.php index 07a7477..78cbb26 100644 --- a/lib/Providers/DAV/Contacts/Cached/ContactCollection.php +++ b/lib/Providers/DAV/Contacts/Cached/ContactCollection.php @@ -248,6 +248,8 @@ public function childExists($id): bool { * @return array */ public function getMultipleChildren(array $ids): array { + // remove extension + $ids = array_map(fn ($id) => str_replace('.vcf', '', $id), $ids); // construct filter $filter = $this->_store->entityListFilter(); $filter->condition('cid', $this->_collection->getId()); @@ -296,6 +298,9 @@ public function getChild($id): ContactEntity|false { * @return string entity signature */ public function createFile($id, $data = null): string { + if (is_resource($data)) { + $data = stream_get_contents($data); + } // remove extension $id = str_replace('.vcf', '', $id); // evaluate if data is in UTF8 format and convert if needed diff --git a/lib/Providers/DAV/Contacts/Cached/ContactEntity.php b/lib/Providers/DAV/Contacts/Cached/ContactEntity.php index 3253f5c..113c173 100644 --- a/lib/Providers/DAV/Contacts/Cached/ContactEntity.php +++ b/lib/Providers/DAV/Contacts/Cached/ContactEntity.php @@ -78,6 +78,9 @@ public function get() { * @inheritDoc */ public function put($data) { + if (is_resource($data)) { + $data = stream_get_contents($data); + } return $this->_collection->modifyFile($this->_entity, $data); } diff --git a/lib/Providers/DAV/Contacts/Hybrid/ContactCollection.php b/lib/Providers/DAV/Contacts/Hybrid/ContactCollection.php index 1cf0e1a..30c8fb8 100644 --- a/lib/Providers/DAV/Contacts/Hybrid/ContactCollection.php +++ b/lib/Providers/DAV/Contacts/Hybrid/ContactCollection.php @@ -101,7 +101,7 @@ public function getACL(): array { 'principal' => $this->getOwner(), 'protected' => true ]; - }, $this->collection->permissions ?? ['{DAV:}all']); + }, $this->collection->permissions ?: ['{DAV:}all']); } /** @@ -247,6 +247,8 @@ public function getChildren(): array { * @return bool */ public function childExists($id): bool { + // remove extension + $id = str_replace('.vcf', '', $id); // construct filter $listFilter = $this->localService->entityListFilter(); $listFilter->condition('cid', $this->collection->localId); @@ -264,6 +266,8 @@ public function childExists($id): bool { * @return array */ public function getMultipleChildren(array $ids): array { + // remove extension + $ids = array_map(fn ($id) => str_replace('.vcf', '', $id), $ids); // construct filter $listFilter = $this->localService->entityListFilter(); $listFilter->condition('cid', $this->collection->localId); @@ -287,6 +291,8 @@ public function getMultipleChildren(array $ids): array { * @return ContactEntity|false */ public function getChild($id): ContactEntity|false { + // remove extension + $id = str_replace('.vcf', '', $id); // construct filter $listFilter = $this->localService->entityListFilter(); $listFilter->condition('cid', $this->collection->localId); @@ -311,6 +317,10 @@ public function getChild($id): ContactEntity|false { */ public function createFile($id, $data = null): string { + if (is_resource($data)) { + $data = stream_get_contents($data); + } + $eo = new Entity(); $eo->localCollectionId = $this->collection->localId; $eo->remoteCollectionId = $this->collection->remoteId; diff --git a/lib/Providers/DAV/Contacts/Hybrid/ContactEntity.php b/lib/Providers/DAV/Contacts/Hybrid/ContactEntity.php index ced01b5..26b9b0b 100644 --- a/lib/Providers/DAV/Contacts/Hybrid/ContactEntity.php +++ b/lib/Providers/DAV/Contacts/Hybrid/ContactEntity.php @@ -71,6 +71,9 @@ public function get() { * @inheritDoc */ public function put($data) { + if (is_resource($data)) { + $data = stream_get_contents($data); + } return $this->collection->modifyFile($this->entity, $data); } From c8245b4ac9a0805152ec440326fae7cf484a681c Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 29 May 2026 14:29:14 +0200 Subject: [PATCH 5/7] fix: use DAV URI as entity lookup key, not vCard/iCal UID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After Sabre PUT/MOVE, the immediate PROPFIND failed with "Entity not found" because we stored uuid = parsed vCard/iCal UID, while Sabre looks up by the URI segment the client chose. The two are independent per RFC 6352/4791 and can — and in practice do — differ. - Hybrid createFile: strip extension and set $eo->uuid = $id so the URI segment becomes the stored lookup key. - Cached createFile: override the uuid that extractProperties() extracted from the payload with the URI segment $id. - LocalContactsService / LocalEventsService fromEntityModel: honor $so->uuid when set, falling back to vCard/iCal UID extraction so the harmonization path (which leaves $so->uuid null) is unaffected. Signed-off-by: Hamza --- lib/Providers/DAV/Calendar/Cached/EventCollection.php | 2 ++ lib/Providers/DAV/Calendar/Hybrid/EventCollection.php | 3 +++ .../DAV/Contacts/Cached/ContactCollection.php | 2 ++ .../DAV/Contacts/Hybrid/ContactCollection.php | 3 +++ lib/Service/Local/LocalContactsService.php | 10 +++++++--- lib/Service/Local/LocalEventsService.php | 2 +- 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/Providers/DAV/Calendar/Cached/EventCollection.php b/lib/Providers/DAV/Calendar/Cached/EventCollection.php index b01ed4f..223e255 100644 --- a/lib/Providers/DAV/Calendar/Cached/EventCollection.php +++ b/lib/Providers/DAV/Calendar/Cached/EventCollection.php @@ -386,6 +386,8 @@ public function createFile($id, $data = null): string { $entity->setSignature(md5($entity->getData())); // extract additional properties $this->extractProperties($entity, $vObject); + // URI segment is authoritative for lookups + $entity->setUuid($id); // deposit entity to data store $entity = $this->_store->entityCreate($entity); // return state diff --git a/lib/Providers/DAV/Calendar/Hybrid/EventCollection.php b/lib/Providers/DAV/Calendar/Hybrid/EventCollection.php index f00648d..a7340c3 100644 --- a/lib/Providers/DAV/Calendar/Hybrid/EventCollection.php +++ b/lib/Providers/DAV/Calendar/Hybrid/EventCollection.php @@ -382,11 +382,14 @@ public function createFile($id, $data = null): string { if (is_resource($data)) { $data = stream_get_contents($data); } + // remove extension + $id = str_replace('.ics', '', $id); $eo = new Entity(); $eo->localCollectionId = $this->collection->localId; $eo->remoteCollectionId = $this->collection->remoteId; $eo->remoteEntityId = $id; + $eo->uuid = $id; $eo->data = $data; $remoteService = $this->remoteService(); diff --git a/lib/Providers/DAV/Contacts/Cached/ContactCollection.php b/lib/Providers/DAV/Contacts/Cached/ContactCollection.php index 78cbb26..f0db7fa 100644 --- a/lib/Providers/DAV/Contacts/Cached/ContactCollection.php +++ b/lib/Providers/DAV/Contacts/Cached/ContactCollection.php @@ -320,6 +320,8 @@ public function createFile($id, $data = null): string { $entity->setSignature(md5($data)); // extract additional properties $this->extractProperties($entity, $vObject); + // URI segment is authoritative for lookups + $entity->setUuid($id); // deposit entity to data store $entity = $this->_store->entityCreate($entity); // return state diff --git a/lib/Providers/DAV/Contacts/Hybrid/ContactCollection.php b/lib/Providers/DAV/Contacts/Hybrid/ContactCollection.php index 30c8fb8..d625705 100644 --- a/lib/Providers/DAV/Contacts/Hybrid/ContactCollection.php +++ b/lib/Providers/DAV/Contacts/Hybrid/ContactCollection.php @@ -320,11 +320,14 @@ public function createFile($id, $data = null): string { if (is_resource($data)) { $data = stream_get_contents($data); } + // remove extension + $id = str_replace('.vcf', '', $id); $eo = new Entity(); $eo->localCollectionId = $this->collection->localId; $eo->remoteCollectionId = $this->collection->remoteId; $eo->remoteEntityId = $id; + $eo->uuid = $id; $eo->data = $data; $remoteService = $this->remoteService(); diff --git a/lib/Service/Local/LocalContactsService.php b/lib/Service/Local/LocalContactsService.php index a8fe397..64746f1 100644 --- a/lib/Service/Local/LocalContactsService.php +++ b/lib/Service/Local/LocalContactsService.php @@ -330,9 +330,13 @@ public function fromEntityModel(Entity $so, array $additional = []): ContactEnti // construct correlation signature $to->setCesn($signature . $so->remoteSignature); // extract additional values from object - /** @var \Sabre\VObject\VCard $vo */ - $vo = Reader::read($so->data); - $to->setUuid($vo->UID->getValue()); + if (!empty($so->uuid)) { + $to->setUuid($so->uuid); + } else { + /** @var \Sabre\VObject\VCard $vo */ + $vo = Reader::read($so->data); + $to->setUuid($vo->UID->getValue()); + } // override / assign additional values foreach ($additional as $key => $value) { diff --git a/lib/Service/Local/LocalEventsService.php b/lib/Service/Local/LocalEventsService.php index df3af87..e279c64 100644 --- a/lib/Service/Local/LocalEventsService.php +++ b/lib/Service/Local/LocalEventsService.php @@ -357,7 +357,7 @@ public function fromEntityModel(Entity $so, array $additional = []): EventEntity } } - $to->setUuid($vc->UID->getValue()); + $to->setUuid(!empty($so->uuid) ? $so->uuid : $vc->UID->getValue()); $to->setStartson($vc->DTSTART->getDateTime()->getTimestamp()); if ($vc->DTEND) { From 288c510b8e913fbc109d0a92495e8ef4a26c29f9 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 29 May 2026 15:16:55 +0200 Subject: [PATCH 6/7] fix(user-settings): connect flow UX and per-row collection color picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add loading state to the Connect button (disabled + spinner + "Connecting…" label) and guard against re-entry while a connect is in flight. - Await serviceList() after connect and re-point selectedService to the matching entry from the refreshed list so NcSelect's v-model lines up with one of its options. Previously the new account only appeared in the dropdown after a page reload. - Disable the disconnect button when no service is selected. - Replace the single shared, re-randomizing NcColorPicker with a per-row picker bound to each correlation's color via setContactCorrelationColor / setEventCorrelationColor. Add a picker to the contacts row to match the calendars row. - Persist a random color once when a correlation is created (or on first toggle of a legacy correlation without a color) instead of generating a new randomColor() on every render. Stops the icon color from changing on every re-render and lets the color be saved through localCollectionsDeposit. - Drop the unused selectedcolor ref and global color computed. Signed-off-by: Hamza --- src/views/UserSettings.vue | 114 ++++++++++++++++++++++++++----------- 1 file changed, 82 insertions(+), 32 deletions(-) diff --git a/src/views/UserSettings.vue b/src/views/UserSettings.vue index ac68f7c..c94c6d4 100644 --- a/src/views/UserSettings.vue +++ b/src/views/UserSettings.vue @@ -14,6 +14,7 @@ import NcButton from '@nextcloud/vue/components/NcButton' import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' import NcColorPicker from '@nextcloud/vue/components/NcColorPicker' import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import NcPasswordField from '@nextcloud/vue/components/NcPasswordField' import NcSelect from '@nextcloud/vue/components/NcSelect' import NcTextField from '@nextcloud/vue/components/NcTextField' @@ -80,17 +81,7 @@ const eventsLocalCollections = ref([]) // UI State const configureManually = ref(false) -const selectedcolor = ref('') - -// Computed -const color = computed({ - get() { - return selectedcolor.value || randomColor() - }, - set(value: string) { - selectedcolor.value = value - }, -}) +const connecting = ref(false) // Lifecycle onMounted(() => { @@ -123,6 +114,10 @@ function freshService(): void { selectedService.value = { label: 'New Connection' } as Service } async function connectService(): Promise { + if (connecting.value) { + return + } + connecting.value = true const uri = generateUrl('/apps/integration_davc/service/connect') const data = { service: selectedService.value, @@ -131,14 +126,18 @@ async function connectService(): Promise { const response = await axios.post(uri, data) if (response.data && response.data.id) { showSuccess(t('integration_davc', 'Successfully connected to account')) - selectedService.value = response.data as Service - serviceList() - remoteCollectionsFetch() - localCollectionsFetch() + const connected = response.data as Service + await serviceList() + selectedService.value = configuredServices.value.find( + (s) => String(s.id) === String(connected.id), + ) ?? connected + await Promise.all([remoteCollectionsFetch(), localCollectionsFetch()]) } } catch (error: unknown) { showError(t('integration_davc', 'Failed to authenticate with server') + ': ' + getErrorResponseText(error)) + } finally { + connecting.value = false } } @@ -282,10 +281,14 @@ function changeContactCorrelation(rcid: string | null, e: boolean): void { ccid: rCollection.id, label: rCollection.label, enabled: e, + color: randomColor(), }) } } else { lCollection.enabled = e + if (!lCollection.color) { + lCollection.color = randomColor() + } } } @@ -303,10 +306,14 @@ function changeEventCorrelation(rcid: string | null, e: boolean): void { ccid: rCollection.id, label: rCollection.label, enabled: e, + color: randomColor(), }) } } else { eventsLocalCollections.value[lid].enabled = e + if (!eventsLocalCollections.value[lid].color) { + eventsLocalCollections.value[lid].color = randomColor() + } } } @@ -344,25 +351,59 @@ const establishedEventCorrelation = computed(() => { function establishedContactCorrelationColor(ccid: string | null): string { if (!ccid) { - return randomColor() + return '' } const collection = contactsLocalCollections.value.find((i) => String(i.ccid) === String(ccid)) - if (typeof collection !== 'undefined') { - return collection.color || randomColor() - } else { - return randomColor() - } + return collection?.color ?? '' } function establishedEventCorrelationColor(ccid: string | null): string { if (!ccid) { - return randomColor() + return '' } const collection = eventsLocalCollections.value.find((i) => String(i.ccid) === String(ccid)) - if (typeof collection !== 'undefined') { - return collection.color || randomColor() - } else { - return randomColor() + return collection?.color ?? '' +} + +function setContactCorrelationColor(rcid: string | null, value: string): void { + if (!rcid) { + return + } + const lCollection = contactsLocalCollections.value.find((i) => String(i.ccid) === String(rcid)) + if (lCollection) { + lCollection.color = value + return + } + const rCollection = contactsRemoteCollections.value.find((i) => String(i.id) === String(rcid)) + if (rCollection && rCollection.id) { + contactsLocalCollections.value.push({ + id: null, + ccid: rCollection.id, + label: rCollection.label, + enabled: false, + color: value, + }) + } +} + +function setEventCorrelationColor(rcid: string | null, value: string): void { + if (!rcid) { + return + } + const lCollection = eventsLocalCollections.value.find((i) => String(i.ccid) === String(rcid)) + if (lCollection) { + lCollection.color = value + return + } + const rCollection = eventsRemoteCollections.value.find((i) => String(i.id) === String(rcid)) + if (rCollection && rCollection.id) { + eventsLocalCollections.value.push({ + id: null, + ccid: rCollection.id, + label: rCollection.label, + enabled: false, + color: value, + }) } } @@ -409,7 +450,7 @@ function establishedEventCorrelationHarmonized(ccid: string | null): number { :searchable="false" :options="configuredServices" @option:selected="serviceSelect" /> - + @@ -621,11 +662,12 @@ function establishedEventCorrelationHarmonized(ccid: string | null): number {
- + - {{ t('integration_davc', 'Connect') }} + {{ connecting ? t('integration_davc', 'Connecting…') : t('integration_davc', 'Connect') }}
@@ -661,7 +703,12 @@ function establishedEventCorrelationHarmonized(ccid: string | null): number { type="switch" :modelValue="establishedContactCorrelation(ritem.id)" @update:modelValue="changeContactCorrelation(ritem.id, $event)" /> - + + + @@ -702,7 +749,10 @@ function establishedEventCorrelationHarmonized(ccid: string | null): number { type="switch" :modelValue="establishedEventCorrelation(ritem.id)" @update:modelValue="changeEventCorrelation(ritem.id, $event)" /> - +