Merge pull request 'feat: collection move' (#16) from feat/collection-move into main
Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
@@ -152,7 +152,7 @@ class DefaultController extends ControllerAbstract {
|
||||
'collection.update' => $this->collectionUpdate($tenantId, $userId, $data),
|
||||
'collection.delete' => $this->collectionDelete($tenantId, $userId, $data),
|
||||
'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.list' => $this->entityList($tenantId, $userId, $data),
|
||||
@@ -581,11 +581,11 @@ class DefaultController extends ControllerAbstract {
|
||||
if (!is_string($data['target'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGET);
|
||||
}
|
||||
if (!isset($data['sources'])) {
|
||||
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
|
||||
if (!isset($data['source'])) {
|
||||
throw new InvalidArgumentException(self::ERR_MISSING_SOURCE);
|
||||
}
|
||||
if (!is_array($data['sources'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
|
||||
if (!is_string($data['source'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_SOURCE);
|
||||
}
|
||||
|
||||
$target = ResourceIdentifier::fromString($data['target']);
|
||||
@@ -593,9 +593,10 @@ class DefaultController extends ControllerAbstract {
|
||||
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) {
|
||||
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);
|
||||
|
||||
@@ -671,6 +671,42 @@ class Manager {
|
||||
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 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,8 @@ import type {
|
||||
CollectionDeleteResponse,
|
||||
CollectionDeleteRequest,
|
||||
CollectionInterface,
|
||||
CollectionMoveRequest,
|
||||
CollectionMoveResponse,
|
||||
} from '../types/collection';
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||
import { CollectionObject, CollectionPropertiesObject } from '../models/collection';
|
||||
@@ -132,6 +134,18 @@ export const collectionService = {
|
||||
|
||||
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;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
import { ref, computed, readonly } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { collectionService } from '../services'
|
||||
import { collectionService, entityService } from '../services'
|
||||
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', () => {
|
||||
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 {
|
||||
// State (readonly)
|
||||
@@ -445,5 +490,6 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => {
|
||||
create,
|
||||
update,
|
||||
delete: remove,
|
||||
move,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Collection type definitions
|
||||
*/
|
||||
import type { ListFilter, ListSort, SourceSelector } from './common';
|
||||
import type { CollectionIdentifier, ListFilter, ListSort, SourceSelector } from './common';
|
||||
|
||||
/**
|
||||
* Collection information
|
||||
@@ -119,7 +119,6 @@ export interface CollectionDeleteRequest {
|
||||
identifier: string | number;
|
||||
options?: {
|
||||
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';
|
||||
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