feat: collection move
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -152,7 +152,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
'collection.update' => $this->collectionUpdate($tenantId, $userId, $data),
|
'collection.update' => $this->collectionUpdate($tenantId, $userId, $data),
|
||||||
'collection.delete' => $this->collectionDelete($tenantId, $userId, $data),
|
'collection.delete' => $this->collectionDelete($tenantId, $userId, $data),
|
||||||
'collection.delta' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
'collection.delta' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||||
'collection.move' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
'collection.move' => $this->collectionMove($tenantId, $userId, $data),
|
||||||
|
|
||||||
// Entity operations
|
// Entity operations
|
||||||
'entity.list' => $this->entityList($tenantId, $userId, $data),
|
'entity.list' => $this->entityList($tenantId, $userId, $data),
|
||||||
@@ -581,11 +581,11 @@ class DefaultController extends ControllerAbstract {
|
|||||||
if (!is_string($data['target'])) {
|
if (!is_string($data['target'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGET);
|
throw new InvalidArgumentException(self::ERR_INVALID_TARGET);
|
||||||
}
|
}
|
||||||
if (!isset($data['sources'])) {
|
if (!isset($data['source'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
|
throw new InvalidArgumentException(self::ERR_MISSING_SOURCE);
|
||||||
}
|
}
|
||||||
if (!is_array($data['sources'])) {
|
if (!is_string($data['source'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
|
throw new InvalidArgumentException(self::ERR_INVALID_SOURCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
$target = ResourceIdentifier::fromString($data['target']);
|
$target = ResourceIdentifier::fromString($data['target']);
|
||||||
@@ -593,9 +593,10 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException('Invalid parameter: target must be provider:service:collection');
|
throw new InvalidArgumentException('Invalid parameter: target must be provider:service:collection');
|
||||||
}
|
}
|
||||||
|
|
||||||
$source = ResourceIdentifier::fromArray($data['source']);
|
$source = ResourceIdentifier::fromString($data['source']);
|
||||||
if (!$source instanceof CollectionIdentifier) {
|
if (!$source instanceof CollectionIdentifier) {
|
||||||
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection identifiers');
|
throw new InvalidArgumentException('Invalid parameter: source must be provider:service:collection');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->mailManager->collectionMove($tenantId, $userId, $target, $source);
|
return $this->mailManager->collectionMove($tenantId, $userId, $target, $source);
|
||||||
|
|||||||
@@ -671,6 +671,42 @@ class Manager {
|
|||||||
return $service->collectionDelete($collectionId, $force);
|
return $service->collectionDelete($collectionId, $force);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a specific collection to a new parent collection
|
||||||
|
*
|
||||||
|
* @since 2025.05.01
|
||||||
|
*
|
||||||
|
* @param string $tenantId Tenant identifier
|
||||||
|
* @param string|null $userId User identifier for context
|
||||||
|
* @param CollectionIdentifier $target Target collection identifier (new parent)
|
||||||
|
* @param CollectionIdentifier $source Source collection identifier (collection to move)
|
||||||
|
*
|
||||||
|
* @return CollectionBaseInterface Moved collection
|
||||||
|
*/
|
||||||
|
public function collectionMove(string $tenantId, ?string $userId, CollectionIdentifier $target, CollectionIdentifier $source): CollectionBaseInterface {
|
||||||
|
// validate that source and target are the same provider and service
|
||||||
|
if ($source->provider() !== $target->provider() || $source->service() !== $target->service()) {
|
||||||
|
throw new InvalidArgumentException("Source and target collections must belong to the same provider and service");
|
||||||
|
}
|
||||||
|
// Validate that source and target are not the same
|
||||||
|
if ($source->collection() === $target->collection()) {
|
||||||
|
throw new InvalidArgumentException("Source and target collections are the same");
|
||||||
|
}
|
||||||
|
// retrieve service
|
||||||
|
$service = $this->serviceFetch($tenantId, $userId, $source->provider(), $source->service());
|
||||||
|
|
||||||
|
// Check if service supports collection move
|
||||||
|
if (!($service instanceof ServiceCollectionMutableInterface)) {
|
||||||
|
throw new InvalidArgumentException("Service does not support collection mutations");
|
||||||
|
}
|
||||||
|
if (!$service->capable(ServiceCollectionMutableInterface::CAPABILITY_COLLECTION_MOVE)) {
|
||||||
|
throw new InvalidArgumentException("Service is not capable of moving collections");
|
||||||
|
}
|
||||||
|
|
||||||
|
// move collection
|
||||||
|
return $service->collectionMove($source->collection(), $target->collection());
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Message Operations ====================
|
// ==================== Message Operations ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import type {
|
|||||||
CollectionDeleteResponse,
|
CollectionDeleteResponse,
|
||||||
CollectionDeleteRequest,
|
CollectionDeleteRequest,
|
||||||
CollectionInterface,
|
CollectionInterface,
|
||||||
|
CollectionMoveRequest,
|
||||||
|
CollectionMoveResponse,
|
||||||
} from '../types/collection';
|
} from '../types/collection';
|
||||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||||
import { CollectionObject, CollectionPropertiesObject } from '../models/collection';
|
import { CollectionObject, CollectionPropertiesObject } from '../models/collection';
|
||||||
@@ -132,6 +134,18 @@ export const collectionService = {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a collection to a new target collection
|
||||||
|
*
|
||||||
|
* @param request - move request parameters
|
||||||
|
*
|
||||||
|
* @returns Promise with moved collection object
|
||||||
|
*/
|
||||||
|
async move(request: CollectionMoveRequest): Promise<CollectionObject> {
|
||||||
|
const response = await transceivePost<CollectionMoveRequest, CollectionMoveResponse>('collection.move', request);
|
||||||
|
return createCollectionObject(response);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default collectionService;
|
export default collectionService;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
import { ref, computed, readonly } from 'vue'
|
import { ref, computed, readonly } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { collectionService } from '../services'
|
import { collectionService, entityService } from '../services'
|
||||||
import { CollectionObject, CollectionPropertiesObject } from '../models/collection'
|
import { CollectionObject, CollectionPropertiesObject } from '../models/collection'
|
||||||
import type { SourceSelector, ListFilter, ListSort } from '../types'
|
import type { SourceSelector, ListFilter, ListSort, CollectionIdentifier, CollectionMoveResponse } from '../types'
|
||||||
|
|
||||||
export const useCollectionsStore = defineStore('mailCollectionsStore', () => {
|
export const useCollectionsStore = defineStore('mailCollectionsStore', () => {
|
||||||
const ROOT_IDENTIFIER = '__root__'
|
const ROOT_IDENTIFIER = '__root__'
|
||||||
@@ -425,6 +425,51 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move collections to another target collection.
|
||||||
|
*
|
||||||
|
* Updates local store keys for successfully moved collections when they are
|
||||||
|
* already present in cache.
|
||||||
|
*
|
||||||
|
* @param target - target collection identifier
|
||||||
|
* @param source - source collection identifier
|
||||||
|
*
|
||||||
|
* @returns Promise with move results keyed by source identifier
|
||||||
|
*/
|
||||||
|
async function move(target: CollectionIdentifier, source: CollectionIdentifier): Promise<CollectionObject> {
|
||||||
|
transceiving.value = true
|
||||||
|
try {
|
||||||
|
const response = await collectionService.move({ target, source })
|
||||||
|
|
||||||
|
if (!(response instanceof CollectionObject)) {
|
||||||
|
console.warn('[Mail Manager][Store] - Move failed. Received unexpected response from move operation:', response)
|
||||||
|
throw new Error('Failed to move collection: unexpected response from move operation')
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceCollection = _collections.value[source]
|
||||||
|
|
||||||
|
if (sourceCollection) {
|
||||||
|
deindexCollection(sourceCollection)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete _collections.value[source]
|
||||||
|
|
||||||
|
const movedCollection = response
|
||||||
|
const movedKey = identifierKey(movedCollection.provider, movedCollection.service, movedCollection.identifier)
|
||||||
|
|
||||||
|
_collections.value[movedKey] = movedCollection
|
||||||
|
indexCollection(movedCollection)
|
||||||
|
|
||||||
|
console.debug('[Mail Manager][Store] - Successfully moved collection:', source, ' to ', movedKey)
|
||||||
|
return response
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Mail Manager][Store] - Failed to move collection:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
transceiving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return public API
|
// Return public API
|
||||||
return {
|
return {
|
||||||
// State (readonly)
|
// State (readonly)
|
||||||
@@ -445,5 +490,6 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => {
|
|||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
delete: remove,
|
delete: remove,
|
||||||
|
move,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Collection type definitions
|
* Collection type definitions
|
||||||
*/
|
*/
|
||||||
import type { ListFilter, ListSort, SourceSelector } from './common';
|
import type { CollectionIdentifier, ListFilter, ListSort, SourceSelector } from './common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection information
|
* Collection information
|
||||||
@@ -119,7 +119,6 @@ export interface CollectionDeleteRequest {
|
|||||||
identifier: string | number;
|
identifier: string | number;
|
||||||
options?: {
|
options?: {
|
||||||
force?: boolean; // Whether to force delete even if collection is not empty
|
force?: boolean; // Whether to force delete even if collection is not empty
|
||||||
recursive?: boolean; // Whether to delete child collections/items as well
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,3 +126,13 @@ export interface CollectionDeleteResponse {
|
|||||||
outcome: 'deleted' | 'moved';
|
outcome: 'deleted' | 'moved';
|
||||||
data?: CollectionInterface | null; // If moved, the new location of the collection
|
data?: CollectionInterface | null; // If moved, the new location of the collection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection move
|
||||||
|
*/
|
||||||
|
export interface CollectionMoveRequest {
|
||||||
|
target: CollectionIdentifier;
|
||||||
|
source: CollectionIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollectionMoveResponse extends CollectionInterface {};
|
||||||
|
|||||||
Reference in New Issue
Block a user